隐藏在动量中的梯度累积:少更新几步,效果反而更好?
By 苏剑林 | 2021-08-24 | 35230位读者 |我们知道,梯度累积是在有限显存下实现大batch_size训练的常用技巧。在之前的文章《用时间换取效果:Keras梯度累积优化器》中,我们就简单介绍过梯度累积的实现,大致的思路是新增一组参数来缓存梯度,最后用缓存的梯度来更新模型。美中不足的是,新增一组参数会带来额外的显存占用。
这几天笔者在思考优化器的时候,突然意识到:梯度累积其实可以内置在带动量的优化器中!带着这个思路,笔者对优化了进行了一些推导和实验,最后还得到一个有意思但又有点反直觉的结论:少更新几步参数,模型最终效果可能会变好!
注:本文下面的结果,几乎原封不动且没有引用地出现在Google的论文《Combined Scaling for Zero-shot Transfer Learning》中,在此不做过多评价,请读者自行品评。
SGDM #
在正式讨论之前,我们定义函数
χt/k={1,t≡0(modk)0,t≢0(modk)
也就是说,t是一个整数,当它是k的倍数时,χt/k=1,否则χt/k=0,这其实就是一个t能否被k整除的示性函数。在后面的讨论中,我们将反复用到这个函数。
现在,我们来讨论带动量的梯度下降SGDM,它的一般形式为
{mt=βmt−1+(1−β)gtθt=θt−1−αtmt
这里gt是第t步的梯度,θ是参数,β是滑动系数。假设我们累积k步梯度,那么实际上就是每k步才更新一次,然后每次更新都使用k步的梯度平均,即
{mkt=βmk(t−1)+(1−β)1kk∑i=1gk(t−1)+iθkt=θk(t−1)−αktmkt
我们可以将动量m的更新分拆开来:
mk(t−1)+1=βmk(t−1)+1k(1−β)gk(t−1)+1mk(t−1)+2=mk(t−1)+1+1k(1−β)gk(t−1)+2mk(t−1)+3=mk(t−1)+2+1k(1−β)gk(t−1)+3⋮mkt=mkt−1+1k(1−β)gkt
或者写成一个通式:
mt=[(β−1)χ(t−1)/k+1]mt−1+1k(1−β)gt
同理,参数θ的更新也可以分拆开:
θk(t−1)+1=θk(t−1)θk(t−1)+2=θk(t−1)+1⋮θkt−1=θkt−2θkt=θkt−1−αktmkt
或者写成一个通式:
θt=θt−1−χt/kαtmt
所以,带动量的梯度下降,如果要累积k步梯度,那么只需要按照如下方式更新:
{mt=[(β−1)χ(t−1)/k+1]mt−1+1k(1−β)gtθt=θt−1−χt/kαtmt
而不需要引入新的一组参数。
Adam #
对于Adam来说,它的更新公式如下:
{mt=β1mt−1+(1−β1)gtvt=β2vt−1+(1−β2)g2tˆmt=mt/(1−βt1)ˆvt=vt/(1−βt2)θt=θt−1−αtˆmt/√ˆvt+ϵ
动量m的处理方式同前述SGDM一致,所以关键在于二阶矩v。按照定义,在梯度累积k步的情况下,v的更新公式为
vkt=β2vk(t−1)+(1−β2)(1kk∑i=1gk(t−1)+i)2
很遗憾,由于平方的存在,v无法做到像式(4)那样的分拆,所以严格来说,不新增一组缓存参数是无法实现Adam的梯度累积的。
但是,如果我们假设“平均的平方”可以由“平方的平均”来估计,即假设
(1kk∑i=1gk(t−1)+i)2∼1kk∑i=1g2k(t−1)+i
这里的∼代表有比较紧密的线性相关关系,但不一定是近似相等,比如可以差一个倍数。那么我们依然可以像m那样对v的公式进行修改:
vt=[(β2−1)χ(t−1)/k+1]vt−1+1k(1−β2)g2t
综合起来,就得到修改后的Adam公式:
{mt=[(β1−1)χ(t−1)/k+1]mt−1+1k(1−β1)gtvt=[(β2−1)χ(t−1)/k+1]vt−1+1k(1−β2)g2tˆmt=mt/(1−βt/k1)ˆvt=vt/(1−βt/k2)θt=θt−1−χt/kαtˆmt/√ˆvt+ϵ
笔者实验后发现,这样修改后的Adam,确实能起到跟梯度累积相似的效果。
结论反思 #
总的来说,在上面的两个修改版优化器中,主要包含两个改动:
1、修改m和v的更新公式;
2、参数改为每k步更新一次(或者学习率由αt改为χt/kαt)。
其中m,v的更新公式改为了(不失一般性,以m为例):
mt=[(β−1)χ(t−1)/k+1]mt−1+1k(1−β)gt
如果要对应上原来的mt=βmt−1+(1−β)gt的格式,我们猜测上述迭代可能相当于将β换成了˜β=1−1k(1−β)。
事实上确实如此,我们可以证明将β换成˜β=1−1k(1−β)后的滑动平均动量,近似于原始β时累积k步梯度的滑动平均动量:
mkt=˜βmkt−1+(1−˜β)gkt=˜β2mkt−2+˜β(1−˜β)gkt−1+(1−˜β)gkt=⋯=˜βkmk(t−1)+(1−˜β)k∑i=1˜βi−1gkt−i+1
由于˜β通常相当接近于1,所以我们近似认为˜βi−1≈1,∀i=1,2,⋯,k,并且
˜βk=(1−(1−˜β))k≈1−k(1−˜β)=β
所以有近似
mkt≈βmk(t−1)+(1−˜β)k∑i=1gkt−i+1=βmk(t−1)+(1−β)1kk∑i=1gkt−i+1
这就是梯度累积形式。
所以,如果你面临着batch_size比较小而导致性能不好的问题,但又不想自己实现梯度累积或者修改优化器,那么可以尝试如下操作:
1、将所有β参数改为1−1k(1−β);
2、学习率乘以χt/k,使得参数每k步才更新一次。
有意思的是,笔者发现第2点比第1点更重要:哪怕我们不修改β,直接将参数更新频率改为每k步更新,效果也有所改善(前提是batch_size小是模型当前瓶颈)。这就是标题所提到的“反直觉”现象:
啥都不用改,少更新几次,效果反而更好了。
文章小结 #
本文介绍了笔者在调试模型中的一个发现——梯度累积隐藏在优化器的动量中,然后对此作了进一步的简单分析和实验,发现通过调整滑动系数和学习率也能达到近似的梯度累积效果,进而发现“少更新几次,效果反而更好”的反直觉现象。
转载到请包括本文地址:https://kexue.fm/archives/8634
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Aug. 24, 2021). 《隐藏在动量中的梯度累积:少更新几步,效果反而更好? 》[Blog post]. Retrieved from https://kexue.fm/archives/8634
@online{kexuefm-8634,
title={隐藏在动量中的梯度累积:少更新几步,效果反而更好?},
author={苏剑林},
year={2021},
month={Aug},
url={\url{https://kexue.fm/archives/8634}},
}
August 25th, 2021
点赞苏神的推导!
感觉这个结论根 Lookadead 优化很像,像是个简化版的lookahead
August 27th, 2021
有点pytorch里面的,多次back,一次step的感觉
pytorch里面的“多次back,一次step”就是梯度累积的实现啊。但这里的主要目的不是介绍如何实现梯度累积,而是通过分析梯度累积的等价实现来推出一些启发性的结果。