Skip to content

第 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

审查三步:

  1. 看报错:交叉熵的标签需要整数(Long 或 Byte),现在是浮点;
  2. 看数据:y[0.0, 1.0, ...],浮点;
  3. 修复:标签用整数 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 自查清单(提交前逐条打勾)

  1. 数据划分是否用了 torch.randperm 且固定了种子?
  2. 训练循环是否五步齐全且顺序正确(zero_gradbackward 前)?
  3. 损失函数与任务类型匹配?标签 dtype 正确?
  4. 评估是否 model.eval() + torch.no_grad()?
  5. 是否有保存最优模型并成功加载验证?
  6. 打印的关键指标(损失、RMSE/准确率)能否讲清含义?
  7. 如果用了 GPU,模型和数据是否都在同一 device?

章末练习

基础

  1. 运行 12.2 的完整示例,记录 RMSE 从 epoch 0 到 200 的变化,并解释为什么停在 8 附近。
  2. 把 12.2 的 lr 改为 0.5 重跑,观察 RMSE 曲线,记录现象。
  3. 运行 12.3 的报错示例,把 y 改成整数后确认能跑通。

提高

  1. 在 12.2 中增加「保存最优模型」:每个 epoch 评估一次,验证 RMSE 创新低时保存,训练结束加载对比。
  2. 用 12.3 的审查清单检查你自己写过的第 8~11 章代码,列出至少 2 处之前没注意的问题。

挑战

  1. 完成 12.4 中任选一个综合项目,并按 12.5 自查清单逐条自评。
  2. 自己构造一个「AI 代码」,故意埋 3 个训练类错误,请同学用审查清单找出;再交换角色。

章末自测

每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。

  1. 回归任务标签与预测的形状处理,正确做法是?
    • A. 直接用 (B, 1)(B,) 相减
    • B. 预测 reshape(-1) 后与 (B,) 标签计算 MSE
    • C. 把标签 unsqueeze(0)
    • D. 转成整数
  2. CrossEntropyLoss 对浮点标签会?
    • A. 正常计算
    • B. 报 RuntimeError: expected target dtype to be Long or Byte
    • C. 自动转整数
    • D. 报 ValueError
  3. 训练五步的正确顺序是?
    • A. backward → zero_grad → step
    • B. forward → loss → zero_grad → backward → step
    • C. zero_grad → forward → backward → step → loss
    • D. step → backward → zero_grad
  4. 评估模型必须?
    • A. model.train()
    • B. model.eval() + torch.no_grad()
    • C. 只调 no_grad
    • D. 都不需要
  5. 验证集 RMSE 不再下降,可能的原因是?
    • A. 模型坏了
    • B. 已收敛,误差接近数据噪声水平
    • C. 学习率一定太大
    • D. 一定需要换模型
  6. 保存模型参数的标准写法是?
    • A. model.save("m.pt")
    • B. torch.save(model.state_dict(), "m.pt")
    • C. torch.save(model, "m.pt")(也能用,但不推荐)
    • D. B 与 C 都可,推荐 B
  7. 审查 AI 生成的代码,正确的态度是?
    • A. 直接使用并信任
    • B. 理解、验证、修改后再使用
    • C. 一律不用
    • D. 只看注释
  8. 修复「CPU 张量 + GPU 模型」报错的方法是?
    • A. 把模型和数据都 .to(device)
    • B. 把报错忽略
    • C. 只迁移数据
    • D. 只迁移模型
  9. 固定随机种子是为了?
    • A. 让训练更快
    • B. 结果可复现
    • C. 提升准确率
    • D. 必须设置,否则报错
  10. 综合项目验收时,下列哪项不是必须满足的?
    • A. 可运行、有注释
    • B. 有评估指标与数据集划分
    • C. 必须使用 GPU
    • D. 可复现