第 11 章 · 学习是怎么发生的
评估与数据
上一章反复提到“训练集、验证集、看指标”。这一章把这件事讲透:数据该怎么分、 模型好不好该怎么量(为什么只看“准确率”会被骗)、数据进模型前要怎么预处理, 以及训练不动时的一份排查清单。这是把模型做对、而不只是跑起来的关键一章。
读完这一章,你会明白
- train / val / test 三个数据集各自的职责,以及“测试集不能碰”的铁律;
- 划分数据时的隐藏坑:时间序列不能打乱、同组数据不能串台、训练与上线要同分布;
- 数据少时怎么用交叉验证(含分层 K 折)把每一条数据都用足;
- 为什么只看准确率会骗人,以及先立一个笨基线再谈高低;
- 精确率 / 召回 / F1 / 混淆矩阵怎么读、怎么算,以及指标如何随判定门槛变化(ROC/PR、AUC);
- 多分类(如 MNIST 10 类)的 macro / micro 平均是怎么回事;
- 数据为什么要归一化,以及统计量只能从训练集算(否则泄漏);
- 训练“不降 / 发散 / 过拟合”时的一份调试清单。
1. 三个数据集:学、调、考
要诚实地评估泛化(第 10 章),必须把数据分成互不重叠的三份,各司其职:
- 训练集 train用来学——反向传播更新参数,只用它。
- 验证集 val用来调——边训边监控、选超参数(学习率、层数)、决定何时 early stop。它不参与更新参数,但你会“照着它做决定”。
- 测试集 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 编码;有缺失值要先填补(常用训练集的均值/众数)——同样只用训练集的统计量。
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 这些“专才”结构。