模型评估与验证集
本教程共 60 篇 · 第 17 篇 · 更新于 2026-08-17 · 约 4 分钟阅读
本节目标:搞懂为什么训练集上分数高不算数,学会用验证集判断模型的真实水平,并识别过拟合信号。
训练得好,不等于考得好
前面我们写完训练循环,就盯着训练 loss 一路下降。停一下,这里有个陷阱。
loss 下降只说明模型在训练数据上表现变好。它可能只是在「背答案」,而不是真的学会了规律。就像学生把课本背得滚瓜烂熟,换一套题就露馅。
怎么识破?拿模型没见过的数据考它。这就是验证集(validation set)存在的意义。
三份数据:训练、验证、测试
正规流程要把数据切成三份:
- 训练集(training set):用来更新模型参数,就是训练循环里用的那份
- 验证集(validation set):训练过程中定期「小测」,帮我们调超参数、判断过拟合
- 测试集(test set):训练全部结束后只碰一次,用来汇报模型的最终水平
三者的关系,像练习册、模拟考和高考。练习册天天刷,模拟考用来查漏补缺,高考成绩才是最终答卷。
Warning测试集只能碰一次。拿测试集反复调参,它就变成了「见过的题」,最终分数会虚高。
常见比例是 8:2 或 7:3,数据量大时验证集比例可以更小。划分数据用 random_split 很方便:
from torch.utils.data import random_split
dataset = ... # 你的完整数据集
train_size = int(0.8 * len(dataset))
val_size = len(dataset) - train_size
train_set, val_set = random_split(dataset, [train_size, val_size])
Warning划分前先确认数据集顺序是打乱的。有些数据集按类别排列,直接切前 80% 当训练集,验证集里可能全是一个类别,评估结果就废了。
评估的标准姿势
一轮完整的评估,按顺序做这几步:
- 切到评估模式:
model.eval() - 用
torch.no_grad()包住循环,关掉梯度追踪 - 遍历验证集,累加预测结果
- 算指标:准确率、平均 loss
验证和训练有两个关键区别:关掉梯度、切换模式。
model.eval() # 切到评估模式
with torch.no_grad(): # 不追踪梯度
for inputs, labels in val_loader:
outputs = model(inputs)
...
为什么要这样?第 16 章提过,Dropout 和 BatchNorm 在两种模式下行为不同。忘了调 model.eval(),验证分数会莫名变差。torch.no_grad() 则省显存、跑得快,验证不需要梯度。
准确率怎么算
分类任务最直观的指标是准确率(accuracy):预测对的样本数除以总样本数。
correct = 0
total = 0
with torch.no_grad():
for inputs, labels in val_loader:
outputs = model(inputs)
_, predicted = torch.max(outputs, 1) # 取分数最高的类别
total += labels.size(0)
correct += (predicted == labels).sum().item()
print(f"准确率: {100 * correct / total:.2f}%")
torch.max(outputs, 1) 返回两个东西:最大值和它的下标。_ 是占位符,我们只要下标,那就是预测的类别。
Note分类网络输出的是每个类别的分数,不是直接给出答案。取分数最高的下标当预测,这个模式后面会反复出现。
准确率之外,验证 loss 同样值得记下来。它和训练 loss 一起看,才构成完整的故事。单独盯着训练 loss,只能看到模型对自己人有多熟。
看懂两条 loss 曲线
把训练 loss 和验证 loss 画在一起,能读出模型状态。四种典型情况:
- 两条都降:正常,模型在学真东西
- 训练降、验证升:过拟合(overfitting),模型开始背答案
- 两条都不降:欠拟合(underfitting),模型太简单或学习率不对
- 曲线剧烈震荡:学习率太大,或 batch 太小
Note过拟合的信号是「验证 loss 掉头上升」。拐点一出现,就该停手或加正则化,具体做法第 27 章细讲。
画曲线时有个小技巧:每个 batch 的 loss 跳来跳去,直接画像噪声。把每 50 个 batch 的平均值画上去,曲线会平滑得多,趋势一眼可见。
Tip这些曲线还可以写进 TensorBoard,看交互式图表。第 30 章会讲怎么用
SummaryWriter把标量、图像都记录进去。
早停:见好就收
验证 loss 先降后升,最省事的办法就是盯住它:不降了就停,顺手把最好的那版模型存下来。这叫早停(early stopping)。
best_loss = float('inf')
patience = 5
wait = 0
for epoch in range(100):
val_loss = evaluate() # 你自己实现的验证函数,返回平均验证 loss
if val_loss < best_loss:
best_loss = val_loss
wait = 0
torch.save(model.state_dict(), "best.pt") # 存最佳模型
else:
wait += 1
if wait >= patience:
print(f"早停:验证 loss 连续 {patience} 轮没降")
break
patience 是容忍度:连续几轮没改善就停。设太小容易误停,太大浪费时间,一般取 5 到 10。
实际项目里,很多人不等早停,而是每轮都存模型,训练结束后挑验证分数最高的那版用。思路都一样:最终上线的是「验证集上表现最好的模型」,而不是「训练最后一轮的模型」。
混淆矩阵:错在哪一类
准确率只给一个总数,看不出模型在哪些类别上犯迷糊。混淆矩阵(confusion matrix)把真实类别和预测类别交叉统计,一眼看清。
拿猫狗二分类举例,矩阵有四块区域:预测对的两块在对角线上,剩下的两块分别是「把猫认成狗」和「把狗认成猫」。对角线越大越好,说明分得对。
用 sklearn 算很快:
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_true, y_pred)
print(cm) # 行是真实类别,列是预测类别
Tip类别不平衡时准确率会骗人。比如数据里 99% 是猫,全猜猫也有 99% 的准确率。这时候要去看精确率、召回率,后面章节再展开。
常见坑速查
- 忘写
model.eval():Dropout 还在随机丢神经元,验证分数偏低且每次不一样 - 验证时没包
no_grad():慢、费显存,极端情况会把显存撑爆 - 拿测试集当验证集调参:最终汇报的分数虚高,到了真实环境露馅
- 只记准确率不记 loss:准确率不涨但 loss 在降时,模型其实还在学,别急着停
小结
模型评估就三句话:数据分三份,评估关梯度,盯着验证 loss 找拐点。测试集只碰一次,剩下的交给正则化和调参技巧。