神经网络的优化器
文章目录
- 神经网络的优化器
- 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 的对比。
在上图中,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+ϵλ
就会超乎寻常的相应变小。这种方式也可以抑制抖动,即让那些梯度有剧烈变化的参数有一个较小的学习率。
如图所示 AdaGrad 为 y 配置了较大的学习率,为 x 配置了较小的学习率,从而使其能够快速脱离马鞍。AdaGrad 仅迭代了 2519 轮,而 SGD 迭代了 125005 轮。
我们看到 v t v_t vt 一直在做正数累加,总体上会使全体参数的学习率趋向无穷小,在训练的后期会使模型的收敛速度变得极慢。不可否认的是,在训练的后期是需要降低学习率,从而稳定下降到极小值,避免在极小值处抖动,即使用退火学习率。笔者推测,AdaGrad 也是出于这种考量,使用正数累加的方式从总体上来降低学习率,让模型在训练后期稳定下降。但 AdaGrad 的现实表现却不尽如人意。
我们可以看到,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 是为了实现算法而引入的变量。
可以看到,无论是在马鞍上还是在峡谷中 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 :代表当前自适应梯度的权值。
上面代表二阶动量:用于控制自适应学习率,二阶动量在后面被放置在分母的位置,其越大代表学习率越小,
二阶动量的物理意义:
- 对于经常更新的参数,不希望被单个样本影响太大,希望学习率慢一些。
- 对于偶尔更新的参数,希望能够从偶然出现的样本中多学习一些,也就是希望学习率大一点
通过上图可以看到可以看到 Adam 相对于 RMSProp 在马鞍上的表现更为优秀,下降曲线也比较平滑。
通过上图可以看到,虽然 Adam 的下降速度比 RMSProp 慢一些,但是在峡谷中没有像 RMSProp 一样发生抖动。
通过上图可以更直观的看出 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 是否等价的情况。
当 下面的学习率 λ ′ = λ α \lambda' = \frac{\lambda}{\alpha} λ′=αλ 你可以发现 ,上面使用 L2 正则化来替代 weight decay 是完全等价的。
2、在看看 Adam 的情况下,L2 和 weight decay 是否等价的情况。
从上面可以看出来,只有当 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的性能会急剧下降。
代码参考
文章中的算法流程图:
对应的解释流程图:
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的某种一阶近似:
由于F中的L2权重正则过于简单且光滑性很好,以至于不需要对其进行一阶近似。因此,可以只对训练loss进行一阶近似而忽略L2权重正则,那么Adan的最后一步迭代将会变成:
有趣的是,可以发现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)