☰
PyTorch第五章练习题通关指南:从张量到训练循环的完整拆解
2026/10/14 20:55:16 网站建设 项目流程

1. 为什么“第五章练习题”才是PyTorch入门真正的分水岭

很多人学PyTorch的路径都差不多:第一章看张量创建,第二章看自动求导,第三章看nn.Module,第四章看数据加载,到了第五章——通常是神经网络训练流程的完整串联——就开始卡壳了。卡壳的原因不是某个API不会用,而是前面四章的知识点在这一章被同时调用,任何一个环节理解不到位,整条链路就跑不通。

我见过不少学习者,前四章的代码都能照着敲出来,但一到第五章的练习题,面对“自己搭一个网络、自己写训练循环、自己评估结果”这种综合任务时,就不知道从哪下手。这很正常,因为练习题的设计意图本来就不是考你背API,而是考你能不能把零散的知识组装成一个可运行的系统。

这篇文章就是围绕PyTorch基础学习第五章的练习题展开的。我会把这类练习题通常涉及的几个核心模块拆开讲透:从数据准备、模型定义、训练循环、损失函数与优化器的配合,到验证评估和常见报错的处理。每一部分我都会说清楚“为什么这样做”,而不只是“这样做”。适合已经看完前四章、正在做第五章练习但感觉吃力的读者,也适合想重新梳理训练流程的老手对照检查。

需要提前说明的是,第五章练习题的具体题目因教材而异,但核心考点高度一致:张量操作、自动求导、模块化建模、数据管道、训练与评估循环。我会围绕这些共性考点展开,你手里的具体题目可以对应着看。

2. 练习题里最容易被低估的环节:数据准备与张量形状

2.1 为什么形状问题总是第一个拦路虎

PyTorch里绝大多数报错,追到根上都是形状不匹配。第五章练习题通常会给一个模拟数据集,或者让你自己生成数据,比如用torch.randn造一批特征和标签。看起来简单,但这里埋着第一个坑:你要清楚你的数据是“样本优先”还是“特征优先”。

PyTorch的惯例是样本维度在前。也就是说,一个batch的数据形状通常是(batch_size, num_features),标签形状是(batch_size,)或(batch_size, 1)。如果你从NumPy转过来,可能会习惯(num_features, batch_size)的写法,直接喂给nn.Linear就会报维度错误。

我建议在做任何练习题之前,先养成一个习惯:每创建一个张量,立刻打印它的.shape。这不是笨办法,这是最有效的调试手段。你可以这样操作:

import torch # 模拟:100个样本,每个样本10个特征 X = torch.randn(100, 10) # 回归任务标签 y = torch.randn(100, 1) print(X.shape) # torch.Size([100, 10]) print(y.shape) # torch.Size([100, 1])

如果练习题要求你手动实现数据分批,而不是用DataLoader,那你要自己写切片逻辑。这里有个细节:切片时不要用Python列表的切片语法去切张量再转回张量,直接用张量的切片,效率高且不会断计算图。

2.2 手写数据分批与DataLoader的取舍

第五章练习题有时会要求你不用DataLoader,手动实现mini-batch训练。这是为了让你理解batch的概念。手动实现的核心逻辑是:打乱索引、按batch_size切分、逐个取出。

def manual_batches(X, y, batch_size, shuffle=True): n = X.shape[0] indices = torch.randperm(n) if shuffle else torch.arange(n) for start in range(0, n, batch_size): idx = indices[start:start + batch_size] yield X[idx], y[idx]

这段代码看起来简单,但有两个经验点值得说。第一,torch.randperm每次调用都会重新打乱,如果你在多个epoch里调用同一个生成器,需要每个epoch重新创建。第二,最后一个batch可能不足batch_size,这在大多数情况下没问题,但如果你用了BatchNorm,最后一个batch只有一个样本时会报错,因为BatchNorm在训练模式下无法对单样本计算方差。这是很多人做练习题时遇到的“玄学报错”,其实原因就在这里。

用DataLoader的话,这些问题它帮你处理了,但你要理解它背后做了什么。练习题如果允许用DataLoader,我建议用,但你要能说清楚shuffle=True、batch_size、drop_last这几个参数的含义。drop_last=True就是丢弃最后一个不完整的batch,专门用来规避上面说的BatchNorm问题。

2.3 数据类型不匹配:Long vs Float

另一个高频坑是数据类型。分类任务的标签必须是torch.long(即int64),因为CrossEntropyLoss要求目标张量是long类型。如果你用torch.randn生成了标签,它是float,直接传给损失函数就会报错。

# 错误示范 y = torch.randn(100, 1) # float # loss = nn.CrossEntropyLoss()(output, y) # 报错 # 正确做法 y = torch.randint(0, 3, (100,)) # long,3分类

回归任务则相反,标签和输出都应该是float。这个区分在练习题里经常考,因为出题人会故意混着出,看你能不能分清任务类型。

提示:遇到RuntimeError: expected scalar type Long but found Float这类报错,第一反应就是检查标签的dtype,用.long()或.float()转换。

3. 模型定义:从nn.Module到参数初始化的实战细节

3.1 为什么练习题总要求你继承nn.Module

第五章练习题几乎一定会让你定义一个自己的网络类,继承nn.Module。这不是为了形式,而是因为nn.Module提供了参数注册、设备迁移、状态字典保存等一整套机制。如果你只是用nn.Sequential堆层,简单场景够用,但一旦需要在forward里加条件分支、自定义连接方式,就必须用类的方式。

一个典型的练习题模型长这样:

import torch.nn as nn class SimpleNet(nn.Module): def __init__(self, in_dim, hidden_dim, out_dim): super().__init__() self.fc1 = nn.Linear(in_dim, hidden_dim) self.relu = nn.ReLU() self.fc2 = nn.Linear(hidden_dim, out_dim) def forward(self, x): x = self.fc1(x) x = self.relu(x) x = self.fc2(x) return x

这里有个容易忽略的点:super().__init__()必须放在最前面。如果你先定义了层再调用它,PyTorch会报错,因为参数注册机制还没初始化。这个错误在练习题里很常见,尤其是从其他框架转过来的人。

3.2 激活函数的选择与放置位置

练习题里最常见的激活函数是ReLU,但你要知道什么时候用Sigmoid、什么时候用Tanh。简单说:隐藏层用ReLU是默认选择,因为它在正区间梯度恒定,不容易梯度消失。输出层则取决于任务:二分类用Sigmoid,多分类用Softmax(但通常和CrossEntropyLoss合在一起,不需要手动加),回归任务不加激活。

放置位置也有讲究。nn.ReLU()可以作为层放在__init__里,也可以在forward里用torch.relu()函数调用。前者更规范,后者更灵活。练习题如果要求你“用函数式写法”,那就用后者。

3.3 参数初始化:练习题不考但实际很重要

大多数第五章练习题不会要求你手动初始化参数,因为PyTorch的默认初始化已经够用。但如果你发现模型训练时loss不下降,或者出现NaN,很可能和初始化有关。默认的Linear层用的是Kaiming均匀初始化,对ReLU网络是合适的。如果你自己改了初始化方式,比如全零初始化,那网络就学不到东西,因为所有神经元的梯度相同,对称性无法打破。

我个人的经验是:做练习题时先用默认初始化跑通,如果效果不好再考虑调整。不要一上来就折腾初始化,那是进阶内容。

4. 训练循环的骨架:每一步到底在做什么

4.1 五步循环的完整拆解

PyTorch的训练循环可以概括为五个步骤,第五章练习题的核心就是让你把这五步写对、写全:

  1. 前向传播:把输入喂给模型,得到预测输出。
  2. 计算损失:用损失函数比较预测和真实标签。
  3. 清空梯度:把上一步的梯度清零,否则会累加。
  4. 反向传播:调用loss.backward()计算梯度。
  5. 更新参数:优化器根据梯度更新权重。

这五步的顺序不能乱,尤其是清空梯度必须在反向传播之前。我见过有人把optimizer.zero_grad()放在loss.backward()后面,结果梯度不断累加,训练完全失控。这个错误在练习题里出现频率极高。

optimizer = torch.optim.SGD(model.parameters(), lr=0.01) criterion = nn.MSELoss() for epoch in range(100): for batch_X, batch_y in manual_batches(X, y, batch_size=16): # 1. 前向 pred = model(batch_X) # 2. 损失 loss = criterion(pred, batch_y) # 3. 清梯度 optimizer.zero_grad() # 4. 反向 loss.backward() # 5. 更新 optimizer.step()

4.2 为什么zero_grad的位置如此关键

PyTorch的梯度是累加的,这是设计特性而非缺陷。累加机制在RNN等场景下有用,但在标准训练循环里,你必须每个batch清零一次。如果你忘了清零,梯度会跨batch累积,等效于用了更大的batch size,但又不是真正的batch,导致训练不稳定。

有个记忆口诀:“前损清反更”,五个字对应五步。念起来顺口,写代码时不容易漏。

4.3 训练模式与评估模式的切换

练习题如果包含验证集,就必须在验证前调用model.eval(),验证后调用model.train()。这两个方法影响的是Dropout和BatchNorm的行为。训练模式下Dropout会随机丢弃神经元,BatchNorm用当前batch的统计量;评估模式下Dropout关闭,BatchNorm用训练时累积的全局统计量。

如果你忘了切换,验证结果会偏乐观或偏悲观,取决于具体网络结构。这个细节在练习题评分里经常是扣分点,因为它不影响代码运行,只影响结果正确性。

# 训练 model.train() for batch_X, batch_y in train_loader: ... # 验证 model.eval() with torch.no_grad(): for batch_X, batch_y in val_loader: pred = model(batch_X) ...

torch.no_grad()也是验证阶段必须加的,它关闭自动求导,节省显存并加速计算。练习题如果不加这个,虽然结果对,但会被认为不够专业。

5. 损失函数与优化器的搭配逻辑

5.1 任务类型决定损失函数

第五章练习题通常会覆盖三类任务:回归、二分类、多分类。对应的损失函数选择如下:

任务类型输出层激活损失函数标签类型
回归无MSELossfloat
二分类SigmoidBCELossfloat
多分类无(Softmax内置)CrossEntropyLosslong

这里有个常见混淆:BCELoss和BCEWithLogitsLoss的区别。前者要求输入已经过Sigmoid,后者把Sigmoid和BCE合在一起,数值更稳定。练习题如果让你自己加Sigmoid,那就用BCELoss;如果没加,就用BCEWithLogitsLoss。多分类的CrossEntropyLoss内部已经包含了Softmax,所以输出层不要再加Softmax,否则相当于做了两次,结果会不对。

5.2 优化器选择:SGD还是Adam

练习题通常会用SGD或Adam。SGD需要调学习率,收敛慢但泛化可能更好;Adam自适应学习率,收敛快但有时过拟合。对于练习题这种小规模任务,Adam通常更容易跑出结果,学习率设0.001是安全起点。SGD的话学习率设0.01或0.1,配合动量0.9。

我个人的建议是:先用Adam跑通,确认模型结构和数据没问题,再换SGD调参。这样可以把“代码bug”和“调参问题”分开排查,效率更高。

5.3 学习率设置的经验值

学习率太大会震荡不收敛,太小会收敛极慢。练习题里如果loss一直不降,先检查学习率是不是太小;如果loss变成NaN,检查是不是太大。一个实用的排查方法是:打印每个epoch的loss,观察趋势。正常情况应该是前几个epoch快速下降,然后趋于平缓。

# 打印训练过程 for epoch in range(100): epoch_loss = 0 for batch_X, batch_y in manual_batches(X, y, 16): ... epoch_loss += loss.item() if epoch % 10 == 0: print(f"Epoch {epoch}, Loss: {epoch_loss:.4f}")

loss.item()把标量张量转成Python浮点数,避免累积计算图导致显存爆炸。这个细节很多人不注意,直接用epoch_loss += loss,跑几十个epoch后显存就满了。

6. 练习题中那些让人抓狂的报错与排查路径

6.1 形状不匹配的完整排查链路

假设你遇到RuntimeError: mat1 and mat2 shapes cannot be multiplied,排查步骤如下:

第一步,打印模型第一层的in_features和你输入的最后一维。比如nn.Linear(10, 20)要求输入最后一维是10。如果你的输入是(16, 5),那就对不上。

第二步,检查是否有维度被意外压缩。比如你用x.squeeze()把(16, 1)压成了(16,),再喂给Linear就会报错,因为Linear要求至少二维。

第三步,检查batch维度是否丢失。如果你用x[0]取了一个样本,形状从(16, 10)变成(10,),也会报错。正确做法是用x[0:1]保持二维。

6.2 梯度为None或参数未更新的原因

如果你发现loss.backward()之后某些参数的.grad是None,通常是因为这些参数没有参与前向计算。比如你定义了一个层但forward里没用它,那它就没有梯度。练习题里如果要求你实现残差连接但写错了,就会出现这种情况。

另一个原因是你在torch.no_grad()块里做了前向,这样计算图不会构建,自然没有梯度。检查一下是不是把训练循环写在了no_grad里面。

6.3 损失不下降的五个常见原因

  1. 学习率过大或过小。
  2. 忘记清空梯度。
  3. 数据没有归一化,特征尺度差异太大。
  4. 模型太简单或太复杂,欠拟合或过拟合。
  5. 标签和输出形状不匹配,广播机制导致损失计算错误。

排查顺序建议从后往前:先确认形状和数据类型,再检查梯度流程,最后调学习率。这样能最快定位问题。

7. 从练习题到实际项目:几个值得养成的习惯

做完第五章练习题,你算是把PyTorch的基础流程走通了。但练习题和实际项目之间还有一段距离。我分享几个自己踩过坑之后养成的习惯,供你参考。

第一个习惯:每个项目先写一个“过拟合小样本”的测试。拿10个样本,让模型训练到loss接近0。如果做不到,说明代码有bug,不用继续往下跑。这个技巧能帮你快速区分“代码问题”和“数据问题”。

第二个习惯:固定随机种子。torch.manual_seed(42)和torch.cuda.manual_seed(42)能让结果可复现。练习题如果要求你报告结果,固定种子是基本素养。

第三个习惯:保存和加载模型用state_dict,而不是整个模型对象。torch.save(model.state_dict(), 'model.pth')保存的是参数字典,加载时先实例化模型再load_state_dict。这样代码更灵活,也不容易因为类定义变化而加载失败。

第四个习惯:训练前先跑一个epoch的验证。确认模型在未训练时的表现符合预期,比如多分类任务的初始loss应该接近ln(num_classes)。如果差很远,说明初始化或损失函数有问题。

这些习惯在练习题阶段可能显得多余,但到了实际项目里,它们能帮你省下大量调试时间。第五章练习题的价值不在于题目本身,而在于它逼你把整个流程走一遍,形成肌肉记忆。等你真正做项目时,这些步骤应该是下意识就能写出来的。

最后说一个我自己的体会:PyTorch的入门难点从来不是某个API,而是把多个API正确组合成一个可运行的系统。第五章练习题就是这个组合能力的第一次考验。过了这一关,后面学CNN、RNN、Transformer,本质上都是在这个骨架上换零件。骨架稳了,换什么零件都不慌。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询