最近刷到一篇论文,题目是Revisiting Few-sample BERT Fine-tuning 。论文刚挂到arxiv上,虽然关注的人还不是很多,但是读完之后发现内容很实用,很适合应用到实际的业务中。本文主要就这篇论文中的一些观点进行解读和实验验证。
话不多说,直接进入正题。这篇论文主要探讨的主题是如何更有效地使用bert在小数据集上进行finetune。论文指出目前bert的finetune存在不稳定的问题,尤其是在小数据集上,训练初期,模型会持续震荡,进而会降低整个训练过程的效率,减慢收敛的速度,也会在一定程度上降低模型的精度。文章主要总结了三个优化的方向,分别从优化方法、权重参数、训练方式等角度探讨了如何在小数据集上稳定finetune bert模型。下面将分别从这三个的角度详细解读。
Adam优化的debiasing
不知道大家在使用tensorflow或者pytorch版本的官方bert源码时,有没有发现他们的Adam实现源码与原版的Adam实现略有不同。我们先来简单回顾一下Adam算法的流程:
![126876571f6f0ab542c32466ef5c9998.png](https://i-blog.csdnimg.cn/blog_migrate/365685978e1b6f25395dfe474c7263a8.jpeg)
adam主要是结合了一阶动量、二阶动量滑动平均,并辅以learning rate的adaptive change,使得模型训练能够更加高效且能够自适应改变learning rate。除此之外,adam还有一个算法细节需要关注,即bias correcting。注意到上图中红色框标注的部分,在梯度更新操作之前,需要对一阶动量和二阶动量进行bias修正。这样做的原因在于adam的动量均是使用0来初始化。因此在模型训练初期以及指数衰减率超参数(
推导的主要逻辑在于建立二阶动量
首先根据上图的步骤8,可以将二阶动量
对上式两边同时求期望,可以得到:
(updated on 2020.06.17)新增内容:这里的推导我又研究了一下,最后看到一个网站上的回答有些道理,这里贴出来供大家参考:Understanding a derivation of bias correction for the Adam optimizer
首先要弄清楚
那么接下来还有一个问题就是
自我吐槽一下:高中数学全还给老师了,汗颜。。。
此时将
上述第二项等式通过将
其中,
这块推导由于本人数学能力不强,所以理解得不是很深入,欢迎数学不错的同学前来拍砖。
Bert的adam
我们查看google给出的官方bert源码工程(https://github.com/google-research/bert)中的optimization.py,在其AdamWeightDecay类中,可以看到其省略了上述偏移修正的步骤:
m
(updated on 2020.06.18)通过查阅Bert的原始论文,并没有发现作者在这块有具体的说明, 只能推测使用bert做预训练时,由于训练语料规模非常庞大,且训练的步数也是非常多,因此即使不做偏移修正,模型仍然能够在训练过程中慢慢保持稳定状态,且减去了偏移修正的计算量,整体的计算成本还降低了一些。 (这个推测不太靠谱,权当一个引子,能引出大家对这个问题的思考)
然而,如果在样本较少的下游任务场景下,仍然使用这种优化方式就会出现训练不稳定的问题。为了验证这个结论,论文作者做了细致的比对实验,他们在四个不同的数据集上,尝试了50种不同的随机种子,分别用带偏移修正的原始adam和不带修正的bertAdam去做finetune任务。实验结果分别从不同角度来验证上述的观点,比如下图:
![5ee05ba3190b0de7a8da5384f16bd673.png](https://i-blog.csdnimg.cn/blog_migrate/5fc50cf6b11bf419c02d4c397858cc94.png)
这是一个模型在不同数据集上的测试集效果箱线图,图中表明在四个数据集上,使用偏移修正的adam能够极大提升模型在测试集上的效果。
再看下面这个图:
![ef7ccab658c9de160411664fde2f7975.png](https://i-blog.csdnimg.cn/blog_migrate/1c6617c407f1bf6f6016a940e15a3540.png)
这张图反映了模型在小数据集RTE上的训练曲线。可以看到,使用偏移修正的Adam来finetune能够更快达到收敛,同时获得更小的loss。
再次验证
实践出真知,为了验证上述结论的有效性,我决定找一个小数据集进行实际测试。正好最近有一个ccks举办的实体识别比赛,名称是面向试验鉴定的命名实体识别任务。这个比赛的训练样本只有400条,实体类型有4种,足以称得上是小数据量了,正好可以拿来做实验。实验的模型主体是Bert+crf框架,超参数和随机种子都固定不变,唯一改变条件的就是是否使用偏移修正。
(updated on 2020.06.17)在tf的bert实现中,要将原始的偏移修正补充进去,要添加一定量的代码,主要是增加
class
主要关注_get_beta_accumulators,_finish,以及在各个_apply_方法中进行偏移修正的计算逻辑:
beta2_power
最后,根据实验结果,验证了上述结论的有效性。通过使用误差修正,模型训练效率显著提升,只用了一半的训练步数就达到了未用误差修正的训练loss,相当于加快了收敛的速度。最终模型的精度也有小幅的提升。
(update on 2020.06.18)
(上述实验均基于bert4keras框架,tf的代码虽然初步测试通过,但是目前在一些数据上面测试时有一些bug,仅供参考)
这里提一下苏神的bert4keras框架中,很早就注意到了误差修正这个问题,增加了误差修正的选项和步骤,有兴趣的同学可以去研究一下该框架。建议对adam理解不深入的同学阅读苏神的代码,很简洁易懂,而tf中的源码为了优化图计算写了很多复杂代码。
在bert4keras中,具体的内容在optimizers.py的Adam类中,具体代码如下:
with
Weight Re-initializing
论文提到的第二个优化点是权重再初始化。我们使用bert做finetune时,通常会使用bert的预训练权重去初始化下游任务中的模型参数,这样做是为了充分利用bert在预训练过程中学习到的语言知识,将其能够迁移到下游任务的学习当中。众所周知,bert主要由很多transformer层堆叠构成,那么问题来了,是否所有的transformer层都对下游任务有帮助呢?
之前有一些论文专门讨论了bert中不同层的权重分别学习到了哪些信息,大致思想是靠近底部的层(靠近input)学到的是比较通用的语义方面的信息,比如词性、词法等语言学知识,而靠近顶部的层会倾向于学习到接近下游任务的知识,对于预训练来说就是类似masked word prediction、next sentence prediction任务的相关知识。当使用bert预训练模型finetune其他下游任务(比如序列标注)时,如果下游任务与预训练任务差异较大,那么bert顶层的权重所拥有的知识反而会拖累整体的finetune进程,使得模型在finetune初期产生训练不稳定的问题。
因此,我们可以在finetune时,只保留接近底部的bert权重,对于靠近顶部的层的权重,可以重新随机初始化,从头开始学习。论文做了如下实验来验证上述结论:重新初始化bert的pooler层(文本分类会用到),同时尝试重新初始化bert的top-L层权重,
![da311caa588f1c864d15d40b12d1b0ea.png](https://i-blog.csdnimg.cn/blog_migrate/276f9745c466d1905126e9b30b67606a.png)
![ce3f09090739d06f738391109a4a7ae1.png](https://i-blog.csdnimg.cn/blog_migrate/6cef71274b9e72f37cbb0bea59237861.jpeg)
根据上述实验结果,在四个数据集上,模型通过重新初始化部分权重,在精度上都有不同程度的提升。另外,作者还做了一个实验验证到底该对多少层的权重进行重新初始化。实验结果表明这个并没有显著规律,实际上初始化层数与具体的任务和数据集相关的,需要通过调参来决定。但是有一点是可以肯定的,对于需要用到pooler层的分类任务,对pooler层进行重新初始化肯定能对模型的训练有一定的帮助。
再次验证
同样的,我也在ccks的实体识别比赛中验证了上述的想法。通过固定其他参数(包括不使用偏移修正的Adam),我对bert的前6层进行了重新初始化,具体代码实现只需要在modeling.py中的get_assignment_map_from_checkpoint方法中,将需要重新初始化的权重层参数从assignment_map中过滤掉就可以了,具体如下:
def
通过实验,验证了上述的结论。将bert顶部的6层权重重新初始化后,模型的训练效率有了较大提升,收敛速度加快了30-40%,然而最后模型的精度似乎没有太大的变化,应该还是需要根据验证集来调整最合适的重新初始化层数,才能达到精度的提升。
用更长的步数来finetune
这块优化内容我感觉似乎没有太大的亮点。作者的意思是通过增加训练步数能够提升finetune的效果。但是一般我都是用early-stopping机制来控制训练的步数,因此感觉这块内容帮助不大,这里我就不过多介绍了。
更多对比实验
论文在最后还做了一组对比实验,他将目前几个比较经典的解决训练震荡的方法列了出来,具体如下:
1、Pre-trained Weight Decay,传统的weight decay中,权重参数会减去一个正则项
2、Mixout。在finetune时,每个训练iter都会设定给一个概率p,模型会根据这个p将模型参数随机替换成预训练的权重参数。这个方法主要是为了减缓灾难性遗忘,让模型不至于在finetune任务时忘记预训练时学习到的知识。
3、Layerwise Learning Rate Decay。这个方法我也经常会去尝试,即对于不同的层数,会使用不同的学习率。因为靠近底部的层学习到的是比较通用的知识,所以在finetune时并不需要它过多的去更新参数,相反靠近顶部的层由于偏向学习下游任务的相关知识,因此需要更多得被更新。
4、Transferring via an Intermediate Task。即在finetune一个小样本数据集任务时,先在一个较大的过渡任务上进行finetune。
作者将上述四个方法与本论文中的几个优化点做了对比实验,最后发现相对于只使用偏移修正的Adam优化算法,Pre-trained Weight Decay、Mixout、Layerwise Learning Rate Decay并没有显著的优势。当结合了偏移修正和权重重新初始化之后,上述三个方法的效果是明显有差距的。而对于Transferring via an Intermediate Task,虽然它的效果很好,但是它需要额外的标注数据,成本比较高。而且我自己也做了一些验证测试,我使用了MSRA的中文NER数据集先做了finetune,然后再用其权重参数尝试了ccks的NER任务,结果并没有得到明显的提升,个人认为这个过度任务可能需要与目标任务的领域有一定的相关性,不然还需要做领域迁移的工作。
小结
本文主要解读了论文Revisiting Few-sample BERT Fine-tuning。通过深入研究bert在finetune小样本数据集时遇到的训练不稳定问题,提出了几个优化方法,包括使用带偏移修正的adam优化方法、重新初始化部分权重参数等。作者做了详尽的实验来验证上述方法,同时本人也在一个小样本任务上做了简单的二次验证,最终证明上述方法是有效的。由于上述方法操作非常简便,对原始的代码改动很少,因此非常适合应用于实际的项目中。