阶段 1 · 最小学习机器

训练调试:损失不降的时候
到底该看哪里

上一章《初始化与训练稳定性》管「一开就炸」。这一章讲的是它不工作时怎么办—— 一张从症状反查病因的可点诊断树、一次亲手把训练调坏的实验,加上五个最高频 bug 的检查清单。

1

这活儿本来占 80% 的工作量

大致的时间分配(Full Stack Deep Learning 第 7 讲;Karpathy——特斯拉前 AI 负责人、OpenAI 创始成员之一——确认过)

从业者 80~90% 的时间花在调试和调参上,只有 10~20% 花在推导数学和实现上。 下面每一节,都是那 80% 里的常规动作。

💡 那 80% 分成两半,这一章是前半

这一章只回答一个问题:它坏了,怎么修。症状 → 病因,从有 bug 的地方往回查。

如果你的损失在正常下降、只是还没到最好——那不是 bug,是另一半问题, 归后半章管:《超参怎么选》。 两章的交界处只有一条铁律:先在小数据上过拟合一个 batch。如果连它都过拟合不了,那就不必去翻后半章了。

2

从症状点到病因

这是这一章最有用的东西。选一个最像你的症状,一层层点下去。 能画出真实曲线的那几片叶子,点开后会直接把那条曲线跑给你看—— 不是示意图,是现场训练出来的损失曲线(横轴轮数、纵轴对数损失)。

互动 · 诊断树(每一个叶子都能点开)

🎯 类比

这张树像医院的分诊台:先问「哪儿疼」,再决定去哪个科。 差别在于——模型不会说话,它只会给你一条损失曲线。 所以这一章真正教的不是「照着点」,而是「把曲线的形状翻译成症状」。

3

先做这两件事,能解决一大半问题

动手前,先照下面那张默认配置表把架子搭好。之后这一章的顺序就三步: ① 在小批上过拟合(查 bug)→ ② 扫一遍学习率 → ③ 盯监控表开正式训练。 这一节先讲前两步,第 ③ 步在第 7 节。

① 先在小数据上过拟合 取 几十个样本(本页演示用 8 个点),关掉所有正则化,跑几百轮。 如果连这几个样本都拟合不到接近 0 的损失,那是代码有 bug,不是调参问题。
② 再开一个学习率扫描 让学习率从极小指数增长到大,记录每一步的损失。 曲线下降最陡的地方再除以 3~10,就是你的起点。
为什么一定要先过拟合一个小 batch?直接训完整数据不行吗?

因为这两件事排查的是不同的东西。完整数据训不动,可能是 bug 也可能只是没调好; 但几十个样本训不动,只可能是 bug——模型连死记硬背都做不到,说明前向或反向有错。

💡 为什么「小数据过拟合」是第一条铁律

一个正常的网络应该有记住几十个样本的能力。做不到,说明梯度根本没传对、标签接错了、 或者损失函数用错了。这些都是确定性的 bug——调学习率调到天荒地老也修不好。

第 5 节把这个动作做成了一个可以亲手调坏的实验。先把那张表背下来,再去拖那个滑块。

如果你现在什么都还没搭起来:从这套默认配置开始

新手最需要的一栏是「我该用什么起步」。下面这套配置的选取标准只有一条: 让「出问题时能立刻判断是 bug 还是超参数」这件事变得更容易。

项起点为什么是这个
优化器AdamW自适应步长,对学习率不敏感——出问题时少一个嫌疑人。学习率调通之后再换 SGD 也不迟
学习率3e-4 圈内叫「魔法学习率」:绝大多数模型在这个值上既不会立刻炸、也不会一动不动,正好用来判断「代码对不对」
学习率调度先不用(常数) 调试阶段不要引入第二个会随时间变的量。等基线跑通了再上 cosine(让学习率按余弦曲线慢慢变小)
正则化先全部关掉 weight decay 设 0、dropout 设 0。第一步的目标是「能不能过拟合」,正则化只会挡路
激活 + 初始化ReLU + He
tanh + Glorot
这两个必须配套。ReLU 配 Glorot 会让激活值逐层缩小,十几层后衰减到 1e-2 左右
归一化有残差就用 LayerNorm(Pre-LN) 残差 = 把输入直接绕到后面、和这一层的输出相加;Pre-LN = 归一化放在子层前面。深网络的稳定底座,先有它,再谈别的
batch size32 ~ 256 能塞进显存就行。一开始不用追大批量——大批量还要连带调学习率和 warmup(先小步、再逐步加大步长)
必须一开始就有的固定随机种子 + 记录超参数 没有它,第 8 节讲的「不可复现」就无从查起:你连「改了什么导致变化」都不知道
先别加的复杂增强、多任务损失、混合精度(用 16 位浮点数算,省显存) 每加一样,都让「这到底是 bug 还是设置问题」更难判断。一次只加一个
4

学习率扫描:先扫最大的旋钮

学习率是超参数里最该先扫的那一个。而它有个很好的性质:扫一遍就知道了。

互动 · 拖竖线,看这个学习率会带来什么

学习率区间损失曲线你该怎么做
1e-6 ~ 1e-5几乎是一条水平线,缓慢下降太小了。训练到明年也跑不完
1e-4 ~ 1e-2稳定、明显地往下走就是这里。取曲线最陡处再除以 3~10
1e-1剧烈震荡,上下横跳略大了。配 warmup 还能救(哪个值算大要看网络)
> 1一路上冲,越跑越大炸了。降两个数量级重来

同一个学习率,在不同网络上的后果可以完全不同:表里的「大」和「小」取决于那个网络的曲率。 第 5 节那个 8 点小网络,用默认的 1e-1 就能顺利过拟合——它曲率小,同一档步子在那里不会震荡。

5

过拟合一个 batch 试试

下面是一个真的小网络(2 → 16 → 1,tanh 隐层、线性输出、平方误差损失) 和一批真的 8 个点(两个类别,标签 ±1)。 改一下互动里的控件,它就会现场跑一遍梯度下降——前向、反向、更新,全是这个页面里的代码在算。

示意图 · 下面要过拟合的到底是什么

你的任务不是「看它成功」,而是把它调坏:把学习率拖到两端,或者把梯度方向写反。

互动 · 一个 batch 能过拟合吗

这一步失败的四种信号——全章信息密度最高的表

这张表是这一节存在的意义:过拟合失败时,曲线的形状直接告诉你病因。 上面那三个控件能把你送进其中三种形状(往上走 / 爆炸 / 不降);第四种「震荡」去第 4 节那张扫描图里看。上面那条损失曲线就是现场训练出来的。

现象通常的病因怎么确认
误差往上走损失函数或梯度里符号反了(更新方向写反,等于在爬山) 把学习率调到很小,如果损失仍然单调上升——基本可以确定是符号或目标函数接错
误差爆炸数值问题,或学习率太大 看爆炸前几轮的形状:量级几轮内就越过 1e10、然后溢出成 NaN = 学习率太大;先平稳、再突然冲上去,更像数值问题
误差震荡学习率偏大;也可能标签没洗乱、增强写错 学习率降 10 倍再看一次。还震荡,去查数据管线
压根不降学习率太小、正则化太强;也可能损失函数与数据管线接错 去掉所有正则、学习率 ×10 再看一次
⚠️ 为什么这一步能筛掉「一半以上的错误认知」

很多人遇到训练不收敛,第一反应是「我的模型不够好」。而这一步用的是一个必须能记住 8 个点的网络—— 如果它记不住,问题一定在实现里,不在想法里。这一步把「模型问题」和「代码问题」彻底分开了。

6

五个最常见的 bug

这五条来自 FSDL 第 7 讲的统计,也是复现别人代码时踩得最多的五个坑。 它们的共同点是:都不报错。代码跑得通、损失在降、结果就是比别人差。

Bug隐蔽在哪里你会看到怎么在 5 分钟内验
1 · 张量形状错 框架的静默广播:形状不匹配时它不报错,而是「帮你」扩成一个大得多的张量 代码跑得通、损失也在降,但结果完全不对 print(x.shape) 打在每一步。形状是唯一能在 5 秒内确认的东西
2 · 预处理错 归一化用了全量数据(含验证集);train / test 用了不同的预处理;通道顺序 RGB ↔ BGR 验证分数好得可疑,上线立刻掉 把预处理参数(均值、方差)打印出来;只用训练集的统计量处理验证集
3 · 损失函数接错 最经典的一个:把 softmax 的输出喂给了期望 logits 的损失函数——等于做了两次 softmax 损失在降,但卡在一个明显偏高的水平,准确率还行——所以很难发现 看模型输出的取值范围:概率(0~1)还是 logits(可正可负、无上界)
4 · 忘了切 train / eval 模式 Dropout 和 BatchNorm 在训练与推理时行为不同。忘了 model.eval() 同一张图推理两次结果不一样;或者验证结果比训练差一大截 同一个输入连跑两次,比输出。不一样 = 模式没切对
5 · 数值不稳定 fp16(本节末尾 Bug 5 细讲)下溢——数太小,小到浮点数表示不了,就被当成 0;还有除零、log(0)。它们不抛异常,只产出 inf / NaN 训练到某一轮突然全变 NaN;或者很小的数莫名其妙变成 0 把 dtype(数据类型)换成 bf16 或 fp32 再跑一遍;给 log / 除法加 eps

Bug 1 · 它居然没报错,但结果完全不对

下面这个加法是真的按 NumPy 的广播规则算出来的。左边是一个(4, 1), 右边是一个(1, 4)——你想要的可能是逐元素相加,但框架会给你一个(4, 4)。

互动 · 静默广播:形状不匹配时的真实结果

💡 怎么防

在每个函数的开头写形状断言:assert x.shape == (N, 1)。 PyTorch 里还可以 torch.set_printoptions(precision=3) 配合 print 检查数字量级——广播错误的数字往往大得离谱或完全不符合量纲。 最省事的一招:在第一个 batch 上用 (1, 1) 这种极小形状跑一遍全流程,形状对不对一眼就能看出来。

Bug 2 · 归一化用了全量数据:验证分数会变好看

下面这个演示是现场训练的:一个线性分类器,训练集是一个分布,验证(测试)集是另一个偏移过的分布 (模拟「线上和线下不一样」)。 同一份数据、同一个模型,只改一件事——归一化用的 μ/σ 是从哪批数据上算的。

互动 · 换一个偏移量,真数字跟着变

Bug 3 · 两次 softmax:损失卡在高处,准确率却还行

左边是正确做法(损失吃 logits),右边是把 softmax 的输出又喂给了期望 logits 的损失函数。 两条曲线都是真的训练 180 个样本跑出来的——它们会清楚地分开。

互动 · 拖一下正确类别的 logit,看两次 softmax 把概率压成什么样

正确:损失吃 logits 错误:又过了一次 softmax

Bug 4 · 训练模式和推理模式:同一个输入,两个答案

下面这个演示是真的按公式算出来的(dropout 真的在丢)。 把「模式」按钮在训练 / 推理之间切一下,看同一个输入会发生什么。 这就是忘了 model.eval() 的后果:同一个输入,六次前向给出六个答案。 这里的演示只覆盖 Dropout 这一半——BatchNorm 也分训练 / 推理两套行为,但它的差别要用一个完整的 batch 才看得出来。

互动 · 同一个输入,六次前向:训练每次不同,推理每次一样

Bug 5 · 数值不稳定:fp16 下 1e-9 就是 0,bf16 不是

先认识浮点数:一个数 = 有效数字(尾数)× 2 的多少次方(指数)。位数是固定的, 尾数管「数字有多准」,指数管「能表示多大、多小」。 fp16 的尾数有 10 位、指数只有 5 位;bf16 反过来——尾数 7 位,指数 8 位,和 fp32 一样宽。

示意图 · 三种浮点数把位数花在哪

fp32
符指数 8 位尾数 23 位(有效数字)
fp16
符指数 5 位尾数 10 位
bf16
符指数 8 位尾数 7 位

下面这条曲线是真算出来的相对误差:把一批数按 fp16 和 bf16 各自舍入一次,再量误差。 左边那一段竖直上冲就是 fp16 的悬崖——比它小的数直接归零。

互动 · 真的把数字舍入成 fp16 / bf16,再量误差

fp16(相对误差) bf16(相对误差)

⚠️ 为什么 fp16 会静默地把梯度抹平

梯度经常是 1e-8 ~ 1e-10 这个量级。fp16 能表示的最小正数是 5.96e-8; 浮点数四舍五入到最近能表示的值,比它一半还小的数离 0 更近,就一律舍成 0。 而 bf16 的指数范围和 fp32 一样,能表示到 1e-38。 所以「fp16 训练炸了」经常不是炸,是小梯度被抹成了 0——损失不动,但也不报错。

现代做法有两条:① 损失缩放(loss scaling)——先把损失乘 1024,梯度一起放大,更新前再除回去; ② 直接用 bf16。今天的主流选择是后者,因为它的动态范围不用额外操心。

7

该监控的量:六个就够

不要只盯着一个 loss 看。这六个量能让你在出问题的当下就定位,而不是等训练完才发现白跑。

指标怎么算正常范围(经验值)不正常说明
训练损失每批损失按 epoch 平均平稳下降 不降(代码 bug / 学习率)、震荡(学习率大)、阶跃(数据有问题)
验证损失同一个损失函数,在验证集上算先降后升 持续上升 = 过拟合;和训练损失差距巨大 = 数据分布不一致
梯度范数所有参数的梯度平方和,再开根号1e-2 ~ 1e1 突然出现几千 = 出现坏 batch;持续为 0 = 神经元全死了或梯度断了
各层激活标准差每层输出在 batch 维上的标准差0.1 ~ 10 之间稳定 逐层衰减到 e-8 = 梯度消失(见上一章);逐层增长到 e5 = 初始化/学习率问题
参数更新比例这一步改动的模 ÷ 参数本身的模(|Δθ| / |θ|)1e-3 ~ 1e-2 远小于 1e-3 = 学习率太小或梯度消失
各层权重范数每层权重平方和,再开根号缓慢增长后平稳 持续爆炸式增长 = 权重衰减太弱或学习率太大

互动 · 激活值逐层缩小还是稳住:初始化和激活配不配套

💡 这六个量就是监控表

它不替代诊断树,而是给诊断树提供读数:出问题的当下就能翻,不用等训练完才发现白跑。

8

三个故障:NaN、显存、复现

① NaN 定位到具体层:二分法,三次就够

假设有一个 8 层的网络,某一层的权重在某个坏 batch 之后变成了 NaN。 从最后一层往回查是最笨的办法——因为 NaN 会一路污染到输出。正确做法是二分: 把网络从中间切开,看中间那一层的输出是不是有限。8 层对半切三次就够:8 → 4 → 2 → 1。

下面的演示真的每次前向跑一遍(每层都是真的乘加运算)。点「测一次」它就二分一次。

互动 · 二分定位:第几次能找到坏掉的层

② 显存账本:训练一个参数量为 N 的模型要多少显存

训练时每个参数要付的账是:权重 2 字节 + 梯度 2 字节 + fp32 主副本 4 字节 + Adam 的一阶矩 4 字节 + 二阶矩 4 字节 = 16 字节。 混合精度训练里,权重和梯度用 2 字节(fp16 / bf16)存;另外留一份 4 字节的全精度权重当主副本——更新参数时用它, 免得低精度的舍入误差攒起来。另一大块是激活值——它和 batch、序列长度成正比。

互动 · 拖参数量和 batch,看显存怎么涨

💡 OOM(显存不够、程序被杀)时的排查顺序:从最便宜到最贵

① batch size 减半 + 梯度累积(一次只算一半样本、攒两次再更新——等效批量不变,免费); ② 混合精度 BF16(用 16 位浮点数算,激活和梯度直接砍半); ③ 梯度检查点(前向时不存中间结果、反向时重算一遍:激活显存从「随层数线性增长」降到「随层数的平方根增长」,代价是多算一次前向); ④ 优化器换 8-bit Adam(把优化器状态从 32 位压到 8 位;它占大头,这一项能省好几倍); ⑤ ZeRO / FSDP 切到多张卡(把参数、梯度、优化器状态切开,分摊到几张 GPU 上)。

还有一个最容易被忽略的:忘了 zero_grad()。 梯度会一直累加,数值会失控——这也是「训练到一半突然 NaN」的常见原因之一。

③ 不可复现:同一个代码、同一个数据,两次结果不同

下面是真的跑了 5 个随机种子、每个训练 30 轮的结果。 同一个 batch、同一个学习率、只有初始化不同——损失差距可以有好几倍。 这不是 bug,但你必须能控制它。

互动 · 五个种子的真实散布

互动 · 五条真实训练曲线:只有初始化不同

要固定的东西怎么固定不固定会怎样
随机种子random / numpy / torch / cuda 各自的种子都要设初始化不同 → 结果差好几个点
数据顺序DataLoader 的 shuffle 用独立的生成器并固定种子每个 epoch 的顺序都不同,曲线不可比
非确定性算子开启确定性模式(会给一点速度损失)同样的输入,GPU 的累加顺序不同 → 末位不同
一次实验的记录把超参数、代码版本、数据版本写进日志你连「到底改了什么」都不知道,这比不可复现更严重

⚠️ 推翻一个直觉 即使全部固定,不同随机种子带来的指标波动通常也有 1~2 个点(小实验里更大)。 所以单次实验的微小提升没有意义——看到「涨了 0.3 个点」就改方案, 你很可能只是在追噪声。要跑多个种子取平均,或者只采信差距很大的改动。

M

算笔账 · 一条曲线就是诊断报告

第 2 节那张诊断树,教你把曲线的形状翻译成症状。它之所以成立, 是因为所有训练曲线都是同一个动作跑出来的:每走一步,参数朝下坡方向挪一小段。 把这句话写成大白话算术:下一步的误差 ≈ 这一步的误差 × |1 − 步长 × 曲率|。 这个乘数小于 1,误差就一路缩小;大于 1,误差就越来越大、飞出去。 「不降 / 震荡 / 爆炸」不是三个问题,是同一个乘数的三种取值。

互动 · 一个坑、一个小球:一步迈多大

θ—小球现在在哪 · 起点 ∇L—脚下的坡度 · 从数据算 γ—步子多大 · 上面滑块

微型图解 · 同样是这条式子,四种步子画出四种曲线

💡 为什么这一条式子能解释整章

第 4 节的学习率扫描、第 5 节「亲手把它调坏」、第 2 节诊断树里 「上升 / 爆炸 / 震荡 / 不降」四个分支,全都是同一个乘数的四种取值: 步子太小 → 不降;步子合适 → 平稳下降;步子偏大 → 震荡;步子过大 → 爆炸。 你不是有四个问题,你只有一个问题:步子选得对不对。

但如果你扫的每个步子都炸了——那问题就不在步子

Google 那本 《Deep Learning Tuning Playbook》给这类配置起了个名字,叫 不可行点(infeasible):发散、损失爆炸、或者根本跑不起来的那些配置。 它要求你在读每一次实验的结果之前,先数一件事:这类点占了多大比例?

下面这个演示真的对每一个学习率跑 60 步(就是上面那同一条式子), 然后数一数有几个真飞出去了。拖滑块改变这个问题的病态程度—— 看「飞掉的比例」怎么跟着变。

互动 · 扫一遍 12 个学习率,数一数有几个飞了

⚠️ 飞掉的比例太高,说明要改的不是学习率

论文的原话是「当一次实验里不可行点占了很大比例时,该做的是调整搜索空间的定义—— 而在某些情况下,大量不可行点本身就意味着训练代码里有 bug」。

直觉是这样的:一个健康的问题,大部分学习率都应该能跑——只是快慢不同。 所以如果你扫 10 个学习率、有 8 个都炸了,那不是「学习率选得不好」, 那是代码或数据有问题:输入忘了归一化?初始化尺度太大?损失函数少了个 log? 学习率乘到了不该乘的地方?

(图里那条 1/3 的线是我加的经验参考线, 论文只说「很大比例」,没给具体数字。它的用处是给你一个可以争的锚点,不是一个标准。)

这就是这一章和《超参怎么选》的分界线。 在那一章里,你假设每个配置都能跑、只是好坏不同; 而在这里,「跑不起来」本身就是症状。见不得光的配置占比一高,就别再往下扫了——回去查代码。

9

这套方法什么时候不管用

上面所有方法都有同一个前提:你有一个可以反复跑的、诚实的实验循环。它不成立的时候:

处境这些方法会怎么骗你更该做的事
一次训练要三天 「过拟合一个 batch」 这类动作你根本跑不起,于是你会跳过它们——而它们恰恰是最有效的 先做一个小到能反复跑的版本(小模型 + 小数据);调试的成本必须便宜
数据集本身是坏的(标签错、重复、测试数据混进训练集——这叫泄露) 模型会绕过坏数据,诊断树上的每一个叶子都「看起来正常」 抽样人工看 100 条训练数据:同一条出现两次、标签和内容对不上、测试集里的样本混了进来。这一步的收益经常超过所有调试动作之和
数据量小到无法留出验证集 任何「验证损失」都在抖,你分不清是过拟合还是噪声 用交叉验证;只看差距很大的改动
线上分布和训练集不同 训练日志一切正常,上线就掉点——训练时那六个监控量看不到这件事 见 《超参怎么选》第 8 节:分布偏移——意思是线上数据和训练数据不是一个分布
你在调试一个「已经调了三周」的模型 你会越来越倾向于相信「就是这样」,而不再做最小的验证实验 换一个干净的环境重跑基线。经验值钱,但经验最容易掩盖 bug

最后是这一章反复说的那个提醒——新手最常浪费时间的,是在影响很小的东西上反复折腾:

柱子的长度代表「这个改动通常能带来多少效果」——这只是主观排序,不是实测。从上面往下改。

⚠️ 反直觉但很重要 优化器、激活函数、层数这些「看起来很核心」的选择,实际上对最终效果的影响远小于数据质量。 一个用 Adam 的小模型在干净数据上,往往打败一个用 Muon(一种新优化器)的大模型在脏数据上——这是圈内经验,不是对照实验。 先把数据搞对,再考虑架构。

10

小结

它对应哪条线 ⑦ 拧得动——这一章整章都建立在一个默认前提上:梯度下降本来能走通。 所以训练跑不动时,绝大多数时候不是「优化做不到」,而是学习率、符号、数据把本来很浅的坑变成了走不动的坑—— 你要做的是去测量,而不是怀疑优化本身
一句话 调试本质上是在把「模型不行」和「代码不行」这两件事分开—— 而分开它们的唯一办法,是构造一个「模型一定做得出来」的任务(过拟合 8 个点), 再逼它失败
它牺牲了什么 牺牲了「一次跑到底」的舒适。这套方法要求你把训练切成许多小实验, 每一步都付出重新配置的代价——换来的是「出问题时你知道该看哪里」(回扣暗线 B)
💡 检验一下:你现在能指着哪个互动说这句话

回到第 2 节那张「从症状点到病因」的图。先随便挑一个症状(比如「训练损失不降」),顺着箭头走到病因,然后照着那一格去做——你会发现它让你先去做的事,往往不是「加正则」或「调学习率」,而是先看一眼数据、先过拟合一个 batch。那个「先排除最简单的可能」的顺序,就是这一章真正在教的东西。

所以这章属于哪条线,为什么不是别的

线为什么这章不是它
① 表达力 vs 泛化调试不改变表达力,它只是确认「代码有没有把该有的表达力用出来」
② 没有免费午餐这里没有任何「假设世界长什么样」的选择
③ 规模会赢不涉及——调试是反规模的:能用一个 batch 说明的问题,就不该用整个数据集
④ 学习即压缩不涉及
⑤ 高维里的低维不涉及——这一章的高维内容(张量的形状与量级)是尺寸,不是论点
⑥ 层层组合不涉及——调试不看层数,看的是哪一步把数值搞坏了
⑦ 拧得动✅ 这一章真正的位置:先默认「梯度下降本来能走通」。 静默广播、双重 softmax、fp16 下溢、学习率写错——它们全都在「优化本身没问题、只是被搞坏了」的前提下发生

它在暗线里站在哪(B 和 E 合成一行)

暗线这一章的回答
A 信息流动 数据形状是本章第一嫌疑人。第 6 节的 bug 1 就是「形状对不上但不报错」; 第 8 节的 NaN 二分定位则是「沿着信息流动方向,找第一个非有限值」。 调试的一个通用动作就是:把形状和量级打在每一层上
B 什么被牺牲了 · E 它假设了什么 为了可诊断性,牺牲了「一次跑到底」的舒适;而整套方法都假设你的实验可控、可重复、可比较—— 种子固定、一次只改一个变量。这个假设不成立时,调试会退化成「凭感觉换参数」; 另一个常被忽视的代价:为了排查方便而关掉正则化 / 混合精度 / 复杂增强时,你测的已经不是最终要用的那个配置
C 参数账本 16 字节/参数(权重 2 + 梯度 2 + fp32 主副本 4 + Adam 一阶矩 4 + 二阶矩 4)是训练的显存底价, 第 8 节的显存计算器就是这笔账。加上激活值,训练显存通常是推理的 4~6 倍
D 跑在什么上 OOM 和 NaN 都是硬件边界反射到算法上的症状。显存爆了不是「模型太大」, 是「16 字节/参数 + 激活」这笔账超了;fp16 下溢不是数学问题,是指数位只有 5 位
F 违背了哪个直觉 「代码跑得通」 ≠ 「算得对」。五个高频 bug 全都发生在代码无异常、损失在降的情况下。 所以判断训练对不对,不能看它跑没跑完,只能看两条曲线和每一层的形状
一句话带走调试方法

损失不降时,按这个顺序走完四步:① 照那套默认配置搭好 → ② 在小批上过拟合(查 bug)→ ③ 扫一遍学习率 → ④ 盯监控表开正式训练。 前两步能解决绝大多数问题,而且都是几分钟就能做完的。

从业者大部分时间花在调试和调参上,而不是在推导上。 所以值得把两张表背下来——「过拟合一个 batch 失败时的四种形状」和「五个最常见的 bug」。 它们覆盖了绝大多数「代码跑得通但结果不对」的情况。

别一上来就换优化器、加层数、调正则——那些是最不重要的旋钮。 上一章治「一开就炸」,这一章治「跑起来却不降」;只是不够好,那是《超参怎么选》。

11

拓展阅读

上面讲的都是「够用」的版本。想往下挖,这里有三个入口—— 它们不是必修内容,是给想再往前走一步的读者准备的。

📄 这一章的说法从哪来

💻 工业界怎么写

∑ 更严格的形式