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

为什么梯度下降在峡谷里要走两百多步?

峡谷里的梯度下降:SGD 与动量
全量梯度一步就要扫完全部数据,所以用小批量;可在条件数 100 的峡谷里,学习率被最陡的方向卡在 0.02 以下,缓的方向要 241 步才走到。动量让一致的方向越走越快、来回的方向互相抵消:同一个峡谷 87 步。
❓

本课一问:拿到梯度之后,沿着它走一步——这么简单的规则,为什么在一个「峡谷」形的 loss 面上要走两百多步?

这一课先不碰:让每个参数用不同的步长(第 2 课);让学习率随步数变化(第 4、5 课);batch 该取多大(岔路课)。

从已有出发

「小明把苹果给了小王」走完前五章搭好的网络,每个位置都输出一个下一 token 的概率分布。参数刚随机初始化时,模型对 248,320 个 token 几乎一视同仁,每个位置的 loss ≈ ln 248,320 = 12.42(C0 第 5 课)。反向传播再给 Qwen3.8-27B 的 27,781,427,952 个参数各算出一个梯度:往哪个方向改一点点,loss 降得最快。

从已有出发 · 累计数据流 点已学环节看形状与参数
解码层

前向的每一环都学过了。这一章看的是下面这个循环里「优化器」那一格——拿到梯度之后,参数到底怎么动:

从已有出发 · 训练的一步(点任一环节看它在 Qwen/Qwen3.8-27B 上有多大) 本课本章后面的课
🔁

这一章为什么换例子。「参数怎么动」在例句上看不见:27,781,427,952 维的 loss 面画不出来,也没法手算。所以本章换成两个替身,结论再回到大模型的真实配置上:① 一个画得出、能手算的二维 loss 面——下面的「峡谷」;② 一个能在浏览器里真训练的小模型(第 2 课起),所有 loss 曲线都是它现场训练出来的。

先自己设计:沿梯度走,每步看全部数据

最直接的规则就是梯度下降:θ ← θ − η·∇L(θ)。η 是学习率,决定这一步走多远。

朴素方案 ①:每一步都用全部训练数据算梯度。梯度最准,可代价是一步就要把全部数据前向、反向各算一遍。DeepSeek-V3 预训练用了 14.8T token:算一次全量梯度 = 把整个训练预算花完,只能走 1 步。

改进是小批量随机梯度下降(SGD):每步随机取一小批数据,用它的平均梯度代替全量梯度。随机一批的梯度是全量梯度的无偏估计 ——方向平均来看是对的,只是每一步带点噪声。DeepSeek-V3 的满 batch 是 15,360 条 × 4,096 token = 62,914,560 token,同样 14.8T token 的数据就能走约 235,240 步(按满 batch 算;它前 469B token 内 batch 是渐增的,真实步数更多)。噪声有什么代价、batch 该多大,放到岔路课。

朴素方案 ②:全体参数共用一个 η,就这么一直走下去。先在一维上看清 η 能取多大。下面是峡谷的两个方向各自的一条抛物线,f = ½λe²,两个方向共用同一个 η:

算法模拟器 · 一维抛物线上的梯度下降(浏览器实时算)

一维上每一步是 e ← (1 − ηλ)·e,所以只看 ηλ 这一个数:在 0 到 1 之间单调靠近谷底;1 到 2 之间越过谷底、来回振荡但收敛;超过 2 就越跳越远 。两个方向共用一个 η 时,上限由最陡的方向决定:λ = 100 要求 η 小于 0.02,而 λ = 1 的缓方向在这个 η 下每步只能缩到 0.98 倍左右。

放回二维,两个方向合在一起就是一条狭长的峡谷:f = ½(x² + 100y²),起点 (−9, 1.5)。

  1. ① 朴素方案
  2. ② 它在哪失败
  3. ③ 改进方案
朴素方案梯度下降,全体参数共用一个 η
🎯

试三件事:① 在「朴素方案」里把 GD 的 η 拖到 0.021,看轨迹飞出峡谷、表格里「到 1% 以内」变成「进不去」;再拖回 0.019,右边分解图里实线(缓方向)几乎是一条缓坡,虚线(陡方向)一上一下地收敛——慢和抖同时出现。② 点「看它在哪失败」再点「换成改进方案」,只打开「动量」一条:同样 η = 0.019,步数从 241 降到 87。③ hover 动量轨迹上开头的几个点,读「动量缓冲 v」:x 分量一步步变大(同一方向在累积),y 分量正负交替(来回的方向在抵消)。

为什么会这样:两个方向的曲率差 100 倍

峡谷的两个方向曲率分别是 λ = 1 和 λ = 100,比值 κ = 100 叫条件数。梯度下降要稳,η < 2/λmax;于是缓方向每步最多缩到 1 − 2/κ 倍,要走到 1% 需要的步数和 κ 成正比。学习率不是「选大一点就快」——它被最陡的方向卡住了,而最慢的方向决定总步数。

这不是二维玩具才有的事。任何光滑的 loss 在一个点附近都可以用二次函数近似(二阶泰勒展开),不同方向的曲率一般不同 。真实大模型的曲率谱没法在页面上画出来;下一课改用一个能直接测的量——同一批数据里,各个参数的梯度差多少。

改进:动量

动量(重球法,Polyak 1964)不直接用这一步的梯度,而是维护一个「速度」v:把过去的梯度按 β 衰减着累加起来,再沿 v 走。

重球动量的一步(每个参数各做一遍)
← v + ; θ ← θ −
👆 公式里带下划线的每一项都可以点——点哪项,讲哪项。
写法有两种:PyTorch 的 SGD(momentum=β)用 v ← βv + g;另一种写 v ← βv + (1 − β)g,只差一个常数倍,相当于把 η 换算一下。本课的组件用前一种。

同一个峡谷上,不同的 (η, β) 各要多少步?下面这张热力图的每一格,都是一组 (η, β) 的收敛速度——不是跑出来的,而是把重球法的 2×2 迭代矩阵在两个方向上的特征值现算出来。点任意一格,右边就画出那组参数的真实轨迹。

算法模拟器 · (η, β) 收敛速度热力图 + 点格子看真实轨迹
🎯

试三件事:① 点最下面一行(β = 0,就是 GD)里颜色最深的位置,读「真实轨迹到 1% 以内」:两百多步。② 点白圈标出的最优点(β∗ ≈ 0.669、η∗ ≈ 0.0331):真实轨迹 45 步,渐近估计 22.9 步——渐近估计忽略了开头的过渡段。③ 沿红色虚线从左下往右上看:β 越大,能用的 η 越大;但 β 贴近 0.99 时颜色又变浅——动量太大会过冲,在谷底来回荡很久。

🔍 最优的 β 为什么是 ((√κ − 1)/(√κ + 1))²?

在二次函数上,重球法每个方向的误差满足一个两步递推,收敛因子是特征方程 z² − (1 + β − ηλ)z + β = 0 两根的最大模。当两根是一对共轭复数时,它们的模恰好是 √β,与 λ 无关;让两个极端方向(λmin、λmax)都刚好落在这个区间里,就得到 β∗ = ((√κ − 1)/(√κ + 1))²、η∗ = (2/(√λmin + √λmax))²,收敛因子 (√κ − 1)/(√κ + 1)。κ = 100 时是 9/11 ≈ 0.818,而最好的 GD 是 99/101 ≈ 0.980:步数从正比于 κ 降到正比于 √κ。这是动量在二次函数上可证明的加速;真实网络不是二次函数,所以 β 最后还是靠实验选(常用 0.9)。

纠正旧课的一句话

旧版 M3 的地形实验写过:「把 η 拖过 0.125,SGD 和动量当场发散」。那个地形是 f = 0.5x² + 8y²(条件数 16)。GD 的上限确实是 2/16 = 0.125,但重球动量(β = 0.9)的上限是 2 × 1.9 / 16 = 0.2375——η = 0.13、0.14 时动量照样收敛,只有 GD 发散。

算法模拟器 · 旧 M3 地形复核(κ = 16)
🎯

试两件事:① 把 GD 和动量的 η 都拖到 0.130:GD 的表格一行显示「进不去」、第 400 步 f 变成天文数字,动量那一行照样收敛。② 再把动量的 η 慢慢拖过 0.2375,看它什么时候才开始发散。

⚠ 常见误解

⚠

「学习率越大,收敛越快。」只在 ηλ < 1 的范围里对。越过 1 开始振荡,越过 2 直接发散;而且判断用的是最陡的方向。一维抛物线上 η 从 0.019 调到 0.021 只多了约 10%,陡方向就从收敛变成每步放大 1.1 倍。

⚠

「动量只是让它走得更快。」动量同时改变了稳定性:上限从 2/λ 放宽到 2(1 + β)/λ,所以旧课「动量和 SGD 一起发散」的说法不成立;它也会过冲——β 太大时轨迹冲过谷底再折返,反而更慢(热力图右上角)。

在演进地图上的位置

优化的演进:每一代都被上一代的某个真数字逼出来(点名字去那一课;完整版在章首页)
  1. 全量梯度下降
  2. 小批量随机梯度下降 SGD
  3. 动量(重球法)
  4. Adam:每个参数一把尺子
  5. AdamW:解耦权重衰减
  6. 学习率 warmup
  7. 学习率衰减:cosine → WSD
  8. batch 大小:渐增,还是一开始就大
  9. 全局梯度范数裁剪
  10. Muon:按矩阵的奇异方向更新
第二代动量(重球法)Polyak 1964你在这里
解决了一致的方向累积、来回的方向抵消;β = 0.9 时同一峡谷 87 步,稳定上限放宽到 2(1+β)/λ。
卡在全体参数共用一个 η:微型模型同一批里,非零梯度的 99% 分位是 1% 分位的约 900 倍(第 2 课浏览器实训)。
数字口径:峡谷步数、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 为官方技术报告自报。

证据三问

问本课的回答
① 它解决什么问题?小批量:全量梯度算不起;动量:单一 η 被最陡方向卡住,缓方向爬得慢、陡方向来回锯齿
② 数学上为什么可能有效?小批量梯度是全量梯度的无偏估计 ;二次函数上 GD 的稳定域和重球法的加速都能直接推出来,步数从正比于 κ 降到正比于 √κ
③ 实验上为什么最后选了它?现代优化器都保留了动量:GPT-3、DeepSeek-V3 的 Adam 取 β₁ = 0.9,DeepSeek-V4 的 Muon 取动量 0.95

一句话

📌

学习率被最陡的方向卡住;动量让一致的方向越走越快、来回的方向互相抵消。

🚦

先停在这里:记住「上限看最陡的方向、步数看最缓的方向」,进下一课。
再深一层:展开上面「最优 β」的 Dig,再去读 Distill 的全文。

下一个缺口

峡谷里只有两个方向,动量一个 β、一个 η 就够了。可真实模型有几百亿个参数,它们的梯度大小能差多少?一个 η 管得过来吗? → 第 2 课:一个学习率管得了所有参数吗?

你可能会问

这一课还没有收到真实提问;你的提问会记到 05-问答记录/C5-L1.md。下面是最常被问到的几个:

🔍 Nesterov 动量是什么?

Nesterov 版本先按旧速度「往前看一步」,在那个位置算梯度再更新;常见写法等价于把这一步的更新改成 g 与 v 的一个组合。我们项目的 SGD 默认开着 nesterov(04-实践项目 minillm/optim.py),Muon 也默认用 Nesterov 形式——Keller Jordan 的博客写的理由是「在我们测过的每个设置里都略好一点」,这是实验结论,不是证明。

🔍 大模型的 loss 面真的长成峡谷吗?

没人能把几百亿维的 loss 面画出来。常见的「3D 地形图」都是沿两个随机方向切出来的二维截面(Li et al. 2017),形状取决于怎么选方向,不能当成真实地形。本课的峡谷只是局部二次近似下的一个最小例子:它说明「不同方向曲率不同」会让单一学习率失效。这件事在大模型里怎么表现,下一课用真实训练中的梯度尺度来看。