📑 本页目录(点开跳转)
09 / 先看直觉,再看细节
梯度给方向,
优化器决定怎么走。
先理解学习率控制步长,再看 Adam 如何利用历史梯度调整更新。
先抓住一个具体结果
一次更新,手算就够
参数是 1,梯度是 2,学习率是 0.1。普通梯度下降这次减去 0.2,新参数是 0.8。步子过大仍可能把损失推高。
普通梯度下降的一步
接下来,按需要选一项
你现在想看什么?
这些入口是选择,不是必须按顺序完成的任务。
可以停在这里
先理解学习率控制步长,再看 Adam 如何利用历史梯度调整更新。
需要更多细节时,继续看完整正文 →原有正文、图解和例子都在下方。按需跳转,不必一次读完。
09 · 优化器与学习率
⏱ 34 分钟 | ⭐ 核心
🎯 一句话
有了梯度(第 8 章),还得决定怎么迈这一步。 学习率是全深度学习最重要的超参数——没有之一。
🏔️ 一、梯度下降的三种规模
对照
全批量 (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 的"噪声"为什么反而有益:
关键信息
🚀 二、四代优化器
操作步骤
- SGD: w ← w − lr·g
- 最朴素。在峡谷地形里会来回震荡
- Momentum(动量):v ← βv + g; w ← w − lr·v
- 💡 像小球滚下山,累积惯性
- ✅ 穿过震荡、冲过小坑
- Adagrad/RMSProp:每个参数有自己的学习率
- 💡 梯度一直很大的参数,学习率自动调小
- ✅ 稀疏特征友好
- Adam = Momentum + RMSProp ⭐ 默认选它
- 同时用一阶动量(方向)和二阶动量(自适应步长)
- ✅ 几乎不用调就能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 就对了。
🎚️ 三、学习率:最重要的旋钮
诊断表(背下来,第 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(大模型必备)
为什么需要:训练最开始参数是随机的,梯度方向很不可靠, 大步走容易把模型带偏(尤其配 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
这种先增大学习率、观察损失变化来寻找合理范围的思路,可参见 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 倍,训练会不稳定甚至发散。
✅ 检查点
- mini-batch 相比全批量和单样本的优势?batch size 和 lr 什么关系?
- 为什么小 batch 的"噪声"反而对泛化有益?
- Adam 结合了哪两个想法?偏差修正为什么必要?AdamW 改进了什么?
- loss 变 NaN 该怎么办?loss 下降极慢呢?loss 完全不动呢?
- 微调预训练模型该用多大学习率?不注意会怎样?
- Warmup 为什么有用?什么时候必须加?
zero_grad()忘了会怎样?scheduler.step()放错位置有什么症状?怎么快速发现?- 显存不够的三个招是什么?梯度累积最常见的 bug 是什么?
👀 答案
- 小批量在梯度估计、内存和并行效率之间折中。线性缩放是部分 SGD 场景的实验起点,不是通用法则;没有统一的 8k 失效线,Warmup 也不能保证无限扩大批量。
- 小 batch 的梯度带噪声 → 更新方向随机抖动 → 抖得出尖锐极小值,抖不出平坦极小值 → 最终停在平坦极小值里,而平坦极小值泛化更好。这就是"SGD 的隐式正则化"。
- Momentum(一阶动量,方向惯性)+ RMSProp(二阶动量,每参数自适应步长)。偏差修正是因为 m、v 初始为 0,前几步被严重低估,除以 (1−βᵗ) 修正。AdamW 把权重衰减解耦出来,修了 Adam 里 L2 与自适应学习率纠缠的缺陷。
- NaN = lr 太大梯度爆炸 → ÷10;下降极慢 = lr 太小 → ×3。完全不动(逐位相同)不是 lr 问题,几乎肯定是 bug——查梯度断流或忘了
.step()。 - 1e-5 ~ 5e-5,比从零训练小 100 倍。用从零训练的 lr 去微调会直接把预训练权重冲垮,效果还不如冻结主干。
- 训练初期参数和优化器统计变化较快,Warmup 用较小步长起步,可缓解大学习率带来的不稳定。是否需要、持续多久,要结合模型和训练配置验证,不能仅凭 batch 超过某个值判断。
- 梯度会累加到上一步的梯度上,等于用了错误的(放大且混合的)梯度更新,训练直接废掉。
- 症状:学习率衰减快几百倍,loss 降一点就完全不动,看起来像模型不行。发现方法:训练时打印
optimizer.param_groups[0]["lr"]。 - 梯度累积(慢 N 倍)、混合精度 AMP(显存减半还更快,几乎白赚)、梯度检查点(慢 30% 换显存)。梯度累积最常见的 bug 是忘了把 loss 除以 accum——等效于学习率放大 N 倍。
🛑 可以停在这里
⚡ 走神救援
- 先观察学习率与损失,每次只改一个量;报 NaN 时也要排查数据与运算,不能只认定是学习率。
- Adam 记录梯度及其平方的历史,不是计算 Hessian;AdamW 把权重衰减与自适应更新分开。
- 批量与学习率没有通用换算。Warmup、调度器和微调步长都需要验证,不机械套固定数字。
- 训练不动先检查更新链路:梯度、清零、参数更新、调度器调用频率。
- 显存不足再选工具:梯度累积、混合精度或检查点;按实际配置衡量显存、速度与精度。
下一节 👉 10-正则化全家桶.md