查看原文
其他

百度aistudio事件抽取比赛总结——记一次使用MRC方式做事件抽取任务的尝试

邱震宇 AINLP 2020-10-22

作者:邱震宇(华泰证券股份有限公司 算法工程师)

知乎专栏:我的ai之路




最近一个月主要是参与了百度举办的NLP比赛,其中本来想同时cover事件抽取和关系抽取两个比赛,无奈时间有限,且关系抽取的数据量着实有点大,因此后期主要将精力放在了事件抽取的比赛中。最近比赛临近尾声,我的工作基本上也已经完成,目前在A榜上的成绩是0.856,暂时排12,后续也不会再做优化提升,因此就对这次比赛做个总结(注:最终榜排名第8名)。相关代码已经在github上开源,详见

https://github.com/qiufengyuyi/event_extraction

qiufengyuyi/event_extractiongithub.com

传统的事件抽取任务通常分为两大部分,第一部分是抽取事件触发词,找到事件的类型;第二部分是根据事件触发词和事件类型,提取该事件的论元,如时间、地点、人物或者其他与事件相关的具体属性。

本次事件抽取任务相对于实际业务场景上的事件抽取任务有些许不同。最主要的就是它的评测只会关注(事件类型,事件论元类型,论元内容)三元组的匹配程度,而对事件触发词似乎并不关心。因此相当于可以将该问题转化为一个序列标注任务。

本次比赛我的主要目的在于实践一个想法——即使用MRC的方式来做事件抽取是否可行?之前我在中文NER任务实验小结报告——深入模型实现细节 这篇文章中探讨过使用MRC的方式来做中文的NER问题,经过实验可以得出该方法具有一定的有效性。然而,该实验只是在公开数据集上进行测试,且NER任务相对来说还是比较简单的。因此,希望通过在这次比赛中继续应用MRC方法来验证它的有效性。

数据初探

该比赛任务中,训练集的数量为1万多条,相对于其他赛道的数据来说,数据量相对较少。其中,每条数据包含待处理的文本,事件三元组。具体格式如下:



其中,论元部分会提供论元内容、论元文本在原始文本中的位置以及论元类型;比赛中,给出了样本中囊括的所有事件类型65种,每种事件类型都有不同数量的论元类型。将事件类型与论元类型组合成一种label类型后,能够得到217种类型,比如出售/收购 与 出售方进行整合,得到了出售/收购-出售方这样一个label形态。

关于数据集的数量问题

A榜的测试集是1000多条,最终的测试集足有30000条。根据这个数量可以看出,最终测试集居然比训练集的样本量还多,应该是主办方比较想考察模型在相对较小的数据量上进行训练,是否能够较好得泛化到更多的数据中。为了让模型有较好的泛化性能和较强的鲁棒性,可以对数据做一些数据增强,对模型做一些融合。不过这几块我基本上都没有过多投入,因为主要目标是验证MRC方法的有效性,倾向于用单模型来完成这件任务。

下面,我就着重介绍如何使用MRC的方式来做这个事件抽取任务。整个工作主要分为三个步骤:

1、事件类型抽取。使用MRC做事件抽取任务,很难同时把事件类型抽取和论元抽取都处理好,因此我选择了pipeline的模式,先单独将事件类型抽取任务转化为一个多标签分类任务,使用模型得到每个文本包含的事件类型,然后将结果输出到论元抽取中。

2、事件论元抽取。使用MRC的方式进行论元抽取,包括MRC问题的构建、模型的构建以及span的预测。

3、优化事件论元抽取模块。针对步骤2中出现的unanswerable问题进行处理。

事件类型抽取

这块内容其实做了简化,你并不需要去抽取事件类型的触发词,只需要知道包含哪些事件类型就可以了。(当然,有些同学会用到事件触发词来帮助做论元抽取,那么还是要做触发词抽取的。)因此,我可以将这个子任务转化为一个多标签分类任务。当前一共有65个事件类型,因此可以转化为65个多标签二分类任务。

传统做法就是将文本用预训练模型(bert-style)encode之后,使用其pooled-output连接一个分类层,然后使用sigmoid_cross_entropy_with_logits 的损失函数进行训练。最后得到每个事件类型被该文本包含的一个概率。最后根据一个概率阈值,来判断该文本包含哪些事件。

上述做法有一个问题:仅仅使用了原始文本的信息,但是忽略了事件类型文本的标签信息。因为在这个任务上,我们的标签是有一定的语义信息的。例如“胜负”和“晋级”两个事件都可能指向某个队伍在某个比赛中胜利,因此两个事件同时存在的可能性会比较高。如果将事件本身的语义信息也整合到模型中,会提升模型对标签的理解程度。

因此,我借鉴了NL2SQL中预测SQL语句中包含哪些column的方法,将65个事件分别用一个unused的标签包围,然后拼接到原文本后面,具体如下:

[CLS]泰安今早发生2.9级地震!靠近这个国家森林公园[SEP][unused1]地震[unused2][unused1]死亡[unused2]....

然后就是用BERT类的模型对整合后的文本进行encode,然后则是将所有[unused1]标签位置的tensor提取出来拼接成一个[batch_size*65*hidden_size]的矩阵,最后连接一个二分类层,并同样使用sigmoid_cross_entropy_with_logits 的损失函数进行训练。要注意事件文本拼接顺序和我们预测的事件index要一致。具体模型结构如图:(可以参考我另一篇文章CCFBDCI金融信息负面及主体判定赛题复赛10th总结)



经过实践证明,该方法相对于传统的方法,在最终的F1分数上能够提升0.5-1左右。

事件论元抽取

接下来是使用MRC方式来做事件论元抽取任务。在此模块中,我们会接入已经预测出来的事件类型(训练时直接用ground-truth类型),根据不同事件类型中包含的论元类型来构建MRC样本。

相信看过我中文NER任务实验小结报告——深入模型实现细节 这篇文章的同学肯定知道,将MRC应用到序列标注任务中,主要思想就是构建阅读理解问题,然后将该问题与原始文本passage进行一定的处理,然后用当前比较主流的MRC模型来做span抽取类型的阅读理解任务。因此,query问题的质量关系到整个任务的完成情况。这块就需要一定的人工规则了。目前,将事件类型与论元类型进行整合后,能够得到217条不同的label形态。我们可以将这个217条不同的label形态视作我们要做序列标注的217种不同的标签,之后就是针对每种标签构建一个适用于该标签的问题。在对这217种标签进行分析后,我将这些标签大致分为四类:

1、通用性的标签,比如所有事件类型中的时间、人数、人物对象等论元都是具有一定通用性的,这种标签即使跟不同的事件类型进行整合后,表达的含义基本相同。因此这类论元对应的query基本都不用变化,只需要在query之前增加事件类型字符串以示区分:

获奖-时间:找到获奖事件发生的时间,包含年、月、日、天、周、时、分、秒等
求婚-时间:找到求婚事件发生的时间,包含年、月、日、天、周、时、分、秒等

2、事件强相关的标签。这类标签通常与具体的事件类型有一定的关联,例如晋级-晋级方,罚款-执法机构等。这类标签的query可能需要提到事件类型的某些属性:

罚款-执法机构:拥有相对独立的法律地位和组织结构的行政机构

3、无法生成query的标签。这类标签是由于我实在无法给予其较为合适的问题,因此只是单纯保留其原始的论元类型描述,与事件类型整合,例如涨停-涨停股票等:

涨停-涨停股票:涨停-涨停股票

4、最后一种标签是我在最后对问题生成做了一步优化后得到的一些特殊例子。在对我的baseline进行了初步分析后,我发现我的模型对于数字类的问题回答得较差,例如回答死亡人数时,将年龄答案预测为了该答案。这种错误可以理解为数字类的回答通常都比较短小,且由数字和某个计量单位组成,因此模型很容易将其混淆。而我原始的问题生成时,对于大部分的数字问题都使用了原始的论元描述,需要针对数字问题专门设计问题,例如袭击-死亡人数:

袭击-死亡人数:袭击导致了多少人死亡?通常以人数为计量单位。

通过这种优化方式,为整个模型提供了0.1个百分点的提升,虽然提升不大,但是解决了部分数字类问题。

设计完问题模板后,就可以对每个文本进行MRC样本的生成,代码如下:

for event_type in event_type_list:
complete_slot_str = event_type + "-" + role_type
slot_id = self.labels_map.get(complete_slot_str)
query_str = self.query_map.get(slot_id)
event_type_str = event_type.split("-")[-1]
if query_str.__contains__("?"):
query_str_final = query_str
if query_str == role_type:
query_str_final = "找到{}事件中的{}".format(event_type_str, role_type)
elif role_type == "时间":
query_str_final = "找到{}{}".format(event_type_str, query_str)
else:
query_str_final = "找到{}事件中的{},包括{}".format(event_type_str, role_type, query_str)
return query_str_final

可以看到,MRC方式做论元抽取可以间接得增加数据量。假设一个文本包含n个不同事件类型,每个事件类型平均包含m个可提取内容的论元,那么一个文本可以扩充n*m倍,相当于做了数据增强。

MRC模型构建

使用bert类模型做span抽取类型的MRC任务通常能够得到很可观的效果。传统的一些sota方法通常需要分别对query和passage进行encode,然后设计各种attention方法(q-p and p-qattention等等)将query和passage之间的语义关系充分捕捉,并在passage中获取最终的答案位置。而Bert类方法通常将query和passage拼接在一起,然后统一使用bert类模型进行encode,最后,将整个sequence的tensor输出,预测sequence上每个位置上的结果,根据预测的标签类别可以分为两种做法:

1、每个位置视作一个多分类任务,即使用传统序列标注任务中的BIO(或者其他标注方式)方式,将待抽取span的起始字符标位B,剩余部分标为I,其余非span部分标为O。

2、每个位置视作两个二分类任务,分别是预测该位置是否为span起始位置以及该位置是否为span结束位置。

上述两个方法对于该任务来说基本上效果差不多。由于有些事件类型的论元抽取时,一个论元类型可能会有两个不同的论元内容,例如:离婚这个事件类型中,离婚双方这个论元就必须包含两个不同的人名对象。转化为MRC任务后,相当于是一个multi-answers问题。此时对于第一种标记方式就不需要修改任何东西,直接当做一个多分类序列标注任务就可以;然而对于第二种任务,就需要在predict的时候注意多个start位置、多个end位置以及start和end的对应映射处理。同时也需要注意start位置和end位置相同的情况也是存在的。(详见代码中的event_predict.py).

模型部分很简单,就是最简单的bert+交叉熵损失函数组合。具体可参考中文NER任务实验小结报告——深入模型实现细节 。

基于上述的事件类型抽取+MRC抽取事件论元方法,在chinese-roberta-wwm-base的预训练模型加持下,通过6折交叉验证训练,能够得到0.841的分数。使用chinese-roberta-wwm-large后,能够的得到0.847的分数。因为large模型训练比价慢,所以基本没有调参,如果仔细调调参,应该还会有提升空间。

Retro-Reader提升unanswerable问题效果

对前述的模型预测得到的结果进行了error analysis后发现模型存在着一些问题,即我在之前构建MRC任务数据时,除了构建了文本中包含的论元类型query样本外,还人为添加了一些不被包含的论元类型的negative sample。比如地震类型的事件,其包含的论元类型包括:

{"role": "时间"}, {"role": "死亡人数"}, {"role": "震级"}, {"role": "震源深度"}, {"role": "震中"}, {"role": "受伤人数"}

但不是所有文本都会包含所有论元。比如测试集的第一个例子:

7岁男孩地震遇难,父亲等来噩耗失声痛哭,网友:希望不再有伤亡

该例子中,关于地震的论元似乎一个都不存在。我在训练集中构建这样的例子时,会添加这样的no answer query添加到训练集中,其label均为0(表示非span)。

然而,如此构建样本会导致一些问题。当negative的样本构建过多时,我在测试集上的recall分数会非常低,相反precision分数会相对较高;而当我把所有negative样本都删除时,测试集上的recall分数会比较高,但是precision分数会急剧下降。当然,两者的f1分数提升都非常有限。对于上述测试集中的例子来说,我的模型会提取诸如 死亡人数:7岁,震级:7岁等结果。

显然,单纯通过增减negative sample数量是无法解决上述问题的。因此,经过相当长一段时间的调研和尝试,我终于找到一个易实现且效果较好的方法:Retro-Reader。这是上海交大一位大佬的文章,其在Squad 2.0 leaderboard上的排名还是很高的。其中,该方法在unanswerable问题上的表现是非常好的。具体的论文解读我就不单独做了,可以参考:PaperWeekly:刷新SQuAD2.0 | 上海交通大学回顾式阅读器(Retro-Reader)解析 。

该方法的核心思想有两个:

1、借鉴了人类做阅读理解时的模式,引入了两阶段阅读模式。在第一阶段粗读时,人类/模型主要是通读问题和原文,先判断该问题是否可以在原文中找到答案;在第二阶段精读时,人类/模型同时解决问题是否可解答以及找到原文中答案两个任务。最后,将第一个阶段的预测分析结果与二阶段的结果进行整合,最终得到该问题是否可回答的结论,若可回答则继续从原文中抽取答案。

2、引用了Verify机制。这个方法并非该论文原创,之前很多方法都尝试使用了Verify机制来提升unanswerable问题的回答效果。retro-reader中使用verify的方式为设计两种阶段的Verify概率,第一个阶段即使前述的粗读阶段,论文中为external front verifier,该阶段只是单独训练一个二分类模型来判断问题是否可回答,同时得到一个  。第二阶段为精度阶段,论文中为Internal front verifier,该阶段同时训练两个任务:问题是否可回答的二分类任务以及预测答案span在文章中的起始位置,得到  。然后将两阶段的verifier概率融合,得到一个Rear Verifier:  。在实际操作时,我就直接使用这个Rear Verifier来判断是否可回答。在论文中,作者还引入了一个TAV(Threshold-based Answerable Verifification),大概方法是根据span预测时,每个位置上的概率值,分别得到一个  以及一个  。然后得到一个final分数:  ,当该分数大于某个阈值时表示可回答。其中,s1表示的是[CLS]标记处的start预测概率,e1表示[CLS]标记处的end预测概率。我没有使用TAV的原因有两个:最后的阈值需要根据验证集来调整,而由于test1的数据和test2的数据分布未知,所以阈值比较难调;计算了一些例子中的TAV和最终预测结果,发现该值对于最终的预测结果影响不大,对于一些错误例子,不该回答的问题其TAV值还是很高,并不能剔除掉,个人推测可能对于该任务不太适用吧。

上述内容可以参考论文结构图,还是比较清晰的:



图中,还包含了query和passage之间的attention计算。这里作者做了实验,发现在使用bert类预训练模型做finetune后,模型增加一些后续的interaction的attention计算并没有太大的提升,说明预训练模型已经极大的捕捉到了文本之间的语义信息,因此再添加更多的attention计算并不能有太多的提升效果。对于我来说,跑一个bert模型对于我的机器来说已经够呛了,再加更多的attention计算也不现实,就没有实现,如果有兴趣且有算力的同学可以尝试加一下这个interaction看看有没有效果。

最终,使用Retro-reader,在roberta-base和robert-large的模型上都能带来较大的提升,最终通过阈值的简单调整,在robert-large上得到了0.851的效果。而在roberta-base上能够得到0.847的分数。

简单融合

前面提到了我并没有在模型融合上做过多的工作,不过最后几天,我通过优化了问题描述模板以及增加了对negative sample的取舍(根据query的长度以及数据分布)后,得到了一个新版模型,这个模型在precision上面表现很不错,但是在recall上表现不尽如人意。因此我将之前旧的问题模块得到的模型与新模型做了一个简单的概率融合,最后得到了0.856的个人最高分。

总结

至此,我在该项任务上的工作基本上讲完了。最后还是需要备注一点,即这些方法只在当前的测试集1上产生了上述效果,由于测试集2的数据量较大,模型在上面的泛化性能还未可知。不过,我的验证目的在一定程度上已经达到,可以暂时得到以下结论:

1、使用MRC方式做事件抽取任务是可行的,且容易实现的。它可以应用在实际的工业场景中,需要做的繁琐工作在于设计问题描述模板,这个工作可以迭代进行。通过这种方式,可以进行一定的数据增强,这对于一些缺少标注的公司也是一个不错的选择。

2、MRC领域目前相对于其他领域来说进展较为迅速,目前在Squad 2.0 leaderboard上前排模型已经超过人类水平。因此有许多模型架构可以借鉴。

3、不可否认,应用MRC方式做其他领域的NLP任务会遇到各种各样的问题,例如不可回答的问题,以及问题模板的构建。另外,对于原始任务转化成MRC方式的设计流程也是一个难题,例如对于上述事件抽取任务,还可以将事件触发词抽取任务转化为MRC问题,问题模式可以为“XX事件的触发词是什么?”,并将该问题与论元抽取任务一起整合成一个阅读理解问答库,不过具体实施起来会遇到不少问题,有兴趣的同学可以尝试一下。



本文由作者授权AINLP原创发布于公众号平台,欢迎投稿,AI、NLP均可。原文链接,点击"阅读原文"直达:


https://zhuanlan.zhihu.com/p/141237763

推荐阅读

文本自动摘要任务的“不完全”心得总结

模型压缩实践系列之——layer dropout

模型压缩实践系列之——bert-of-theseus,一个非常亲民的bert压缩方法

这个NLP工具,玩得根本停不下来

征稿启示| 200元稿费+5000DBC(价值20个小时GPU算力)

文本自动摘要任务的“不完全”心得总结番外篇——submodular函数优化

Node2Vec 论文+代码笔记

模型压缩实践收尾篇——模型蒸馏以及其他一些技巧实践小结

中文命名实体识别工具(NER)哪家强?

学自然语言处理,其实更应该学好英语

斯坦福大学NLP组Python深度学习自然语言处理工具Stanza试用

太赞了!Springer面向公众开放电子书籍,附65本数学、编程、机器学习、深度学习、数据挖掘、数据科学等书籍链接及打包下载

数学之美中盛赞的 Michael Collins 教授,他的NLP课程要不要收藏?

自动作诗机&藏头诗生成器:五言、七言、绝句、律诗全了

这门斯坦福大学自然语言处理经典入门课,我放到B站了

关于AINLP

AINLP 是一个有趣有AI的自然语言处理社区,专注于 AI、NLP、机器学习、深度学习、推荐算法等相关技术的分享,主题包括文本摘要、智能问答、聊天机器人、机器翻译、自动生成、知识图谱、预训练模型、推荐系统、计算广告、招聘信息、求职经验分享等,欢迎关注!加技术交流群请添加AINLPer(id:ainlper),备注工作/研究方向+加群目的。


阅读至此了,点个在看吧👇

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存