一句话总结:学习率决定优化器每一步更新参数的尺度。太大可能震荡甚至发散,太小又会走得太慢;但它不是“取梯度的百分之几”,也不存在适合所有模型的万能数值。


17.1 什么是学习率?

17.1.1 先从最简单的 SGD 看起

SGD 与 AdamW 中学习率所控制的更新尺度

神经网络训练时,我们先计算损失函数对参数的梯度,再让优化器更新参数。对最简单的梯度下降(SGD),公式是:

θt+1=θt−ηgt,gt=∇θL(θt)\theta_{t+1}=\theta_t-\eta g_t,\qquad g_t=\nabla_\theta L(\theta_t)

这里:

  • θt\theta_t 是更新前的参数;
  • gtg_t 是当前梯度;
  • η\eta(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 参数组的更新流程

完整训练一个 Transformer 时,常见的可学习参数包括:

  1. 词嵌入:vocab_size × d_model;
  2. Attention 投影:Wq、Wk、Wv、Wo;
  3. FFN:经典两层 FFN,或门控结构中的多个投影;
  4. 归一化参数:例如 LayerNorm 的缩放与偏置,或 RMSNorm 的缩放;
  5. 输出投影:从隐藏状态映射到词表 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 更新例子

PyTorch 中从前向传播到 AdamW 更新的一次完整训练步

下面用一个很小的分类网络演示。重点不是模型本身,而是 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}")

顺序也很重要:

  1. zero_grad() 清掉上一步累积的梯度;
  2. 前向传播得到 logits 和 loss;
  3. loss.backward() 计算梯度;
  4. 可选的 gradient clipping 限制整体梯度范数;
  5. optimizer.step() 才真正修改参数。

不要只看打印到四位小数的权重来判断“有没有更新”。保存更新前的副本,再比较差值,证据会更直接。


17.5 SGD、Adam 与 AdamW 的区别

17.5.1 SGD:公式最直观

optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

不考虑 momentum 时,它就是前面的 θt+1=θt−ηgt\theta_{t+1}=\theta_t-\eta g_t。这个公式很适合解释学习率,但不等于所有优化器都原样使用梯度。

17.5.2 Adam:先整理梯度的历史

Adam 会维护梯度的一阶矩 mtm_t 和二阶矩 vtv_t,经过偏差修正后,更新大致是:

θt+1=θt−ηm^tv^t+ϵ\theta_{t+1}=\theta_t-\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon}

这会产生逐坐标的归一化更新。通俗地说,它不只看“这次坡有多陡”,还参考前几步的方向与尺度。把它说成“自动找到每个参数的最佳学习率”就过头了。

17.5.3 AdamW:把 weight decay 分开

AdamW 的关键不是“修好 Adam”,而是把 weight decay 与 loss gradient 的自适应更新解耦。忽略实现细节,一步可以理解为:

θt+1=(1−ηλ)θt−ηm^tv^t+ϵ\theta_{t+1}=(1-\eta\lambda)\theta_t-\eta\frac{\hat m_t}{\sqrt{\hat v_t}+\epsilon}

其中 λ\lambda 是 weight decay 系数。对 SGD,L2 正则与 weight decay 在常见条件下可以等价;对 Adam 这类带自适应预条件的优化器,两者通常不再等价。

AdamW 是 Transformer 训练中很常见的强基线,但不是所有任务、所有规模下唯一正确的优化器。实际配方也常让二维权重衰减,而把 bias 和归一化参数放进 weight_decay=0 的参数组。


17.6 为什么学习率还要变化?

从 warmup 到 cosine decay 的学习率调度及边界

固定学习率可以训练,但很多 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 Small125M0.5M6e-4
GPT-3 XL1.3B1.0M2e-4
GPT-3 13B13B2.0M1e-4
GPT-3 175B175B3.2M6e-5
LLaMA 7B / 13B7B / 13B4.0M3e-4
LLaMA 33B / 65B33B / 65B4.0M1.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 - 推理逻辑

我们会从模型结构开始,把前面讲过的每一块变成能运行的代码。

引用本文 / Cite
Zhang, Wayland (2026). 第 17 章:学习率的理解 - 训练稳定的关键. In Transformer 架构:从直觉到实现. https://waylandz.com/llm-transformer-book/%E7%AC%AC17%E7%AB%A0-%E5%AD%A6%E4%B9%A0%E7%8E%87%E7%9A%84%E7%90%86%E8%A7%A3-%E8%AE%AD%E7%BB%83%E7%A8%B3%E5%AE%9A%E7%9A%84%E5%85%B3%E9%94%AE/
@incollection{zhang2026transformer_17_-_-,
  author = {Zhang, Wayland},
  title = {第 17 章:学习率的理解 - 训练稳定的关键},
  booktitle = {Transformer 架构:从直觉到实现},
  year = {2026},
  url = {https://waylandz.com/llm-transformer-book/%E7%AC%AC17%E7%AB%A0-%E5%AD%A6%E4%B9%A0%E7%8E%87%E7%9A%84%E7%90%86%E8%A7%A3-%E8%AE%AD%E7%BB%83%E7%A8%B3%E5%AE%9A%E7%9A%84%E5%85%B3%E9%94%AE/}
}