NAdam 和 RAdam:Adam 的两个改进版,修正了原始 Adam 的什么缺陷?

你大概也和我一样,拿到一个新任务,第一件事就是把优化器设成 Adam。它比 SGD 快,比 Adagrad 稳,几乎不需要调参。但有一次,我训练一个文本分类模型,把学习率设成 1e-3,结果 Loss 在头几轮剧烈震荡,然后直接变成 NaN。我排查了一整天,最后发现问题不在代码,而在 Adam 自身的启动阶段。

AI technology illustration

NAdam 和 RAdam 是两个针对 Adam 的改进版,它们修的问题并不一样。但要想理解它们,你得先接受一个事实:Adam 的「自适应」在训练早期其实是在放大噪声,而不是适应梯度。

Adam 的「自适应」是怎么来的?

Adam 的更新公式只有三行核心代码。第一行,维护一个动量项 m_t,记录梯度的指数移动平均;第二行,维护一个二阶矩 v_t,记录梯度平方的指数移动平均;第三行,用 m_t 除以 sqrt(v_t) 来标准化梯度方向,再乘以学习率。

m_t = beta1*m_{t-1} + (1-beta1)*g_t
v_t = beta2*v_{t-1} + (1-beta2)*g_t^2
theta_{t+1} = theta_t - lr * m_t / (sqrt(v_t) + eps)

这里 m_t 相当于惯性,告诉你之前大概率在往哪走;v_t 相当于梯度的大小,如果一个维度老是出现大梯度,v_t 就会变大,从而减小这个方向的步长——这就是「自适应学习率」的来源。

但这里有个严重的初始偏差:因为 m_0=0v_0=0,前几步的统计量都会被低估。原始 Adam 用了一个偏差修正,把前几步的数值除以 1-beta^t 来放大。这个修正做的其实是「均值修正」——但它修不了方差。

Adam 的一号缺陷:二阶矩在启动时方差太大

问题出在 v_t 上。假设你训练的模型参数很多,每个维度上的梯度大小和方向差异很大。在训练刚开始的几步,v_t 只是基于 2、3 个样本的平方平均,它的估计方差极大。当你用 sqrt(v_t) 去除梯度时,等于用一个非常不稳定的数字去缩放步长。

结果就是:有些维度上的学习率被分母的抖动放大到离谱,参数更新的第一步就可能迈出十万八千里。这就是为什么用 Adam 时,初始学习率稍微调大一点,Loss 就直接爆掉。

RAdam 的论文里放了一张图,展示了二阶矩的方差随步数 t 的变化:在 t 小于 10 时,方差大到无法直视;直到 t 超过几十步,才慢慢稳定。也就是说,Adam 在它最脆弱的启动阶段,用了最不可靠的缩放因子。

Adam 的二号缺陷:动量的反应总是慢半拍

另一个问题来自动量项 m_t。Adam 的动量项是历史梯度的指数平均值,它是「落后」于真实梯度的。当优化方向突然改变(比如越过一个山谷,或者遇到一个陡坡),动量项会拖着你往原来的方向继续冲,这就是惯性过冲。

传统 SGD 阵营早就有一个解决办法,叫 Nesterov 动量,也叫 Nesterov 加速梯度。它的核心思想是:先沿着动量方向走一小步,看看那里的梯度是什么,再用那个「未来位置」的梯度来修正当前的方向。相当于开车过弯时,先稍微打一把方向盘,再根据车头指向调整,而不是等冲出弯道才反应。

而 Adam 用的只是经典动量,没有这种预判机制。这就是 NAdam 要补的洞。

NAdam:给 Adam 配上 Nesterov 预判

NAdam 是 Dozat 在 2016 年的论文 Incorporating Nesterov Momentum into Adam 中提出的。它的做法很简单:把 Adam 里的动量更新改成 Nesterov 风格。

具体来说,Adam 的更新方向就是 m_t;NAdam 在计算当前步时,把当前梯度的作用也同时算进去,相当于先「预演」一下动量的走向,再决定这步迈多大。这个改动在数学上对应了 Nesterov 的前瞻步骤。

实际效果是:NAdam 在梯度方向快速变化时,能比 Adam 更快地「刹住车」并转向。它和 Adam 的收敛速度差不多,但在某些任务上更稳,尤其是在有噪声的梯度环境中。

注意,NAdam 并没有解决二阶矩方差的问题。它在前期照样可能因为 sqrt(v_t) 抖动而出现尖刺。如果给 Adam 的两个缺陷排个序,NAdam 只填了后面那个。

RAdam:训练前期先老老实实当 SGD

RAdam 来自 2019 年论文 On the Variance of the Adaptive Learning Rate and Beyond,作者是当时在 Salesforce 的 Liu 等人。它的观点非常直接:既然早期 v_t 的估计不可靠,那就别用自适应。

RAdam 计算了一个「整流系数」r_t。训练早期 r_t 远小于 1,相当于把自适应步长强行缩小;当统计量变得可靠后,r_t 逐渐逼近 1,优化器在后期恢复为标准 Adam。这就是「整流」一词的含义。

论文的具体做法是设置一个阈值 rho_t。如果它小于等于 4,说明 v_t 的方差太大,此时 RAdam 干脆退化为带动量的 SGD;一旦超过 4,才启用自适应学习率,并乘上一个整流系数来修正残余偏差。用一句话概括:在 Adam 最不靠谱的早期,强制它使用最朴素的更新方式。

# RAdam 简化伪代码(概念示意,非精确公式)
rho_t = rho_inf - 2*t*beta2^t/(1-beta2^t)
if rho_t > 4:
r_t = compute_rectification(rho_t) # 0 到 1 之间
step = lr / (sqrt(v_hat_t) * r_t)
else:
step = lr # 退化为 SGD
theta_{t+1} = theta_t - step * m_hat_t

这里有一个我个人的认知转变:我最早以为 RAdam 只是把 Adam 的学习率调小一点,后来看了论文才明白,它是在「用不用自适应」之间做硬切换。这种激进的做法反而很有效,因为它承认了一个事实——前期数据太少,任何自适应都是自欺欺人。

一张表说清 Adam、NAdam、RAdam 的区别

优化器 修动量延迟 修二阶矩方差 核心机制
Adam 经典动量 + 自适应学习率
NAdam Nesterov 动量,预判方向
RAdam 前期退化为 SGD,后期启用自适应

从表里可以看到,它们修的东西完全不同,所以不存在谁取代谁。事实上,你完全可以写一个结合两者优点的优化器(已有研究这样做),只是在大多数框架里,你只能选择其中一个。

那实际项目里到底该用哪个?

  • 如果你从零训练一个深层网络,数据不是特别充足,训练初期容易爆 Loss,推荐 RAdam。它能让你把初始学习率调大一点,不用担心前期震荡。
  • 如果你训练的是 GAN 或其它方向频繁变化的模型,NAdam 的预判动量可能更快把 Loss 打下来。
  • 如果你习惯用带权重衰减的 Adam,那么可以考虑 NAdamWRAdamW,PyTorch 里都有实现。

但实话实说,在这两个改进出现之后,大多数人的默认优化器并没有改变。因为一个更工程化的解决方案——学习率预热(warmup)——已经可以有效缓解早期方差问题。你可以把 warmup 看作是 RAdam 的「手动挡」,RAdam 则是「自动挡」。而 NAdam 的收益在大规模任务上并不总是肉眼可见。

理解它们依然有价值。因为这么一点点数学上的调整,背后反映的是对优化器假设的重新审视:自适应学习率并不是万能的,它有自己的前提条件。下次再看到训练初期 Loss 诡异抖动,你至少知道该往哪个方向排查,而不是像我当年一样,傻乎乎地怀疑自己的代码。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/393.html

(0)
上一篇 4天前
下一篇 4天前

相关推荐