📑 本页目录(点开跳转)
05 / 先看直觉,再看细节
先问分数是否可信,
再问能不能更高。
评估要同时守住两件事:不偷看答案,指标确实对应你的目标。
先抓住一个具体结果
95% 准确率,也可能一个都没找到
100 个样本里有 5 个正例。如果全部预测成负例,准确率仍有 95%,但正例召回率是 0。只看准确率,会错过真正关心的问题。
接下来,按需要选一项
你现在想看什么?
这些入口是选择,不是必须按顺序完成的任务。
可以停在这里
评估要同时守住两件事:不偷看答案,指标确实对应你的目标。
需要更多细节时,继续看完整正文 →原有正文、图解和例子都在下方。按需跳转,不必一次读完。
05 · 评估与过拟合
⏱ 30 分钟 | ⭐⭐ 全教程最重要的一章
如果你这份教程只学一章,学这章。 模型算法会过时,「怎么知道模型到底行不行」这件事永远不会过时。
🎯 一句话
机器学习里 90% 的翻车,不是模型不好,而是你以为的好是假的。这一章教你怎么不骗自己。
⚖️ 一、偏差-方差:过拟合的根源
关键信息
🔑 第 1 章那张决策树深度表,画出来就是这张图。 回去再看一眼。
🔬 想知道这不是打比方而是一个严格等式吗? 「期望误差 = 偏差² + 方差 + σ²」是可以推导出来的,而且它直接解释了集成学习为什么有效 —— 见《机器学习的数学原理》第 8 章。
怎么判断自己在哪一侧:
| 现象 | 诊断 | 怎么办 |
|---|---|---|
| 训练集也差 | 欠拟合(高偏差) | 加模型复杂度、加特征、减正则、训久点 |
| 训练好、测试差 | 过拟合(高方差) | 加数据 ⭐、加正则、减复杂度、早停 |
| 两边都好 | ✅ 收工 | — |
| 训练集差但测试集好 | 🤨 有 bug 或数据泄漏 | 查代码 |
💡 加数据是唯一同时降低方差又不增加偏差的手段——所以它永远是第一选择。
🔄 二、交叉验证:别信单次切分
问题:第 1 章那个 91.8%,换个 random_state 就变了。单次切分的分数带着很大的运气成分。
实测(同一个模型,只换切分种子):
对照
10 次不同切分的单次测试分:94.7% ~ 98.2% 极差 3.5% (!)
5 折交叉验证: 96.5% ± 1.5%
⚠️ 极差 3.5%。这意味着:如果你靠单次切分判断「模型 A 比 B 高 2%」,那大概率是噪声。
K 折交叉验证
信息关系
变体,选对了才有意义:
| 类型 | 什么时候用 |
|---|---|
| KFold | 普通情况 |
| StratifiedKFold ⭐ | 分类任务默认用它——保证每折的类别比例一致 |
| GroupKFold | 同一个用户/病人有多条数据时,防止同组数据跨折泄漏 |
| TimeSeriesSplit ⭐ | 时间序列必须用——只能用过去预测未来 |
🔗 推荐算法第 3 章说的「按时间切分,别随机切」,就是 TimeSeriesSplit 的思想。 Kaggle 第 1 章讲的「验证集构造」讲的也是这件事—— CV 划分方式和线上/测试场景不一致,是竞赛掉分的头号原因。
💀 三、数据泄漏:最致命的错误
定义:训练时用到了「本不该知道的信息」,导致离线分数虚高、上线暴死。
🔨 一个会让你后背发凉的实验
import numpy as np
from sklearn.model_selection import cross_val_score, KFold
from sklearn.linear_model import LogisticRegression
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.pipeline import make_pipeline
rng = np.random.RandomState(0)
X = rng.randn(200, 5000) # 200 个样本,5000 个【纯随机噪声】特征
y = rng.randint(0, 2, 200) # 标签也是【纯随机】的
# → 特征和标签毫无关系,任何诚实的模型准确率都该是 50%
# ❌ 错误做法:先用全部数据选特征,再做交叉验证
sel = SelectKBest(f_classif, k=20).fit(X, y)
bad = cross_val_score(LogisticRegression(max_iter=2000),
sel.transform(X), y, cv=KFold(5, shuffle=True, random_state=0))
# ✅ 正确做法:把特征选择放进 Pipeline,在每折内部重新做
pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression(max_iter=2000))
good = cross_val_score(pipe, X, y, cv=KFold(5, shuffle=True, random_state=0))
print(f"❌ 先选特征再CV:{bad.mean():.1%}")
print(f"✅ 放进Pipeline:{good.mean():.1%}")
实测结果:
算一算
❌ 先选特征再CV:76.5% ← 纯噪声数据上凭空刷出 76.5%!
✅ 放进Pipeline:47.5% ← 诚实(≈50% 随机水平)
💡 发生了什么:从 5000 个噪声特征里选「和标签最相关的 20 个」时, 你已经偷看了全部标签——总有些特征碰巧和标签相关。 这 20 个特征把标签信息「记」进来了,交叉验证再怎么切都逃不掉。
🔑 这就是为什么必须用 Pipeline:所有「会看到 y」或「会统计全体数据」的步骤 (特征选择、标准化、目标编码、缺失值填充、过采样) 都必须放进 Pipeline,让它们在每折内部重新执行。
常见泄漏清单
| 泄漏形式 | 例子 |
|---|---|
| 预处理泄漏 ⭐ | 用全量数据 fit scaler / 选特征 / 填缺失值 |
| 时间泄漏 ⭐ | 用未来数据预测过去(推荐算法第 3 章的坑) |
| 分组泄漏 | 同一用户的数据同时出现在训练和测试集 |
| 目标泄漏 | 特征里混进了标签的衍生物(如「是否退款」预测「是否投诉」) |
| 重复样本 | 去重没做干净,同一条数据两边都有 |
⚠️ 这一章讲的随机划分,有一整类数据上不能直接用。 当样本彼此同源(同一个用户的多条记录、同一个家族的多条蛋白序列), 随机切会把「几乎一样的东西」分到训练集和测试集两边 —— 测的是记忆不是泛化。 ⭐ 两个方向的展开:数据这一关 14 · 数据泄漏的七种来源 讲全部七种;科学问题怎么变成模型 07 · 评测的坑 实测了它最狠的样子 —— 同一份数据只改划分方式,R² 从 0.707 掉到 −1.199(负数 = 还不如猜均值)。
📏 四、选对指标(准确率是个陷阱)
🔨 实测:99% 的准确率毫无意义
数据:1.6% 的正样本(如欺诈检测、罕见病)
算一算
方案 准确率 精确率 召回率 F1 AUC PR-AUC
全predict 0(傻子) 98.4% 0.0% 0.0% 0.0% 0.50 0.00
逻辑回归(默认) 98.4% 0.0% 0.0% 0.0% 0.65 0.07
+ class_weight平衡 67.7% 2.7% 55.3% 5.1% 0.62 0.04
默认模型 阈值=0.1 98.1% 17.6% 6.4% 9.4% 0.65 0.07
默认模型 阈值=0.05 95.4% 10.3% 25.5% 14.7% 0.65 0.07
三个必须看懂的点:
- 傻子模型和逻辑回归的准确率一模一样(98.4%) —— 准确率完全无法区分它们
- AUC 0.65 说明模型其实有信号,但默认阈值 0.5 下一个正样本都没抓到
- PR-AUC 只有 0.07 —— 比 AUC 诚实得多。类别极不平衡时看 PR-AUC,别看 AUC
指标速查
| 指标 | 含义 | 什么时候用 |
|---|---|---|
| 准确率 | 对的比例 | 类别均衡时。⚠️ 不平衡时是陷阱 |
| 精确率 Precision | 说是正的里面真的有多少 | 误报代价高(如垃圾邮件误杀) |
| 召回率 Recall | 真的正的里面抓到多少 | 漏报代价高(如癌症筛查)⭐ |
| F1 | 精确率和召回率的调和平均 | 两者都要 |
| AUC | 随机取一正一负,正的排前面的概率 | 排序质量,与阈值无关 ⭐ |
| PR-AUC | 精确率-召回率曲线下面积 | 类别极不平衡时比 AUC 可靠 ⭐ |
🔗 推荐算法第 12 章的 GAUC 就是 AUC 的分组版本—— 因为推荐场景要看的是「每个用户自己的列表里排得对不对」。
⭐ AUC 有两种等价定义:教科书版是「ROC 曲线(横轴假正率、纵轴真正率)下面的面积」, 上表用的是另一版「随机取一正一负,正的排前面的概率」——两者数值完全相同。 之所以用后者,是因为这个定义本身就是一个算法——不用画 ROC 曲线,排个序数秩就行。 🔗 附录 C 第 9 题把它写成了 15 行 $O(n\log n)$ 的实现。 去那里主要是为了一个这一节没提的坑:分数并列必须取平均秩。 树模型大量样本落进同一个叶子拿到完全相同的分数,并列是常态,不处理算出来的 AUC 就是错的。 那边还有一个高频追问的实测答案:对分数做单调变换(×100 再 +5),AUC 一位不变——因为它只看排序, 所以两个模型 AUC 一样、概率校准可能天差地别。
阈值不是 0.5
上表最后两行说明:模型输出的是概率,把它变成 0/1 的那个阈值是你的自由参数。 调阈值不用重训模型,却能大幅改变精确率/召回率的平衡。这是最便宜的优化手段。
📈 五、学习曲线:加数据有用吗
学习曲线:样本变多后发生了什么
| 训练样本数 | 训练集 | 测试集 | 差距 |
|---|---|---|---|
| 39 | 100.0% | 91.8% | 8.2% |
| 79 | 100.0% | 93.6% | 6.4% |
| 159 | 100.0% | 94.7% | 5.3% |
| 398 | 100.0% | 94.2% | 5.8% |
怎么读这张表:
结果对照
💡 这张图能帮你回答那个昂贵的问题:「我该花钱标更多数据,还是花时间调模型?」
🧪 六、一套可以照抄的评估流程
关键信息
- ① 先切出【最终测试集】,锁进保险柜,全程只用一次 ⭐
- 反复在测试集上调参 = 慢性泄漏,测试集会失去意义
- ② 在剩下的数据上做交叉验证(分类用 Stratified,时序用 TimeSeries)
- ③ 所有预处理放进 Pipeline
- ④ 选对指标(不平衡看 PR-AUC/F1,排序看 AUC)
- ⑤ 记录【均值 ± 标准差】,差距小于一个标准差的改动别当真
- ⑥ 定好之后,才打开保险柜跑最终测试集,只跑一次
🔗 这套流程和你在智能体工程第 14 章学的评测方法论同源: 「读转录」「分维度看」「对小差距保持怀疑」在这里全都成立。
✅ 检查点
- 偏差和方差各对应什么现象?怎么区分自己在哪一侧?
- 为什么单次切分的分数不可信?实测极差有多大?
- 分类任务默认该用哪种交叉验证?时间序列呢?
- 为什么特征选择必须放进 Pipeline?纯噪声实验的两个数字是多少?
- 1.6% 正样本时,98.4% 的准确率说明什么?该看什么指标?
- 学习曲线怎么告诉你「该加数据还是该改模型」?
- 为什么最终测试集只能用一次?
👀 答案
- 偏差=模型太简单,训练集就差(欠拟合);方差=对训练数据太敏感,训练好测试差(过拟合)。看训练集分数:训练集也差就是欠拟合,训练好测试差就是过拟合。
- 带很大运气成分。实测同一模型只换切分种子,10 次结果在 94.7%~98.2% 之间,极差 3.5%。
- 分类用 StratifiedKFold(保持类别比例);时间序列必须用 TimeSeriesSplit(只能用过去预测未来)。
- 因为特征选择会"看到"全部标签,把标签信息记进选中的特征里。纯噪声实验:错误做法 76.5%,正确做法 47.5%。
- 什么都说明不了——全预测多数类的傻子模型也是 98.4%。该看 PR-AUC、F1、召回率,并且调阈值。
- 测试集还在上升→加数据有用;已经平了→加数据没用,去改模型/特征。
- 反复在测试集上调参会造成慢性泄漏,测试集逐渐变成"第二个训练集",失去无偏评估的意义。
🛑 可以停在这里
⚡ 走神救援
先记住这几件事
- 验证集负责选方案,最终测试集负责检验泛化,不能反复拿测试集调参。
- 预处理只在训练部分拟合;时间、用户等分组关系也必须体现在划分里。
- 结合训练与验证曲线判断欠拟合、过拟合,再调整容量、正则化或数据。
- 指标必须对应实际任务;保留基线并观察不同划分或种子的波动。
下一节 👉 06-无监督-聚类与降维.md