🏠 总目录📚 本教程 09 · 优化器与学习率 ← →
📑 本页目录(点开跳转)

09 / 先看直觉,再看细节

梯度给方向,
优化器决定怎么走。

先理解学习率控制步长,再看 Adam 如何利用历史梯度调整更新。

先抓住一个具体结果

一次更新,手算就够

参数是 1,梯度是 2,学习率是 0.1。普通梯度下降这次减去 0.2,新参数是 0.8。步子过大仍可能把损失推高。

普通梯度下降的一步

$$w_{\mathrm{new}}=w-\eta g_t$$

接下来,按需要选一项

你现在想看什么?

这些入口是选择,不是必须按顺序完成的任务。

可以停在这里

先理解学习率控制步长,再看 Adam 如何利用历史梯度调整更新。

需要更多细节时,继续看完整正文 →
继续探索

原有正文、图解和例子都在下方。按需跳转,不必一次读完。

09 · 优化器与学习率

⏱ 34 分钟 | ⭐ 核心


🎯 一句话

有了梯度(第 8 章),还得决定怎么迈这一步。 学习率是全深度学习最重要的超参数——没有之一。

最优SGDMomentumAdam来回横跳惯性抵消震荡每维自适应步长在这种「狭长山谷」地形上,三个优化器走出完全不同的路⭐ 问题不在「学习率大小」,在于<各个方向的陡峭程度差太多>SGD 只有一个全局步长:够小才不震荡,但那样在平缓方向就几乎不动Adam 给每个维度各自的步长 —— 这就是它在实践中好用的核心原因
在「狭长山谷」地形上,三个优化器走出完全不同的路。⭐ 问题不在学习率大小,在于各个方向的陡峭程度差太多 —— SGD 只有一个全局步长,够小才不震荡,但那样在平缓方向几乎不动。Adam 给每个维度各自的步长,这才是它好用的核心原因。
太小走一辈子也到不了底刚好稳稳下到谷底太大来回震荡,甚至发散同一个损失曲面,只是步子迈多大不同⭐ 学习率是最该先调的超参 —— 它错了,别的怎么调都没用
同一个损失曲面,只是每一步迈多大不同。⭐ 太小走不到底,太大来回震荡甚至发散 —— 学习率是最该先调的超参,它错了别的怎么调都没用。

🏔️ 一、梯度下降的三种规模

对照

全批量 (Batch GD):用全部数据算一次梯度,走一步

✅ 方向准 ❌ 慢、内存装不下

随机 (SGD):一个样本算一次梯度

✅ 快 ❌ 抖得厉害

小批量 (Mini-batch) ⭐ 实际都用这个

每次用 32~512 个样本

✅ 兼顾速度和稳定,且能吃满 GPU 并行

batch size 的影响:

小 batch (32) 大 batch (1024)
梯度噪声 大 小
泛化 通常更好(噪声是隐式正则) 可能略差
速度 慢(GPU 没吃满) 快
显存 少 多

💡 一种实验起点:部分大批量 SGD 配置会让 batch size 与学习率同比例增加(线性缩放规则)。它不是所有模型、优化器都适用的定律,改完仍要检查验证表现和训练稳定性。

📐 为什么是"线性"缩放(想看再点)

batch 里的梯度是 B 个样本梯度的平均。假设各样本梯度独立、方差为 σ²:

$$\text{Var}(\bar g) = \frac{\sigma^2}{B}$$

先分清两个结论:batch 翻倍,梯度噪声的标准差变为原来的 1/√2;如果只想保持 lr × 标准差不变,推出来的是学习率乘 √2,不是乘 2。

线性缩放的另一种近似来自 “多次小批量更新”和“一次大批量更新”:假设这几步之间梯度变化很小,把 k 个小批量合成一个大批量时,学习率乘 k,才近似相当于原来的 k 次更新。这一假设在训练初期尤其容易失效。

Warmup 可以缓解初期突然使用大学习率的问题,但不能保证任意大 batch 都有效,也没有通用的“8k 失效线”。原始 大批量 SGD 实验 的 8192 是特定任务、模型与训练方案下的结果。

💡 小 batch 的"噪声"为什么反而有益:

关键信息

小 batch 的梯度是【带噪声的】真实梯度
更新方向随机抖动
抖得出尖锐的局部极小值,抖不出平坦的
最终停在【平坦极小值】里
⭐ 而平坦极小值泛化更好(参数微小扰动不改变结果)
这就是"SGD 的隐式正则化"

🚀 二、四代优化器

操作步骤

  1. SGD: w ← w − lr·g
  2. 最朴素。在峡谷地形里会来回震荡
  3. Momentum(动量):v ← βv + g; w ← w − lr·v
  4. 💡 像小球滚下山,累积惯性
  5. ✅ 穿过震荡、冲过小坑
  6. Adagrad/RMSProp:每个参数有自己的学习率
  7. 💡 梯度一直很大的参数,学习率自动调小
  8. ✅ 稀疏特征友好
  9. Adam = Momentum + RMSProp ⭐ 默认选它
  10. 同时用一阶动量(方向)和二阶动量(自适应步长)
  11. ✅ 几乎不用调就能work
📐 Adam 的四行完整公式(想看再点)

$$m_t = \beta_1 m_{t-1} + (1-\beta_1)g_t \qquad \text{(一阶动量:方向)}$$ $$v_t = \beta_2 v_{t-1} + (1-\beta_2)g_t^2 \qquad \text{(二阶动量:步长的尺度)}$$ $$\hat m_t = \frac{m_t}{1-\beta_1^t},\quad \hat v_t = \frac{v_t}{1-\beta_2^t} \qquad \text{(偏差修正)}$$ $$w \leftarrow w - \text{lr}\cdot\frac{\hat m_t}{\sqrt{\hat v_t}+\varepsilon}$$

三个值得注意的点:

点 说明
偏差修正为什么必要 m、v 初始为 0,前几步会被严重低估。除以 (1−βᵗ) 修正回来
除以 √v 的效果 ⭐ 梯度一直大的参数 → v 大 → 步长自动变小;梯度小的参数步长变大。相当于每个参数有自己的学习率
ε 不只是防除零 它还限制了"梯度极小时步长能有多大"。默认 1e-8,某些任务调到 1e-6 更稳

💡 Adam 的默认 β₁=0.9, β₂=0.999 几乎不用改—— 但如果训练很不稳,把 β₂ 降到 0.95 常有奇效(让二阶动量对近期更敏感)。 大模型训练里 β₂=0.95 很常见。

实用选择:

场景 选择
不确定时 Adam / AdamW,lr=1e-3 ⭐
Transformer / 大模型 AdamW(正确的权重衰减实现)
CNN 图像分类刷精度 SGD + Momentum(0.9) 常能拿到更好的最终精度,但要调
稀疏特征(推荐系统) Adagrad 类

⚠️ AdamW vs Adam:Adam 里的 L2 正则实现有缺陷(和自适应学习率纠缠)。 AdamW 把权重衰减解耦出来,是现在的正确做法。用 AdamW 就对了。


🎚️ 三、学习率:最重要的旋钮

loss训练步数 太大:抖太小:慢合适:较快下降
示意图,不是实测结果。先比较同样训练步数下的下降速度与波动:实线标出“抖”和“较快下降”,虚线标出“慢”。这些是排查线索,不是唯一诊断;NaN 也可能由数据或数值运算引起。

诊断表(背下来,第 11 章要用):

现象 诊断 动作
loss 变 NaN / inf lr 太大,梯度爆炸 lr ÷ 10
loss 剧烈震荡不下降 lr 太大 lr ÷ 3
loss 下降极慢但稳定 lr 太小 lr × 3
loss 降到一半就平了 需要学习率衰减 加调度器
loss 前几步就爆 lr 太大 或 缺 warmup 加 warmup
loss 完全不动(逐位相同) 不是 lr 问题 ⭐ 查梯度是否断流 / 忘了 .step()

⚠️ 最后一行很重要:「下降极慢」和「完全不动」是两种不同的病。 完全不动(loss 数值一位不差)几乎肯定是 bug,不是超参问题—— 调学习率是浪费时间,去查梯度。🔗 第 11 章有完整流程。

📏 学习率的量级参考(不知道从哪开始时)

场景 起手 lr
从零训练 MLP / CNN(Adam) 1e-3 ⭐
从零训练(SGD+Momentum) 0.1(配 cosine 衰减)
微调预训练模型 1e-5 ~ 5e-5 ⭐(小 100 倍!)
微调时的新分类头 1e-3(比主干大 10~100 倍)
LoRA 微调 1e-4 ~ 3e-4

🔑 "微调要用小 100 倍的学习率"是最容易忽略的一条—— 用从零训练的 lr 去微调,会直接把预训练权重冲垮,效果还不如冻结主干。

学习率调度(Scheduler)

关键信息

  • 固定 lr → 后期在最优点附近震荡,下不去
  • 逐渐衰减 lr → 前期跑得快,后期精细收敛 ✅
  • 常用三种:
  • Step:每 N 轮 × 0.1
  • Cosine ⭐:余弦曲线平滑衰减到 0,现在最主流
  • ReduceLROnPlateau:验证集不降了就自动砍学习率

⭐ Warmup(大模型必备)

① Warmup② 余弦衰减目标学习率预热结束 lr0训练步数
示意图,不是实测结果。先看左边的上坡:在设定的预热步数内,把学习率从较小值升到目标值(图中从 0 起步);再看右边的余弦下降。预热多久、最后降到多低都由配置决定,不固定为“前几百步”。

为什么需要:训练最开始参数是随机的,梯度方向很不可靠, 大步走容易把模型带偏(尤其配 Adam 时二阶动量估计还不准)。先小步试探。

🔗 你在全景导论主线 4 · 它怎样从续写机变助手看到的大模型训练,几乎都用 Warmup + Cosine。


🔨 四、怎么找学习率(LR Range Test)

不用瞎猜,有个标准方法:

# 🧩 骨架:`loader` 来自你自己的代码,这一段只看写法
# 让 lr 从 1e-7 指数增长到 1,记录每步的 loss
lrs, losses = [], []
lr = 1e-7
for batch in loader:
    for g in optimizer.param_groups: g["lr"] = lr
    loss = train_one_step(batch)
    lrs.append(lr); losses.append(loss)
    lr *= 1.1
    if loss > 4 * min(losses): break     # loss 炸了就停

# 画 loss vs log(lr),选【loss 下降最陡】的那个 lr
# 保守起见取那个点的 1/3 ~ 1/10
下降明显的候选区间开始发散 loss小学习率大学习率log(lr)
示意图,不是实测结果。横轴是 log(lr),不是训练轮数;关注 loss 明显下降、尚未发散的区间,作为后续试验的起点,不把示意曲线的最低点当成通用最佳学习率。测试后恢复测试前的模型与优化器状态,再正式训练。

这种先增大学习率、观察损失变化来寻找合理范围的思路,可参见 Leslie Smith 的学习率范围测试说明。


📋 五、可以照抄的默认配置

# 🧩 骨架:`model` 来自你自己的代码,这一段只看写法
import torch

optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=epochs)

for epoch in range(epochs):
    for xb, yb in train_loader:
        optimizer.zero_grad()             # ⭐ 必须!梯度默认累加
        loss = criterion(model(xb), yb)
        loss.backward()
        torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)   # 防梯度爆炸
        optimizer.step()
    scheduler.step()

四个不能省的细节: 1. zero_grad() —— 忘了它,梯度会累加,训练直接废掉(最常见的低级 bug) 2. clip_grad_norm_ —— 梯度裁剪,RNN/Transformer 上几乎必备 3. scheduler.step() 的位置 —— 通常每个 epoch 一次(有些调度器是每 step) 4. weight_decay —— AdamW 的正则化(第 10 章)

⚠️ scheduler.step() 的位置是个高频错误:

调度器 该在哪调用
CosineAnnealingLR、StepLR 每个 epoch 一次(放在 epoch 循环末尾)
OneCycleLR、带 warmup 的 每个 step 一次(放在 optimizer.step() 之后)⭐
ReduceLROnPlateau 每个 epoch,且必须传验证指标:scheduler.step(val_loss)

💡 放错位置的症状:学习率衰减得快几百倍(该按 epoch 却按 step 调用), loss 降一点就完全不动了——看起来像模型不行,实际是 lr 已经衰减到 0。 训练时打印一下 optimizer.param_groups[0]["lr"] 就能立刻发现。


💾 六、显存不够时的三个招

招 做什么 代价
梯度累积 攒 N 个小 batch 的梯度再更新一次 = 模拟大 batch 慢 N 倍
混合精度 AMP 用 FP16 算,FP32 存主权重 显存减半,速度还更快 ⭐ 几乎白赚
梯度检查点 只存部分层的激活,反向时重算 慢约 30%,显存大降
# 🧩 骨架:`loader` 来自你自己的代码,这一段只看写法
# 梯度累积:注意 loss 要除以 accum_steps ⭐
accum = 4
for i, (xb, yb) in enumerate(loader):
    loss = criterion(model(xb), yb) / accum      # ⭐ 别忘了这个除法
    loss.backward()
    if (i + 1) % accum == 0:
        optimizer.step()
        optimizer.zero_grad()

⚠️ 梯度累积最常见的 bug 就是忘了除以 accum —— 等效于把学习率放大了 4 倍,训练会不稳定甚至发散。


✅ 检查点

  1. mini-batch 相比全批量和单样本的优势?batch size 和 lr 什么关系?
  2. 为什么小 batch 的"噪声"反而对泛化有益?
  3. Adam 结合了哪两个想法?偏差修正为什么必要?AdamW 改进了什么?
  4. loss 变 NaN 该怎么办?loss 下降极慢呢?loss 完全不动呢?
  5. 微调预训练模型该用多大学习率?不注意会怎样?
  6. Warmup 为什么有用?什么时候必须加?
  7. zero_grad() 忘了会怎样?
  8. scheduler.step() 放错位置有什么症状?怎么快速发现?
  9. 显存不够的三个招是什么?梯度累积最常见的 bug 是什么?
👀 答案
  1. 小批量在梯度估计、内存和并行效率之间折中。线性缩放是部分 SGD 场景的实验起点,不是通用法则;没有统一的 8k 失效线,Warmup 也不能保证无限扩大批量。
  2. 小 batch 的梯度带噪声 → 更新方向随机抖动 → 抖得出尖锐极小值,抖不出平坦极小值 → 最终停在平坦极小值里,而平坦极小值泛化更好。这就是"SGD 的隐式正则化"。
  3. Momentum(一阶动量,方向惯性)+ RMSProp(二阶动量,每参数自适应步长)。偏差修正是因为 m、v 初始为 0,前几步被严重低估,除以 (1−βᵗ) 修正。AdamW 把权重衰减解耦出来,修了 Adam 里 L2 与自适应学习率纠缠的缺陷。
  4. NaN = lr 太大梯度爆炸 → ÷10;下降极慢 = lr 太小 → ×3。完全不动(逐位相同)不是 lr 问题,几乎肯定是 bug——查梯度断流或忘了 .step()。
  5. 1e-5 ~ 5e-5,比从零训练小 100 倍。用从零训练的 lr 去微调会直接把预训练权重冲垮,效果还不如冻结主干。
  6. 训练初期参数和优化器统计变化较快,Warmup 用较小步长起步,可缓解大学习率带来的不稳定。是否需要、持续多久,要结合模型和训练配置验证,不能仅凭 batch 超过某个值判断。
  7. 梯度会累加到上一步的梯度上,等于用了错误的(放大且混合的)梯度更新,训练直接废掉。
  8. 症状:学习率衰减快几百倍,loss 降一点就完全不动,看起来像模型不行。发现方法:训练时打印 optimizer.param_groups[0]["lr"]。
  9. 梯度累积(慢 N 倍)、混合精度 AMP(显存减半还更快,几乎白赚)、梯度检查点(慢 30% 换显存)。梯度累积最常见的 bug 是忘了把 loss 除以 accum——等效于学习率放大 N 倍。

🛑 可以停在这里

⚡ 走神救援

  • 先观察学习率与损失,每次只改一个量;报 NaN 时也要排查数据与运算,不能只认定是学习率。
  • Adam 记录梯度及其平方的历史,不是计算 Hessian;AdamW 把权重衰减与自适应更新分开。
  • 批量与学习率没有通用换算。Warmup、调度器和微调步长都需要验证,不机械套固定数字。
  • 训练不动先检查更新链路:梯度、清零、参数更新、调度器调用频率。
  • 显存不足再选工具:梯度累积、混合精度或检查点;按实际配置衡量显存、速度与精度。

下一节 👉 10-正则化全家桶.md

打卡记录保存在你的浏览器里,首页能看到总进度