向AI转型的程序员都关注了这个号👇👇👇
在知乎上看到2022搜狐校园NLP算法大赛情感分析第一名方案分享分享,觉得方案非常简单优雅,同时又有点prompt learning的意味在里面(严格来说不是prompt learning),并且效果非常好。虽然在他们的方案分享中也给出了比较详细的思路和基于pytorch-lightning的代码,但是有一些细节的地方还不够清楚,同时代码也不太容易理解,因此在博客中做更加清晰的说明和分享更加简洁(更好理解基于torch)的代码。
一、比赛和方案理解
这次比赛的任务是——面向实体对象的文本描述情感极性及色彩强度分析。情感极性和强度分为五种情况:极正向、正向、中立、负向、极负向。选手需要针对给定的每一个实体对象,从文本描述的角度,分析出对该实体的情感极性和强度。
数据如下:
针对上面数据中的content文本和给出的entity,分析出它们分别在content中包含的情感色彩。很明显这是一个分类任务,我当时看见这个赛题的时候,头脑中闪现出的解决方案就是和他们给出的baseline一模一样:
按照上面把content和每个entity拼接起来后,送入bert模型提取句向量,然后过分类器,这样就完成了这个任务,这个方案在比赛中也有人使用据说效果不是很理想。下面来看看比赛第一名的方案:
baseline的缺陷
如下图(引用比赛作者方案分享中的图)
因为每条数据的实体数据不相等,所以如同baseline那样的拼接方案,会导致模型见到content文本次数不一样,对最终的效果可能会有影响;同时把每一条数据复制了entity数量次,导致训练数据过多,效率比较低下。还有一个问题就是,模型得到的句向量的选择也会有一定的误差,baseline的方案中最后要么去cls或者所有token的embedding做meanPooling,这样也会对最后的结果产生一定的影响;最后就是那每个实体单独拼接,感觉有点弱化了每个实体间的联系,对最后的结果会产生一定的影响。
第一名的方案
如上图(引用比赛作者方案分享中的图),把每条数据中的实体用[MASK]拼接起来,然后和content文本使用[SEP]拼接起来,这样就可以高效的在一条数据中构建一个分类任务,而不需要如果baseline那样对每一条数据重复多次。同时这里也避免了最后句向量的取舍问题,直接把[MASK]处对应的embedding作为每个实体情感的分类embedding。这个方案中[MASK]的引入,也有一点prompt learning的意味在里面,效果上作者说比较好。另一方面,它又不是严格的prompt learning,它不需要预测出[Mask]处具体的token是什么,然后做类别映射,也就是不需要做Prompt 答案空间映射(Verbalizer)的构造,只是做了一个Prompt 模版(Template)的构造。
一些补充的思考
要说这个方法为什么会有用,我一开始推测是因为考虑了实体之间的潜在关系,而且对数据分布的假设更加合理。
后来决赛答辩的时候听到有选手提到这个数据存在leak,也就是在数据中标签非0的实体会被排在前面,标签为0的实体会被排在后面。我突然就觉得这可能就是这个方法提升巨大的真正原因,用了这个方法之后,相当于模型从中学到了一个bias,就是靠近文本末尾的实体,标签为0的可能性更大。
另外,在比赛中期,“灵境”组在讨论区公开了一个方案,我们发现该方案的核心思路和我们不谋而合。在该方案公开后很多队伍的分数都追上来了,在决赛答辩过程中我也发现很多高分团队都搬运了这套方案。公开的方案和我们做法基本一致,不过使用了一个含有MLM的全套BERT类模型,第二段文本(在该方案中被称为Prompt)的形式为:“在这句话中,<实体1>是[MASK],<实体2>是[MASK]......”,然后MLM头输出词表大小维度(21128)的向量,取五个Token作为Verbalizer(坏、差、平、行、好),分别对应五个情感极性标签,忽略其他的Token。
然而,这套方案和我们的做法还存在一定差别,这也是我认为该方案在这个任务上存在的一些问题:
- 我们不称输入的第二段文本为"Prompt",因为这容易和Prompt Tuning概念中的Prompt混淆。该任务并不适合Prompt Tuning范式,而仍然是采用普通的对全模型进行参数更新的Full Tuning范式。因此在该题中,“Prompt”的形式如何并不重要,增加一些没什么用的词反而会挤占第一段文本的输入长度。
- 该方案使用了BERT的MLM头进行分类,21128维的词表中只有五个Token映射到有效标签,其余Token都被忽略。这和我们的方案在结构上基本等价,唯一的区别是该方案有MLM头的参数初始化而我们的分类层为随机初始化,这个区别是否会带来性能提升不知道,但是直观的是模型增加了至少768*21123=16M(或者1024*21123=22M)的无用参数量,在题目有模型总大小限制的情况下这意味着可以融合的模型变少了。
模型优化
针对上述提出的模型,我们进行了很多优化尝试,下面主要讨论上分较多的技巧,没什么用的东西就在最后放一小节补充说明。很多优化技巧都会导致训练或测试阶段时空开销大大提升,比赛时还是应该视情况使用。
线下数据划分方式
队友发现,初赛阶段使用前90%数据训练,后10%验证,可以取得最好的线上效果,随机10%效果会变差一些,增加训练数据也不能使效果变好。复赛阶段使用了同样的数据划分方式。
对抗训练(FGM)
在各类文本分类任务中,常用的提升NLP模型训练鲁棒性和泛化能力的有效方法。简单来说是在Embedding层的参数有一定程度扰动时也尽量保证模型能分类正确。事后估计初赛线上提升1%左右。
参考了这篇知乎文章的实现方法:Nicolas:【炼丹技巧】功守道:NLP中的对抗训练 + PyTorch实现
模型平均 (SWA)
对训练过程中的多个checkpoint进行权重平均,或许可以有助于模型收敛到loss landscape平坦区域的中心,提升模型的泛化能力。具体而言,我们在验证指标的最高点开始,将这一轮和到Early Stopping之前的各轮验证时,验证指标与最高点差值小于一定值的模型权重放进来平均。事后估计初赛线上提升1%左右。
模型融合
没什么好说的,几个模型预测的logits平均得到最终结果。值得注意的是这题有2G的模型总大小限制,因此我们需要考虑融合模型的异构度不能盲目做K折,最后融合了2个稍微异构的XLNet-Mid + 1个MacBERT-Large + 1个RoBERTa-Large,全部保存为FP16格式,模型文件总大小2043M正好小于2G。估计初赛提升大约1%,复赛提升大约2%。
伪标签
在模型融合的基础上,使用融合模型预测的测试集标签作为伪标签,将测试集数据加入训练集中再次训练模型。在复赛中,我们为了避免多模型在测试集上的预测结果失去异构性,我们没有把全部测试数据都加入训练集,而是四个模型预测结果投票,大于等于三个模型预测一致的数据才会被加入训练集。这个训练集会重新被用于训练四个模型,然后重新进行融合。复赛在模型融合基础上还有1%左右的提升。
复赛数据适配
如图所示。在复赛开始的时候,起初我们使用初赛训练集+复赛训练集的全量训练数据对模型进行训练,结果发现效果不好。后来发现复赛数据相比初赛数据的分布可能发生了较大的偏移,因此我们考虑用初赛训练好的模型的权重来对模型进行初始化,然后只在复赛数据集上训练。相比全量数据训练提升近3%,验证了我们的猜想。
总体行来说,这个方案确实比较优雅,当然效果也比较好,让人一看就有点耳目一新的感觉。当然看论文(prompt learning)比较多的话,应该也能想到类似的方案。代码上实现的一些细节——矩阵的维度变换,给一个更加清晰的说明,理解整个方案就更加的容易了。
数据维度变化
一个batch的数据
二、代码实现
第一名代码
- 关注微信公众号 datayx 然后回复 搜狐 即可获取。
作者给出了基于pytorch-lightning的代码,我认为封装的比较高了,不太容易理解,在此基础上,我实现了一版基于torch的代码:
模型代码
参考文章
https://zhuanlan.zhihu.com/p/533808475