©PaperWeekly 原创 · 作者 | 苏剑林
单位 | 追一科技
研究方向 | NLP、神经网络
这段时间笔者一直在实验《Google新搜出的优化器Lion:效率与效果兼得的“训练狮”》所介绍的 Lion 优化器。之所以对 Lion 饶有兴致,是因为它跟笔者之前的关于理想优化器的一些想法不谋而合,但当时笔者没有调出好的效果,而 Lion 则做好了。
相比标准的 Lion,笔者更感兴趣的是它在 时的特殊例子,这里称之为“Tiger”。Tiger 只用到了动量来构建更新量,根据《隐藏在动量中的梯度累积:少更新几步,效果反而更好?》[1] 的结论,此时我们不新增一组参数来“无感”地实现梯度累积!这也意味着在我们有梯度累积需求时,Tiger 已经达到了显存占用的最优解,这也是“Tiger”这个名字的来源(Tight-fisted Optimizer,抠门的优化器,不舍得多花一点显存)。此外,Tiger 还加入了我们的一些超参数调节经验,以及提出了一个防止模型出现 NaN(尤其是混合精度训练下)的简单策略。我们的初步实验显示,Tiger 的这些改动,能够更加友好地完成模型(尤其是大模型)的训练。
相比 Lion,它就是选择了参数 ;相比 SignSGD [2],它则是新增了动量和权重衰减。https://github.com/bojone/tiger下表对比了 Tiger、Lion 和 AdamW 的更新规则:尽管 Tiger 已经相当简化,但仍有几个超参数要设置,分别是滑动平均率 、学习率 以及权重衰减率 ,下面我们分别讨论这几个参数的选择。比较简单的是 。我们知道,在基本形式上 Tiger 相当于 Lion 取 的特殊情形,那么一个直觉是 Tiger 应当取 。在 Lion 的原论文中,对于 CV 任务有 ,所以我们建议 CV 任务取 ;而对于 NLP 任务则有 ,所以我们建议 NLP 任务取 。对于学习率,Tiger 参考了 Amos、LAMB [3] 等工作,将学习率分两种情况设置。第一种是线性层的 bias 项和 Normalization 的 beta、gamma 参数,这类参数的特点是运算是 element-wise,我们建议学习率选取为全局相对学习率 的一半;第二种主要就是线性层的 kernel 矩阵,这类参数的特点是以矩阵的身份去跟向量做矩阵乘法运算,我们建议学习率选取为全局相对学习率 乘以参数本身的 (Root Mean Square):这样设置的好处是我们把参数的尺度分离了出来,使得学习率的调控可以交给一个比较通用的“全局相对学习率” ——大致可以理解为每一步的相对学习幅度,是一个对于模型尺度不是特别敏感的量。换句话说,我们在 base 版模型上调好的 ,基本上可以不改动地用到 large 版模型。注意 带有下标 ,所以它包含了整个学习率的 schedule,包括 Wamrup 以及学习率衰减策略等,笔者的设置经验是 ,至于怎么 Warmup 和衰减,那就是大家根据自己的任务而设了,别人无法代劳。笔者给的 tiger 实现,内置了一个分段线性学习率策略,理论上可以用它模拟任意的 。最后是权重衰减率 ,这个 Lion 论文最后一页也给出了一些参考设置,一般来说 也就设为常数,笔者常用的是 0.01。特别的是,不建议对前面说的 bias、beta、gamma 这三类参数做权重衰减,或者即便要做, 也要低一个数量级以上。因为从先验分布角度来看,权重衰减是参数的高斯先验, 跟参数方差是反比关系,而 bias、beta、gamma 的方差显然要比 kernel 矩阵的方差大,所以它们的 应该更小。
对于很多算力有限的读者来说,通过梯度累积来增大 batch_size 是训练大模型时不可避免的一步。标准的梯度累积需要新增一组参数,用来缓存历史梯度,这意味着在梯度累积的需求下,Adam 新增的参数是 3 组,Lion 是 2 组,而即便是不加动量的 AdaFactor [4] 也有 1.x 组(但说实话 AdaFactor 不加动量,收敛会慢很多,所以考虑速度的话,加一组动量就变为 2.x 组)。
而对于 Tiger 来说,它的更新量只用到了动量和原参数,根据《隐藏在动量中的梯度累积:少更新几步,效果反而更好?》[1],我们可以通过如下改动,将梯度累积内置在 Tiger 中:可以看到,这仅仅相当于修改了滑动平均率 和学习率 ,几乎不增加显存成本,整个过程是完全“无感”的,这是笔者认为的 Tiger 的最大的魅力。需要指出的是,尽管 Lion 跟 Tiger 很相似,但是 Lion 并不能做到这一点,因为 时,Lion 的更新需要用到动量以及当前批的梯度,这两个量需要用不同的参数缓存,而 Tiger 的更新只用到了动量,因此满足这一点。类似滴,SGDM 优化器也能做到一点,但是它没有 操作,这意味着学习率的自适应能力不够好,在 Transformer 等模型上的效果通常不如意(参考《Why are Adaptive Methods Good for Attention Models?》[5])。
对于大模型来说,混合精度训练是另一个常用的“利器”(参考《在 bert4keras 中使用混合精度和 XLA 加速训练》[6])。混合精度,简单来说就是模型计算部分用半精度的 FP16,模型参数的储存和更新部分用单精度的 FP32。之所以模型参数要用 FP32,是因为担心更新过程中参数的更新量过小,下溢出了 FP16 的表示范围(大致是 ),导致某些参数长期不更新,模型训练进度慢甚至无法正常训练。然而,Tiger(Lion 也一样)对更新量做了 运算,这使得理论上我们可以全用半精度训练!分析过程并不难。首先,只要对 Loss 做适当的缩放,那么可以做到梯度 不会溢出 FP16 的表示范围;而动量 只是梯度的滑动平均,梯度不溢出,它也不会溢出, 只能是 ,更加不会溢出了;之后,我们只需要保证学习率不小于 ,那么更新量就不会下溢了,事实上我们也不会将学习率调得这么小。因此,Tiger 的整个更新过程都是在 FP16 表示范围内的,因此理论上我们可以直接用全 FP16 精度训练而不用担心溢出问题。
然而,笔者发现对于同样的配置,在 FP32 下训练正常,但切换到混合精度或者半精度后有时会训练失败,具体表现后 Loss 先降后升然后 NaN,这我们之前在《在 bert4keras 中使用混合精度和 XLA 加速训练》[6] 也讨论过。虽然有一些排查改进的方向(比如调节 epsilon 和无穷大的值、缩放 loss 等),但有时候把该排查的都排查了,还会出现这样的情况。
经过调试,笔者发现出现这种情况时,主要是对于某些 batch 梯度变为 NaN,但此时模型的参数和前向计算还是正常的。于是笔者就想了个简单的应对策略:对梯度出现 NaN 时,跳过这一步更新,并且对参数进行轻微收缩,如下其中 代表收缩率,笔者取 , 则是参数的初始化中心,一般就是 gamma 取 1,其他参数都是 0。经过这样处理后,模型的 loss 会有轻微上升,但一般能够恢复正常训练,不至于从头再来。个人的实验结果显示,这样处理能够缓解一部分 NaN 的问题。当然,该技巧一般的使用场景是同样配置下 FP32 能够正常训练,并且已经做好了 epsilon、无穷大等混合精度调节,万般无奈之下才不得已使用的。如果模型本身超参数设置有问题(比如学习率过大),连 FP32 都会训练到 NaN,那么就不要指望这个技巧能够解决问题了。此外,有兴趣的读者,还可以尝试改进这个技巧,比如收缩之后可以再加上一点噪声来增加参数的多样性,等等。
不考虑梯度累积带来的显存优化,Tiger 就是 Lion 的一个特例,可以预估 Tiger 的最佳效果肯定是不如 Lion 的最佳效果的,那么效果下降的幅度是否在可接受范围内呢?综合到目前为止多方的实验结果,笔者暂时得出的结论是:也就是说,考虑效果 Lion 最优,考虑显存占用 Tiger 最优(启用梯度累积时),效果上 Tiger 不逊色于 AdamW,所以 Tiger 替代 AdamW 时没有太大问题的。
具体实验结果包括几部分。第一部分实验来自 Lion 的论文《Symbolic Discovery of Optimization Algorithms》[7],论文中的 Figure 12 对比了 Lion、Tiger、AdamW 在不同尺寸的语言模型上的效果:▲ Lion、Tiger(Ablation)、AdamW 在语言模型任务上的对比这里的 Ablation0.95、Ablation0.98,就是 Tiger的 分别取 0.95、0.98。可以看到,对于 small级别模型,两个 Tiger 持平 AdamW,而在 middle 和 large 级别上,两个 Tiger 都超过了 AdamW。但正如前面所说, 取两者的均值 0.965,有可能还会有进一步的提升。至于在 CV 任务上,原论文给出了 Table 7:▲ Lion、Tiger(Ablation)、AdamW在图像分类任务上的对比
同样地,这里的 Ablation0.9、Ablation0.99,就是 Tiger 的 分别取 0.9、0.99。在这个表中,Tiger 跟 AdamW 有明显差距。但是考虑到作者只实验了 0.9、0.99 两个 ,而笔者推荐的是 ,所以笔者跟原作者取得了联系,请他们做了补充实验,他们回复的结果是“ 分别取 0.92、0.95、0.98 时,在 ViT-B/16 上 ImageNet 的结果都是 80.0% 左右”,那么对比上图,就可以确定在精调 时,在 CV 任务上 Tiger 应该也可以追平 AdamW 的。最后是笔者自己的实验。笔者常用的是 LAMB 优化器,它的效果基本跟 AdamW 持平,但相对更稳定,而且对不同的初始化适应性更好,因此笔者更乐意使用 LAMB。特别地,LAMB 的学习率设置可以完全不改动地搬到 Tiger 中。笔者用 Tiger 重新训练了之前的 base 版 GAU-α 模型,训练曲线跟之前的对比如下:▲ 笔者在GAU-α上的对比实验(accuracy曲线)可以看到,Tiger 确实可以取得比 LAMB 更优异的表现。
Tiger 还有改进空间吗?肯定有,想法其实有很多,但都没来得及一一验证,大家有兴趣的可以帮忙继续做下去。
Lion 通过 操作平等地对待了每一个分量,使得模型充分地发挥了每一个分量的作用,从而有更好的泛化性能。如果是 SGD,那么更新的大小正比于它的梯度,然而有些分量梯度小,可能仅仅是因为它没初始化好,而并非它不重要,所以 Lion 的 操作算是为每个参数都提供了“恢复活力”甚至“再创辉煌”的机会。然而,细思之下就会发现,这里其实有一个改进空间。“平等地对待了每一个分量”在训练的开始阶段是很合理的,它保留了模型尽可能多的可能。然而,如果一个参数长时间的梯度都很小,那么很有可能这个参数真的是“烂泥扶不上墙”,即已经优化到尽头了,这时候如果还是“平等地对待了每一个分量”,那么就对那些梯度依然较大的“上进生”分量不公平了,而且很可能导致模型震荡。
一个符合直觉的想法是,优化器应该随着训练的推进,慢慢从 Tiger 退化为 SGD。为此,我们可以考虑将更新量设置为这里的绝对值和幂运算都是 element-wise 的, 是从 1 到 0 的单调递减函数,当 时对应 Tiger,当 时对应 SGDM。可能读者会吐槽这里多了 这个 schedule 要调整,问题变得复杂很多。确实如此,如果将它独立地进行调参,那么确实会引入过多的复杂度了。但我们不妨再仔细回忆一下,抛开 Warmup 阶段不算,一般情况下相对学习率 不正是一个单调递减至零的函数?我们是否可以借助 来设计 呢?比如 不正好是一个从 1 到 0 的单调递减函数?能否用它来作为 ?当然也有可能是 、 更好,调参空间还是有的,但至少我们不用重新设计横跨整个训练进程的 schedule 了。更发散一些,既然有时候学习率我们也可以用非单调的 schedule(比如带 restart的 cosine annealing),那么 我们是否也可以用非单调的(相当于 Tiger、SGDM 反复切换)?这些想法都有待验证。
在这篇文章中,我们提出了一个新的优化器,名为 Tiger(Tight-fisted Optimizer,抠门的优化器),它在 Lion 的基础上做了一些简化,并加入了我们的一些超参数经验。特别地,在需要梯度累积的场景下,Tiger 可以达到显存占用的理论最优(抠)解![1] https://kexue.fm/archives/8634
[2] https://arxiv.org/abs/1802.04434
[3] https://kexue.fm/archives/7094#层自适应
[4] https://kexue.fm/archives/7302
[5] https://arxiv.org/abs/1912.03194
[6] https://kexue.fm/archives/9059
[7] https://arxiv.org/abs/2302.06675
🔍
现在,在「知乎」也能找到我们了
进入知乎首页搜索「PaperWeekly」
点击「关注」订阅我们的专栏吧