命名实体识别常用算法及工程实现

Posted 小贤算法屋

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了命名实体识别常用算法及工程实现相关的知识,希望对你有一定的参考价值。

This browser does not support music or audio playback. Please play it in Weixin or another browser.
命名实体识别常用算法及工程实现


命名实体识别常用算法及工程实现


命名实体识别常用算法及工程实现

这篇文章将会记录一个作者在很久之前在Github写的一个开源项目:分别使用BiLSTM-CRF和Bert-BiLSTM-CRF这两种传统的模型做命名实体识别(Named Entity Recognition, NER)。

Bidirectional LSTM -CRF models for sequence ta gging》



长短期记忆网络



命名实体识别常用算法及工程实现

项目中我自己画的BiLSTM-CRF示意图以上,这是比较通用的NER模型方案,它由一个Embedding层,一个双向LSTM和一个条件随机场(CRF)组成,Embedding层可以是通过词表在模型里面初始化也可以外接词/句向量,两个LSTM分别从左至右和从右至左的“理解”句子的上下文语义,而CRF用来限定和预测最终合理的序列标签输出。这里先说说LSTM。

长短期记忆网络(Long Short-Term Memory, LSTM)在1997年就被提出解决RNN带来的梯度消失问题,RNN比较基础这里先不写了(给自己挖个坑命名实体识别常用算法及工程实现),这里主要写写LSTM的原理。为了解决梯度问题,LSTM将信息的流动用三个门进行控制,分别是遗忘门、输入门和输出门,分别对应下图的   、   和   ,遗忘门控制上一个单元状态过来的信息的保存与遗忘,输入门决定向本单元写入信息哪部分内容,输出门决定哪些内容信息输出到下一个单元的隐藏状态。

命名实体识别常用算法及工程实现

LSTM的隐层不仅包含隐状态   ,还专门开辟了一个cell来保存过去的“记忆”   ,LSTM希望用   来传递很久以前的信息,达到长距离依赖的目的。所以LSTM隐层的神经元的输入是上一个时刻的隐状态   和记忆   ,输出是当前时刻的隐状态   和希望传递给下一个时刻的记忆   。
我们有一个输入序列   ,希望计算一个隐藏状态   和单元状态   的序列。在   时刻为了控制遗忘和记忆,LSTM的两个门   和   它们都是上一个时刻的隐状态   和当前时刻的输入   经过   函数输出的结果,   控制遗忘哪些记忆:   ,   控制写入哪些新的记忆:   ,其中   即为期望写入的新记忆,它是隐状态   和当前时刻的输入   经过   函数输出的结果。
最终,新时刻   的记忆   就是这两部分的组合。输出门   控制哪些记忆需要输出到下一个隐状态   ,   是隐状态   和当前时刻的输入   经过   函数输出的结果。下图是这个单元内部的的计算公式(两个图片的角标表示有点不一样,理解前请脑补把下角标改成上角标命名实体识别常用算法及工程实现)

命名实体识别常用算法及工程实现

LSTM解决梯度消失最直接的方法就是,遗忘门选择不遗忘,每一时刻遗忘门    都选择记住前一时刻的记忆   ,然后直接传递给下一时刻。那么,所有前   时刻的记忆都会被完整的传递给第   时刻,从而对   时刻的输出产生影响。LSTM依然存在梯度消失或梯度爆炸问题,但是这两种情况得以缓解,且LSTM性能不错,所以把它用在NER任务里面替代RNN也会有这样的好处,通常来说,LSTM在NER任务中的局部特征捕捉会好于别的编码器。就像开头说的,为了在NER任务中让模型能够双向的理解字所处环境的上下文,这个模型就叠加了两个LSTM,我们管它叫双向LSTM(BiLSTM)。


命名实体识别



早期刚引入循环神经网络做NER的时候,LSTM就可以直接拿来做这项任务了,但是直接由BiLSTM进行NER任务会有一些不可避免的问题。所以引入了CRF来加到模型后面。


2.1 条件随机场
假设我们有的数据集里面有两类实体,分别是人名(Person)和机构名(Organization),通过BIO(B-begin,I-inside,O-outside)的序列标注办法我们会就在数据集里面得到5个实体的类别,它们分别是B-Person,I-Person,B- Organiza tion,I- Organiza tion和O。此处,假设   是包含了5个单词的一句话   ,假设这个句子中,   是人名实体,   是机构实体,其它的实体类型都是O。

命名实体识别常用算法及工程实现

如上图所示,   这个字经过BiLSTM层的之后,BiLSTM会给出它这个时刻的输出,对应上述五个实体类别( B-Perso n,I-Person,B- Organiza tion ,I- Organiza tion, O)的输出分布为(1.5,0.9,0.1,0.08,0.05),这些分数会被输入到CRF中,由CRF最后判断类别序列中分数最高的类别就是我们预测的最终结果。
如果只用BiLSTM不接CRF也是可以的,结果如下图所示,最后的结果直接由BiLSTM模型最后输出 logits的最大值决定这个字的类别(红色的数值):

命名实体识别常用算法及工程实现

在上图中的输出中,模型得出的结果是可以得到最后的序列预测结果的 ( B-Perso n,I-Person,B- Organiza tion ,I- Organiza tion, O ),但是不排除下面的图所示的这种情况:

命名实体识别常用算法及工程实现

根据对序列标注的先验条件了解, I- Organiza tion前面需要一个B-OrganizationI-Person也是同理,如果BiLSTM模型给出上述这种情况的输出,序列预测就出错了。但是CRF层可以自动学习到句子的约束条件,确保上述问题基本不会出现,有效保证了序列预测的正确性。
限于篇幅的长度,文章这里不会推导CRF的原理和公式,以后会在基础知识的文章里面去写(继续给自己挖坑)。

2.2 Bert-BiLSTM-CRF

命名实体识别常用算法及工程实现

前面有说到, Embedding层 可以是通过词表在模型里面初始化也可以外接词/句向量,那么将 Embedding层替换为Bert肯定也是可行的,事实上,在非垂直的自然语料下面,直接使用 不经过 fine-tune的Bert得到的句向量后接可训练BiLSTM-CRF上就可以得到很好的效果,仓库中已经实现了这种办法。当然如果使用垂直语料,可以联合Bert进行微调,但是在微调的情况下,直接用Bert-CRF或许也能取得不错的效果。但是请不要盲目的堆砌各类模型,参考《工业界求解NER问题的12条黄金法则》文章内容里面总结的关于NER的七条经验:
  1. 提升NER性能的方法往往不是直接堆砌⼀个Bert+CRF,这样做不仅性能不一定会好,推断速度也非常堪忧;就算直接使用BERT+CRF进行finetune,BERT和CRF层的学习率也不要设成一样,让CRF层学习率要更大一些(一般是BERT的5~10倍),要让CRF层快速学习。

  2. 在NER任务上,也不要试图对BERT进⾏蒸馏压缩,很可能吃⼒不讨好。

  3. NER任务是⼀个重底层的任务,上层模型再深、性能提升往往也是有限的(甚至是下降的);因此,不要盲目搭建很深的网络,也不要痴迷于各种attention了

  4. NER任务不同的解码方式(CRF/指针网络/Biaffine)之间的差异其实也是有限的,不要过分拘泥于解码⽅式。

  5. 通过QA阅读理解的方式进行NER任务,效果也许会提升,但计算复杂度上来了,你需要对同一个文本进行多次编码(对同⼀文本会构造多个question)

  6. 设计NER任务时,尽量不要引入嵌套实体,不好做,这往往是一个长尾问题。

  7. 不要直接拿Transformer做NER,这是不合适的。

虽然接入Bert之后的模型在人民日报上NER任务的F1达到了0.946(前面直接用BiLSTM-CRF的F1是0.867,这里可能有维度的影响,Bert的Embedding向量是768,项目用的BiLSTM-CRF中Embedding的维度配置的300) ,考虑到接预训练模型确实会提高性能,但是也带来的是 几百倍于BiLSTM-CRF的预测速度。鉴于上述七点经验,我们想要更好的精度又需要更快的推断速度做工业界的应用,没必要一定要堆彻模型,可以把中心放到下述工作:
  1. 建立丰富的实体字典,融入更多的规则,在实体数据的丰富度和标注上多下功夫。

  2. 把精力集中在Eembedding层,引入丰富的特征比如char、bigram、词典特征、词性特征等等,还有更多业务相关的特征,在这个文章项目中,只用到了char级别的特征,有兴趣的朋友可以尝试融合以上更多特征。

  3. 我们知道中文NER通常是基于字符进行标注的,这是由于基于词汇标注存在分词误差问题。但词汇边界对于实体边界是很有用的,我们该怎么把蕴藏词汇信息的词向量“恰当”地引入到模型中呢?一种行之有效的方法就是信息无损的、引入词汇信息的NER方法,我们可以做词汇增强。参考《中文NER的正确打开方式: 词汇增强方法总结 (从Lattice LSTM到FLAT)》

工程代码及使用



在项目的仓库中,模型相关代码位于engines/model.py里,值得注意的是,项目并没有把Bert层写在这个文件中,原因有两个,一方面我只是想把Bert作为一种特征增强的手段,只是取Bert对句子的嵌入结果,没有想去对Bert做微调,另外一方面,如果把Bert模型写在里面,那么每次保存checkpoints的时候会非常慢而且保存的文件很大,因为它包含了Bert。
from abc import ABC
import tensorflow as tffrom tensorflow_addons.text.crf import crf_log_likelihood

class BiLSTM_CRFModel(tf.keras.Model, ABC): def __init__(self, configs, vocab_size, num_classes, use_bert=False): super(BiLSTM_CRFModel, self).__init__()        # 是否是使用Bert做Embedding self.use_bert = use_bert        # Embedding层 self.embedding = tf.keras.layers.Embedding(vocab_size, configs.embedding_dim, mask_zero=True)        # LSTM隐藏层维度 self.hidden_dim = configs.hidden_dim        # 神经网络的遗忘率        self.dropout_rate = configs.dropout self.dropout = tf.keras.layers.Dropout(self.dropout_rate)        # 定义双向的LSTM层 self.bilstm = tf.keras.layers.Bidirectional(tf.keras.layers.LSTM(self.hidden_dim, return_sequences=True))        # 定义全连接层 self.dense = tf.keras.layers.Dense(num_classes)        # 定义CRF的转移状态矩阵 self.transition_params = tf.Variable(tf.random.uniform(shape=(num_classes, num_classes)))
@tf.function def call(self, inputs, inputs_length, targets, training=None): if self.use_bert: embedding_inputs = inputs else:            # 如果不使用Bert,则就使用模型自己初始化的Embedding层 embedding_inputs = self.embedding(inputs) dropout_inputs = self.dropout(embedding_inputs, training) bilstm_outputs = self.bilstm(dropout_inputs) logits = self.dense(bilstm_outputs) tensor_targets = tf.convert_to_tensor(targets, dtype=tf.int32)        # 将BiLSTM的结果输入到CRF层,得到每个位置对应的对数似然和转移矩阵 log_likelihood, self.transition_params = crf_log_likelihood( logits, tensor_targets, inputs_length, transition_params=self.transition_params) return logits, log_likelihood, self.transition_params
训练部分的核心代码放在train.py里面,项目把Bert取Embedding的这个过程放到了这里做,值得注意得是,这里梯度下降使用的目标损失函数loss是CRF输出的各时刻的对数似然均值的负数:
for i in range(epoch): start_time = time.time() logger.info('epoch:{}/{}'.format(i + 1, epoch)) for step, batch in tqdm(train_dataset.shuffle(len(train_dataset)).batch(batch_size).enumerate()): if configs.use_bert: X_train_batch, y_train_batch, att_mask_batch = batch # 计算没有加入pad之前的句子的长度 inputs_length = tf.math.count_nonzero(X_train_batch, 1) # 获得bert的模型输出 model_inputs = bert_model(X_train_batch, attention_mask=att_mask_batch)[0] else: X_train_batch, y_train_batch = batch # 计算没有加入pad之前的句子的长度 inputs_length = tf.math.count_nonzero(X_train_batch, 1) model_inputs = X_train_batch with tf.GradientTape() as tape: logits, log_likelihood, transition_params = bilstm_crf_model( inputs=model_inputs, inputs_length=inputs_length, targets=y_train_batch, training=1) loss = -tf.reduce_mean(log_likelihood) # 定义好参加梯度的参数 gradients = tape.gradient(loss, bilstm_crf_model.trainable_variables) # 反向传播,自动微分计算 optimizer.apply_gradients(zip(gradients, bilstm_crf_model.trainable_variables))
使用代码的时候,需要在system.config中配置好各个参数,每个参数上面都会有相应的配置介绍,项目中提供了两种模式(mode),分别是训练(train)和预测(interactive_predict)。在训练的时候,推荐使用GPU加速训练过程,如果你已经配置好GPU的各项系统环境,包括tensorflow-gpu的2.3版本,cudnn7.6和cuda10.1,只需要在配置中设置CUDA_VISIBLE_DEVICES这个参数就行。项目目前仅支持一个GPU训练,暂时不支持多卡训练。

命名实体识别常用算法及工程实现

预测也是很方便的:

命名实体识别常用算法及工程实现


参考及引用

https://createmomo.github.io/2017/09/12/CRF_Layer_on_the_Top_of_BiLSTM_1/

工业界求解NER问题的12条黄金法则

JayLou娄杰,公众号:夕小瑶的卖萌屋

中文NER的正确打开方式: 词汇增强方法总结 (从Lattice LSTM到FLAT)

公众号:机器学习算法与自然语言处理


https://github.com/StanleyLsx/entity_extractor_by_ner


命名实体识别常用算法及工程实现
命名实体识别常用算法及工程实现

  扫描二维码

小贤算法屋

以上是关于命名实体识别常用算法及工程实现的主要内容,如果未能解决你的问题,请参考以下文章

命名实体识别算法

命名实体识别(NER)详解及小样本下的NER问题解法汇总

NLP(6)——命名实体识别

学习--基于深度学习命名实体识别综述

学习--基于深度学习命名实体识别综述

机器学习 - 命名实体识别之Hidden Markov Modelling