第 11 章 · 学习是怎么发生的

评估与数据

上一章反复提到“训练集、验证集、看指标”。这一章把这件事讲透:数据该怎么分、 模型好不好该怎么量(为什么只看“准确率”会被骗)、数据进模型前要怎么预处理, 以及训练不动时的一份排查清单。这是把模型做对、而不只是跑起来的关键一章。

读完这一章,你会明白

  • train / val / test 三个数据集各自的职责,以及“测试集不能碰”的铁律;
  • 划分数据时的隐藏坑:时间序列不能打乱、同组数据不能串台、训练与上线要同分布;
  • 数据少时怎么用交叉验证(含分层 K 折)把每一条数据都用足;
  • 为什么只看准确率会骗人,以及先立一个笨基线再谈高低;
  • 精确率 / 召回 / F1 / 混淆矩阵怎么读、怎么算,以及指标如何随判定门槛变化(ROC/PR、AUC);
  • 多分类(如 MNIST 10 类)的 macro / micro 平均是怎么回事;
  • 数据为什么要归一化,以及统计量只能从训练集算(否则泄漏);
  • 训练“不降 / 发散 / 过拟合”时的一份调试清单

1. 三个数据集:学、调、考

要诚实地评估泛化(第 10 章),必须把数据分成互不重叠的三份,各司其职:

训练集 train(~80%) val test

一种常见比例(80/10/10)。数据越多,val/test 的比例可以越小。关键是三份互不重叠

最隐蔽的坑:数据泄漏(data leakage)

如果测试集的信息偷偷进了训练(比如用全量数据算归一化参数、或同一张图既在训练又在测试), 你会得到一个虚高的分数,上线却翻车。预处理的统计量只能从训练集算,再套用到 val/test——这是纪律。

划分时最容易忽略的三个坑

“随机切三份”听着简单,但下面三种情况随机切就会偷偷泄漏,让分数虚高、上线打回原形:

2. 数据太少?交叉验证

数据很少时,单独切一大块当验证集太奢侈——训练数据本就不够,再挖走一块就更学不好。 K 折交叉验证把数据平分成 K 份,轮流拿其中 1 份当验证、其余 K−1 份训练,做 K 次, 再把 K 次成绩取平均。这样每条数据都当过一次“考题”,评估更稳、也更省数据;代价是要训 K 次,拿算力换可靠。

把数据分 5 份,每折换一块当验证,训 5 次取平均 第1折 第2折 第3折 第4折 第5折 验证 训练

5 折为例:每一折用不同的一块当验证(橙),其余训练(绿),最后把 5 次成绩取平均。

3. 只看“准确率”会骗人

准确率(accuracy) = 猜对的比例,直观,但在类别不平衡时会严重误导。举个例子:

一个“99% 准确率”的废物模型

假设 1000 笔交易里只有 10 笔是欺诈。有个模型偷懒,无脑判所有交易都正常—— 它的准确率是 990/1000 = 99%,看着很牛。可它一笔欺诈都没抓到,毫无用处。 准确率被“正常样本太多”给稀释了。这时就需要更细的指标。

先立一个“笨基线”,再谈高低

任何分数单看都没意义,得和一个零成本的笨办法比。分类可以用“永远猜最多的那一类”(majority), 回归可以用“永远猜平均值”。上面那个 99% 的模型,其实连“全猜正常”这个笨基线都没赢——所以 99% 一文不值。 先跑基线,你的模型至少要明显超过它,才谈得上“有用”。

4. 混淆矩阵、精确率、召回、F1

把“预测”和“实际”交叉列成一张 2×2 表,就是混淆矩阵。以“抓欺诈”为例(欺诈=阳性):

预测:欺诈预测:正常
实际:欺诈TP 真阳(抓对了)FN 漏报(放跑了)
实际:正常FP 误报(冤枉了)TN 真阴(放对了)

TP/FP/FN/TN 四个格子,是下面所有指标的原料。

精确率 = TPTP + FP     召回率 = TPTP + FN     F1 = 2 · 精确率 · 召回率精确率 + 召回率 precision:抓的里有多少是真的;recall:真的里抓到了多少;F1:两者的调和平均
查准和查全常常“按下葫芦浮起瓢”

把判定门槛调严,报得少而准(精确率↑),但会漏掉更多(召回率↓);调松则相反。选哪个更重要看场景: 抓欺诈/查癌症宁可误报也别漏(重召回);垃圾邮件宁可漏也别把重要邮件误杀(重精确)。

把上面的欺诈例子算到底

还是 1000 笔交易、10 笔欺诈。对比两个模型,看四个格子和三个指标各是多少:

模型TP/FP/FN/TN准确率精确率召回率F1
全猜正常(笨基线)0 / 0 / 10 / 99099%未定义00
真做事的模型8 / 20 / 2 / 97097.8%0.290.800.42

准确率更低的模型反而有用——它抓住了 10 个坏人里的 8 个。只看准确率就会选错。

第二行怎么来的:抓到 8 笔真欺诈(TP=8)、漏了 2 笔(FN=2)、误报 20 个好人(FP=20)、其余 970 笔放对(TN=970)。代入公式: 精确率 = 8/(8+20) ≈ 0.29(报警里大多是虚惊),召回率 = 8/(8+2) = 0.80(十个坏人抓到八个), F1 = 2·(0.29·0.80)/(0.29+0.80) ≈ 0.42。抓欺诈更看重召回,这个模型合格;嫌误报太多,再想办法提精确率。

指标会随“判定门槛”变:ROC 与 PR 曲线

模型其实输出的是一个概率(“这笔有 0.7 的概率是欺诈”),报不报警取决于你设的门槛。 门槛调高:报得少而准(精确率↑、召回↓);门槛调低:宁可错杀(召回↑、精确率↓)。 所以精确率、召回率从来不是一个死数,而是随门槛滑动的一条曲线:

一句话:报一个指标前先想清楚它是在哪个门槛下算的;要比较模型的整体能力,用 AUC 这种“扫遍所有门槛”的数更公平。

多分类怎么办(如 MNIST 10 类)

上面是二分类。像 MNIST 认 0~9 有 10 类,混淆矩阵就变成 10×10:对角线是认对的,非对角线告诉你谁被认成了谁(比如 4 常被认成 9)。 精确率/召回率改成逐类各算一份,再汇总成一个数,常见两种汇总:

本仓库 MNIST demo 报的是总体准确率(约 97.8%,第 23 章),因为 10 个数字大致均衡; 一旦类别不均衡,就该看 macro 平均或逐类召回,才不会被大类的高分掩盖了小类的糟糕表现。

换个任务就换指标:回归(预测数值)看 MSE / MAE(第 4 章);语言模型看困惑度(第 19 章)。 指标要配任务选,没有万能的一个数。

5. 数据预处理:先把数据“摆正”

原始数据往往量纲乱七八糟(有的特征 0~1,有的 0~10000)。直接喂进去,大数值特征会主导梯度、训练很不稳。 归一化 / 标准化就是把各特征缩放到差不多的尺度。最常见两种:

src/demo/mnist/mnist_data.h(思路)
// 像素 0~255 归一化到 [0,1] 再喂进网络
pixel_value = raw_pixel / 255.0;                1
  1. 本仓库的 MNIST 就把像素除以 255,压到 [0,1](不是二值化)。optimizer_bench 也共用这份预处理。归一化后各特征尺度一致,训练更稳、更快。

关键纪律:统计量只能从训练集算

归一化/标准化要用到一些统计量(最小值、最大值、均值、标准差)。这些数只能在训练集上算, 然后把同一组数原样套到 val/test 上——而不是各自重新算。

用“全量数据”算均值 = 数据泄漏

要是图省事,拿训练 + 测试合在一起算均值和标准差,测试集的信息就顺着这几个统计量进了训练, 分数虚高、上线原形毕露。正确姿势:μ、σ ← 只用训练集,再对 val/test 做 (x − μ) / σ。 这正是第 1 节那条泄漏铁律最常见的一次翻车。

归一化还是标准化?怎么选

6. 训练不动?一份排查清单

真上手训练,十有八九会遇到“不对劲”。按这个顺序排查,能解决大部分问题:

黄金第一步:先过拟合一小撮

10 条样本让模型死记到损失≈0。如果连这都做不到,基本是代码/数据管道有 bug, 而不是“模型不够强”。这一招能帮你在浪费大量算力前,先揪出低级错误。

小结

  • train/val/test:学 / 调 / 考,三份互不重叠;测试集全程不能碰,当心数据泄漏。
  • 划分小心三个坑:时间序列不打乱、同组不串台、训练与上线同分布
  • 数据少用 K 折 / 分层 K 折,每条数据都当一次考题。
  • 先立笨基线;准确率在不平衡时会骗人,用混淆矩阵推出精确率、召回、F1,并记得它们随门槛变(ROC/PR、AUC)。
  • 指标要配任务:分类看 P/R/F1(多类看 macro/micro),回归看 MSE,语言模型看困惑度。
  • 喂数据前先归一化(MNIST 除以 255),统计量只从训练集算;训练不动就按清单排查,先“过拟合一小撮”验证管道。

到这里,监督学习“如何训练好一个网络”的完整功夫就齐了。但学习不止监督这一种—— 下一章我们认识一种很不一样的范式:强化学习,没有标准答案,只靠“奖励”在试错里学策略; 它也是日后对齐大模型(RLHF)的底子。之后再进入第三部分,认识 CNN、RNN 这些“专才”结构。