命名实体识别 (NER)#

本笔记本来自 AI for Beginners Curriculum

在这个示例中,我们将学习如何在 命名实体识别的标注语料库 数据集上训练 NER 模型。在开始之前,请将 ner_dataset.csv 文件下载到当前目录中。

In [62]:
import pandas as pd
from tensorflow import keras
import numpy as np

准备数据集#

我们将从将数据集读取到一个数据框开始。如果你想了解更多关于使用 Pandas 的内容,可以访问我们的数据科学入门中的数据处理课程

In [3]:
df = pd.read_csv('ner_dataset.csv',encoding='unicode-escape')
df.head()
Sentence # Word POS Tag
0 Sentence: 1 Thousands NNS O
1 NaN of IN O
2 NaN demonstrators NNS O
3 NaN have VBP O
4 NaN marched VBN O

让我们获取唯一标签并创建查找字典,以便我们可以将标签转换为类别编号:

In [4]:
tags = df.Tag.unique()
tags
array(['O', 'B-geo', 'B-gpe', 'B-per', 'I-geo', 'B-org', 'I-org', 'B-tim',
       'B-art', 'I-art', 'I-per', 'I-gpe', 'I-tim', 'B-nat', 'B-eve',
       'I-eve', 'I-nat'], dtype=object)
In [8]:
id2tag = dict(enumerate(tags))
tag2id = { v : k for k,v in id2tag.items() }

id2tag[0]
'O'

现在我们需要对词汇做同样的处理。为简单起见,我们将在不考虑词频的情况下创建词汇表;在实际应用中,您可能需要使用 Keras 向量化工具,并限制单词的数量。

In [14]:
vocab = set(df['Word'].apply(lambda x: x.lower()))
id2word = { i+1 : v for i,v in enumerate(vocab) }
id2word[0] = '<UNK>'
vocab.add('<UNK>')
word2id = { v : k for k,v in id2word.items() }

我们需要创建一个句子数据集用于训练。让我们遍历原始数据集,并将所有单独的句子分离为 X(单词列表)和 Y(标记列表):

In [41]:
X,Y = [],[]
s,t = [],[]
for i,row in df[['Sentence #','Word','Tag']].iterrows():
    if pd.isna(row['Sentence #']):
        s.append(row['Word'])
        t.append(row['Tag'])
    else:
        if len(s)>0:
            X.append(s)
            Y.append(t)
        s,t = [row['Word']],[row['Tag']]
X.append(s)
Y.append(t)
In [93]:
def vectorize(seq):
    return [word2id[x.lower()] for x in seq]

def tagify(seq):
    return [tag2id[x] for x in seq]

Xv = list(map(vectorize,X))
Yv = list(map(tagify,Y))

Xv[0], Yv[0]
([10386,
  23515,
  4134,
  29620,
  7954,
  13583,
  21193,
  12222,
  27322,
  18258,
  5815,
  15880,
  5355,
  25242,
  31327,
  18258,
  27067,
  23515,
  26444,
  14412,
  358,
  26551,
  5011,
  30558],
 [0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0])

为了简单起见,我们将所有句子用0标记填充到最大长度。在现实生活中,我们可能希望使用更聪明的策略,仅在一个小批量内填充序列。

In [51]:
X_data = keras.preprocessing.sequence.pad_sequences(Xv,padding='post')
Y_data = keras.preprocessing.sequence.pad_sequences(Yv,padding='post')

定义标记分类网络#

我们将使用两层双向 LSTM 网络进行标记分类。为了对最后一层 LSTM 的每个输出应用密集分类器,我们将使用 TimeDistributed 构造,它会在每一步将相同的密集层复制到 LSTM 的每个输出:

In [94]:
maxlen = X_data.shape[1]
vocab_size = len(vocab)
num_tags = len(tags)
model = keras.models.Sequential([
    keras.layers.Embedding(vocab_size, 300, input_length=maxlen),
    keras.layers.Bidirectional(keras.layers.LSTM(units=100, activation='tanh', return_sequences=True)),
    keras.layers.Bidirectional(keras.layers.LSTM(units=100, activation='tanh', return_sequences=True)),
    keras.layers.TimeDistributed(keras.layers.Dense(num_tags, activation='softmax'))
])
model.compile(loss='sparse_categorical_crossentropy',optimizer='adam',metrics=['acc'])
model.summary()
Model: "sequential_3"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding_4 (Embedding)     (None, 104, 300)          9545400   
                                                                 
 bidirectional_6 (Bidirectio  (None, 104, 200)         320800    
 nal)                                                            
                                                                 
 bidirectional_7 (Bidirectio  (None, 104, 200)         240800    
 nal)                                                            
                                                                 
 time_distributed_3 (TimeDis  (None, 104, 17)          3417      
 tributed)                                                       
                                                                 
=================================================================
Total params: 10,110,417
Trainable params: 10,110,417
Non-trainable params: 0
_________________________________________________________________

请注意,这里我们为数据集明确指定了 maxlen——如果我们希望网络能够处理可变长度的序列,那么在定义网络时需要更巧妙一些。

现在让我们开始训练模型。为了加快速度,我们只训练一个周期,但你可以尝试训练更长时间。此外,你可能希望将数据集的一部分分离出来作为训练数据集,以观察验证准确率。

In [57]:
model.fit(X_data,Y_data)
1499/1499 [==============================] - 740s 488ms/step - loss: 0.0667 - acc: 0.9841
<keras.callbacks.History at 0x16f0bb2a310>

测试结果#

现在让我们看看实体识别模型在一个示例句子上的表现:

In [91]:
sent = 'John Smith went to Paris to attend a conference in cancer development institute'
words = sent.lower().split()
v = keras.preprocessing.sequence.pad_sequences([[word2id[x] for x in words]],padding='post',maxlen=maxlen)
res = model(v)[0]
In [92]:
r = np.argmax(res.numpy(),axis=1)
for i,w in zip(r,words):
    print(f"{w} -> {id2tag[i]}")
john -> B-per
smith -> I-per
went -> O
to -> O
paris -> B-geo
to -> O
attend -> O
a -> O
conference -> O
in -> O
cancer -> B-org
development -> I-org
institute -> I-org

关键点#

即使是简单的 LSTM 模型在命名实体识别(NER)任务中也能表现出不错的效果。然而,如果想要获得更好的结果,可以考虑使用大型预训练语言模型,例如 BERT。使用 Huggingface Transformers 库训练 BERT 进行 NER 的方法可以在这里找到。


免责声明
本文档使用AI翻译服务 Co-op Translator 进行翻译。尽管我们努力确保翻译的准确性,但请注意,自动翻译可能包含错误或不准确之处。应以原始语言的文档作为权威来源。对于关键信息,建议使用专业人工翻译。我们对因使用此翻译而引起的任何误解或误读不承担责任。