比赛海报
这是Kaggle春节前结束的一个阅读理解的比赛[1],我和管老师曹老师最终获得16/1233的成绩。成绩来自于管老师的提交,我自己的最好成绩大概排在23名的样子,不好不坏,略低于我们的预期。
这次比赛的数据集来自于Google,名为Natural Questions,简称NQ。数据集早在19年初就已经公布,在官网[2]上还有排行榜。
这个数据集和SQuAD挺像的,关于SQuAD的介绍大家可以在这篇文章中找到。NQ的训练集包含30多万个样例,每个样例包含一篇来源于维基百科的文章和一个问题。每篇文章可以被分为多个“候选长答案”,所谓候选长答案,可能是一个段落、一张表格、一个列表等等。候选长答案有可能有包含关系,但大部分的标注出来的长答案(95%)都是顶层候选长答案。在所有样例中,有大约一半样例的问题可以用候选长答案来回答。对于有的问题,还可以用更加简短的文章区间来回答,这种区间称为短答案。大约有三分之一的样例可以用短答案来回答。短答案并不一定是一个连续区间,有可能是多个离散的区间。
从上面的描述可以看出这个数据集比SQuAD复杂不少,大家可以到这个页面[3]看一些官方提供的可视化样例。由于复杂,NQ的难度也比SQuAD大不少,我认为主要体现在两点:
Kaggle这次的比赛形式是Kernel赛,允许参赛者线下训练,但必须在线上完成测试集的推理,推理时间限制为2小时。
谷歌针对这一数据集提供了一个开源的Baseline模型[4],并且有一篇简短的论文[5]介绍这个模型。这里需要介绍一下它,因为它真的是一个很强的基线模型。
模型叫做BERTjoint,joint的含义是用单个模型完成长答案和短答案的寻找。这个模型的基础是BERT,输入进BERT的数据由问题和文章正文用[sep]拼接而成。因为文章正文很长,所以使用了滑窗的方式截取正文,默认的步长是128个token。由于问题长度几乎都很小,所以如果模型的输入长度是512个token的话,大概每个token会出现在四个滑窗样本(chunk)里。
具体的数据集构造过程大概如下:
训练的方法比较简单,对于找开头和结尾这个标准阅读理解任务,直接在BERT后面加了一个全连接,然后对概率在句子维度上做softmax;对于答案类别分类则是在[CLS]的输出后面跟一个全连接。论文中指出,训练1轮的效果是最好的,我们在训练时也使用了这个设定。
baseline使用的模式和这张图非常像,只是扩展了可回答性的部分
推理过程如下:
可以看出,由于输入输出明确,模型其实是整个pipeline里最简单的部分。而对于长文本、长短答案、特殊答案类型的处理大多是有数据预处理和后处理来完成的。其实这也说明一个道理,模型能力不是一个算法工程师最重要的能力,它们已经被封装得越来越好,使用门槛越来越低。
当时看了Baseline之后我感觉这个解法有几个比较明显的不足:
针对这几个方面,我决定走一条和baseline不同的道路,主要操作如下:
由于我不会用TensorFlow,所以我花了很长时间把相关代码写成pytorch;整个调模型的过程也并不顺利,一方面是训练所需时间特别长,另一方面是因为我很晚才建立起比较稳健的线下验证体系。我的LB成绩一直比曹老师的baseline模型低很多,这让我有点慌了阵脚。赛后回看,曹老师的模型很严重地过拟合了LB,这也是比较诡异的一件事情。当时我们就发现了这种可能性,所以我也很乐观地相信我们前面有队伍肯定过拟合了,并在结束前把队名改成了“Shake up过新年”。虽然没有在B榜上升到金牌区,但确实上升了10名。
最终我最好的成绩来自一个BERT Large WWM SQuAD和Roberta SQuAD的融合,线下F1 是0.66,线上LB 0.638,private score 0.670。两个单模的线下CV都是0.65左右,融合带给我大概0.01的boost。
这次比赛里我踩的一个大坑是我前期为了加快训练,一直使用的是基于序列长度的Bucket Sampler。这个trick使我的训练速度提高了一倍,但模型的性能却大打折扣。一开始我并不知道有这个情况,直到最后几天我为了使用多卡训练的distributedSampler而无法使用这个技巧之后成绩突然猛增,我才意识到这个问题。后来回想,由于我是对单个长答案进行直接滑窗,不同训练样本的非padding长度差异比较大,这么训练很可能带来bias。但这个技巧在提升推理速度方面是很有成效的,后面有机会的话给大家介绍这个技巧。
这次比赛的另一大遗憾是队伍里的模型没有很好地融合起来。这是因为我们一开始没有考虑到这一题的特殊性带来的融合难度。首先我们都没有做检索用的快速模型,这导致我们推理的时间都比较长,即使融合也只能塞2-3个模型。第二是由于大家的预处理不太一样,输入token层面就产生了差异,需要提前将预测结果映射回word空间再进行融合,但我们一开始没有想到这点,到后面已经来不及了。
做的比较好的地方也有一些,例如在比赛最后几天Huggingface放出了他们Rust实现的新版Tokenizer,比原来快了好几倍,我在第一时间使用了这个库;预处理的代码经过精心考虑,做了很多缓存,大大加快了速度等等。
总体来看大家都没有超出baseline的模式,但还是有很多值得借鉴的地方。曹老师在讨论区开了一个帖子[6],收集了所有的金牌方案
很开心地看到Guanshuo老师使用了和我一样的数据生成模式,但他做了很重要的一步就是hard negative sampling。他先用一个模型得到了训练集所有候选长答案的预测概率,然后根据这个概率来对负样本进行采样。每个模型训练了3-4个epochs。相比于我们直接随机采样负样本(baseline只保留了2%的负样本)这种方式更能保留有训练价值的数据。这个技巧应该会在后面的比赛中被越来越多地使用。
大佬的另一个牛逼之处就是由于做了上面的操作,他可以在推理的时候很自然地也引入这个机制。他使用一个bert base来事先筛选出可能性较高的候选长答案,然后再用大模型求短答案。因此他在最后融合了惊人的5个模型!而且推理时间只有1小时。这个方式我们也想到了,但是在线下实验的时候可能由于代码写错了没有成功。
在训练方面,预测开始和结束位置的loss只有在训练样本为正样本时才会被计算。
老师的方法很朴素,几乎跟baseline一样。他说他的关键点也是采样,他调高了负样本的保留概率。
融合了3个模型,3个模型在训练时的主要差别是使用了不同的滑窗步长生成的数据以及略有差异的训练目标。
一方面提高了负样本比例,另一方面使用了知识蒸馏。知识蒸馏之后的学生模型在验证集上相比教师模型有0.01的boost。
可能是由于运算时间的限制或者评测数据集不一样,我发现Kaggle竞赛冠军的成绩也没有达到官网Leaderboard最顶尖的水平。但很神奇的是这个task几乎没人发paper,所以无从知晓排行榜上大佬们做了什么操作。如果大家对阅读理解有兴趣,可以尝试一下这个任务。不像SQuAD里机器已经大幅超越人类,在这里目前机器的水平离人类还有较大差距,巨大的空间等着大家探索。
[1]
比赛链接: "https://www.kaggle.com/c/tensorflow2-question-answering/overview"
[2]
Natural Questions官网: https://ai.google.com/research/NaturalQuestions
[3]
数据可视化页面: https://ai.google.com/research/NaturalQuestions/visualization
[4]
Baseline模型: https://github.com/google-research/language/tree/master/language/question_answering
[5]
Baseline论文: https://arxiv.org/abs/1901.08634
[6]
已公开金牌方案: https://www.kaggle.com/c/tensorflow2-question-answering/discussion/127409