4. 训练/验证/测试集划分与交叉验证策略
我刚开始调参那会儿,最常犯的错误就是拿全量数据训练,然后看个准确率就交差了。结果呢?换到新数据上直接翻车。后来我才明白——数据划分才是调参的基石。这章咱们就聊聊怎么把数据切明白,以及交叉验证到底该怎么玩。
4.1 三刀切:训练集、验证集、测试集
很多人只知道「训练集和测试集」,但其实中间还藏着个验证集。我个人习惯这样分:
- 训练集:喂给模型学参数的,占比最大。
- 验证集:用来选超参数、调结构的。验证集不参与训练,但参与决策。
- 测试集:最后统一打分的,只能用一次。
我曾经踩过坑:有一回偷懒,用测试集反复调参,结果模型在测试集上指标特别好,上线直接崩。测试集一旦被污染,它就不再是「未知数据」了。所以,测试集一定要锁在保险柜里,调参期间绝不碰它。
标准划分比例呢?我一般这么配:
| 数据集 | 常见比例 | 典型数量(万条) | 用途 |
|---|---|---|---|
| 训练集 | 60%–80% | 6–8 | 学习参数 |
| 验证集 | 10%–20% | 1–2 | 调超参、选模型 |
| 测试集 | 10%–20% | 1–2 | 最终评估 |
数据量小的时候,比如几千条,按百分比分可能会让验证集或测试集样本太少,评估结果方差巨大。那怎么办呢?——用交叉验证。
4.2 K折交叉验证:小数据的救星
说白了,K折就是把数据平分成K份,每次拿1份当验证集,剩下K-1份当训练集。循环K次,最后取平均分数。这样每一条数据都有机会当验证样本。
我常用的K值:
- K=5:适用大多数场景,方差和偏差平衡得好。
- K=10:数据量较小(几千条)且对评估精度有较高要求时用。我记得有一回做医学影像分类,样本只有2000张,我用10折交叉验证,评估结果稳定很多。
- K=2:极快但粗糙,适合快速验证想法。
小技巧:K折要做分层采样(Stratified),尤其分类任务。简单随机切分可能导致某一折里全是A类,另一折全是B类,模型直接学废了。Scikit-learn的
StratifiedKFold帮我省了不少事。
咱们看个代码示例:
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.ensemble import RandomForestClassifier
import numpy as np
X = ... # 特征
y = ... # 标签(0/1二分类)
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
model = RandomForestClassifier(n_estimators=100)
scores = cross_val_score(model, X, y, cv=skf, scoring='accuracy')
print("各折准确率:", scores)
print("平均准确率: {:.4f} ± {:.4f}".format(np.mean(scores), np.std(scores)))
输出类似这样:
各折准确率: [0.8320 0.8195 0.8440 0.8275 0.8380]
平均准确率: 0.8322 ± 0.0087
你看,标准差才0.0087,说明模型表现比较稳定。如果标准差超过0.05,就得怀疑数据划分或者模型本身有毛病了。
4.3 留一法(LOOCV):极端但精准
当数据量极小(比如几十条),K折可能还不够用。那可以用留一法:每个样本单独当验证集一次,剩下N-1条训练。这样评估最无偏,但计算代价极高——N个样本就要训练N次模型。
我个人建议:少于100条且模型训练很快(比如逻辑回归)时才用留一法。千万别对深层网络用留一法,训练一次都要几小时,100次就是几百小时,直接崩溃。
4.4 时间序列的特殊划分
如果你处理的是股票、传感器、语音等时序数据,随机打乱切分就是作死。想想看,用未来的数据预测过去?这不科学。
我习惯用时间序列交叉验证:
- 固定一个起始训练窗口(比如前30天)。
- 预测下一个时间步(第31天)。
- 窗口向前滚动一位(1–31天),再预测第32天。如此反复。
- 最后取所有预测步的平均指标。
代码实现可以参考Scikit-learn的TimeSeriesSplit:
from sklearn.model_selection import TimeSeriesSplit
import numpy as np
X = np.arange(100).reshape(-1, 1)
y = np.random.rand(100)
tscv = TimeSeriesSplit(n_splits=5)
for train_index, test_index in tscv.split(X):
print("TRAIN:", train_index, "TEST:", test_index)
输出将展示训练集永远在测试集前面,不会泄露未来信息。
4.5 本章知识体系图
下面这张图把核心流程串起来了,帮你一图理清:
4.6 终极建议:什么时候用哪种策略
咱们总结一张表,以后直接瞄一眼就行:
| 场景 | 推荐方案 | 备注 |
|---|---|---|
| 数据量 ≥ 10万条,分类均衡 | 简单划分(70/15/15) | 单次验证就够了 |
| 数据量 1千–10万条 | 5折分层交叉验证 | 平衡效率与稳定性 |
| 数据量 < 1000条 | 10折或留一法 | 留一法更谨慎,但训练慢 |
| 时序数据(任何量级) | TimeSeriesSplit | 绝对不要随机打乱 |
| 严重类别不平衡 | 分层采样 + 交叉验证 | 保证每折类比例一致 |
我自己的习惯:不管项目大小,我都会先用一次5折交叉验证跑个基线。如果5折的分数波动大(std > 0.03),那说明数据或者模型本身不稳定。这时候不要急着调参,先回头去看看数据有没有脏值、标签有没有错。
4.7 避坑指南(我踩过的坑)
- 坑1:忘记shuffle。 如果原始数据是按类别顺序排列的(比如前50%是A类,后50%是B类),不shuffle直接切分可能导致验证集只有一类。一定要设
shuffle=True。 - 坑2:将数据标准化/归一化放在划分之前。 应该先划分,再在训练集上计算均值和标准差,然后用训练集的统计量去变换验证集和测试集。否则会引入未来信息。
- 坑3:交叉验证和最终测试集混淆。 交叉验证得到的是模型在验证集上的平均表现,不代表测试集分数。我见过有人拿交叉验证分数直接当最终结果,上线后差一大截。
嗯,数据划分就是这些。记住一句话:验证集帮你找方向,测试集帮你打分,交叉验证帮你判断模型稳不稳。 这三者各司其职,别混着用。