4、数据划分:训练集、验证集、测试集的比例策略,K折交叉验证在微小模型中的应用,时间序列数据的分割陷阱
数据怎么切,这事儿看着简单,其实坑特别多。
我刚开始做模型的时候,觉得数据划分不就是按个比例随机分一下嘛。后来被现实狠狠教育了几次,才明白这里面门道有多深。尤其是做微小模型,数据本来就少,切错了直接导致模型废掉。
4.1 经典三件套:训练集、验证集、测试集
先说最基础的。训练集用来学参数,验证集用来调超参,测试集用来做最终评估。这三者的关系,说白了就是:
- 训练集:让模型背答案
- 验证集:看看背得怎么样,调整学习方法
- 测试集:最终考试,不能提前偷看
比例怎么定?我个人的习惯是:
| 数据总量 | 训练集 | 验证集 | 测试集 |
|---|---|---|---|
| 1000条以下 | 70% | 15% | 15% |
| 1000-10000条 | 80% | 10% | 10% |
| 10000条以上 | 90% | 5% | 5% |
注意,这只是参考。数据越少,验证集和测试集的比例可以适当调高,但别低于10%。否则评估结果方差太大,你根本分不清模型好坏。
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 总结一下我的经验
数据划分这事儿,说白了就三条:
- 数据够多:用经典70/15/15,省事
- 数据少:上K折交叉验证,别偷懒
- 有时序:按时间切,别随机
最后说一句:划分完数据后,记得检查一下各个集合的分布是否一致。我见过有人切完数据,训练集全是晴天数据,测试集全是雨天数据——模型能不崩吗?
嗯,数据划分看似基础,但做对了能省下后面调参的无数时间。别嫌麻烦,这一步值得认真对待。