为什么不能用测试集调模型?一次看懂训练、验证与数据泄漏
用十个样本和标准化手算,解释训练集、验证集、测试集的职责边界,掌握无泄漏的数据划分与 sklearn Pipeline。
上一篇中,我们把训练写成经验风险最小化:模型不断调整参数,让训练样本上的平均损失下降。
但这立刻带来一个问题:同一批数据既参与学习,又负责给模型打分,这个分数还能代表未来表现吗?
通常不能。模型可能记住训练样本,研究者也可能在反复试验中“记住”测试集。今天只讲透一件事:怎样划分数据,才能得到可信的泛化评估。
01 三份数据不是简单切三刀#
监督学习中,三类数据承担不同职责:
| 数据 | 允许做什么 | 不允许做什么 |
|---|---|---|
| 训练集(Training Set) | 学习权重、偏置和预处理统计量 | 代表最终泛化成绩 |
| 验证集(Validation Set) | 选择模型、学习率、正则化强度、停止轮次 | 参与参数拟合 |
| 测试集(Test Set) | 所有方案确定后做一次最终验收 | 反复查看并据此修改方案 |
这里要区分两类参数:
- Parameter(模型参数):由训练算法直接学到,如神经网络权重 。
- Hyperparameter(超参数):训练前或训练外做出的选择,如学习率、树深度、隐藏层宽度。
训练集学习参数,验证集帮助选择超参数,测试集估计最终方案面对未知数据时的表现。
原始数据
│
├── 训练集 ──► 拟合参数 ───────────────┐
│ │
├── 验证集 ──► 比较方案、选择超参数 ───┤ 循环
│ │
└── 测试集 ──► 最终一次评估 ◄──────────┘
▲
方案冻结后才使用text02 用十个样本看懂“选择偏差”#
假设只有 10 个按编号排列的样本:
样本编号: 1 2 3 4 5 6 7 8 9 10
划分结果: T T T T T T V V E EtextT:6 个训练样本。V:2 个验证样本。E:2 个测试样本。
现在训练三个模型:
| 模型 | 训练误差 | 验证误差 | 测试误差 |
|---|---|---|---|
| A:简单 | 0.20 | 0.22 | 未查看 |
| B:中等 | 0.08 | 0.12 | 未查看 |
| C:复杂 | 0.00 | 0.30 | 未查看 |
训练误差会偏爱复杂模型 C,但验证误差提示 B 更可能泛化。因此我们选择 B,冻结全部方案,最后才查看测试集。
假设 B 的测试误差是 0.15,这就是当前最可信的泛化估计。
如果不满意 0.15,又回去增加特征、换模型,直到测试误差降到 0.08,会怎样?你已经针对这两个测试样本完成了人工调参。即使模型权重没有直接用测试集训练,人的决策过程也对测试集发生了拟合。
这叫 Selection Bias(选择偏差):尝试的方案越多,越容易偶然找到一个在固定测试集上表现很好的方案。
交互判断:测试集只看了一次,但看完后改了数据增强,算泄漏吗?
算。数据增强是训练方案的一部分,你根据测试结果改变它,测试信息便参与了方案选择。应重新准备未被查看的测试集,或明确承认原测试集已降级为验证集。
03 数据泄漏到底泄漏了什么?#
Data Leakage(数据泄漏):训练或模型选择阶段使用了在真实预测时不可能获得的信息,导致离线指标过于乐观。
它不只意味着“把测试标签放进训练集”。更隐蔽的情况是:在划分数据前,对全部样本计算均值、做特征选择或填补缺失值。
一个可以手算的标准化泄漏#
训练数据只有两个值:
测试数据只有一个值:
标准化公式是:
其中 和 分别是均值与标准差,它们不是固定常量,而是需要从数据中估计的预处理参数。
正确做法只用训练集:
因此:
测试值 100 是明显的分布外样本,这个事实被保留下来。
如果先合并全部数据再标准化:
此时训练样本的变换方式已经受测试样本 100 影响。模型在训练时虽然没看到测试标签,却提前获得了测试分布的信息。这就是泄漏。
关键规则只有一句:
任何需要通过
fit学习统计量的步骤,都只能在训练数据上fit;验证集和测试集只能transform。
04 随机划分并不总是正确#
train_test_split 很方便,但它默认随机打乱样本。只有当样本近似独立同分布时,随机划分才合理。
IID(Independent and Identically Distributed,独立同分布):样本彼此近似独立,并来自同一个概率分布。
下面三类任务需要更谨慎:
时间序列:必须尊重时间方向#
用 2026 年数据训练,却随机把部分 2025 年样本放进测试集,可能让模型间接“看到未来”。合理划分是:
过去 ─────────────────────────────► 未来
[ Train ][Validation][ Test ]text同一主体有多条记录:按组划分#
医学数据中,同一患者的多张影像高度相似。如果其中一些进入训练集、另一些进入测试集,模型可能识别患者而不是疾病。应让同一患者只属于一个集合。
分类比例失衡:考虑分层#
若正样本只有 5%,纯随机划分可能让小验证集几乎没有正样本。**Stratified Split(分层划分)**会尽量保持各集合的类别比例。
| 数据结构 | 推荐方法 | 关键约束 |
|---|---|---|
| 普通表格 IID 数据 | 随机或分层划分 | 固定随机种子 |
| 时间序列 | 按时间向前划分 | 训练不能使用未来 |
| 患者、用户、设备多记录 | Group Split | 同一组不能跨集合 |
| 小样本 | 交叉验证 | 仍保留独立测试集 |
05 从原则落到 sklearn 代码#
下面以二分类为例。输入输出形状是:
X: [num_samples, num_features]
y: [num_samples]
model.predict(X_test): [num_test_samples]
model.predict_proba(X_test): [num_test_samples, 2]text先划分,再做任何需要拟合的预处理#
from sklearn.model_selection import train_test_split
X_train_val, X_test, y_train_val, y_test = train_test_split(
X,
y,
test_size=0.20,
random_state=42,
stratify=y,
)
X_train, X_val, y_train, y_val = train_test_split(
X_train_val,
y_train_val,
test_size=0.25, # 0.25 × 0.80 = 原数据的 0.20
random_state=42,
stratify=y_train_val,
)python最终比例是 60% 训练、20% 验证、20% 测试。
random_state=42:让划分可复现,不表示 42 有特殊统计意义。stratify=y:让类别比例尽量一致。test_size:可以是比例,也可以是样本数量。
用 Pipeline 把防泄漏规则写进代码#
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import log_loss, accuracy_score
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
model = make_pipeline(
StandardScaler(),
LogisticRegression(C=1.0, max_iter=1000),
)
model.fit(X_train, y_train)
val_prob = model.predict_proba(X_val)
val_pred = model.predict(X_val)
print("val log loss:", log_loss(y_val, val_prob))
print("val accuracy:", accuracy_score(y_val, val_pred))pythonPipeline.fit(X_train, y_train) 会先让 StandardScaler 只在训练集上计算均值和标准差,再把变换后的训练数据交给分类器。调用 predict(X_val) 时,Pipeline 复用训练阶段保存的统计量,不会在验证集上重新拟合。
确定 C、特征和模型结构后,常见的最终流程是用训练集与验证集重新拟合一次,再在测试集上验收:
final_model = make_pipeline(
StandardScaler(),
LogisticRegression(C=best_c, max_iter=1000),
)
final_model.fit(X_train_val, y_train_val)
test_pred = final_model.predict(X_test)
test_accuracy = accuracy_score(y_test, test_pred)
print("final test accuracy:", test_accuracy)python测试指标应与模型版本、数据版本、划分规则和随机种子一起记录。
06 小数据怎么办?#
数据很少时,单个验证集的结果可能高度依赖“恰好分到了哪些样本”。这时可以使用 Cross-Validation(交叉验证):把训练数据分成 份,轮流用一份验证、其余训练,最后汇总 次结果。
Fold 1: [Val][Train][Train][Train][Train]
Fold 2: [Train][Val][Train][Train][Train]
Fold 3: [Train][Train][Val][Train][Train]
Fold 4: [Train][Train][Train][Val][Train]
Fold 5: [Train][Train][Train][Train][Val]text交叉验证替代的是固定验证集,不是独立测试集。所有超参数确定后,测试集仍只用于最后评估。
07 真正做项目时的检查清单#
- 先定义预测时刻。 问清楚模型做预测时,哪些字段确实已经存在。
- 先划分,后预处理。 缺失值填补、标准化、PCA、特征选择都遵守这一顺序。
- 按真实部署单位划分。 患者、用户、设备、场站和时间都可能是隔离单位。
- 保留数据划分清单。 保存样本 ID,而不只是保存随机种子。
- 不要盯着单一指标。 同时观察均值、方差、类别指标和置信区间。
- 测试集设访问边界。 大型项目中可由独立脚本或负责人执行最终测试。
08 什么时候仍会失败?#
即使数据完全无泄漏,测试结果仍可能失真:
- 测试集太小,估计方差很大。
- 测试数据与上线流量不是同一分布。
- 数据在时间上发生 Distribution Shift(分布漂移)。
- 指标与业务目标不一致,例如只追求准确率却忽视少数类召回率。
- 反复发表和比较结果,公共测试集逐渐被整个社区“过拟合”。
因此,正确划分不是终点,而是可信实验的最低条件。
09 今天真正需要记住什么?#
- 训练集学习参数,验证集选择方案,测试集只做最终验收。
- 看过测试结果并据此修改方案,就已经对测试集发生了拟合。
- 标准化、填补缺失值和特征选择也会学习参数,必须只在训练集上
fit。 - 时间、患者、用户等结构决定划分方式,随机切分不是万能答案。
- Pipeline 能把预处理与模型绑定,降低交叉验证和部署中的泄漏风险。
10 思考题与小练习#
练习 1:找出泄漏
某项目先对全部 10,000 个样本做 PCA,再划分训练集和测试集。问题在哪里?PCA 的主轴使用了测试集分布。正确做法是先划分,再只用训练集拟合 PCA;使用 Pipeline 能在交叉验证的每一折中正确执行。
练习 2:应该随机划分吗?
你要用过去 30 天的传感器数据预测下一小时是否故障。应按时间划分,训练使用更早数据,验证和测试使用更晚数据;随机划分会把未来状态混入训练。
练习 3:计算两次切分比例
第一次留下 20% 测试集,第二次从剩余 80% 中留下 25% 验证集。最终训练、验证、测试比例是多少?答案是 60%、20%、20%。
相关工作#
- scikit-learn: Common pitfalls and recommended practices ↗:官方的数据泄漏案例与防范原则。
- scikit-learn: train_test_split ↗:随机、分层划分接口及参数说明。
- scikit-learn: Pipeline ↗:将预处理与估计器组合,避免交叉验证中的泄漏。
- Kohavi: A Study of Cross-Validation and Bootstrap ↗:比较交叉验证与 Bootstrap 的经典研究。
- Sculley et al.: Hidden Technical Debt in Machine Learning Systems ↗:从系统角度讨论数据依赖和实验债务。
11 下一篇预告#
有了可信的数据边界,下一步才能讨论“模型究竟应该优化什么”。下一篇将聚焦损失函数:为什么评价指标是准确率时,分类模型通常不直接优化准确率;以及一个可优化的损失需要具备哪些性质。