我们知道,梯度累积是在有限显存下实现大batch_size训练的常用技巧。在之前的文章《用时间换取效果:Keras梯度累积优化器》中,我们就简单介绍过梯度累积的实现,大致的思路是新增一组参数来缓存梯度,最后用缓存的梯度来更新模型。美中不足的是,新增一组参数会带来额外的显存占用。

这几天笔者在思考优化器的时候,突然意识到:梯度累积其实可以内置在带动量的优化器中!带着这个思路,笔者对优化了进行了一些推导和实验,最后还得到一个有意思但又有点反直觉的结论:少更新几步参数,模型最终效果可能会变好!

注:本文下面的结果,几乎原封不动且没有引用地出现在Google的论文《Combined Scaling for Zero-shot Transfer Learning》中,在此不做过多评价,请读者自行品评。

SGDM #

在正式讨论之前,我们定义函数
χt/k={1,t0(modk)0,t0(modk)


也就是说,t是一个整数,当它是k的倍数时,χt/k=1,否则χt/k=0,这其实就是一个t能否被k整除的示性函数。在后面的讨论中,我们将反复用到这个函数。

现在,我们来讨论带动量的梯度下降SGDM,它的一般形式为
{mt=βmt1+(1β)gtθt=θt1αtmt


这里gt是第t步的梯度,θ是参数,β是滑动系数。假设我们累积k步梯度,那么实际上就是每k步才更新一次,然后每次更新都使用k步的梯度平均,即
{mkt=βmk(t1)+(1β)1kki=1gk(t1)+iθkt=θk(t1)αktmkt

我们可以将动量m的更新分拆开来:
mk(t1)+1=βmk(t1)+1k(1β)gk(t1)+1mk(t1)+2=mk(t1)+1+1k(1β)gk(t1)+2mk(t1)+3=mk(t1)+2+1k(1β)gk(t1)+3mkt=mkt1+1k(1β)gkt

或者写成一个通式:
mt=[(β1)χ(t1)/k+1]mt1+1k(1β)gt

同理,参数θ的更新也可以分拆开:
θk(t1)+1=θk(t1)θk(t1)+2=θk(t1)+1θkt1=θkt2θkt=θkt1αktmkt

或者写成一个通式:
θt=θt1χt/kαtmt

所以,带动量的梯度下降,如果要累积k步梯度,那么只需要按照如下方式更新:
{mt=[(β1)χ(t1)/k+1]mt1+1k(1β)gtθt=θt1χt/kαtmt

而不需要引入新的一组参数。

Adam #

对于Adam来说,它的更新公式如下:
{mt=β1mt1+(1β1)gtvt=β2vt1+(1β2)g2tˆmt=mt/(1βt1)ˆvt=vt/(1βt2)θt=θt1αtˆmt/ˆvt+ϵ


动量m的处理方式同前述SGDM一致,所以关键在于二阶矩v。按照定义,在梯度累积k步的情况下,v的更新公式为
vkt=β2vk(t1)+(1β2)(1kki=1gk(t1)+i)2

很遗憾,由于平方的存在,v无法做到像式(4)那样的分拆,所以严格来说,不新增一组缓存参数是无法实现Adam的梯度累积的。

但是,如果我们假设“平均的平方”可以由“平方的平均”来估计,即假设
(1kki=1gk(t1)+i)21kki=1g2k(t1)+i


这里的代表有比较紧密的线性相关关系,但不一定是近似相等,比如可以差一个倍数。那么我们依然可以像m那样对v的公式进行修改:
vt=[(β21)χ(t1)/k+1]vt1+1k(1β2)g2t

综合起来,就得到修改后的Adam公式:
{mt=[(β11)χ(t1)/k+1]mt1+1k(1β1)gtvt=[(β21)χ(t1)/k+1]vt1+1k(1β2)g2tˆmt=mt/(1βt/k1)ˆvt=vt/(1βt/k2)θt=θt1χt/kαtˆmt/ˆvt+ϵ

笔者实验后发现,这样修改后的Adam,确实能起到跟梯度累积相似的效果。

结论反思 #

总的来说,在上面的两个修改版优化器中,主要包含两个改动:

1、修改mv的更新公式;

2、参数改为每k步更新一次(或者学习率由αt改为χt/kαt)。

其中m,v的更新公式改为了(不失一般性,以m为例):
mt=[(β1)χ(t1)/k+1]mt1+1k(1β)gt


如果要对应上原来的mt=βmt1+(1β)gt的格式,我们猜测上述迭代可能相当于将β换成了˜β=11k(1β)

事实上确实如此,我们可以证明将β换成˜β=11k(1β)后的滑动平均动量,近似于原始β时累积k步梯度的滑动平均动量:
mkt=˜βmkt1+(1˜β)gkt=˜β2mkt2+˜β(1˜β)gkt1+(1˜β)gkt==˜βkmk(t1)+(1˜β)ki=1˜βi1gkti+1


由于˜β通常相当接近于1,所以我们近似认为˜βi11,i=1,2,,k,并且
˜βk=(1(1˜β))k1k(1˜β)=β

所以有近似
mktβmk(t1)+(1˜β)ki=1gkti+1=βmk(t1)+(1β)1kki=1gkti+1

这就是梯度累积形式。

所以,如果你面临着batch_size比较小而导致性能不好的问题,但又不想自己实现梯度累积或者修改优化器,那么可以尝试如下操作:

1、将所有β参数改为11k(1β)

2、学习率乘以χt/k,使得参数每k步才更新一次。

有意思的是,笔者发现第2点比第1点更重要:哪怕我们不修改β,直接将参数更新频率改为每k步更新,效果也有所改善(前提是batch_size小是模型当前瓶颈)。这就是标题所提到的“反直觉”现象:

啥都不用改,少更新几次,效果反而更好了。

文章小结 #

本文介绍了笔者在调试模型中的一个发现——梯度累积隐藏在优化器的动量中,然后对此作了进一步的简单分析和实验,发现通过调整滑动系数和学习率也能达到近似的梯度累积效果,进而发现“少更新几次,效果反而更好”的反直觉现象。

转载到请包括本文地址:https://spaces.ac.cn/archives/8634

更详细的转载事宜请参考:《科学空间FAQ》

如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。

如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!

如果您需要引用本文,请参考:

苏剑林. (Aug. 24, 2021). 《隐藏在动量中的梯度累积:少更新几步,效果反而更好? 》[Blog post]. Retrieved from https://spaces.ac.cn/archives/8634

@online{kexuefm-8634,
        title={隐藏在动量中的梯度累积:少更新几步,效果反而更好?},
        author={苏剑林},
        year={2021},
        month={Aug},
        url={\url{https://spaces.ac.cn/archives/8634}},
}