神经网络的优化器
文章目录
- 神经网络的优化器
- GD 梯度下降算法
- 重球法
- SGD随机梯度下降
- Momentum动量梯度
- NAG(Nesterov accelerated gradient)
- AdaGrad(Adaptive gradient)
- RMSProp(Root mean square prop)
- Adam(Adaptive Moment Estimation)
- AdamW
- Adan(Adaptive Nesterov Momentum)
本片博客记录一下不同的神经网络的优化器
参考大神链接:神经网络的优化器_电器爆破专家的博客-CSDN博客
目录:
设置一些符号的含义如下:损失值为 ℓ ℓ ℓ,需要被更新的可训练参数为 w w w,使用 b b b 来泛指除 w w w 外的其它可训练参数,记 ℓ \ell ℓ 对 w w w 的偏导函数为 J ( w , b ) = ∂ ℓ ∂ w J(w, \, b)=\frac{\partial \ell}{\partial w} J(w,b)=∂w∂ℓ,该函数的自变量为全体可训练参数,记 g t = J ( w t , b t ) g_t=J(w_t, \, b_t) gt=J(wt,bt),设学习率为 λ \lambda λ(常取值为 0.01 0.01 0.01),优化器迭代的次数为 t t t。
GD 梯度下降算法
计算所有样本的预测值和真实值之间的差值作为损失值 ℓ \ell ℓ ,然后去更新当前网络的参数 w w w,每次更新完成,再次计算所有样本的损失值 ℓ \ell ℓ,再次计算梯度,更新权值 w,这样反复进行,知道真实值很预测值之间的差值小于跟定阈值,则停止迭代。
w t + 1 = w t − λ g t a l l w_{t+1} = w_t - \lambda \ g^{all}_t wt+1=wt−λ gtall
使用全部的样本使得更新的速度太慢,下面的随机梯度解决了这个问题,当然了还有很多的问题,慢慢来看
重球法
相比于传统的梯度法, 重球法在迭代中引入冲量 m t = w t − w t − 1 m_t = w_t−w_{t−1} mt=wt−wt−1, 即
m t = w t − w t − 1 w t + 1 = w t + β m t − λ g t m_t = w_t - w_{t-1}\\ w_{t+1} = w_t + \beta m_t - \lambda g_t mt=wt−wt−1wt+1=wt+βmt−λgt
因为引入了两个时刻之间权值的差值作为后一个时刻的一个权重,使得权重的更新更加稳定,会综合考虑的更多。然而, 重球法少被使用, 因为它可以被下面的性能更好的加速梯度下降法替代。与重球法齐名的冲量技巧——Nesterov冲量算法:
SGD随机梯度下降
随机梯度下降法(stochastic gradient descent, SGD)是原始 BP 算法提供的优化器,也是最早在深度学习中应用的优化器。其主要来源于梯度下降算法,但将其改编成不采用全部样本的损失值作为 loss ,而是采用部分样本的损失值作为loss,因为全部样本更新起来太慢了,其公式如下:
w t + 1 = w t − λ g t w_{t+1} = w_t - \lambda \ g_t wt+1=wt−λ gt
SGD 算法面临着诸多挑战:
- 当使用 SGD 下降到沟壑或盆地时,SGD 可能产生剧烈的抖动。一方面,抖动可能会使其跳出当前极小值,有机会找到更优的极小值;另一方面,抖动可能使得收敛速度减慢或无法收敛到极小值,此时只能通过手动降低学习率来降低抖动。研究者们最先提出了学习率计划表,为损失值设定阈值及其对应的学习率,当损失值下降到某一阈值时,启用该阈值对应的学习率。但学习率计划表,有针对性没有广泛性,对每一个数据集都需要编制其独有的学习率计划表。
- SGD 对于所有的可训练参数使用相同的学习率是不恰当的。我们不希望以同样的程度来更新所有参数,对于那些频繁更新的参数我们希望它每次更新能有一个较小的幅度,那些更新频率较低的参数我们希望它每次更新能有一个较大的幅度。
Momentum动量梯度
在沟壑中 SGD 会在沟壑两侧剧烈抖动,而在沟壑的下降方向移动十分缓慢。动量法(momentum)通过累积的方式,可以抑制在沟壑两侧方向上的抖动,在下降方向上使速度叠加。其公式如下:
m t = α m t − 1 + λ g t w t + 1 = w t − m t m_t =αm_{t−1} +λ \ g_t \\ w _{t+1} = w_t − m_t mt=αmt−1+λ gtwt+1=wt−mt
其中
α
\alpha
α 是新引入的常量参数(常取值为
α
=
0.9
\alpha=0.9
α=0.9),m_t 是为了实现算法而引入的变量。当
g
t
−
1
g_{t−1}
gt−1 的符号与
g
t
g_t
gt 的正负不同时,
m
t
m_t
mt 的累加就会使二者得到一定的抵消,即抑制抖动的作用;当
g
t
−
1
g_{t-1}
gt−1 的符号与
g
t
g_t
gt 的正负相投时
m
t
m_t
mt 的累加就会使二者叠加,即叠加速度的作用。

从上图可以看出 Momentum 在短时间内就将抖动抑制,而 SGD 抖动从未停止。并且 Momentum 对在沟壑下降方向上对速度的叠加效果也很明显,仅用 1426 轮迭代就走出了模型,而 SGD 使用了 14778 轮。
NAG(Nesterov accelerated gradient)
在传统凸优化领域,有一个与重球法齐名的冲量技巧——Nesterov冲量算法:
我们蒙着眼睛向前走时,总是伸出自己的两只手,探测自己的前方有无障碍物,以便及时更改前进方向。内斯特洛夫加速梯度(nesterov accelerated gradient,NAG)就使用了这种方法,而是使用前方的梯度来修正当前的前进方向。
其公式如下:
w t + 1 ′ = w t − α m t − 1 m t = α m t − 1 + λ g ( w t + 1 ′ , b t + 1 ′ ) w t + 1 = w t − m t w'_{t+1} = w_t −\alpha m_{t−1} \\ m_t =αm_{t−1} +\lambda \ g(w'_{t+1} ,b'_{t+1}) \\ w_{t + 1} = w_t − m_t wt+1′=wt−αmt−1mt=αmt−1+λ g(wt+1′,bt+1′)wt+1=wt−mt
其中 α \alpha α 是新引入的常量参数(常取值为 α = 0.9 \alpha=0.9 α=0.9 ), w t + 1 ′ w'_{t+1} wt+1′ 是假设的下一时刻已经更新好的权值, m t m_t mt 是为了实现算法而引入的变量。接下来我们将通过一幅示意图为读者介绍 NAG 的原理,以及其与 Momentum 的对比。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5c6YpzWU-1677582212079)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/0ee6e298-c662-4b4e-9e2a-39a25c654899/Untitled.png)]](https://img-blog.csdnimg.cn/24f961d88e74494bad3a1cc29be2e249.png#pic_center)
在上图中,Momentum 求当前点的梯度得到图中蓝色短线所示向量,然后再加上动量(图中蓝色长线所示向量)得到最终的更新向量,即图中紫色线所示向量;NAG 不再求当前点的梯度,而是求当前点加上动量所到达的点的梯度,即图中绿色短线所示向量,与动量复合即得到红色线所示的向量。最终 Momentum 将按照紫色向量更新,NAG 将按照红色向量更新。
Momentum的当前梯度为这一个时刻的梯度,加上之前的动量,nesterov 为下一个时刻的梯度加上之前的动量。
Nesterov冲量算法在光滑且一般凸的问题上,拥有比重球法更快的理论收敛速度,并且理论上也能承受更大的batch size。同重球法不同的是,Nesterov算法不在当前点计算梯度,而是利用冲量找到一个外推点,在该点算完梯度以后再进行冲量累积。
外推点能帮助Nesterov算法提前感知当前点周围的几何信息。这种特性使得Nesterov冲量更加适合复杂的训练范式和模型结构(如ViT),因为它并不是单纯地依靠过去的冲量去绕开尖锐的局部极小点,而是通过提前观察周围的梯度,调整更新的方向。
尽管Nesterov冲量算法拥有一定的优势,但是在深度优化器中,却鲜有被应用与探索。其中一个主要的原因就是Nesterov算法需要在外推点计算梯度,在当前点更新,期间需要多次模型参数重载以及需要人为地在外推点进行back-propagation (BP)。这些不便利性极大地限制了Nesterov冲量算法在深度模型优化器中的应用。
AdaGrad(Adaptive gradient)
前面我们提到为可训练参数设置相同的学习率是不合理的。自适应梯度(adaptive gradient, AdaGrad)提供了一种为参数动态调整学习率的方法。它为频繁更新的参数设置较低的学习率,为不经常更新的参数设置较高的学习率,从而使每个参数都有自己的更新幅度。其公式如下:
v t = v t − 1 + g t 2 w t + 1 = w t − λ v t + ϵ ⋅ g t v_t =v_{t−1} + g_t^2 \\ w_{t+1}=w_t-\frac{\lambda}{\sqrt{v_t+\epsilon}} \cdot g_t vt=vt−1+gt2wt+1=wt−vt+ϵλ⋅gt
其中为了避免分母为零而引入的常量参数
ϵ
\epsilon
ϵ (常取值为
ϵ
=
1
×
1
0
−
8
\epsilon=1 \times 10^{-8}
ϵ=1×10−8,
v
t
v_t
vt 是为了实现算法而引入的变量。
v
t
v_t
vt 一直在对
g
t
2
g_t^2
gt2 做累加,如果一个参数频繁更新必然会导致
v
t
v_t
vt 增大的幅度超乎寻常,那么
λ
v
t
+
ϵ
\frac{\lambda}{\sqrt{v_t+\epsilon}}
vt+ϵλ
就会超乎寻常的相应变小。这种方式也可以抑制抖动,即让那些梯度有剧烈变化的参数有一个较小的学习率。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lGITemPF-1677582212079)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/fab7a6db-8fa5-474f-994a-bac73b88e23d/Untitled.png)]](https://img-blog.csdnimg.cn/cc82988351974a2c9a1eabfa0947db9e.png#pic_center)
如图所示 AdaGrad 为 y 配置了较大的学习率,为 x 配置了较小的学习率,从而使其能够快速脱离马鞍。AdaGrad 仅迭代了 2519 轮,而 SGD 迭代了 125005 轮。
我们看到 v t v_t vt 一直在做正数累加,总体上会使全体参数的学习率趋向无穷小,在训练的后期会使模型的收敛速度变得极慢。不可否认的是,在训练的后期是需要降低学习率,从而稳定下降到极小值,避免在极小值处抖动,即使用退火学习率。笔者推测,AdaGrad 也是出于这种考量,使用正数累加的方式从总体上来降低学习率,让模型在训练后期稳定下降。但 AdaGrad 的现实表现却不尽如人意。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p6rUnWfx-1677582212079)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d40a8e9c-ebc7-4fb1-9660-a04cc0eb5cad/Untitled.png)]](https://img-blog.csdnimg.cn/36759ddcfd0948fab7c6b86001162ca0.png#pic_center)
我们可以看到,AdaGrad 在峡谷中十分稳定没有分毫抖动,但不断下降的学习率让它步履维艰,迭代了 100000 轮还没有走出峡谷。
若想深入了解该方法可查阅原始文献《Adaptive Subgradient Methods for Online Learning and Stochastic Optimization》
RMSProp(Root mean square prop)
均方根支撑(root mean square prop, RMSProp)是 Geoff Hinton 在他的课堂讲义中提出的一个尚未发表的方法。RMSProp 相对于 AdaGrad 单调减少的学习率有了很大改善,它的 v t v_t vt 不再是做正数累加,而是使用了衰减平均值,使其能够稳定在一定的范围之中。其公式如下:
v t = β v t − 1 + ( 1 − β ) g t 2 w t + 1 = w t − λ v t + ϵ ⋅ g t v t =βv_{t−1} +(1−β)g_t^2 \\ w_{t+1}=w_t - \frac{\lambda}{\sqrt{v_t + \epsilon}} \cdot g_t vt=βvt−1+(1−β)gt2wt+1=wt−vt+ϵλ⋅gt
其中常量参数 ϵ \epsilon ϵ 的作用及常用取值与 AdaGrad 一致,新引入常量参数 β \beta β 作为 v t v_t vt 的衰减系数(常取值为 β = 0.9 \beta=0.9 β=0.9 ), v t v_t vt 是为了实现算法而引入的变量。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-L1gPbQDg-1677582212080)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4b759127-3d7d-4f67-ab08-ffedf69cec05/Untitled.png)]](https://img-blog.csdnimg.cn/819b15633f0043a1be720d706859f198.png#pic_center)
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6rrr0agT-1677582212080)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ec287574-0d68-4474-96f3-6a51c33ddc81/Untitled.png)]](https://img-blog.csdnimg.cn/4eed9d0a187743e78912ef3f7e6c4aee.png#pic_center)
可以看到,无论是在马鞍上还是在峡谷中 RMSProp 在速度和抑制抖动方面都有着非常出色的表现。但细心观察会发现 RMSProp 在峡谷底部还是有细微的抖动,看来仅凭学习率来抑制抖动,还是无法做到根除。
若想深入了解该方法可查阅原始文献《rmsprop: Divide the gradient by a running average of its recent magnitude》
Adam(Adaptive Moment Estimation)
自适应矩估计(Adaptive Moment Estimation, Adam)是个缝合怪,它把 Momentum 和 RMSProp 缝合到了一起,使得它既有自适应调节学习率的能力,也有动量抑制抖动、叠加速度的能力。其表达式如下:
{ m t = α ⋅ m t − 1 + ( 1 − α ) g t v t = β ⋅ v t − 1 + ( 1 − β ) g t 2 w t + 1 = w t − λ v t 1 − β t + ϵ ⋅ m t 1 − α t \left\{ \begin{array}{rcl} m_t =\alpha⋅m_{t−1} +(1− \alpha)g_t \\ v_t =\beta ⋅v_{t−1} +(1−\beta)g_t^2 \end{array}\right. \\ w_{t+1}=w_t - \frac{\lambda}{\sqrt{\frac{v_t}{1-\beta^t}}+\epsilon} \cdot \frac{m_t}{1-\alpha^t} {mt=α⋅mt−1+(1−α)gtvt=β⋅vt−1+(1−β)gt2wt+1=wt−1−βtvt+ϵλ⋅1−αtmt
其中 α \alpha α 和 β \beta β 是用作衰减系数的常量参数(常取值为 α = 0.9 , β = 0.999 \alpha=0.9,\beta=0.999 α=0.9,β=0.999),常量参数 ϵ \epsilon ϵ 的作用及常用取值与 AdaGrad 一致, m t m_t mt 和 v t v_t vt 是为了实现算法而引入的变量。值得注意的是 Adam 的作者对 m t m_t mt 和 v t v_t vt 做了如下处理:
m t 1 − α t v t 1 − β t \frac{m_t}{1-\alpha^t} \\ \frac{v_t}{1-\beta^t} 1−αtmt1−βtvt
因为作者发现 m t m_t mt 和 v t v_t vt 在初始化时为零,所以在刚开始迭代时其值很小(特别是在衰减值设置的很大的时候)。所以作者加入,在刚开始迭代时使其得到适当放大。可以看到随着迭代次数的增加 1 − α t 1-\alpha^t 1−αt 与 1 − β t 1-\beta^t 1−βt 的值逐渐趋于 1 1 1,所以迭代次数达到一定值时,二者的影响就可以忽略不计了。
具体解释:
m t = α ⋅ m t − 1 + ( 1 − α ) g t m_t =\alpha⋅m_{t−1} +(1− \alpha)g_t mt=α⋅mt−1+(1−α)gt :代表当前梯度和当前动量的结合。
上面代表一阶动量:代表惯性,当前梯度更新的方向不仅要考虑当前梯度,还要 考虑历史梯度的影响;
v t = β ⋅ v t − 1 + ( 1 − β ) g t 2 v_t =\beta ⋅v_{t−1} +(1−\beta)g_t^2 vt=β⋅vt−1+(1−β)gt2 :代表当前自适应梯度的权值。
上面代表二阶动量:用于控制自适应学习率,二阶动量在后面被放置在分母的位置,其越大代表学习率越小,
二阶动量的物理意义:
- 对于经常更新的参数,不希望被单个样本影响太大,希望学习率慢一些。
- 对于偶尔更新的参数,希望能够从偶然出现的样本中多学习一些,也就是希望学习率大一点
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSD0FQhm-1677582212080)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/01175672-eaab-44a1-977b-1fbb6d8593f4/Untitled.png)]](https://img-blog.csdnimg.cn/9b575beb2ea74ede9be1619b350f0316.png#pic_center)
通过上图可以看到可以看到 Adam 相对于 RMSProp 在马鞍上的表现更为优秀,下降曲线也比较平滑。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7MXV6YGw-1677582212081)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b400185e-55d6-4d8c-8331-ee2fdb07706f/Untitled.png)]](https://img-blog.csdnimg.cn/d536c5c2be4345e995321ed7c95f7314.png#pic_center)
通过上图可以看到,虽然 Adam 的下降速度比 RMSProp 慢一些,但是在峡谷中没有像 RMSProp 一样发生抖动。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aWkuQN8d-1677582212081)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/46c6983f-73c1-4803-90cf-80b0812577f4/Untitled.png)]](https://img-blog.csdnimg.cn/60856f246fba46aa9fc25d142a241001.png#pic_center)
通过上图可以更直观的看出 Adam 的优势,Adam 经过 1379 轮迭代后下降到了最小值点,而 RMSProp 一直在最小值附近抖动,经过 100000 轮迭代还没有稳定下来。
若想深入了解该方法可查阅原始文献《ADAM: A METHOD FOR STOCHASTIC OPTIMIZATION》
但是这么好的算法,在提出之后,并没有像想象中的大放异彩,而是在各大论文中不断的被论证 Adam 的精度还会低于 SGD。让我们先来分析一下为什么会出现这种情况。
Adam 缺点分析:
其实Adam本身没有问题,问题在于目前大多数DL框架都是在优化器之前加上L2正则项来替代weight decay。
但是在 Adam 优化器的情况下,使用 L2 正则化来替代 weight decay 并不是等价的。
1、先看在 SGD 的情况下,L2 和 weight decay 是否等价的情况。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zwzc8ANr-1677582212081)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/9bb041bc-6bc9-4a96-bb1c-c67dfa7f654f/Untitled.png)]](https://img-blog.csdnimg.cn/d7ecfbc49c954a3cb69c7262599d2827.png#pic_center)
当 下面的学习率 λ ′ = λ α \lambda' = \frac{\lambda}{\alpha} λ′=αλ 你可以发现 ,上面使用 L2 正则化来替代 weight decay 是完全等价的。
2、在看看 Adam 的情况下,L2 和 weight decay 是否等价的情况。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1VsMbfAp-1677582212082)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/e52527a3-ebff-4822-9a96-4cd725739554/Untitled.png)]](https://img-blog.csdnimg.cn/1d14e9fd65d04cdea17c4ac859e13c9e.png#pic_center)
从上面可以看出来,只有当 M t = k I M_t = kI Mt=kI 的时候,L2 和 weight decay 是等价,但是这样就代表着,一阶动量要始终为 单位矩阵的时候,这样一阶动量就没有预先设想的那样,带来了很好的效果。
原因:
1、使用Adam优化带L2正则的损失并不有效。如果引入L2正则项,在计算梯度的时候会加上对正则项求梯度的结果 f t r e g ′ = f t ′ ( w ) + λ w f_t^{reg'} = f_t'(w) + \lambda w ftreg′=ft′(w)+λw 。
2、那么如果本身比较大的一些权重对应的梯度也会比较大,由于Adam计算步骤中减去项会除以梯度平方的累积开根号,使得减去项偏小。按常理说,越大的权重应该惩罚越大,但是在Adam并不是这样。分子分母相互抵消掉了。公式如下:
w t + 1 = w t − λ v t 1 − β t + ϵ ⋅ m t 1 − α t w_{t+1}=w_t - \frac{\lambda}{\sqrt{\frac{v_t}{1-\beta^t}}+\epsilon} \cdot \frac{m_t}{1-\alpha^t} wt+1=wt−1−βtvt+ϵλ⋅1−αtmt
假设 w t w_t wt 是比较大的,那么 我们会发现
g t = ℓ ′ ( w t , b ) + γ w t λ v t 1 − β t + ϵ ⋅ m t 1 − α t = λ v t 1 − β t + ϵ ⋅ β m t − 1 + ( 1 − β ) ( ℓ ′ ( w t − 1 , b ) + γ w t − 1 ) 1 − α t λ v t 1 − β t + ϵ ⋅ ( β m t − 1 1 − α t + ( 1 − β ) ( ℓ ′ ( w t − 1 , b ) + γ w t − 1 ) 1 − α t ) g_t = \ell'(w_t, b) + \gamma w_t \\\frac{\lambda}{\sqrt{\frac{v_t}{1-\beta^t}}+\epsilon} \cdot \frac{m_t}{1-\alpha^t} \\ = \frac{\lambda}{\sqrt{\frac{v_t}{1-\beta^t}}+\epsilon} \cdot \frac{\beta m_{t-1} + (1-\beta)(\ell'(w_{t-1}, b) + \gamma w_{t-1})}{1-\alpha^t} \\ \frac{\lambda}{\sqrt{\frac{v_t}{1-\beta^t}}+\epsilon} \cdot \left( \frac{\beta m_{t-1} }{1-\alpha^t} + \frac{(1-\beta)(\ell'(w_{t-1}, b) + \gamma w_{t-1})}{1-\alpha^t} \right) gt=ℓ′(wt,b)+γwt1−βtvt+ϵλ⋅1−αtmt=1−βtvt+ϵλ⋅1−αtβmt−1+(1−β)(ℓ′(wt−1,b)+γwt−1)1−βtvt+ϵλ⋅(1−αtβmt−1+1−αt(1−β)(ℓ′(wt−1,b)+γwt−1))
对于权重的大参数, v t 1 − β t \sqrt{\frac{v_t}{1-\beta^t}} 1−βtvt 有很大的值,造成 γ w t − 1 v t 1 − β t \frac{\gamma \ w_{t-1}}{\sqrt{\frac{v_t}{1-\beta^t}}} 1−βtvtγ wt−1 很小,反而使得,在大权重上这个方向上,权重 W W W 被正则化的更少。 反而更新率几乎很小,不变了。
3、而权重衰减对所有的权重都采用相同的系数进行更新,越大的权重显然惩罚越大。
4、在常见的深度学习库中只提供了L2正则,并没有提供权重衰减的实现。
那么如何缓和上述adam的局限性呢?且看下面的AdamW
AdamW
因为 Adam 在大的权重更新上面,反而会出现惩罚变小的情况,导致训练效果不佳。AdamW 只是在 Adam 的基础之上,在更新参数的时候,再加上对应权重的正则化的值。
g t = ℓ ′ ( w t , b ) + γ w t { m t = α ⋅ m t − 1 + ( 1 − α ) g t v t = β ⋅ v t − 1 + ( 1 − β ) g t 2 m t ^ = m t a − α t v t ^ = v t 1 − β t w t + 1 = w t − η t ( λ m t ^ v t ^ + ϵ + γ w t ) g_t = \ell'(w_t, b) + \gamma w_t\\ \left\{ \begin{array}{rcl} m_t =\alpha⋅m_{t−1} +(1− \alpha)g_t \\ v_t =\beta ⋅v_{t−1} +(1−\beta)g_t^2 \end{array}\right. \\ \hat{m_t} = \frac{m_t}{a-\alpha_t} \\ \hat{v_t} = \frac{v_t}{1-\beta_t} \\ w_{t+1}=w_t - \eta_t(\frac{\lambda \hat{m_t}}{\sqrt{\hat{v_t}}+\epsilon} + \gamma w_t) gt=ℓ′(wt,b)+γwt{mt=α⋅mt−1+(1−α)gtvt=β⋅vt−1+(1−β)gt2mt^=a−αtmtvt^=1−βtvtwt+1=wt−ηt(vt^+ϵλmt^+γwt)
就是在原有 Adam的基础之上,将原有的 正则项 的倒数加入到参数的更新当中了。
总之一句话,如果使用了weightdecay就不必再使用L2正则化了。
还有:随着Adam训练原始ViT失败,它的改进版本AdamW渐渐地变成了训练ViT甚至ConvNext的首选。但是AdamW并没有改变Adam中的冲量范式,因此在当batch size超过4,096的时候,AdamW训练出的ViT的性能会急剧下降。
代码参考
文章中的算法流程图:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ypn9F2YU-1677582212082)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/c292484c-ec9c-470a-bc0c-f9d339c551a3/Untitled.png)]](https://img-blog.csdnimg.cn/388c63ebccce433aab20f1428789da2c.png#pic_center)
对应的解释流程图:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DWYsXf9Z-1677582212082)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a7e60f73-7d9a-4d4f-acf7-bb8c01e8dd90/Untitled.png)]](https://img-blog.csdnimg.cn/af5c6304b126494cb63e9fb5cc89059c.png#pic_center)
Adan(Adaptive Nesterov Momentum)
1、论文链接 2、代码链接 3、参考链接
通过结合改写的Nesterov冲量与自适应优化算法,并引入解耦的权重衰减,可以得到最终的Adan优化器。利用外推点,Adan可以提前感知周围的梯度信息,从而高效地逃离尖锐的局部极小区域,以增加模型的泛化性。
先从下面两个改进,再将两个改进加在一起就变成了 adan 。
1) 自适应的 Nesterov 冲量
先从 Nesterov 梯度优化器推导:
w t + 1 ′ = w t − α m t − 1 m t = α m t − 1 + λ g ( w t + 1 ′ , b t + 1 ′ ) w t + 1 = w t − m t w'_{t+1} = w_t −\alpha m_{t−1} \\ m_t =αm_{t−1} +\lambda \ g(w'_{t+1} ,b'_{t+1}) \\ w_{t + 1} = w_t − m_t wt+1′=wt−αmt−1mt=αmt−1+λ g(wt+1′,bt+1′)wt+1=wt−mt
但是,计算外推点 w t + 1 ′ w'_{t+1} wt+1′ 处的梯度,会因为同时保留 w t + 1 ′ w'_{t+1} wt+1′ 和 w t w_t wt 带来额外的计算和内存开销。
先优化 Nesterov 梯度外导点的方式:
使用 g t + ( 1 − β 2 ) ( g t − g t − 1 ) g_t + (1-\beta_2)(g_t - g_{t-1}) gt+(1−β2)(gt−gt−1) 来替代 g ( w t + 1 ′ ) g(w'_{t+1}) g(wt+1′) 。
替代完成的公式如下:
w t + 1 ′ = w t − α m t − 1 m t = β 1 m t − 1 + [ g t + ( 1 − β 1 ) ( g t − g t − 1 ) ] w t + 1 = w t − m t w'_{t+1} = w_t −\alpha m_{t−1} \\ m_t =\beta_1 m_{t−1} + [g_t + (1-\beta_1)(g_t - g_{t-1})] \\ w_{t + 1} = w_t − m_t wt+1′=wt−αmt−1mt=β1mt−1+[gt+(1−β1)(gt−gt−1)]wt+1=wt−mt
可以证明,改写的Nesterov冲量算法与原算法等价,两者的迭代点可以相互转化,且最终的收敛点相同。可以看到,通过引入梯度的差分项,已经可以避免手动的参数重载和人为地在外推点进行BP。
将改写的Nesterov冲量算法同自适应类优化器相结合, 将 m t m_t mt的更新由累积形式替换为移动平均形式,并使用二阶moment对学习率进行放缩:
m t = β 1 m t − 1 + [ g t + ( 1 − β 1 ) ( g t − g t − 1 ) ] n t = ( 1 − β 3 ) n t − 1 + β 3 [ g t + ( 1 − β 2 ) ( g t − g t − 1 ) ] 2 η t = η n t + ϵ w t + 1 = w t − η t ∘ m t m_t =\beta_1 m_{t−1} + [g_t + (1-\beta_1)(g_t - g_{t-1})]\\ n_t = (1 - \beta_3)n_{t-1} + \beta_3[g_t + (1-\beta_2)(g_t - g_{t-1})]^2 \\ \eta_t = \frac{\eta}{\sqrt{n_t + \epsilon}} \\ w_{t+1} = w_{t} - \eta_t \circ m_t mt=β1mt−1+[gt+(1−β1)(gt−gt−1)]nt=(1−β3)nt−1+β3[gt+(1−β2)(gt−gt−1)]2ηt=nt+ϵηwt+1=wt−ηt∘mt
至此已经得到了Adan的算法的基础版本。
理解移动平代替累积形式:
1、累积形式:原始的 m t = m t − 1 + λ g ( w t + 1 ′ , b t + 1 ′ ) m_t =m_{t−1} +\lambda \ g(w'_{t+1} ,b'_{t+1}) mt=mt−1+λ g(wt+1′,bt+1′) 为累积形式,就是简单将输出的梯度不断地累加。
2、移动平均: m t = β m t − 1 + ( 1 − β ) g ( w t + 1 ′ , b t + 1 ′ ) m_t =\beta m_{t−1} +(1-\beta) \ g(w'_{t+1} ,b'_{t+1}) mt=βmt−1+(1−β) g(wt+1′,bt+1′) 为移动平均,对输入的两个输入给予总和为 1 的权重,使得输出的在两个输入之间移动。
2) 梯度差分的冲量
可以发现, m t = β 1 m t − 1 + [ g t + ( 1 − β 1 ) ( g t − g t − 1 ) ] m_t =\beta_1 m_{t−1} + [g_t + (1-\beta_1)(g_t - g_{t-1})] mt=β1mt−1+[gt+(1−β1)(gt−gt−1)] 的更新将梯度与梯度的差分耦合在一起 ,但是在实际场景中,往往需要对物理意义不同的两项进行单独处理,因此研究人员引入梯度差分的冲量 v t = ( 1 − β 2 ) v t − 1 + β 2 ( g t − g t − 1 ) v_t = (1 - \beta_2)v_{t-1} + \beta_2(g_t - g_{t-1}) vt=(1−β2)vt−1+β2(gt−gt−1)
替换完成的公式如下:
m t = ( 1 − β 1 ) m t − 1 + β 1 g t v t = ( 1 − β 2 ) v t − 1 + β 2 ( g t − g t − 1 ) n t = ( 1 − β 3 ) n t − 1 + β 3 [ g t + ( 1 − β 2 ) ( g t − g t − 1 ) ] 2 η t = η n t + ϵ w t + 1 = w t − η t ∘ m t m_t = (1 - \beta_1)m_{t-1} + \beta_1 g_t \\ v_t = (1 - \beta_2)v_{t-1} + \beta_2(g_t - g_{t-1}) \\ n_t = (1 - \beta_3)n_{t-1} + \beta_3[g_t + (1-\beta_2)(g_t - g_{t-1})]^2 \\ \eta_t = \frac{\eta}{\sqrt{n_t + \epsilon}} \\ w_{t+1} = w_{t} - \eta_t \circ m_t mt=(1−β1)mt−1+β1gtvt=(1−β2)vt−1+β2(gt−gt−1)nt=(1−β3)nt−1+β3[gt+(1−β2)(gt−gt−1)]2ηt=nt+ϵηwt+1=wt−ηt∘mt
3) 解耦的权重衰减
对于带L2权重正则的目标函数,目前较流行的AdamW优化器通过对L2正则与训练loss解耦,在ViT和ConvNext上获得了较好的性能。但是AdamW所用的解耦方法偏向于启发式,目前并不能得到其收敛的理论保证。
基于对L2正则解耦的思想,也给Adan引入解耦的权重衰减策略。目前Adan的每次迭代可以看成是在最小化优化目标F的某种一阶近似:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-noA4wgs5-1677582212083)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/09dc8474-d010-4e71-a367-94fba7bbf80f/Untitled.png)]](https://img-blog.csdnimg.cn/567058f864d34b778dca2f7058cd8d3d.png)
由于F中的L2权重正则过于简单且光滑性很好,以至于不需要对其进行一阶近似。因此,可以只对训练loss进行一阶近似而忽略L2权重正则,那么Adan的最后一步迭代将会变成:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D7aIwYm1-1677582212083)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/cfd38bf9-7974-4d71-9768-40504d4e5ae2/Untitled.png)]](https://img-blog.csdnimg.cn/37fb085698474d419e130948cf1b9fc9.png)
有趣的是,可以发现AdamW的更新准则是Adan更新准则在学习率eta接近0时的一阶近似。因此,可从proximal 算子的角度给Adan甚至AdamW给出合理的解释而不是原来的启发式改进。
所以提出了新的优化器 Nesterov momentum estimation (NME).
具体公式如下:
m t = ( 1 − β 1 ) m t − 1 + β 1 g t v t = ( 1 − β 2 ) v t − 1 + β 2 ( g t − g t − 1 ) n t = ( 1 − β 3 ) n t − 1 + β 3 [ g t + ( 1 − β 2 ) ( g t − g t − 1 ) ] 2 η t = η n t + ϵ w t + 1 = ( 1 + λ η ) − 1 [ w t − η t ∘ ( m t + ( 1 − β 2 ) v k ) ] m_t = (1 - \beta_1)m_{t-1} + \beta_1 g_t \\ v_t = (1 - \beta_2)v_{t-1} + \beta_2(g_t - g_{t-1}) \\ n_t = (1 - \beta_3)n_{t-1} + \beta_3[g_t + (1-\beta_2)(g_t - g_{t-1})]^2 \\ \eta_t = \frac{\eta}{\sqrt{n_t + \epsilon}} \\ w_{t+1} = (1+\lambda \eta)^{-1}[w_{t} - \eta_t \circ ( m_t + (1-\beta_2)v_k)] mt=(1−β1)mt−1+β1gtvt=(1−β2)vt−1+β2(gt−gt−1)nt=(1−β3)nt−1+β3[gt+(1−β2)(gt−gt−1)]2ηt=nt+ϵηwt+1=(1+λη)−1[wt−ηt∘(mt+(1−β2)vk)]
具体公式解析:
第一行:计算了动量
第二行:计算了自适应学习率的更新参数
第三行:其中 g t + ( 1 − β 2 ) ( g t − g t − 1 ) g_t + (1-\beta_2)(g_t - g_{t-1}) gt+(1−β2)(gt−gt−1) 是被用来替代上面 Nesterov 中的下一适合的假象梯度 g ( w t + 1 ′ ) g(w'_{t+1}) g(wt+1′),这样就可以节约计算和内存带来的开销。
第四行:自适应动量的参数
第五行:引入 动态L2正则 的权重衰减项
Adan结合了自适应优化器、Nesterov冲量以及解耦的权重衰减策略的优点,能承受更大的学习率和batch size,以及可以实现对模型参数的动态L2正则。
优化器的表现可视化所使用的代码:
from matplotlib import pyplot as plt
from matplotlib import colors
import numpy as np
class Ravine:
@staticmethod
def get_name():
return 'Ravine'
# 模型的方程
@staticmethod
def function(x, y):
return -np.cos(2 * x) * 50 + np.power(np.e, y)
# 模型的梯度
@staticmethod
def gradient(x, y):
return np.sin(2 * x) * 100, np.power(np.e, y)
# 输出模型的范围,依次为:x 轴最小值、x 轴最大值、y 轴最小值、y 轴最大值
@staticmethod
def get_scope():
return -1, 1, -5, 1
# 输出优化器在本模型上梯度下降的起点
@staticmethod
def get_start():
return -0.8, 0.5
class Saddle:
@staticmethod
def get_name():
return 'Saddle'
@staticmethod
def function(x, y):
return x * x - y * y * y * y
@staticmethod
def gradient(x, y):
return x * 2, -y * y * y * 4
@staticmethod
def get_scope():
return -2, 2, -2, 2
@staticmethod
def get_start():
return -1, -0.01
class Beale:
@staticmethod
def get_name():
return 'Beale'
@staticmethod
def function(x, y):
return (1.5 - x * y)**2 + (2.25 - x - x * y * y)**2 + (2.625 - x + x * y * y * y)**2
@staticmethod
def gradient(x, y):
gradient_x = 2 * ((1.5 - x + x * y) * (-1 + y) + (2.25 - x + x * y * y) * (-1 + y * y) + (
2.625 - x + x * y * y * y) * (-1 + y * y * y))
gradient_y = 2 * ((1.5 - x + x * y) * x + (2.25 - x + x * y * y) * (2 * x * y) + (2.625 - x + x * y * y * y) * (
3 * x * y * y))
return gradient_x, gradient_y
@staticmethod
def get_scope():
return -5, 5, -5, 5
@staticmethod
def get_start():
return 1.5, 1.2
class SGD:
_learning_rate = 0.01
def optimize(self, gradient_w, w, t):
w = w - self._learning_rate * gradient_w
return w
class Momentum:
_learning_rate = 0.01
_alpha = 0.9
def __init__(self):
self.m = 0
def optimize(self, gradiant_w, w, t):
self.m = self._alpha * self.m + self._learning_rate * gradiant_w
w = w - self.m
return w
class NAG:
_learning_rate = 0.01
_alpha = 0.9
def __init__(self):
self.m = 0
def get_momentum(self):
return self._alpha * self.m
def optimize(self, detection_gradiant_w, w, t):
self.m = self._alpha * self.m + self._learning_rate * detection_gradiant_w
w = w - self.m
return w
class AdaGrad:
_learning_rate = 0.01
_epsilon = 0.0000000001
def __init__(self):
self.v = 0
def optimize(self, gradient_w, w, t):
self.v = self.v + gradient_w**2
w = w - self._learning_rate / np.sqrt(self.v + self._epsilon) * gradient_w
return w
class RMSProp:
_learning_rate = 0.01
_beta = 0.9
_epsilon = 0.0000000001
def __init__(self):
self.v = 0
def optimize(self, gradient_w, w, t):
self.v = self._beta * self.v + (1 - self._beta) * gradient_w**2
w = w - self._learning_rate / np.sqrt(self.v + self._epsilon) * gradient_w
return w
class AdaDelta:
_beta = 0.9
_epsilon = 0.0000000001
def __init__(self):
self.v = 0
self.d = 0
def optimize(self, gradient_w, w, t):
self.v = self._beta * self.v + (1 - self._beta) * gradient_w**2
t = np.sqrt(self.d + self._epsilon) / np.sqrt(self.v + self._epsilon) * gradient_w
w = w - t
self.d = self._beta * self.d + (1 - self._beta) * t**2
return w
class Adam:
_learning_rate = 0.01
_alpha = 0.9
_beta = 0.99
_epsilon = 0.0000000001
def __init__(self):
self.m = 0
self.v = 0
def optimize(self, gradient_w, w, t):
self.m = self._alpha * self.m + (1 - self._alpha) * gradient_w
self.v = self._beta * self.v + (1 - self._beta) * gradient_w**2
w = w - self._learning_rate \
/ (np.sqrt(self.v / (1 - np.power(self._beta, t))) + self._epsilon) \
* self.m / (1 - np.power(self._alpha, t))
return w
optimizers = {
'SGD': SGD,
'Momentum': Momentum,
'NAG': NAG,
'AdaGrad': AdaGrad,
'RMSProp': RMSProp,
'AdaDelta': AdaDelta,
'Adam': Adam,
}
def experiment(axes, model, optimizer):
scope = model.get_scope()
x, y = np.meshgrid(np.linspace(scope[0], scope[1], 100), np.linspace(scope[2], scope[3], 100))
z = model.function(x, y)
# axes.plot_surface(x, y, z, zorder=1) # 在图上绘制模型
axes.plot_surface(x, y, z, zorder=1, norm=colors.LogNorm(), cmap='jet') # 在图上绘制模型
axes.set_xlabel('x')
axes.set_ylabel('y')
axes.set_zlabel('z')
axes.set_title(f'%s in %s' % (optimizer, model.get_name()))
optimizer_x = optimizers[optimizer]() # 为 x 生成优化器
optimizer_y = optimizers[optimizer]() # 为 y 生成优化器
x, y = model.get_start()
xa, ya = [x], [y] # 用于记录下降过程中经过的点
t = 1 # 记录迭代轮次
while (t < 10
or (t < 100000
and not (ya[-1] == ya[-2] and xa[-1] == xa[-2])
and (scope[0] < x < scope[1] and scope[2] < y < scope[3]))):
if optimizer == 'NAG': # 计算梯度
gradient_x, gradient_y = model.gradient(x - optimizer_x.get_momentum(), y - optimizer_y.get_momentum())
else:
gradient_x, gradient_y = model.gradient(x, y)
x = optimizer_x.optimize(gradient_x, x, t) # 用优化器优化
y = optimizer_y.optimize(gradient_y, y, t) # 用优化器优化
xa.append(x)
ya.append(y)
t = t + 1
za = [model.function(i, j) for i, j in zip(xa, ya)] # 生成下降时经过的点的 z 轴坐标
axes.plot(xa, ya, za, zorder=3, label=optimizer) # 在图上绘制下降路线
axes.text(x, y, model.function(x, y), f'epoch=%d' % t)
axes.legend()
if __name__ == '__main__':
experiment(plt.subplot(121, projection='3d'), Beale, 'RMSProp')
experiment(plt.subplot(122, projection='3d'), Beale, 'Adam')
plt.show()
以下为生成 NAG 示意图的代码:
from matplotlib import pyplot as plt
import numpy as np
ax = plt.subplot(111, aspect='equal')
ax.axis('off')
ax.arrow(0.00, 0.00, 0.02, 0.04, length_includes_head=True, color='b')
ax.arrow(0.02, 0.04, 0.08, 0.04, length_includes_head=True, color='b')
ax.arrow(0.00, 0.00, 0.10, 0.08, length_includes_head=True, color='m')
ax.arrow(0.00, 0.00, 0.08, 0.04, length_includes_head=True, color='g')
ax.arrow(0.08, 0.04, 0.02, -0.04, length_includes_head=True, color='g')
ax.arrow(0.00, 0.00, 0.10, 0.00, length_includes_head=True, color='r')
ax.text(-0.015, 0.02, r'$-\lambda \cdot J(w_t, \omega_t)$', color='b', size=12)
ax.text(0.048, 0.061, r'$-\alpha m_t$', color='b', size=12)
ax.text(0.06, 0.045, r'$-\alpha m_t-\lambda \cdot J(w_t, \omega_t)$', color='m', size=12)
ax.text(0.083, 0.082, 'Momentum', color='m', size=16)
ax.text(0.04, 0.027, r'$-\alpha m_t$', color='g', size=12)
ax.text(0.045, 0.010, r'$-\lambda \cdot J(w_t - \alpha m_t, \omega_t - \alpha\mu_t)$', color='g', size=12)
ax.text(0.02, -0.004, r'$-\alpha m_t - \lambda \cdot J(w_t - \alpha m_t, \omega_t - \alpha\mu_t)$', color='r', size=12)
ax.text(0.102, -0.002, 'NAG', color='r', size=16)
plt.show()
AdamW 的官方代码
def apply_gradients(self, grads_and_vars, global_step=None, name=None):
"""See base class."""
assignments = []
for (grad, param) in grads_and_vars:
if grad is None or param is None:
continue
param_name = self._get_variable_name(param.name)
m = tf.get_variable(
name=param_name + "/adam_m",
shape=param.shape.as_list(),
dtype=tf.float32,
trainable=False,
initializer=tf.zeros_initializer())
v = tf.get_variable(
name=param_name + "/adam_v",
shape=param.shape.as_list(),
dtype=tf.float32,
trainable=False,
initializer=tf.zeros_initializer())
# Standard Adam update.
next_m = (
tf.multiply(self.beta_1, m) + tf.multiply(1.0 - self.beta_1, grad))
next_v = (
tf.multiply(self.beta_2, v) + tf.multiply(1.0 - self.beta_2,
tf.square(grad)))
update = next_m / (tf.sqrt(next_v) + self.epsilon)
# Just adding the square of the weights to the loss function is *not*
# the correct way of using L2 regularization/weight decay with Adam,
# since that will interact with the m and v parameters in strange ways.
#
# Instead we want ot decay the weights in a manner that doesn't interact
# with the m/v parameters. This is equivalent to adding the square
# of the weights to the loss with plain (non-momentum) SGD.
if self._do_use_weight_decay(param_name):
update += self.weight_decay_rate * param
update_with_lr = self.learning_rate * update
next_param = param - update_with_lr
assignments.extend(
[param.assign(next_param),
m.assign(next_m),
v.assign(next_v)])
return tf.group(*assignments, name=name)



















