当前位置:网站首页 > 技术博客 > 正文

nlp入门命名实体识别



作者:王岳王院长 知乎:https://www.zhihu.com/people/wang-yue-40-21 github: https://github.com/wavewangyue 编辑:yuquanle

最近在做命名实体识别(Named Entity Recognition, NER)的工作,也就是序列标注(Sequence Tagging),老 NLP task 了,就是从一段文本中抽取到找到任何你想要的东西,可能是某个字,某个词,或者某个短语

为什么说流水的NLP铁打的NER?NLP四大任务嘛,分类、生成、序列标注、句子对标注。分类任务,面太广了,万物皆可分类,各种方法层出不穷;句子对标注,经常是体现人工智能(zhang)对人类语言理解能力的标准秤,孪生网络、DSSM、ESIM 各种模型一年年也是秀的飞起;生成任务,目前人工智障 NLP 能力的天花板,虽然经常会处在说不出来人话的状态,但也不断吸引 CopyNet、VAE、GAN 各类选手前来挑战;唯有序列标注,数年如一日,不忘初心,原地踏步,到现在一提到 NER,还是会一下子只想到 LSTM-CRF,铁打不动的模型,没得挑也不用挑,用就完事了,不用就是不给面子

虽然之前也做过 NER,但是想细致地捋一下,看一下自从有了 LSTM-CRF 之后,NER 在做些什么,顺便记录一下最近的工作,中间有些经验和想法,有什么就记点什么

因为能力有限,还是跟之前一样,就少讲理论少放公式,多画模型图多放代码,还是主要从工程实现角度记录和分享下经验,也记录一些个人探索过程。如果有新人苦于不知道怎么实现一个 NER 模型,不知道 LSTM-CRF、BERT-CRF 怎么写,看到代码之后便可以原地起飞,从此打开新世界的大门;或者有老 NLPer 从我的某段探索过程里感觉还挺有意思的,那我就太开心了。就这样

还是先放结论

命名实体识别虽然是一个历史悠久的老任务了,但是自从2015年有人使用了BI-LSTM-CRF模型之后,这个模型和这个任务简直是郎才女貌,天造地设,轮不到任何妖怪来反对。直到后来出现了BERT。在这里放两个问题:

经过我不完善也不成熟的调研之后,好像的确是的,一个能打的都没有

既然模型打不动了,然后我找了找 ACL2020 做NER的论文,看看现在的NER还在做哪些事情,主要分几个方面:

所以沿着上述思路,就在一个中文NER任务上做一些实践,写一些模型。都列在下面了,首先是 LSTM-CRF 和 BERT-CRF,然后就是几个多任务模型, Cascade 开头的(因为实体类型比较多,把NER拆成两个任务,一个用来识别实体,另一个用来判断实体类型),后面的几个模型里,WLF 指的是 Word Level Feature(即在原本字级别的序列标注任务上加入词级别的表征),WOL 指的是 Weight of Loss(即在loss函数方面通过设置权重来权衡Precision与Recall,以达到提高F1的目的),具体细节后面再讲

ok 下面正经开工

用纯 HMM 或者 CRF 做 NER 的话就不讲了,比较古老了。从 LSTM+CRF 开始讲起,应该是2015年被提出的模型[1],模型架构在今天来看非常简单,直接上图

BI-LSTM 即 Bi-directional LSTM,也就是有两个 LSTM cell,一个从左往右跑得到第一层表征向量 l,一个从右往左跑得到第二层向量 r,然后两层向量加一起得到第三层向量 c

如果不使用CRF的话,这里就可以直接接一层全连接与softmax,输出结果了;如果用CRF的话,需要把 c 输入到 CRF 层中,经过 CRF 一通专业缜密的计算,它来决定最终的结果

这里说一下用于表示序列标注结果的 BIO 标记法。序列标注里标记法有很多,最主要的还是 BIO 与 BIOES 这两种。B 就是标记某个实体词的开始,I 表示某个实体词的中间,E 表示某个实体词的结束,S 表示这个实体词仅包含当前这一个字。区别很简单,看图就懂。一般实验效果上差别不大,有些时候用 BIOES 可能会有一内内的优势

另外,如果在某些场景下不考虑实体类别(比如问答系统),那就直接完事了,但是很多场景下需要同时考虑实体类别(比如事件抽取中需要抽取主体客体地点机构等等),那么就需要扩展 BIO 的 tag 列表,给每个“实体类型”都分配一个 B 与 I 的标签,例如用“B-brand”来代表“实体词的开始,且实体类型为品牌”。当实体类别过多时,BIOES 的标签列表规模可能就爆炸了

「基于 Tensorflow 来实现 LSTM+CRF 代码也很简单,直接上」

Tensorflow 里调用 CRF 非常方便,主要就 crf_log_likelihood 和 crf_decode 这两个函数,结果和 loss 就都给你算出来了。它要学习的参数也很简单,就是这个 transition_matrix,形状为 V*V,V 是输出端 BIO 的词表大小。但是有一个小小的缺点,就是官方实现的 crf_log_likelihood 里某个未知的角落有个 stack 操作,会悄悄地吃掉很多的内存。如果 V 较大,内存占用量会极高,训练时间极长。比如我的实验里有 500 个实体类别,也就是 V=500*2+1=1001,训练 1epoch 的时间从 30min 暴增到 400min

不过好消息是,Tensorflow2.0 里,这个问题不再有了

坏消息是,Tensorflow2.0 直接把 tf.contrib.crf 移除了,目前还没有官方实现的 CRF 接口

再说一下为什么要加 CRF。从开头的 Leaderboard 里可以看到,BiLSTM 的 F1 Score 在72%,而 BiLSTM+CRF 达到 80%,提升明显

那么为什么提升这么大呢?CRF 的原理,网上随便搜就一大把,就不讲了(因为的确很难,我也没太懂),但是从实验的角度可以简单说说,就是 LSTM 只能通过输入判断输出,但是 CRF 可以通过学习转移矩阵,看前后的输出来判断当前的输出。这样就能学到一些规律(比如“O 后面不能直接接 I”“B-brand 后面不可能接 I-color”),这些规律在有时会起到至关重要的作用

例如下面的例子,A 是没加 CRF 的输出结果,B 是加了 CRF 的输出结果,一看就懂不细说了

用 BERT 来做,结构上跟上面是一样的,只是把 LSTM 换成 BERT 就 ok 了,直接上代码

首先把 BERT 这部分模型搭好,直接用 BERT 的官方代码。这里我把序列长度都标成了“S+2”是为了提醒自己每条数据前后都加了“[CLS]”和“[SEP]”,出结果时需要处理掉

然后在后面接东西就可以了,可以接 LSTM,可以接 CRF

其实我原来不太相信 BERT 在中文上的效果,加上我比较排斥这种不讲道理的庞然大物

真正实验了发现,BERT确实强啊

把我显存都给吃光了,但确实强啊

训练一轮要那么久,但确实强啊

讲不出任何道理,但确实强啊

相比较单纯使用 BERT,增加了 CRF 后效果有所提高但区别不大,再增加 BiLSTM 后区别很小,甚至降低了那么一内内

另外,BERT 还有一个至关重要的训练技巧,就是调整学习率。BERT内的参数在 fine-tuning 时,学习率一定要调小,特别时后面还接了别的东西时,一定要按两个学习率走,甚至需要尝试多次反复调,要不然 BERT 很容易就步子迈大了掉沟里爬不上来,个人经验

参数优化时分两个学习率,实现起来就是这样

上面提到过,如果需要考虑实体类别,那么就需要扩展 BIO 的 tag 列表,给每个“实体类型”都分配一个 B 与 I 的标签,但是当类别数较多时,标签词表规模很大,相当于在每个字上都要做一次类别数巨多的分类任务,不科学,也会影响效果

从这个点出发,就尝试把 NER 改成一个多任务学习的框架,两个任务,一个任务用来单纯抽取实体,一个任务用来判断实体类型,直接上图看区别

这个是参考 ACL2020 的一篇论文[2]的思路改的,“Cascade”这个词是这个论文里提出来的。翻译过来就是“级联”,直观来讲就是“锁定对应关系”。结合模型来说,在第一步得到实体识别的结果之后,返回去到 LSTM 输出那一层,找各个实体词的表征向量,然后再把实体的表征向量输入一层全连接做分类,判断实体类型

关于如何得到实体整体的表征向量,论文里是把各个实体词的向量做平均,但是我搞了好久也没明白这个操作是怎么通过代码实现的,后来看了他的源码,好像只把每个实体最开头和最末尾的两个词做了平均。然后我就更省事,只取了每个实体最末尾的一个词

具体实现上这样写:在训练时,每个词,无论是不是实体词,都过一遍全连接,做实体类型分类计算 loss,然后把非实体词对应的 loss 给 mask 掉;在预测时,就取实体最后一个词对应的分类结果,作为实体类型。上图解释

代码不贴了,感兴趣的可以在 git 里看

说一下效果。将单任务 NER 改成多任务 NER 之后,基于 LSTM 的模型效果降低了 0.4%,基于 BERT 的模型提高了 2.7%,整体还是提高更明显。另外,由于 BIO 词表得到了缩减,CRF 运行时间以及消耗内存迅速减少,训练速度得到提高

P.S. 另外,既然提到了 NER 中的实体类型标签较多的问题,就提一下之前看过的一篇文章[3]。这篇论文主要就是为了解决实体类型标签过多的问题(成千上万的数量级)。文中的方法是:把标签作为输入,也就是把所有可能的实体类型标签都一个个试一遍,根据输入的标签不同,模型会有不同的实体抽取结果。文章没给代码,我复现了一下,效果并不好,具体表现就是无论输入什么标签,模型都倾向于把所有的实体都抽出来,不管这个实体是不是对应这个实体类型标签。也可能是我复现的有问题,不细讲了,就是顺便提一句,看有没有人遇到了和我一样的情况

❝Scaling Up Open Tagging from Tens to Thousands: Comprehension Empowered Attribute Value Extraction from Product Title. ACL 2019 ❞

中文 NER 和英文 NER 有个比较明显的区别,就是英文 NER 是从单词级别(word level)来做,而中文 NER 一般是字级别(character level)来做。不仅是 NER,很多 NLP 任务也是这样,BERT 也是这样

因为中文没法天然分词,只能靠分词工具,分出来的不一定对,比如“黑啤酒精酿”,如果被错误分词为“黑啤、酒精、酿”,那么“啤酒”这个实体就抽取不到了。类似情况有很多

但是无论字级别、词级别,都是非常贴近文本原始内容的特征,蕴含了很重要的信息。比如对于英文来说,给个单词“Geilivable”你基本看不懂啥意思,但是看到它以“-able”结尾,就知道可能不是名词;对于中文来说,给个句子“小龙女说我也想过过过儿过过的生活”就一时很难找到实体在哪,但是如果分好词给你,一眼就能找到了。就这个理解力来说,模型跟人是一样的

在英文 NLP 任务中,想要把字级别特征加入到词级别特征上去,一般是这样:单独用一个BiLSTM 作为 character-level 的编码器,把单词的各个字拆开,送进 LSTM 得到向量 vc;然后和原本 word-level 的(经过 embedding matrix 得到的)的向量 vw 加在一起,就能得到融合两种特征的表征向量。如图所示

但是对于中文 NER 任务,我的输入是字级别的,怎么把词级别的表征结果加入进来呢?

ACL2018 有个文章[4]是做这个的,提出了一种 Lattice-LSTM 的结构,但是涉及比较底层的改动,不好实现。后来在 ACL2020 论文里看到一篇文章[5],简单明了。然后我就再简化一下,直接把字和词分别通过 embedding matrix 做表征,按照对应关系,拼在一起就完事了,看图就懂

具体代码就不放了,感兴趣可以上 git 看

从结果上看,增加了词级别特征后,提升很明显

很可惜,我还没有找到把词级别特征结合到 BERT 中的方法。因为 BERT 是字级别预训练好的模型,如果单纯从 embedding 层这么拼接,那后面那些 Transformer 层的参数就都失效了

上面的论文里也提到了和 BERT 结合的问题,论文里还是用 LSTM 来做,只是把句子通过 BERT 得到的编码结果作为一个“额外特征”拼接过来。但是我觉得这不算“结合”,至少不应该。但是也非常容易理解为什么论文里要这么做,BERT 当道的年代,不讲道理,打不过就只能加入,方法不同也得强融,么得办法

本来打算到这就结束了,后来临时决定再加一点,因为感觉这点应该还挺有意思的

大多数 NLP task 的评价指标有这三个:Precision / Recall / F1Score,Precision 就是找出来的有多少是正确的,Recall 是正确的有多少被找出来了,F1Score是二者的一个均衡分。这里有三点常识

对于一个分类任务,是很容易通过设置一个可调的“阈值”来达到控制 P/R 的目的的。举个例子,判断一张图是不是 H 图,做一个二分类模型,假设模型认为图片是 H 图的概率是 p,人为设定一个阈值 a,假如 p>a 则认为该图片是 H 图。默认情况 p=0.5,此时如果降低 p,就能达到提高 Recall 降低 Precision 的目的

但是 NER 任务怎么整呢,他的结果是一个完整的序列,你又不能给每个位置都卡一个阈值,没有意义

然后我想了一个办法,通过控制模型学习时的 Loss 来控制 P/R:如果模型没有识别到一个本应该识别到的实体,就增大对应的 Loss,加重对模型的惩罚;如果模型识别到了一个不应该识别到的实体,就减小对应的 Loss,当然是选择原谅他

实现上也是通过 mask 来实现,看图就懂

实现也非常简单,放一下对应的代码

从实验效果来看,原本 Precision 远大于 Recall,通过权衡,把两个分数拉到同个水平,可以提升最终的 F1Score

除此之外,在所有深度学习任务上,都可以通过调整 Loss 来达到各种特殊的效果,还是挺有意思的,放飞想象,突破自我

总结放在开头了,就这样

完结,撒花

「参考」

  • 上一篇: 创建用户并指定uid
  • 下一篇: c++ map和multimap
  • 版权声明


    相关文章:

  • 创建用户并指定uid2025-01-04 18:30:01
  • ssh远程执行shell脚本2025-01-04 18:30:01
  • 树状数组求最大值2025-01-04 18:30:01
  • java集合概述2025-01-04 18:30:01
  • 生成树协议是由什么标准规定2025-01-04 18:30:01
  • c++ map和multimap2025-01-04 18:30:01
  • python安装win322025-01-04 18:30:01
  • getchar gets2025-01-04 18:30:01
  • jinja2是什么2025-01-04 18:30:01
  • 移动端ui框架(一篇文章搞懂!vue中pc端与移动端适配解决方案(亲测有效))2025-01-04 18:30:01