损失函数给出"地形",优化器决定"怎么下山"。从朴素 SGD 到 Adam,每一步演进都在解决前一代的一个具体痛点:噪声、震荡、不同参数的尺度差异、稀疏梯度。理解这条演进链,比记住调哪个超参重要得多。

直觉:在高维损失面上摸黑下山

参数更新的通用形式:

θt+1=θtηgt,gt=θL(θt)\theta_{t+1} = \theta_t - \eta\, g_t,\qquad g_t = \nabla_\theta \mathcal{L}(\theta_t)

η\eta 是学习率。问题在于神经网络的损失面是百万到千亿维的非凸曲面,充满了陡峭的峡谷、平坦的高原、鞍点。朴素梯度下降在这种地形上会遇到三类麻烦:(1) 全量梯度太贵;(2) 病态曲率导致来回震荡;(3) 不同参数需要不同步长。优化器的全部历史,就是逐一应对这三点。

SGD:用噪声换速度

Batch GD 用全部数据算一次梯度,准但慢、且容易卡在尖锐极小值。SGD(随机梯度下降) 每步只用一个 mini-batch 估计梯度:

gt1BiBθLi(θt)g_t \approx \frac{1}{|B|}\sum_{i\in B}\nabla_\theta \mathcal{L}_i(\theta_t)

mini-batch 引入的梯度噪声不全是坏事——它像退火,能帮模型逃离尖锐极小值、倾向更平坦(泛化更好)的解。但纯 SGD 在病态曲率(一个方向陡、另一个方向缓)的峡谷里会剧烈震荡:陡方向反复横跳,缓方向爬得极慢。

Momentum:给下山加上惯性

Momentum 引入"速度"变量,让历史梯度以指数衰减的方式累积:

vt=μvt1+gt,θt+1=θtηvtv_t = \mu\, v_{t-1} + g_t,\qquad \theta_{t+1} = \theta_t - \eta\, v_t

μ\mu(动量系数,常取 0.9)控制惯性。物理直觉:小球带着惯性滚下山。在峡谷里,横跳方向的梯度正负相消、被抑制;缓坡方向梯度持续同号、被累加放大。结果是震荡减少、收敛加速。Nesterov 变体更进一步——先按惯性"前瞻"一步再算梯度,相当于提前刹车,在凸问题上有更好的理论收敛率。

AdaGrad / RMSProp:让每个参数自适应步长

Momentum 解决了方向问题,但所有参数共享同一个 η\eta。可现实中不同参数的合适步长差异巨大(想想 embedding 里高频词和低频词的梯度尺度)。AdaGrad 给每个参数维护历史梯度平方和,用它来缩放步长:

θt+1=θtηGt+ϵgt,Gt=τ=1tgτ2\theta_{t+1} = \theta_t - \frac{\eta}{\sqrt{G_t}+\epsilon}\odot g_t,\qquad G_t = \sum_{\tau=1}^{t} g_\tau^2

梯度一直很大的参数步长被压小,稀疏/小梯度参数步长被放大。问题是 GtG_t 单调累加,分母越来越大,学习率会单调衰减到几乎为零,训练后期停滞。

RMSProp 一招修复:把"累加"换成"指数移动平均",让远古梯度被遗忘:

E[g2]t=βE[g2]t1+(1β)gt2E[g^2]_t = \beta\, E[g^2]_{t-1} + (1-\beta)\, g_t^2

步长不再无限衰减,对非平稳目标(如 RNN、RL)尤其稳。

Adam:Momentum × RMSProp + 偏差校正

Adam 把两条线合并:一阶矩(动量,方向)+ 二阶矩(自适应步长)。

m_t = \beta_1 m_{t-1} + (1-\beta_1) g_t \quad\text{(一阶矩)} v_t = \beta_2 v_{t-1} + (1-\beta_2) g_t^2 \quad\text{(二阶矩)}

关键细节:m0,v0m_0, v_0 初始化为 0,导致训练初期估计偏向 0。Adam 用偏差校正修正:

m^t=mt1β1t,v^t=vt1β2t\hat{m}_t = \frac{m_t}{1-\beta_1^t},\qquad \hat{v}_t = \frac{v_t}{1-\beta_2^t}

最终更新:

θt+1=θtηm^tv^t+ϵ\theta_{t+1} = \theta_t - \eta \cdot \frac{\hat{m}_t}{\sqrt{\hat{v}_t}+\epsilon}

默认 β1=0.9, β2=0.999, ϵ=108\beta_1{=}0.9,\ \beta_2{=}0.999,\ \epsilon{=}10^{-8}。下面是去掉框架封装后的核心,看清它到底在干什么:

1
2
3
4
5
6
7
8
def adam_step(theta, grad, state, lr=1e-3, b1=0.9, b2=0.999, eps=1e-8):
state['t'] += 1
t = state['t']
state['m'] = b1 * state['m'] + (1 - b1) * grad
state['v'] = b2 * state['v'] + (1 - b2) * grad**2
m_hat = state['m'] / (1 - b1**t) # 偏差校正,初期尤其重要
v_hat = state['v'] / (1 - b2**t)
return theta - lr * m_hat / (v_hat**0.5 + eps)

工程权衡与显存

  • 显存成本。 SGD 无额外状态;带 momentum 多存 1 份参数量的状态;Adam 要存 mmvv 两份。对一个 PP 参数的模型,仅优化器状态就约 2P2P。混合精度训练里通常还要存一份 fp32 主权重,于是 Adam 的"模型权重 + 梯度 + 两个矩 + fp32 主副本"会让显存占用是纯前向的数倍——这正是大模型训练显存吃紧、催生 8-bit Adam、ZeRO 分片等技术的根源。
  • AdamW ≠ Adam。 原始 Adam 把 L2 正则混进梯度,会被二阶矩缩放,等价的权重衰减强度被扭曲。AdamW 把 weight decay 从梯度里解耦,直接作用在参数上,泛化更好——现代 Transformer 训练几乎默认用 AdamW。
  • Adam 收敛快但未必泛化最好。 经验上 Adam 前期下降迅猛,而精调的 SGD+Momentum 有时能找到更平坦、泛化更优的解。CV 任务里 SGD 仍常胜出;大语言模型则几乎离不开 AdamW。
  • 学习率调度不可省。 warmup(前期线性升 η\eta,避免 Adam 初期方差估计不稳导致的乱跳)+ cosine 衰减,是大模型训练的标配组合。

常见误区

把学习率当唯一旋钮调——其实 β\beta、weight decay、warmup 步数同等关键;以为 Adam"自适应"就不用调 η\eta——它只调相对尺度,全局 η\eta 仍要调;忽略 ϵ\epsilon 在低精度下的数值作用——bf16 训练里 ϵ\epsilon 太小会引入不稳定。

小结

SGD→Momentum→RMSProp→Adam 是一条针对性极强的演进链:噪声换速度、惯性抑震荡、二阶矩做自适应、偏差校正补初期。Adam(实战中是 AdamW)凭借鲁棒和快速成为默认选择,但它用双倍优化器状态换来的便利,是大模型显存预算里一笔必须正视的开销。没有万能优化器——理解每一步在解什么问题,才能在自己的任务上做对取舍。