法律AI挑战赛 基础BERT到多任务学习的模型进化
本文记录了CAIL2019法律智能挑战赛的参赛历程。从基础BERT模型出发,逐步优化YES/NO问题和未知答案处理策略,最终发展为多任务学习的端到端模型。文章分析了数据集特点、模型迭代过程和技术难点,展示了AI竞赛中如何通过持续改进提升模型效果。
自从取了"like cxk"这个队名后,发现很多人和我有相似的想法,取名"不是cxk"、"唱跳rap篮球",还有一些"ikun"之类的,我们看起来就像是彼此的小号,哈哈哈哈。
这个比赛组织方提供了两个基准模型,在第一阶段的性能分别是:
在第一阶段,我直接采用了最简单的bert模型,使用的是huggingface设计的框架。当时这个框架的名字还是"pytorch_pretrained_bert",现在已经改名了。新版本进一步封装设计了整个流程,而当时的run_squad.py版本则是根据tf版本修改的,整个流程就是训练一次然后预测一次。所以我就略微改写了整个流程,在训练中验证模型的效果,保存性能最好的模型。当然理论上来说,这和原来版本保存最后三个模型相差不太多。
相对于squad2.0,这个比赛增加了YES/NO类型的问题。最初的版本,我先不考虑YES/NO类型,所以就在预处理阶段把这些问题去除,然后运行squad2.0版本的代码。结果出乎意料,f1值居然只有6点几。然后我就想,这个数据集这么难吗,连bert都回答不好?进一步分析数据后,发现很多文章长度都超过了512(bert的max_seq限制),于是我猜测可能是预处理出了问题。检查了一下代码,发现没有问题。之后我就放弃了这个比赛。
过了几天,闲着没事,我就把训练脚本中的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转成了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})])
接着我就简单调了调参数,然后按照上面的思路改写了训练流程,将数据预处理、模型、结果后处理和训练流程分为几个文件,修改了max_answer_length,设置了完整的steps或epochs,然后F1值大概提升到67.9。之后就再也提升不上去了,于是又玩了几天。 闲暇时,我重新回顾了比赛并分析了数据分布。YES/NO类型的问题约占总问题的12%,这意味着相比之前的模型,我有约12%的结果F1值为0。
因此,需要设计一个能处理YES/NO类型问题的模型,通常做法是在BERT输出上添加一个分类器(参考Hotpot数据集的基准模型)。但我懒得修改模型和输入输出,所以查看了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中含有反面(负面)词,则答案很可能是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"})
然后人生就遇到了瓶颈,无法进步,调整参数无效,规则也找不到门路。开始分析错误样例,沉迷于理清投保人、保险人、被保险人等之间的关系,分析了很多保险类的问题,一度觉得自己可以去卖保险了。也发现了数据集本身存在一些问题,主要有:
- 标注不正确:答案错误
- 标注不一致:比如同样的问题,有些标准答案是"300元",有些则是"300",而文中是"300元"。这种情况似乎普遍存在,在之前的xxx评测中,追一科技对超过1万个样本进行了重新标注。
- 标注质量不佳:这是一个比较致命的问题。例如,需要预测一个命名实体,比如"张3",而文中多次出现"张3",标注者给出的答案往往是"张3"第一次出现的位置。这会导致一个问题:我们的模型本质上是一个匹配模型,假设文章中出现"被告人李4和张3","被保险人为张3","张3"出现了两次,而对于问题"被保险人是谁?",标准答案标注的位置却是第一个被告人张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"这篇论文背景章节的介绍。在预测时传入阈值参数(null_score_diff_threshold=5),线上性能也有显著提升。至此,整套模型的性能接近官方BERT基准。
接着,又闲着没事就对错误样例进行分析,领悟了很多人生道理。在人类语言面前,模型就是人工"智障"。比如,一个问题加入了时间或场景限定,模型就立即迷失方向。对于多目标跟踪,如"犯罪分工情况",模型只能识别出"xxx望风",而答案是"xxx望风xxx抢劫"。由于刑事类文书的问题很多是描述性的,不像民事类那样找"实体"(是谁?是多少?),所以模型难以控制自己预测的答案是长还是短,是多还是少。模型无法解决的问题还有很多。尝试寻找规律,却发现还是太年轻。
又玩了几天,想着大概可以开始做集成了。而集成怎么做呢?最简单的方法就是对每个模型预测的最佳span的分数进行排序。这就是最原始版本的集成模型。可以参考"squad_2.0 ensemble",集成还是有一定效果的。第一阶段用了4个模型集成,成功超过了BERT基准(讯飞)。突然觉得好像要夺冠了?(想太多)
然后又玩了两周,第二阶段开始前,发现其他人已经刷到85分了!(后悔玩了两周)
第二阶段开始后,数据增加到了4万条,我们在这些数据上(按9:1比例分割)训练了之前的模型。提交后发现线上F1值只有75左右,落后其他参赛者很多。因此推测,其他人可能已经修改了原始的BERT模型,以适应未知词和是/否类型的问题。于是我们设计了几个方案:
关于第一个方案:我们不得不修改原有的模型,主要是在预处理和模型输入阶段。需要添加"是"、"否"和"未知"的标签。对于这三个类别,我们使用BERT在[CLS]位置的输出作为一个小型网络(全连接层)的输入,预测出3个logits,与原有的span logits拼接,进行softmax操作,然后计算损失。本质上这是一个非常基础的多任务学习,损失函数为loss=loss_span+loss_yes+loss_no+loss_unk。其实应该加入超参数设置,变成加权求和。当然,还需要在预测阶段添加"是"、"否"和"未知"的预测。经过这样的改造,我们完成了一个端到端的模型(无需额外处理是/否类型问题)。这个端到端模型在是/否类型的预测上,竟然能达到("是"95%以上,"否"40%以上)的效果,说明模型能够很好地捕捉到问题中"是否"这类关键特征。
同时,我们也更深入地检查了run_squad.py中预处理和答案后处理的代码。在原有的run_squad.py中,处理长文本时使用doc_stride间隔,将文章切成一段段作为新样本处理。在预测阶段,这可能导致一个词在多个片段中出现,而预测只需要这个词的一个概率。那么我们应该选择哪个片段中的这个词作为最终概率呢?这就是"_check_is_max_context"函数解决的问题。它的核心思想是,如果一个词在一段文本中的位置越靠中间,那么这个词包含的上下文信息就越多,因此在这个上下文场景下预测的这个词的得分就应该更可信。这说明了一个词的上下文语境的重要性,也解释了为什么语言模型比单纯的词向量效果好那么多。虽然原作者的这种做法无法证明,但感觉很有道理,难以反驳。而XLNet的作者对答案预测的后处理则采用了另一种思路,同样令人信服。
上述的端到端单模型在线上达到了78+的性能。这是在两张显卡,batch_size=12(每张卡batch_size=6)的情况下得到的结果。
接着,我们尝试了几种不同的预训练BERT模型,性能大致如下: bert-wwm > google > [ERNIE](>THU,因此之后我们一直使用bert-wwm这个预训练模型。 上述模型存在一个问题,yes、no和unk竟然共享了同一个输入和网络参数。YES/NO类型的问题也存 在一些unk情况,但YES/NO实际上能根据上下文推理出正确答案,即在文章中能找到一些evidence span作为支持,这与前面提到的利用预测span中的某些特征来预测NO的现象是一致的。而unk呢?通常无法在文章中找到有利的支持,因此导致无法回答这个问题。所以我们似乎至少应该使用两个小型网络分别处理yes/no问题和unk问题。基于此,我仍然使用[CLS]位置的输出作为输入来预测unk,而对于yes/no,则使用self attention+sum pooling的方式将整个序列的输出压缩成一个向量作为输入,用来预测yes/no。这种模型在线上能够达到79+的效果。
后来我查看了一些answer verified模块的论文,但都不太满意。有一天,我发现SOUGO在SMRCTOOKIT中发布了他们在coqa上使用的bert+answer verified模型,于是我参考他们的tf版本,改写了一个pytorch版本。由于这个数据集没有rational,所以略有不同。多次运行这个模型后,线上效果能够达到80左右。
考虑到K折交叉验证,我估计这个比赛最多支持3G的压缩包,而pytorch模型大约400M,所以做了一个8折的交叉验证。通过这样划分,训练了8个base模型,这些模型在线上基本维持在80左右(由于提交次数有限,没有一一验证)。
在集成方面,第一阶段的集成方法大概能达到80.8。原来的方法存在什么问题呢?因为有些问题是YES/NO或者是unk的,这种情况似乎用投票的方式来解决会更好。所以我修改了集成方法,假设一个样本有8个答案,如果yes、no或unk超过半数,则直接用投票的方式,而不是选择得分最高的作为答案。
同时,我与组里做阅读理解的专家讨论了几个集成方案:
实际上,第三个和第四个方案在本地效果差不多,但在线上第三个方案优于第四个方案。可能是因为第三个方案对yes/no/unk类问题更友好。最终这个集成方案在线上达到了F1值为81.7的效果。
接下来就是抽奖时间了,让模型运行,玩了两天,然后选择不同的模型组合,最终效果差不多达到了81.77。然而此时我遇到了瓶颈,其他人刷到了82、83,而我却再也提升不了了。
于是又到了"人工"智能时刻,我开始分析错误的案例,阅读了大量法律文书,学到了很多人生道理。我学习了很多法律知识和保险知识,然而我的模型却理解不了我的用心良苦。
最后几天,我只好做一些答案后处理的工作。虽然方法很低级,但确实提高了线上性能,F1值达到了81.815,这就是第二阶段的最终成绩。最后,主办方剔除了一些小号后,我的排名定格在了第4位。
突然就成为第三名了...可能是因为"like cxk"这个队名的魔力吧。
总之,人生就是这么奇妙
最后,感谢我的队友caldreaming提供了答案验证模块!
[x] 后期预计会整理一下代码,提供一个可以直接运行的版本。 更新 向各位道歉,当年忙于发表论文和找工作,最终没能提供一个完整的代码。现在的Hugging Face库已经非常完善,使用现在的库能很快实现类似的思路。如今大模型盛行,很多技巧已经失效了~ 欢迎大家用大模型来做机器阅读理解~
AI辅助编程,代码自动修复
Trae是一种自适应的集成开发环境(IDE),通过自动化和多元协作改变开发流程。利用Trae,团队能够更快速、精确地编写和部署代码,从而提高编程效率和项目交付速度。Trae具备上下文感知和代码自动完成功能,是提升开发效率的理想工具。
AI小说写作助手,一站式润色、改写、扩写
蛙蛙写作—国内先进的AI写作平台,涵盖小说、学术、社交媒体等多场景。提供续写、改写、润色等功能,助力创作者高效优化写作流程。界面简洁,功能全面,适合各类写作者提升内容品质和工作效率。
全能AI智能助手,随时解答生活与工作的多样问题
问小白,由元石科技研发的AI智能助手,快速准确地解答各种生活和工作问题,包括但不限于搜索、规划和社交互动,帮助用户在日常生活中提高效率,轻松管理个人事务。
实时语音翻译/同声传译工具
Transly是一个多场景的AI大语言模型驱动的同声传译、专业翻译助手,它拥有超精准的音频识别翻译能力,几乎零延迟的使用体验和支持多国语言可以让你带它走遍全球,无论你是留学生、商务人士、韩剧美剧爱好者,还是出国游玩、多国会议、跨国追星等等,都可以满足你所有需要同传的场景需求,线上线下通用,扫除语言障碍,让全世界的语言交流不再有国界。
一键生成PPT和Word,让学习生活更轻松
讯飞智文是一个利用 AI 技术的项目,能够帮助用户生成 PPT 以及各类文档。无论是商业领域的市场分析报告、年度目标制定,还是学生群体的职业生涯规划、实习避坑指南,亦或是活动策划、旅游攻略等内容,它都能提供支持,帮助用户精准表达,轻松呈现各种信息。
深度推理能力全新升级,全面对标OpenAI o1
科大讯飞的星火大模型,支持语言理解、知识问答和文本创作等多功能,适用于多种文件和业务场景,提升办公和日常生活的效率。讯飞星火是一个提供丰富智能服务的平台,涵盖科技资讯、图像创作、写作辅助、编程解答、科研文献解读等功能,能为不同需求的用户提供便捷高效的帮助,助力用户轻松获取信息、解决问题,满足多样化使用场景。
一种基于大语言模型的高效单流解耦语音令牌文本到语音合成模型
Spark-TTS 是一个基于 PyTorch 的开源文本到语音合成项目,由多个知名机构联合参与。该项目提供了高效的 LLM(大语言模型)驱动的语音合成方案,支持语音克隆和语音创建功能,可通过命令行界面(CLI)和 Web UI 两种方式使用。用户可以根据需求调整语音的性别、音高、速度等参数,生成高质量的语音。该项目适用于多种场景,如有声读物制作、智能语音助手开发等。
AI助力,做PPT更简单!
咔片是一款轻量化在线演示设计工具,借助 AI 技术,实现从内容生成到智能设计的一站式 PPT 制作服务。支持多种文档格式导入生成 PPT,提供海量模板、智能美化、素材替换等功能,适用于销售、教师、学生等各类人群,能高效制作出高品质 PPT,满足不同场景演示需求。
选题、配图、成文,一站式创作,让内容运营更高效
讯飞绘文,一个AI集成平台,支持写作、选题、配图、排版和发布。高效生成适用于各类媒体的定制内容,加速品牌传播,提升内容营销效果。
专业的AI公文写作平台,公文写作神器
AI 材料星,专业的 AI 公文写作辅助平台,为体制内工作人员提供高效的公文写作解决方案。拥有海量公文文库、9 大核心 AI 功能,支持 30 + 文稿类型生成,助力快速完成领导讲话、工作总结、述职报告等材料,提升办公效率,是体制打工人的得力写作神器。
最新AI工具、AI资讯
独家AI资源、AI项目落地
微信扫一扫关注公众号