12 课 · 训练流程与优化器
LESSON 12 · 卷十二 那一步怎么迈

零件都齐了,可你连第一步都迈不稳

前向(第 5~7 课)、损失(第 10 课)、梯度(第 11 课)—— 你手里已经有一套完整的零件。 把它们串起来,代码长这样: for step in range(8,000), 里面五行。真正难的不是这五行,而是第 4 行里那个数字: 这一步该迈多大?迈小了 8,000 步走不完, 迈大了第 7 步就飞到 nan。这一课把「迈步」这件事从一行公式, 拆成三个优化器、一条学习率曲线、两个限速器。

STAGE 01
串成循环
五个动作 · batch 怎么切
STAGE 02
往哪走
SGD / Momentum / Adam
STAGE 03
走多大
预热 + 余弦衰减
STAGE 04
别炸
裁剪 + 权重衰减

先说清楚这节课的位置。到第 11 课为止, 你学会了「怎么算出梯度」—— 那是一个数学问题,答案唯一。 这一课开始,换成工程问题:梯度到手之后, 往哪个方向走、一步走多远,没有唯一正确答案, 只有「这样调通常更好」。

所以我们不打算给你一堆名词。我们打算给你一座山谷和三条轨迹 —— 真的把三个优化器跑一遍,每一步的数字都印在页面上。 看完你会自己得出那个结论:这三个名字的差别, 全在「它怎么决定这一步迈多大」

第 1 站

五个动作,串成一个循环

你笔记本上那五行代码,第一次跑起来时会卡在第一行:一次喂几条样本?语料一共 4,000 条, 一次全塞进去行不行?

先别急着回答。我们把循环里的五行动作摆出来,点开每一行看它在那一步里的真实数字。 模型还是第 10 课那座最简单的小山谷: loss(w) = (w − 3,起点 w = 9 —— 每一步都能按计算器核对。

招牌演示你早晚要写的那五行 —— 一行一行走给你看
0 步 | 现在 w = 9.000000,loss = 36.000000
还没开始走 —— 按一下「走一步」。循环一启动,这五行会按顺序各干各的活, 点开哪一行,右边就是那一步里这行的真实数字
看清楚:整个循环里没有一步是在「求导」 —— 导数在第 3 行(loss.backward())就已经全部算完了。 第 4 行只是照着一个极简的公式挪一下: w ← w − 0.1 × 梯度。这一课要讲的, 就是这一行的四个变种(SGD / Momentum / Adam),以及它前面那些 「步子该多大」的开关。
另外注意第 5 行:它什么也不学,只是把账本划掉。 少了这一行,梯度会一步接一步叠上去 —— 第 11 课第 5 站那个坑。
已经走过的 0 步(每行都能按计算器核对:w ← w − 0.1 × 2(w − 3))
w(这一步的起点)
loss
梯度
w ← w − 0.1×梯度
点上面的「走一步」试试 →
一个 batch 装几道题?语料 4,000 条,训练 8,000
batch 大小
过一遍语料要几步
8,000 步 = 过几遍
每条样本被用几次
batch = 8
500
16.0
16
batch = 32
125
64.0
64
batch = 128
32
250.0
256
batch = 512
8
1000.0
1024
一个 epoch = 把 4,000 条语料从头到尾过一遍 = 125 步。所以 8,000 步 等于把语料过了 64.0 遍, 每条样本平均被看了 64 次。
batch 越小,一步里看到的样本越少、噪声越大 —— 但同样步数下语料被反复嚼的次数越多。 这不是「哪个对」的问题,是一对必须一起调的旋钮:小 batch 靠步数多来补,大 batch 靠步子大来补
点「走一步」:五行代码依次亮起来,右边显示这一步里这行的真实数字, 下面那张表把走过的每一步都记下来。 第 4 行是主角,第 1 行管数据,第 5 行只管清账。
图 12-1 · 山谷 loss(w) = (w − 3)²,起点 w = 9,学习率 0.1; 第 1 步梯度 12.000000,走 24 步 loss 从 36.000000 掉到 0.000803
为什么不能一次把 4,000 条语料全塞进去算一次梯度? 想想一块显卡的显存里同时要装下什么。先别往下翻,真的想 30 秒。

上面演示里 batch 那排按钮,换一换就能看到另一件容易搞混的事:「步」和「遍」不是一回事

行囊 · step / batch / epoch 到底是三个什么量
batch:一次喂给模型的样本条数(这里可拖 8 / 32 / 128 / 512)。
step:走一次那五行代码 = 看一个 batch = 更新一次参数。
epoch:把 4,000 条语料从头到尾过一遍

换成 batch = 32:一个 epoch 要 125 步; batch = 512 时只要 8 步。

所以「练了 8,000 步」这句话, 在 batch=32 时是过了 64.0 遍语料(每条样本被看了 64 次), 在 batch=512 时是过了 1000.0 遍 (每条被看了 1024 次)。同样多的步数,样本被嚼的次数差 16: 大 batch 一步顶很多步的信息量,就要用更少的步走完同样遍数, 于是必须把步子迈大 —— 这两件事是绑在一起的。
第 2 站

同一座山谷,三条路

第 10 课那座山谷只有一维:一个参数,一个方向。真实模型不是 —— 它有 798,720 个参数,也就是 798,720 个方向。 而这 798,720 个方向里, 大部分几乎平坦,少数极其陡峭。

我们造一座最小的「病态」山谷,只留两个方向,陡峭程度差 20

L(w1, w2) = ½ · (1·w1² + 20·w2²)

w1 方向是一条又长又平的长廊,w2 方向是一堵陡壁。 从 (9, 1) 出发,loss = 50.500000。 先用最朴素的那个办法 —— SGD,也就是第 10 课学的 「w ← w − 学习率 × 梯度」—— 走几步看看,用 0.09 这个学习率:

SGD @ lr=0.09
出发点
第 1 步
第 2 步
第 3 步
w1(平方向)
9.000000
8.190000
7.452900
6.782139
w2(陡方向)
1.000000
-0.800000
0.640000
-0.512000

看清楚 w2 那一行:1 → −0.8 → 0.64 → −0.512。 符号每步翻一次,绝对值每步乘 0.8 —— 它在陡壁上来回弹, 一步冲到对面,再一步弹回来。而 w1 那一行: 9 → 8.1900007.4529006.782139, 每步只乘 0.910000。 为什么差这么多?因为更新的系数是 1 − 学习率 × 这个方向的二阶导

平方向:1 − 0.09 × 1 = 0.910000 ← 慢慢缩
陡方向:1 − 0.09 × 20 = -0.800000 ← 缩得很凶,而且是负数

那个负数就是「弹」的来源。同一个学习率,在平方向太小、在陡方向太大 —— 这就是所有优化器要解决的第一个问题。下面的图把这件事画出来了: 三条轨迹跑的是真代码,每一步都踩在页面上。

招牌动画同一座山谷、同一个起点、同一个学习率 —— 三条真轨迹
学习率:SGD 正好踩在甜点上
学习率0.09
步数:现在停在 第 60 步只看:
最外圈:L = 45谷底 (0, 0),loss = 0起点 (9, 1),loss = 50.500000← w1(平方向,系数 1↑ w2(陡方向,系数 20注意纵轴被拉长了约 4 倍 —— 真实比例下这些椭圆细得像发丝
SGD · 只看脚下这一步Momentum · 带着惯性走Adam · 每个参数自己调步子
👆 图怎么读:一圈一圈的椭圆是等损失线(真按 L = ½(1w1² + 20w2²) = 常数 算出来的,水位 0.5 / 2 / 6 / 15 / 30 / 45),越往里 loss 越小;绿点是谷底 (0, 0)。 每条折线是一步一步踩出来的真实轨迹,拐弯的地方就是「越过谷底又折回来」。 轨迹走出画框时,会变成虚线并标一个 ✕ —— 那不是画不下, 是参数真的跑到几万以外去了(旁边表格里有具体数字)。 换学习率档位、拖学习率滑块、按「单步 ▸」一步一步踩,看谁先飞、谁还活着 —— 这张图就是「调学习率」这件事的全貌。
学习率 0.09
SGD
Momentum
Adam
60 步的 loss
0.000493
0.069051
8.598993
这一步的 w1
0.031385
0.317393
4.144192
这一步的 w2
0.000002
-0.043223
-0.034396
山谷是 L = ½·(1·w1² + 20·w2²) —— w1 方向的坡是 1,w2 方向是 20陡了 20。 真实的等高线椭圆,长短轴差 √204.5 倍, 细得像根发丝;图上为了看得清,纵轴被拉长了约 4 倍, 所以它看起来才像「一圈一圈」。 三个优化器都从 (9, 1) 出发(loss = 50.500000), 用同一个学习率 0.09,各走 60 步。
现在这一档 SGD 正好踩在甜点上:60 步把 loss 从 50.500000 压到 4.93e-4。 注意 SGD 的 w2 一步就回 0 了,之后 w1 每步 ×(1 − 0.09×1) = 0.91 地往回缩 —— 平方向只能这么一步一点地磨。
把学习率从头扫到尾每一档都换一个冠军 —— 但只有一个从来没炸过
学习率
SGD
Momentum
Adam
0.01
12.124906
0.013940 ★
37.446353
0.02
3.585784
0.004921 ★
30.689579
0.05
0.085958
0.048795 ★
18.926438
0.09
4.93e-4 ★
0.069051
8.598993
0.15
💥 第 20 步
0.070193 ★
1.530515
0.2
💥 第 13 步
💥 第 32 步
0.100429 ★
0.5
💥 第 7 步
💥 第 7 步
0.058431 ★
2
💥 第 4 步
💥 第 4 步
0.032924 ★
表里每一格都是「用这个学习率跑满 60 步之后的 loss」(★ = 这一档最好的那个)。 竖着看一遍就明白了:lr = 0.01 时 SGD 几乎没动、Adam 更慢lr = 0.09 换成 SGD 赢;再往上 SGD 和 Momentum 先后爆炸, 只剩 Adam 一路走到 lr = 2 都还在走。
所以「Adam 比 SGD 好吗」这个问题问错了。它们的学习率根本不在一个量纲上: SGD 一步挪的是 lr × 梯度,梯度大就挪得多; Adam 一步挪的是 约等于 lr(因为它把梯度除掉了自己的尺度)。 调 SGD 是在调「每走一步看多远」,调 Adam 是在调「每步固定挪多长」—— 前者要贴着曲率小心试,后者宽容得多,代价是它对每一条方向都一视同仁, 在平方向上反而慢。真实的默认选择是 AdamW + 1e-4 ~ 1e-3, 正因为这个量级对绝大多数任务都「不死」,省下的调参时间比那点速度值钱。
👆 表怎么读:横着看一行 = 同一档学习率下三个优化器的终局; 竖着看一列 = 同一个优化器在不同学习率下的耐受度。 💥 表示参数已经飞到 |w| > 1e6 —— 数值上的「发散」, 实际训练里你会看到 loss 变成 nan。 注意小档位那两行:Adam 反而最差。这不是 bug —— 它的步长被钉死在 lr 附近,60 步根本走不完从 9 到 0 这段路。
左边换学习率档位、换步数、只看某一条。 等高线是真按 L = ½(w1² + 20w2²) 算出来的椭圆; 折线是一步一步算出来的位置。先把学习率停在「中 · 0.09」看清 SGD 怎么在陡壁上弹, 再切到「大 · 0.5」—— 那才是这一站最要紧的一幕。
图 12-2 · Start (9, 1),loss = 50.500000; 用 0.0960 步后:SGD 4.93e-4、 Momentum 0.069051、 Adam 8.598993
两个方向陡 20 倍,同一个学习率伺候不了两边。 如果让你给「迈步」这件事加一条规则,你会加什么?先想一个你自己的主意,再往下看。
学习率调的不是「步子有多大」,
是「步子相对这个方向的陡峭程度有多大」。

下面这张表是这一站的全部答案。它把学习率从 0.01 一路扫到 2, 每一档都让三个优化器各跑 60 步,看谁还剩着:

学习率
SGD
Momentum
Adam
0.01
12.124906
0.013940 ★
37.446353
0.02
3.585784
0.004921 ★
30.689579
0.05
0.085958
0.048795 ★
18.926438
0.09
4.93e-4 ★
0.069051
8.598993
0.15
💥 第 20 步
0.070193 ★
1.530515
0.2
💥 第 13 步
💥 第 32 步
0.100429 ★
0.5
💥 第 7 步
💥 第 7 步
0.058431 ★
2
💥 第 4 步
💥 第 4 步
0.032924 ★

请注意你刚刚做了什么 —— 你把「学习率」这个旋钮从头拧到尾, 看到了一件平时很难看清的事:每一档都换一个冠军。 小档位 Momentum 赢,中档 SGD 赢,两个大档只剩 Adam 没炸。 所以「哪个优化器更好」这个问题本身问错了, 真正该问的是:它能容忍多宽的学习率范围?SGD 的窗口只有 0.01~0.09 这么窄, Adam 从 0.092 都活着。

面试常问 · Adam 到底替掉了什么

标准答案是「Adam 用梯度的一阶矩和二阶矩做自适应学习率」。 但面试官更想听的是代价那一半

自适应是有代价的 —— 它给每个参数各存了两份额外的量(m 和 s)。 我们这个 798,720 参数的小模型, 参数本身只有 3,194,880 字节(3.05 MiB), 加上优化器状态就变成 9,584,640 字节 (9.14 MiB)—— 整整 3。 换成 7B 模型,光这一份就是 78.2 GiB。

另一件事:SGD 的梯度是「这批样本的平均」,它本身没有噪声以外的偏差; 而 Adam 的分母 √s 在训练早期很小,会把梯度放大得厉害 —— 这就是必须预热(下一站)的原因之一。知道它强在哪、也说得清它贵在哪,才算答完整了。
第 3 站

学习率不是常数

到这里为止,我们一直把学习率当成一个固定值。可真实的训练里, 它从第 0 步到第 8,000 步, 会变化整整 10

这不是什么玄学调参技巧,是两个非常具体的理由——一个管开局,一个管收尾。

开局:第 0 步时,参数是随机初始化的,算出来的梯度方向几乎全是噪声。 这时候如果直接用峰值学习率 3e-4, 等于闭着眼睛一脚油门踩到底 —— 第 11 课讲的那个梯度爆炸,在这里不是意外,是必然。 所以要先用几百步小碎步,让优化器摸清「这块地形的尺度大概是多大」, 再放开跑。这段越来越大的过程叫预热(warmup)

收尾:训练后期,loss 已经躺在一个又窄又浅的坑底附近。 学习率不降,算法就会一直从坑壁上弹过去弹回来 —— 就是第 2 站 adam 那条轨迹。降下来才收得住。

动手拖预热 + 余弦衰减:一条学习率曲线上的每个点都是算出来的
预热步数:
第几步4,000
峰值 3e-4(= 0.0003峰值的 1/10 = 3e-50.0000299999999999999974,000 步:1.88e-402000400060008000训练步数(8,000 步)学习率
第 n 步
0
100
400
800
1,600
4,000
8,000
学习率
0.00e+0
3.75e-5
1.50e-4
3.00e-4
2.92e-4
1.88e-4
3.00e-5
拖上面那个「第几步」的滑块 —— 现在停在 4,000,那一刻的学习率是 1.88e-4(图上那个红点)。这一档的预热是 800 步
这一步已经过了预热段(s = 4000 ≥ W = 800),公式是 lr = P × ( 0.1 + 0.9 × ½ × (1 + cos(π × (s − W) ÷ (T − W))) )
先算进度:p = (s − W) ÷ (T − W) = (4000800) ÷ (8000800) = 0.4444
再算余弦:cos(π × 0.4444) = 0.1738
括号里:0.1 + 0.9 × ½ × (1 + 0.1738) = 0.6282
最后乘上峰值:lr = 3e-4 × 0.6282 = 1.88e-4
停在任意一步都能这样算一遍 —— 印出来的每个数都从上一个数往下算,按计算器能复现。
曲线上的每一个点都是 lrAt(step, 800) 当场算的: 前 800 步从 0 线性升到峰值 3e-4, 之后按余弦从峰值慢慢降到 3e-5 —— 整条曲线的总跨度 10
为什么要预热?刚开局时参数还是随机的,梯度方向几乎全是噪声。 这时候就用峰值学习率,等于「闭着眼睛一脚油门踩到底」—— 第 11 课那个梯度爆炸不是意外,是必然。 先用几百步小碎步让优化器摸清地形的尺度,再放开跑。
为什么要衰减?到了后期,loss 已经躺在一个又窄又浅的坑底附近。 学习率不降,算法就一直在坑壁上弹来弹去(就是第 2 站 adam 那条震荡的轨迹)—— 降下来才收得住。预热管开局不炸,衰减管收尾不收不住,两件事各管一头。
👆 图怎么读:横轴是第几步,纵轴是那一刻用的学习率。 两条灰虚线是天花板(峰值)和地板(峰值的 1/10)。 点上面的档位切换预热步数,看四条曲线怎么变 —— 黄色那条是你现在选的。拖「第几步」的滑块,红点会沿着曲线走, 下面的读数就把那一刻的 lr 和公式代入一起算给你看。 注意末尾都一样: 不管预热多久,最后都落到同一个地板上, 因为余弦的形状只由总步数和两端决定。
换「预热多少步」,看曲线的前半段怎么变。 每个点都是 lrAt(step, 预热步数) 当场算的,不是画出来的示意折线。 末尾四条曲线会重合 —— 因为余弦的形状只由总步数和两端决定。
图 12-3 · 峰值 3e-4,末尾 3e-5; 预热 800 步时第 0 步 0.00e+0、第 800 3.00e-4、第 44001.65e-4
这条曲线到底是怎么算的
设预热步数 W,总步数 T = 8,000, 峰值 P = 3e-4,地板 = P 的 1/10。那么第 s 步的学习率是:

若 s < W: lr = P × s ÷ W(线性升上去)
若 s ≥ W: lr = P × ( 0.1 + 0.9 × ½ × (1 + cos(π × (s − W) ÷ (T − W))) )

取 W = 800、s = 0:lr = 3e-4 × 0 ÷ 800 = 0.00e+0(从 0 起步)
s = 800 时刚好升到顶:lr = P = 3.00e-4
s = 4400(衰减段的正中间,此时 (s − W) ÷ (T − W) = ½): cos(π × ½) = cos(π/2) = 0, 所以 lr = P × (0.1 + 0.9 × ½) = P × 0.55 = 1.65e-4
s = T 时:cos(π) = −1,lr = P × (0.1 + 0) = 3.00e-5

每一步都能按计算器复现 —— 页面上那个表格里印的,就是这个式子。
面试常问 · 预热到底防的是什么

常见的错误答案是「防止梯度太大」。更准确的说法是:防止「优化器对梯度尺度的估计」还没准的时候就下重手。对 Adam 尤其明显 —— 它第一步的分母 √s 只由一两个样本的梯度算出来, 非常不稳,会给出一个方向离谱、长度极大的步子。 预热就是给这个「估计过程」留出时间。这也是为什么换优化器时预热长度往往要跟着改,它不是个能照抄的超参。

第 4 站

给更新量装两个限速器

学好了方向和步子大小,还有一类事故没防住:某一步的梯度突然大得离谱。语料里混进一条格式奇怪的样本、或者某个数值刚好卡在极端 —— 一步就能把攒了很久的参数踹飞,前面几万步白跑。

对策简单到粗暴:整批梯度加完之后,量一下它的长度; 如果超过阈值,就整体按比例缩回去。方向一点都不改,只把长度砍到阈值。

动手拖梯度裁剪与权重衰减:两个「限速器」的真数字
裁剪阈值 max_norm:
g₁g₂|g| = 1没裁剪 (9.000000, 12.000000),长度 15.000000裁剪后 (0.600000, 0.800000),长度 1.000000
三条样本各自的梯度没裁剪的整批梯度裁剪后(方向不变,长度砍到 1
这一批三条样本的梯度是 ((3, 6) + (4, 5) + (2, 1)), 加起来 (9.000000, 12.000000), 长度 |g| = √(9² + 12²) = 15.000000
阈值设成了 1,而实际长度是 15.000000 —— 超了 15.0。所以整批梯度乘上 0.066667(= 1 ÷ 15.000000), 变成 (0.600000, 0.800000),长度正好被压回 1.000000
注意它缩的是整批的方向,不是把某条样本挑出来扔掉。 这一点很关键:方向不变、只砍长度,所以该往哪走的信息一点没丢, 丢的只是「这一步迈多大」。
👆 图怎么读:箭头都是从原点出发的梯度向量 —— 灰的细箭头是三条样本各自的梯度(第 1 条最陡); 深蓝的粗箭头是它们相加的结果(整批的梯度,长度 15.000000); 橙色的是裁剪之后的(长度 = 1)。 虚线圆是阈值圈:整批梯度一旦伸出圈外,就被按比例拉回圈上。现在是超出状态,所以你看到蓝橙两根箭头不一样长。
第二个限速器权重衰减:每一步顺手把参数往 0 拽一点点
权重衰减系数 wd:
wd0.010
出发时 w = 1.000 —— 每一步都往这儿拽一点0.000.250.500.751.0002,0004,0006,0008,000训练步数(8,000 步)参数还剩多少8,000 步后剩 97.63%(缩掉 2.37%)
👆 下半块这张图怎么读:纵轴是「参数还剩多少」(出发时都是 1.00), 横轴是第几步。每步乘一次 (1 − lr × wd)(这里的 lr 就用这一课的峰值 3e-4),连起来就是一条指数衰减曲线。 拖上面的 wd 滑块:曲线立刻变陡(wd 大)或者变平(wd 小), 红点标出 8,000 步之后还剩 97.63%。 灰的三条是预设档位的对照 —— wd = 0 那条就是贴着 1.00 的水平线,一点都没拽动。 注意它永远只是「往 0 靠」,不会真的到 0:越接近 0,拽的力(wd × w)也越小。
wd = 0
wd = 0.01
wd = 0.1
每步 × 1.00000000
每步 × 0.99999700
每步 × 0.99997000
8,000 步缩掉 0.00%
8,000 步缩掉 2.37%
8,000 步缩掉 21.34%
代码里它就一行:w ← w × (1 − lr × wd), 乘开之后约等于 w ← w − lr × wd × w —— 相当于在梯度上偷偷加了一项 wd × w,把参数往 0 的方向拉。
wd = 0.01 时每步乘上 0.99999700,看着像没变; 但 8,000 步乘下来, 一个「梯度永远是 0」的参数会被拽走 2.37%。 这就是它的全部作用:不许任何一个参数长得太大。 大到没必要的参数,通常是模型在死记训练数据(第 13 课要讲的过拟合)—— 所以这一项是治过拟合的第一味药。
注意它和裁剪的分工:裁剪看的是这一步的梯度,衰减看的是参数本身的大小。 一个管「别被一条烂样本带跑偏」,一个管「别把权重养得太肥」。
上半块是裁剪:换阈值,看整批梯度的长度怎么被压回去,方向一点没变。 下半块是权重衰减:换 wd,看 8,000 步下来 参数会被拽走百分之几。
图 12-4 · 三条样本梯度 (3,6)+(4,5)+(2,1) = (9,12),长度 15.000000; 阈值 1 时缩成 (0.6, 0.8),长度 1
梯度长度是 15.000000,阈值设成 3。为什么不干脆把三条样本里最长的那条扔掉,而要整批一起缩?想想这两件事对「方向」的影响有什么不同。

第二个限速器是权重衰减。它跟梯度没关系, 看的是参数本身有多大:每一步顺手把参数往 0 的方向拽一点点。w ← w × (1 − 学习率 × wd),就这样一行。 为什么需要它?—— 因为「参数长得太大」通常是模型在死记硬背训练数据的信号。 这正是下一课的主题。

预告 · 第 13 课

训练 loss 一直掉,为什么不是好消息

这一课你学会了把模型训起来:batch 怎么切、步子怎么迈、 学习率怎么变、怎么别炸。 但「训练 loss 掉到很低」和「模型真的学会了」是两件事。 下一课就去看那个所有训练曲线里最该盯的数字 ——训练 loss 和验证 loss 中间那道缝。 这道缝太大叫过拟合,太小说明你还欠着 —— 而刚才那个「不要让它长得太大」的直觉,会在那里变成一整站的主角。

第 5 站

把这一课攒成一张配置单

真实的训练脚本开头,都有一坨这样的常量。 现在你已经知道每一个的来历了 —— 我们把它整理成一张单子, 顺便把「这些东西一共要花多少代价」算清楚。

配置项
这一课的值
它是干什么的 / 谁给它定的
batch_size
32
显存能装多少 × 一步想算多准 —— 第 1 站
steps
8,000
一共走几个 batch —— 第 9 课那份记录就跑到这里
optimizer
AdamW
Adam + 权重衰减 —— 第 2 站和第 4 站的合体
peak_lr
3e-4
轨迹最高点 —— 第 3 站,Adam 的量级(不是 SGD 的)
warmup_steps
800
从 0 升到峰值用多少步 —— 第 3 站
min_lr_ratio
0.1
末尾降到峰值的几分之一 —— 第 3 站
weight_decay
0.01
每步把参数往 0 拽一点 —— 第 4 站
max_norm
1.0
梯度长度的上限 —— 第 4 站,防那一脚离谱的

最后算一笔这一课反复提到、但一直没算清的账:优化器自己要占多少地方?模型有 798,720 个参数(第 8 课那本账)。 一个 float32 占 4 字节,那么光参数本身:

798,720 × 4 字节 = 3,194,880 字节 = 3.05 MiB

而 Adam 要多背两份同尺寸的账(一阶距 m、二阶距 s):

798,720 × 4 字节 × 3 份 = 9,584,640 字节 = 9.14 MiB

9.14 MiB 听着不多 —— 因为我们的模型本来就小。 换成 7B(78.2 GiB 光优化器状态), 这笔账就会变成决定「你买得起什么显卡」的东西。 第 16 课要把这笔账算到底。

动手实验优化器状态显存账:同一个模型,换个优化器就多背几份
优化器AdamW3 份,多背一阶距 m + 二阶距 s
参数量7.00 B(十亿)
规模快选
AdamW · 7.00 B(十亿) 个参数每份 26.08 GiB × 3
78.23 GiB
同一根刻度:满格 = 当前规模下 AdamW 全量 78.23 GiB;红线 = 16 GB 显卡(16.00 GiB,落在 20.5% 的位置
参数本身(权重) · 26.08 GiB一阶距 m · 26.08 GiB二阶距 s · 26.08 GiB16 GB 显卡参考线
每个参数身上挂着的东西字节为什么
参数本身(权重)28,000,000,000模型自己,一个 float32 占 4 字节
一阶距 m28,000,000,000梯度自己的滑动平均 —— 分子管方向
二阶距 s28,000,000,000梯度平方的滑动平均 —— 分母管尺度
合计84,000,000,000AdamW:每个参数 3 × 4 = 12 个字节
AdamW7.00 B(十亿) 个参数上要占:7,000,000,000 × 4 字节 × 3 份 = 84,000,000,000 字节 = 78.23 GiB,是 SGD 的 3
16 GB 的卡装不下 —— 超出 62.23 GiB, 只优化器状态这一项就占到了显存的 489%。 而且这还没算梯度、激活值和前向时的中间结果。
对照一下:本课这个小模型参数本身只有 3.05 MiB,AdamW 全量 9.14 MiB —— 小到看不出疼;而 7B 光优化器状态就是 78.2 GiB。同一笔账,规模一换就从「无所谓」变成「买不买得起卡」。
👆 先点优化器那三个按钮:SGD 只有蓝的一段,Momentum 多出琥珀色的「速度 v」, AdamW 再多出绿色的「二阶距 s」—— 条形跟着变长,读数里的「是 SGD 的几倍」也跟着变。 再拖「参数量」滑块:拖到 7B,红线(16 GB 显卡)会被压在左边,条形直接越过去变红。 注意换优化器改的是份数(最多乘 3),拖滑块改的是每份多大(从本课小模型一路拖到 70B,差了 87,640 倍)—— 两个旋钮,一个乘 3,一个乘几万。
先点「SGD / Momentum / AdamW」三个按钮:条形里会多出「速度 v」、 「一阶距 m / 二阶距 s」两块,总宽度和读数跟着一起变。 再拖「参数量」滑块(从本课小模型一路拖到 70B): 红线是那张 16 GB 显卡的位置 —— 拖到 7B 时条形直接越过去变红。
图 12-5 · 本课小模型 798,720 个参数:参数本身 3.05 MiB,AdamW 三份合计 9.14 MiB; 同一个 AdamW 换到 7B 就是 78.2 GiB

还有一件更隐蔽的代价:训练比推理慢得多,也吃显存得多。 第 11 课讲过原因 —— 反向传播要乘的那些数,是前向时存下来的中间结果。 一步训练的代价大约是 3 次前向(1 次前向 + 约 2 次前向的反向)。这也是为什么 第 9 课那种「生成一个字」的推理可以随手跑,而这一课的循环要跑几十分钟。

本课核心 · TAKEAWAY

优化器不是三选一,是三件事的三个答案

SGD 回答「往哪走」;Momentum 回答「平方向上怎么走快一点」; Adam 回答「每个参数怎么自己管自己的步子」。 学习率曲线的预热与衰减回答「什么时候谨慎、什么时候收敛」; 裁剪和权重衰减回答「怎么别翻车」。你调的不是一组超参,是一整套「步子该怎么迈」的策略。这一课留下最实用的那条直觉,是这个:学习率的意义永远要跟「谁在用」绑在一起说 ——3e-4 对 Adam 是正常的,对 SGD 是胡来。

这一课你亲手做完了

  • 走完了训练循环:五行代码点开看真数字, 第 1 步 w 从 9 变成 7.800000, 24 步后 loss 掉到 0.000803
  • 切过 batch:语料 4,000 条, batch=32 时一个 epoch 125 步、 batch=5128 步, 同样 8,000 步, 样本被嚼的次数差 16 倍。
  • 看着三个优化器跑完 60: 同一座山谷、同一个起点、同一个学习率。 SGD 的 w2 走 1 → −0.8 → 0.64 → −0.512(在陡壁上弹), w1 每步只乘 0.910000(在平方向上磨)。
  • 扫过一整排学习率0.01 / 0.02 / 0.05 / 0.09 / 0.15 / 0.2 / 0.5 / 2 —— 每一档换一个冠军,SGD 窗口最窄,Adam 一路活到 2
  • 量过两个限速器:整批梯度 (9,12) 长度 15.000000, 阈值 1 时缩成 (0.600000, 0.800000) 长度 1.000000(方向不变); wd = 0.018,000 步, 参数被拽走 2.37%。

学习小测验

已完成 0 / 60.0%答对 0
还没提交过 —— 每题先选一个选项,再点「提交」,答完就能看到诊断。
Q1训练循环里的 optimizer.step() 到底在做什么?
Q2山谷 L = ½·(1·w1² + 20·w2²) 在两个方向上陡了 20 倍。同一个学习率同时伺候两边,会出什么事?
Q3把学习率从 0.02 一路调到 2,谁活了下来?
Q4为什么要给学习率加一段「预热」?
Q5这一批的梯度加起来长度是 15.000000,你把 max_norm 设成 3。训练会怎么样?
Q6Adam 优化器要额外背多少账?模型有 798,720 个参数。
NEXT · 第 13 课

泛化与过拟合:训练 loss 掉得越低越好吗

这一课把机器装好了,可以真的开训了。 但开训之后你要盯的第一个数字不是训练 loss —— 而是它和验证 loss 之间那道缝。 下一课我们造一批模型从没见过的题, 看它在新题上是真的会算,还是只是把 4,000 条语料背了下来。 你会发现我们那个 798,720 参数的模型 和 4,000 条语料之间, 存在一个很危险的数字比例 —— 下一课就去算它。

从零手册 —— 下一课:泛化与过拟合:训练 loss 掉得越低越好吗