第 11 章 · 学习是怎么发生的
评估与数据
上一章反复提到“训练集、验证集、看指标”。这一章把这件事讲透:数据该怎么分、 模型好不好该怎么量(为什么只看“准确率”会被骗)、数据进模型前要怎么预处理, 以及训练不动时的一份排查清单。这是把模型做对、而不只是跑起来的关键一章。
读完这一章,你会明白
- train / val / test 三个数据集各自的职责,以及“测试集不能碰”的铁律;
- 划分数据时的隐藏坑:时间序列不能打乱、同组数据不能串台、训练与上线要同分布;
- 数据少时怎么用交叉验证(含分层 K 折)把每一条数据都用足;
- 为什么只看准确率会骗人,以及先立一个笨基线再谈高低;
- 精确率 / 召回 / F1 / 混淆矩阵怎么读、怎么算,以及指标如何随判定门槛变化(ROC/PR、AUC);
- 多分类(如 MNIST 10 类)的 macro / micro 平均是怎么回事;
- 数据为什么要归一化,以及统计量只能从训练集算(否则泄漏);
- 原始数据的四类脏(缺失 / 异常 / 重复 / 标签错)怎么识别与处理,以及为什么去重要在切分前做;
- 训练“不降 / 发散 / 过拟合”时的一份调试清单。
1. 三个数据集:学、调、考
要诚实地评估泛化(第 10 章),必须把数据分成互不重叠的三份,各司其职:
- 训练集 train用来学——反向传播更新参数,只用它。
- 验证集 val用来调——边训边监控、选超参数(学习率、层数)、决定何时 early stop。它不参与更新参数,但你会“照着它做决定”。
- 测试集 test用来考——最终一考,模拟“真实世界的新数据”。训练和调参全程绝不能碰,否则等于提前看了考题,分数不作数。
训练集 = 平时做的练习册(对着答案反复练,把知识学进去); 验证集 = 模拟考(不对答案训练,只用来看自己练得怎么样、要不要换套复习策略); 测试集 = 真正的高考(只考一次、平时绝不能看)。 三份必须互不重叠,道理就像:拿练习册原题当模拟考,你只会高估自己;而高考题要是提前泄露,分数更是毫无意义。
验证集到底“怎么用”?——它在训练循环里的位置
光说“用来调”太虚。具体讲:训练是一轮一轮(epoch)进行的,验证集就穿插在每轮之间,形成一个“训练 → 自检 → 做决定”的小循环:
- 用训练集训一轮:反向传播更新参数(第 6 章)。只有这一步动参数。
- 在验证集上跑一遍、只看不学:算出这轮的验证损失 / 准确率。不做反向传播、不更新任何参数——纯粹“考一下自己现在几斤几两”。
- 照这个验证分数做决定:验证损失还在降 → 继续训;第 10 章那条验证损失开始回升 → 该 early stop 了;想比较“学习率 0.1 还是 0.01 好”“3 层还是 5 层好” → 看谁的验证分数高就选谁。
- 回到第 1 步,继续下一轮。
所以验证集的作用一句话概括:它是训练过程里唯一“诚实的镜子”——参数照着训练集学,但“学得好不好、该不该停、超参怎么选”这些决定,都照着验证集来。它不直接改参数,却间接左右了你怎么训。
不能用训练集看: 模型正是照着训练集学的,拿它自我评估,就像“对着答案做过的题再考自己”,分数虚高、发现不了过拟合——训练损失一直降,你根本看不出它是不是在背书。
不能用测试集看: 只要你照着某个数据集的分数反复调(调学习率、挑模型、决定早停),就等于让模型偷偷适应了这份数据——它就“不再新鲜”、失去了模拟真实世界的资格。测试集必须全程锁起来、只在最后考一次,才能给出可信的“真实水平”。
正因为“调”这个动作会污染数据,才必须专门牺牲一份 val 出来挨这个污染,把 test 保护干净。
一种常见比例(80/10/10)。数据越多,val/test 的比例可以越小。关键是三份互不重叠。
如果测试集的信息偷偷进了训练(比如用全量数据算归一化参数、或同一张图既在训练又在测试), 你会得到一个虚高的分数,上线却翻车。预处理的统计量只能从训练集算,再套用到 val/test——这是纪律。
划分时最容易忽略的三个坑
“随机切三份”听着简单,但下面三种情况随机切就会偷偷泄漏,让分数虚高、上线打回原形:
- 时间序列别打乱:预测股价、天气、下周销量这类,必须按时间切——拿过去训练、未来测试。若随机打乱,模型就在“用未来预测过去”,等于开卷考试。
- 同组数据别串台:同一个病人的多张片子、同一个用户的多条记录,要整组只放进一边。否则模型见过“张三的左手”,再考“张三的右手”就是作弊(group leakage)。
- 训练与上线要同分布:训练数据若和真实场景分布不一致(只用晴天照片训、却要在雨天用),线下再高、上线也崩。这叫分布偏移(distribution shift)。
2. 数据太少?交叉验证
数据很少时,单独切一大块当验证集太奢侈——训练数据本就不够,再挖走一块就更学不好。 K 折交叉验证把数据平分成 K 份,轮流拿其中 1 份当验证、其余 K−1 份训练,做 K 次, 再把 K 次成绩取平均。这样每条数据都当过一次“考题”,评估更稳、也更省数据;代价是要训 K 次,拿算力换可靠。
5 折为例:每一折用不同的一块当验证(橙),其余训练(绿),最后把 5 次成绩取平均。
- 分层 K 折(stratified):类别不平衡时用。切分时保持每折的类别比例和整体一致,免得某一折恰好一个欺诈样本都没有,评估失真。
- 留一法(LOO):K 取到“样本总数”那么大,每次只留 1 条当验证。极省数据但要训 N 次,只在数据极少时才划算。
- 什么时候别用:数据很多时直接单切一份 val 就够了,没必要训 K 次;时间序列要用“按时间滚动”的 CV,绝不能随机分折(理由见上一节的坑)。
3. 只看“准确率”会骗人
准确率(accuracy) = 猜对的比例,直观,但在类别不平衡时会严重误导。举个例子:
假设 1000 笔交易里只有 10 笔是欺诈。有个模型偷懒,无脑判所有交易都正常—— 它的准确率是 990/1000 = 99%,看着很牛。可它一笔欺诈都没抓到,毫无用处。 准确率被“正常样本太多”给稀释了。这时就需要更细的指标。
任何分数单看都没意义,得和一个零成本的笨办法比。分类可以用“永远猜最多的那一类”(majority), 回归可以用“永远猜平均值”。上面那个 99% 的模型,其实连“全猜正常”这个笨基线都没赢——所以 99% 一文不值。 先跑基线,你的模型至少要明显超过它,才谈得上“有用”。
4. 混淆矩阵、精确率、召回、F1
把“预测”和“实际”交叉列成一张 2×2 表,就是混淆矩阵。以“抓欺诈”为例(欺诈=阳性):
| 预测:欺诈 | 预测:正常 | |
|---|---|---|
| 实际:欺诈 | TP 真阳(抓对了) | FN 漏报(放跑了) |
| 实际:正常 | FP 误报(冤枉了) | TN 真阴(放对了) |
TP/FP/FN/TN 四个格子,是下面所有指标的原料。
- 精确率(查准):模型报“欺诈”的那些里,有多少真是欺诈。低了 = 老是误报冤枉好人。
- 召回率(查全):所有真欺诈里,被抓到了多少。低了 = 老是漏报放跑坏人。
- F1:精确率和召回率的调和平均,想“两个都不太差”时看它。
把判定门槛调严,报得少而准(精确率↑),但会漏掉更多(召回率↓);调松则相反。选哪个更重要看场景: 抓欺诈/查癌症宁可误报也别漏(重召回);垃圾邮件宁可漏也别把重要邮件误杀(重精确)。
把上面的欺诈例子算到底
还是 1000 笔交易、10 笔欺诈。对比两个模型,看四个格子和三个指标各是多少:
| 模型 | TP/FP/FN/TN | 准确率 | 精确率 | 召回率 | F1 |
|---|---|---|---|---|---|
| 全猜正常(笨基线) | 0 / 0 / 10 / 990 | 99% | 未定义 | 0 | 0 |
| 真做事的模型 | 8 / 20 / 2 / 970 | 97.8% | 0.29 | 0.80 | 0.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 的概率是欺诈”),报不报警取决于你设的门槛。 门槛调高:报得少而准(精确率↑、召回↓);门槛调低:宁可错杀(召回↑、精确率↓)。 所以精确率、召回率从来不是一个死数,而是随门槛滑动的一条曲线:
- PR 曲线:横轴召回、纵轴精确率。类别不平衡(如欺诈)时最该看它。
- ROC 曲线:横轴“误报率”、纵轴“召回率”。曲线下面积 AUC 用一个数概括整体好坏——1.0 完美、0.5 等于瞎猜。
一句话:报一个指标前先想清楚它是在哪个门槛下算的;要比较模型的整体能力,用 AUC 这种“扫遍所有门槛”的数更公平。
多分类怎么办(如 MNIST 10 类)
上面是二分类。像 MNIST 认 0~9 有 10 类,混淆矩阵就变成 10×10:对角线是认对的,非对角线告诉你谁被认成了谁(比如 4 常被认成 9)。 精确率/召回率改成逐类各算一份,再汇总成一个数,常见两种汇总:
- macro 平均:先算每类的指标,再简单平均——每个类同等重要,少数类不会被淹没。
- micro 平均:把所有类的 TP/FP/FN 汇总到一起再算——样本多的大类说了算,数值上等于总体准确率。
本仓库 MNIST demo 报的是总体准确率(约 97.8%,第 23 章),因为 10 个数字大致均衡; 一旦类别不均衡,就该看 macro 平均或逐类召回,才不会被大类的高分掩盖了小类的糟糕表现。
换个任务就换指标:回归(预测数值)看 MSE / MAE(第 4 章);语言模型看困惑度(第 19 章)。 指标要配任务选,没有万能的一个数。
5. 数据预处理:先把数据“摆正”
原始数据往往量纲乱七八糟(有的特征 0~1,有的 0~10000)。直接喂进去,大数值特征会主导梯度、训练很不稳。 归一化 / 标准化就是把各特征缩放到差不多的尺度。最常见两种:
- 归一化到 [0,1]:减最小值、除以范围。MNIST 的像素(0~255)就是这么除以 255 变成 [0,1] 的。
- 标准化(zero-mean):减均值、除标准差,变成“均值 0、标准差 1”。(是不是很眼熟?LayerNorm 就是这套,第 18 章。)
// 像素 0~255 归一化到 [0,1] 再喂进网络
pixel_value = raw_pixel / 255.0; 1
- 本仓库的 MNIST 就把像素除以 255,压到
[0,1](不是二值化)。optimizer_bench也共用这份预处理。归一化后各特征尺度一致,训练更稳、更快。
关键纪律:统计量只能从训练集算
归一化/标准化要用到一些统计量(最小值、最大值、均值、标准差)。这些数只能在训练集上算, 然后把同一组数原样套到 val/test 上——而不是各自重新算。
要是图省事,拿训练 + 测试合在一起算均值和标准差,测试集的信息就顺着这几个统计量漏进了训练,
分数虚高、上线原形毕露。正确姿势:μ、σ ← 只用训练集,再对 val/test 做 (x − μ) / σ。
这正是第 1 节那条泄漏铁律最常见的一次翻车。
归一化还是标准化?怎么选
- [0,1] 归一化:数据有明确上下界时最自然(像素 0~255、百分比 0~100%)。缺点是对异常值敏感——一个超大值会把其余全压扁。
- 标准化(zero-mean):数据近似钟形分布、或各特征量纲差异大时更稳,对异常值也更耐受,是深度网络里最常用的一种。
- 别漏了非数值特征:类别型(如“星期几”“城市”)一般转成 one-hot 编码;有缺失值要先填补(常用训练集的均值/众数)——同样只用训练集的统计量。
不止缩放:先处理缺失、异常、重复与标签错
归一化只是把干净数据“摆正”。可现实里的原始数据往往先是脏的:有空洞、有离谱的值、有重复、还有贴错的标签。 这些不先处理,再好的模型也是“垃圾进,垃圾出”。下面四类是最常见的脏,也是喂数据前该先扫一遍的清单:
| 脏在哪 | 长什么样 | 危害 | 常用处理 |
|---|---|---|---|
| 缺失值 | 某些特征是空的(没填年龄、传感器掉数) | 很多算法算不动;直接填 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. 训练不动?一份排查清单
真上手训练,十有八九会遇到“不对劲”。按这个顺序排查,能解决大部分问题:
- 损失是 NaN / 直接发散 → 学习率太大,先调小 10 倍;检查有没有忘了归一化;考虑梯度裁剪(第 9 章)。
- 损失几乎不降 → 学习率太小,或模型太弱/激活选错(第 7 章);先确认能在一小撮数据上过拟合(连背都背不下来,说明代码有 bug)。
- 训练降、验证不降甚至回升 → 过拟合,上正则化(第 10 章):weight decay、Dropout、early stop、更多数据。
- 训练/验证差距很大又都不好 → 可能数据有问题(标签错、泄漏、分布不一致),回头查数据。
- 分数看着不错,但吃不准好不好 → 回到笨基线比一比:没明显超过基线,先别调参,查数据和指标选得对不对。
拿 10 条样本让模型死记到损失≈0。如果连这都做不到,基本是代码/数据管道有 bug, 而不是“模型不够强”。这一招能帮你在浪费大量算力前,先揪出低级错误。
小结
- train/val/test:学 / 调 / 考,三份互不重叠;测试集全程不能碰,当心数据泄漏。
- 划分小心三个坑:时间序列不打乱、同组不串台、训练与上线同分布。
- 数据少用 K 折 / 分层 K 折,每条数据都当一次考题。
- 先立笨基线;准确率在不平衡时会骗人,用混淆矩阵推出精确率、召回、F1,并记得它们随门槛变(ROC/PR、AUC)。
- 指标要配任务:分类看 P/R/F1(多类看 macro/micro),回归看 MSE,语言模型看困惑度。
- 喂数据前先修脏:处理缺失、异常、重复、标签错,去重要在切分前做免得泄漏。
- 喂数据前先归一化(MNIST 除以 255),统计量只从训练集算;训练不动就按清单排查,先“过拟合一小撮”验证管道。
到这里,监督学习“如何训练好一个网络”的完整功夫就齐了。但学习不止监督这一种—— 下一章我们认识一种很不一样的范式:强化学习,没有标准答案,只靠“奖励”在试错里学策略; 它也是日后对齐大模型(RLHF)的底子。之后再进入第三部分,认识 CNN、RNN 这些“专才”结构。