趋近智
requires_grad)backward()).grad)torch.nn 搭建模型torch.nn.Module 基类torch.nn 损失)torch.optim)torch.utils.data.Datasettorchvision.transforms)torch.utils.data.DataLoader动手练习旨在巩固应用常见调试难点和可视化工具的技能。这些练习侧重于识别错误、检查模型行为,并使用 TensorBoard 和标准调试方法监控训练进度。涵盖的场景包括形状不匹配、设备放置错误和设置可视化。
形状不匹配是构建或修改神经网络时常见的错误。请看下面这个简单模型,它旨在处理 28x28 灰度图像(如 MNIST),并将其展平为 784 元素的向量:
import torch
import torch.nn as nn
class SimpleMLP(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(784, 128) # 输入 784,输出 128
self.activation = nn.ReLU()
self.layer2 = nn.Linear(128, 64) # 输入 128,输出 64
# layer3 的输入大小不正确 - 应该是 64
self.layer3 = nn.Linear(100, 10) # 输入 100(错误!),输出 10
def forward(self, x):
x = self.layer1(x)
x = self.activation(x)
x = self.layer2(x)
x = self.activation(x)
# 这行代码将导致错误
x = self.layer3(x)
return x
# 创建一个模拟输入批次(批次大小 4,特征 784)
dummy_input = torch.randn(4, 784)
model = SimpleMLP()
# 尝试前向传播
try:
output = model(dummy_input)
print("模型运行成功!")
except RuntimeError as e:
print(f"捕获到错误:{e}")
运行代码: 执行上面的代码片段。你会遇到一个 RuntimeError。仔细查看错误信息。它通常指向尺寸不匹配,常常会提到特定层的预期输入尺寸和实际输入尺寸(在本例中是 mat1 and mat2 shapes cannot be multiplied)。
诊断: 错误发生在输入 x 到达 self.layer3 时。前一个层 self.layer2 输出一个形状为 (batch_size, 64) 的张量。然而,self.layer3 被定义为 nn.Linear(100, 10),它预期输入有 100 个特征。这种不匹配导致了错误。
# 在 forward 方法中,在 self.layer3(x) 之前
print("layer3 之前的形状:", x.shape)
x = self.layer3(x)
修正代码: 修改 __init__ 方法中 self.layer3 的定义,以接受正确数量的输入特征(即 64,self.layer2 的输出大小)。
# 修正后的层定义
self.layer3 = nn.Linear(64, 10) # 输入 64,输出 10
验证: 使用修正后的层定义重新运行脚本。前向传播现在应该能顺利完成,没有错误。
使用 GPU 时,很重要的一点是模型和数据都在同一个设备上。我们来模拟一个错误场景,其中模型被移到 GPU,但输入张量仍留在 CPU 上。
import torch
import torch.nn as nn
# 假设有可用的 CUDA GPU
if torch.cuda.is_available():
device = torch.device("cuda")
print("正在使用 GPU:", torch.cuda.get_device_name(0))
else:
device = torch.device("cpu")
print("正在使用 CPU")
class SimpleNet(nn.Module):
def __init__(self):
super().__init__()
self.linear = nn.Linear(10, 5)
def forward(self, x):
return self.linear(x)
# 创建模型并将其移动到目标设备(例如,GPU)
model = SimpleNet().to(device)
print(f"模型参数位于:{next(model.parameters()).device}")
# 创建输入数据 - 有意留在 CPU 上
input_data = torch.randn(8, 10)
print(f"输入数据位于:{input_data.device}")
# 尝试前向传播 - 如果设备是 'cuda',这可能会导致错误
try:
output = model(input_data)
print("前向传播成功!")
except RuntimeError as e:
print(f"\n捕获到错误:{e}")
print("\n提示:检查模型和输入数据是否在同一个设备上。")
运行代码: 如果你有支持 CUDA 的 GPU,运行这段代码会产生一个 RuntimeError。错误信息很可能会显示类似 Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu! 的内容。
诊断: 打印语句确认模型位于 cuda 设备上(如果可用),而 input_data 位于 cpu 设备上。PyTorch 操作通常要求操作数位于同一个设备上。
修正代码: 在将 input_data 传递给模型之前,将其移动到模型所在的设备上。
# 将输入数据移动到正确设备
input_data = input_data.to(device)
print(f"输入数据已移至:{input_data.device}")
# 现在,再次尝试前向传播
output = model(input_data)
print("数据移动后前向传播成功!")
验证: 重新运行修正后的脚本。前向传播应该能顺利执行,没有设备不匹配错误。记住这个原理也适用于训练循环中;从 DataLoader 获取的每个批次都需要被移动到相应的设备上。
TensorBoard 为训练过程提供了宝贵的参考信息。让我们将其集成到一个简化训练循环中。我们将模拟训练数据并追踪一个模拟损失值。
import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.tensorboard import SummaryWriter
import time
# 1. 设置 TensorBoard 写入器
# 日志文件将保存在 'runs/simple_experiment' 目录中
writer = SummaryWriter('runs/simple_experiment')
# 2. 定义一个简单模型、损失函数和优化器
model = nn.Linear(10, 2) # 简单的线性模型
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 模拟一个简单数据集
inputs = torch.randn(100, 10) # 100 个样本,10 个特征
targets = torch.randn(100, 2) # 100 个样本,2 个输出值
# 3. 简单训练循环
print("开始模拟训练...")
num_epochs = 50
for epoch in range(num_epochs):
optimizer.zero_grad() # 梯度清零
outputs = model(inputs) # 前向传播
loss = criterion(outputs, targets) # 计算损失
# 模拟损失变化(在实际训练中替换为真实损失)
# 为演示目的,让损失随周期递减
simulated_loss = loss + torch.randn(1) * 0.1 + (num_epochs - epoch) / num_epochs
simulated_loss.backward() # 反向传播(使用模拟损失进行演示)
optimizer.step() # 更新权重
# 4. 将指标记录到 TensorBoard
if (epoch + 1) % 5 == 0: # 每 5 个周期记录一次
# 记录标量“损失”值
writer.add_scalar('Training/Loss', simulated_loss.item(), epoch)
# 记录模型权重分布(以线性层为例)
writer.add_histogram('Model/Weights', model.weight, epoch)
writer.add_histogram('Model/Bias', model.bias, epoch)
print(f'周期 [{epoch+1}/{num_epochs}],模拟损失:{simulated_loss.item():.4f}')
time.sleep(0.1) # 模拟训练时间
# 5. 添加模型图(可选)
# 确保输入形状与模型预期一致
# writer.add_graph(model, inputs[0].unsqueeze(0)) # 提供一个样本输入批次
# 6. 关闭写入器
writer.close()
print("模拟训练完成。TensorBoard 日志已保存到 'runs/simple_experiment'。")
print("在你的终端中运行 'tensorboard --logdir=runs' 来查看。")
runs 文件夹的目录(而不是 runs 文件夹内部),并运行命令:
tensorboard --logdir=runs
http://localhost:6006/)。在你的网页浏览器中打开这个 URL。simple_experiment 运行记录。在“标量”(Scalars)标签页下,你会看到“训练/损失”(Training/Loss)图表,它显示了我们模拟损失的递减趋势。在“直方图”(Histograms)或“分布”(Distributions)标签页下,你可以观察模型权重和偏差的分布如何随周期变化(或者在这个简单模拟中变化不大)。如果你取消注释了 add_graph 那行代码,你还会发现模型架构的可视化图表在“图”(Graphs)标签页下。下面这个图表显示了一个在 TensorBoard 中查看时,损失如何随周期递减的例子。
一个折线图,描绘了 50 个周期内模拟训练损失的递减情况,每 5 个周期记录一次。
有时,打印语句不足以解决问题,你需要交互式地检查程序状态。Python 调试器(pdb)是一个强大的工具。让我们回顾练习 1 中的形状不匹配场景,并使用 pdb。
通过在顶部添加 import pdb 并在导致错误的行之前添加 pdb.set_trace(),修改练习 1 中 原始的 失败代码:
import torch
import torch.nn as nn
import pdb # 导入调试器
class SimpleMLP(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = nn.Linear(784, 128)
self.activation = nn.ReLU()
self.layer2 = nn.Linear(128, 64)
# layer3 的输入大小不正确
self.layer3 = nn.Linear(100, 10) # 这里有错误!
def forward(self, x):
x = self.layer1(x)
x = self.activation(x)
x = self.layer2(x)
x = self.activation(x)
print("即将进入 pdb...")
pdb.set_trace() # 在此设置断点
# 执行将在此暂停
print("layer3 之前的形状:", x.shape) # 我们可以在 pdb 中检查 x
x = self.layer3(x) # 这行代码将导致错误
return x
dummy_input = torch.randn(4, 784)
model = SimpleMLP()
output = model(dummy_input) # 运行现在将在 forward() 内部暂停
pdb.set_trace() 时,它会暂停,你会在终端中看到 (Pdb) 提示符。p x.shape(打印 x.shape)然后按回车键。你会看到 torch.Size([4, 64])。p self.layer3 然后按回车键。你会看到定义 Linear(in_features=100, out_features=10, bias=True)。n(下一步)然后按回车键。这将尝试执行下一行代码(x = self.layer3(x)),这会导致 RuntimeError,并可能退出调试器或在其中显示错误堆栈。c(继续)让程序继续运行直到下一个断点或出现错误。q(退出)立即退出调试器并终止脚本。q 退出,然后像练习 1 中那样修正代码,并删除 import pdb 和 pdb.set_trace() 行。这个练习为你在 PyTorch 项目中处理调试和监控任务打下了基础。记住,使用打印语句进行快速检查,pdb 进行交互式检查,以及 TensorBoard 用于可视化训练过程和模型结构。这些工具对于构建、训练和改进高效的深度学习模型来说非常重要。
这部分内容有帮助吗?
© 2026 ApX Machine Learning用心打造