Chapter 05

数据划分与模型验证

训练集、验证集、测试集的哲学;交叉验证与数据泄露防范

01

核心概念:为什么必须划分数据?

在机器学习中,模型的最终目标是泛化(Generalization):对从未见过的数据做出准确预测。如果我们用全部数据训练,再用同样的数据评估,就如同考试时把题目提前泄露给学生——分数会虚高,但真实能力被掩盖。因此,数据划分是机器学习流程中最基础、也最关键的一步。

三类数据集的角色

数据集占比(参考)用途使用次数
训练集 (Training Set)60%-80%训练模型参数反复使用
验证集 (Validation Set)10%-20%调参、选模型、早停多次评估
测试集 (Test Set)10%-20%最终无偏估计仅一次
核心洞察

测试集就像高考卷——你只能做一次。一旦你用测试集的结果来反向调整模型(如调参),测试集就"被污染"了,失去了衡量真实泛化能力的意义。

随机划分与分层抽样

最简单的划分方式是随机划分(Random Split),通过随机抽样将数据分为训练集和测试集。scikit-learn 的 train_test_split 默认采用这种方式。然而,当目标变量的类别分布不均匀时,随机划分可能导致某一类别在训练集或测试集中比例失衡。

分层抽样(Stratified Sampling)则保证划分后每个子集中各类别的比例与原始数据集一致。例如,原始数据中"违约"客户占5%,分层抽样后训练集和测试集中违约比例仍约为5%。这在医学诊断、金融风控等不平衡场景中至关重要。

分层约束:P(y = k | 训练集) ≈ P(y = k | 全集) ≈ P(y = k | 测试集)

交叉验证:更可靠的评估

单次随机划分可能因"运气"导致评估结果波动。K折交叉验证(K-Fold Cross-Validation)将数据集分为K个子集(Fold),轮流用K-1个子集训练、剩余1个子集验证,最终取K次评估的平均值。

常见变体包括:

数据泄露(Data Leakage)——建模中的隐形杀手

数据泄露指训练阶段"偷看"了测试阶段才能知道的信息,导致评估结果虚高。常见形式:

  • 目标泄露:特征中包含了目标变量的衍生信息(如用"购买金额"预测"是否购买")。
  • 时间泄露:时间序列中混入了未来特征(如用下个月的销售数据预测本月)。
  • 预处理泄露:先对整个数据集做标准化/归一化,再划分训练/测试——测试集信息已通过全局统计量泄漏到训练过程。

正确做法:所有预处理(缩放、特征选择、降维)必须只在训练集上拟合,然后变换应用到测试集。

02

计算方法:K-Fold 与分层抽样原理

K-Fold 流程详解

设数据集 D 有 N 个样本,设定折数 K(通常取 5 或 10):

  1. 将 D 随机打乱后均分为 K 份:D₁, D₂, ..., Dₖ。
  2. 对于第 i 轮(i = 1 到 K):用 D \ Dᵢ 训练模型,在 Dᵢ 上计算评估指标(如准确率、MSE)。
  3. 最终得分取 K 轮指标的平均:Score = (1/K) Σ Scoreᵢ。
K-Fold 误差估计:E_cv = (1/K) Σₖ L( fₖ, Dₖ )
其中 fₖ 为第 k 轮在 D \ Dₖ 上训练的模型,L 为损失函数

K 越大,每轮训练集越接近全量数据,偏差越小,但方差增大且计算量增加;K=5 和 K=10 是实践中常用的平衡点。

分层抽样的数学保证

设类别 c 在全集中的比例为 p_c = n_c / N。分层抽样要求每折中类别 c 的比例近似 p_c。当 K 能整除各类别样本数时,可以实现严格分层;否则采用近似分配。

对于二分类问题,StratifiedKFold 在 scikit-learn 中的实现逻辑是:先将每个类别的样本排序并分块,再从每类中循环抽取样本到各折,确保每折的类别比例与总体一致。

偏差-方差权衡视角

单次划分的验证误差方差较大;K-Fold 通过平均降低了方差,但代价是 K 倍的计算量。留一法偏差最低,但方差可能较高(因为各训练集高度重叠),且计算成本极高。理解这一点有助于在不同数据规模和场景下选择合适的验证策略。

时间序列划分的特殊性

对于时间序列数据,标准的随机 K-Fold 会引入未来信息泄露——用未来的模式预测过去。TimeSeriesSplit 保证第 i 轮的训练集只包含验证集时间点之前的数据,模拟真实的"用过去预测未来"场景。

03

工程应用

金融模型时序验证

股票价格、信用违约预测具有强时间依赖性。使用标准随机划分会严重高估模型表现(因为模型"偷看"了未来价格走势)。正确的做法是按时间顺序划分训练/验证/测试窗口,如用 2019-2021 训练、2022 验证、2023 测试,并采用滚动窗口交叉验证(Rolling Window CV)持续评估模型在时间推移后的稳定性。

医学实验交叉验证

医学数据往往样本量小(如罕见疾病仅数百例)、类别极度不平衡(阳性率 < 1%)。留一法或分层 5-Fold 是常用选择。在基因组学中,同一患者的多个样本必须归入同一折(GroupKFold),避免同一患者的数据同时出现在训练集和测试集,导致模型学到患者特异性而非疾病特异性特征。

推荐系统离线评估

推荐系统的核心挑战是"用户-物品"交互矩阵极度稀疏。随机划分用户或物品需要确保训练集中出现过的用户/物品在测试集中也有合理分布。实践中常采用按用户划分(Leave-one-user-out)或按时间切分:用用户早期的点击行为训练,预测后期的兴趣。

A/B 测试对照设计

A/B 测试是线上验证模型效果的黄金标准,但离线验证决定了哪些模型有资格上线。交叉验证帮助筛选出泛化能力强的候选模型;上线后,通过 A/B 测试将用户随机分流到对照组(旧模型)和实验组(新模型),比较核心指标(点击率、转化率),最终确认模型在真实环境中的增益。

04

Python 实践:划分与验证

1. 基础划分:train_test_split

Python
from sklearn.model_selection import train_test_split from sklearn.datasets import make_classification # 生成模拟数据:1000样本,2个类别,不平衡(80%/20%) X, y = make_classification(n_samples=1000, weights=[0.8, 0.2], random_state=42) # 随机划分 vs 分层划分 X_train_rand, X_test_rand, y_train_rand, y_test_rand = train_test_split( X, y, test_size=0.2, random_state=42 ) X_train_strat, X_test_strat, y_train_strat, y_test_strat = train_test_split( X, y, test_size=0.2, stratify=y, random_state=42 ) print("原始类别比例:", y.mean()) print("随机测试集比例:", y_test_rand.mean()) print("分层测试集比例:", y_test_strat.mean())

2. 交叉验证:K-Fold 与 StratifiedKFold

Python
from sklearn.model_selection import StratifiedKFold, cross_val_score from sklearn.linear_model import LogisticRegression # 分层 5-Fold 交叉验证 cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) model = LogisticRegression(max_iter=1000) scores = cross_val_score(model, X, y, cv=cv, scoring="accuracy") print("每折准确率:", scores) print("平均准确率: %.3f (+/- %.3f)" % (scores.mean(), scores.std()))

3. 可视化 K-Fold 划分

Python
import matplotlib.pyplot as plt import numpy as np def plot_cv_indices(cv, X, y, ax, n_splits): """可视化训练/验证集的划分情况""" y = np.array(y) cm = plt.cm.coolwarm for ii, (tr, tt) in enumerate(cv.split(X=X, y=y)): indices = np.array([np.nan] * len(y)) indices[tt] = 1 # 验证集 indices[tr] = 0 # 训练集 ax.scatter(range(len(indices)), [ii + .5] * len(indices), c=indices, cmap=cm, vmin=-.2, vmax=1.2, edgecolors="k", lw=2, s=60) ax.set_ylim([n_splits + .2, -.2]) ax.set_title("StratifiedKFold (n_splits=5)", fontsize=14) fig, ax = plt.subplots(figsize=(10, 4)) plot_cv_indices(cv, X, y, ax, 5) plt.xlabel("样本索引"); plt.ylabel("折数") plt.tight_layout(); plt.show()

4. 时间序列划分

Python
from sklearn.model_selection import TimeSeriesSplit # 模拟时间序列数据 X_ts = np.arange(100).reshape(-1, 1) y_ts = np.arange(100) tscv = TimeSeriesSplit(n_splits=5) for i, (train_idx, test_idx) in enumerate(tscv.split(X_ts)): print(f"Fold {i+1}: 训练集 {train_idx[0]}-{train_idx[-1]}, 测试集 {test_idx[0]}-{test_idx[-1]}")
交叉验证得分分布图
05

例题与解析

例题 1:划分数据集

某数据集共 1000 条记录,类别 A 占 70%,类别 B 占 30%。现需划分为训练集(70%)和测试集(30%)。若采用随机划分,测试集中类别 B 可能只有约 20%;若采用分层抽样,测试集中类别 B 仍保持约 30%。

问题

当样本量减小到 100 条时,随机划分导致测试集类别比例严重偏离的概率为何显著增加?

解析

小样本下随机抽样的方差更大。100条中B类仅30条,随机抽30条测试集时,B类数量服从超几何分布,标准差约为 √(30×70/100 × 70/99) ≈ 3.9,相对波动可达 ±13%。分层抽样通过强制比例约束消除了这一波动。

例题 2:执行交叉验证

使用 5-Fold 交叉验证评估某分类器,各折准确率为 [0.82, 0.85, 0.80, 0.83, 0.81]。

问题

计算平均准确率和标准差,并判断模型是否稳定。

解析

均值 μ = (0.82+0.85+0.80+0.83+0.81)/5 = 0.822;标准差 σ ≈ 0.018。各折得分波动在 ±2% 以内,说明模型稳定性较好。若 σ > 0.05,则提示数据分布不一致或模型对某些子集过拟合。

结果:平均准确率 82.2% ± 1.8%(稳定)
例题 3:避免数据泄露

某数据科学家在预测员工是否离职时,将"上月绩效评级"作为特征,并发现模型 AUC 高达 0.96。但上线后效果骤降。

问题

分析可能的泄露来源,并给出正确的特征工程顺序。

解析

"上月绩效评级"可能在员工提交离职申请后才由主管打分(带有 retrospective bias),即特征生成时间晚于标签定义时间,构成目标泄露。正确流程:(1) 按时间切分数据;(2) 只用离职时间点之前的信息构建特征;(3) 在训练集上拟合标准化器/编码器;(4) 变换测试集;(5) 评估。

例题 4:时间序列划分设计

你有一份 2020-2024 年的月度销售数据,需预测未来 3 个月的销售额。

问题

如何设计训练/验证划分以真实评估模型能力?

解析

采用滚动窗口验证:训练集逐步扩展,验证集始终紧跟训练集末端。例如:

  • Round 1: 训练 2020.01-2021.12,验证 2022.01-2022.03
  • Round 2: 训练 2020.01-2022.03,验证 2022.04-2022.06
  • Round 3: 训练 2020.01-2022.06,验证 2022.07-2022.09

最终测试集为 2024.10-2024.12,全程不可用于调参。

← 上一章:数据清洗与特征工程 下一章:回归分析 →