损失函数给出"地形",优化器决定"怎么下山"。从朴素 SGD 到 Adam,每一步演进都在解决前一代的一个具体痛点:噪声、震荡、不同参数的尺度差异、稀疏梯度。理解这条演进链,比记住调哪个超参重要得多。
直觉:在高维损失面上摸黑下山
参数更新的通用形式:
是学习率。问题在于神经网络的损失面是百万到千亿维的非凸曲面,充满了陡峭的峡谷、平坦的高原、鞍点。朴素梯度下降在这种地形上会遇到三类麻烦:(1) 全量梯度太贵;(2) 病态曲率导致来回震荡;(3) 不同参数需要不同步长。优化器的全部历史,就是逐一应对这三点。
SGD:用噪声换速度
Batch GD 用全部数据算一次梯度,准但慢、且容易卡在尖锐极小值。SGD(随机梯度下降) 每步只用一个 mini-batch 估计梯度:
mini-batch 引入的梯度噪声不全是坏事——它像退火,能帮模型逃离尖锐极小值、倾向更平坦(泛化更好)的解。但纯 SGD 在病态曲率(一个方向陡、另一个方向缓)的峡谷里会剧烈震荡:陡方向反复横跳,缓方向爬得极慢。
Momentum:给下山加上惯性
Momentum 引入"速度"变量,让历史梯度以指数衰减的方式累积:
(动量系数,常取 0.9)控制惯性。物理直觉:小球带着惯性滚下山。在峡谷里,横跳方向的梯度正负相消、被抑制;缓坡方向梯度持续同号、被累加放大。结果是震荡减少、收敛加速。Nesterov 变体更进一步——先按惯性"前瞻"一步再算梯度,相当于提前刹车,在凸问题上有更好的理论收敛率。
AdaGrad / RMSProp:让每个参数自适应步长
Momentum 解决了方向问题,但所有参数共享同一个 。可现实中不同参数的合适步长差异巨大(想想 embedding 里高频词和低频词的梯度尺度)。AdaGrad 给每个参数维护历史梯度平方和,用它来缩放步长:
梯度一直很大的参数步长被压小,稀疏/小梯度参数步长被放大。问题是 单调累加,分母越来越大,学习率会单调衰减到几乎为零,训练后期停滞。
RMSProp 一招修复:把"累加"换成"指数移动平均",让远古梯度被遗忘:
步长不再无限衰减,对非平稳目标(如 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{(二阶矩)}关键细节: 初始化为 0,导致训练初期估计偏向 0。Adam 用偏差校正修正:
最终更新:
默认 。下面是去掉框架封装后的核心,看清它到底在干什么:
1 | def adam_step(theta, grad, state, lr=1e-3, b1=0.9, b2=0.999, eps=1e-8): |
工程权衡与显存
- 显存成本。 SGD 无额外状态;带 momentum 多存 1 份参数量的状态;Adam 要存 和 两份。对一个 参数的模型,仅优化器状态就约 。混合精度训练里通常还要存一份 fp32 主权重,于是 Adam 的"模型权重 + 梯度 + 两个矩 + fp32 主副本"会让显存占用是纯前向的数倍——这正是大模型训练显存吃紧、催生 8-bit Adam、ZeRO 分片等技术的根源。
- AdamW ≠ Adam。 原始 Adam 把 L2 正则混进梯度,会被二阶矩缩放,等价的权重衰减强度被扭曲。AdamW 把 weight decay 从梯度里解耦,直接作用在参数上,泛化更好——现代 Transformer 训练几乎默认用 AdamW。
- Adam 收敛快但未必泛化最好。 经验上 Adam 前期下降迅猛,而精调的 SGD+Momentum 有时能找到更平坦、泛化更优的解。CV 任务里 SGD 仍常胜出;大语言模型则几乎离不开 AdamW。
- 学习率调度不可省。 warmup(前期线性升 ,避免 Adam 初期方差估计不稳导致的乱跳)+ cosine 衰减,是大模型训练的标配组合。
常见误区
把学习率当唯一旋钮调——其实 、weight decay、warmup 步数同等关键;以为 Adam"自适应"就不用调 ——它只调相对尺度,全局 仍要调;忽略 在低精度下的数值作用——bf16 训练里 太小会引入不稳定。
小结
SGD→Momentum→RMSProp→Adam 是一条针对性极强的演进链:噪声换速度、惯性抑震荡、二阶矩做自适应、偏差校正补初期。Adam(实战中是 AdamW)凭借鲁棒和快速成为默认选择,但它用双倍优化器状态换来的便利,是大模型显存预算里一笔必须正视的开销。没有万能优化器——理解每一步在解什么问题,才能在自己的任务上做对取舍。