8.3 训练集、验证集与测试集
随着容量增加,拟合模型总能越来越细致地描述已经见过的样本。机器学习真正关心的是泛化(generalization):模型在未来部署样本上的表现。数据划分是在排练这个未来,但只有划分方式尊重观测之间的关系,这场排练才可信。
让训练、验证与测试承担不同职责
三类数据不能互换使用:
| 数据部分 | 允许承担的职责 | 不能承担的职责 |
|---|---|---|
| 训练集 | 拟合预处理状态和模型参数 | 提供无偏的最终估计 |
| 验证集 | 比较特征、模型家族、超参数和阈值 | 训练正在被评分的候选模型 |
| 测试集 | 对冻结后的工作流做一次估计 | 指导下一轮方案选择 |
只有当各行近似独立、部署人群也与采样人群相似时,随机留出才是合理起点:
from sklearn.model_selection import train_test_split
X_development, X_final_test, y_development, y_final_test = train_test_split(
X,
y,
test_size=0.20,
stratify=y,
random_state=42,
)stratify=y 会近似保持类别比例,对稀少正类很有帮助;但它不会解决行间依赖。应该根据模型部署后要面对的对象来选择划分:
- 随机划分:适合近似独立同分布的未来样本。
- 分层划分:在随机划分上保持目标比例;它不能代替分组或时间边界。
- 分组划分:让每位客户、患者、设备、门店或文档只出现在一侧。若一个实体有多行相关记录,或部署要面对未见实体,就应考虑分组。
- 时间划分:用过去训练,在更晚的数据上评估。模型要预测未来,且变量定义、行为或目标比例可能漂移时,应保持时间顺序。
如果目标是泛化到新客户,客户分组信息应作为划分元数据传入,而不是作为目标:
from sklearn.model_selection import GroupShuffleSplit
splitter = GroupShuffleSplit(
n_splits=1,
test_size=0.20,
random_state=42,
)
train_positions, test_positions = next(
splitter.split(X, y, groups=orders["customer_id"])
)
train_customers = set(orders.iloc[train_positions]["customer_id"])
test_customers = set(orders.iloc[test_positions]["customer_id"])
assert train_customers.isdisjoint(test_customers)时间划分中,仅排序有时还不够。要断言训练集最大时间早于测试集最小时间;如果特征或标签使用滑动窗口,还可能需要在边界间留出 gap,防止窗口重叠。若同一订单被派生成多行,也要让完整信息单位待在同一侧。
请保留划分索引、随机种子、各集合目标比例、时间范围和策略理由。random_state 只能让伪随机结果可复现,不能证明某一次划分具有代表性。稍后的交叉验证会揭示结论对不同验证折是否敏感。
把最终测试集当作封存证据
模型开发本来就是自适应过程:分析者看到验证结果,修改特征或超参数,再试一次。方案会刻意适应验证反馈,因此尝试足够多次后,验证结果也可能过于乐观。最终测试集只有不参与这条循环,才能提供独立检查。
一套安全顺序是:
1. 在可能影响建模选择的探索之前,就分离最终测试行。
2. 只用开发数据决定特征、预处理、模型、超参数与阈值。
3. 查看测试结果前,冻结预测契约、特征列表、完整 Pipeline、首要指标和决策规则。
4. 用允许的开发数据拟合选中工作流,只评估一次最终测试集。
5. 同时报告此前验证分布和最终测试结果,并包含重要部署切片。
模型参数与超参数跨越的边界不同。调用 fit 会从训练折学习模型参数;搜索过程根据验证折选择超参数;最后的 refit 则使用已选超参数,在全部开发数据上重新学习参数。这些步骤都不需要最终测试标签。
from sklearn.model_selection import GridSearchCV
search = GridSearchCV(
estimator=pipeline,
param_grid={"classifier__C": [0.1, 1.0, 10.0]},
scoring="average_precision",
cv=5,
refit=True,
)
search.fit(X_development, y_development)
# 只有工作流与报告计划冻结后才执行:
final_probabilities = search.best_estimator_.predict_proba(X_final_test)[:, 1]X_final_test 这个变量名能提供一点有用的“摩擦”,提醒审查者它承担特殊职责。团队还可以把测试集放入单独文件、受控表或专门评估函数,建立更强的访问边界。
查看最终测试表现但不做任何修改,属于评估;查看后改模型再查看,就属于选择。测试驱动的修改越多,这个分数越只代表模型对当前测试样本的适应,而非对未触碰未来数据的表现。此时要恢复无偏最终估计,必须取得真正的新数据;给同一测试集改名并不会清除它的使用历史。
单个留出集仍可能碰巧偏难或偏易。下一节会用 Pipeline 与交叉验证重复开发期比较,同时阻止预处理状态或实体信息跨越折边界。