Ch05 评估与特征工程修订于 二〇二六年七月二日

5.6 交叉验证完全指南

交叉验证是机器学习中最基本的评估技术之一。它的核心思想很简单:不要只用一次划分来评估模型,多次划分取平均,结果更可靠。

5.6.1 为什么需要交叉验证

单次训练/测试划分的问题:

  • 测试集恰好全是简单样本 → 模型看起来比实际好
  • 测试集恰好全是困难样本 → 模型看起来比实际差
  • 数据量少时,测试集太小,评估结果方差大,不可靠
  • 你无法知道模型是在”学规律”还是”刚好这套划分效果好”

交叉验证通过多次划分和平均,解决了这些问题。每个数据点都有机会被测试,评估结果是对模型泛化能力的更可靠估计。

5.6.2 K-Fold 交叉验证的详细步骤

步骤 1:把数据集随机打乱,分成 K 等份(每份大小尽量相等)。

步骤 2:进行 K 轮训练和评估:

  • 第 i 轮:用第 i 份作为验证集(测试集),其余 K-1 份合并作为训练集
  • 训练模型,在验证集上评估,记录得分 s_i

步骤 3:最终得分 = K 轮得分的平均值。也可以计算标准差来衡量模型稳定性。

K=5 的完整示例(假设 1000 条数据,分成 5 份,每份 200 条):

第1轮: [训练: 份2+份3+份4+份5 (800条)] [测试: 份1 (200条)] → 得分 0.85
第2轮: [训练: 份1+份3+份4+份5 (800条)] [测试: 份2 (200条)] → 得分 0.83
第3轮: [训练: 份1+份2+份4+份5 (800条)] [测试: 份3 (200条)] → 得分 0.87
第4轮: [训练: 份1+份2+份3+份5 (800条)] [测试: 份4 (200条)] → 得分 0.84
第5轮: [训练: 份1+份2+份3+份4 (800条)] [测试: 份5 (200条)] → 得分 0.86

最终得分 = (0.85 + 0.83 + 0.87 + 0.84 + 0.86) / 5 = 0.85 标准差 = 0.016(得分波动小,模型稳定)

这个 0.85 比单次划分可靠得多。它不仅给出了”模型有多好”的估计,还通过标准差给出了”这个估计有多可靠”。

5.6.3 K 值的选择

K 值的选择是一个偏差-方差权衡:

K 太小(如 K=2):

  • 训练数据只有一半,模型可能学不好(欠拟合)→ 评估结果偏悲观(高偏差)
  • 但每次训练的数据量小,训练快
  • 不推荐,除非数据量极大

K 太大(如 K=N,即留一法):

  • 训练数据几乎等于全部数据,模型学得很好 → 评估结果可能偏乐观(低偏差)
  • 但每轮训练数据几乎一样,评估结果高度相关 → 方差大
  • 计算量巨大,需要训练 N 次
  • 不推荐,除非数据量极小(N < 100)

K=5 或 K=10:工业界经验值。

  • 在偏差和方差之间取得良好平衡
  • 计算成本可接受
  • 大多数研究论文使用 K=5 或 K=10
  • K=10 比 K=5 略好(更多训练数据),但计算量翻倍
  • 当数据量小于 1000 时,K=10 更合适;数据量大于 10000 时,K=5 足够

5.6.4 Stratified K-Fold:保持类别比例

普通的 K-Fold 随机划分,可能导致某一折中正例特别多或特别少——这对不平衡数据是灾难。

Stratified K-Fold:在划分时保持每一折的类别比例与原始数据一致。如果原始数据正例占 10%,每一折的正例也占约 10%。

为什么重要:假设正例只占 1%,普通 K-Fold 可能让某一折完全没有正例,那一折的评估结果完全没有意义。Stratified K-Fold 保证每一折都有正例。

在 Scikit-learn 中:

from sklearn.model_selection import StratifiedKFold, cross_val_score
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
scores = cross_val_score(model, X, y, cv=skf, scoring='f1')

2026 年的建议:对于分类问题,几乎总是使用 Stratified K-Fold 而不是普通 K-Fold。shuffle=True 在划分前打乱数据,random_state=42 保证结果可复现。

5.6.5 留一法(LOOCV, Leave-One-Out Cross-Validation)

做法:每次留 1 个样本作为测试集,其余 N-1 个样本作为训练集。重复 N 次。

优点:

  • 训练数据几乎等于全部数据(N-1 个),偏差最小
  • 不受随机划分影响,结果完全确定(没有随机性)

缺点:

  • 计算量巨大:需要训练 N 次模型,N=1000 就是 1000 次
  • 方差大:每轮训练数据几乎一样,评估结果高度相关
  • 对于大模型(如神经网络),完全不可行

适用场景:数据量极小(N < 100)且模型训练很快(如线性回归、KNN)。2026 年,LOOCV 在深度学习时代几乎不被使用,但它在统计学和传统机器学习中仍有理论价值。

5.6.6 时间序列交叉验证

时间序列数据不能随机打乱——过去的数据训练,未来的数据测试。否则会造成”用未来预测过去”的数据泄露。

TimeSeriesSplit(滚动预测):

  • 第 1 轮:用前 20% 的数据训练,预测第 21-30% 的数据
  • 第 2 轮:用前 30% 的数据训练,预测第 31-40% 的数据
  • 第 3 轮:用前 40% 的数据训练,预测第 41-50% 的数据
  • …以此类推

关键原则:训练集的时间必须早于测试集的时间。永远不可以用”未来”的数据训练模型来预测”过去”。

Scikit-learn 实现:

from sklearn.model_selection import TimeSeriesSplit
tscv = TimeSeriesSplit(n_splits=5)
for train_idx, test_idx in tscv.split(X):
    X_train, X_test = X[train_idx], X[test_idx]
    # 训练和评估

适用场景:股票价格预测、天气预测、销售预测、任何有明确时间顺序的数据。

5.6.7 Group K-Fold:防止数据泄露

当数据中有”组”结构时,同一个组的数据不能同时出现在训练集和测试集中。

典型场景:

  • 医疗数据:同一个病人的多次检查记录必须在同一折中——不能让模型在训练时看到病人的一些检查,测试时又看到同一个病人的其他检查。这会导致数据泄露(模型通过记住病人特征而不是学习病理特征来作弊)
  • 用户行为数据:同一个用户的多次行为必须在同一折中
  • 图片数据:同一场景的多张图片必须在同一折中

Group K-Fold:确保同一个 group 的所有样本要么全在训练集,要么全在测试集。

from sklearn.model_selection import GroupKFold
gkf = GroupKFold(n_splits=5)
for train_idx, test_idx in gkf.split(X, y, groups=patient_ids):
    # groups 参数指定每个样本属于哪个组

5.6.8 嵌套交叉验证:用于模型选择

普通的交叉验证用于评估一个固定模型的性能。但如果你要同时选择模型(如选 SVM 还是随机森林)和调超参数,就需要嵌套交叉验证。

外层循环:评估模型的泛化性能(K 折) 内层循环:在训练数据上做超参数调优(K 折)

外层循环(5 折):
  for 每一折:
    训练集(外层)→ 内层循环(5 折):
      在训练集上做超参数搜索
      选出最优超参数
    用最优超参数在训练集上训练
    在测试集(外层)上评估

为什么需要嵌套:如果不用嵌套,你在整个数据集上选超参数,然后用同样的数据评估——评估结果会偏乐观,因为你用测试集”指导”了超参数选择。

代价:嵌套交叉验证的计算量是 K_outer × K_inner × 超参数组合数——非常大。对于大模型(如神经网络),嵌套交叉验证通常不可行,一般用单独的验证集来做超参数选择。

2026 年的实践:嵌套交叉验证主要用于传统机器学习(数据量小于 10000 时)。深度学习时代,通常用固定的 train/val/test 划分,在 val 上选超参数,在 test 上做最终评估。

Scikit-learn 一行代码完成 K-Fold:

from sklearn.model_selection import cross_val_score
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"平均分: {scores.mean():.3f} (+/- {scores.std() * 2:.3f})")

← 上一节:5.5 混淆矩阵深度解析 | 下一节:5.7 特征工程实战 →

关联5

知识图谱

局部图谱 · 点节点跳转