第3章 数据划分原则:训练集、验证集、测试集
做机器学习的人都知道,数据怎么切,直接影响模型到底靠不靠谱。我刚入行那会儿,组里有个老哥直接把所有数据扔进模型,调完参数就用同一批数据评估,结果上线后准确率直接腰斩。嗯,这其实就是没把训练集、验证集、测试集分清楚。
今天这章,咱们就把这三兄弟的职责、切法、坑位一次性捋清楚。你想想看,如果连评估数据都污染了,后面的特征工程、模型调优不就成了瞎子摸象?
三个角色,各司其职
先给个直观的对比。我习惯用一句话记忆:训练集是学生做题用的课本,验证集是模拟考,测试集是最终高考。
| 数据集 | 用途 | 是否参与训练 | 常见误用 |
|---|---|---|---|
| 训练集 (Training) | 拟合模型参数 | 是 | 过拟合后又在验证集上“调优” |
| 验证集 (Validation) | 调超参、选模型、早停 | 否(但间接影响) | 把它当测试集用,反复调参导致信息泄露 |
| 测试集 (Test) | 最终评估泛化能力 | 否 | 在调参过程中偷看测试结果 |
说白了,验证集是你和模型之间的裁判,测试集是送审的最终评委。裁判可以反复沟通,评委只能见一次面。
切分比例,不是拍脑袋
我见过很多入门教程直接写“70/15/15”,但实际项目里比例得看数据量和任务复杂度。下面是我常用的参考表:
| 总样本量 | 典型切分(训练/验证/测试) | 注意事项 |
|---|---|---|
| 小于 1 万 | 60% / 20% / 20% | 尽量用交叉验证代替固定验证集 |
| 1 万 ~ 10 万 | 70% / 15% / 15% | 验证集足够评估超参 |
| 10 万 ~ 100 万 | 80% / 10% / 10% | 验证集可进一步缩减 |
| 大于 100 万 | 90% / 5% / 5% | 甚至 95% / 2.5% / 2.5% |
注意,这里有一个关键原则:验证集和测试集必须来自与训练集相同的分布,但绝对不能有任何交叉。有一次我在项目中把清洗后的数据直接随机切分,结果发现验证集里的某些样本其实是训练集中同一条用户的另一条记录——这对时间序列任务来说就是严重泄露。
分层采样:让切分更公平
分类问题里,如果原始数据本身不平衡(比如正负样本比 1:9),简单随机切分很可能让验证集或测试集变成纯负样本。我建议用 sklearn.model_selection.train_test_split 的 stratify 参数:
from sklearn.model_selection import train_test_split
X_temp, X_test, y_temp, y_test = train_test_split(
X, y, test_size=0.2, stratify=y, random_state=42
)
X_train, X_val, y_train, y_val = train_test_split(
X_temp, y_temp, test_size=0.1875, stratify=y_temp, random_state=42
)
这里先切出 20% 作为测试集,再在剩余 80% 中按类别比例分出验证集(0.1875 = 15% / 80%)。这样三个集合的类别分布都跟原始一致。记得固定 random_state,不然每次跑结果不一样,调试时你会怀疑人生。
时间序列:小心未来信息
如果是股市、传感器、日志这类时序数据,千万别用随机切分。我吃过一次亏:用随机切分训练了一个股价预测模型,验证集上的 RMSE 低得离谱,结果上线后发现模型学到了“未来数据”…… 因为随机切分把未来时间点的样本混进了训练集。
正确做法是按时间先后切分:
import pandas as pd
df_sorted = df.sort_values('timestamp')
split_date_val = df_sorted['timestamp'].quantile(0.7)
split_date_test = df_sorted['timestamp'].quantile(0.85)
train = df_sorted[df_sorted['timestamp'] < split_date_val]
val = df_sorted[(df_sorted['timestamp'] >= split_date_val) & (df_sorted['timestamp'] < split_date_test)]
test = df_sorted[df_sorted['timestamp'] >= split_date_test]
甚至可以考虑滚动窗口交叉验证,但最基本的底线就是:训练集的时间必须早于验证集,验证集必须早于测试集。
避坑指南——我曾经犯过的错
我曾经在一个风控项目里,为了图方便,用整个数据集做了特征选择,然后再划分训练/验证/测试。结果验证集和测试集的表现虚高,实际上模型是“看了全卷答案再做选择题”。这个叫 数据泄露 (Data Leakage),是新手最容易踩的坑。
解决方案很简单:在划分数据之前,不做任何全局统计操作。例如归一化、缺失值填充、特征选择,都要在训练集上拟合后,再应用到验证集和测试集。说白了,验证集和测试集从划分出来那一刻起,就应该被当作“未来数据”来对待。
黄金法则:测试集只能被触碰一次,且必须在模型完全固定之后。
小技巧:如果你用 PyTorch 或 TensorFlow,可以在 DataLoader 里定义一个 val_split 参数,自动从训练集尾部切分。但一定要确保 shuffle=False 且按时间顺序。
警告:不要只用一次随机种子就认为划分没问题。建议用 5~10 个不同随机种子运行实验,观察验证集和测试集性能的波动范围。如果方差过大,说明你的数据量太少或分布不稳定,需要更严格的交叉验证。
全流程一图胜千言
下面这张图总结了我们刚才讲的核心逻辑:原始数据经过分层/时间顺序切分,拆成三个集合,各自承担不同任务,且验证集和测试集不能倒流回训练过程。
这张图里有个细节:虚线箭头表示验证集的反馈只能作用于训练过程(调超参),不能修改测试集。反向的红色箭头则强调测试集必须保持“被隔离”状态。
我建议你在每次实验开始前,先花 10 分钟把这三份数据的切分逻辑写进代码注释里,或者用配置文件明确定义。别小看这个习惯——它能帮你省掉事后排查“为什么测试集分数一直不降”的无数时间。嗯,我自己吃过亏才养成的这个习惯。