怎样根据 loss、accuracy 和梯度诊断训练问题

训练 loss 不降?降了又升?训练好测试差?这篇把前面所有训练知识串成一套系统化的诊断流程,让你看曲线就知道问题在哪。

学习路线:PyTorch 深度学习基础(43~58) · 第 58 课
第 58 课第 58 课训练诊断视频封面,展示 loss、accuracy、梯度范数和过拟合欠拟合排查路径。
Learning Path

PyTorch 深度学习基础(43~58)

在前 20 篇 PyTorch 入门上继续深化,补齐张量操作、自动微分、优化器家族和训练排错能力。

学完本阶段你能做到:独立搭建一个 PyTorch 训练脚本,正确处理 Shape、device、梯度清空、优化器选择和学习率策略,并能根据 loss/accuracy 曲线诊断训练问题。

推荐读法:当前更新主线,建议跟更;每篇都要配合代码自检。

查看完整阶段 · 16 篇
Lesson Guide

这一课怎么学

先看目标,再带着问题读正文。读完后用练习确认自己真的理解了。

本节你会学到

  • 说清楚「怎样根据 loss、accuracy 和梯度诊断训练问题」解决的核心问题
  • 知道它在「PyTorch 深度学习基础(43~58)」中的位置
  • 把 过拟合、DataLoader、Linear 层 这些关键词联系起来

前置知识

  • 建议先读完上一篇:正则化、Dropout 与 Early Stopping:防止过拟合的方法地图
  • 能区分输入、输出、数据和模型目标。

概念回顾

  • 【过拟合】过拟合就像背答案,训练题都会了,但真正考试时不一定会。 前面或后面会反复用到它。
  • 【DataLoader】DataLoader 负责把数据一批一批喂给模型,训练循环才不会手忙脚乱。 前面或后面会反复用到它。
  • 【Linear 层】Linear 层做的是矩阵乘法加偏置,是很多神经网络模块的基础零件。 前面或后面会反复用到它。
  • 【Tensor】可以先把 Tensor 理解成支持 GPU、梯度和批量计算的多维数组。 前面或后面会反复用到它。

常见误区

  • 不要只记定义,要追问它解决了什么问题。
  • 不要只看 API 名字,要同时关注输入输出 shape 和训练流程。

课后练习

  • 用 3 句话向一个零基础朋友解释「怎样根据 loss、accuracy 和梯度诊断训练问题」。
  • 打开概念库里的「过拟合」,补一遍它和本文的关系。
  • 回到路线页,选择下一阶段继续补齐。

自检练习与参考答案

1. 如果一段 PyTorch 代码报 device 不一致,第一步应该检查什么?

先检查模型参数、输入 Tensor、标签 Tensor 是否在同一个设备上。常见修正是把它们统一 `.to(device)`。

2. 为什么学习 PyTorch 时不能只看 API 名字?

因为训练是否正确经常取决于 shape、dtype、device 和梯度流,API 名字只能告诉你工具,不会保证数据契约正确。

3. 读完本文后,至少应该能画出哪条训练主线?

`输入数据 → forward → loss → backward → optimizer.step()`,并知道本文主题位于这条链路的哪一环。

这一阶段读完后,建议回到路线页选择下一阶段。

你训练一个模型,loss 降不下去。是学习率的问题?数据的问题?模型结构的问题?还是代码有 bug?

没有诊断方法,就只能瞎试。这一篇把前面 13 篇 PyTorch 训练知识串成一套系统化的诊断流程——看 loss 曲线、accuracy 曲线、梯度分布,就能定位问题。

第 58 课视频 - 怎样根据 loss、accuracy 和梯度诊断训练问题

概念回顾

这是阶段四的最后一篇。前面我们学了:计算图(#45)、Shape(#46-47)、Dataset/DataLoader(#48-49)、nn.Module(#50)、初始化(#51)、优化器家族(#52-54)、学习率(#55)、GPU 训练(#56)、正则化(#57)。今天把这些全部串起来,形成诊断能力。


一句话解释

训练诊断的核心是“看曲线 + 分情况”:loss 不降看学习率和代码,loss 降了准确率不升看 Shape 和损失函数,训练好测试差看过拟合。


诊断流程图

遇到训练问题,按这个顺序排查:

1. loss 完全不动 → 学习率 / 代码 bug
2. loss 震荡或 NaN → 学习率太大 / 梯度爆炸
3. loss 降但 acc 不升 → Shape / 损失函数 / 标签
4. 训练好测试差 → 过拟合
5. 训练测试都差 → 欠拟合

情况 1:loss 完全不动

症状

Epoch 1, Loss: 2.3026
Epoch 2, Loss: 2.3026
Epoch 3, Loss: 2.3026

loss 几乎不变化,像一条水平线。

排查清单

检查项 怎么查
学习率是否太小 print(optimizer.param_groups[0]['lr']),试试大 10 倍
梯度是否为 None print(model.fc1.weight.grad),None 说明 backward 没工作
梯度是否全 0 检查初始化是否全 0(第 51 篇)
zero_grad 位置错 确认在 backward 前调用
数据和模型在同一设备 print(x.device, next(model.parameters()).device)
requires_grad 是否开启 print(param.requires_grad) 应为 True
# 诊断代码
for name, param in model.named_parameters():
    print(f"{name}: requires_grad={param.requires_grad}, grad={param.grad}")

情况 2:loss 震荡或变 NaN

症状

Epoch 1, Loss: 2.3000
Epoch 2, Loss: 1.8000
Epoch 3, Loss: 5.2000    # 突然飙升
Epoch 4, Loss: nan       # 变 NaN

排查清单

原因 解决
学习率太大 降 10 倍
梯度爆炸 加梯度裁剪 clip_grad_norm_
数据有异常值 检查是否有 inf/nan,做标准化
batch_size 太小 适当增大
# 梯度裁剪
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()

# 检查数据
print(torch.isnan(x).any(), torch.isinf(x).any())

情况 3:loss 降但 accuracy 不升

这是最阴险的问题——loss 在降,但准确率不动。说明模型在“学”,但学错了方向。

排查清单

检查项 怎么查
标签 Shape 对不对 多分类要 [B] 不是 [B, num_classes](第 46 篇)
标签类型对不对 CrossEntropyLoss 要 long,不是 float
损失函数选对没 多分类用 CrossEntropyLoss,二分类用 BCEWithLogitsLoss
多加了 Softmax CrossEntropyLoss 内部自带,别手动加
输出维度对不对 最后一层 Linear 的 out_features 要等于 num_classes
# 诊断代码
logits = model(x)
print(f"logits shape: {logits.shape}")     # 应为 [B, num_classes]
print(f"y shape: {y.shape}")                # 应为 [B]
print(f"y dtype: {y.dtype}")                # 应为 torch.long
print(f"y 内容: {y[:10]}")                   # 应为 0~num_classes-1 的整数

情况 4:训练好测试差(过拟合)

症状

训练集: Loss 0.10, Acc 99%
验证集: Loss 0.80, Acc 75%

训练和验证之间有大 gap。

解决方案

上一篇(#57)讲过的正则化方法全套上:

  1. Weight Decay(AdamW 的 weight_decay=0.01)
  2. Dropout(0.3-0.5)
  3. Early Stopping(patience=5)
  4. 数据增强(图像任务)
  5. 减小模型容量

情况 5:训练测试都差(欠拟合)

症状

训练集: Loss 1.80, Acc 45%
验证集: Loss 1.85, Acc 43%

训练集都没学好。

解决方案

方法 作用
增大模型 加宽/加深网络
训练更久 增加 epoch
换更好的优化器 SGD → Adam
降正则化强度 Dropout/Weight Decay 可能太强
检查数据质量 标签是否正确、特征是否有用
学习率调整 太小学不动

梯度健康检查

训练中定期检查梯度,能发现很多隐藏问题。

# 检查梯度分布
for name, param in model.named_parameters():
    if param.grad is not None:
        grad_norm = param.grad.norm().item()
        print(f"{name}: grad_norm={grad_norm:.6f}")
梯度范数 可能问题
0 梯度消失或代码 bug
很小(<1e-7) 梯度消失,深层学不动
正常(0.01-1) 健康
很大(>100) 梯度爆炸,可能 NaN

训练日志模板

好的训练日志能帮你快速定位问题:

for epoch in range(epochs):
    model.train()
    train_loss, train_correct, total = 0, 0, 0

    for x, y in train_loader:
        x, y = x.to(device), y.to(device)
        optimizer.zero_grad()
        logits = model(x)
        loss = criterion(logits, y)
        loss.backward()
        optimizer.step()

        train_loss += loss.item()
        train_correct += (logits.argmax(1) == y).sum().item()
        total += y.size(0)

    # 验证
    model.eval()
    val_loss, val_correct, val_total = 0, 0, 0
    with torch.no_grad():
        for x, y in val_loader:
            x, y = x.to(device), y.to(device)
            logits = model(x)
            val_loss += criterion(logits, y).item()
            val_correct += (logits.argmax(1) == y).sum().item()
            val_total += y.size(0)

    print(f"Epoch {epoch+1}: "
          f"Train Loss={train_loss/total:.4f} Acc={train_correct/total:.4f} | "
          f"Val Loss={val_loss/val_total:.4f} Acc={val_correct/val_total:.4f} | "
          f"LR={optimizer.param_groups[0]['lr']:.6f}")

典型输出:

Epoch 1: Train Loss=1.8523 Acc=0.3520 | Val Loss=1.7501 Acc=0.4000 | LR=0.001000
Epoch 5: Train Loss=0.5234 Acc=0.8100 | Val Loss=0.6012 Acc=0.7800 | LR=0.001000
Epoch 10: Train Loss=0.1200 Acc=0.9700 | Val Loss=0.3500 Acc=0.8800 | LR=0.001000
Epoch 15: Train Loss=0.0500 Acc=0.9900 | Val Loss=0.5200 Acc=0.8500 | LR=0.000500

看第 15 行:训练 loss 还在降,验证 loss 开始升——过拟合了,该 Early Stopping。


课后练习

练习 1:训练 loss 2.30 不动(10 分类),accuracy 10%(等于随机猜)。列出 3 个可能原因和排查方法。

练习 2:训练 loss 从 2.0 降到 0.3,但验证 loss 从 0.5 升到 1.2。这是什么问题?怎么解决?

练习 3:写出你遇到 loss 变 NaN 时的完整排查步骤(至少 4 步)。

参考答案 / 自检思路

练习 1:loss=2.30 是 ln(10),10 分类的随机猜测 loss。

  1. 检查梯度:print(param.grad),如果是 None 或 0,检查 backward 和 requires_grad
  2. 检查学习率:可能太小,试 0.01
  3. 检查标签:print(y[:10]) 确认是 0-9 的整数,类型是 long
  4. 检查数据:print(x.shape, x.mean(), x.std()) 确认输入合理

练习 2:典型过拟合。训练在学,但开始记训练集的噪声。解决:① Early Stopping,在验证 loss 最低点(约 epoch 10)停止。② 加 Dropout。③ 加 Weight Decay。④ 数据增强。

练习 3

  1. 降低学习率 10 倍重试
  2. 加梯度裁剪 clip_grad_norm_(model.parameters(), max_norm=1.0)
  3. 检查数据有没有 inf/nan:torch.isnan(x).any()
  4. 检查是否有除零操作或 log(0)
  5. 检查初始化是否太大
  6. 减小 batch_size 看是否是 BatchNorm 问题

核心要点小结

  • 诊断流程:loss 不动 → 学习率/代码;震荡 → 降学习率/梯度裁剪;loss 降 acc 不升 → Shape/损失函数;训练好测试差 → 过拟合
  • 训练时打印 train/val 的 loss 和 accuracy,看 gap 判断过拟合
  • 梯度范数 0 是 bug,太小是消失,太大是爆炸
  • loss=ln(num_classes) 且不动 = 随机猜测,检查代码
  • 好的训练日志是诊断的基础
  • 阶段四结束——你现在能独立搭建 PyTorch 训练脚本并诊断问题了

下一篇进入阶段五:用真实项目把前面学的全串起来,从线性回归开始。

Discussion

留言讨论