G. 常见错误与排查
模型没有学好时,最容易做的事是再调一次学习率。可如果目标和预测根本没有对齐,学习率调得再认真,也是在优化另一个问题。
排错可以从一个朴素的问题开始:程序实际算的,是不是我们以为它在算的东西? 先找一个小到能够手算的输入,再逐步扩大,往往比同时更换网络、数据和优化器更快。
1. 先缩小问题,不急着改变方法
保留出现问题的命令、输入、环境和完整报错。从 traceback 最后几行找到异常类型,再向上找到自己的第一处调用。错误出现在库内部,并不等于库本身出了问题,也可能是输入已经不符合约定。
然后尽量把问题缩成一条样本、一批数据或一次前向。把以下信息直接打印出来:
print(inputs.shape, inputs.dtype, inputs.device)
print(targets.shape, targets.dtype, targets.device)
print(torch.isfinite(inputs).all().item())
如果只是文件找不到,先打印 Path.cwd() 和解析后的文件路径。相对路径跟着当前工作目录走,不一定跟着脚本所在目录走;本书常用 Path(__file__).resolve().parent 定位随脚本分发的资源。
2. 程序没报错,也可能减错了答案
三个预测是 1、2、3,三个目标也正好是 1、2、3,正确 MSE 应为零。可是把预测排成 (3,1)、目标留成 (3,),相减会广播成 (3,3),得到的 MSE 约为 1.333333。
这个错误没有触发异常,因为广播规则允许它。修复前先确认哪条轴表示样本,再统一形状;不要盲目使用 squeeze(),把批量恰好为 1 时需要保留的轴也挤掉。
对于词元预测,还要检查输入位置与目标位置是否错开了一格、PAD 是否从损失中排除、掩码是否遮住未来。损失能下降,只说明某个目标被优化,不证明这个目标就是原来的任务。
完整广播图见附录 A。这类检查可以在训练开始前,用小数组完成。
3. 梯度不见了:沿着计算图找断点
item() 会把单元素张量取成普通 Python 数字。如果把它用于后续损失,再用 torch.tensor(...) 包回来,新张量不会自动记得原来的计算图。
例如,设 ,希望对 求导,结果应为 4。若先取 x.item() 再平方、重建张量,反传会失败;直接计算 x ** 2 则能保留依赖关系。
detach()、转换到 NumPy、用不参与求导的新张量替换中间结果,也可能在你需要梯度的地方断开路径。它们本身并不是错误:记录日志、固定教师输出等场景本来就可能需要停止求导。关键在于放置的位置。
梯度为 None、梯度恰好为零、参数没有更新,也不是同一件事。先检查参数是否参与当前损失、是否需要梯度、是否交给了优化器,然后再检查数值。还要注意:backward() 默认累加梯度;eval() 不会自动关闭求导。PyTorch 自动求导文档可以帮助确认这些行为。
4. 出现 NaN:找到第一个不再有限的数
直接计算 会超过常见浮点表示范围。若随后出现无穷除以无穷,结果就可能是 NaN。把 softmax 先减去最大分数,可以在这个例子里避免指数溢出,数学推导见附录 B。
但不是所有 NaN 都来自 softmax。还可能有除零、负数开方、无效标签、梯度爆炸,或低精度计算范围不足。沿前向与反向逐层检查有限性,找到最早出现问题的位置。
不要把最后的 NaN 一律替换为零就当作修复。这样也许能让程序继续运行,却可能隐藏了错误的目标或梯度。先恢复一个稳定的小输入,再检查学习率、归一化、损失实现与精度选择。
5. 成绩好得异常:也要怀疑数据
训练和测试图片文件名不同,不表示它们来自不同的人、不同原始文档或不同视频。重复的上游对象可能让测试变得过于容易。
本页用一个极小例子检查分组:训练包含 writer-a、writer-b,测试包含 writer-a、writer-c。两边共享 writer-a,就不能把这个划分当作完全未见书写者的评价。
发现交叉后,应回到目标决定划分单位,再重新建立开发流程。如果已经看过测试结果并按它改变了方案,要保留这段历史;不能通过换个目录名,把原来的测试数据重新变成“从未见过”。
还有一种常见泄漏是先在全部数据上拟合标准化、特征选择或词表,再划分数据。应先明确数据职责,让会学习统计量的步骤只使用被允许的部分。更完整的例子见第八章和第九章。
6. 运行四个真实的小故障
排错脚本使用现有 NumPy、PyTorch 环境。它在内存中实际触发误广播、断图反传、指数溢出和分组重叠,不读取外部业务数据。
python src/examples/debugging_lab.py
实际输出:
广播误差形状:(3, 3),错误 MSE=1.333333,正确 MSE=0.000000
item() 后重建张量的反传失败:True;保留计算图后梯度=4.0
直接指数概率全部有限:False;稳定概率和=1.000000
跨划分重复的书写者: ['writer-a']
修复范围:数值路径与分组检查;不等于模型已能泛化。
程序只检查出分组交叉,没有擅自生成一份新的真实数据划分。前三项展示了具体数值路径的修正,也不意味着整个训练系统已经可靠。
展开完整代码
"""附录 G:实际触发并定位四种常见错误,全部在内存中进行。"""
import numpy as np
import torch
def experiment():
prediction = np.array([[1.], [2.], [3.]])
target = np.array([1., 2., 3.])
wrong_loss = ((prediction - target) ** 2).mean()
right_loss = ((prediction[:, 0] - target) ** 2).mean()
x = torch.tensor(2., requires_grad=True)
detached = torch.tensor(x.item() ** 2)
detached_error = False
try:
detached.backward()
except RuntimeError:
detached_error = True
(x ** 2).backward()
right_gradient = x.grad.item()
values = np.array([1000., 1001.])
with np.errstate(over="ignore", invalid="ignore"):
raw = np.exp(values) / np.exp(values).sum()
shifted = np.exp(values - values.max())
stable = shifted / shifted.sum()
train_groups, test_groups = ["writer-a", "writer-b"], ["writer-a", "writer-c"]
overlap = sorted(set(train_groups) & set(test_groups))
return dict(wrong_shape=(prediction - target).shape, wrong_loss=float(wrong_loss),
right_loss=float(right_loss), detached_error=detached_error,
right_gradient=right_gradient, raw_finite=bool(np.isfinite(raw).all()),
stable=stable, overlapping_groups=overlap)
def main():
r = experiment()
print(f'广播误差形状:{r["wrong_shape"]},错误 MSE={r["wrong_loss"]:.6f},正确 MSE={r["right_loss"]:.6f}')
print(f'item() 后重建张量的反传失败:{r["detached_error"]};保留计算图后梯度={r["right_gradient"]:.1f}')
print(f'直接指数概率全部有限:{r["raw_finite"]};稳定概率和={r["stable"].sum():.6f}')
print("跨划分重复的书写者:", r["overlapping_groups"])
print("修复范围:数值路径与分组检查;不等于模型已能泛化。")
if __name__ == "__main__":
main()
7. 按现象找到下一步
| 现象 | 先看什么 |
|---|---|
| 明明安装了,仍提示找不到包 | 当前解释器路径与 python -m pip 是否一致 |
| 矩阵乘法形状不匹配 | 写出每条轴的含义,再检查内侧维度 |
| 训练损失完全不动 | 梯度、优化器参数、学习率与真实目标是否正确 |
| 小批量也无法拟合 | 先减少到几个已知标签,检查实现与数据对应 |
| 训练很好,验证很差 | 数据差异、过拟合、预处理和标签约定 |
| 结果异常完美 | 重复样本、分组、未来信息和目标泄漏 |
| 显存不断增长 | 是否把带计算图的张量长期存入列表;是否存在未释放引用 |
| 生成文本只有输入、一直重复或突然结束 | 输出切片、聊天模板、EOS、采样与长度上限 |
| 重跑无法一致 | 数据/代码版本、随机数来源、设备与非确定性算子 |
| 速度测量忽快忽慢 | 预热、同步、并发、计时范围与系统负载 |
这一列是调查入口,不是对所有情况通用的处方。比如减小批量可能降低显存,却不能解决不必要的计算图引用;设置随机种子也不能修复不同版本的预处理。
最后,把修复留下来:原始症状、最小输入、原因、改动、修复前后结果。若错误涉及核心计算或数据隔离,可以再留一个针对这个失败条件的小检查。这样,下次改动才不会悄悄把同一个问题带回来。
自测
- 为什么形状合法仍可能算错损失?
- 为日志调用
loss.item()与用这个数字继续构造训练损失,有什么区别? - 为什么在输出端屏蔽 NaN,不一定能修复梯度?
- 模型能把四条样本拟合到很低损失,是否证明它能泛化?
- 为自己遇到的一次错误写出“最小输入—预期结果—实际结果”,暂时不要先写修复方案。
参考答案
- 程序只检查运算规则,不知道样本应该怎样配对;广播就是典型例子。
- 取数用于显示可以是合理的;若训练依赖这个普通数字,原来的求导路径就丢失了。
- 无效运算可能早已进入计算图,反向仍会经过它。应从运算本身和最早异常处排查。
- 不能。它提供了局部实现检查,泛化需要合适的保留数据和评价设计。
- 例如:三个相同预测与目标,预期 MSE 为零,实际为 1.333333。这样的描述可以直接指导下一次检查。