Adam(Adaptive Moment Estimation)把动量与自适应学习率装进同一个更新公式:两个指数移动平均分别估计梯度的方向与尺度,偏差修正消除冷启动偏置。它是深度学习优化器的默认起点,而工业界真正跑在显卡上的是修正版 AdamW。
为什么需要 Adam
训练一个稍大的网络,损失面的典型形态是”峡谷”:某些方向陡峭、某些方向平缓,不同参数对损失的敏感度差几个数量级。梯度下降手里只有一把全局学习率,顾此失彼——步长压小了在平缓方向挪不动,放大了在陡峭方向直接发散。历次修补各解决一半问题:
| 优化器 | 问题 |
|---|---|
| 朴素 SGD | 单一固定学习率,收敛慢、易震荡 |
| 带动量 SGD | 平滑了方向,但没有任何参数级自适应 |
| AdaGrad | 梯度平方只加不忘,累积单调增长,后期学习率衰减到 0 |
| RMSprop | 改成指数衰减加权,解决了遗忘问题,但丢掉了动量 |
两条改进脉络最终在 Adam 汇合:动量法一脉解决方向震荡——用历史梯度的加权平均代替当前单步梯度;AdaGrad/RMSprop 一脉解决尺度失衡——用梯度平方的历史信息给每个参数单独缩放步长。Adam 一阶矩管方向、二阶矩管步长、偏差修正管冷启动,三个组件的数学内核都是指数移动平均。
数学原理
符号定义
| 符号 | 定义 |
|---|---|
| 模型参数(权重、偏置) | |
| 第 步梯度, | |
| 全局学习率 | |
| 一阶矩衰减率(动量系数) | |
| 二阶矩衰减率 | |
| 一阶矩估计(梯度 EMA) | |
| 二阶矩估计(梯度平方 EMA) | |
| 偏差修正后的矩估计 | |
| 防除零常数 |
一阶矩:梯度的 EMA
是梯度的指数移动平均,也就是动量法的 EMA 写法。 对应约 10 步的有效窗口:持续同向的梯度分量被累加放大,来回变号的震荡分量相互抵消,更新沿”近期平均方向”前进,而不是被单步噪声牵着走。
二阶矩:梯度平方的 EMA
估计 ,即梯度的二阶原点矩——注意它同时编码信号与噪声:。更新时除以 ,效果是对每个参数做归一化:梯度长期偏大的参数得小步长,梯度长期偏小的参数得大步长,全部参数的有效步长被拉到同一量级。
的有效窗口约 1000 步,比 长两个数量级——尺度估计需要更长历史才稳定,也埋下了训练后期的隐患(见超参数一节)。
偏差修正
冷启动时 ,EMA 被零初值往小拖。设梯度恒为 ,对递推式归纳可得 —— 时第一步只有 , 时第一步只有 ,严重低估。除掉这个系数即可还原真实量级:
时 ,修正自动失效,只作用于前几十步。没有它,训练开头的更新幅度会偏小一到两个数量级,冷启动阶段几乎不动。
参数更新
- 分子 :动量平滑后的更新方向
- 分母 :参数级自适应缩放系数
- :防除零,典型 1e-8
这个比值有个漂亮解读:,本质是梯度的信噪比。方向稳定(方差小)时比值趋近 ,有效步长约等于 ;梯度噪声大时比值自动收缩,步长随之变小。由此推出两个实践结论:
- Adam 的更新对损失整体缩放近似不变——loss 乘个常数,分子分母同缩放,步长不变。想靠改 loss 尺度解决收敛问题行不通,该调的还是
- 有效步长有 量级的自然上界,这是 Adam 对初始学习率比 SGD 鲁棒得多的根源
伪代码
输入:α=1e-3, β1=0.9, β2=0.999, ε=1e-8
初始化:t=0, m₀=0, v₀=0
循环:
t = t + 1
g_t = ∇_θ f(θ_{t-1}) # 反向传播
m_t = β1*m_{t-1} + (1-β1)*g_t
v_t = β2*v_{t-1} + (1-β2)*g_t²
m̂_t = m_t / (1 - β1^t) # 偏差修正,前几十步生效
v̂_t = v_t / (1 - β2^t)
θ_t = θ_{t-1} - α * m̂_t / (√v̂_t + ε)实现要点:、 与 是优化器持久状态,checkpoint 时必须一起保存,否则恢复训练后偏差修正和矩历史全部错乱。
AdamW:解耦权重衰减
把 L2 正则加进 loss,衰减项就作为梯度的一部分进入 和 ,随后被 缩放。问题在于两路尺度不匹配:权重绝对值通常远大于其梯度,本想对全体权重做均匀压制,实际压制强度被自适应分母搅乱,正则几乎失效。这也是”Adam + weight_decay”与”AdamW”看起来参数一样、效果却差很多的经典陷阱。
AdamW 的修法是把衰减从梯度通道里拿出来,直接作用于参数:
衰减项不再进任何矩估计,每步精确收缩 。完整推导见 权重衰减与L2正则化。
| 特性 | 原始 Adam + L2 | AdamW |
|---|---|---|
| 权重衰减路径 | 混入梯度,被二阶矩缩放 | 直接作用于参数 |
| 正则强度 | 失真、参数间不均匀 | 精确等于 |
| 工业界状态 | 基本淘汰 | 默认选择 |
衰减量随学习率调度变化
AdamW 每步衰减 ,warmup 阶段 很小,衰减同样很弱。若需要衰减强度与学习率调度完全解耦,主流框架没有现成开关,得自己实现。
超参数配置
| 超参数 | 默认值 | 说明 |
|---|---|---|
| 学习率 | 1e-3(通用) 1e-4~3e-4(LLM) | 唯一值得花时间调的参数 |
| 0.9 | 99% 场景不动 | |
| 0.999(通用) 0.95(LLM) | 窗口约 1000 步 vs 约 20 步 | |
| 1e-8 | FP16 混合精度可放宽到 1e-6 | |
| 权重衰减 | 1e-2(CV) 1e-4~1e-1(LLM) | AdamW 专用 |
是最容易被忽视的一个。Gopher 的消融实验发现 在大模型训练后期不稳定:约 1000 步的窗口太长, 跟不上后期梯度噪声结构的变化,步长估计过时导致 loss 尖峰;改用 0.95(窗口约 20 步)后显著缓解,此后 LLM 训练普遍采用短窗口。
在数学上只是防除零,工程上却是 FP16/BF16 下的安全垫——分母 半精度下溢出时,放大的 能挡住 NaN。混合精度训练中矩状态应保持 FP32 主副本,见 混合精度训练。
变种与适用场景
| 变种 | 改进 | 适用场景 |
|---|---|---|
| AdamW | 解耦权重衰减 | 通用默认 |
| Adamax | 用 范数替代二阶矩缩放 | 稀疏梯度、嵌入层 |
| NAdam | 动量法的 Nesterov 版本 | 加速收敛 |
| LAMB | 层级自适应学习率 | 大 batch 分布式训练 |
| AdaFactor | 二阶矩低秩分解,省掉 的显存 | 显存受限的大模型 |
| 8-bit Adam | 矩状态量化到 8-bit | 消费级显卡微调 |
选型逻辑围绕两个轴:要不要动量变体(NAdam),以及显存预算(AdaFactor、8-bit Adam 都是用精度或结构换 的存储)。
优缺点
优点
- 收敛快:动量抑制震荡,参数级自适应让冷启动对学习率不敏感,第一版超参就能跑
- 调参成本低:除 外默认值覆盖绝大多数场景
- 与现代架构绑定:Transformer 的嵌入层梯度稀疏、层间尺度差异大,恰好是自适应方法的主场,SGD 在这类模型上几乎无法直接使用
缺点
- 显存开销:每个参数多存 、 两个 FP32 状态,混合精度下”模型 + 梯度 + 优化器状态”合计约 16 bytes/参数,优化器状态占一半,这是 ZeRO 分片的首要目标,见 优化器状态
- 泛化争议:自适应方法倾向于停在更 sharp 的极小值附近,部分 CV 任务上精调阶段切回 SGD 泛化更好——“Adam 收敛快、SGD 终点好”在视觉模型上仍部分成立
- 后期不稳定: 窗口太长时 过时,训练后期出现 loss 尖峰,LLM 场景改 0.95 已成惯例
泛化 gap 的边界
“Adam 泛化更差”的结论来自 Wilson et al. (2017) 在经典 CV 模型上的观察。Transformer 时代没有可比的 SGD 基线(SGD 根本训不动),这条缺点在 LLM 语境下更多是历史注脚。
工程实践
import math
import torch
from torch.optim import AdamW
optimizer = AdamW(
model.parameters(),
lr=3e-4, # Transformer 通用量级,LLM 预训练 1e-4~3e-4
betas=(0.9, 0.999), # LLM 长训可改 (0.9, 0.95)
eps=1e-8, # FP16/BF16 混合精度可放宽到 1e-6
weight_decay=1e-4, # CV 常用 1e-2
)
def lr_lambda(step: int) -> float:
"""线性 warmup 1k 步 + 余弦衰减到峰值 10%"""
warmup, total = 1_000, 100_000
if step < warmup:
return step / warmup
progress = (step - warmup) / (total - warmup)
return 0.1 + 0.45 * (1 + math.cos(math.pi * progress))
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
for batch in dataloader:
loss = model(batch)
loss.backward()
# 梯度裁剪:范数超 1.0 整体缩放,防尖峰
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
scheduler.step()
optimizer.zero_grad()安全默认配置
AdamW + warmup(1k) + cosine 衰减 + 梯度裁剪(max_norm=1.0) 覆盖绝大多数场景。显存吃紧再换 8-bit AdamW(bitsandbytes)或 AdaFactor;多卡大 batch 训练上 LAMB。
其余细节按需叠加:混合精度训练把矩状态留在 FP32、forward/backward 用低精度跑;前向传播与反向传播详解中的梯度计算图不受优化器选择影响,优化器只消费 backward() 产出的梯度。
回到最初的问题,为什么默认答案是 AdamW:两个 EMA 分别吸收动量与自适应两条脉络的成果,偏差修正解决冷启动,解耦衰减修复正则。机制压缩成一句话——方向看历史平均,步长看信噪比,衰减独立走。真正需要调的只有学习率;对 、 这类参数,知道何时偏离默认值,比频繁调它们更有价值。