/cail2019

法研杯2019 阅读理解赛道 top3

Primary LanguagePythonApache License 2.0Apache-2.0

比赛心得&复盘

第一阶段

自从取了like cxk这个队名后,发现好多人和我有一致的想法,取名“不是 cxk”,“唱跳rap篮球”,就是什么“ikun”之类,我们看起来就像是彼此间的小号哈哈哈哈

这个比赛赛方提供了两个baseline,在第一阶段的性能分别是

  • BIDAF F1=63.9%
  • Bert F1=78.6

在第一阶段,我直接采用了最简单的bert模型,使用的是huggingface 设计的框架,当时这个框架的名字还是“pytorch_pretrained_bert”,现在已经变了,新版的进一步的封装设计了整个pipeline, 而当时的run_squad.py版本则是根据tf版本修改的,整个流程就是训练一次然后预测一次。所以我就略微改写了下整个pipeline,在训练中验证 模型的效果,保存性能最好的模型,当然理论上来说和原来版本保存最后三个模型相差不太多。

相对于squad2.0,这个比赛增加了YES/NO类型。最初始的版本,先不考虑YES/NO类型,所以就把这些问题在预处理阶段去除,然后跑squad2.0版本的代码。结果出乎意料,f1值居然只有6点几。 然后我就想,这个数据集这么难啊,bert都回答不好,进一步分析了数据,发现文章长度很多都是超过512(bert的max_seq的限制),然后就在想 是不是预处理出了问题,check了一下代码,发现没有问题。然后放弃了这个比赛。 过了几天,闲着没事,就把训练脚本的bert模型改成huggingface里自动从亚马逊云下载的作者转换好的模型,发现结果突然正常了。 结果大概是(step=500的情况下)

    - OrderedDict([('civil', {'em': 46.6, 'f1': 61.9, 'qas': 500}), ('criminal', {'em': 39.2, 'f1': 62.7, 'qas': 500}), 
    ('overall', {'em': 42.9, 'f1': 62.3, 'qas': 1000})])

这个结果咋还是那么低呢,还没到官方给的BIDAF的结果,同期,我发现在github上有人把ERNIE(baidu)转成了pytorch版本ERNIE, 然后我就用了这个跑模型,结果出乎意料,更低了

    - OrderedDict([('civil', {'em': 40.6, 'f1': 58.2, 'qas': 500}), ('criminal', {'em': 36.8, 'f1': 59.3, 'qas': 500}), 
        ('overall', {'em': 38.7, 'f1': 58.8, 'qas': 1000})])

接着就先简单的调参,然后将训练的pipeline按照上面的思路改写,把数据预处理,模型,结果后处理和训练pipeline分为若干个文件, 修改max_answer_length,设置完整的steps或者epochs,然后F1值大概提升到67.9。然后就再也提升不上去了,然后又玩了几天。

闲着没事,回到比赛,我重新分析了下数据的分布,YES/NO类型的大概占问题的12%,相当于相对前面的模型,我有12%左右的结果都为0(F1值)。 所以需要设计一个模型能够处理这个YES/NO类型的问题,相当于在bert的输出上加上一个分类器(基本都是这么做的,参考Hotpot数据集的baseline, 但是懒得改模型和输入输出,所以看了下YES/NO类型的问题,忽然发现YES/NO问题都包含了非常明显的特征,只要包含“是否”或者是“是xxx的吗?”线索的问题, 大部分都是YES/NO问题,统计了比例在99个包含着两个关键线索的问题里,有86个问题都是YES/NO/UNK,进一步分析,YES和NO的比例大概是6.5:4.5, 那就意味着,不需要模型,只要将这类问题都回答YES,那性能就能提升8到9个点。简单试了下这个想法,果然提升了8到9个点。这时候的F1值差不多就是75左右, 第一阶段排名瞬间拉高,忽然有拿奖的希望?(并没有)。

接着就是一顿分析这些是非问题,我不能这么简单的都预测成YES, 是不是有一个方式能够把答案预测成NO,忽然想到,虽然训练的时候没有考虑是非问题,但是预测的时候对于是非问题还是会预测 一个span,所以通过观察预测的span,忽然发现这些span中有一些对问题的一些倾向的特征,比如预测的span中含有反面(负面) 的词,则答案很有可能是NO,所以就分析了下负面词,然后在性能上也有略微的提升,主要是体现在部分是非问题上NO类型问题回答正确。

    value = 'predict span'
    # value.find('未') >= 0 or value.find('没有') >= 0 or value.find('不是') >= 0 
    if value.find('无责任') >= 0 or value.find('不归还') >= 0 \
        or value.find('不予认可') >= 0 or value.find('拒不') >= 0 \
        or value.find('无效') >= 0 or value.find('不是') >= 0 \
        or value.find('未尽') >= 0 or value.find('未经') >= 0 \
        or value.find('无异议') >= 0 or value.find('未办理') >= 0\
        or value.find('均未') >= 0:
        preds.append({'id': key, 'answer': "NO"})

然后人生就遇到了瓶颈,上不去了,调参无用,规则无门,开始分析错误样例,沉迷于分清投保人,保险人, 被保险人等之间的关系,分析了很多保险类的问题,一度觉得自己可以去卖保险了。也发现了一些数据集上本身的问题, 主要有:

  • 标注不正确:答案错误
  • 标注不一致:比如同样的问钱,有些gold answer是“300元”,有些则是“300”,而文中是“300元”,这个似乎都会出现,在之前的xxx测评中,追一科技对1w加的样本进行了重新标注。参考追一科技CMRC的解决方案
  • 标注得不好:这是一个比较致命的问题,比如要预测一个命名实体,比如说“张3”,而文中出现多次“张3”,而标注者给出的标注 答案,往往都是“张3”第一次出现的位置,这会导致一个问题,我们的模型本质上是一个匹配模型,假设文章中出现的是 “被告人李4和张3”, “被保险人为张3”,“张3”出现了两次,而对于问题“被保险人是谁?”gold answer标注的位置则是第一个被告人张3,则模型很容易将“被保险人-被告人”映射成一组关系,则会导致模型在下一个样本预测的时候,预测成了“李4”而不是“张3”。可以看出,模型无非是从span的上下文语境来判断当前span是否为正确答案,而这种标注会导致模型学习到一个错误的上下文语境,从而学习到了一个错误的匹配方式,导致预测的错误。所以标注者给出正确上下文场景下的答案标注位置是相当重要的。

对于标注不一致问题,因为这个测评看不到开发集和测试集,所以自己重新标注不一定更符合原有分布(主要是懒)。对于标注得不好这个问题,就想等着下一阶段的数据出来再重新标注(并没有)。

然后忽然想起,没有对unk类型的问题进行处理,unk问题大概占7%?分析了下,基本没啥显著的特征可以像YES/NO问题类型一样处理,则遵循run_squad.py里的处理方式,设置一个阈值,score(start_null)*score(end_null)>null_thresh,这里的start_null和end_null按照原来的处理都是序列的下标为0的位置。可以参考Read + Verify这个论文里background章节介绍。在预测的时候,传入阈值参数(null_score_diff_threshold=5),线上性能也有显著提升。至此,整套模型的性能接近官方BERT baseline。

接着,又是闲着没事就对错误样例一通分析,明白了很多人生道理,模型在人类语言面前,就是人工“智障”,比如一个问题加入了时间或者场景的限定,模型就立即找不到北了。而对于多目标跟踪,比如“犯罪分工情况”,模型只能知道“xxx望风”,而答案是“xxx望风xxx抢劫”,由于刑事类文书的问题很多都是描述类的问题,而不是像民事类的找“实体”(是谁?是多少?),所以模型难以控制自己预测的答案是长还是短,是多还是少。模型无法解决的问题还有很多。尝试想要找规律,发现还是太年轻。

又玩了几天,想着大概可以开始做集成了。而集成怎么做呢?最简单的方法就是rank每个模型预测的最好的span的分数。 这就是最原始版本的集成模型。可以参考squad_2.0 ensemble,集成还是有一定效果的,第一阶段用了4个模型集成,成功超过了BERT baseline(讯飞)。忽然觉得好像要夺冠了?(想太多)

然后又玩了两周,第二阶段开始前,发现其他人已经刷到85了!(后悔玩了两周)

第二阶段

第二阶段开始后,数据增大到了4w,将之前的模型在这个数据(按9:1分)上训练了一个模型,提交完之后发现线上只有F1值75左右的性能,落后别人一大截,所以猜想,大家应该已经修改了原始的bert模型,已适应unk和yes/no类型的问题。所以大概设了几个方案

  • 模型+分类:本质上是一个简单的多任务学习(multi-task),训练样本预处理,答案后处理
  • K折交叉&集成
  • data argument,从squad榜单上可以看到,有很多模型对数据进行了增强,比如用回译的方法,利用一个翻译软件将中文翻译成英文,再英文翻译成中文,则得到一个新的训练数据,这个思路大概是QANET应用的方法,在squad榜单上缩写MMT之类的大概都有使用了这个技术。
  • 预训练一个新的bert模型(或者xlnet模型),这个方案需要大量的语料->受限只有两张1080Ti能跑实验,所以放弃了。->后面清华团队放出了一个经过大量法律文书增量预训练的bert模型,尝试了一下,效果不如原来的好,可能的原因在于它将刑事和民事两类数据分别预训练的模型,而我们的训练样本则是两类数据融合在一起的,而我只用了其中一个预训练模型,但是在两个问题上的表现都同步下降了,所以我的推理也不一定是正确的。

对于第一个方案:不得不去改原有的模型了,主要是预处理和模型输入阶段,需要添加yes的label,no的label,和unk的label, 对于这三个类别,使用bert在[CLS]位置上的输出作为一个小型网络(全连接层)的输入,预测出的3个logits,和原有的span logits进行拼接,做softmax操作,再计算损失。本质上这是一个非常base的多任务学习(multi-task),loss=loss_span+loss_yes+loss_no+loss_unk,其实应该加入超参数设置搞成加权求和。当然,还需要在预测阶段添加yes,no,和unk的预测,经过这样一番改造,完成了一个端到端的模型(无需额外处理yes/no类型问题),这个端到端的模型在yes/no类型的预测上,竟然能达到(YES 95%以上,NO 40%以上)的效果,所以说明问题里“是否”这类的关键性特征模型能够很好的捕获到。

同期,对run_squad.py预处理和答案后处理的方式也更深入的check了下代码。在原有的run_squad.py,在处理长文本文章的时候,是使用doc_stride间隔,切出一段一段文本作为新的样本处理,而在预测阶段,则会导致一个词可能在多个片段中出现,而预测则只要预测这个词的一个prob,那我们选哪个片段的这个词作为最终prob呢?这就是“_check_is_max_context”函数所解决的问题,它的核心**在于,如果一个词在一段文本中越中间的位置,那么这个词蕴含的上下文信息就会越多,那又这个上下文场景预测的这个词的score就要更可信,大家可以体会一下这个**,这也说明了一个词的上下文语境是多么重要,也就是LM模型相对词向量效果好这么多的很重要的一点(开始瞎说)。而原作者这种做法虽然无法证明,但是就是觉得好有道理,无法反驳。而XLNET的作者对答案预测的后处理则是另一种思路,也是好有道理,无法反驳。

而上述的端到端单模型则在线上达到了78+的性能。这样的性能是在两张卡,batch_size=12(一张卡batch_size=6)的情况下跑出来的。

接着,尝试了几种不同的预训练bert模型,性能差不多是这样的: bert-wwm>google>ERNIE>THU,所以之后一直延续使用了bert-wwm这个预训练模型。

上述模型有一个问题,yes,no和unk居然share了一个输入和一个网络参数,YES/NO类型的问题也存在着一些unk问题,YES/NO其实是能根据上下文推理出一个正确的答案的,也就是它在文章中能够找到一些evidence span作为supports,这和前面说到的利用预测的span中一些features来预测NO的现象是一致的。而unk呢?往往都是无法在文章中找到一些有利的supports,所以才导致这个问题无法回答。所以我们似乎至少应该使用两个小型网络把yes/no问题和unk问题分别处理。所以根据这个,我依然用[CLS]位置的输出作为输入,用来预测unk,而对于yes/no,则使用一个self attention+sum pooling的方式把整个sequence的输出压缩成一个向量,作为输入,用来预测yes/no。这样的模型线上能够79+

后面看了一些answer varified模块的论文,都没有比较满意的,有一天我发现了SOUGO在SMRCTOOKIT中发布了他们在coqa上bert+answer varified的模型,所以我就参考他们的tf版本,改写了一个pytorch版本的,因为这个数据集没有rational,所以略微有些不一样,多跑了几次这个模型,线上能够80左右。

K折交叉,预估了下,这个比赛最多支持3G的压缩包,而pytorch模型大约400M,所以就做了一个8折的交叉,通过这样划分,训练8个base模型,这些模型在线上基本维持在80左右(提交次数太少,也没有一一验证)。

集成,第一阶段的集成方法,大概是80.8。原来的方法有什么问题呢?因为有些问题是YES/NO或者是unk的,这种似乎用vote的方式来解决会更好,所以就改了下集成的方法,假设一个样本有8个答案,yes或者no或者unk超过半数的,则直接用vote的方式,而不选得分最大的作为答案。

同期,和组里做阅读理解的大佬讨论了下,得到几个集成方案:

  • 选择得分最大的方式
  • 对yes/no,unk和span分别处理,使用得分最大+vote的方式
  • 完全使用vote的方式,也就是说如果一个span被多个模型所预测,则选它作为答案
  • 所有模型在序列上的概率求平均,然后单成一个模型的输出预测(这个方案我认为会导致预测边界模糊,大佬说不一定会,然后也没讨论出结果来。是否能给出一个证明,证明这种集成方式不会导致边界模糊?)

在实际中,第三个方案和第四个方案在本地上的效果差不多,线上来说,第三个方案优于第四个方案。可能是因为第三个方案对于yes/no/unk这类问题的友好性。最终这个集成方案线上达到了F1值=81.7的效果。

接下来就是抽奖时间了,把模型放去跑,然后玩两天,然后选择不同的模型组合,最终差不多就是81.77的效果。然后人生又遇到了瓶颈,其他人刷到了82,83,我却再也上不去了。

然后又到了“人工”智能时刻,就开始对错误的case一顿分析,看着一件件法律文书,懂得了很多人生道理。学习了很多法律知识,保险知识,然而我的模型却理解不了我的用心良苦。

最后几天,只好做一些答案后处理的工作,虽然方式很低端,但是也是确实的提高了线上的性能,F1值来到了81.815,最终这就是第二阶段的成绩。最后,主办方剔除了一些小号后,定格在了rank 4.

第三阶段

忽然就第三名了。。可能是因为like cxk这个队名的魔力吧

总结

  • 友情提示,因为代码没有整理,不太建议看代码。
  • 比赛最重要的就是先了解你的数据,再想办法解决问题,无论是用名字听起来就牛逼的办法还是用“人工”智能的办法。
  • 整个复盘中最有用的几个点
    • 多任务学习
    • 集成方法
    • 错误样例分析:包括标注问题等
    • run_squad.py预处理和后处理的依据
  • 缺乏的是什么?
    • 这是一个法律文本的task,没有根据法律文本的特点制定模型,法律文本有什么特点呢?进一步,是否应该根据民事和刑事区分训练模型呢?
    • 对于人工标注不好的数据我们怎么应对这些噪声呢?特别是标注问题的第三点,是否能设计一个模型去解决这个问题?还是只能人工重新标注?
    • external knowledge,无论是推理能力还是法律常识,模型都很难去使用,这是现有阅读理解模型都很难解决的问题,而BERT是一个大型匹配网络。
    • 模糊性和精确性匹配的权衡,我记得我遇到过一个问题,只是因为问题中“遇到”改写成了“运到”,回答的答案就找不到北了,有点类似在squad对抗样本和ACL2019的论文谈论过的话题一样。
    • 加入条件限制的问题为什么回答的都不好?
    • ...

总之,人生就是这么奇妙

最后,感谢我的队友caldreaming提供了答案验证模块!

schedule

[ ] 后期预计会整理一下代码,给一个能直接跑通的代码。