4、数据划分:训练集、验证集、测试集的比例策略,K折交叉验证在微小模型中的应用,时间序列数据的分割陷阱

数据怎么切,这事儿看着简单,其实坑特别多。

我刚开始做模型的时候,觉得数据划分不就是按个比例随机分一下嘛。后来被现实狠狠教育了几次,才明白这里面门道有多深。尤其是做微小模型,数据本来就少,切错了直接导致模型废掉。

4.1 经典三件套:训练集、验证集、测试集

先说最基础的。训练集用来学参数,验证集用来调超参,测试集用来做最终评估。这三者的关系,说白了就是:

  • 训练集:让模型背答案
  • 验证集:看看背得怎么样,调整学习方法
  • 测试集:最终考试,不能提前偷看

比例怎么定?我个人的习惯是:

数据总量 训练集 验证集 测试集
1000条以下 70% 15% 15%
1000-10000条 80% 10% 10%
10000条以上 90% 5% 5%

注意,这只是参考。数据越少,验证集和测试集的比例可以适当调高,但别低于10%。否则评估结果方差太大,你根本分不清模型好坏。

我的小技巧: 做微小模型时,我经常把测试集固定为100-200条。不管总量多少,测试集够用就行。剩下的全扔给训练和验证。

4.2 K折交叉验证:微小模型的救命稻草

数据少的时候,单次划分太看运气了。你想想看,万一验证集里恰好都是难样本,模型得分低得离谱,你岂不是白忙活?

这时候K折交叉验证就派上用场了。

做法很简单:把数据分成K份,每次拿K-1份训练,1份验证。轮着来K次,最后取平均。我一般用5折或10折。

from sklearn.model_selection import KFold
import numpy as np

data = np.arange(100)
kf = KFold(n_splits=5, shuffle=True, random_state=42)

for fold, (train_idx, val_idx) in enumerate(kf.split(data)):
    print(f"第{fold+1}折:训练{len(train_idx)}条,验证{len(val_idx)}条")
    # 这里放你的训练代码

为什么说它对微小模型特别友好?因为:

  • 每个样本都有机会当验证集,评估更稳定
  • 你不需要单独留出一大块验证数据,训练数据更充分
  • 可以观察到模型在不同子集上的表现差异,发现数据中的异常
重要提醒: 交叉验证不是万能的。它只是评估方法,不是训练方法。最终模型还是要用全部数据重新训练一遍。

我在项目中遇到过一个问题:用5折交叉验证时,有一折的准确率突然掉到60%,其他四折都是85%以上。查了半天,发现那一折里混进了几个标注错误的数据。嗯,交叉验证帮我提前发现了数据质量问题。

4.3 时间序列数据的分割陷阱

这个坑,我踩过不止一次。

时间序列数据绝对不能随机划分。为什么?因为时间有顺序,未来的数据不能用来预测过去。你想想看,如果用明天的股价去预测今天的涨跌,这不就是作弊吗?

正确的做法是:按时间顺序切分。

# 错误示范:随机划分
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)
# 这会导致数据泄露!

# 正确做法:按时间切分
split_point = int(len(X) * 0.8)
X_train, X_test = X[:split_point], X[split_point:]
y_train, y_test = y[:split_point], y[split_point:]

我曾经做过一个传感器故障预测项目,数据是按小时采集的。一开始我用了随机划分,模型在测试集上准确率高达98%。我差点就上线了。后来同事提醒我检查一下时间顺序,才发现模型其实是在用未来的数据预测过去——说白了就是作弊。

时间序列的交叉验证也有专门的玩法:

  • 滚动窗口法:固定训练窗口大小,逐步向后滑动
  • 扩展窗口法:训练集不断累积,越往后数据越多
# 扩展窗口交叉验证示例
import numpy as np

def expanding_window_cv(data, n_splits=5):
    n = len(data)
    fold_size = n // (n_splits + 1)
    
    for i in range(1, n_splits + 1):
        train_end = i * fold_size
        val_end = (i + 1) * fold_size
        
        train_idx = np.arange(train_end)
        val_idx = np.arange(train_end, min(val_end, n))
        
        yield train_idx, val_idx

# 使用
for train_idx, val_idx in expanding_window_cv(data, n_splits=5):
    print(f"训练:{train_idx[0]}-{train_idx[-1]},验证:{val_idx[0]}-{val_idx[-1]}")
注意: 时间序列的验证集必须严格在训练集之后。不能出现训练集包含未来数据的情况。另外,如果数据有周期性(比如每周规律),切分时要注意不要破坏周期完整性。

4.4 总结一下我的经验

数据划分这事儿,说白了就三条:

  1. 数据够多:用经典70/15/15,省事
  2. 数据少:上K折交叉验证,别偷懒
  3. 有时序:按时间切,别随机

最后说一句:划分完数据后,记得检查一下各个集合的分布是否一致。我见过有人切完数据,训练集全是晴天数据,测试集全是雨天数据——模型能不崩吗?

嗯,数据划分看似基础,但做对了能省下后面调参的无数时间。别嫌麻烦,这一步值得认真对待。