📑 本页目录(点开跳转)
05 · 评估与过拟合
⏱ 30 分钟 | ⭐⭐⭐ 全教程最重要的一章
如果你这份教程只学一章,学这章。 模型算法会过时,「怎么知道模型到底行不行」这件事永远不会过时。
🎯 一句话
机器学习里 90% 的翻车,不是模型不好,而是你以为的好是假的。这一章教你怎么不骗自己。
⚖️ 一、偏差-方差:过拟合的根源
总误差 = 偏差² + 方差 + 不可消除的噪声
偏差 Bias:模型太简单,系统性地偏离真相
→ 打靶总打在左下角
→ 训练集就差 = 欠拟合
方差 Variance:模型对训练数据太敏感,换批数据结果就变
→ 打靶散得到处都是
→ 训练好测试差 = 过拟合
🔑 第 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 折交叉验证
全部数据切成 5 份:
第1折: [测试][训练][训练][训练][训练] → 分数1
第2折: [训练][测试][训练][训练][训练] → 分数2
...
第5折: [训练][训练][训练][训练][测试] → 分数5
最终 = 5 个分数的均值 ± 标准差
→ 每个样本都当过一次测试,分数更稳
变体,选对了才有意义:
| 类型 | 什么时候用 |
|---|---|
| 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 章的坑) |
| 分组泄漏 | 同一用户的数据同时出现在训练和测试集 |
| 目标泄漏 | 特征里混进了标签的衍生物(如「是否退款」预测「是否投诉」) |
| 重复样本 | 去重没做干净,同一条数据两边都有 |
📏 四、选对指标(准确率是个陷阱)
🔨 实测: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、召回率,并且调阈值。
- 测试集还在上升→加数据有用;已经平了→加数据没用,去改模型/特征。
- 反复在测试集上调参会造成慢性泄漏,测试集逐渐变成"第二个训练集",失去无偏评估的意义。
🛑 可以停在这里
⚡ 走神救援
本章是全教程最重要的一章——90% 的翻车不是模型不好,而是你以为的好是假的。⭐总误差=偏差²+方差+噪声:训练好测试差=过拟合(⭐加数据/加正则/早停)。⭐过拟合不是"学得太多",是方差涨得比偏差降得快的那一段;💡加数据是唯一同时降方差又不增偏差的手段。⭐别信单次切分:同一模型只换种子,10 次测试分在 94.7%~98.2%、极差 3.5%——靠单次切分判断"A 比 B 高 2%"大概率在读噪声。分类默认 StratifiedKFold、⭐时序必须 TimeSeriesSplit。💀数据泄漏最致命:200 个样本配 5000 个纯随机噪声特征、标签也随机(真值 50%),"先用全部数据选出最相关的 20 个特征再做交叉验证"刷出 76.5%,放进 Pipeline 后只有 47.5%——选特征时你已经偷看了全部标签。⭐凡是"会看到 y"的步骤(选特征、标准化、目标编码、填缺失、过采样)都必须进 Pipeline,在每折内部重做。⚠️准确率是陷阱:正样本只占 1.6% 时,全预测 0 的傻子模型和逻辑回归的准确率一模一样都是 98.4%,AUC 0.65 说明有信号,但默认阈值 0.5 一个正样本都没抓到,⭐PR-AUC 只有 0.07,不平衡时看它;⭐阈值是免费的自由参数,调到 0.05 召回就从 0 涨到 25.5%。学习曲线:样本 39→159 时测试集 91.8%→94.7%(还在涨=去搞数据),到 398 反而 94.2%(平了=改模型)。⚠️最终测试集锁进保险柜、只跑一次(反复调参是慢性泄漏),记录均值±标准差,差距小于一个标准差的改动别当真。
下一节 👉 06-无监督-聚类与降维.md