第 11 章 训练、评估与保存
学习目标
- 理解训练集与验证集划分的意义
- 掌握
model.train()与model.eval()的区别 - 掌握
torch.no_grad()下的评估与准确率计算 - 掌握
torch.save/load_state_dict保存和恢复模型 - 掌握设备管理:
device与.to(device)
11.1 为什么要把数据分成训练集和验证集
前面的例子都用全部数据训练,再用同一批数据算准确率——这只能说明「模型记住了数据」,不能说明「模型学会了规律」。真实做法是划分数据集:
- 训练集:训练时更新参数;
- 验证集(validation set):训练中每轮检查模型对没见过数据的表现。
验证集准确率接近训练集,说明模型泛化良好;验证集远差于训练集,说明过拟合(背数据)。先把数据打乱,再按比例切开:
import torch
torch.manual_seed(42)
n = 600
x0 = torch.randn(n // 2, 2) + torch.tensor([-1.5, 0.0])
x1 = torch.randn(n // 2, 2) + torch.tensor([1.5, 0.0])
X = torch.cat([x0, x1])
y = torch.cat([torch.zeros(n // 2), torch.ones(n // 2)]).long()
perm = torch.randperm(n) # 随机顺序
X, y = X[perm], y[perm]
n_train = 420
X_train, y_train = X[:n_train], y[:n_train]
X_val, y_val = X[n_train:], y[n_train:]
print("训练集:", X_train.shape, "验证集:", X_val.shape)输出:
训练集: torch.Size([420, 2]) 验证集: torch.Size([180, 2])torch.randperm(n) 生成 0~n-1 的随机排列,用它对数据洗牌,保证两类样本均匀混入两个集合。
11.2 model.train() 与 model.eval()
模型有两种模式:
model.train():训练模式。启用 Dropout、BatchNorm 等层的行为;model.eval():评估模式。关掉随机行为,用稳定的参数推理。
没有 Dropout/BatchNorm 的模型(如本书的 MLP、CNN)两种模式计算结果相同,但养成习惯:训练循环开始前 model.train(),评估前 model.eval()。等以后用 Dropout 时,漏调 eval() 会让评估结果随机抖动。
11.3 no_grad 下的评估
评估(推理)不需要梯度:不用 backward,也不需要保存计算图。用 torch.no_grad() 包住,既省内存又更快(第 6 章)。
写一个可复用的评估函数:
import torch
def evaluate(model, X, y):
model.eval()
with torch.no_grad():
logits = model(X)
acc = (logits.argmax(1) == y).float().mean().item()
return acclogits.argmax(1) 取每个样本分数最高的类别,与真实标签比较,平均后就是准确率。
11.4 完整示例:训练 + 每轮评估
用 2 维合成数据(两个有重叠的高斯簇)训练一个 MLP,每轮同时打印训练集和验证集准确率:
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
torch.manual_seed(42)
n = 600
x0 = torch.randn(n // 2, 2) * 1.0 + torch.tensor([-1.5, 0.0])
x1 = torch.randn(n // 2, 2) * 1.0 + torch.tensor([1.5, 0.0])
X = torch.cat([x0, x1])
y = torch.cat([torch.zeros(n // 2), torch.ones(n // 2)]).long()
perm = torch.randperm(n)
X, y = X[perm], y[perm]
X_train, y_train = X[:420], y[:420]
X_val, y_val = X[420:], y[420:]
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, 2),
)
loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
def evaluate(model, X, y):
model.eval()
with torch.no_grad():
logits = model(X)
acc = (logits.argmax(1) == y).float().mean().item()
return acc
for epoch in range(20):
model.train()
for batch_x, batch_y in train_loader:
logits = model(batch_x)
loss = loss_fn(logits, batch_y)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 4 == 0 or epoch == 19:
train_acc = evaluate(model, X_train, y_train)
val_acc = evaluate(model, X_val, y_val)
print(f"epoch {epoch}: train_acc={train_acc:.3f} val_acc={val_acc:.3f}")输出:
epoch 0: train_acc=0.924 val_acc=0.917
epoch 4: train_acc=0.943 val_acc=0.928
epoch 8: train_acc=0.943 val_acc=0.922
epoch 12: train_acc=0.943 val_acc=0.928
epoch 16: train_acc=0.943 val_acc=0.928
epoch 19: train_acc=0.945 val_acc=0.928两个簇有重叠,理论上限约 0.93,所以验证集在 0.93 附近不再上涨是正常的——不是没训好,是数据本身可分性有限。
11.5 保存与加载模型
训练结束后,把模型参数存到磁盘,下次直接用:
torch.save(model.state_dict(), "model.pt")state_dict() 是「参数名 → 参数张量」的字典,保存它(而不是整个模型对象)是标准做法。加载时先建一个结构完全相同的模型,再把参数填进去:
model2 = nn.Sequential(
nn.Linear(2, 16),
nn.ReLU(),
nn.Linear(16, 2),
)
model2.load_state_dict(torch.load("model.pt", weights_only=True))
print("加载后验证集准确率:", evaluate(model2, X_val, y_val))输出:
加载后验证集准确率: 0.9277777671813965加载后的模型与训练完的模型表现一致。weights_only=True 是安全选项:只允许加载张量,防止恶意文件在加载时执行任意代码,加载不可信来源的模型文件时务必带上。
注意:保存的文件不包含模型结构。结构不同就加载会报错(见常见错误),所以要么保存 state_dict 时顺手把结构写在代码里,要么保存整个模型对象 torch.save(model, "model.pt")(不推荐,兼容性差)。
11.6 设备管理:CPU 与 GPU
第 1 章提过 device。通用写法是「有 CUDA 用 CUDA,没有用 CPU」:
import torch
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(device)在本机输出:
cuda无 GPU 的机器输出 cpu。把模型和张量搬到设备上:
model.to(device)
x = torch.ones(2).to(device)
print(x.device)在本机输出:
cuda:0规则:参与同一运算的张量和模型必须在同一设备。CPU 张量与 GPU 模型相乘会报 RuntimeError: Expected all tensors to be on the same device。训练脚本的标准做法:定义 device 后,对模型和数据都调用 .to(device)。
动手实践
- 把 11.4 的完整示例复制运行,记录每轮的 train_acc 与 val_acc,说明两者为什么接近。
- 在 11.4 里故意删掉
model.eval()或torch.no_grad(),观察运行是否仍然正确,解释这样做的隐患。 - 训练后保存
state_dict,新建相同结构模型加载,打印验证集准确率确认一致。 - 故意把加载用的模型结构改成
nn.Linear(2, 8)(层维度不同),运行load_state_dict,记录报错并解释。 - 在代码开头加上
device判断,把模型和一批数据搬到 GPU(或 CPU)上训练,验证x.device与模型参数设备一致。
常见错误
| 错误写法 | 现象 | 原因 |
|---|---|---|
评估时忘了 model.eval() | 有 Dropout 时结果随机抖动 | 评估必须切到 eval 模式 |
评估时忘了 torch.no_grad() | 内存上涨、速度慢 | 推理不需要计算图 |
load_state_dict 但结构不同 | RuntimeError: Error(s) in loading state_dict ... size mismatch | 加载模型的层必须与保存时完全一致 |
| 保存整个模型对象再用 | 反序列化慢、升级后不兼容 | 用 state_dict 保存参数 |
| CPU 张量喂给 GPU 模型 | RuntimeError: Expected all tensors to be on the same device | 数据与模型都要 .to(device) |
| 标签没转 long | 交叉熵报 dtype 错误 | 分类标签用 .long() |
章末练习
基础
- 用
torch.randperm把 100 个样本打乱,取前 70 个做训练集、后 30 个做验证集,打印两个集合的形状。 - 写一个
evaluate函数(接收 model、X、y,返回准确率),并说明为什么要包torch.no_grad()。 - 训练一个简单模型,保存
state_dict到model.pt,再加载并输出验证准确率。
提高
- 解释
model.train()与model.eval()的区别,并说明为什么评估时必须用eval()。 - 把 11.4 的训练轮数改成 200,观察 train_acc 与 val_acc 的差距是否拉大;结合「过拟合」概念解释你看到的现象。
挑战
- 在 11.4 里加一个「最佳验证准确率」跟踪:每轮评估后,如果比历史最好还高,就保存
state_dict;训练结束后加载最优模型再评估,并与最后一个 epoch 的模型对比。 - 写一个脚本:在 CPU 上训练 5 轮计时,再把模型和数据搬到 GPU 上训练 5 轮计时(本机有 CUDA 时),比较耗时并解释差异。
章末自测
每题选择一个最佳答案。本书不附答案:完成后交由老师或 AI 老师批改讲解。
- 验证集的作用是?
- A. 参与训练更新参数
- B. 检查模型对没见过数据的表现
- C. 让模型记住更多数据
- D. 替代训练集
- 评估模型前应该调用?
- A.
model.train() - B.
model.eval() - C.
model.backward() - D.
model.step()
- A.
- 推理时包
torch.no_grad()的目的是?- A. 让结果更准
- B. 不建计算图,省内存提速
- C. 必须包,否则报错
- D. 让模型不可训练
- 计算分类准确率时,
logits.argmax(1)返回的是?- A. 每个样本分数最大的类别
- B. 每个样本的分数
- C. 损失值
- D. 概率分布
- 保存模型参数的标准做法是?
- A.
torch.save(model, "model.pt") - B.
torch.save(model.state_dict(), "model.pt") - C.
model.save("model.pt") - D.
torch.save(model.parameters(), "model.pt")
- A.
- 加载
state_dict前,必须先?- A. 什么都不用做
- B. 创建结构完全相同的模型
- C. 把模型训练一轮
- D. 调用
model.eval()
- 加载结构不同的模型会?
- A. 自动适配
- B. 报
RuntimeError: size mismatch - C. 静默丢弃多余参数
- D. 训练后自动修复
torch.load("model.pt", weights_only=True)中weights_only=True的作用是?- A. 只加载参数,防止恶意代码执行
- B. 加载更快
- C. 只加载结构
- D. 压缩文件
- CPU 张量与 GPU 模型运算会?
- A. 自动迁移
- B. 报
RuntimeError: Expected all tensors to be on the same device - C. 静默出错
- D. 结果正确
- 通用设备判断写法是?
- A.
device = torch.device("gpu") - B.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") - C.
device = torch.cuda.device() - D.
device = "gpu"
- A.
