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 时间序列的特殊划分

如果你处理的是股票、传感器、语音等时序数据,随机打乱切分就是作死。想想看,用未来的数据预测过去?这不科学。

我习惯用时间序列交叉验证:

  1. 固定一个起始训练窗口(比如前30天)。
  2. 预测下一个时间步(第31天)。
  3. 窗口向前滚动一位(1–31天),再预测第32天。如此反复。
  4. 最后取所有预测步的平均指标。

代码实现可以参考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 本章知识体系图

下面这张图把核心流程串起来了,帮你一图理清:

数据划分与交叉验证决策流程 原始数据集 数据量 大于多? 大 → 直接划分:训练/验证/测试 小 → 使用K折交叉验证 时序数据? Time Series Split 否 → Stratified KFold 输出稳定评估指标 (均值±标准差) 使用建议 大数据:直接三刀切 小数据:K折(K=5/10) 时序数据:TimeSeriesSplit

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:交叉验证和最终测试集混淆。 交叉验证得到的是模型在验证集上的平均表现,不代表测试集分数。我见过有人拿交叉验证分数直接当最终结果,上线后差一大截。

嗯,数据划分就是这些。记住一句话:验证集帮你找方向,测试集帮你打分,交叉验证帮你判断模型稳不稳。 这三者各司其职,别混着用。