深度学习之神经网络的优化器篇

神经网络的优化器


本片博客记录一下不同的神经网络的优化器

参考大神链接:神经网络的优化器_电器爆破专家的博客-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=wtwt1, 即

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=wtwt1wt+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=αmt1+λ gtwt+1=wtmt

其中 α \alpha α 是新引入的常量参数(常取值为 α = 0.9 \alpha=0.9 α=0.9),m_t 是为了实现算法而引入的变量。当 g t − 1 g_{t−1} gt1 的符号与 g t g_t gt 的正负不同时, m t m_t mt 的累加就会使二者得到一定的抵消,即抑制抖动的作用;当 g t − 1 g_{t-1} gt1 的符号与 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αmt1mt=αmt1+λ g(wt+1,bt+1)wt+1=wtmt

其中 α \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)]

在上图中,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=vt1+gt2wt+1=wtvt+ϵ λgt

其中为了避免分母为零而引入的常量参数 ϵ \epsilon ϵ (常取值为 ϵ = 1 × 1 0 − 8 \epsilon=1 \times 10^{-8} ϵ=1×108 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)]

如图所示 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)]

我们可以看到,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=βvt1+(1β)gt2wt+1=wtvt+ϵ λ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)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6rrr0agT-1677582212080)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/ec287574-0d68-4474-96f3-6a51c33ddc81/Untitled.png)]

可以看到,无论是在马鞍上还是在峡谷中 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=αmt1+(1α)gtvt=βvt1+(1β)gt2wt+1=wt1β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=αmt1+(1α)gt :代表当前梯度和当前动量的结合。

上面代表一阶动量:代表惯性,当前梯度更新的方向不仅要考虑当前梯度,还要 考虑历史梯度的影响;

v t = β ⋅ v t − 1 + ( 1 − β ) g t 2 v_t =\beta ⋅v_{t−1} +(1−\beta)g_t^2 vt=βvt1+(1β)gt2 :代表当前自适应梯度的权值。

上面代表二阶动量:用于控制自适应学习率,二阶动量在后面被放置在分母的位置,其越大代表学习率越小,

二阶动量的物理意义:

  • 对于经常更新的参数,不希望被单个样本影响太大,希望学习率慢一些。
  • 对于偶尔更新的参数,希望能够从偶然出现的样本中多学习一些,也就是希望学习率大一点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LSD0FQhm-1677582212080)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/01175672-eaab-44a1-977b-1fbb6d8593f4/Untitled.png)]

通过上图可以看到可以看到 Adam 相对于 RMSProp 在马鞍上的表现更为优秀,下降曲线也比较平滑。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7MXV6YGw-1677582212081)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/b400185e-55d6-4d8c-8331-ee2fdb07706f/Untitled.png)]

通过上图可以看到,虽然 Adam 的下降速度比 RMSProp 慢一些,但是在峡谷中没有像 RMSProp 一样发生抖动。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aWkuQN8d-1677582212081)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/46c6983f-73c1-4803-90cf-80b0812577f4/Untitled.png)]

通过上图可以更直观的看出 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)]

当 下面的学习率 λ ′ = λ α \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)]

从上面可以看出来,只有当 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=wt1β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βmt1+(1β)((wt1,b)+γwt1)1βtvt +ϵλ(1αtβmt1+1αt(1β)((wt1,b)+γwt1))

对于权重的大参数, 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 γ wt1 很小,反而使得,在大权重上这个方向上,权重 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=αmt1+(1α)gtvt=βvt1+(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)]

对应的解释流程图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DWYsXf9Z-1677582212082)(https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a7e60f73-7d9a-4d4f-acf7-bb8c01e8dd90/Untitled.png)]

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αmt1mt=αmt1+λ g(wt+1,bt+1)wt+1=wtmt

但是,计算外推点 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)(gtgt1) 来替代 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αmt1mt=β1mt1+[gt+(1β1)(gtgt1)]wt+1=wtmt

可以证明,改写的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=β1mt1+[gt+(1β1)(gtgt1)]nt=(1β3)nt1+β3[gt+(1β2)(gtgt1)]2ηt=nt+ϵ ηwt+1=wtηtmt

至此已经得到了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=mt1+λ 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=βmt1+(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=β1mt1+[gt+(1β1)(gtgt1)] 的更新将梯度与梯度的差分耦合在一起 ,但是在实际场景中,往往需要对物理意义不同的两项进行单独处理,因此研究人员引入梯度差分的冲量 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)vt1+β2(gtgt1)

替换完成的公式如下:

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)mt1+β1gtvt=(1β2)vt1+β2(gtgt1)nt=(1β3)nt1+β3[gt+(1β2)(gtgt1)]2ηt=nt+ϵ ηwt+1=wtηtmt

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)]

由于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)]

有趣的是,可以发现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)mt1+β1gtvt=(1β2)vt1+β2(gtgt1)nt=(1β3)nt1+β3[gt+(1β2)(gtgt1)]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)(gtgt1) 是被用来替代上面 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)

猜你喜欢

转载自blog.csdn.net/To_be_little/article/details/129267632