第2章:数据集划分艺术:训练集、验证集、测试集的黄金比例与分层采样策略
说实话,我见过太多新手一上来就把数据随便切三份,然后跑模型。结果呢?模型在测试集上表现惊艳,一上线就崩了。这种坑,我踩过不止一次。
数据集划分,说白了就是给你的模型安排「考试规则」。你想想看,如果考试前就把答案泄露了,那成绩再好也是假的。模型评估也是这个道理。
2.1 三个集合,各司其职
我们先搞清楚这三兄弟分别干嘛的:
- 训练集:模型学习的「课本」。模型从这里学知识、找规律。
- 验证集:模型调优的「模拟考」。你调超参数、选特征、做早停,都得看它的脸色。
- 测试集:最终「高考」。只有这一次,你才能知道模型真正的泛化能力。
⚠️ 我曾经犯过的错:有一次做时间序列预测,我不小心把测试集的信息泄露到了训练集里。模型在测试集上准确率高达98%,结果上线后直接掉到60%。后来排查了三天才发现是数据切分时没考虑时间顺序。从那以后,我对数据划分就格外小心了。
2.2 黄金比例:到底怎么分?
很多人喜欢问:「训练集、验证集、测试集的比例到底多少最合适?」
我的回答是:看数据量。
| 数据规模 | 训练集 | 验证集 | 测试集 | 我的建议 |
|---|---|---|---|---|
| 小样本(<1万) | 60% | 20% | 20% | 考虑交叉验证 |
| 中等规模(1万-10万) | 70% | 15% | 15% | 常用比例 |
| 大规模(10万-100万) | 80% | 10% | 10% | 验证集可再小点 |
| 超大规模(>100万) | 98% | 1% | 1% | 验证集1万条就够 |
我个人习惯是:数据量越大,验证集和测试集的比例可以越小。为什么?因为1万条验证数据已经能给出足够稳定的评估了,没必要浪费训练数据。
💡 小技巧:如果你做的是深度学习,而且数据量很大(比如百万级),我建议验证集固定为1万条左右就够了。这样既保证评估稳定,又不浪费训练数据。
2.3 分层采样:别让分布失衡
嗯,这里要注意。如果你直接随机切分,可能会遇到一个尴尬的情况:
比如你有一个二分类问题,正样本只占5%。随机切分后,验证集里可能一个正样本都没有。那模型在验证集上的准确率永远是95%,你根本看不出模型好坏。
这就是分层采样要解决的问题。
# 我常用的分层采样代码
from sklearn.model_selection import train_test_split
# 假设 y 是标签,X 是特征
X_train, X_temp, y_train, y_temp = train_test_split(
X, y,
test_size=0.3, # 先分出30%作为临时集
stratify=y, # 分层采样!
random_state=42
)
# 再从临时集中分出验证集和测试集
X_val, X_test, y_val, y_test = train_test_split(
X_temp, y_temp,
test_size=0.5, # 各占15%
stratify=y_temp, # 继续分层!
random_state=42
)
print(f"训练集类别分布: {np.bincount(y_train) / len(y_train)}")
print(f"验证集类别分布: {np.bincount(y_val) / len(y_val)}")
print(f"测试集类别分布: {np.bincount(y_test) / len(y_test)}")
你看,加了 stratify=y 之后,三个集合的类别分布会保持一致。这在分类问题中几乎是必须的。
2.4 特殊场景的处理
实际项目中,数据划分远不止「随机切分」这么简单。我遇到过几种特殊情况:
- 时间序列数据:绝对不能随机切分!必须按时间顺序,用前70%做训练,中间15%做验证,最后15%做测试。否则就是数据泄露。
- 分组数据:比如同一个用户的多条记录。如果你随机切分,同一个用户的数据可能同时出现在训练集和测试集里。这会导致模型「作弊」。要用
GroupKFold保证同一组数据只出现在一个集合中。 - 极度不平衡数据:正样本只有0.1%时,分层采样可能还不够。我建议在训练集中做上采样或下采样,但验证集和测试集要保持原始分布。
🔑 核心要点:数据集划分不是简单的「三七分」。你要考虑数据分布、时间顺序、分组结构。一个好的划分,能让你的模型评估结果更接近真实上线表现。
2.5 交叉验证:小数据的救星
如果你的数据量很小(比如几千条),那单次划分的验证结果可能很不稳定。这时候我会用交叉验证。
from sklearn.model_selection import StratifiedKFold
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)):
X_train_fold = X[train_idx]
y_train_fold = y[train_idx]
X_val_fold = X[val_idx]
y_val_fold = y[val_idx]
# 训练模型
model.fit(X_train_fold, y_train_fold)
# 评估
score = model.score(X_val_fold, y_val_fold)
print(f"Fold {fold+1}: {score:.4f}")
print(f"平均得分: {scores.mean():.4f} ± {scores.std():.4f}")
交叉验证的好处是:每个样本都有机会做验证。但代价是训练时间翻倍。我一般只在数据量小于1万时用。
2.6 实战建议总结
最后,给你几个我踩坑总结出来的建议:
- 先划分,再预处理:千万别先做归一化、缺失值填充,再划分数据。否则测试集的信息会泄露到训练集里。正确的做法是:先划分,再在训练集上计算均值和标准差,然后用同样的参数去变换验证集和测试集。
- 固定随机种子:每次跑实验都用同一个
random_state。这样你才能复现结果,也才能确定模型改进是真的有效,而不是数据划分的随机波动。 - 保留一份「终极测试集」:我习惯把测试集藏起来,只在最终评估时用一次。平时调参只看验证集的结果。这样能避免「对着测试集调参」的陷阱。
- 考虑业务场景:比如做欺诈检测,你的测试集应该模拟真实场景中的正样本比例。如果真实场景中欺诈率是0.1%,那测试集也应该保持这个比例。
数据集划分,看起来是个小步骤,但它的影响贯穿整个模型开发流程。我见过太多项目因为这一步没做好,导致后续所有评估都不可靠。所以,花点时间把这一步做扎实,绝对值得。