第 12 章 综合实践:用 PyTorch 解决真实问题
学习目标
- 综合运用第 1~11 章:数据、模型、训练、评估、保存
- 能独立完成一个完整的回归项目并解读结果
- 学会审查、验证、修复 AI 生成的 PyTorch 代码
- 掌握综合项目的验收标准与自查清单
12.1 本章定位
前 11 章是零件,本章把它们装成一台机器。完整项目的固定流程:
准备数据 → 划分训练/验证集 → 搭模型 → 选损失与优化器 →
训练循环(分批、五步)→ 每轮评估 → 保存最优模型 → 最终评估本书的核心理念之一是「能读懂、验证 AI 生成的代码」。AI 能写出 90% 正确的 PyTorch 代码,剩下 10% 的坑(标签类型、设备、zero_grad、形状)需要你作为工程师来把关——这正是本书训练的能力。
12.2 完整示范:房价预测
合成数据:房价由「面积、房间数」决定,价格 = 3×面积 + 10×房间数 + 5 + 噪声。目标:训练 MLP,让验证集 RMSE 接近噪声水平(8 左右)。
python
import math
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
torch.manual_seed(7)
n = 400
area = torch.rand(n) * 100 + 20
rooms = torch.randint(1, 5, (n,)).float()
X = torch.stack([area, rooms], dim=1)
price = 3.0 * area + 10.0 * rooms + 5.0 + 8.0 * torch.randn(n)
perm = torch.randperm(n)
X, price = X[perm], price[perm]
X_train, y_train = X[:320], price[:320]
X_val, y_val = X[320:], price[320:]
train_loader = DataLoader(TensorDataset(X_train, y_train),
batch_size=32, shuffle=True)
model = nn.Sequential(
nn.Linear(2, 16),
nn.ReLU(),
nn.Linear(16, 1),
)
loss_fn = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
def evaluate_rmse(model, X, y):
model.eval()
with torch.no_grad():
pred = model(X).reshape(-1)
return math.sqrt(loss_fn(pred, y).item())
for epoch in range(201):
model.train()
for batch_x, batch_y in train_loader:
loss = loss_fn(model(batch_x).reshape(-1), batch_y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 50 == 0:
print(f"epoch {epoch}: val RMSE = {evaluate_rmse(model, X_val, y_val):.3f}")
torch.save(model.state_dict(), "house_model.pt")输出:
epoch 0: val RMSE = 223.305
epoch 50: val RMSE = 10.136
epoch 100: val RMSE = 8.361
epoch 150: val RMSE = 8.361
epoch 200: val RMSE = 8.417解读:
- 初始 RMSE 223 说明模型「什么都不会」;50 轮后降到 10,说明学到了规律;
- 数据噪声标准差是 8,所以 RMSE ≈ 8.4 已经是「尽可能好」——误差来自数据本身,不是模型没学好;
- 之后轮次 RMSE 不再下降是正常的:模型已收敛。
一个工程细节:回归任务的输出是 (B, 1),标签是 (B,),所以对预测调 .reshape(-1) 对齐形状再算 MSE——形状不匹配时 MSELoss 会广播,悄悄算错,这是回归里最容易踩的坑。
12.3 审查 AI 生成的代码:一个实战案例
假设 AI 给了你这段分类训练代码,要求「训练 5 轮」:
python
import torch
import torch.nn as nn
logits = torch.randn(4, 2)
y = torch.tensor([0.0, 1.0, 0.0, 1.0])
loss = nn.CrossEntropyLoss()(logits, y)运行报错:
RuntimeError: expected target dtype to be Long or Byte, but got Float审查三步:
- 看报错:交叉熵的标签需要整数(Long 或 Byte),现在是浮点;
- 看数据:
y是[0.0, 1.0, ...],浮点; - 修复:标签用整数
torch.tensor([0, 1, 0, 1]),或统一.long()。
修复后:
python
y = torch.tensor([0, 1, 0, 1])
loss = nn.CrossEntropyLoss()(logits, y)
print(loss.item())能跑通。这 10% 的「AI 常错点」就是你要盯的地方。审查任何 AI 生成的训练代码,按下面清单逐条过:
AI 代码审查清单
12.4 综合项目(三选一)
从下面三个主题中任选一个,独立完成。也可以自拟同等难度的主题,但必须先按 12.5 的清单自评。
主题 A:温度预测(回归)
- 合成数据:1000 天的「最高气温」,由
sin周期 + 线性趋势 + 噪声生成(自己定公式); - 特征:第几天(和/或前一天的天气);标签:当天气温;
- 训练 MLP,输出验证集 RMSE 与训练曲线;
- 用训练好的模型预测「未来 30 天」并打印。
主题 B:图像二分类(卷积)
- 合成数据:400 张 28×28 灰度图,类别 0 为「左半边亮」、类别 1 为「右半边亮」;
- 用第 10 章的 TinyCNN 训练,每轮打印 train/val 准确率;
- 保存最优
state_dict,加载后输出最终验证准确率; - 至少一张「测试图」的预测类别与真实类别对比。
主题 C:审查并修复 AI 生成的代码
- 让 AI 生成一段「用 DataLoader 训练 CNN 做分类」的完整代码(或使用下面附的代码);
- 故意保留其中 3 个错误(如漏
zero_grad、标签未转 long、设备不一致); - 逐条定位、修复,并解释每个错误的报错信息与原因;
- 修复后完整训练,输出验证准确率。
附:主题 C 的起始代码(含 3 处错误):
python
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
torch.manual_seed(0)
X = torch.randn(200, 1, 8, 8)
y = (X.mean(dim=(2, 3)) > 0).long().reshape(-1)
loader = DataLoader(TensorDataset(X, y), batch_size=32, shuffle=True)
model = nn.Sequential(
nn.Flatten(),
nn.Linear(8 * 8, 2),
)
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
for epoch in range(5):
for batch_x, batch_y in loader:
loss = nn.CrossEntropyLoss()(model(batch_x), batch_y.float())
loss.backward()
optimizer.step()提示:至少两个错误会让训练直接报错或完全不更新参数。
统一验收标准(全部满足才算通过):
| 标准 | 说明 |
|---|---|
| 可运行 | 按注释可一键运行,无语法错误 |
| 有注释 | 关键函数有注释,说明「为什么」 |
| 有容错 | 空数据、缺失值、异常值不会导致程序崩溃 |
| 结构清晰 | 用函数组织代码,没有大段重复 |
| 有评估 | 训练/验证集划分明确,输出评估指标 |
| 可复现 | 随机种子固定,重跑结果一致 |
12.5 自查清单(提交前逐条打勾)
- 数据划分是否用了
torch.randperm且固定了种子? - 训练循环是否五步齐全且顺序正确(
zero_grad在backward前)? - 损失函数与任务类型匹配?标签 dtype 正确?
- 评估是否
model.eval()+torch.no_grad()? - 是否有保存最优模型并成功加载验证?
- 打印的关键指标(损失、RMSE/准确率)能否讲清含义?
- 如果用了 GPU,模型和数据是否都在同一
device?
章末练习
基础
- 运行 12.2 的完整示例,记录 RMSE 从 epoch 0 到 200 的变化,并解释为什么停在 8 附近。
- 把 12.2 的
lr改为 0.5 重跑,观察 RMSE 曲线,记录现象。 - 运行 12.3 的报错示例,把
y改成整数后确认能跑通。
提高
- 在 12.2 中增加「保存最优模型」:每个 epoch 评估一次,验证 RMSE 创新低时保存,训练结束加载对比。
- 用 12.3 的审查清单检查你自己写过的第 8~11 章代码,列出至少 2 处之前没注意的问题。
挑战
- 完成 12.4 中任选一个综合项目,并按 12.5 自查清单逐条自评。
- 自己构造一个「AI 代码」,故意埋 3 个训练类错误,请同学用审查清单找出;再交换角色。
章末自测
每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。
- 回归任务标签与预测的形状处理,正确做法是?
- A. 直接用
(B, 1)与(B,)相减 - B. 预测
reshape(-1)后与(B,)标签计算 MSE - C. 把标签
unsqueeze(0) - D. 转成整数
- A. 直接用
CrossEntropyLoss对浮点标签会?- A. 正常计算
- B. 报
RuntimeError: expected target dtype to be Long or Byte - C. 自动转整数
- D. 报
ValueError
- 训练五步的正确顺序是?
- A. backward → zero_grad → step
- B. forward → loss → zero_grad → backward → step
- C. zero_grad → forward → backward → step → loss
- D. step → backward → zero_grad
- 评估模型必须?
- A.
model.train() - B.
model.eval()+torch.no_grad() - C. 只调
no_grad - D. 都不需要
- A.
- 验证集 RMSE 不再下降,可能的原因是?
- A. 模型坏了
- B. 已收敛,误差接近数据噪声水平
- C. 学习率一定太大
- D. 一定需要换模型
- 保存模型参数的标准写法是?
- A.
model.save("m.pt") - B.
torch.save(model.state_dict(), "m.pt") - C.
torch.save(model, "m.pt")(也能用,但不推荐) - D. B 与 C 都可,推荐 B
- A.
- 审查 AI 生成的代码,正确的态度是?
- A. 直接使用并信任
- B. 理解、验证、修改后再使用
- C. 一律不用
- D. 只看注释
- 修复「CPU 张量 + GPU 模型」报错的方法是?
- A. 把模型和数据都
.to(device) - B. 把报错忽略
- C. 只迁移数据
- D. 只迁移模型
- A. 把模型和数据都
- 固定随机种子是为了?
- A. 让训练更快
- B. 结果可复现
- C. 提升准确率
- D. 必须设置,否则报错
- 综合项目验收时,下列哪项不是必须满足的?
- A. 可运行、有注释
- B. 有评估指标与数据集划分
- C. 必须使用 GPU
- D. 可复现
