梯度告诉你往哪里走,optimizer 决定怎么走。它可能保留上几步的方向,也可能按每个坐标过去的变化调整步长。把这两件事分开,很多名字就不难记了。

以下公式讨论常见的实数、稠密参数、最小化设置。$t$ 表示一次 optimizer update,不是一个 epoch;$g_t=\nabla L_t(\theta_{t-1})$。所有平方、除法和平方根默认按坐标进行。

本文先讲逐坐标更新。想知道矩阵优化器改了什么,可接着读 Muon:梯度是一张矩阵,不必为了入门先读完它。

SGD:先学会看学习率

最简单的更新是 $\theta_t=\theta_{t-1}-\eta g_t$。对 $L(w)=\frac12w^2$,就变成

$$w_t=(1-\eta)w_{t-1}.$$

从 $w_0=1$ 出发:$\eta=0.5$ 得到 1 → 0.5 → 0.25;$\eta=1.5$ 得到 1 → -0.5 → 0.25,虽然来回摆,仍收敛;$\eta=2.1$ 得到 1 → -1.1 → 1.21,越走越远。这个一维凸例子的稳定区间是 $0<\eta<2$,不应原样套给神经网络。

随机 / mini-batch 梯度还带有采样噪声。SGD 不只是“会卡在局部最优”,Momentum 也不是“保证跳出局部最优”的按钮。

Momentum 与 Nesterov:方向要不要有记忆?

一种常见的 Momentum 约定是

$$v_t=\mu v_{t-1}+g_t,\qquad \theta_t=\theta_{t-1}-\eta v_t.$$

方向一致的梯度会积累,来回反向的部分会抵消一些。有的推导在 $g_t$ 前乘 $1-\mu$,那是另一种缩放约定;学习率不能不加说明地照搬。框架还可能对首步动量做特殊处理。

Nesterov 的直观写法是先在预测位置 $\theta_{t-1}-\eta\mu v_{t-1}$ 求梯度,再形成更新。实现可以通过重参数化避免真的临时移动权重。它是在特定假设下改善优化过程,不是提前知道下一步真实梯度。PyTorch SGD说明了实现与部分论文公式的差别。

自适应步长:为什么要记住平方梯度

设两个坐标的梯度分别是 1 和 100。用相同学习率会给后者大 100 倍的更新,但这不一定是合理的尺度。自适应方法利用历史平方梯度调整每个坐标的步长。

方法 保存什么 主要取舍
AdaGrad $s_t=s_{t-1}+g_t^2$ 累积量只增不减;长期更新的坐标步长可能越来越小
RMSProp $s_t=\rho s_{t-1}+(1-\rho)g_t^2$ 更关注近期尺度;不是完整 Hessian
AdaDelta 梯度平方与更新量平方的滑动平均 用更新尺度 / 梯度尺度形成比值,不只是把 AdaGrad 换成 EMA
Adam 一阶矩 $m_t$ 与二阶矩 $v_t$ 同时平滑方向、调整尺度;还有偏差修正与参数开销

AdaGrad 常见分母是 $\sqrt{s_t}+\epsilon$。平方累积较少的坐标可能得到更大有效步长,但“出现次数少”不完全等于“平方和小”。AdaGrad 原论文讨论的是利用数据几何结构自适应调整。

AdaDelta 的 $s_t$ 用 RMSProp 那种梯度平方 EMA(不是上面 AdaGrad 的累加和),另外还维护 $u_t=\rho u_{t-1}+(1-\rho)\Delta_t^2$,用

$$\Delta_t=-\frac{\sqrt{u_{t-1}+\epsilon}}{\sqrt{s_t+\epsilon}}g_t.$$

它与只保存梯度平方 EMA 的基本 RMSProp 不相同。实际库可能再提供 learning-rate 系数,不能把“原始推导减少手动学习率需求”理解成所有实现都没有学习率。见 AdaDelta。

Adam:为什么第一步要做偏差修正

从 $m_0=v_0=0$ 开始:

$$m_t=\beta_1m_{t-1}+(1-\beta_1)g_t,\qquad v_t=\beta_2v_{t-1}+(1-\beta_2)g_t^2.$$

零初始化会使早期 EMA 偏小,因此使用

$$\hat m_t=\frac{m_t}{1-\beta_1^t},\quad \hat v_t=\frac{v_t}{1-\beta_2^t},\quad \theta_t=\theta_{t-1}-\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon}.$$

若首步 $g_1=2,\beta_1=0.9,\beta_2=0.999$,原始状态为 $m_1=0.2,v_1=0.004$,修正后为 2 和 4。忽略 epsilon,更新幅度约为 $\eta$。这不是以后每步都固定长度:梯度方向变化、历史状态和 epsilon 都会影响结果。

Adam 来自 adaptive moment estimation,不是 AdaDelta 与 Momentum 名字拼接;二阶矩也不是二阶导数,Adam 不是 Newton 法。Adam 原论文给出算法与修正。NAdam 再把 Nesterov 思路结合进来,但不存在对所有任务都最好的“终极形态”。

AdamW:weight decay 为什么要拿出来单独做

对没有 Momentum 的普通 SGD,把平方 L2 的梯度 $\lambda\theta$ 加入 $g$,有

$$\theta'=(1-\eta\lambda)\theta-\eta g.$$

这与直接衰减权重等价。但如果先把 $g+\lambda\theta$ 送进 Adam 的两个状态,惩罚也会改变历史矩和按坐标缩放,通常不再等价。

AdamW 把衰减与自适应梯度更新分开:

$$\theta_t=(1-\eta\lambda)\theta_{t-1} -\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon}.$$

用全新状态、$\theta=2$、数据梯度为 0、$\eta=0.1,\lambda=0.2$ 举例:AdamW 只衰减到 1.96。把 L2 梯度 0.4 送进 Adam,则首步约到 1.9。不是同一个更新。Decoupled Weight Decay专门区分了这两件事。

动画 AdamW 与 Adam + L2 的第一步

这张图需要启用 JavaScript。

import math

def adamw_scalar(parameter, gradient, state, learning_rate=0.1,
                 decay=0.0, beta1=0.9, beta2=0.999, epsilon=1e-8):
    first, second, steps = state
    values = (parameter, gradient, first, second, learning_rate, decay, beta1, beta2, epsilon)
    if not all(math.isfinite(value) for value in values):
        raise ValueError("finite values required")
    if type(steps) is not int or steps < 0 or second < 0:
        raise ValueError("invalid optimizer state")
    if not 0 <= beta1 < 1 or not 0 <= beta2 < 1:
        raise ValueError("betas must be in [0, 1)")
    if learning_rate < 0 or decay < 0 or epsilon <= 0:
        raise ValueError("invalid optimizer settings")
    steps += 1
    first = beta1 * first + (1 - beta1) * gradient
    second = beta2 * second + (1 - beta2) * gradient ** 2
    first_corrected = first / (1 - beta1 ** steps)
    second_corrected = second / (1 - beta2 ** steps)
    updated = parameter * (1 - learning_rate * decay)
    updated -= learning_rate * first_corrected / (math.sqrt(second_corrected) + epsilon)
    return updated, (first, second, steps)

这个标量实现只展示更新顺序,不支持稀疏梯度、AMSGrad、混合精度或分布式。gradient=0 也不等于框架中的 grad=None:后者可能让该参数整步跳过。真实行为以 AdamW 实现为准。

接到真实训练循环里

决定 要检查的内容
学习率与 scheduler warmup / decay 按成功的 optimizer update 还是 microbatch 计数?
梯度累积 多个 microbatch 的样本数 / 有效 token 数不同,分母是否仍正确?
梯度裁剪 AMP 下是否先 unscale,再裁剪?裁剪的是哪组参数?
恢复训练 除权重外,是否恢复 moments、step、scheduler 与随机状态?
模块冻结 optimizer 是否包含正确参数,旧状态是否还占用内存?

AdamW 是值得比较的基线(baseline),不是无需调参的结论。比较 optimizer 时,让各自有合理的学习率搜索预算,记录验证表现、达到目标所需时间和状态内存。只拿一个学习率跑所有算法,往往是在比较谁更适合这个学习率。

最后把这一篇接回一次训练:backward() 计算并累积梯度,step() 才根据这些状态更新权重。两者不是同一件事。

本页术语 1
基线 baseline
用来比较新方法的参考方法或配置