上一篇确定了预测时刻(Issue Time)的信息边界:站在时刻 ,只用当时可获得的信息预测未来。本篇要把一条连续的序列,变成一组有明确输入和标签的训练样本。
滑动窗口看起来只是切片,但窗口长度、移动步长和时间边界会同时影响样本数量、计算成本与评估结论。
1. 问题:一条序列如何变成训练样本?
假设我们有一条逐小时负荷序列。在本系列的固定窗口任务中,每个样本都应对应一次真实可执行的预测,输入也不能随意包含未来数据:
过去 L 小时的负荷 | 未来 H 小时的负荷
输入 | 标签
↑
Issue Time
对于本系列的建筑负荷案例,目标是预测未来 24 小时,因此 。回看多久()需要结合日周期、周周期和验证集表现决定。例如 表示回看过去 7 天,它是一个候选值,而不是预先认定的最优值。
2. 直觉:沿时间轴移动同一个观察框
先看一个只有 10 个观测值的例子。取 Lookback 、Horizon ,每次向前移动两格:
时间位置 0 1 2 3 4 5 6 7 8 9
第 1 个窗口 [0 1 2 3] [4 5]
第 2 个窗口 [2 3 4 5] [6 7]
第 3 个窗口 [4 5 6 7] [8 9]
输入 L=4 标签 H=2
方括号中的数字是时间位置,不是负荷值。第二个窗口站在位置 5 之后发起预测;此时位置 4、5 的真实值已经成为历史,可以作为输入。第一个窗口的标签与第二个窗口的输入使用了同一段观测,这本身不构成泄漏;前提是每个窗口都只使用其 Issue Time 已知的信息,并且训练与评估的划分正确。
3. 方法与数学定义
3.1 Lookback、Horizon 与 Stride
设序列为 ,共 个观测值。用零起始索引 表示窗口起点,定义:
这个样本的 Issue Time 是 :输入最晚到此为止,标签从下一个时间点开始。三个参数分别控制:
| 参数 | 含义 | 建筑负荷案例中的问题 |
|---|---|---|
| :Lookback | 每个样本包含多少个历史观测 | 看过去 1 天、1 周,还是更久? |
| :Horizon | 每次预测多少个未来观测 | 本系列固定为未来 24 小时 |
| :Stride | 相邻窗口的起点相隔多少个观测 | 每小时建一个样本,还是每天一个? |
当起点依次为 时,最后一个窗口必须满足 。因此,若 ,样本数为:
若 ,一个完整样本也构造不出来,。上面的 10 个观测值在 时产生 个样本。
3.2 Stride 改变了哪些重叠?
当 时,相邻窗口的输入大量重叠;若 ,标签也会重叠。这样能从有限历史中构造更多训练样本,但样本之间高度相关,不能把窗口个数直接当成独立观测的个数。
当 时,相邻窗口的标签区间不重叠。如果 ,输入区间仍然重叠。因此,“Stride 等于 Horizon 就得到完全独立的样本”是错误的。
Stride 还有两个使用场景,不能混为一谈:
- 训练步长决定从训练期生成多少学习样本;
- 评估或线上发起频率决定隔多久真正发布一次预测。
例如,训练时每小时取一个窗口(),线上每天 00:00 预测未来 24 小时,是完全可行的。评估时则应模拟每天 00:00 的发起频率,而不是把训练窗口的步长自动当成线上调度频率。如果线上确实每小时发布一次 24 小时预测,评估也可以使用每小时一个 Issue Time;此时不同预测会覆盖相同的未来时刻,需要说明指标的汇总口径。
3.3 多变量输入和未来已知特征
窗口不一定只包含目标历史。对建筑 在时刻 发起的预测,输入还可以包含:
历史窗口:负荷、截至 t 的天气观测
未来窗口:t+1 到 t+H 的日历特征、当时可获得的天气预报
静态特征:建筑用途、面积
标签窗口:t+1 到 t+H 的真实负荷
这几类数据的时间切片不同。尤其要避免把未来实测天气切入输入窗口:离线文件虽然保存了这些值,但 Issue Time 并不知道它们。天气预报的来源与误差将在 Part 6 讨论。
4. 一个最小实现
先用单变量列表验证索引和样本数量。数据清洗、时间戳对齐和多变量拼接会在后续 Part 实现。
def make_windows(values, lookback, horizon, stride=1):
if min(lookback, horizon, stride) < 1:
raise ValueError("lookback, horizon and stride must be positive")
windows = []
last_start = len(values) - lookback - horizon
for start in range(0, last_start + 1, stride):
x = values[start : start + lookback]
y = values[start + lookback : start + lookback + horizon]
windows.append((x, y))
return windows
examples = make_windows(list(range(10)), lookback=4, horizon=2, stride=2)
for x, y in examples:
print(x, "->", y)
输出应为:
[0, 1, 2, 3] -> [4, 5]
[2, 3, 4, 5] -> [6, 7]
[4, 5, 6, 7] -> [8, 9]
如果输入不足 个观测,函数返回空列表。真实数据还需要检查时间戳是否连续、缺失值如何处理,以及每个窗口是否属于同一栋建筑;否则“相邻两行”未必真是相邻两个小时。
5. 如何选择参数?
先由任务决定 Horizon:本系列需要一次给出未来一天逐小时负荷,因此 。Lookback 再作为可比较的设计选择,例如 24、168、336 小时,分别覆盖一天、一周、两周历史。
选择 时,需要同时考虑:
- 周期信息:若负荷有明显日周期或周周期,窗口至少要能提供相应历史,或通过滞后特征显式补足;
- 数据质量:更长窗口更容易碰到缺失段和异常值;
- 模型成本:输入长度增加会提高存储、训练或推理成本,具体程度因模型而异;
- 陈旧信息:旧的运行模式在节假日、换季或设备调整后可能不再有用。
一个可执行的比较方法是:先固定 和同一套时间划分,在验证集比较几个 候选值,并同时报告整体误差、各预测步长误差与计算成本。测试集只用于报告最终方案,不用于反复选择窗口长度。关于时间划分和 Walk-Forward Validation,Part 3 会正式定义。
Stride 同样需要记录在实验配置中。缩小训练 Stride 会增加样本数量,但不会凭空增加独立信息;增大 Stride 可以减少训练成本,也可能漏掉重要的发起时刻,例如只在午夜取样会看不到白天发起预测时的表现。它是否合适,应由实际预测频率和验证结果共同决定。
6. 常见错误
先对全量序列开窗,再随机分训练和测试
相邻窗口共享大量观测。随机拆分容易让训练样本的标签与测试样本的输入、甚至标签出现在相邻时段,无法模拟真正的向前预测。应先确定时间边界,再约束每个窗口的 Issue Time 和标签所属区间。
只检查输入落在哪个区间
一个训练窗口即使输入全部落在训练期,只要它的预测标签伸入验证期,就不能再当成训练样本。边界检查应覆盖整个标签区间。验证或测试窗口是否允许使用边界前的历史作为上下文,需要按真实上线时可获得的信息明确规定,而不是无意间截断。
把重叠当成泄漏,或把不重叠当成独立
训练期内的重叠窗口是正常的数据构造方式。泄漏发生在使用了 Issue Time 不可获得的信息,或用未来数据影响了训练和选择。反过来,标签不重叠也不意味着输入和误差独立。
只按数组位置切片
缺失小时、夏令时、重复时间戳、跨建筑拼接都会让位置间隔与真实时间间隔不同。Part 2 完成时间戳和实体校验后,才能把这个最小函数扩展为真实数据集构造器。
7. 本文产出与下一篇
现在我们有了明确的窗口契约:每条样本包含 个历史观测和 个未来标签,起点按 Stride 移动,Issue Time 是输入的最后一个时刻。我们还知道训练步长和线上发起频率应分别定义。
这些规则将在后续实现为数据集构造与边界检查代码。本篇的列表函数只是索引示例,尚未产生 BDG2 的实验结果,也未确定最优 Lookback。
下一篇讨论:拿到同样的历史窗口后,模型应如何预测未来 24 个值?是逐步递推、分别预测每一步,还是一次输出整条曲线?
参考资料
- Hyndman 与 Athanasopoulos,《Forecasting: Principles and Practice》,5.10 Time series cross-validation:滚动预测起点与多步误差评估。