8.4 Pipeline、交叉验证与数据泄漏
当前工作流已经包含多个有状态操作:填补会学习替代值,缩放会学习位置与离散程度,编码会学习类别词表,估计器则学习模型参数。可信评估必须在每次划分中,把训练边界重新包住所有步骤。scikit-learn 的 Pipeline 能把这条边界变成可执行对象。
把拟合边界封装成对象
Pipeline 是一串有序转换器,最后接一个估计器。执行 fit 时,每个转换器先在输入上拟合并把转换结果交给下一步,最后估计器再拟合。执行 predict 时,已拟合转换器只对新 做转换,已拟合估计器再预测;任何步骤都不会从新行继续学习。
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
pipeline = Pipeline([
("preprocess", preprocess),
("classifier", LogisticRegression(max_iter=1000)),
])
pipeline.fit(X_train, y_train)
valid_probabilities = pipeline.predict_proba(X_valid)[:, 1]步骤名会形成参数路径。例如,classifier__C 表示 classifier 步骤的参数 C;嵌套预处理参数还可能出现多层双下划线。搜索过程因此能配置整条工作流,而不需要在外面单独拟合预处理。
正确的交叉验证顺序是:
对每一折:
克隆一条尚未拟合的 Pipeline
只用该折训练行拟合填补、缩放、编码与模型
对该折验证行只做 transform 与 predict
用验证目标为预测评分这与“先在全量数据预处理一次,再交叉验证”完全不同。假设验证折改变了全局均值,全量拟合的缩放器会把验证信息编码进每一条转换后的训练行,即使估计器从没直接看到验证标签,评估契约也已经破裂。对特征选择或目标编码来说,乐观偏差可能更大。
应把经过模式审批的原始 直接交给评估函数:
from sklearn.model_selection import StratifiedKFold, cross_validate
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
splits = list(cv.split(X_development, y_development))
scores = cross_validate(
pipeline,
X_development,
y_development,
cv=splits,
scoring={
"balanced_accuracy": "balanced_accuracy",
"average_precision": "average_precision",
},
return_train_score=True,
)cross_validate 返回的是数组,每一项对应一折。请保留这些结果。只有均值、没有标准差、范围或最差折,会掩盖不稳定模型;训练折结果也能帮助诊断持续存在的 train–validation gap,但不能代替部署切片分析。
拟合后的 Pipeline 还应成为部署对象:线上把符合声明模式的原始行交给它的 predict 或 predict_proba。若 API 另写一套预处理,就会产生训练—服务偏差(training-serving skew),线上输入转换与评估不再一致。
让折模拟部署,再主动搜查泄漏
在 折交叉验证中,数据被分为 份;每一份轮流作为验证折,其余 份训练工作流。每一行都会获得一次折外评估,折分布也能显示结论对采样变化有多敏感。5 折或 10 折只是常见约定,不是普遍定律;更多折会消耗更多计算,也修不好错误的独立性假设。
划分器必须编码部署契约:
| 部署问题 | 合理起点 | 必须验证的边界 |
|---|---|---|
| 独立新行、类别稀少 | StratifiedKFold | 类别比例合理,预测单位不重复 |
| 未见客户或设备 | GroupKFold 或 StratifiedGroupKFold | 任一组不能同时出现在训练与验证中 |
| 未来时段 | TimeSeriesSplit 或显式滚动折 | 每条训练观测都早于对应验证观测 |
对分组分类问题,应预先生成并审计各折:
from sklearn.model_selection import StratifiedGroupKFold
cv = StratifiedGroupKFold(
n_splits=5,
shuffle=True,
random_state=42,
)
splits = list(cv.split(X, y, groups=customer_id))
for train_positions, valid_positions in splits:
train_groups = set(customer_id.iloc[train_positions])
valid_groups = set(customer_id.iloc[valid_positions])
assert train_groups.isdisjoint(valid_groups)交叉验证降低了结果对单个留出集的依赖,却不会自动阻止泄漏。至少要审计四条边界:
1. 语义边界:每个特征是否真的在预测时刻可用,有没有目标代理?
2. 拟合边界:填补、缩放、编码、特征选择、重采样等数据依赖操作,是否都在每个训练折内拟合?
3. 预测单位边界:相关行、重复行、同一客户或重叠窗口能否跨折?
4. 选择边界:是否反复使用同一组交叉验证结果筛选大量人为想法?最终测试集是否仍未触碰?
分数好得不真实时,要做负面对照。移除最可疑特征,在完全相同的折上重跑;打乱 后重复整条 Pipeline,表现应降到机会基线附近;还应检查重复行哈希、组交集,以及时间、地区、数据源切片。负面对照通过并不能证明一切干净,但失败会给出明确调查线索。
不要选择“得分最高那一折”的模型。交叉验证是在同一系列折上评价每个候选,选择应基于总体水平与稳定性。如果项目反复在同一 CV 循环上同时调特征与超参数,可以用嵌套交叉验证(nested cross-validation)的外层折评估整套选择程序。它计算成本更高,也不能代替一份符合部署方式的最终测试集。
本章检查点
一套可辩护的机器学习工作流,现在由一连串显式契约组成:
决策与预测时刻
↓
目标、预测单位与期限
↓
时间上合法的特征白名单
↓
模拟部署的训练/验证/测试划分
↓
训练期预处理 + 估计器组成一条 Pipeline
↓
各折分布、泄漏审计与一次最终测试有了这些边界,后续机器学习章节就可以专注回归、分类、指标与模型解释,而不会把一个漂亮高分误认为可信证据。