4、滑动窗口与数据集构建:如何将时序数据转为监督学习、滑动窗口大小选择、生成训练/测试集
好,咱们进入第四个实战环节。
前面我们拿到了清洗后的股票数据,也做了归一化。但有个核心问题摆在我们面前:LSTM 是监督学习模型,它需要输入 X 和标签 y。而时序数据本身是一串连续的点,没有现成的「特征-标签」对。
怎么办?
答案就是——滑动窗口。
4.1 从时序到监督:窗口的本质
说白了,滑动窗口就是拿过去一段时间的值,去预测未来一个或多个时刻的值。
举个例子:
假设我们有 10 天的收盘价:[10, 11, 12, 13, 14, 15, 16, 17, 18, 19]。
如果我们设定窗口大小为 3,意思就是用前 3 天的价格预测第 4 天的价格。
那么数据集就变成这样:
| X (特征) | y (标签) |
|---|---|
| [10, 11, 12] | 13 |
| [11, 12, 13] | 14 |
| [12, 13, 14] | 15 |
| [13, 14, 15] | 16 |
| [14, 15, 16] | 17 |
| [15, 16, 17] | 18 |
| [16, 17, 18] | 19 |
你看,原本 10 个点,经过滑动窗口处理后,变成了 7 个样本。每个样本是一个长度为 3 的序列,对应一个标签。
这就是「时序转监督」的核心操作。
样本数 = 总长度 - 窗口大小
输入形状 = (样本数, 窗口大小, 特征数)
4.2 窗口大小怎么选?我的经验之谈
这个问题,我几乎每次上课都会被问到。说实话,没有标准答案。但我可以分享一些实战中的经验。
第一,看数据的周期性。
如果你做的是股票日线数据,通常有 5 日、20 日、60 日这样的均线周期。我个人习惯把窗口大小设为这些周期的整数倍。比如 5、10、20、30。
为什么?因为市场本身就有这种节奏感。你想想看,5 日均线代表一周的走势,20 日均线代表一个月的走势。模型如果能捕捉到这种节奏,效果往往更好。
第二,不要太大,也不要太小。
窗口太小,比如设为 2 或 3,模型只能看到极短期的波动,容易过拟合噪声。
窗口太大,比如设为 100,模型要记住 100 步之前的信息。虽然 LSTM 有长记忆能力,但训练会变慢,而且可能引入太多无关信息。
我一般建议:从 10 开始试,然后 20、30、60,对比验证集效果。
如果你不确定,可以先用 20。这个值在大多数金融时序任务中表现都不错。我在做期货预测时,20 日窗口经常是 baseline 的首选。
第三,考虑你的预测步长。
如果你要预测未来 1 天,窗口可以小一点。如果你要预测未来 5 天,窗口最好大一些,让模型看到更长的历史。
我曾经犯过一个错误:预测未来 7 天,窗口只设了 5。结果模型完全学不到趋势,因为 5 天窗口根本覆盖不了 7 天的波动范围。后来我把窗口调到 30,效果明显改善。
4.3 生成训练集和测试集
窗口选好了,接下来就是切分数据。
这里有个关键点:时序数据不能随机打乱。你不能像图像分类那样 shuffle 数据,因为时间顺序一旦破坏,未来信息就会泄露到训练集中。
正确的做法是:按时间顺序切分。
假设我们有 1000 天的数据,窗口大小 20:
- 前 800 个样本作为训练集(对应前 820 天的原始数据)
- 后 200 个样本作为测试集(对应后 200 天的原始数据)
注意:测试集必须完全在训练集之后,不能交叉。
我曾经在项目里不小心用了随机切分,结果模型在测试集上准确率高达 98%。当时我还挺高兴,后来发现是数据泄露了——测试集里混入了未来的信息。从那以后,我每次切分都会再三确认时间顺序。
下面是我常用的代码模板:
import numpy as np
def create_dataset(data, window_size=20):
X, y = [], []
for i in range(len(data) - window_size):
X.append(data[i:i+window_size])
y.append(data[i+window_size])
return np.array(X), np.array(y)
# 假设 data 是归一化后的收盘价,形状 (1000,)
window = 20
X, y = create_dataset(data, window)
# 按 8:2 比例切分,保持时间顺序
split = int(len(X) * 0.8)
X_train, X_test = X[:split], X[split:]
y_train, y_test = y[:split], y[split:]
print(f"训练集形状: {X_train.shape}, 测试集形状: {X_test.shape}")
# 输出: 训练集形状: (784, 20), 测试集形状: (196, 20)
嗯,这里要注意一点:X_train 的形状是 (样本数, 窗口大小)。但 LSTM 要求输入是三维的,所以后面我们还需要 reshape 成 (样本数, 窗口大小, 特征数)。这个我们下一节再细说。
4.4 多步预测的窗口设计
有时候我们想预测未来多步,比如未来 3 天的价格。这时候窗口设计要稍微调整一下。
方法有两种:
- 直接多步输出:窗口大小不变,但标签 y 变成多个值。比如用前 20 天预测未来 3 天。
- 递归预测:先预测第 1 天,然后把预测值作为输入,再预测第 2 天,依次类推。
我个人更推荐第一种,因为训练更稳定。递归预测容易累积误差,一步错步步错。
直接多步的代码实现:
def create_multistep_dataset(data, window_size=20, forecast_steps=3):
X, y = [], []
for i in range(len(data) - window_size - forecast_steps + 1):
X.append(data[i:i+window_size])
y.append(data[i+window_size:i+window_size+forecast_steps])
return np.array(X), np.array(y)
# 预测未来 3 天
X, y = create_multistep_dataset(data, window_size=20, forecast_steps=3)
print(f"X形状: {X.shape}, y形状: {y.shape}")
# 输出: X形状: (977, 20), y形状: (977, 3)
你看,y 变成了 3 列,分别对应未来第 1、2、3 天的价格。
4.5 本章知识结构图
下面我用一张图总结一下滑动窗口与数据集构建的核心流程:
这张图清晰地展示了从原始时序数据到训练/测试集的完整流程。核心就三步:滑动窗口 → 构建监督数据集 → 按时间顺序切分。
4.6 几个容易踩的坑
- 窗口大小和数据长度的关系:如果数据只有 100 条,窗口设 50,那只能生成 50 个样本,太少了。我建议窗口大小不要超过数据长度的 20%。
- 多特征时的窗口处理:如果你有多个特征(比如开盘价、收盘价、成交量),每个特征的窗口都要同步滑动。也就是说,X 的形状会变成
(样本数, 窗口大小, 特征数)。 - 预测未来多步时,标签不要包含当前时刻:有些新手会把当前时刻也放进 y 里,这相当于让模型预测已知值,训练时 loss 会很低,但实际预测完全没用。
滑动窗口是时序预测的基石。窗口大小选 20 是个不错的起点。切分数据时务必保持时间顺序。多步预测推荐直接输出多个未来值。
好了,数据集构建完成。下一节我们就要真正搭建 LSTM 模型了。到时候你会看到,刚才构建的 X_train 和 y_train 是怎么喂给神经网络的。