一句话总结:学习率决定优化器每一步更新参数的尺度。太大可能震荡甚至发散,太小又会走得太慢;但它不是“取梯度的百分之几”,也不存在适合所有模型的万能数值。
17.1 什么是学习率?
17.1.1 先从最简单的 SGD 看起
神经网络训练时,我们先计算损失函数对参数的梯度,再让优化器更新参数。对最简单的梯度下降(SGD),公式是:
这里:
- 是更新前的参数;
- 是当前梯度;
- (eta)就是学习率。
拿一个参数来算:
learning_rate = 0.1
old_weight = 0.90
gradient = -0.4
new_weight = 0.90 - 0.1 × (-0.4)
= 0.94
梯度是负数,沿负梯度方向走就会增加这个权重,所以它从 0.90 变成 0.94。
17.1.2 学习率不是“梯度的百分比”
lr = 0.1 只表示用标量 0.1 乘梯度,不能笼统地说成“走梯度的 10%”。参数、梯度和损失的尺度会随模型与参数化方式改变,这个 0.1 并没有天然的百分比含义。
而且上面的公式只直接描述 SGD。Adam、AdamW 会先用一阶矩、二阶矩处理梯度,再由学习率缩放最终更新量。因此在真实的大模型训练中,学习率更准确的理解是:它控制优化器更新的总体尺度。
17.2 步子太小、太大与可用区间
可以把训练想成蒙着雾下山:你能感觉脚下最陡的方向,却看不到整座山。
学习率太小:
- 每一步都很短,损失可能下降得很慢;
- 在有限训练预算内,看起来像“没有学会”;
- 但“走得慢”不等于一定困在局部最优。
学习率太大:
- 容易越过谷底,在两边来回震荡;
- 更新量可能不断放大,最终出现发散或
NaN; - 混合精度训练时,数值问题可能更早暴露出来。
落在可用区间:
- 训练损失整体向下;
- 更新量没有突然爆炸;
- 在相同计算预算下,能比过小的学习率更快取得进展。
这里说的是“可用区间”,而不是某个神奇的“完美学习率”。二维碗形曲线只是帮助理解。真实 Transformer 的损失是上亿甚至上千亿参数构成的高维曲面,其中还有鞍点、平坦方向和参数对称性,不能真的靠一张 3D 图找到“全局最低点”。
你也许见过“Adam 就填 3e-4”的玩笑。它之所以好笑,是因为这个数确实常出现在一些训练配方里;但模型规模、批次、数据、参数化和调度一变,它就不再是同一个问题。
17.3 到底哪些参数会被更新?
完整训练一个 Transformer 时,常见的可学习参数包括:
- 词嵌入:
vocab_size × d_model; - Attention 投影:
Wq、Wk、Wv、Wo; - FFN:经典两层 FFN,或门控结构中的多个投影;
- 归一化参数:例如 LayerNorm 的缩放与偏置,或 RMSNorm 的缩放;
- 输出投影:从隐藏状态映射到词表 logits。
输出投影不一定是另一整块独立参数。GPT-2 一类模型常把输出权重与词嵌入权重绑定(weight tying),两处实际引用的是同一份参数。
在 PyTorch 中,一个参数要被某次 optimizer.step() 更新,通常要同时满足:
requires_grad=True;- 它参与了从 loss 到参数的计算图,并得到了梯度;
- 它被交给了这个 optimizer。
如果 param.grad is None,PyTorch 优化器通常会跳过它。反过来,梯度是全零张量时,动量或 AdamW 的 weight decay 仍可能让参数变化。
同一次 optimizer.step() 会遍历各参数组,但并不代表所有参数“走同样一步”。不同参数组可以有不同学习率;AdamW 还会根据每个坐标的历史一阶矩与二阶矩得到不同的有效更新量。
17.4 一个真正连通的 PyTorch 更新例子
下面用一个很小的分类网络演示。重点不是模型本身,而是 loss 必须由这个 model 计算出来,optimizer 也必须持有同一组参数:
import torch
import torch.nn.functional as F
torch.manual_seed(7)
model = torch.nn.Sequential(
torch.nn.Linear(4, 8),
torch.nn.GELU(),
torch.nn.Linear(8, 5),
)
optimizer = torch.optim.AdamW(
model.parameters(), lr=1e-3, weight_decay=0.01
)
x = torch.randn(6, 4)
targets = torch.tensor([0, 3, 1, 4, 2, 3])
before = model[0].weight.detach().clone()
optimizer.zero_grad(set_to_none=True)
logits = model(x)
loss = F.cross_entropy(logits, targets)
loss.backward()
grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
max_change = (model[0].weight.detach() - before).abs().max().item()
assert max_change > 0
print(f"loss={loss.item():.4f}, grad_norm={grad_norm:.4f}")
print(f"max parameter change={max_change:.6f}")
顺序也很重要:
zero_grad()清掉上一步累积的梯度;- 前向传播得到
logits和loss; loss.backward()计算梯度;- 可选的 gradient clipping 限制整体梯度范数;
optimizer.step()才真正修改参数。
不要只看打印到四位小数的权重来判断“有没有更新”。保存更新前的副本,再比较差值,证据会更直接。
17.5 SGD、Adam 与 AdamW 的区别
17.5.1 SGD:公式最直观
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
不考虑 momentum 时,它就是前面的 。这个公式很适合解释学习率,但不等于所有优化器都原样使用梯度。
17.5.2 Adam:先整理梯度的历史
Adam 会维护梯度的一阶矩 和二阶矩 ,经过偏差修正后,更新大致是:
这会产生逐坐标的归一化更新。通俗地说,它不只看“这次坡有多陡”,还参考前几步的方向与尺度。把它说成“自动找到每个参数的最佳学习率”就过头了。
17.5.3 AdamW:把 weight decay 分开
AdamW 的关键不是“修好 Adam”,而是把 weight decay 与 loss gradient 的自适应更新解耦。忽略实现细节,一步可以理解为:
其中 是 weight decay 系数。对 SGD,L2 正则与 weight decay 在常见条件下可以等价;对 Adam 这类带自适应预条件的优化器,两者通常不再等价。
AdamW 是 Transformer 训练中很常见的强基线,但不是所有任务、所有规模下唯一正确的优化器。实际配方也常让二维权重衰减,而把 bias 和归一化参数放进 weight_decay=0 的参数组。
17.6 为什么学习率还要变化?
固定学习率可以训练,但很多 Transformer 配方会使用 warmup + decay:
- Warmup:最初几步从较小的学习率逐渐升到峰值;
- Decay:后续逐渐降低,常见选择之一是 cosine decay;
- 最后学习率:可以降到 0,也可以保留为峰值的一小部分。
Warmup 不是因为“初始梯度一定不准确”,而是训练刚开始时,激活尺度、梯度尺度以及 Adam 的矩估计都还没有稳定。立刻使用峰值学习率,可能让 update-to-weight ratio 过大。Warmup 给系统一个渐进进入工作区间的过程。
下面把步数定义清楚:训练步 step 从 1 数到 total_steps,第 warmup_steps 步正好到峰值,最后一步正好到 min_lr。
import math
def lr_at_step(step, total_steps, warmup_steps, peak_lr, min_lr):
assert 1 <= step <= total_steps
assert 0 < warmup_steps < total_steps
if step <= warmup_steps:
return peak_lr * step / warmup_steps
progress = (step - warmup_steps) / (total_steps - warmup_steps)
cosine = 0.5 * (1.0 + math.cos(math.pi * progress))
return min_lr + (peak_lr - min_lr) * cosine
for step in range(1, total_steps + 1):
lr = lr_at_step(step, total_steps, warmup_steps, peak_lr, min_lr)
for group in optimizer.param_groups:
group["lr"] = lr
optimizer.zero_grad(set_to_none=True)
loss = compute_loss(model, batch)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
这里每一步只调用一次 optimizer.step(),也没有把 cosine 阶段的长度误写成总训练步数。
17.7 配方可以参考,不能照抄
17.7.1 公开模型告诉我们的不是一个万能数值
同一系列里,模型变大时峰值学习率有时会下降,但这不是只由参数量决定的定律。下面是论文报告的具体训练配置,不是我替你的模型开的药方:
| 公开模型 | 参数量 | 全局 batch(tokens) | 峰值学习率 |
|---|---|---|---|
| GPT-3 Small | 125M | 0.5M | 6e-4 |
| GPT-3 XL | 1.3B | 1.0M | 2e-4 |
| GPT-3 13B | 13B | 2.0M | 1e-4 |
| GPT-3 175B | 175B | 3.2M | 6e-5 |
| LLaMA 7B / 13B | 7B / 13B | 4.0M | 3e-4 |
| LLaMA 33B / 65B | 33B / 65B | 4.0M | 1.5e-4 |
数据来自 GPT-3 的 Table 2.1 与 LLaMA 的 Table 2。LLaMA 还报告了 AdamW、betas=(0.9, 0.95)、weight_decay=0.1、gradient clipping 1.0、2000 步 warmup,以及降到峰值 10% 的 cosine schedule。这些数要作为一整套配方理解,不能只抄其中的 3e-4。
17.7.2 Batch 变大,学习率就一定翻倍吗?
“Batch 翻倍,学习率也翻倍”的 linear scaling rule,来自 Goyal 等人对 ResNet-50、ImageNet、同步 SGD 的大批次实验,并且与 warmup 一起使用。它是特定条件下很有用的起点,不是 AdamW 训练 LLM 的普遍定律。
当你改变全局 batch、序列长度、梯度累积、并行方式或模型宽度时,最好把它当作需要重新验证的新配置。真正值得看的还有:loss 曲线、gradient norm、参数更新相对权重的比例,以及相同 token 预算下的验证结果。
17.7.3 一个诚实的起点
如果只是训练本书里的小模型,可以先把下面当作待验证的起点:
optimizer = torch.optim.AdamW(
model.parameters(),
lr=3e-4,
betas=(0.9, 0.95),
weight_decay=0.1,
)
然后做短跑实验:例如固定几百到几千步和相同 token 数,比较 1e-4、3e-4、6e-4。这个范围不是“大模型默认答案”;它只是一组便于发现数量级是否离谱的探针。
17.8 看到异常,别只怪学习率
| 现象 | 学习率相关的可能性 | 也要检查 |
|---|---|---|
| 训练 loss 几乎不降 | 学习率过小 | 标签错位、参数被冻结、loss 未连到模型、数据或代码错误 |
| loss 剧烈震荡 | 学习率过大 | batch 太小、异常样本、梯度尖峰 |
loss 变成 NaN / Inf | 学习率或更新量过大 | 混合精度溢出、坏数据、归一化、除零或 mask 错误 |
| 训练 loss 下降,验证 loss 上升 | 学习率/调度可能不理想 | 过拟合、数据分布差异、验证集污染或太小 |
| 训练 loss 先降后升 | 调度或峰值可能有问题 | 数据顺序变化、恢复 checkpoint 错误、数值不稳定 |
学习率查找器可以做小规模诊断,但对昂贵的 LLM 预训练,它不是一次扫描就能找出“最佳值”的仪器。先用短跑排除明显错误,再在真实 batch、精度和分布式设置上确认,通常更可靠。
17.9 本章总结
核心认知
- SGD 中,学习率直接缩放梯度;AdamW 中,它缩放经过矩估计归一化后的更新,并同时影响 decoupled weight decay 的强度。
- 学习率太大可能震荡、发散;太小会浪费训练预算。我们寻找的是稳定而有效的区间,不是永远正确的常数。
- 只有参与计算图、得到梯度并交给 optimizer 的参数才会按这次优化步骤更新;参数组和自适应归一化会让有效步长不同。
- Warmup + cosine decay 是常见配方,不是唯一配方;边界、总步数与最小学习率必须写清楚。
- GPT-3、LLaMA 的数值应当连同 batch、规模和完整优化器设置一起阅读。
本章交付物
学完这一章,你应该能够:
- 用 SGD 公式解释学习率,而不把它说成百分比
- 说明 SGD、Adam 和 AdamW 的更新为什么不同
- 写出一次前向、反向、裁剪与更新都连通的训练步
- 实现边界明确的 warmup + cosine decay
- 把公开训练配方当作参考,而不是万能默认值
Part 4 总结
恭喜你完成了 Part 4:完整架构!
| 章节 | 主题 | 核心内容 |
|---|---|---|
| 第 13 章 | 残差连接与 Dropout | 残差支路提供短路径,Dropout 只在训练时随机屏蔽 |
| 第 14 章 | 词嵌入 + 位置信息 | 加法保持 d_model 宽度,并把内容与位置送入同一残差流 |
| 第 15 章 | 完整前向传播 | 从 token IDs 到 logits 与训练 loss 的完整流程 |
| 第 16 章 | 训练 vs 推理 | 训练可并行计算序列位置,自回归解码仍有逐 token 依赖 |
| 第 17 章 | 学习率 | 梯度、优化器、参数更新与调度如何真正连起来 |
现在你已经不仅能看懂 Transformer 的前向传播,也知道误差如何沿计算图返回,最后怎样通过优化器真的改变参数。
下一章预告
Part 5 将进入代码实现阶段:
- 第 18 章:手写 Model.py - 模型定义
- 第 19 章:手写 Train.py - 训练循环
- 第 20 章:手写 Inference.py - 推理逻辑
我们会从模型结构开始,把前面讲过的每一块变成能运行的代码。