68自然语言处理底层技术实现及应用--基于深度学习的命名实体识别方法

基于深度学习的命名实体识别方法

命名实体识别介绍

和分词和词性标注一样,命名实体识别 也是自然语言处理里面一项非常基础但又非常重要的技术。命名实体识别(Named Entity Recongition),简称 NER。命名实体识别指的是指从文本中识别出命名性质的称项,为关系抽取等下游任务做铺垫。
狭义上来说,命名实体识别指的是识别出人名、地名和组织机构名这三类命名实体。因为像时间、货币名称等会构成规律明显的实体类型,因此可以直接用正则表达式等方式来进行识别。一个命名实体识别的例子如下:

image.png

当然,在特定的领域中,会相应地定义领域内的各种实体类型。例如化学领域的各种化学物质名称【氯化钠】【二氧化钾】【聚酸铵】等。

命名实体识别方法

同中文分词和词性标注一样,命名实体识别也是一种典型的序列标注任务。命名实体识别可以基于词来进行识别,也可以基于字来进行识别。而在本次实验中,我们主要讲解基于字的识别方法,其类似于中文分词,也是对一个句子中的每个字进行一个标记。

假设现在的任务是识别出文本中的人名、地名、机构名。则可以定义以下规则来对句子进行标记。
B-PER:人名的第一个字。
I-PER:人名的后几个字。
B-ORG:机构名的第一个字。
I-ORG:机构名的后几个字。
B-LOC:地名的第一个字。
I-LOC:地名的后几个字。
O:非实体名称的其他字。

通过定义上面所述的规则,就可以把一个句子转化成为标注的形式。例如下图所示:

image.png

而现在的命名实体识别的任务就是输入一个句子,然后输出该句子每个字相应的标注,然后再通过规则转换得到实体名称。
目前,命名实体识别的方法主要以有监督学习方法为主,在工业应用上亦是如此。例如美国斯坦福大学开发的命名实体识别工具 Stanford NER 的基本模型为条件随机场。当然,条件随机场也是目前最有效的命名实体识别方法之一。
条件随机场在上一个实验中,我们已经进行详细的介绍。因此本节将重点介绍近年来在学术界比较受欢迎的方法: 长短时记忆网络+条件随机场 的方法。

循环神经网络简介

双向长短时记忆网络+条件随机场的方法一般简称 BiLSTM+CRF。其主要由双向长短时记忆网络 (BiLSTM) 和条件随机场 (CRF) 构成。要想了解长短时记忆网络,先从简单的循环神经网络开始讲解。
单向循环神经网络
循环神经网络,简称 RNN (Recurrent Neural Network),主要是用来解决含有上下文关系的输入数据。也就是说,循环神经网络能够提取时间序列每个时刻之间相关的特征。
你可能会问,为什么要提取上下文的特征呢?这其实很简单,假设现在有一个任务:输入一个句子的前大部分的字,预测出一个句子的最后两个字。例如这句话【白宫是美国总统居住的地方,而特朗普是美国总统,所以特朗普居住在__ 】。
对于人来说,很简单答案肯定是【白宫】。现在想一想你是怎么得出这个答案的。按照人类的思考逻辑,答案肯定是和前面的句子相关的。但计算机并没有人类的思考逻辑,因此要定义一套算法来模仿这种逻辑,即答案要依赖于前文的信息。而循环神经网络正是这样的一套算法。

循环神经网络的网络结构图如下图所示:


image.png

image.png

在上式中,f(⋅) 表示激活函数,其作用是过滤掉无用的信息,保留有用的信息。可以选择 Relu 函数、Tanh 函数、Sigmoid 函数等。一般选择 Tanh 函数作为激活函数,其表达式如下:


image.png

为了更好的理解,现在将 Tanh 函数的图像绘制出来。
import numpy as np
from matplotlib import pyplot as plt
%matplotlib inline
x = np.linspace(-10, 10, 100)
y = (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x))
# 这里主要设置坐标轴
ax = plt.gca()                               # 得到图像的Axes对象
ax.spines['right'].set_color('none')         # 将图像右边的轴设为透明
ax.spines['top'].set_color('none')           # 将图像上面的轴设为透明
ax.xaxis.set_ticks_position('bottom')        # 将x轴刻度设在下面的坐标轴上
ax.yaxis.set_ticks_position('left')          # 将y轴刻度设在左边的坐标轴上
ax.spines['bottom'].set_position(('data', 0))  # 将两个坐标轴的位置设在数据点原点
ax.spines['left'].set_position(('data', 0))
# 画出图形
plt.plot(x, y)

从上图可以看到 Tanh 函数的将输入置于 -1 到 1 区间。这可以增加输入与输出的非线性关系,使模型能够拥有更强的非线性拟合能力。

image.png

同理,g(⋅) 表示激活函数。可以选择 Softmax 函数等。
image.png

双向循环神经网络
前面中所讲的网络都是单向的,也就是后面时刻的输出值可以依赖于前面时刻的输入值的历史信息,反过来则不行。也就是前面时刻的输出值不能依赖于后面时刻的输入值。如下图所示:
image.png

image.png

image.png

长短时记忆网络简介

长短时记忆网络,也简称为 LSTM( Long Short Term Memory)。其是简单循环神经网络的一种重要变体。上文讲解的循环神经网络主要存在一个缺点,那就是不能记忆长期的特征。例如当输入的句子太长时,标准的循环神经网络并不能很好的把一些重要的历史信息一直传递下去。例如下图:

image.png

image.png

为了解决这一问题,Hochreiter 等人于上个世纪 90 年代提出了长短时记忆网络。后来经过其他学者的改良并得到广泛的应用。LSTM 每个记忆单元(也称为细胞)的结构如下图所示。
image.png

LSTM 的整体架构与标准的循环神经网络一样。与之不同的是,LSTM 每个单元的内部结构更复杂,其主要是通过定义三个门来实现对长期历史信息的记忆。
现在来逐步解析其内部结构原理。
遗忘门
遗忘门主要是针对上一个节点传来的输入信息进行选择性的忘记。简单来说就是忘记不重要的信息,记住重要的信息。
image.png

image.png

为了便于理解,这里将 Sigmoid 函数图绘制出来。

x = np.linspace(-10, 10, 100)
y = 1/(1+np.exp(-x))
ax = plt.gca()                              # 得到图像的Axes对象
ax.spines['right'].set_color('none')        # 将图像右边的轴设为透明
ax.spines['top'].set_color('none')          # 将图像上面的轴设为透明
ax.xaxis.set_ticks_position('bottom')       # 将x轴刻度设在下面的坐标轴上
ax.yaxis.set_ticks_position('left')         # 将y轴刻度设在左边的坐标轴上
ax.spines['bottom'].set_position(('data', 0))   # 将两个坐标轴的位置设在数据点原点
ax.spines['left'].set_position(('data', 0))
plt.plot(x, y)

image.png

输入门
image.png

image.png

image.png

输出门
image.png

image.png

image.png

以上就是 LSTM 一个单元的内部计算过程。 LSTM 是目前最经典的循环神经网络之一。因为其通过对每个单元内部进行巧妙设计,从而有效避免了信息断流的问题。

双向长短时记忆网络

前面主要讲解了循环神经网络的理论。现在就动手来实现一个 BiLSTM 来进行命名实体识别。本次实验所构建的网络结构如下图所示:


image.png

上图看起来可能稍显复杂,但仔细看其实很简单。在上图中,我们假设输入的句子为【朝鲜最高领导人金正恩】,其对应的标签为【B-ORG I-ORG O O O O O B-PER I-PER I-PER】。
因为神经网络要求输入的是数值向量,所以将每个字都转成对应的向量形式。在这里,我们将每个字用一个长度为1×20 的向量进行表示。
转换成为向量之后就直接送入到 BiLSTM 网络。在 BiLSTM 网络中,设置正向网络和逆向网络的输出为24 。合并两者得到1×48 的向量。然后再乘以一个矩阵形状为48×7 的矩阵得到1×7 的向量。该向量的每个位置负责预测一个标签的值。
你可能会有疑问,为什么最终的输出要设置成为1×7 形式的向量呢?其实这主要是根据我们的数据集标签种类的数量来决定的。在本次实验中,使用的是人民日报上的标注数据集。让我们先下载数据。

!wget -nc "https://labfile.oss.aliyuncs.com/courses/1329/ner_data.txt"

下载完成之后,我们现在就来读取数据集。

f = open("ner_data.txt", "r", encoding='utf8')
data = f.readlines()
data[:10]

可以看到,数据中的每一行都是由一个字和对应的标签,【\n】表示换行符。当一行没有字或对应的标注时,则表示前面的文字构成一句话,类似于句号的意思。我们现在先对其进行预处理。

sentences = []
labels = []
seq_data = []
seq_label = []
for char in data:
    char = char.strip()
    lst = char.split(" ")
    if len(lst) == 2:
        seq_data.append(lst[0])
        seq_label.append(lst[1])
    else:  # 语料中是空行分隔句子
        sent = " ".join(seq_data)
        seq_data.clear()
        sentences.append(sent)
        label = " ".join(seq_label)
        seq_label.clear()
        labels.append(label)
len(sentences), len(labels)

通过处理之后,得到 50658 个句子,现在打印出一个句子和对应的标签。

print(sentences[3])
print(labels[3])

通过上面的预处理,我们得到的每个句子还是字符串的形式,现在将每个字拆分开。

data_X = []
data_y = []
sentence_length = []
for i in range(len(sentences)):
    sentence_length.append(len(sentences[i].split()))
    data_X.append(sentences[i].split())
    data_y.append(labels[i].split())
data_X[0][:10]

为了便于处理,将每个字转换成为一个对应的数字。因此这里先构建出转换的字典。

tag_to_ix = {}           # 汉字转换成为数字的字典
word_to_ix = {"": 0}  # 标签转换成为数字的字典,并且生字给id=0

for sentence in data_X:
    for word in sentence:
        if word not in word_to_ix:
            word_to_ix[word] = len(word_to_ix)

for tags in data_y:
    for tag in tags:
        if tag not in tag_to_ix:
            tag_to_ix[tag] = len(tag_to_ix)

让我们打印出标签转化字典。

tag_to_ix

此时,标签的总类别数为 7 个。因此对应于模型最终输出的长度为 7 的向量。
前面我们只是构建出了每个字对应于数字的一个字典。现在定义将字转换成为数字的函数。需要注意的是,因为每个句子的长度不一样,所以统一设置句子的长度为 100,如果一个句子的长度未达到 100,则在其后面添加 0。

# 这里增加未登录字处理
def word2indx(seq, to_ix):
    idxs = [to_ix[w] if w in to_ix else to_ix[""] for w in seq]
    for i in range(100-len(idxs)):  # 这里做 padding
        idxs.append(0)
    return idxs


def tag2indx(tags, to_ix):
    idxs = [to_ix[t] for t in tags]
    for i in range(100-len(idxs)):  # 这里做 padding
        idxs.append(0)
    return idxs

划分训练集和测试集。

spl = int(len(data_X)*0.8)
train_X_char = data_X[:spl]
train_y_char = data_y[:spl]
train_sentence_length = sentence_length[:spl]

test_X_char = data_X[spl:]
test_y_char = data_y[spl:]
test_sentence_length = sentence_length[spl:]

转换所有数据,并将数值转化成为 NumPy 格式,以便后续的处理。

train_X = [word2indx(sent, word_to_ix) for sent in train_X_char]
train_y = [tag2indx(tag, tag_to_ix) for tag in train_y_char]
test_X = [word2indx(sent, word_to_ix) for sent in test_X_char]
test_y = [tag2indx(tag, tag_to_ix) for tag in test_y_char]
train_X = np.array(train_X)
train_y = np.array(train_y)
test_X = np.array(test_X)
test_y = np.array(test_y)
test_sentence_length = np.array(test_sentence_length)
train_sentence_length = np.array(train_sentence_length)
# 打印一份数据,查看转化结果
train_X[0]

定义模型的一些超参数,这里定义为全局变量。需要注意的是,这里为了便于模型训练,词向量、循环单元等超参数都设置得比较小。如果自己线下跑,可以适当的增大这些值。

import tensorflow as tf
batch_size = 8                   # 每次训练使用的样本数
max_sent_length = 100            # 每个句子 padding 后的长度,
word_vec_length = 20             # 词向量的长度
num_tags = 7                     # 标签总数
cell_unit = 24                   # LSTM 单元数
learning_rate = 0.001            # 学习率
word_length = len(word_to_ix)    # 总词数

按照上面所展示的 BiLSTM 模型图,构建出 BiLSTM 模型。这里我们主要使用 Google 开源的 TensorFlow 深度学习框架来完成。
这里需要注意的是,前文我们讲到 LSTM 每个时刻的输入xt 都是一个向量。而前面我们完成的数据预处理得到的是每个字对应于一个数字。因此要将数字转换成为向量。
首先自定义我们需要的 BiLSTM 层:

class BiLSTM(tf.keras.layers.Layer):    # 通过继承 tf.keras.layers.Layer 类来自定义层
    def __init__(self, cell_unit):
        super().__init__()
        # 正向的 LSTM 层
        fwd_lstm = tf.keras.layers.LSTM(cell_unit, return_sequences = True, go_backwards= False)
        # 反向的 LSTM 层
        bwd_lstm = tf.keras.layers.LSTM(cell_unit, return_sequences = True, go_backwards = True)
        # 使用 tf.keras.layers.Bidirectional 将两个 LSTM 层合并为双向长短时记忆网络
        self.bilstm = tf.keras.layers.Bidirectional(merge_mode = "concat", layer = fwd_lstm, backward_layer = bwd_lstm)

    def call(self, inputs, training):
        outputs = self.bilstm(inputs, training = training)
        
        return outputs

下面就可以定义我们的模型了,包含一个用于向量化的 Embedding 层,一个 BiLSTM 层,以及一个全连接层。

class MyModel(tf.keras.Model):
    def __init__(self, cell_unit, word_length, word_vec_length):
        super().__init__()
        self.embedding_layer = tf.keras.layers.Embedding(word_length, word_vec_length, embeddings_initializer = "uniform")
        self.bilstm_layer = BiLSTM(cell_unit)
        self.dense_layer = tf.keras.layers.Dense(num_tags, activation = tf.nn.softmax)

    def call(self, input_x, training):
        # Embedding 层,输入形状为【batch_size, max_sent_length】,输出形状为【batch_size, max_sent_length, word_vec_length】
        x = self.embedding_layer(input_x)
        # BiLSTM 层,输出形状为【batch_size, max_sent_length, 2 * cell_unit】
        x = self.bilstm_layer(x, training = training)
        # 全连接层,输出形状为【batch_size, max_sent_length, num_tags】
        output = self.dense_layer(x)
        
        return output

下面我们定义一个用于选择数据的函数,它随机选择 batch_size 个数据,用于训练或测试:

def data_choice(X, y, sentence_length, batch_size):
    """
    X, y:分别为句子和对应的标签
    sentence_length:句子的实际长度
    """
    index = np.random.choice(len(X), size=batch_size, replace=True)
    X_rd = X[index]
    y_rd = y[index]
    sentence_length_rd = sentence_length[index]
    
    return X_rd, y_rd, sentence_length_rd
image.png
def bilstm_loss(y_true, y_pred, sent_len):
    """
    y_true:真实的标签,形状为【batch_size,max_sent_length】
    y_pred:模型的输出,形状为【batch_size,max_sent_length,num_tags】
    sent_len:每个句子的真实长度,形状为【batch_size】
    """
    # 交叉熵损失函数
    loss = tf.keras.losses.sparse_categorical_crossentropy(y_true=y_true, y_pred=y_pred)
    # 掩码操作,只保留句子真实长度的部分的损失
    mask = tf.sequence_mask(sent_len, max_sent_length)
    loss = tf.boolean_mask(loss, mask)
    loss = tf.reduce_mean(loss)
    
    return loss

下面定义一个计算准确率的函数,用于评价我们得到的模型。

def calculate_accuracy(y_true, y_pred, sent_len):
    # 掩码操作,只保留句子真实长度的部分的准确率
    mask = tf.sequence_mask(sent_len, max_sent_length)
    y_true = tf.boolean_mask(y_true, mask)
    # 取 y_pred 最大的索引作为标签
    y_pred = tf.argmax(y_pred, -1)
    y_pred = tf.boolean_mask(y_pred, mask)
    # 计算预测值与真实值相等的个数
    correct_labels = tf.equal(y_pred, y_true).numpy().sum()
    # 计算准确率
    accuracy = 100.0 * correct_labels / y_true.shape[0]
    
    return accuracy

现在开始训练模型。

from tqdm.notebook import tqdm

# 实例化模型
model = MyModel(cell_unit=cell_unit, word_length=word_length, word_vec_length=word_vec_length)
# 定义优化器
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
for i in tqdm(range(30)):
    # 每次随机取一个 batch 的数据进行训练
    train_X_rd, train_y_rd, train_sentence_length_rd = data_choice(train_X, train_y, train_sentence_length, batch_size)
    with tf.GradientTape() as tape:
        y_pred = model(train_X_rd, training=True)
        loss = bilstm_loss(y_true=train_y_rd, y_pred=y_pred, sent_len=train_sentence_length_rd)
    grads = tape.gradient(loss, model.variables)
    optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables))
    accuracy1 = calculate_accuracy(y_true=train_y_rd, y_pred=y_pred, sent_len=train_sentence_length_rd)
    
    # 每次随机取一个 batch 的数据进行测试
    test_X_rd, test_y_rd, test_sentence_length_rd = data_choice(test_X, test_y, test_sentence_length, batch_size)
    y_pred_test = model(test_X_rd, training=False)
    accuracy2 = calculate_accuracy(y_true=test_y_rd, y_pred=y_pred_test, sent_len=test_sentence_length_rd)
    
    print("训练集准确率:{},测试集准确率:{} ".format(accuracy1, accuracy2))

从上面的实验结果可以看到,基于 BiLSTM 的方法有着很高的识别准确率。

BiLSTM+CRF 方法

上面我们主要完成了 BiLSTM 模型的构建并进行训练。现在开始讲解 BiLSTM+CRF 的方法。为什么要加上 CRF 呢?
我们都知道 CRF 表示条件随机场,在上一个实验中讲解到,条件随机场的的预测是一个寻找最大路径的问题。也许是给定一个句子,得出的是许多满足要求的标注序列,然后寻找出概率最大的那一条序列。换句话说,就是条件随机场在预测时,会考虑整个句子之间的上下文关系。
现在再来看 BiLSTM。在上面讲解的 BiLSTM 中,最终的输出是每个时刻都对应一个长度为 7 的向量。而向量的每个位置负责预测一个类别标签。每个时刻之间的预测都是独立的。因此,BiLSTM 在预测时没有依赖上下文关系。
所以,为了使模型在预测时也能考虑上下文之间的关系,所以在 BiLSTM 的输出加上一层 CRF。对于这个问题,如果你想了解更多,可以参考这篇 论文
下面我们就来实现一下 BiLSTM+CRF 模型。BiLSTM+CRF 模型与 BiLSTM 差不多,只是在 BiLSTM 的输出加上一层 CRF 而已。而 CRF 在 TensorFlow 中已经实现,只需调用函数即可。
现在先来定义添加 CRF 层后的损失函数。

import tensorflow_addons as tfa

def bilstm_crf_loss(y_true, y_pred, sent_len):
    # 计算 CRF 层的损失函数,直接调用 tensorflow_addons 提供的接口
    log_likelihood, transition_params = tfa.text.crf_log_likelihood(y_pred, train_y_rd, sent_len)
    # 使用维特比算法进行预测。得到预测序列
    viterbi_sequence, viterbi_score = tfa.text.crf_decode(y_pred, transition_params, sent_len)
    loss = tf.reduce_mean(-log_likelihood)
    
    return loss

和前面训练 BiLSTM 模型一样,现在进行训练模型。

model = MyModel(cell_unit=cell_unit, word_length=word_length, word_vec_length=word_vec_length)
optimizer = tf.keras.optimizers.Adam(learning_rate=learning_rate)
for i in tqdm(range(30)):
    # 每次随机取一个 batch 的数据进行训练
    train_X_rd, train_y_rd, train_sentence_length_rd = data_choice(train_X, train_y, train_sentence_length, batch_size)
    with tf.GradientTape() as tape:
        y_pred = model(train_X_rd, training=True)
        loss = bilstm_crf_loss(y_true=train_y_rd, y_pred=y_pred, sent_len=train_sentence_length_rd)
    grads = tape.gradient(loss, model.variables)
    optimizer.apply_gradients(grads_and_vars=zip(grads, model.variables))
    accuracy1 = calculate_accuracy(y_true=train_y_rd, y_pred=y_pred, sent_len=train_sentence_length_rd)
    
    # 每次随机取一个 batch 的数据进行测试
    test_X_rd, test_y_rd, test_sentence_length_rd = data_choice(test_X, test_y, test_sentence_length, batch_size)
    y_pred_test = model(test_X_rd, training=False)
    accuracy2 = calculate_accuracy(y_true=test_y_rd, y_pred=y_pred_test, sent_len=test_sentence_length_rd)
    
    print("训练集准确率:{},测试集准确率:{} ".format(accuracy1, accuracy2))

从实验结果可以看出,BiLSTM+CRF 模型的整体识别准确率与 BiLSTM 模型差不多。差距不是很明显,主要的原因是这里我们只迭代了 30 次。这对于许多深度学习模型来说是远远不够的。如果你感兴趣,可以自己线下跑一遍。
下面打印出,模型的预测结果。

print('模型预测标签:', tf.argmax(y_pred_test, -1)[0])
print('-'*100)
print('模型真实标签:', test_y_rd[0])

从上面的结果可以看到,模型几乎都预测成为了 O。这主要是我们为了节约时间,模型置训练了 30 次。这导致模型还没学习到许多的东西。
那假设我们预测得到了标签,那该怎么转化为提取的实体呢?下面就来解释这一问题。
我们通过模型得到的是数字形式的标签,而前面我们已经定义了将标签转换为数字的字典。而现在要求的是将数字转化为标签,所以定义一个将数字转化为标签的字典。只需将前面所构建的字典反过来即可。

ix_to_tag = dict(zip(tag_to_ix.values(), tag_to_ix.keys()))
ix_to_tag

刚刚只是定义了转换字典,现在定义转化函数。

def indx2tag(sent, to_tags, sent_len):
    tags = [to_tags[ix] for ix in sent]
    # 只需要真实标签部分即可
    tags = tags[:sent_len]
    return tags

定义完字典和转化函数之后,就可以将预测的结果为数字的标注值转换成为标签值。
由于我们前面训练的模型,因参数简单,而且迭代次数过小的原因,不能很准确的预测。这里为了讲解如何根据预测的标注结果来提取命名实体。直接使用真实的标签值来讲解。当然,你也可以在线下对模型进行充分的训练,然后再进行预测。

test_pred = []
for i in range(50):
    test_pred.append(indx2tag(test_y[i], ix_to_tag, test_sentence_length[i]))
test_pred[0]

通过转化之后,得到如上的标签形式。现在使用标签来对句子进行切分。切出命名实体部分,下面定义一个切分函数。

def ner(sentence_tags, sentence_char):
    """
    sentence_tags:预测标注序列
    sentence_char:原中文句子序列
    """
    sentence_tags.append('O')
    per = []
    loc = []
    org = []
    for j in range(len(sentence_tags)-1):
        if sentence_tags[j] == 'B-PER':
            per_s = j
            I_PER = 0
        if sentence_tags[j] == 'I-PER':
            I_PER += 1
            if sentence_tags[j+1] != 'I-PER':
                word = ''.join(sentence_char[per_s:per_s+I_PER+1])
                I_PER = 0
                per.append(word)
        if sentence_tags[j] == 'B-LOC':
            loc_s = j
            I_LOC = 0
        if sentence_tags[j] == 'I-LOC':
            I_LOC += 1
            if sentence_tags[j+1] != 'I-LOC':
                word = ''.join(sentence_char[loc_s:loc_s+I_LOC+1])
                I_LOC = 0
                loc.append(word)
        if sentence_tags[j] == 'B-ORG':
            org_s = j
            I_ORG = 0
            start = 1
        if sentence_tags[j] == 'I-ORG':
            I_ORG += 1
            if sentence_tags[j+1] != 'I-ORG':
                word = ''.join(sentence_char[org_s:org_s+I_ORG+1])
                I_ORG = 0
                org.append(word)
    return per, loc, org

测试上面所定义的实体切分函数。

for i in range(50):
    print(''.join(test_X_char[i]))
    print('-'*100)
    per, loc, org = ner(test_pred[i], test_X_char[i])
    print('人名:{};地名:{};机构名:{}'.format(per, loc, org))
    print('='*100)

从上面的结果可以看到,已经成功提取出命名实体。

实现基于规则的命名实体识别方法

挑战介绍

在上一个实验中,我们讲解了命名实体识别的基本概念,并使用 TensorFlow 深度学习框架实现了基于 BiLSTM+CRF 的方法。深度学习方法虽然识别准确率较高,但是其需要大量标注的数据,而运行时间较长。其实,在实际应用中还是以基于规则的方法为主。而本次挑战的主角就基于规则的命名实体识别方法。

我们先来简单介绍一下,基于规则的命名实体识别方法。假设有下面一段文本。
决定书指出,北京国旺和美科技有限公司应主动司纠正相关违法北行为,并于 2024 年 05 北月 17 日后申请中移出严重违法失信企业名单。如对本决定有异议,可以自公示之日起三十日内向北京市市场监督管理局提出书面申请并提交相关证明材料,要求核实。也可在接到本决定书之日起六十日内向中国国家市场监督管理总局或者北京市人民政府申请行政复议;或者六个月内向北京市海淀区人民法院提起行政诉讼。

我们现在的任务是识别出上面文本的组织机构名,即黑体字。仔细观察可以发现,这些机构名中,开头的字大多都以地名开头,例如:“北京”,“中国”等。结尾大多以一些比较规则的词结尾,例如:“公司”,“局”,“院” 等。

为了便于理解这里只看第一个字和最后一个字。我们现在定义两个字典,分别是地名字典和特征字典,地名字典用于存放常用的第一个字,例如在这个例子中,该字典为【“北”,“中”】,特征字典存放为最后一个字,例如在这个例子中为【“司”,“局”,“府”,“院”】,我们现在定义个规则,如下:
要是一个字在地名字典中则标记为 S。
要是一个字在特征字典中则标记为 E。
其他情况标记为 O。

使用上面规则,现在就可以将一份文本转化为相应的标记,例如【决定书指出,北京国旺和美科技有限公司应主动】被标记为【O O O O O O S O O O O O O O O O O E O O O】。
转化成为标记之后现在就可以,使用正则表达式对标记序列进行提取了。正则表达式是一个很有效的工具。如果你对正则表达式不熟悉,可以去看 《正则表达式必知必会》或是查阅相关的资料。
那怎么提取呢?在正则表达式中,我们可以定义一个模式,例如:'SO*E' 表示匹配两端为 S 和 E ,中间为 O 的任意长度的子字符串,可以为 SOE、SOOE、SOOOOE 等。这有什么用呢,来看下面例子。

import re
# 句子为
sentence= '决定书指出,北京国旺和美科技有限公司应主动'
# 对应的字符串 label 为 
label= 'OOOOOOSOOOOOOOOOOEOOO'
p='SO*E'               #定义的匹配模式
pattern=re.compile(p)  #使用正则表达式
ne_label=re.finditer(pattern,label)# 寻找字符串 label 中所有可能匹配到的子字符串。
                                   # 这里得到的子串应为 ' SOOOOOOOOOOE ' 的一个正则表达式的对象。
for ne in ne_label:
    print(ne.group())# 返回子字符串 ' SOOOOOOOOOOE ' 
    print(ne.start())# 返回子串第一个字符(S)在原字符串 label 中的位置,这里为 6
    print(ne.end())  # 返回子串最后一个字符(E)在原字符串 label 中的位置,这里为 18

那么通过正则表达式就可以得到一个实体在一个句子中开始出现的第一个字以及最后一个字的位置,然后通过切片的方法就可以将实体从句子中抽出。

我们现在再来梳理一下上面所述的基于规则的方法的运行流程:
输入句子、地名字典、特征字典。
转换句子变成对应的标签形式。
使用正则表达式求出实体的位置。
使用切片的方法,将实体从原输入句子中抽出。

以上就是一种简单的基于规则的命名实体识别方法。而现在的挑战就是让你实现一个基于规则的识别方法。

挑战内容

在本次挑战中,你需要在 ~/Code/regu_ner.py 文件中编写一个函数 re_ner,re_ner 函数接受三个参数,分别是要提取的句子、地名字典以及特征字典。然后返回抽取结果,抽取结果用 list 的形式存放。

挑战要求

代码必须写入 ~/Code/regu_ner.py 文件中。
函数名必须是 re_ner 。
测试时请使用 /home/shiyanlou/anaconda3/bin/python 运行 regu_ner.py ,避免出现无相应模块的情况。

你可能感兴趣的:(68自然语言处理底层技术实现及应用--基于深度学习的命名实体识别方法)