本文为 AI 研习社编译的技术博客,原标题 : Named Entity Recognition (NER) with keras and tensorflow 作者 | Nasir Safdari 翻译 | 邓普斯•杰弗 校对 | 酱番梨 整理 | 菠萝妹 原文链接: https://towardsdatascience.com/named-entity-recognition-ner-meeting-industrys-requirement-by-applying-state-of-the-art-deep-698d2b3b4ede 注:本文的相关链接请点击文末【阅读原文】进行访问
如何和用keras和tensorflow构建企业级NER
应用最新的深度学习方法来满足工业的需求
图片来源:pexels
几年前,当我在一家初创公司做软件工程实习生的时候,我在一份发布网络应用程序的工作中看到了一个新特性。这个应用程序能够识别和解析简历中的重要信息,比如电子邮件地址、电话号码、学位信息等等。我开始与我们的团队讨论可能的方法,我们决定用python构建一个基于规则的解析器,以解析简历的不同部分。在开发解析器一段时间之后,我们意识到上述实现的答案可能不是基于规则实现的。我们开始用google搜索它是如何实现的,我们遇到了术语自然语言处理(NLP)以及与机器学习相关的更具体的命名实体识别(NER)。
图片来源:meenavyas
NER是一种用于识别和分类文本中命名实体的信息提取技术。这些实体可以是预先定义的和通用的,比如位置名称、组织、时间等,或者它们可以非常具体,比如简历中的示例。NER在业务中有各种各样的应用。我认为,当你在写一封电子邮件,你在邮件中提到一个时间或者附加一个文件,gmail会提供设置一个日历通知,或者提醒你附加文件,以防你发送电子邮件时没有附加附件。NER的其他应用包括:从法律、金融和医疗文档中提取重要的命名实体、对新闻提供者的内容进行分类、改进搜索算法等。对于本文的其余部分,我们将简要介绍解决NER问题的不同方法,然后将跳转到实现最先进的NER方法。下面是Suvro对NER的更详细的介绍。
图片来源:pexels
图片来源:abajournal
构建高性能深层学习方法的另一个重要策略是理解哪种类型的神经网络最适合处理NER问题,因为文本是顺序数据格式。是的,你猜对了……长短期记忆网络(LSTM)。有关LSTM的更多细节见此链接。但并不是任何类型的LSTM都使用NER,我们需要使用双向LSTM,因为使用标准的LSTM进行预测将只考虑文本序列中的“过去”信息。对于NER,由于上下文按顺序覆盖过去和未来标签,因此我们需要同时考虑上文和下文信息。双向LSTM是两个LSTM的组合,一个是从“右到左”向前运行,一个是从“左到右”向后运行。
我们将通过参考实际的研究论文来快速地查看四种最先进结构,然后我们将继续以实现其中精准度最高的方法。
1.双向 LSTM-CRF:
更多细节和实现参考keras。
来自论文(Bidirectional LSTM-CRF Models for Sequence Tagging)
2. 双向 LSTM-CNNs:
更多细节和实现见Keras.
来自文章(Named Entity Recognition with Bidirectional LSTM-CNNs)
3. 双向 LSTM-CNNS-CRF:
来自文章(End-to-end Sequence Labeling via Bi-directional LSTM-CNNs-CRF)
4. ELMo (嵌入式语言模型):
Jay Alammar: https://jalammar.github.io/illustrated-bert/
最近的一篇论文(Deep contextualized word representations)介绍了一种新型的深层上下文化词表示,它模拟了词语使用的复杂特征(例如,句法和语义),以及这些用法如何在语言上下文中变化(即,对多义进行建模)。新方法(ELMo)具有三个重要表示:
1.上下文:每个单词的表达取决于使用它的整个上下文。 2.深度:单词表达结合了深度预训练神经网络的所有层。 3.基于字符:ELMo表示是纯粹基于字符的,允许网络使用形态学线索来形成对在训练中看不到的词汇的鲁棒表示。
ELMo对语言有很好的理解,因为它是在一个庞大的数据集上训练的,ELMo嵌入是在10亿字的基准上训练的。这种训练被称为双向语言模型(biLM),它能够从过去中过去,并按照单词序列(如句子)预测下一个单词。让我们看看如何实现这种方法。我们将使用kaggle的数据集。
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
plt.style.use("ggplot")
data = pd.read_csv("ner_dataset.csv", encoding="latin1")
data = data.drop(['POS'], axis =1)
data = data.fillna(method="ffill")
data.tail(12)
words = set(list(data['Word'].values))
words.add('PADword')
n_words = len(words)
n_words
35179
tags = list(set(data["Tag"].values))
n_tags = len(tags)
n_tags
17
在我们的数据集中,有47958个句子,35179个不同的单词和17个不同的命名实体(标签)。
让我们看一下数据集中语句长度的分布:
class SentenceGetter(object):
def __init__(self, data):
self.n_sent = 1
self.data = data
self.empty = False
agg_func = lambda s: [(w, t) for w, t in zip(s["Word"].values.tolist(),s["Tag"].values.tolist())]
self.grouped = self.data.groupby("Sentence #").apply(agg_func)
self.sentences = [s for s in self.grouped]
def get_next(self):
try:
s = self.grouped["Sentence: {}".format(self.n_sent)]
self.n_sent += 1
return s
except:
return None
这个类负责将每个具有命名实体(标记)的句子转换为元组列表[(单词,命名实体),…]
getter = SentenceGetter(data)
sent = getter.get_next()
print(sent)
[('Thousands', 'O'), ('of', 'O'), ('demonstrators', 'O'), ('have', 'O'), ('marched',
sentences = getter.sentences
print(len(sentences))
47959
largest_sen = max(len(sen) for sen in sentences)
print('biggest sentence has {} words'.format(largest_sen))
biggest sentence has 104 words
所以最长的句子有140个单词,我们可以看到几乎所有的句子都少于60个单词。
这种方法的最大好处之一是我们不需要任何特征工程;我们所需要的只是句子及其标注的单词,其余的工作由ELMo嵌入完成。为了把我们的句子输入LSTM网络,它们都需要具有相同的大小。查看分布图,我们可以将所有句子的长度设置为50,并为空白空间添加一个通用词;这个过程称为填充(50是一个好数字的另一个原因是我的笔记本电脑不能处理更长的句子)。
max_len = 50
X = [[w[0]for w in s] for s in sentences]
new_X = []
for seq in X:
new_seq = []
for i in range(max_len):
try:
new_seq.append(seq[i])
except:
new_seq.append("PADword")
new_X.append(new_seq)
new_X[15]
['Israeli','officials','say','Prime','Minister','Ariel',
'Sharon', 'will','undergo','a', 'medical','procedure','Thursday',
'to','close','a','tiny','hole','in','his','heart','discovered',
'during','treatment', 'for','a', 'minor', 'stroke', 'suffered', 'last', 'month', '.', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword',
'PADword', 'PADword', 'PADword', 'PADword', 'PADword', 'PADword',
'PADword', 'PADword']
并且,对于命名实体的一些应用也是如此,但是这次我们需要将标签映射到数字:
from keras.preprocessing.sequence import pad_sequences
tags2index = {t:i for i,t in enumerate(tags)}
y = [[tags2index[w[1]] for w in s] for s in sentences]
y = pad_sequences(maxlen=max_len, sequences=y, padding="post", value=tags2index["O"])
y[15]
array([4, 7, 7, 0, 1, 1, 1, 7, 7, 7, 7, 7, 9, 7, 7, 7, 7, 7, 7,接下来,我们将数据分割成训练和测试集,然后导入.orflowHub(用于发布、发现和消费机器学习模型的可重用部分的库)来加载ELMo嵌入特性和keras以开始构建网络。
接下来,我们将数据分割成训练和测试集,然后导入tensorflow Hub(用于发布、发现和使用机器学习模型的可重用部分的库)来加载ELMo嵌入特性和keras以开始构建网络。
from sklearn.model_selection import train_test_split
import tensorflow as tf
import tensorflow_hub as hub
from keras import backend as K
X_tr, X_te, y_tr, y_te = train_test_split(new_X, y, test_size=0.1, rando
sess = tf.Session()
K.set_session(sess)
elmo_model = hub.Module("", trainable=True)
sess.run(tf.global_variables_initializer())
sess.run(tf.tables_initializer())
第一次运行以上代码块的会花点时间,因为ELMo将近400 MB。下面,我们使用一个函数将句子转化为ELMo嵌入:
batch_size = 32
def ElmoEmbedding(x):
return elmo_model(inputs={"tokens": tf.squeeze(tf.cast(x, tf.string)),"sequence_len": tf.constant(batch_size*[max_len])
},
signature="tokens",
as_dict=True)["elmo"]
现在让我们开始构建神经网络:
from keras.models import Model, Input
from keras.layers.merge import add
from keras.layers import LSTM, Embedding, Dense, TimeDistributed, Dropout, Bidirectional, Lambda
input_text = Input(shape=(max_len,), dtype=tf.string)
embedding = Lambda(ElmoEmbedding, output_shape=(max_len, 1024))(input_text)
x = Bidirectional(LSTM(units=512, return_sequences=True,
recurrent_dropout=0.2, dropout=0.2))(embedding)
x_rnn = Bidirectional(LSTM(units=512, return_sequences=True,
recurrent_dropout=0.2, dropout=0.2))(x)
x = add([x, x_rnn]) # residual connection to the first biLSTM
out = TimeDistributed(Dense(n_tags, activation="softmax"))(x)
model = Model(input_text, out)
model.compile(optimizer="adam", loss="sparse_categorical_crossentropy", metrics=["accuracy"])
由于我们批尺寸为32,所以给网络输入必须以全部是32的倍数的块为单位:
X_tr, X_val = X_tr[:1213*batch_size], X_tr[-135*batch_size:]
y_tr, y_val = y_tr[:1213*batch_size], y_tr[-135*batch_size:]
y_tr = y_tr.reshape(y_tr.shape[0], y_tr.shape[1], 1)
y_val = y_val.reshape(y_val.shape[0], y_val.shape[1], 1)
history = model.fit(np.array(X_tr), y_tr, validation_data=(np.array(X_val), y_val
Train on 38816 samples, validate on 4320 samples
Epoch 1/3
38816/38816 [==============================] - 834s 21ms/step - loss: 0.0625 - acc: 0.9818 - val_loss: 0.0449 - val_acc: 0.9861
Epoch 2/3
38816/38816 [==============================] - 833s 21ms/step - loss: 0.0405 - acc: 0.9869 - val_loss: 0.0417 - val_acc: 0.9868
Epoch 3/3
38816/38816 [==============================] - 831s 21ms/step - loss: 0.0336 - acc: 0.9886 - val_loss: 0.0406 - val_acc: 0.9873
最初的目标是通过参数调优来达到更高的精度,但我的笔记本电脑不能处理超过3个epoch和大于32的betch或增加测试大小。我正在Geforce GTX 1060上运行keras,花了将近45分钟来训练这3个epoch,如果您有更好的GPU,可以通过改变其中的一些参数进行尝试。
0.9873的验证准确度是一个很好结果,但我们不感兴趣以准确度为度量来评估我们的模型。让我们看看如何获得精确度、召回和F1份数:
from seqeval.metrics import precision_score, recall_score, f1_score, classification_report
X_te = X_te[:149*batch_size]
test_pred = model.predict(np.array(X_te), verbose=1)
4768/4768 [==============================] - 64s 13ms/step
idx2tag = {i: w for w, i in tags2index.items()}
def pred2label(pred):
out = []
for pred_i in pred:
out_i = []
for p in pred_i:
p_i = np.argmax(p)
out_i.append(idx2tag[p_i].replace("PADword", "O"))
out.append(out_i)
return out
def test2label(pred):
out = []
for pred_i in pred:
out_i = []
for p in pred_i:
out_i.append(idx2tag[p].replace("PADword", "O"))
out.append(out_i)
return out
pred_labels = pred2label(test_pred)
test_labels = test2label(y_te[:149*32])
print(classification_report(test_labels, pred_labels))
precision recall f1-score support
org 0.69 0.66 0.68 2061
tim 0.88 0.84 0.86 2148
gpe 0.95 0.93 0.94 1591
per 0.75 0.80 0.77 1677
geo 0.85 0.89 0.87 3720
art 0.23 0.14 0.18 49
eve 0.33 0.33 0.33 33
nat 0.47 0.36 0.41 22
avg / total 0.82 0.82 0.82 11301
F1分数为0.82,是一个突出的成绩。它胜过本节开头提到的其它三种深度学习方法的结果,也很容易被业界所采用。
最后,让我们看一下预测会是什么样?
i = 390
p = model.predict(np.array(X_te[i:i+batch_size]))[0]
p = np.argmax(p, axis=-1)
print("{:15} {:5}: ({})".format("Word", "Pred", "True"))
print("="*30)
for w, true, pred in zip(X_te[i], y_te[i], p):
if w != "__PAD__":
print("{:15}:{:5} ({})".format(w, tags[pred], tags[true]))
Word Pred : (True)
==============================
Citing :O (O)
a :O (O)
draft :O (O)
report :O (O)
from :O (O)
the :O (O)
U.S. :B-org (B-org)
Government :I-org (I-org)
Accountability :I-org (O)
office :O (O)
, :O (O)
The :B-org (B-org)
New :I-org (I-org)
York :I-org (I-org)
Times :I-org (I-org)
said :O (O)
Saturday :B-tim (B-tim)
the :O (O)
losses :O (O)
amount :O (O)
to :O (O)
between :O (O)
1,00,000 :O (O)
and :O (O)
3,00,000 :O (O)
barrels :O (O)
a :O (O)
day :O (O)
of :O (O)
Iraq :B-geo (B-geo)
's :O (O)
declared :O (O)
oil :O (O)
production :O (O)
over :O (O)
the :O (O)
past :B-tim (B-tim)
four :I-tim (I-tim)
years :O (O)
. :O (O)
PADword :O (O)
PADword :O (O)
PADword :O (O)
PADword :O (O)
PADword :O (O)
PADword :O (O)
PADword :O (O)
PADword :O (O)
PADword :O (O)
PADword :O (O)
像往常一样,代码和jupyter笔记本也可以在我的Github上查看。
最后,非常感谢您的提问与评论。
参考文献:
想要继续查看该篇文章相关链接和参考文献?
长按链接点击打开或点击底部【阅读原文】:
https://ai.yanxishe.com/page/TextTranslation/1344