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

评估与数据

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

读完这一章,你会明白

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

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

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

用“备考”理解三份数据

训练集 = 平时做的练习册(对着答案反复练,把知识学进去); 验证集 = 模拟考(不对答案训练,只用来看自己练得怎么样、要不要换套复习策略); 测试集 = 真正的高考(只考一次、平时绝不能看)。 三份必须互不重叠,道理就像:拿练习册原题当模拟考,你只会高估自己;而高考题要是提前泄露,分数更是毫无意义。

验证集到底“怎么用”?——它在训练循环里的位置

光说“用来调”太虚。具体讲:训练是一轮一轮(epoch)进行的,验证集就穿插在每轮之间,形成一个“训练 → 自检 → 做决定”的小循环:

  1. 用训练集训一轮:反向传播更新参数(第 6 章)。只有这一步动参数
  2. 在验证集上跑一遍、只看不学:算出这轮的验证损失 / 准确率。不做反向传播、不更新任何参数——纯粹“考一下自己现在几斤几两”。
  3. 照这个验证分数做决定:验证损失还在降 → 继续训;第 10 章那条验证损失开始回升 → 该 early stop 了;想比较“学习率 0.1 还是 0.01 好”“3 层还是 5 层好” → 看谁的验证分数高就选谁。
  4. 回到第 1 步,继续下一轮。

所以验证集的作用一句话概括:它是训练过程里唯一“诚实的镜子”——参数照着训练集学,但“学得好不好、该不该停、超参怎么选”这些决定,都照着验证集来。它不直接改参数,却间接左右了你怎么训。

那为什么不能直接用训练集或测试集来“调”?

不能用训练集看: 模型正是照着训练集学的,拿它自我评估,就像“对着答案做过的题再考自己”,分数虚高、发现不了过拟合——训练损失一直降,你根本看不出它是不是在背书
不能用测试集看: 只要你照着某个数据集的分数反复调(调学习率、挑模型、决定早停),就等于让模型偷偷适应了这份数据——它就“不再新鲜”、失去了模拟真实世界的资格。测试集必须全程锁起来、只在最后考一次,才能给出可信的“真实水平”。
正因为“调”这个动作会污染数据,才必须专门牺牲一份 val 出来挨这个污染,把 test 保护干净。

训练集 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 节那条泄漏铁律最常见的一次翻车。

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

不止缩放:先处理缺失、异常、重复与标签错

归一化只是把干净数据“摆正”。可现实里的原始数据往往先是的:有空洞、有离谱的值、有重复、还有贴错的标签。 这些不先处理,再好的模型也是“垃圾进,垃圾出”。下面四类是最常见的脏,也是喂数据前该先扫一遍的清单:

脏在哪长什么样危害常用处理
缺失值某些特征是空的(没填年龄、传感器掉数)很多算法算不动;直接填 0 会被当成“真有个 0”而误导训练集的均值/中位数/众数填补,或额外加一列“是否缺失”标记
异常值 / 离群点年龄 999、金额是负数、单位填错大了 1000 倍把归一化的尺度整个带偏(一个超大值压扁其余),梯度也跟着乱先查是录错还是真极端:录错就改/删,真极端可截断(clip)或改用更耐受的标准化
重复样本同一条数据抄了好几遍、爬虫抓到镜像页让模型反复背同一条,还会在划分时“串台”造成泄漏按关键字段去重;近似重复(轻微改写)也要一并清掉
标签噪声猫的图被标成狗、评论情感标反等于拿错答案教模型,天花板被直接压低抽检标注质量、多人交叉标注取一致,或用模型找出“最可疑”的标签复核

四类脏数据:先扫一遍再谈建模。清洗做得好,常比换个更大的模型更划算。

怎么判「异常」和「近似重复」?给两个能上手的判据

异常值:最常用的是「离均值多少个标准差」——把偏离均值超过 3 个标准差的点先拎出来看(所谓 3σ 法则); 或用四分位距(IQR),落在 [Q1−1.5·IQR, Q3+1.5·IQR] 之外的算离群。判出来后别急着删,先分清是录错还是真极端
近似重复:完全一样的用哈希比对就行;只是「改了标题的转载」这种,要看两段文字的词/片段重合度(集合的交集比并集,即 Jaccard 相似度), 超过某个阈值(如 0.8)就当同一份——这套在大模型语料里就是 MinHash 近似去重,原理完全一样,只是规模大得多。

重复样本会顺手制造“数据泄漏”

如果同一条(或近似重复的)数据一份落进训练集、一份落进测试集,模型在测试时其实是在“考做过的原题”, 分数虚高、上线打回原形。所以去重要在切分之前做,别让重复的数据跨越 train/test 的墙

顺序很重要:先修脏、再切分、最后才算统计量

正确的流水线是:①去重、修异常、补缺失、核标签(把数据修干净)→ ②划分 train/val/test③只用训练集算均值/标准差等统计量,再套到三份上做归一化。顺序反了——比如先用全量数据算统计量或先归一化再切分——就会把测试集信息进训练,前功尽弃。

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

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

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

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

小结

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

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