©作者 | 邱震宇
单位 | 华泰证券算法工程师
研究方向 | NLP方向
前言
熟悉我的读者,应该看过我之前写过的一篇关于中文 NER 任务实践的文章(邱震宇:中文 NER 任务实验小结报告——深入模型实现细节 [1] ,在那篇文章中,我介绍了一个做 NER 任务的新范式:将实体抽取任务转化为抽取式阅读理解任务。
其核心思想就是将待抽取的实体标签描述作为 query 与原始的文本进行拼接,然后基于 BERT 做对应 span 的抽取。通过这种方式能够让模型学习到实体标签本身的语义信息,引入先验知识,从而提升模型的能力。在我之后的实现中,也进一步验证了这个方法的有效性。但我尝试将这个方法应用到实际场景任务时,却遇到了很多限制。
其中,影响最大的就是在线预测的效率。在实际场景中,我们的实体或者其他序列标注要素的标签类型通常会有很多(假定有 |C| 个),这意味着我们要将每个待预测的文本与 |C| 个标签 query 模板进行拼接得到模型输入,并需要调用 |C| 次模型的前向计算才能完成一个样本的抽取。假设 query 的最长文本长度为 n,原始文本的文本长度为 m,由于进行了 self-attention 的计算,整个计算的复杂度为,这样的在线预测效率是无法满足线上的需求的。
针对这个问题,我尝试了一些优化方法,但是都不太理想,最近终于在一篇 EMNLP2021 的论文中找到了比较好的方案,论文如下:Enhanced Language Representation with Label Knowledge forSpan Extraction [2] 。这篇论文提出了一个 LEAR(Label knowledge EnhAnced Representation) 模型架构,尝试优化 BERT-MRC 的一些缺陷,下面我将详细给大家描述这个方法,并对其进行有效性的验证。
本文涉及源码:
https://github.com/qiufengyuyi/lear_ner_extraction
LEAR原理
2.1 BERT-MRC的缺陷
首先,论文提到了之前使用 BERT-MRC 的方式做序列标注问题,虽然相比传统的 BERT-CRF 方法有一定的效果提升,但是仍然有两个缺陷。其中一个就是前言中提到的 效率问题 。另外一个则是 BERT-MRC 并没有充 分利用标签的知识信息 。前言中提到,BERT-MRC 引入了标签的先验知识,然而 LEAR 论文中通过对 attention 部分进行可视化分析,发现模型可能没怎幺用到 query 的信息。如图 1 所示,实体 judge 对应的高分 attention 并非如预期一样集中在问题的核心部分,而是分散在了一些无关的信息中(如 [CLS] 等 token)。
▲ 图1 attention 可视化
通过上述分析,可以发现 BERT-MRC 对于先验知识的利用率并不如预期。因此,LEAR 设计了一种新的标签知识整合方式,让模型高效利用标签先验知识,同时解决 BERT-MRC 的效率和知识利用率问题。
2.2 LEAR模型架构
LEAR 的模型架构如图 2 所示。
▲ 图2 LEAR 模型
其中,模型的输入分为两部分:原始的待抽取文本以及所有标签对应的描述文本(先验知识)。与 BERT-MRC 不同,我们不会将标签描述文本与原始文本进行拼接,而是使用 BERT 的编码器分别进行编码得到不同的文本表示。
值得注意的是,为了提升训练效率和减小模型的尺寸,原始文本和标签描述文本 共享 BERT 的编码器权重 ;之后我们引入一个标签知识融合模块,将所有标签描述文本的表示融合到原始文本中;最后我们使用引入分类器来识别待抽取 span 的 start 和 end 位置的概率分布,并使用一些 decoding 策略来抽取实体。下面,我将详细描述融合模块和分类模块的具体内容。
标签信息融合模块
假设我们通过 BERT 编码器得到了原始文本的表示以及所有标签的描述文本表示,其中 n 表示原始文本的长度,m 为标签文本的统一长度(经过 padding 之后),|C| 表示标签类别数量,d 表示 hidden_size。标签信息融合模块本质上是一个注意力机制模块,其目的就是要让模型学习到原始文本中的各个 token 会关注到标签描述文本中的哪些内容。
对每个标签 c,我们将视作 attention 计算中的 key 和 value 元素,将作为 attention 中的 query 元素(关于 attention 中的Q、K、V 可以参考我以前的 attention 文章介绍 Attention 机制简单总结 [3] ,进行点乘模式的 attention 计算并得到 attention 分数。在做点乘计算之前,我们参考 attention 典型的做法分别对原始文本表示和标签描述文本表示进行线性映射:
然后我们对其进行 attention 计算:
接下来就是对 value 元素应用所得到的 attention 分数进行加权求和操作,得到原始文本某个 token的 context 信息(这里在实现时要使用标签描述文本的 mask 信息,把 padding 位置的值 mask 掉):
我们接下来要做的就是将 context 信息融合到原始文本向量表示中。论文使用的方式是直接用 ADD 操作,将与 context 相加:
我尝试过 concat 拼接融合,最后发现效果并不如直接 add。在此架构下,add 操作的融合有更高的信息利用率。
之后,我们引入一个 dense layer,将最终的文本表达输出,其中激活函数使用了 tanh,相比 sigmoid,其值域更广,而且防止向量表示的绝对值过大:
最后,我们对每个文本 token、标签类别都进行上述流程,最终得到完整的融合向量表示:。
在模型实现时,所有标签类别和所有 token 的计算可以在一次矩阵的计算中完成。
span抽取分类模块
与 BERT-MRC 类似,LEAR 最后也是预测某个 span 的 start 和 end 的位置 token。但由于 LEAR 中将所有标签类别的预测都放在的一次前向计算中,因此最后的分类层与传统的方式有所不同。对于某个原文文本的 token,我们计算其作为某类实体的 start 概率分布:
其中,和分别是可训练的权重,而 表示 element-wise的矩阵乘法,而表示对输入矩阵中的 hidden_size 所在维度进行求和,最终得到一个 |C| 维的向量。
虽然上面的公式看起来还是有点绕,但实现起来也不是很复杂,在 TensorFlow 的框架下,可以这幺实现:
def classifier_layers(self,input_features,type="start"): #input features # batch_size,input_seq_len,num_labels,hidden_size # input_shape = modeling.get_shape_list(input_features) # batch_size = input_shape[0] # seq_length = input_shape[1] classifer_weight = tf.get_variable( name="classifier_weight"+"_"+type, shape=[self.num_labels, self.attention_size], initializer=modeling.create_initializer(self.bert_config.initializer_range)) classifer_bias = tf.get_variable(name="classifier_bias"+"_"+type,shape=[self.attention_size],initializer=tf.constant_initializer(0)) output = tf.multiply(input_features,classifer_weight) output += classifer_bias #[batch_size, input_seq_len, num_labels] output = tf.reduce_sum(output,axis=-1) return output
其中 tf.multiply 是一个 element-wise 矩阵乘法,且支持 broadcasting 机制。
2.3 span decoding
对于某类实体的 end 概率计算,与 start 的流程是一样的。最后每个样本将得到两个概率输出:,。根据这两个输出与预先设计的概率阈值(一般是 0.5),我们就能一次性得到所有标签类别的起始位置列表,但是要得到具体的 span 实体,还需要设计一些 decoding 策略。
论文中提出了两种 decoding 策略,分别为 最近距离策略 和 启发式策略 。最近距离策略就是先定位所有的 start 位置,然后找距离 start 位置最近的 token 作为 end。而启发式策略则稍微复杂一些,若定位到一个 start1 位置,不会马上决定它作为一个 span 的起始位置,若后面的 token 如果也是一个候选的 start2,且其概率还要大于 start1,则会选择新的 start2 作为 start 候选。具体大家可以参考论文后面的附录,有详细的算法流程。
说实话,我感觉论文中附录的算法伪代码有些问题,对于 end 位置的判断应该不需要像 start 一样选择概率最高的。另外,我自己的训练数据预处理时,对于单字成实体的情况,我的 end 位置是置为 0 的,而论文和开源代码中的设置却不同,因此不好做直接对比。
我自己实现时,使用的是变种的最近距离策略。因为选择与 start 最近的 end 时,有可能会越过下一个新的 start 位置,这有可能是算法本身预测有问题,或者有实体嵌套的情况(目前还未考虑嵌套实体的 case),如图 3 所示。
▲ 图3 最近距离decoding策略
我这边采取了比较简单的策略,就是在 start1 和 start2 的区间中,来定位与 start1 最近的 end 位置。那幺上图中的情况下,就不会选择 s1-e1 作为结果,而是只选择 s1 位置处的单个 token 作为结果输出:
▲ 图4 自定义的最近距离decoding策略
大家可以自行尝试不同的 decoding 策略,我感觉每种策略的适用场景都不一样,需要根据实际情况来判断。
2.4 LEAR的效率
LEAR 相对于 BERT-MRC 的最大优势在于其较高的 inference 效率。由于 LEAR 不需要为每个原始文本构造 |C| 个新样本,理论上 LEAR 的计算复杂度为。可以看到,这个复杂度是远小于 BERT-MRC 的,在后面的验证中,我也得到了预期的效果。
LEAR实现
我最近分享的一些方法大部分都经过实验并验证有效的,这次介绍的 LEAR 也不例外。论文有放出 pytorch 的源码: https://github.com/ Akeepers/LEAR 。我照例还是使用 tensorflow 来实现,最近重新看了下之前的 NER 框架代码,感觉有很多地方写的不太合理,因此我又重新开了个 repo。LEAR 的实现也不是太复杂,需要注意的地方主要有以下几个方面:
1. 构造模型输入时,要专门针对实体标签的描述文本进行预处理。在做 input_fn 的时候,使用 tf.data.Dataset.from_generator 来进行 batch 数据流的构建,但是我们的标签文本本来是没有 batch 概念的,所有训练样本都只用一份标签文本,因此在 model 方法定义中,要人为将 tf 添加的 batch 维度去掉:
if label_token_ids.shape.ndims == 3: label_token_ids = label_token_ids[0,:,:] label_token_type_ids = label_token_type_ids[0,:,:] label_token_masks = label_token_masks[0,:,:] label_token_length = label_token_length[0,:] label_lexicons = label_lexicons[0,:,:,:]
2. 原始文本和标签文本共享 encoder 参数,这里要对 google原始的 BERT 的 modeling.py 进行修改,在模型定义的时候,添加 auto_reuse 配置,同时在调用 bert 的时候,传入 scope=”bert”:
with tf.variable_scope(scope, default_name="bert",reuse=tf.AUTO_REUSE):
3. 论文和官方开源的代码都没有对 attention 计算后的分数进行 scaling。我觉得可能的原因是融合模块中的 attention 操作只有一层, 且注意力的分数并没有直接用于词 softmax 的计算,而是融合到了原始文本的向量表示中,另外在最后输出的时候使用了 tanh 进行了激活,输出的值不会太大,因此即使不做 scale,也不会产生很严重的梯度消失问题,并影响后面的分类器的计算。我这边也验证了,加入了 scaling 之后,效果相差不多。
最后,我将 LEAR 在中文的 MSRA 的 NER 数据集上进行了验证,同时与之前已经实现过的 BERT-MRC 进行了对比。验证指标则是考虑了实体类型后的 micro-level 的 f1 分数,具体来说我会将实体类型字符串与实体内容拼接,做去重后,进行 exact match 匹配。最终,得到的结果大体如下:
可以看到,LEAR 的效果相比 BERT-MRC 来说有一定提升,但是不够明显,但是 inference 的效率却是显着提升了,这也是我最关注的地方,这意味着这个方法可以应用在实际的业务场景中!
彩蛋!
因为很长时间没写文章了,所以写一次就尽量多包含点内容。为了奖励读到这边的同学,我再奉送一段额外的 NER 优化技巧。
读过我上一篇中文 NER 总结的同学应该记得我在那篇文章中提到尝试在 BERT 中引入词汇信息,当时尝试的办法很依赖分词的质量,且融合词汇信息的方式也比较简单。因此,这次我参考了最近比较火的一篇论文来做词汇增强:Simplify the Usage of Lexicon in Chinese NER [4] 。
这篇论文解读我就不做了,大家可以参考这篇:JayJay:中文 NER 的正确打开方式: 词汇增强方法总结(从 Lattice LSTM 到 FLAT) [5] 。这个方法的核心思想是先准备一份词向量和词表,然后对当前所有语料中的文本字符统计其分别属于 BMES 的信息,B 代表词的开头,M 代表词的中间,E 代表词的结束,S 代表单独成词。
假设对于某个 token x,若某个样本 A 中存在片段 span,其开头为 x,则将 span 对应的词向量添加到 B 对应的列表中,其他类型同样操作。最后,我们将每个类型中的词向量做加权平均,得到增加的词汇信息与原始的 token 向量表示拼接。
▲ 图5 词汇增强方法
这个方法实现的最大难点在于如何快速找到包含某个字符,且符合特定位置关系的 span 词。原论文开放了代码:
https://github.com/v-mipeng/LexiconAugmentedNER
其使用了 trie 数来构建词典树,这也是海量字符串匹配场景中会使用的一种方法。我在实现的时候,没有用这种方式,为了快速得到效果,使用了一种比较基础的方式,就是在数据预处理的时候,将每个字符对应的 BMES 的词列表信息都预先存储起来,在模型输入的时候直接读取信息,这样做速度不慢,但是对内存的要求就比价高了,类似于空间换时间。
另外,在使用 tensorflow 实现完整的功能时,我发现坑有点多。因为静态图中,你要将每个字符的 BMES 词表序列作为输入传到模型图中,然后用 embedding_lookup 分别找到词对应的词向量,再做加权平均,而每个字符实际的 BMES 词表 size 都不一样,这意味着更多 padding 和 mask 的处理,想想都头大。。。所以我偷了个懒, 不让词汇增强部分参与训练 , 而是将其作为固定的向量表示与原始 token 向量拼接 ,看其是否仍有增益的效果,结果居然也有一定的增益,结合 LEAR 架构,我得到了如下的结果:
TIPS:我在构建词表的时候,为了提高速度,将出现频率小于 5 的词都过滤掉了。
可以看到增加了词汇增强信息后,LEAR 的效果有一定的提升。如果对这个方法感兴趣,可以尝试实现完整功能的增强,让增强信息也参与到模型学习中,最后应该会有很大的提升。
小结
本文主要介绍了一种优化 BERT-MRC 的方法,针对其预测效率低、没有充分利用标签信息的两个缺陷,设计了一种专门针对标签文本的注意力机制融合方法,在提升模型效果的同时,大大提升了基于 MRC 做 NER 的效率,使其能够应用在实际的业务场景中。另外,本文也额外介绍了一种不依赖于分词工具的词汇增强方法,经过验证,证明其与其他 BERT 类的方法结合能够提升模型的抽取效果。
参考文献
[1] https://zhuanlan.zhihu.com/p/103779616
[2] https://aclanthology.org/2021.emnlp-main.379.pdf
[3] https://zhuanlan.zhihu.com/p/46313756
[4] https://aclanthology.org/2020.acl-main.528.pdf
[5] https://zhuanlan.zhihu.com/p/142615620
Be First to Comment