优化方法总结以及Adam存在的问题(SGD, Momentum, AdaDelta, Adam, AdamW,LazyAdam)

文章目录

优化方法概述

模型优化方法的选择直接关系到最终模型的性能。有时候效果不好,未必是特征的问题或者模型设计的问题,很可能是优化算法的问题,而且好的优化算法还能够帮助加速训练模型。

深度学习模型的发展进程:
SGD -> SGDM ->NAG -> AdaGrad -> AdaDelta -> Adam -> Nadam

1.整体框架

首先定义:
待优化参数 w w w, 目标函数: f ( w ) f(w) f(w),学习率 α \alpha α
每次迭代:

  1. 计算目标函数关于参数的梯度: g t = ▽ f ( w t ) g_t=\bigtriangledown f(w_t) gt=f(wt)
  2. 根据历史梯度计算一阶动量和二阶动量: m t = ϕ ( g 1 , g 2 , . . . , g t ) , v t = ψ ( g 1 , g 2 , . . . , g t ) m_t=\phi(g_1, g_2, …, g_t), v_t=\psi(g_1, g_2,…,g_t) mt=ϕ(g1,g2,...,gt),vt=ψ(g1,g2,...,gt)
  3. 计算当前时刻的下降梯度: η t = α ⋅ m t / v t \eta_t= \alpha \cdot m_t/ \sqrt{v_t} ηt=αmt/vt
  4. 根据下降梯度对参数进行更新: w t + 1 = w t − η t w_{t+1}=w_t-\eta_t wt+1=wtηt

1.1 SGD

没有动量的概念,第三步中 η t = α ⋅ g t \eta_t=\alpha \cdot g_t ηt=αgt
其中batch_size是一个重要的变量,需要做一个快速尝试,才能够找到能够最有效地减少成本函数的那个,一般是 2 n 2^n 2n
缺点:

  • 有可能会陷入局部最小值;
  • 不会收敛,最终会一直在最小值附近波动,并不会达到最小值并停留在此;
  • 下降速度慢;

1.2 Momentum

SGD下降方法的缺点是参数更新方向只依赖于当前batch计算出的梯度,因此十分的不稳定。为了抑制SGD的震荡,动量认为梯度下降的过程中可以加入惯性。动量梯度下降法运行速度总是快于标准的梯度下降法,其基本思想是在SGD的基础上引入了一阶动量:
m t = β m t − 1 + ( 1 − β ) g t m_t=\beta m_{t-1}+(1-\beta)g_t mt=βmt1+(1β)gt
一阶动量指的是各个时刻梯度的指数加权平均,约等于 1 1 − β 1 \frac{1}{1-\beta_1} 1β11个历史时刻的梯度向量和的平均值,也就是t时刻的下降方向,不仅由当前点的梯度方向决定,还由此前的累积的梯度来决定, β \beta β的经验值一般为0.9,也就是意味着下降方向主要是此前累积的下降方向,并略微偏向当前时刻的下降方向。并利用当前batch微调最终的更新方向。如果当前梯度方向与历史梯度一致,会增强该方向的梯度。如果不一致,梯度会衰减。
优点

  • 增加了稳定性;
  • 下降速度更快;
  • 还有一定摆脱局部最优的能力。

1.2.1 理解指数加权平均

使得 β = 0.9 \beta=0.9 β=0.9
m 100 = 0.9 m 99 + 0.1 θ 100 m_{100}=0.9m_{99}+0.1\theta_{100} m100=0.9m99+0.1θ100
m 99 = 0.9 m 98 + 0.1 θ 99 m_{99}=0.9m_{98}+0.1\theta_{99} m99=0.9m98+0.1θ99
m 98 = 0.9 m 97 + 0.1 θ 98 m_{98}=0.9m_{97}+0.1\theta_{98} m98=0.9m97+0.1θ98
把公式带入会得到:
m 100 = 0.1 θ 100 + 0.1 ∗ 0.9 θ 99 + 0.1 ∗ ( 0.9 ) 2 θ 98 + 0.1 ∗ ( 0.9 ) 3 θ 97 + 0.1 ∗ ( 0.9 ) 4 θ 96 m_{100}=0.1\theta_{100}+0.1*0.9\theta_{99}+0.1*(0.9)^2\theta_{98}+0.1*(0.9)^3\theta_{97}+0.1*(0.9)^4\theta_{96} m100=0.1θ100+0.10.9θ99+0.1(0.9)2θ98+0.1(0.9)3θ97+0.1(0.9)4θ96
可以看出这是一个第100天的数据包含了99,98,97天的数据,而且是一个指数衰减的过程,这些系数的和为1或者逼近于1。到底平均了多少天的数据? 0. 9 10 0.9^{10} 0.910大约为0.35,约等于 1 e \frac{1}{e} e1的值。也就是10天之后曲线的高度下降到 1 3 \frac{1}{3} 31,相当于只关注了过去10天的数据,因为10天后,权重下降到不到当日权重的三分之一。如果 β = 0.98 \beta=0.98 β=0.98,那么 0.9 8 50 0.98^{50} 0.9850大约等于 1 e \frac{1}{e} e1,可以看作平均了50天的数据,由此得到公式平均了大约 1 1 − β \frac{1}{1-\beta} 1β1的数据。

好处:占用极少的内存,每次把最新的公式带入不断覆蓋就可以了。虽然不是最精确的,如果直接平均过去天的数据往往会得到更好的估计,但是需要保存最近的温度数据必须占用更多的内存,执行更加复杂,计算成本也更高。

1.2.2 偏差修正

计算移动平均数时,初始化 m 0 = 0 , m 1 = 0.9 m 0 + 0.1 θ 1 m_0=0,m_1=0.9m_0+0.1\theta_1 m0=0,m1=0.9m0+0.1θ1 m 1 = 0.1 θ 1 m_1=0.1\theta_1 m1=0.1θ1,显然第一天的值会小很多,估计不准确;而且 m 2 = 0.98 m 1 + 0.02 θ 2 m_2=0.98m_1+0.02\theta_2 m2=0.98m1+0.02θ2,如果带入 m 1 m_1 m1然后相乘得到 m 2 = 0.9 ∗ 0.1 θ 1 + 0.1 θ 2 m_2=0.9*0.1\theta_1+0.1\theta_2 m2=0.90.1θ1+0.1θ2 m 2 m_2 m2要远小于 θ 1 \theta_1 θ1 θ 2 \theta_2 θ2,所以 m 2 m_2 m2不能很好的估计出前两天的数据,而当t足够大时, m t ^ = m t \hat{m_t}=m_t mt^=mt
偏差修正能够改正这个问题,特别是在初期。 m 1 / ( 1 − β t ) = m 1 / 0.1 m_1/(1-\beta^t)=m_1/0.1 m1/(1βt)=m1/0.1后, m 1 = θ 1 m_1=\theta_1 m1=θ1就去除了偏差。而随着t的增加, β t \beta^t βt接近于0,所以当t很大的时候,偏差修正几乎没有作用。
在计算指数加权平均数的大部分时候,都不在乎执行偏差修正,因为大部分人宁愿熬过初始时期,拿到具有偏差的估测,然后继续计算下去。如果关心初始时期的偏差,在刚开始计算指数加权移动平均数的时候,偏差修正能帮助在早期获取更好的估测。
m ~ t = m t 1 − β 1 t v ~ t = v t 1 − β 2 t \tilde{m}_{t}=\frac{m_{t}}{1-\beta_1^t}\\ \tilde{v}_{t}=\frac{v_{t}}{1-\beta_2^t} m~t=1β1tmtv~t=1β2tvt

1.3 AdaGrad

之前的方法都没有用到二阶动量,二阶动量的出现,才意味着“自适应率”优化算法的到来。SGD以及动量以同样的学习率更新每个参数。神经网络模型往往包含大量的参数,但是这些参数并不会总是用得到,更新频率也不一样。对于经常更新的参数,不希望其被单个样本影响太大,希望学习速率慢一些;对于偶尔更新的参数,了解的信息太少,希望能够从每个偶然出现的样本身上多学一些,即学习速率大一些。

怎么去度量历史更新频率呢?就是二阶动量:至今为止所有梯度值的平方和:
v t = ∑ t = 1 T g t 2 v_t=\sum_{t=1}^{T}g_t^2 vt=t=1Tgt2
可以看出,此时实质上的学习率由 α \alpha α变为了 α / v t \alpha/\sqrt{v_t} α/vt ,一般为了避免分母为0,会在分母上加一个小的平滑项,因此 v t \sqrt{v_t} vt 是恒大于0的。所以如果参数更新频繁,其二阶动量越大,学习率就越小。

优点

  • 不同更新频率的参数具有不同的学习率,减少摆动,在稀疏数据场景下表现会非常好;
  • 允许使用一个更大的学习率 α \alpha α,从而加快算法的学习速度;

缺点

  • 因为 v t \sqrt{v_t} vt 是不断累积单调递增的,会使得学习率单调递减至0,可能会使得训练过程提前结束,即使后续还有数据也无法学到需要的知识;

1.4 AdaDelta/RMSProp

由于AdaGrad单调递减的学习率变化过于激进,我们考虑一个改变二阶动量计算方法的策略:不累加全部历史梯度,而只关注过去一段时间窗口的下降梯度,很自然的想到之前动量使用的指数加权平均,它所计算的就是过去一段时间的平均值,所以使用这一方法来计算二阶累积动量:
v t = β 2 ∗ v t − 1 + ( 1 − β 2 ) g t 2 v_t=\beta_2 *v_{t-1} + (1- \beta_2)g_t^{2} vt=β2vt1+(1β2)gt2
这就避免了二阶动量持续累积,导致训练过程提前结束的问题了。

1.5 Adam(Adaptive Moment Estimation)

谈到这里,Adam的出现就自然而然了,它是前述方法的集大成者。动量在SGD的基础上增加了一阶动量,AdaGrad和AdaDelta在SGD的基础上增加了二阶动量。Adam实际上就是将Momentum和RMSprop集合在一起,把一阶动量和二阶动量都使用起来了,具体方法为:

I t e r a t i o n : t = t + 1 m t = β 1 v t − 1 + ( 1 − β 1 ) g t v t = β 2 S t − 1 + ( 1 − β 2 ) g t 2 α t = α ∗ 1 − β 2 t / ( 1 − β 1 t ) w t + 1 = w t − α t m t v t + ϵ Iteration:\\ t=t+1\\ m_{t}=\beta_1v_{t-1}+(1-\beta_1)g_t \\ v_{t}=\beta_2S_{t-1}+(1-\beta_2)g_{t}^{2} \\ \alpha_t=\alpha*\sqrt{1-\beta^t_2}/(1-\beta^t_1) \\ w_{t+1}=w_{t}-\alpha_t\frac{m_{t}}{\sqrt{v_{t}}+\epsilon} Iteration:t=t+1mt=β1vt1+(1β1)gtvt=β2St1+(1β2)gt2αt=α1β2t /(1β1t)wt+1=wtαtvt +ϵmt

本算法中有很多超参数,超参数学习率 α \alpha α很重要,也经常需要调试。 β 1 \beta_1 β1 β 2 \beta_2 β2是加权平均数,用于控制一阶动量和二阶动量。常用的缺省值为0.9和0.999。关于 ϵ \epsilon ϵ的选择其实没那么重要,Adam论文的作者建议 ϵ \epsilon ϵ 1 0 − 8 10^{-8} 108,因为它并不会影响算法表现。

优点

  • 自动调整参数的学习率;
  • 大幅提升了训练速度;
  • 提高了稳定性;

1.6 Adam的改进

1.6.1 Adamw

Adam有很多的优点,但是在很多数据集上的最好效果还是用SGD with Momentum细调出来的。可见Adam的泛化性并不如SGD with Momentum。https://arxiv.org/pdf/1711.05101.pdf 中提出其中一个重要原因就是Adam中L2正则化项并不像在SGD中那么有效。

  1. L2正则和Weight Decay在Adam这种自适应学习率算法中并不等价,只有在标准SGD的情况下,可以将L2正则和Weight Decay看做一样。特别是,当与自适应梯度相结合时,L2正则化导致具有较大历史参数和/或梯度幅度的权重比使用权重衰减时更小。

  2. 使用Adam优化带L2正则的损失并不有效,如果引入L2正则化项,在计算梯度的时候会加上正则项求梯度的结果。正常的权重衰减是对所有的权重都采用相同的系数进行更新,本身比较大的一些权重对应的梯度也会比较大,那么惩罚也越大。而由于Adam计算步骤中减去项会有除以梯度平方的累积,使得梯度大的减去项偏小,从而具有大梯度的权重不会像解耦权重衰减那样得到正规化。 这导致自适应梯度算法的L2和解耦权重衰减正则化的不等价。

而在常见的深度学习库中只提供了L2正则,并没有提供权重衰减的实现。这可能就是导致Adam跑出来的很多效果相对SGD with Momentum有偏差的一个原因。
《优化方法总结以及Adam存在的问题(SGD, Momentum, AdaDelta, Adam, AdamW,LazyAdam)》
红色的为原始的Adam+L2 regularization的方法,如果把line 6,line 7,line 8都带入到line 12,并且假设 η t = 1 \eta_t=1 ηt=1
θ t → θ t − 1 − α β 1 m t − 1 + ( 1 − β 1 ) ( ▽ f t + λ θ t − 1 ) v t ^ + ϵ \theta_t \rightarrow \theta_{t-1}-\alpha\frac{\beta_1m_{t-1}+(1-\beta_1)(\bigtriangledown f_t+\lambda \theta_{t-1})}{\sqrt{\hat{v_t}}+\epsilon} θtθt1αvt^ +ϵβ1mt1+(1β1)(ft+λθt1)

分子右上角的 λ θ t − 1 \lambda \theta_{t-1} λθt1向量各个元素被分母的 v t ^ \sqrt{\hat{v_t}} vt^ 项调整了。梯度快速变化的方向上, v t ^ \sqrt{\hat{v_t}} vt^ 有更大的值,而使得调整后的 λ θ t − 1 v t ^ \frac{\lambda\theta_{t-1}}{\sqrt{\hat{v_t}}} vt^ λθt1更小,在这个方向上参数 θ \theta θ被正则化的更少。这显然是不合理的,L2 regularization和weight decay都应该是各向同性的。所以作者提出以绿色的方式来在Adam中正确的引入weight decay的方式,称作AdamW,也就是不让 λ θ t − 1 \lambda \theta_{t-1} λθt1项被 v t ^ \sqrt{\hat{v_t}} vt^ 调整,使用相同的 λ \lambda λ来正则化所有的权重,完成了梯度下降与weight decay的解耦。

目前bert训练采用的优化方法就是Adamw,对除了layernorm,bias项之外的模型参数做weight decay。大部分的模型都会有L2 regularization约束项,因此很有可能出现Adam的最终效果没有sgd的好。

L2 regularization与Weight decay在SGD时是等价的

  • L2正则化的目的是为了在一定程度上减少模型过拟合的问题。

    f t r e g ( θ ) = f t ( θ ) + λ ′ 2 ∣ ∣ θ ∣ ∣ 2 2 f_t^{reg}(\theta)=f_t(\theta)+\frac{\lambda'}{2}||\theta||^2_2 ftreg(θ)=ft(θ)+2λθ22

    θ t + 1 → θ t − α ▽ f t r e g ( θ t ) = θ t − α ▽ f t ( θ t ) − α λ ′ θ t \theta_{t+1} \rightarrow \theta_t-\alpha\bigtriangledown f_t^{reg}(\theta_t)=\theta_t-\alpha\bigtriangledown f_t(\theta_t)-\alpha\lambda'\theta_t θt+1θtαftreg(θt)=θtαft(θt)αλθt

  • weight decay
    是在每次更新的梯度基础上减去一个梯度:
    θ t + 1 → ( 1 − λ ) θ t − α ▽ f t ( θ t ) \theta_{t+1} \rightarrow (1-\lambda) \theta_t-\alpha\bigtriangledown f_t(\theta_t) θt+1(1λ)θtαft(θt)

可以看出当 λ ′ = λ α \lambda'= \frac{\lambda}{\alpha} λ=αλ时,L2 regularization 和 Weight decay 是等价的(仅在使用标准SGD优化时成立)。

权重衰减(L2正则化项)为什么能够避免模型过拟合的问题?

  • 奥卡姆剃刀法则;
  • 过拟合模型的系数往往非常大,因为过拟合就是需要顾忌每一个点,最终形成的拟合函数波动很大,这就意味在某些小区间里的导数值非常大,也就是系数很大,而通过正则化约束参数的范数使其不要太大,可以在一定程度上减少过拟合情况。

1.6.2 LazyAdam

和图像等领域不同,对于NLP之类的任务,每个batch采样到的词有限,每次更新对embedding的梯度估计都是稀疏的。对于 momentum-based 的 Optimizer,现在所有框架的实现都会用当前的 momentum 去更新所有的词,即使这些词在连续的几十步更新里都没有被采样到。这可能会使 Embedding 过拟合。
LazyAdam是Adam的变体,可以更有效地处理稀疏更新。原始的Adam算法为每个可训练变量维护两个移动平均累加器,累加器在每一步都会更新。 而此类为稀疏变量提供了更加懒惰的梯度更新处理,它仅更新当前batch中出现的稀疏变量索引的移动平均累加器,而不是更新所有索引的累加器。 与原始的Adam优化器相比,它可以为某些应用提供模型训练吞吐量的大幅改进。 但是它的语义与原始的Adam算法略有不同,可能会导致不同的实验结果。

LazyAdam的源码:

```python/py
 def _apply_sparse(self, grad, var):
    beta1_power, beta2_power = self._get_beta_accumulators()
    beta1_power = math_ops.cast(beta1_power, var.dtype.base_dtype)
    beta2_power = math_ops.cast(beta2_power, var.dtype.base_dtype)
    lr_t = math_ops.cast(self._lr_t, var.dtype.base_dtype)
    beta1_t = math_ops.cast(self._beta1_t, var.dtype.base_dtype)
    beta2_t = math_ops.cast(self._beta2_t, var.dtype.base_dtype)
    epsilon_t = math_ops.cast(self._epsilon_t, var.dtype.base_dtype)
    lr = (lr_t * math_ops.sqrt(1 - beta2_power) / (1 - beta1_power))

    # \\(m := beta1 * m + (1 - beta1) * g_t\\)
    m = self.get_slot(var, "m")
    m_t = state_ops.scatter_update(m, grad.indices,
                                   beta1_t * array_ops.gather(m, grad.indices) +
                                   (1 - beta1_t) * grad.values,
                                   use_locking=self._use_locking)#一阶动量

    # \\(v := beta2 * v + (1 - beta2) * (g_t * g_t)\\)
    v = self.get_slot(var, "v")
    v_t = state_ops.scatter_update(v, grad.indices,
                                   beta2_t * array_ops.gather(v, grad.indices) +
                                   (1 - beta2_t) * math_ops.square(grad.values),
                                   use_locking=self._use_locking) #二阶动量

    # \\(variable -= learning_rate * m_t / (epsilon_t + sqrt(v_t))\\)
    m_t_slice = array_ops.gather(m_t, grad.indices)
    v_t_slice = array_ops.gather(v_t, grad.indices)
    denominator_slice = math_ops.sqrt(v_t_slice) + epsilon_t
    var_update = state_ops.scatter_sub(var, grad.indices,
                                       lr * m_t_slice / denominator_slice,
                                       use_locking=self._use_locking)
    return control_flow_ops.group(var_update, m_t, v_t)

可以看出公式与Adam都相同,不同的是每次迭代根据当前batch的indices来对一阶动量和二阶动量进行更新。

1.6.3 Madam


class MaskedAdamOptimizer(adam.AdamOptimizer):
    def _apply_sparse_shared(self, grad, var, indices, scatter_add):
        beta1_power, beta2_power = self._get_beta_accumulators()
        beta1_power = math_ops.cast(beta1_power, var.dtype.base_dtype)
        beta2_power = math_ops.cast(beta2_power, var.dtype.base_dtype)
        lr_t = math_ops.cast(self._lr_t, var.dtype.base_dtype)
        beta1_t = math_ops.cast(self._beta1_t, var.dtype.base_dtype)
        beta2_t = math_ops.cast(self._beta2_t, var.dtype.base_dtype)
        epsilon_t = math_ops.cast(self._epsilon_t, var.dtype.base_dtype)
        lr = (lr_t * math_ops.sqrt(1 - beta2_power) / (1 - beta1_power))
        # m_t = beta1 * m + (1 - beta1) * g_t
        m = self.get_slot(var, "m")
        m_scaled_g_values = grad * (1 - beta1_t)
        m_t = state_ops.assign(m, m * beta1_t,
                               use_locking=self._use_locking)   #与LazyAdam的不同之处
        with ops.control_dependencies([m_t]):
            m_t = scatter_add(m, indices, m_scaled_g_values)
        # v_t = beta2 * v + (1 - beta2) * (g_t * g_t)
        v = self.get_slot(var, "v")
        v_scaled_g_values = (grad * grad) * (1 - beta2_t)
        v_t = state_ops.assign(v, v * beta2_t, use_locking=self._use_locking)
        with ops.control_dependencies([v_t]):
            v_t = scatter_add(v, indices, v_scaled_g_values)
        gather_m_t = array_ops.gather(m_t, indices)
        gather_v_t = array_ops.gather(v_t, indices)
        gather_v_sqrt = math_ops.sqrt(gather_v_t)
        var_update = scatter_add(var, indices, -lr * gather_m_t / (gather_v_sqrt + epsilon_t))
        return control_flow_ops.group(*[var_update, m_t, v_t])

Madam与Lazy Adam唯一的不同在于对一阶动量m和二阶动量 v 进行 decay 的操作,Madam是全部都要 decay,LazyAdam 是只 decay 采样到的embedding。(在计算指数加权平均时,LazyAdam只对当前采样到变量之前的平均值进行累加)

1.7 到底是用Adam还是用SGD

在上一篇中,我们用同一个框架让各类算法对号入座。可以看出,大家都是殊途同归,只是相当于在SGD基础上增加了各类学习率的主动控制。如果不想做精细的调优,那么Adam显然最便于直接拿来上手。但这样的傻瓜式操作并不一定能够适应所有的场合。如果能够深入了解数据,研究员们可以更加自如地控制优化迭代的各类参数,实现更好的效果也并不奇怪。

Adam的罪状一

这篇是正在深度学习领域顶级会议之一 ICLR 2018 匿名审稿中的 On the Convergence of Adam and Beyond,探讨了Adam算法的收敛性,通过反例证明了Adam在某些情况下可能会不收敛。
回忆一下上文提到的各大优化算法的学习率: η t = α / v t \eta_t=\alpha/ \sqrt{v_t} ηt=α/vt
其中,SGD没有用到二阶动量,因此学习率是恒定的(实际使用过程中会采用学习率衰减策略,因此学习率递减)。AdaGrad的二阶动量不断累积,单调递增,因此学习率是单调递减的。因此,这两类算法会使得学习率不断递减,最终收敛到0,模型也得以收敛。

但AdaDelta和Adam则不然。二阶动量是固定时间窗口内的累积,随着时间窗口的变化,遇到的数据可能发生钜变,使得 V_t 可能会时大时小,不是单调变化。这就可能在训练后期引起学习率的震荡,导致模型无法收敛。

这篇文章也给出了一个修正的方法。由于Adam中的学习率主要是由二阶动量控制的,为了保证算法的收敛,可以对二阶动量的变化进行控制,避免上下波动。
v t = m a x ( β 2 ∗ v t − 1 + ( 1 − β 2 ) g t 2 , v t − 1 ) v_t=max(\beta_2*v_{t-1}+(1-\beta_2)g_t^2,v_{t-1}) vt=max(β2vt1+(1β2)gt2,vt1)
这样就保证了 ∣ ∣ v t ∣ ∣ ≥ ∣ ∣ v t − 1 ∣ ∣ ||v_t|| \geq ||v_{t-1}|| vtvt1,使得学习率单调递减。

Adam的罪状二

深度神经网络往往包含大量的参数,在这样一个维度极高的空间内,非凸的目标函数往往起起伏伏,拥有无数个高地和洼地。有的是高峯,通过引入动量可能很容易越过;但有些是高原,可能探索很多次都出不来,于是停止了训练。

近期Arxiv上的两篇文章谈到这个问题。第一篇就是前文提到的吐槽Adam最狠的 The Marginal Value of Adaptive Gradient Methods in Machine Learning 。文中说到,同样的一个优化问题,不同的优化算法可能会找到不同的答案,但自适应学习率的算法往往找到非常差的答案。他们通过一个特定的数据例子说明,自适应学习率算法可能会对前期出现的特征过拟合,后期才出现的特征很难纠正前期的拟合效果。

另外一篇是 Improving Generalization Performance by Switching from Adam to SGD,进行了实验验证。他们CIFAR-10数据集上进行测试,Adam的收敛速度比SGD要快,但最终收敛的结果并没有SGD好。他们进一步实验发现,主要是后期Adam的学习率太低,影响了有效的收敛。他们试着对Adam的学习率的下界进行控制,发现效果好了很多。

于是他们提出了一个用来改进Adam的方法:前期用Adam,享受Adam快速收敛的优势;后期切换到SGD,慢慢寻找最优解。这一方法以前也被研究者们用到,不过主要是根据经验来选择切换的时机和切换后的学习率。这篇文章把这一切换过程傻瓜化,给出了切换SGD的时机选择方法,以及学习率的计算方法,效果看起来也不错。

到底该用Adam还是SGD?

所以,谈到现在,到底Adam好还是SGD好?这可能是很难一句话说清楚的事情。去看学术会议中的各种paper,用SGD的很多,Adam的也不少,还有很多偏爱AdaGrad或者AdaDelta。可能研究员把每个算法都试了一遍,哪个出来的效果好就用哪个了。

而从这几篇怒怼Adam的paper来看,多数都构造了一些比较极端的例子来演示了Adam失效的可能性。这些例子一般过于极端,实际情况中可能未必会这样,但这提醒了我们,理解数据对于设计算法的必要性。优化算法的演变历史,都是基于对数据的某种假设而进行的优化,那么某种算法是否有效,就要看你的数据是否符合该算法的胃口了。算法固然美好,数据才是根本。另一方面,Adam之流虽然说已经简化了调参,但是并没有一劳永逸地解决问题,默认参数虽然好,但也不是放之四海而皆准。因此,在充分理解数据的基础上,依然需要根据数据特性、算法特性进行充分的调参实验,找到最优解。

2. 学习率衰减

在训练模型的时候,通常会遇到这种情况:我们平衡模型的训练速度和损失后选择了相对合适的学习率,但是训练集的损失下降到一定的程度后就不再下降了,最后最小值在附近摆动,不会精确地收敛,这是因为mini-batch中有噪声。遇到这种情况通常可以通过适当降低学习率来实现。但是,降低学习率又会延长训练所需的时间。

学习率衰减(learning rate decay)就是一种可以平衡这两者之间矛盾的解决方案。学习率衰减的基本思想是:学习率随着训练的进行逐渐衰减。
几种梯度衰减的方式:

  • α = 1 1 + d e c a y   r a t e ∗ e p o c h − n u m α 0 \alpha=\frac{1}{1+decay\ rate*epoch-num}\alpha_0 α=1+decay rateepochnum1α0 (decay rate为衰减率)
  • 指数衰减: α = 0.9 5 e p o c h − n u m α 0 \alpha=0.95^{epoch-num} \alpha_0 α=0.95epochnumα0
  • α = k e p o c h − n u m α 0 \alpha=\frac{k}{\sqrt{epoch-num}}\alpha_0 α=epochnum kα0
  • 离散下降:一次衰减一半

3.局部最优

在深度学习研究早期,人们总是担心优化算法会困在极差的局部最优,不过随着深度学习理论不断发展,对局部最优的理解也发生了改变。
《优化方法总结以及Adam存在的问题(SGD, Momentum, AdaDelta, Adam, AdamW,LazyAdam)》
这是曾经在想到局部最优时脑海里会出现的图,平面的高度就是损失函数。在图中似乎各处都分布着局部最优。梯度下降法或者某个算法可能困在一个局部最优中,而不会抵达全局最优。比如说这两个维度,就容易出现有多个不同局部最优的图,而这些低维的图曾经影响了我们的理解,但是这些理解并不正确。事实上,一个神经网络中通常梯度为零的点并不是这个图中的局部最优点,实际上损失函数的零梯度点,通常是鞍点。

在一个具有高维度空间的函数,如果梯度为0,那么在每个方向,它可能是凸函数,也可能是凹函数。如果你在2万维空间中,那么想要得到局部最优,所有的2万个方向都需要是凹函数,发生的机率也许很小,更有可能遇到的是有些方向的曲线会向上弯曲,另一些方向曲线向下弯,而不是所有的都向上弯曲,因此在高维度空间更可能碰到鞍点,导数为0的点,像下面这种:
《优化方法总结以及Adam存在的问题(SGD, Momentum, AdaDelta, Adam, AdamW,LazyAdam)》
如果在训练较大的神经网络,存在大量参数,并且成本函数被定义在较高的维度空间,那么不太可能困在极差的局部最优中。如果局部最优不是问题,那么问题是什么?结果是平稳段会减缓学习,平稳段是一块区域,其中导数长时间接近于0,如果你在此处,梯度会从曲面从上向下下降,因为梯度等于或接近0,曲面很平坦,你得花上很长时间慢慢抵达平稳段的这个点。
《优化方法总结以及Adam存在的问题(SGD, Momentum, AdaDelta, Adam, AdamW,LazyAdam)》

这样使得学习十分缓慢,这也是像Momentum或是RMSprop,Adam这样的算法,能够加速学习算法的地方。在这些情况下,更成熟的优化算法,如Adam算法,能够加快速度尽早走出平稳段。

参考资料:

  1. 深度学习笔记
  2. L2正则=Weight Decay?并不是这样
  3. 权重衰减(weight decay)与学习率衰减(learning rate decay)
  4. 都9102年了,别再用Adam + L2 regularization了
  5. DECOUPLED WEIGHT DECAY REGULARIZATION
  6. Adam那么棒,为什么还对SGD念念不忘 (2)—— Adam的两宗罪
点赞