Lookahead、LazyOptimizer、MaskedAdamOptimizer、AdaBound

AdaBound算法:像Adam一样快,又像SGD一样好的优化器

论文地址

https://openreview.net/pdf?id=Bkg3g2R9FX

GitHub地址:(Pytorch)

https://github.com/Luolc/AdaBound

GitHub地址:(Tensorflow)

https://github.com/taki0112/AdaBound-Tensorflow

 

SGD的缺点:


SGD现在后期调优时还是经常使用到,但SGD的问题是前期收敛速度慢。SGD前期收敛慢的原因: SGD在更新参数时对各个维度上梯度的放缩是一致的,并且在训练数据分布极不均很时训练效果很差。而因为收敛慢的问题应运而生的自适应优化算法Adam、AdaGrad、RMSprop 等,但这些自适应的优化算法虽然可以在训练早期展现出快速的收敛速度,但其在测试集上的表现却会很快陷入停滞,并最终被 SGD 超过。

Adam等自适应学习率算法缺点:


这就是目前很多大牛任然喜欢SGD的原因。这篇文章对于Adam后期的毛病进行了分析,原因出在自适应方法训练后期不稳定的极端学习率。换句话说,就是自适应学习率训练到后期,学习率出现极端情况,更新参数时有些维度上学习率特别大,有些维度学习率特别小。

 采样参数的学习率,每个单元格包含一个通过对学习率进行数值运算得到的值,颜色越浅代表学习率越小。

我们可以看到,当模型接近收敛时,学习率中有大量的极端值(包含许多小于 0.01 和大于 1000 的情况)。这一现象表明在实际训练中,极端学习率是实际存在的。

发现这个问题怎么解决?如何融合上面两种方法的优点?


那就对自适应学习率加一下限制吧。具体做法是对学习率进行动态裁剪,在这一设置下,在训练早期由于上下界对学习率的影响很小,算法更加接近于 Adam;而随着时间增长裁减区间越来越收紧,模型的学习率逐渐趋于稳定,在末期更加贴近于 SGD。AMSBound 可以对 AMSGrad 采用类似的裁剪得到。

换句话说,Adam和SGD是AdaBound的特殊情况。

在这一设置下,在训练早期由于上下界对学习率的影响很小,算法更加接近于 Adam;而随着时间增长裁减区间越来越收紧,模型的学习率逐渐趋于稳定,在末期更加贴近于 SGD。AMSBound 可以对 AMSGrad 采用类似的裁剪得到。

 

Adam作者大革新, 联合Hinton等人推出全新优化方法Lookahead

论文地址

https://arxiv.org/abs/1907.08610v1

GitHub地址:(Pytorch)

https://github.com/alphadl/lookahead.pytorch

GitHub地址:(Tensorflow)

https://github.com/Janus-Shiau/lookahead_tensorflow

Lookahead的思路很朴素,准确来说它并不是一个优化器,而是一个使用现有优化器的方案。简单来说它就是下面三个步骤的循环执行:

附:《机器之心的Lookahead的介绍》

LazyAdam、MaskedAdamOptimize

LazyAdam

和图像等领域不同,对 NLU 之类的任务,每个 batch 采样到的词有限,每次更新对 Embedding 的梯度估计都是稀疏的。非 momentum-based 的 Optimizer 每步只会更新采样到的词,而对于所有带动量的优化器(自然也就包括Adam以及带动量的SGD)都存在一个问题:当前batch中没被采样到的词,依然会使用历史动量来更新,这可能导致Embedding层过拟合。具体来说,当一个词的被采样过后,它的Embedding的梯度不为0,这个梯度也会被记录在动量中,实际更新是用动量去更新的;在后面的batch中,假如该词没有被采样到,它的Embedding的梯度为0,但是它的动量并不为0,所以该词还是被更新了。这样一来就算没有被反复采样的词,对应的Embedding也被反复更新了,就导致了过拟合。

所以,一个改进的方案是只有当该词被采样过才更新,这就是LazyOptimizer的基本原理了。

LazyAdam是Adam的变体,可以更有效地处理稀疏更新。原始的Adam算法为每个可训练变量维护两个移动平均累加器,累加器在每一步都会更新**。 而此类为稀疏变量提供了更加懒惰的梯度更新处理,它仅更新当前batch中出现的稀疏变量索引的移动平均累加器,而不是更新所有索引的累加器。 与原始的Adam优化器相比,它可以为某些应用提供模型训练吞吐量的大幅改进。 但是它的语义与原始的Adam算法略有不同,可能会导致不同的实验结果。

在实现上,我们要如何判断一个词有没有被采样过呢?当然终极方法肯定是传入被采样过的词的index了,但这使用上不够友好。我这里使用了一个近似的方法:判断该词的Embedding对应的梯度是否为0,如果为0意味着它“很可能”在当前batch没有被采样到。背后的原理在于,如果它没有被采样到,那么梯度一定为0,如果它被采样到了,那么梯度为0的概率是非常小的,毕竟那么多分量,同时为0的可能性很小,所以这样实现也够用了。

AdamOptimizer源码中函数_apply_sparse和_resource_apply_sparse 主要用在稀疏向量的更新操作上,而具体的实现是在函数_apply_sparse_shared中

LazyAdam的源码:

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来对一阶动量和二阶动量进行更新。

Madam

from tensorflow.python.ops import array_ops
from tensorflow.python.training import adam
from tensorflow.python.framework import ops
from tensorflow.python.ops import control_flow_ops
from tensorflow.python.ops import math_ops
from tensorflow.python.ops import resource_variable_ops
from tensorflow.python.ops import state_ops
from tensorflow.python.ops import variable_scope
from tensorflow.python.training import optimizer

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

两者在计算移动平均累加器时(一阶动量和二阶动量)有所不同:

LazyAdam:

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)

Madam:

m_scaled_g_Values = grad * (1 - beta1_t)
        m_t = state_ops.assign(m, m * beta1_t,
                               use_locking=self._use_locking)  
        with ops.control_dependencies([m_t]):
            m_t = scatter_add(m, indices, m_scaled_g_Values)

Madam其实是介于Lazy Adam和 Adam之间的一种方法,其与Lazy Adam唯一的不同在于对一阶动量m和二阶动量 V 进行 decay 的操作,Madam是全部都要 decay,即当前batch没有采样到的变量所对应的之前动量的累积值也要考虑。 而LazyAdam 是只 decay 采样到的embedding。(在计算指数加权平均时,LazyAdam只对当前采样到的变量之前的平均值进行累加,没有采样到的样本不累加,而Madam要全部累加)。

LazyAdam存在的一个问题是当梯度为0时不更新对应的m和v。实际上当其他权重改变时m和v应该更新。Madam应该是解决了这个问题所以性能变得更好。

为了更形象的说明它们的差异,通过一个假设的例子来说明,用一阶动量来举例:


参考文章:https://kexue.fm/archives/6869

  • 0
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值