从0训练LLM · 现代大模型体系 阅读 + 实验 ≈ 30 分钟 页内实验 ×2
C5 优化:参数怎么更新得又快又稳 第 3 课

Adam 里的 weight decay 为什么失效了?

AdamW:把衰减从梯度里拿出来
把 λθ 加进梯度(L2 正则),它会和梯度一起被 √v̂ 除:同一个 λ,梯度均方根差 1,000 倍的两个参数,衰减力度也差 1,000 倍。浏览器里真训练 400 步:Adam+L2 把 embedding 行范数从 3.99 压到 1.19,而且 λ 从 0.01 换到 0.3 几乎不变——λ 失去了调节作用。AdamW 把衰减放到 Adam 之外。
❓

本课一问:训练 LLM 几乎都要加 weight decay,每一步把参数往 0 轻轻拉一点。把它照 SGD 的老办法加进 Adam 的梯度里,它还是原来那个 weight decay 吗?

这一课先不碰:学习率怎么随步数变(第 4、5 课);weight decay 为什么能帮助泛化——那是正则化的话题,本课只看「它有没有按设计的力度起作用」。

从已有出发

「小明」「小王」这类常见 token 的 embedding 行几乎每一批都收到梯度;语料里只出现过几次的字,那一行很少收到(第 2 课的零梯度)。weight decay 的本意是:不管梯度大小,每个参数每步都按同一个比例往 0 缩一点,防止某些权重无限长大。

从已有出发 · 累计数据流 点已学环节看形状与参数
解码层
从已有出发 · 训练的一步(点任一环节看它在 Qwen/Qwen3.8-27B 上有多大) 本课本章后面的课

这一课的「更新」那一格多了一件事。它有两种写法,在 SGD 里完全等价:

所以很多实现(包括 PyTorch 的 torch.optim.Adam)就用 L2 的写法:把 λθ 加进梯度,交给优化器。用两个例子看它在 Adam 里的后果:先用公式算一步的账(输入是你设定的,不是测量值),再在浏览器里的小模型上真训练 400 步。

先自己设计:照搬 L2,把 λθ 加进 Adam 的梯度

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案Adam + L2:把 λθ 加进梯度,再送进 Adam
🎯

试三件事(在「朴素方案」的计算器里):① 默认值读最后一行:L2 的衰减比是 1,000 倍,AdamW 是 1 倍。② 把两个参数的梯度均方根拖成一样:L2 的差距消失——问题只在「不同参数梯度尺度不同」时出现,而第 2 课说过,这正是常态。③ 把 λ 拖大拖小:L2 那两列同比例变,两行的比值始终等于梯度均方根之比。

改进:AdamW

AdamW 的一步更新(对每个参数 θ)
← θ − · ( / + )
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
每参数的优化器状态:m(4 字节)+ v(4 字节)。混合精度训练里还要再存一份 FP32 的参数副本,合起来 12 字节——旧版课程在这里讲的那块「最大的橙色」;完整的显存账在 C9。
🔍 AdamW 的「W」是什么?

AdamW 的 W 是解耦的权重衰减(decoupled weight decay)。论文标题里根本没有「AdamW」——它叫《Decoupled Weight Decay Regularization》。它的贡献不是发明一个新优化器,而是指出一个大家都没注意到的实现问题:在 Adam 里,以 L2 形式塞进梯度的衰减会被自适应分母除掉,于是「参数或梯度的历史幅度大的权重,被约束得更少」(论文 §1 原话的意思)。解耦后衰减直接作用在参数上。arXiv 1711.05101,本地论文库有(S3-05)。

真训练一遍:同一个 λ,衰减到了哪里

三个 run 共用同一个初始化和取批顺序,学习率 3×10−3(常数,前 20 步 warmup)、400 步,只改衰减的写法:

浏览器内真训练 · Adam + L2 vs AdamW(每改一次 λ 重新训练)
Adam + L2 与 AdamW:同一个 λ,衰减到了哪里? 字级 MLP · 24,896 参数 · 《三國志演義》24,000 字 · 浏览器内真训练
等待开始(滚动到这里会自动开始)

数字来源:浏览器实时训练(纯 JS,Float64)。模型、语料、种子写在上方;同一组对照里所有 run 共用初始化和取批顺序,只改被比较的那一项。换种子重跑可以看到噪声有多大——差异小于种子间波动时不下结论。

🎯

试三件事:① 默认 λ = 0.1:embedding 行范数从 3.99 出发,Adam+L2 压到约 1.19,AdamW 约 3.57,验证 loss 约 3.81 对 3.66。② 切到 λ = 0.01 再训练:AdamW 的行范数回到约 3.95(衰减变弱了),Adam+L2 仍是约 1.2。③ 再切到 λ = 0.3:AdamW 降到约 2.85,Adam+L2 还是约 1.19——下方累积表把你跑过的 λ 排在一起,Adam+L2 那一列几乎不动。

为什么 Adam+L2 的结果几乎与 λ 无关?这个小模型里 embedding 的梯度均方根只有 10−4–10−3 量级(第 2 课:初始化时约 2×10−4,训练 100 步后约 1.5×10−3),而 λw 在 0.1 量级。送进 Adam 的「梯度」g + λw 几乎全是 λw,于是 m̂ ÷ √v̂ ≈ λw ÷ |λw| = w 的符号:每步把每个参数往 0 推约一个 η,λ 被分子分母约掉了。这就是计算器里那个比例关系的另一个极端:梯度越小的参数,L2 在 Adam 里越接近「按符号推满 η」。

两种失效其实是同一件事:衰减一旦混进梯度,它的力度就由 √v̂ 决定,而不是由 λ 决定。

⚖️

这里的 loss 对比只说明「写法不同,结果不同」,不说明 AdamW 在所有任务上都更好。这个 2.5 万参数的小模型 400 步就过了一遍多数据,正则化的作用有限;论文的泛化收益来自图像分类上的实验。本课要带走的是可证明的那一半:两种写法不等价,Adam+L2 的实际衰减力度不受 λ 控制。

现代做法:AdamW 成为默认,剩下的是「哪些参数衰减」

⚠ 常见误解

⚠

「weight decay 就是 L2 正则。」只在 SGD 下成立。在 Adam 这类自适应优化器里,两者不等价:L2 的梯度 λθ 会被 √v̂ 除掉。

⚠

「torch.optim.Adam(weight_decay=0.1) 就是 AdamW。」不是。PyTorch 源码里这个参数的写法是 grad = grad.add(param, alpha=weight_decay),文档写着「weight decay (L2 penalty)」——实现的正是本课的朴素方案。要解耦衰减,用 torch.optim.AdamW,或者在新版 Adam 里另开 decoupled_weight_decay=True。

⚠

「λ 调大,约束就变强。」在 AdamW 里是;在 Adam+L2 里不一定——上面的实验里 λ 从 0.01 到 0.3 翻了 30 倍,embedding 行范数只从 1.22 变到 1.19。

在演进地图上的位置

优化的演进:每一代都被上一代的某个真数字逼出来(点名字去那一课;完整版在章首页)
  1. 全量梯度下降
  2. 小批量随机梯度下降 SGD
  3. 动量(重球法)
  4. Adam:每个参数一把尺子
  5. AdamW:解耦权重衰减
  6. 学习率 warmup
  7. 学习率衰减:cosine → WSD
  8. batch 大小:渐增,还是一开始就大
  9. 全局梯度范数裁剪
  10. Muon:按矩阵的奇异方向更新
第四代AdamW:解耦权重衰减Loshchilov & Hutter 2017你在这里
解决了衰减放在 Adam 之外:每个矩阵参数每步乘 (1 − ηλ),与梯度大小无关。
卡在开局第 1 步,每个参数都走满一个 η:微型模型峰值 η = 0.3 时,第 2 步的 loss 从 5.55 跳到35.4(第 4 课浏览器实训)。
数字口径:峡谷步数、Newton-Schulz 范围、DeepSeek-V3 与 WSD 的步数账在构建时现算;标「浏览器实训」的来自本章 MicroTrainer 的默认设置(种子 1,Chrome 实测);DeepSeek-V3 数字出自其报告 §4.2(本地 PDF);Moonlight「约 2 倍」出自 arXiv 2502.16982 摘要;Kimi K2「max logit > 1000」出自 arXiv 2507.20534;Qwen3.8-Flash-Next 为官方技术报告自报。

证据三问

问本课的回答
① 它解决什么问题?让 weight decay 真的按 λ 设计的力度作用在每个参数上,与梯度尺度无关
② 数学上为什么可能有效?「SGD 下 L2 与 weight decay 等价、Adam 下不等价」是代数事实 ;weight decay 为什么帮助泛化,是正则化的理论动机,不是定理
③ 实验上为什么最后选了它?论文在图像分类上报告 AdamW 明显改善了 Adam 的泛化、并让学习率与衰减系数两个超参更好分开调 ;之后 GPT-3、DeepSeek-V3、Kimi K3 等都用解耦衰减

一句话

📌

衰减应该作用在参数上,不应该混进梯度再被 √v̂ 除掉。

🚦

先停在这里:记住「L2 进 Adam 会被 √v̂ 除,AdamW 把衰减拿出来」,进下一课。
再深一层:打开本地论文 S3-05,看算法 2 里 Adam+L2 与 AdamW 只差的那一行,再读命题 2(自适应优化器下两者不等价)。

下一个缺口

优化器本身(AdamW)准备好了。可是第 2 课埋下的那件事还没解决:第 1 步,每个参数都走满一个 η。如果一开局就用峰值学习率,会怎样? → 第 4 课:为什么一开局就用峰值学习率会炸?

你可能会问

这一课还没有收到真实提问;你的提问会记到 05-问答记录/C5-L3.md。

🔍 为什么归一化层的增益、偏置通常不做衰减?

它们是一维参数,作用是调节尺度或平移,本来就不多;把归一化增益往 0 拉,相当于逼着整层输出变小,和它「恢复尺度」的作用对着干。常见实现(包括我们项目的 optim.py)只对矩阵做衰减。这是工程惯例,各家不完全一样:OLMo 2 连 embedding 也不衰减,DeepSeek-V4 把 RMSNorm 权重交给 AdamW 并用 weight decay 0.1(报告 §4.2.2 的写法是对 AdamW 部分整体设 0.1,是否排除一维参数没有单独说明)。