如何和用keras和tensorflow构建企业级NER

如何和用keras和tensorflow构建企业级NER

应用最新的深度学习方法来满足工业的需求


1_hHAsAlD6xZrjTQoOvccl6w.jpeg

图片来源:pexels

几年前,当我在一家初创公司做软件工程实习生的时候,我在一份发布网络应用程序的工作中看到了一个新特性。这个应用程序能够识别和解析简历中的重要信息,比如电子邮件地址、电话号码、学位信息等等。我开始与我们的团队讨论可能的方法,我们决定用python构建一个基于规则的解析器,以解析简历的不同部分。在开发解析器一段时间之后,我们意识到上述实现的答案可能不是基于规则实现的。我们开始用google搜索它是如何实现的,我们遇到了术语自然语言处理(NLP)以及与机器学习相关的更具体的命名实体识别(NER)。

 


图片来源:meenavyas

NER是一种用于识别和分类文本中命名实体的信息提取技术。这些实体可以是预先定义的和通用的,比如位置名称、组织、时间等,或者它们可以非常具体,比如简历中的示例。NER在业务中有各种各样的应用。我认为,当你在写一封电子邮件,你在邮件中提到一个时间或者附加一个文件,gmail会提供设置一个日历通知,或者提醒你附加文件,以防你发送电子邮件时没有附加附件。NER的其他应用包括:从法律、金融和医疗文档中提取重要的命名实体、对新闻提供者的内容进行分类、改进搜索算法等。对于本文的其余部分,我们将简要介绍解决NER问题的不同方法,然后将跳转到实现最先进的NER方法。下面是Suvro对NER的更详细的介绍。

图片来源:pexels

NER方法:

 

  • 经典方法:最通用的“基于规则”。以下是Sentdex提供的一个精彩的小视频链接,该视频使用python中的NLTK包实现NER。
  • 机器学习方法:在这个类别中有两种主要的方法:A:将问题看作多类分类,其中命名实体是我们的标签,因此我们可以应用不同的分类算法。NER问题需要,识别和标记命名实体需要彻底理解句子的上下文和句子中单词标签的序列,这种方法忽略了这一点。B:这一类的另一种方法是条件随机场(CRF)模型。它是一种概率图模型,可用于对序列数据进行建模,如句子中的单词标签。有关用python实现CRF的更多细节和完整实现,请参阅Tobias的sarticle。CRF模型能够按顺序捕获当前和先前标签的特征,但是它不能理解正向标签的上下文;这个缺点加上训练CRF模型所涉及的额外特征工程,使得它不太适合于业界。

 


 

          图片来源:abajournal

  • 深度学习方法

在讨论NER的深度学习方法(最新技术)的细节之前,我们需要分析适当和清晰的度量来评估模型的性能。当在训练神经网络的不同迭代(epochs)期中,通常使用准确性作为度量指标。然而,在NER的情况下,我们可能正在处理重要的金融、医疗或法律文件,这些文件中的命名实体的精确标识决定了模型的成功。换句话说,假阳性和假阴性在NER任务中具有业务成本。因此,我们评估模型的主要指标将是F1评分,因为我们需要在精确度和召回度之间取得平衡。

 

构建高性能深层学习方法的另一个重要策略是理解哪种类型的神经网络最适合处理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', 'O'), ('through', 'O'), ('London', 'B-geo'), ('to', 'O'), ('protest', 'O'), ('the', 'O'), ('war', 'O'), ('in', 'O'), ('Iraq', 'B-geo'), ('and', 'O'), ('demand', 'O'), ('the', 'O'), ('withdrawal', 'O'), ('of', 'O'), ('British', 'B-gpe'), ('troops', 'O'), ('from', 'O'), ('that', 'O'), ('country', 'O'), ('.', 'O')]
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, random_state=2018)

 

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),batch_size=batch_size, epochs=3, verbose=1)

 

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上查看。
 

最后,非常感谢您的提问与评论。

 

参考文献:

  1. https://www.depends-on-the-definition.com/named-entity-recognition-with-residual-lstm-and-elmo/

  2. http://www.wildml.com/2016/08/rnns-in-tensorflow-a-practical-guide-and-undocumented-features/

  3. https://allennlp.org/elmo

  4. https://jalammar.github.io/illustrated-bert/

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值