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

一个学习率管得了所有参数吗?

Adam:每个参数一把尺子
在浏览器里真训练的小模型上,同一批数据里梯度的 99% 分位是 1% 分位的约 900 倍,还有八分之一的参数梯度恰好是 0。一个学习率顾此失彼。Adam 给每个参数除以它自己梯度的均方根:第 1 步每个参数恰好走 ±η。代价是每个参数多存两份状态,以及它只认坐标轴。
❓

本课一问:上一课的峡谷只有两个方向。一个真模型有几百亿个参数,它们的梯度大小能差多少?一个学习率管得过来吗?

这一课先不碰:weight decay 放在哪(第 3 课);学习率随步数怎么变(第 4、5 课);一个权重矩阵的整体结构(第 7 课)。

从已有出发

「小明把苹果给了小王」进模型的第一步,是按 token 查 embedding 表的行(C1)。这一批数据里出现过的 token,它们那几行会收到梯度;没出现过的 token,那一行这一步的梯度恰好是 0。再往后,注意力、FFN、输出层的梯度大小又各不相同。同一个模型里的参数,「该走多大一步」显然不是一回事。

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

用哪个模型测。锚点 Qwen3.8-27B 的训练过程没有公开,我们也不在页面里训练它。所以这一课用一个在浏览器里真训练的字级小模型:《三國志演義》前 24,000 字,4 个字的上下文预测下一个字,24,896 个参数(结构写在组件标题下)。它小,但梯度是真算出来的;同样的测量在我们项目的 3.7M 参数模型上也会做(本课最后)。

先自己设计:全体参数共用一个 η

最省事的就是上一课的做法:动量 SGD,全体参数一个 η。先量一量它要面对的梯度。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案全体参数共用一个 η(上一课的动量 SGD)
同一批数据里,各个参数的梯度差多少? 字级 MLP · 24,896 参数 · 《三國志演義》24,000 字 · 浏览器内真训练
等待开始(滚动到这里会自动开始)

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

🎯

试三件事(先看「朴素方案」):① 读表格:embedding E 的梯度均方根只有输出层 W2 的约 1/55,而 E 里约 77% 的数这一步恰为 0。② 切到「训练 100 步后」:99%/1% 的比值降到约 560,但各组之间仍差好几倍——这不是初始化才有的现象。③ 换「第 2 批」「第 3 批」:比值在 860–950 之间,零的比例在 12% 左右——换一批数据,结论不变。

改进:Adam

Adam(Kingma & Ba 2014)对每个参数单独做两件事:像上一课那样对梯度做滑动平均(一阶矩 m,方向更稳);再对梯度的平方做滑动平均(二阶矩 v,记住这个参数的梯度通常有多大),最后用 m 除以 √v。梯度一直大的参数被压小,梯度一直小的被放大——每个参数有了一把自己的尺子。

Adam 的一步(每个参数各做一遍;t 是第几步)
, , , θ ← θ − · m̂ /
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
第 1 步做完偏置修正后 m̂ = g、√v̂ = |g|,相除只剩符号:每个参数恰好走 ±η,与梯度大小无关(上面「改进方案」里 t = 1 那一行)。第 4 课会回到这件事。
🎯

试三件事(在上面「改进方案」的账本里):① t = 1:缓方向梯度 −9、陡方向梯度 150,位移都是 0.1000(= η)。② 把「偏置修正」切到「关」:第 1 步变成 3.162η;再把 β₂ 切到 0.95:变成 0.447η——偏置修正就是为了消掉这个开局的偏差。③ 把 t 拖到 20:陡方向已经越过谷底(y 变成负数),m 正在由正转负,步长降到 η 的一半以下(0.443η);缓方向仍是 0.958η——梯度大的方向,步子反而没那么大。

放回峡谷里和前两种方法比。注意 Adam 的 η 是另一个单位,下面给它单独一条滑杆:

算法模拟器 · 峡谷上的 GD、动量与 Adam(浏览器实时算)
🎯

试三件事:① 默认设置下读表:GD 241 步、动量 87 步、Adam(η = 0.3)83 步。② hover Adam 轨迹的前几个点:它一开始沿对角线走——两个方向每步各走约 0.3,不管梯度差了 17 倍。③ 把 β₂ 切到 0.95(大模型的常用值),再把 Adam 的 η 拖到 0.5:400 步内进不去 1% 圈,轨迹在谷底附近来回跳。β₂ 小,v 记性短,谷底附近梯度一变小,分母跟着变小,步子又放大——常数学习率停不稳,第 5 课回到这个问题。

代价一:每个参数多存两份

m 和 v 都与参数同形。以 Qwen3.8-27B 的 embedding 表为例:

Adam 对一张参数表的状态(锚点 embed_tokens 的真实形状)
m ← β1m + (1−β1)g, v ← β2v + (1−β2)g⊙g, Δθ = −η·m̂ ⊘ (√v̂ + ε)
输入输出中间结果
边长按对数比例缩放,块边上的数字是真实值;同一维度符号的边在全图等长。
维度含义 真实规模 小规模
V 词表大小(embedding 行数) 248,320 6
d 隐藏维度 hidden_size 5120 4
三个张量都和 embedding 表同形:光这一张表就是 1,271,398,400 个数,m、v 各存一份 FP32,就是 10.2 GB。全模型 27,781,427,952 个参数的 m + v 共约 222.3 GB——比 FP32 的参数本身(111.1 GB)还多一倍。完整的显存账在 C9。

代价二:它只认坐标轴

Adam 的缩放是逐坐标的:第 i 个参数除以第 i 个参数自己的 √v̂。峡谷的两条轴恰好对齐坐标轴时,这就是完美的缩放。如果峡谷是斜的呢?

算法模拟器 · 把峡谷转一个角度(浏览器实时算)
🎯

试三件事:① 把转角从 0° 拖到 45°:GD 始终 241 步、动量始终 87 步——它们只用梯度向量本身,整张图转一下,轨迹跟着转。② 同时看 Adam:83 步涨到 233 步;右边曲线把每 5° 的结果都算了出来。③ 把 Adam 的 η 改成 0.2 再拖一遍:0° 时 101 步,45° 时 327 步。逐坐标的尺子,只在「坐标轴 = 曲率主方向」时量得准。一个权重矩阵的「主方向」一般不对齐任何一个坐标,第 7 课回到这里。

在我们的项目里

同样的测量(各参数组梯度的均方根、分位数与零占比)会在项目的 3.7M 参数 Transformer 上做一遍,第 0 步和第 200 步各一次:

项目真实日志 · 各参数组的梯度尺度

⚠ 常见误解

⚠

「Adam 的学习率和 SGD 的学习率是同一个东西。」不是。SGD 的位移 = η × 梯度,η 的单位是「参数 ÷ 梯度」;Adam 的位移 ≈ η × (±1),η 的单位就是参数本身。所以 GPT-3 的 Adam 取 0.6×10−4、我们微型模型的动量 SGD 取 0.03,两个数放在一起比没有意义。

⚠

「Adam 是按曲率缩放的,相当于二阶方法。」v 是梯度平方的滑动平均,不是 Hessian。峡谷里陡方向的梯度恰好也大,所以看起来像;但到了谷底附近梯度变小,v 仍记着以前的大梯度——它量的是「梯度通常有多大」,不是「这里有多弯」。上面的旋转实验也说明它不是真正的二阶方法:真二阶方法对旋转不敏感。

⚠

「Adam 自适应,所以不用调学习率。」峡谷上取 β₂ = 0.95 时,η 从 0.3 调到 0.5 就进不了 1% 圈;实验课里 AdamW 的三个学习率,最终 loss 也不一样。自适应的是「各参数之间的相对步长」,整体步长 η 仍然要调。

在演进地图上的位置

优化的演进:每一代都被上一代的某个真数字逼出来(点名字去那一课;完整版在章首页)
  1. 全量梯度下降
  2. 小批量随机梯度下降 SGD
  3. 动量(重球法)
  4. Adam:每个参数一把尺子
  5. AdamW:解耦权重衰减
  6. 学习率 warmup
  7. 学习率衰减:cosine → WSD
  8. batch 大小:渐增,还是一开始就大
  9. 全局梯度范数裁剪
  10. Muon:按矩阵的奇异方向更新
第三代Adam:每个参数一把尺子Kingma & Ba 2014你在这里
解决了每个参数除以自己梯度的均方根;第 1 步每个参数的位移恰好是 ±η。峡谷上 η = 0.3 时 83 步。
卡在weight decay 以 L2 形式混进梯度,也被 √v̂ 除:同一个 λ,梯度均方根 10 与 0.01 的两个参数,衰减力度差1,000 倍(R4 算例)。
数字口径:峡谷步数、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 为官方技术报告自报。

证据三问

问本课的回答
① 它解决什么问题?不同参数的梯度尺度差上千倍、还有大量参数这一步梯度为 0,一个全局 η 顾此失彼
② 数学上为什么可能有效?偏置修正后第 1 步每个参数恰好走 ±η;把某个参数的梯度整体乘一个常数,它的更新不变(逐坐标缩放不变) ;「这样的缩放有利于优化」是动机,不是定理
③ 实验上为什么最后选了它?原论文在多种任务上对比;此后 LLM 预训练长期默认 Adam 系:GPT-3 用 Adam(β₂ = 0.95)、DeepSeek-V3 用 AdamW(β₂ = 0.95)

一句话

📌

Adam 给每个参数配一把自己的尺子:步长约等于 η,与梯度大小无关。

🚦

先停在这里:记住「m 管方向、v 管尺度、第 1 步恰好 ±η」,进下一课。
再深一层:读 Adam 原论文算法 1,把上面账本里每一列和算法里的每一行对上。

下一个缺口

Adam 有了。可训练 LLM 时几乎都还要加一个 weight decay,把参数往 0 轻轻拉。把它加进 Adam 的梯度里,它还是原来那个 weight decay 吗? → 第 3 课:Adam 里的 weight decay 为什么失效了?

回扣先记下:「第 1 步每个参数都走满 η」会在第 4 课变成一个问题;「只认坐标轴」会在第 7 课变成 Muon 的出发点。

你可能会问

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

🔍 Adam 之前还有哪些「自适应」方法?

Adam 论文自己说它借鉴了之前的算法(摘要:「on which Adam was inspired」),正文讨论的是 AdaGrad 和 RMSProp:前者把历史梯度平方全部累加(步长只会越来越小),后者改成滑动平均(Adam 的 v 就是这个思路)。RMSProp 出自 Hinton 的课程讲义,没有正式论文。这些细节本课没有逐一核实原文,只作脉络参考。

🔍 PaLM 为什么不用 Adam?

PaLM 用的是 Adafactor(省显存的 Adam 变体,不存完整的 v),并把 β₂ 设成随步数变化的 1 − k−0.8。原文给的理由正好对应本课的零梯度现象:稀有 token 的 embedding 很久才收到一次梯度,短窗口里二阶矩估计得不准(R4 从 PaLM 原文 arXiv 2204.02311 摘录)。