#优化器深度解析附录(新增:设计动机、数学推导与系统对比)
前面的第九批优化器题库已经覆盖了"每个优化器是什么"和"怎么用",但面试中最能拉开差距的追问往往是:"为什么要这样设计?""如果从零设计一个优化器,你会怎么思考?""这几个优化器本质区别在哪?"
本节从设计演进、核心数学和工程选型三个维度,把优化器从"背诵公式"提升到"理解原理"。
#一、优化器设计演进:为什么从 SGD 走到 AdamW
#1. SGD 的核心问题与 Momentum 的解决思路
SGD 的更新规则:
theta_{t+1} = theta_t - lr * g_t
其中 g_t 是当前 mini-batch 的梯度。
SGD 的问题:
- 震荡:在峡谷型 loss surface(一个维度很陡、另一个维度很平)中,SGD 会在陡坡上来回震荡,在平坡上进展缓慢。
- 噪声敏感:每次更新只依赖当前 batch 的梯度,噪声大时方向不稳定。
- 统一学习率:所有参数共享同一个
lr,但不同参数的梯度尺度可能差异很大(如 embedding 层和深层 FFN)。
Momentum 的设计动机:把物理中的"动量"概念引入优化。不再只看当前梯度,而是把历史梯度做指数滑动平均,形成"速度":
v_t = beta * v_{t-1} + g_t
theta_{t+1} = theta_t - lr * v_t
为什么这样设计有效? 想象一个球滚下山:
- 在峡谷两侧来回震荡时,横向速度会相互抵消,纵向速度会累积;
- 最终球会沿着山谷底部平稳前进,而不是在两侧来回反弹。
这就是 Momentum 的核心价值:用历史信息的累积来平滑更新方向,减少震荡。
#2. 从 Momentum 到 RMSProp:自适应学习率的直觉
Momentum 解决了"方向不稳定",但没有解决"不同参数需要不同学习率"。
RMSProp 的洞察:如果某个参数的历史梯度一直很大,说明这个参数可能在一个"很陡"的方向上,应该减小它的学习率;反之,如果某个参数的历史梯度一直很小,应该增大它的学习率。
RMSProp 更新规则:
v_t = beta * v_{t-1} + (1 - beta) * g_t^2
theta_{t+1} = theta_t - lr * g_t / (sqrt(v_t) + eps)
这里 v_t 是梯度平方的滑动平均(二阶矩估计),用它来做逐维度的归一化:
- 梯度大的维度:
sqrt(v_t)大,更新步长被压缩; - 梯度小的维度:
sqrt(v_t)小,更新步长被放大。
面试追问:为什么用梯度平方而不是梯度绝对值?
- 平方对大梯度惩罚更重,对小梯度更宽容,这和优化中常见的"先把大梯度方向压下来"的直觉一致。另外,平方和后续的 Adam 设计一脉相承。
#3. Adam 的合成:Momentum + RMSProp
Adam 的设计动机:Momentum 在"方向平滑"上很好,RMSProp 在"自适应步长"上很好,能不能把两者结合起来?
Adam 更新规则(带 bias correction):
m_t = beta1 * m_{t-1} + (1 - beta1) * g_t # 一阶矩(动量)
v_t = beta2 * v_{t-1} + (1 - beta2) * g_t^2 # 二阶矩(自适应)
m_hat_t = m_t / (1 - beta1^t) # bias correction
v_hat_t = v_t / (1 - beta2^t) # bias correction
theta_{t+1} = theta_t - lr * m_hat_t / (sqrt(v_hat_t) + eps)
bias correction 的必要性:因为 m_t 和 v_t 初始化为 0,前几步的估计会系统性地偏小。除以 (1 - beta^t) 补偿这个偏差,让早期估计更准确。
具体数值:beta1=0.9, beta2=0.999:
beta1=0.9表示约看最近 10 步的梯度平均;beta2=0.999表示约看最近 1000 步的梯度平方平均。
为什么 beta2 远大于 beta1? 因为二阶矩(梯度平方)变化更慢、需要更长记忆,而一阶矩(梯度方向)需要更快响应。
#4. AdamW 的关键修正:Decoupled Weight Decay
问题的根源:在标准 Adam 中,如果直接把 L2 正则 lambda * theta 加到梯度里:
g_t = grad(loss) + lambda * theta_t
theta_{t+1} = theta_t - lr * m_hat_t / (sqrt(v_hat_t) + eps)
这个 lambda * theta_t 会被二阶矩 v_hat_t 缩放。结果是:
- 历史梯度大的参数,
v_hat_t大,L2 正则的实际效果被压缩; - 历史梯度小的参数,
v_hat_t小,L2 正则的实际效果被放大。
L2 正则的语义被扭曲了! 你想让所有参数都按同样比例衰减,但 Adam 的自适应机制让不同参数的衰减强度完全不同。
AdamW 的修正:把 weight decay 从梯度更新中彻底解耦:
# 先按 Adam 规则更新(只用数据梯度)
theta_t' = theta_t - lr * m_hat_t / (sqrt(v_hat_t) + eps)
# 再单独做一次参数收缩(和自适应无关)
theta_{t+1} = theta_t' - lr * lambda * theta_t
这样 lambda 的效果是明确的:所有参数都按 lr * lambda 的比例收缩,不会被二阶矩扭曲。
为什么这在大模型训练中特别重要?
- 大模型参数数量级差异大(embedding 层可能有几十亿参数,某些小矩阵只有几百万);
- 如果 weight decay 强度随参数乱飘,调参会变成噩梦;
- AdamW 让正则化"可控、可解释、可独立调参"。
#二、核心优化器对比表
| 维度 | SGD | Momentum | RMSProp | Adam | AdamW |
|---|---|---|---|---|---|
| 状态量 | 0 | 1x (速度) | 1x (二阶矩) | 2x (一阶+二阶) | 2x (一阶+二阶) |
| 自适应 | 否 | 否 | 是(逐维) | 是(逐维) | 是(逐维) |
| 平滑性 | 无 | 有(动量) | 无 | 有(一阶矩) | 有(一阶矩) |
| weight decay | L2=decay | L2=decay | L2=decay | L2(扭曲) | 解耦 decay |
| 适用场景 | 简单凸问题 | 震荡严重的 loss | 非平稳梯度 | 通用默认 | 大模型默认 |
| 调参难度 | 高 | 中高 | 中 | 低 | 低 |
面试时的快速记忆法:
- SGD = 盲人摸象(只看当前梯度);
- Momentum = 滚雪球(历史方向有惯性);
- RMSProp = 因人而异(不同参数不同步长);
- Adam = 又稳又自适应(动量 + 自适应);
- AdamW = 正则化也干净(解耦 decay)。
#三、大模型场景下的优化器选型决策树
模型能否放入单卡显存(含 optimizer state)?
├─ 是 ───────────────────────────────────────────┐
│ └─ 数据质量高、训练稳定? │
│ ├─ 是 ─> AdamW(默认选择) │
│ └─ 否 ─> 尝试 Lion(更轻量,可能更稳) │
└─ 否(optimizer state 爆显存)───────────────────┘
├─ 模型 < 20B ─> 8-bit AdamW / Adafactor
├─ 模型 20B-70B ─> ZeRO-1/2 + AdamW
└─ 模型 > 70B ─> ZeRO-3/FSDP + 考虑 Adafactor / 8-bit
关键决策点:
- 显存够吗? optimizer state 是 AdamW 最大的代价(2-3x 参数显存)。如果不够,要么换省状态优化器,要么上 ZeRO/FSDP。
- 训练稳定吗? AdamW 的默认参数(lr=1e-4~3e-4, beta1=0.9, beta2=0.95, weight_decay=0.1)在大多数大模型预训练中已经很稳。如果还不稳,检查数据质量和梯度裁剪。
- 追求极致效率? Lion、Adafactor 等新优化器在特定场景下可能更好,但工程生态不如 AdamW 成熟。
#四、常见面试深挖追问与应答
Q1: 为什么 Adam 前期收敛快但泛化不一定最好?
- Adam 的自适应机制让它在早期快速找到 loss 下降方向;
- 但自适应也可能"过度拟合"训练集梯度的局部统计,找到尖锐极小值;
- SGD(调得好)更容易找到平坦极小值,泛化更好;
- AdamW 通过解耦 weight decay 部分缓解了这个矛盾。
Q2: 如果让你从头设计一个优化器,你会怎么思考?
- 第一步:定义问题。是震荡?是不同参数尺度不均?是显存不够?是收敛慢?
- 第二步:选择工具。需要动量?需要自适应?需要省状态?
- 第三步:验证。在小数据集上快速实验,看 loss 曲线和梯度范数。
- 第四步:调参。学习率、warmup、weight decay、gradient clipping 的组合。
Q3: Adam 的 bias correction 在早期有多重要?
t=1时,m_1 = (1-beta1)*g_1,只有真实均值的0.1倍(若 beta1=0.9);- 除以
(1-0.9^1) = 0.1后,估计恢复为g_1,完全无偏; - 如果没有 bias correction,前几十步的更新会系统性地偏小,可能导致训练初期"假死"。
Q4: 学习率 warm-up 和优化器是什么关系?
- 不是优化器本身的特性,但和优化器配合至关重要;
- Adam/AdamW 早期
v_t很小(因为还没积累足够梯度平方),如果没有 warm-up,lr / sqrt(v_t)会异常大,导致早期更新爆炸; - warm-up 让学习率从零缓慢上升,给二阶矩足够时间积累稳定估计。
#五、优化器状态显存占用速算表
以 AdamW + BF16 训练为例,每 1B 参数的显存占用:
| 组件 | 计算 | 每 1B 参数 |
|---|---|---|
| 参数(BF16) | 1B × 2 字节 | 2 GB |
| 梯度(BF16) | 1B × 2 字节 | 2 GB |
| 一阶矩 m(FP32) | 1B × 4 字节 | 4 GB |
| 二阶矩 v(FP32) | 1B × 4 字节 | 4 GB |
| 合计 | 12 GB |
如果加 FP32 master weights(mixed precision):再加 4 GB,总计 16 GB / 1B 参数。
这就是为什么:
- 7B 模型全参数训练,单卡至少需要 7 × 12 = 84 GB 显存(这还不含激活);
- 必须使用 ZeRO/FSDP/8-bit optimizer 来切分或压缩 optimizer state。