第23课:PyTorch|深度学习训练海量报错一站式排错大全【让报错不再是拦路虎】

文章目录
📖 课前导读
报错不是敌人,是导师
每个深度学习开发者都经历过这样的时刻:满怀期待地运行训练脚本,却看到满屏血红的错误信息。初学者往往会感到沮丧,甚至怀疑自己是否适合这个领域。
但请记住:报错是学习过程中最宝贵的反馈。每次成功解决一个报错,你对PyTorch底层机制的理解就更深一层。事实上,许多资深的算法工程师也会频繁遇到各类报错——只是他们拥有系统化的排错方法论,能够快速定位并解决问题。
本课的目的,就是为你提供一套完整的高频报错排查手册。我们将按照错误类型分类,每个错误都包含三个部分:
- 报错现象:错误信息长什么样
- 原因分析:为什么会出现这个错误
- 解决方案:根治方法 + 代码修改示例
同时,我们还会介绍通用的排错流程和调试技巧,帮助你在遇到新错误时也能从容应对。
💡 学习建议:不必一次记住所有错误,可以将本课作为“字典”使用——遇到具体报错时,快速查找对应章节,找到解决方案。随着经验积累,你会发现自己处理报错的速度越来越快。
学完这一课,你将能够:
- ✅ 快速解读PyTorch的Traceback,定位错误代码行
- ✅ 解决90%以上的维度不匹配错误
- ✅ 处理梯度相关的各种问题(requires_grad、梯度清零、梯度爆炸)
- ✅ 诊断显存溢出并采取优化措施
- ✅ 解决数据类型和设备不匹配问题
- ✅ 排查训练不收敛、准确率不上涨的深层原因
- ✅ 使用调试工具(pdb、print、assert)高效调试
一、知识原理:错误分类与排错方法论
1.1 错误的两大类
| 类别 | 特点 | 常见例子 |
|---|---|---|
| 编译/运行时错误 | 程序无法运行,立即报错 | 维度不匹配、数据类型错误、CUDA错误 |
| 语义/逻辑错误 | 程序能运行,但结果不对 | 训练不收敛、损失为NaN、准确率不涨 |
第一类错误有明确的错误信息,相对容易定位;第二类错误需要结合训练曲线和数值分析,更具挑战性。
1.2 排错三步法
- 读错误信息:不要害怕英文,关键信息通常在后半部分。找到
RuntimeError:或ValueError:后面的具体描述。 - 定位代码行:Traceback会指出出错的文件和行号。从下往上读,找到你自己的代码行(不是PyTorch源码)。
- 检查变量:使用
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过大
- 模型本身太大
- 中间激活值太多(如存储了完整计算图)
- 其他进程占用显存
解决方案(按推荐顺序):
- 减小batch size
- 使用梯度累积模拟大batch
- 使用混合精度训练(AMP)
- 使用梯度检查点(checkpointing)
- 清理不再使用的变量:
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:损失一直不下降
原因:
- 学习率太大或太小
- 数据未归一化
- 梯度消失(深层网络)
- 损失函数选择错误
排查步骤:
- 打印梯度范数,检查是否接近0
- 尝试一个极小数据集(如10个样本),看能否过拟合
- 调整学习率(使用学习率查找)
- 检查数据预处理(是否归一化)
# 打印梯度范数
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 从入门到精通》系列课程导航
🌟 感谢您耐心阅读到这里!
💡 如果本文对您有所启发欢迎:
👍 点赞📌 收藏 📤 分享给更多需要的伙伴。
🗣️ 期待在评论区看到您的想法, 共同进步。
🔔 关注我,持续获取更多干货内容~
🤗 我们下篇文章见~
更多推荐


所有评论(0)