🏠 总目录📚 本教程 05 · 评估与过拟合
📑 本页目录(点开跳转)

05 · 评估与过拟合

30 分钟 | ⭐⭐⭐ 全教程最重要的一章

如果你这份教程只学一章,学这章。 模型算法会过时,「怎么知道模型到底行不行」这件事永远不会过时。


🎯 一句话

机器学习里 90% 的翻车,不是模型不好,而是你以为的好是假的。这一章教你怎么不骗自己。

模型复杂度 / 训练轮数 →误差最优点欠拟合两边都差过拟合训练好 / 验证差训练误差验证误差训练误差一路降,验证误差却是 U 形 —— 两者的差就是过拟合
训练误差一路降,验证误差却是 U 形。⭐ 两条线之间的缝隙就是过拟合的量 —— 你要找的不是训练误差最低的点,而是绿色那条竖线所在的位置。

⚖️ 一、偏差-方差:过拟合的根源

   总误差 = 偏差² + 方差 + 不可消除的噪声

   偏差 Bias:模型太简单,系统性地偏离真相
              → 打靶总打在左下角
              → 训练集就差 = 欠拟合

   方差 Variance:模型对训练数据太敏感,换批数据结果就变
              → 打靶散得到处都是
              → 训练好测试差 = 过拟合
误差刚好最优点在这欠拟合(高偏差)过拟合(高方差)模型复杂度 →测试误差(总误差)偏差²方差训练误差不可消除的噪声
看绿线和红线反着走:复杂度往右,偏差² 一路降、方差一路涨,而你真正在意的测试误差是这两条加起来的和 —— 所以它必然是 U 形。⭐ 过拟合不是「学得太多」,是方差涨得比偏差降得快的那一段;正则、剪枝、早停做的都是同一件事:把红线压下去,让最低点往右挪。

🔑 第 1 章那张决策树深度表,画出来就是这张图。 回去再看一眼。

🔬 想知道这不是打比方而是一个严格等式吗? 「期望误差 = 偏差² + 方差 + σ²」是可以推导出来的,而且它直接解释了集成学习为什么有效 —— 见《机器学习的数学原理》第 8 章

怎么判断自己在哪一侧

现象 诊断 怎么办
训练集也差 欠拟合(高偏差) 加模型复杂度、加特征、减正则、训久点
训练好、测试差 过拟合(高方差) 加数据 ⭐、加正则、减复杂度、早停
两边都好 ✅ 收工
训练集差但测试集好 🤨 有 bug 或数据泄漏 查代码

💡 加数据是唯一同时降低方差又不增加偏差的手段——所以它永远是第一选择。


🔄 二、交叉验证:别信单次切分

① 随机 K 折 —— 独立同分布数据用这个第1折第2折第3折② 时间序列切分 —— 有时间顺序时【必须】用这个第1折第2折第3折③ 分组切分 —— 同一个用户/病人的数据不能跨组第1折第2折G0G0G1G1G2G2G3G3同一批数据,切错了方式,分数就是假的橙色 = 验证 蓝色 = 训练 灰色 = 不用(未来数据)⭐ 时序切分里灰色那部分【绝不能】进训练集 —— 那就是用未来预测过去
同一批数据,切错方式分数就是假的。⭐ 时序切分里那些灰色格子绝不能进训练集 —— 那就是用未来预测过去;分组切分则保证同一个用户/病人的数据不会横跨训练和验证。

问题:第 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 划分方式和线上/测试场景不一致,是竞赛掉分的头号原因。


💀 三、数据泄漏:最致命的错误

① 时间穿越用了预测时点之后才有的信息「用户总订单数」包含了未来订单② 预处理泄漏在全量数据上fit 了转换器标准化/PCA/目标编码偷看了验证集分布③ 分组泄漏同一实体的数据同时在训练和验证同一用户的两条记录被随机切到了两边三条路径不同,症状却一样:离线分数好得不真实⭐ 一个通用自检:把特征逐个问一遍「预测的那一刻,这个值真的已经存在了吗?」以及:所有预处理都放进 Pipeline,让交叉验证自动隔离💀 实测:纯噪声数据(真实准确率 50%),泄漏时能跑出 76.5%
三条路径不同,症状却一样:离线分数好得不真实。⭐ 通用自检:把特征逐个问一遍「预测的那一刻,这个值真的已经存在了吗?」并把所有预处理放进 Pipeline,让交叉验证自动隔离。

定义:训练时用到了「本不该知道的信息」,导致离线分数虚高、上线暴死。

🔨 一个会让你后背发凉的实验

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 章的坑)
分组泄漏 同一用户的数据同时出现在训练和测试集
目标泄漏 特征里混进了标签的衍生物(如「是否退款」预测「是否投诉」)
重复样本 去重没做干净,同一条数据两边都有

📏 四、选对指标(准确率是个陷阱)

假正率 FPR(把好的误判成坏的)真正率TPR← 一个阈值 = 一个点调阈值就是在这条线上滑AUC = 这块面积随机猜AUC 只看排序,不看分数准不准⚠️ 两个模型 AUC 一样,概率校准可能天差地别 —— 要做阈值决策时必须另看校准
曲线上每一个点对应一个阈值,调阈值就是在这条线上滑。AUC 是曲线下的面积。⚠️ 但 AUC 只看排序 —— 两个模型 AUC 一样,概率校准可能天差地别。

🔨 实测: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

三个必须看懂的点

  1. 傻子模型和逻辑回归的准确率一模一样(98.4%) —— 准确率完全无法区分它们
  2. AUC 0.65 说明模型其实有信号,但默认阈值 0.5 下一个正样本都没抓到
  3. 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 章学的评测方法论同源: 「读转录」「分维度看」「对小差距保持怀疑」在这里全都成立。


✅ 检查点

  1. 偏差和方差各对应什么现象?怎么区分自己在哪一侧?
  2. 为什么单次切分的分数不可信?实测极差有多大?
  3. 分类任务默认该用哪种交叉验证?时间序列呢?
  4. 为什么特征选择必须放进 Pipeline?纯噪声实验的两个数字是多少?
  5. 1.6% 正样本时,98.4% 的准确率说明什么?该看什么指标?
  6. 学习曲线怎么告诉你「该加数据还是该改模型」?
  7. 为什么最终测试集只能用一次?
👀 答案
  1. 偏差=模型太简单,训练集就差(欠拟合);方差=对训练数据太敏感,训练好测试差(过拟合)。看训练集分数:训练集也差就是欠拟合,训练好测试差就是过拟合。
  2. 带很大运气成分。实测同一模型只换切分种子,10 次结果在 94.7%~98.2% 之间,极差 3.5%。
  3. 分类用 StratifiedKFold(保持类别比例);时间序列必须用 TimeSeriesSplit(只能用过去预测未来)。
  4. 因为特征选择会"看到"全部标签,把标签信息记进选中的特征里。纯噪声实验:错误做法 76.5%,正确做法 47.5%。
  5. 什么都说明不了——全预测多数类的傻子模型也是 98.4%。该看 PR-AUC、F1、召回率,并且调阈值。
  6. 测试集还在上升→加数据有用;已经平了→加数据没用,去改模型/特征。
  7. 反复在测试集上调参会造成慢性泄漏,测试集逐渐变成"第二个训练集",失去无偏评估的意义。

🛑 可以停在这里

走神救援

本章是全教程最重要的一章——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

打卡记录保存在你的浏览器里,首页能看到总进度