在这里插入图片描述

文章目录


📖 课前导读

报错不是敌人,是导师

每个深度学习开发者都经历过这样的时刻:满怀期待地运行训练脚本,却看到满屏血红的错误信息。初学者往往会感到沮丧,甚至怀疑自己是否适合这个领域。

但请记住:报错是学习过程中最宝贵的反馈。每次成功解决一个报错,你对PyTorch底层机制的理解就更深一层。事实上,许多资深的算法工程师也会频繁遇到各类报错——只是他们拥有系统化的排错方法论,能够快速定位并解决问题。

本课的目的,就是为你提供一套完整的高频报错排查手册。我们将按照错误类型分类,每个错误都包含三个部分:

  1. 报错现象:错误信息长什么样
  2. 原因分析:为什么会出现这个错误
  3. 解决方案:根治方法 + 代码修改示例

同时,我们还会介绍通用的排错流程和调试技巧,帮助你在遇到新错误时也能从容应对。

💡 学习建议:不必一次记住所有错误,可以将本课作为“字典”使用——遇到具体报错时,快速查找对应章节,找到解决方案。随着经验积累,你会发现自己处理报错的速度越来越快。

学完这一课,你将能够:

  • ✅ 快速解读PyTorch的Traceback,定位错误代码行
  • ✅ 解决90%以上的维度不匹配错误
  • ✅ 处理梯度相关的各种问题(requires_grad、梯度清零、梯度爆炸)
  • ✅ 诊断显存溢出并采取优化措施
  • ✅ 解决数据类型和设备不匹配问题
  • ✅ 排查训练不收敛、准确率不上涨的深层原因
  • ✅ 使用调试工具(pdb、print、assert)高效调试

一、知识原理:错误分类与排错方法论

1.1 错误的两大类

类别特点常见例子
编译/运行时错误程序无法运行,立即报错维度不匹配、数据类型错误、CUDA错误
语义/逻辑错误程序能运行,但结果不对训练不收敛、损失为NaN、准确率不涨

第一类错误有明确的错误信息,相对容易定位;第二类错误需要结合训练曲线和数值分析,更具挑战性。

1.2 排错三步法

  1. 读错误信息:不要害怕英文,关键信息通常在后半部分。找到RuntimeError:或ValueError:后面的具体描述。
  2. 定位代码行:Traceback会指出出错的文件和行号。从下往上读,找到你自己的代码行(不是PyTorch源码)。
  3. 检查变量:使用print(x.shape)、print(x.dtype)、print(x.device)打印相关张量的属性。

1.3 调试利器

  • Print调试:最常用,在可疑位置打印形状、数值范围。
  • 断言(assert):assert x.shape == (batch, 10), f"Shape error: {x.shape}"
  • pdb:import pdb; pdb.set_trace() 设置断点,单步调试。
  • torch.autograd.set_detect_anomaly(True):精确定位产生NaN的梯度计算点。

二、环境搭建与准备

本课不需要特殊环境,标准PyTorch环境即可。为了演示错误,我们将故意构造一些错误代码,并展示修正方法。

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset
import numpy as np

print(f"PyTorch版本: {torch.__version__}")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"使用设备: {device}")

三、代码实战:高频错误分类详解

3.1 维度不匹配错误

这是最常见的错误,几乎每个开发者都会遇到。

错误1:RuntimeError: mat1 and mat2 shapes cannot be multiplied

报错现象:

RuntimeError: mat1 and mat2 shapes cannot be multiplied (32x784 and 128x10)

原因分析:矩阵乘法时,第一个矩阵的列数(784)不等于第二个矩阵的行数(128)。常见于全连接层的输入维度与权重维度不匹配。

典型场景:定义nn.Linear(784, 10),但实际输入特征维度是128。

解决方案:检查模型定义中nn.Linear(in_features, out_features)的in_features是否正确。计算前一层输出的总元素个数。

# 错误示例
class WrongNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc = nn.Linear(784, 10)  # 期望输入784
    def forward(self, x):
        x = x.view(x.size(0), -1)  # 假设x来自卷积层,实际展平后是128
        return self.fc(x)  # 报错:128 != 784

# 正确做法:打印形状确认
class CorrectNet(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv = nn.Conv2d(3, 16, 3)
        self.pool = nn.MaxPool2d(2)
        # 先计算展平后的维度
        self.fc = None  # 动态创建
    
    def forward(self, x):
        x = self.pool(torch.relu(self.conv(x)))
        # 动态计算展平后的维度
        if self.fc is None:
            flat_dim = x.view(x.size(0), -1).size(1)
            self.fc = nn.Linear(flat_dim, 10).to(x.device)
        x = x.view(x.size(0), -1)
        return self.fc(x)
错误2:IndexError: index out of range in self

原因:在CrossEntropyLoss中,标签值超出了类别数范围(例如标签值为10,但类别数只有10,索引从0到9)。

解决方案:检查标签的最小值和最大值,确保在[0, num_classes-1]内。

# 检查标签
print(f"标签范围: {labels.min().item()} - {labels.max().item()}")
assert labels.max() < num_classes, f"标签最大值{labels.max()} >= 类别数{num_classes}"
错误3:RuntimeError: shape '[x, y]' is invalid for input of size z

原因:使用view()或reshape()时,指定的新形状的总元素数与原张量元素数不一致。

解决方案:使用-1让PyTorch自动推断其中一个维度,或先打印原形状。

x = torch.randn(4, 3, 32, 32)  # 总元素 4*3*32*32 = 12288
# 错误:12288 不能被 6 整除
# y = x.view(6, -1)  # 报错

# 正确:确保乘积相等
y = x.view(4, -1)  # 4 * 3072 = 12288

3.2 梯度相关错误

错误4:RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn

原因:对不需要梯度的张量调用了.backward(),或者在计算图中没有requires_grad=True的叶子节点。

解决方案:确保损失函数关联的参数都设置了requires_grad=True(通常模型参数默认需要梯度)。

# 错误示例
x = torch.tensor([1.0, 2.0])  # 默认 requires_grad=False
y = x * 2
y.backward()  # 报错:x不需要梯度

# 正确
x = torch.tensor([1.0, 2.0], requires_grad=True)
y = x * 2
y.sum().backward()
错误5:RuntimeError: Trying to backward through the graph a second time

原因:默认情况下,.backward()后计算图被释放以节省内存。第二次调用backward()时会报错。

解决方案:

  • 如果确实需要多次反向传播,在第一次调用时传入retain_graph=True
  • 更常见的错误是:在训练循环中忘记清零梯度,导致误用了上一次的图
# 错误:重复使用同一个loss调用backward
loss = model(x)
loss.backward()
loss.backward()  # 报错

# 正确方式1:保留计算图
loss.backward(retain_graph=True)
loss.backward()

# 正确方式2:每次重新计算loss(最常见)
for epoch in range(epochs):
    for data in loader:
        loss = model(data)
        loss.backward()  # 每次都是新的计算图
错误6:梯度爆炸导致损失变成NaN

原因:学习率过大、梯度更新步长太大,导致参数变成NaN。

解决方案:梯度裁剪、降低学习率、检查数据是否包含NaN。

# 在反向传播后、优化器step前添加梯度裁剪
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
optimizer.step()
错误7:梯度消失,损失不下降

原因:深层网络的梯度逐层衰减到0,靠近输入层的参数几乎不更新。

解决方案:

  • 使用残差连接(ResNet)
  • 使用BatchNorm
  • 更换激活函数(ReLU取代Sigmoid/Tanh)
  • 使用更好的初始化(Kaiming初始化)

3.3 显存溢出错误

错误8:RuntimeError: CUDA out of memory

报错现象:

RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB (GPU 0; 8.00 GiB total capacity; 6.50 GiB already allocated; 500.00 MiB free; 7.00 GiB reserved in total by PyTorch)

原因分析:

  • batch size过大
  • 模型本身太大
  • 中间激活值太多(如存储了完整计算图)
  • 其他进程占用显存

解决方案(按推荐顺序):

  1. 减小batch size
  2. 使用梯度累积模拟大batch
  3. 使用混合精度训练(AMP)
  4. 使用梯度检查点(checkpointing)
  5. 清理不再使用的变量:del x; torch.cuda.empty_cache()
# 方案1:梯度累积
accumulation_steps = 4
for i, (inputs, labels) in enumerate(train_loader):
    outputs = model(inputs)
    loss = criterion(outputs, labels)
    loss = loss / accumulation_steps  # 归一化
    loss.backward()
    if (i + 1) % accumulation_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

# 方案2:混合精度
from torch.cuda.amp import autocast, GradScaler
scaler = GradScaler()
with autocast():
    outputs = model(inputs)
    loss = criterion(outputs, labels)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
错误9:CUDA error: device-side assert triggered

原因:这通常是索引越界或数值问题在CUDA上的表现。常见于标签超出范围、Softmax输入包含NaN等。

解决方案:切换到CPU运行,会得到更详细的错误信息。

# 临时切换到CPU调试
model_cpu = model.cpu()
inputs_cpu = inputs.cpu()
labels_cpu = labels.cpu()
outputs = model_cpu(inputs_cpu)
loss = criterion(outputs, labels_cpu)
# CPU上会直接报出具体的索引错误

3.4 数据类型与设备错误

错误10:Expected object of scalar type Float but got scalar type Long

原因:某个操作期望浮点型输入,但传入了整数型(如标签)。

解决方案:显式转换为浮点型。

# 错误示例
loss_fn = nn.MSELoss()
pred = torch.randn(3, 1)
target = torch.tensor([0, 1, 0])  # LongTensor
loss = loss_fn(pred, target)  # 报错

# 正确
target_float = target.float()
loss = loss_fn(pred, target_float)
错误11:RuntimeError: Input type (torch.cuda.FloatTensor) and weight type (torch.FloatTensor) should be the same

原因:模型和数据不在同一个设备上(一个在CPU,一个在GPU)。

解决方案:确保模型和数据都移动到同一个设备。

# 统一使用device
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = MyModel().to(device)
data = data.to(device)

3.5 模型加载与保存错误

错误12:Missing key(s) in state_dict 或 Unexpected key(s)

原因:保存的模型权重与当前模型结构不匹配。常见于修改了模型定义后加载旧权重。

解决方案:

  • 严格加载:model.load_state_dict(torch.load('model.pth'), strict=False),忽略缺失或多余的键。
  • 或者只加载匹配的部分。
# 非严格加载,忽略不匹配的层
state_dict = torch.load('model.pth')
model.load_state_dict(state_dict, strict=False)

# 只加载匹配的键
model_dict = model.state_dict()
pretrained_dict = {k: v for k, v in state_dict.items() if k in model_dict and v.shape == model_dict[k].shape}
model_dict.update(pretrained_dict)
model.load_state_dict(model_dict)
错误13:PickleError 或 AttributeError: Can't get attribute 'MyModel'

原因:尝试直接保存整个模型对象(torch.save(model, 'model.pth')),但加载时模型类的定义不在当前环境中。

解决方案:始终保存state_dict而非整个模型。

# 保存(推荐)
torch.save(model.state_dict(), 'model_weights.pth')

# 加载
model = MyModel()  # 需要先定义类
model.load_state_dict(torch.load('model_weights.pth'))

3.6 数据加载错误

错误14:RuntimeError: DataLoader worker (pid xxx) is killed by signal: Bus error (macOS)

原因:macOS上多进程数据加载的已知问题。

解决方案:设置num_workers=0。

loader = DataLoader(dataset, batch_size=32, num_workers=0)  # 避免多进程
错误15:EOFError: Ran out of input

原因:读取的pickle文件为空或损坏。

解决方案:删除损坏的数据文件,重新下载。

3.7 训练不收敛问题

错误16:损失一直不下降

原因:

  • 学习率太大或太小
  • 数据未归一化
  • 梯度消失(深层网络)
  • 损失函数选择错误

排查步骤:

  1. 打印梯度范数,检查是否接近0
  2. 尝试一个极小数据集(如10个样本),看能否过拟合
  3. 调整学习率(使用学习率查找)
  4. 检查数据预处理(是否归一化)
# 打印梯度范数
total_norm = 0
for p in model.parameters():
    if p.grad is not None:
        param_norm = p.grad.data.norm(2)
        total_norm += param_norm.item() ** 2
total_norm = total_norm ** 0.5
print(f"梯度范数: {total_norm}")
错误17:损失变为NaN

原因:

  • 梯度爆炸
  • 学习率过大
  • 数据包含NaN或inf
  • 对数运算中出现log(0)

解决方案:

  • 梯度裁剪
  • 降低学习率
  • 检查数据:torch.isnan(data).any()
  • 在损失函数中添加小epsilon(如交叉熵内部已处理)
# 检查数据中是否有NaN
assert not torch.isnan(inputs).any(), "输入包含NaN"
assert not torch.isnan(labels).any(), "标签包含NaN"

3.8 准确率不上涨问题

错误18:验证准确率与随机猜测相近

原因:

  • 标签泄露或打乱错误
  • 数据预处理错误(如标签与图像不匹配)
  • 模型输出层未正确设置

解决方案:

  • 检查数据加载器的shuffle设置
  • 打印几个样本的图像和标签,肉眼验证
  • 确认损失函数和输出层匹配(分类用Softmax/CrossEntropy,回归用MSE)
# 可视化验证数据
import matplotlib.pyplot as plt
images, labels = next(iter(train_loader))
for i in range(4):
    plt.subplot(2, 2, i+1)
    plt.imshow(images[i].permute(1,2,0))
    plt.title(f"Label: {labels[i]}")
plt.show()
错误19:训练准确率高,验证准确率低(过拟合)

原因:模型容量过大,训练数据不足。

解决方案:

  • 添加Dropout、BatchNorm
  • 增加L2正则化(weight_decay)
  • 数据增强
  • 早停

3.9 多GPU训练错误

错误20:RuntimeError: module must have its parameters and buffers on device cuda:0

原因:在调用DistributedDataParallel前,模型必须在正确的GPU上。

解决方案:先model.to(device),再包装。

# 正确顺序
device = torch.device(f'cuda:{local_rank}')
model = MyModel().to(device)
model = DDP(model, device_ids=[local_rank])

四、通用排错技巧

4.1 阅读理解Traceback

Traceback应该从下往上读。最后一行通常是异常类型和简要描述。向上找到第一个指向你的代码文件的行。

示例:

Traceback (most recent call last):
  File "train.py", line 45, in <module>
    loss.backward()
  File "/.../torch/_tensor.py", line 487, in backward
    torch.autograd.backward(self, gradient, retain_graph, create_graph, inputs=inputs)
  File "/.../torch/autograd/__init__.py", line 200, in backward
    Variable._execution_engine.run_backward(...)
RuntimeError: element 0 of tensors does not require grad

关键信息:train.py第45行调用了loss.backward(),错误原因是某个张量不需要梯度。

4.2 最小可复现示例

当错误复杂时,创建一个最小可复现示例(MRE),剥离所有无关代码,只保留触发错误的必要部分。这通常能自己发现问题。

4.3 使用断言进行防御性编程

在关键位置添加断言,提前捕获错误:

assert x.dim() == 4, f"Expected 4D input, got {x.dim()}D"
assert x.shape[1] == 3, f"Expected 3 channels, got {x.shape[1]}"
assert y.max() < num_classes, f"Label {y.max()} out of range"

4.4 启用异常检测

PyTorch提供了详细的异常检测模式,会检查中间梯度是否为NaN:

torch.autograd.set_detect_anomaly(True)
# 训练代码...

注意:这会降低性能,仅在调试时使用。


五、课后总结

错误速查表

错误信息关键词常见原因快速解决方案
mat1 and mat2 shapes cannot be multiplied维度不匹配检查nn.Linear的in_features
element 0 of tensors does not require grad张量没有梯度设置requires_grad=True
Trying to backward through the graph a second time重复反向传播添加retain_graph=True或检查梯度清零
CUDA out of memory显存不足减小batch size、梯度累积、AMP
Expected object of scalar type Float but got Long数据类型错误使用.float()转换
Input type and weight type should be the same设备不匹配统一.to(device)
Missing key(s) in state_dict模型结构不匹配使用strict=False
loss is NaN梯度爆炸或数据问题梯度裁剪、降低lr、检查数据
训练不收敛多种原因打印梯度范数、尝试过拟合小样本

排错心态

  • 不要恐慌:报错是正常的,每个开发者都会遇到。
  • 先读错误:错误信息往往直接指出了问题所在。
  • 分而治之:隔离问题,逐步缩小范围。
  • 善用搜索引擎:复制错误信息到Google/Stack Overflow,很可能有人遇到过。

检查清单

  • 学会阅读Traceback,定位到自己的代码行
  • 能够区分运行时错误和逻辑错误
  • 掌握常用的调试技巧(print、assert、pdb)
  • 遇到维度错误时,会打印形状并计算
  • 遇到显存错误时,知道从哪些方面优化
  • 遇到梯度问题时,能检查requires_grad和梯度值
  • 遇到NaN时,会逐一排查数据、损失、优化器
  • 能独立解决90%以上的常见报错

六、课后作业

作业1:故意制造并修复维度错误

创建一个简单的全连接网络,输入是torch.randn(4, 20),但nn.Linear的in_features错误地设置为10。运行代码,记录报错信息,然后修正。

作业2:梯度爆炸实验

构造一个没有激活函数的10层线性网络,使用极大的学习率(如10.0)训练。观察损失是否变成NaN。添加梯度裁剪后再次训练,对比结果。

作业3:数据类型错误修复

编写一个函数,接收一个整数标签张量labels,将其传入CrossEntropyLoss(它期望LongTensor),故意传入FloatTensor,然后修正。

作业4:显存优化实验

使用ResNet-50(或任何较大模型)在CIFAR-10上训练,初始batch size=128导致显存溢出。依次尝试减小batch size、梯度累积、混合精度,记录每种方案的batch size和显存占用。

作业5:模型加载错误

保存一个自定义模型的state_dict。然后修改模型定义(例如增加一层),尝试加载旧权重,观察Missing key(s)错误。使用strict=False加载,并手动初始化缺失的层。

作业6:诊断训练不收敛

给定一个训练脚本,损失在初始值附近震荡不下降。使用本课学到的方法(打印梯度范数、检查数据归一化、尝试过拟合小样本)找出问题原因并修复。


七、下一课预告

第24课我们将学习PyTorch高阶语法与工程化代码规范,内容包括:

  • 模型模块化拆分、配置文件解耦
  • 参数统一管理、日志系统封装
  • 数据集类封装、训练器Trainer封装
  • 深度学习项目标准目录结构
  • 企业级开发编码规范、代码复用技巧

学完第24课,你将能写出结构清晰、可维护、可复用的深度学习工程代码。


🔗《精讲25课|PyTorch 从入门到精通》系列课程导航

去订阅

🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~

更多推荐