☰
多变量多步CNN时间序列预测:数据切片与模型实战
2026/10/14 8:38:58 网站建设 项目流程

1. 从这张图聊起:为什么单步预测在业务里常常不够用

如果你正卡在“时间序列预测”这个题目上,大概率已经被老板问过类似的问题:“你这个模型能不能直接预测未来7天?”“能不能同时预测好几个门店的销量?”这时候你会发现,单步预测、单变量预测在论文演示里很漂亮,落到真实业务里却有些使不上劲。

我这次记录的是 CNN 在时间序列预测里的第二篇实战:Multi-step CNN(多步预测)和Multivariate Multi-step CNN(多变量多步预测)。标题里的 “02” 说明这是接续上一篇单变量单步预测模型之后的进阶内容。如果你还没看过前一篇,其实也不影响理解,我会把需要用到的前置概念重新交代一遍。

简单说,这篇内容围绕这几个问题展开:把 CNN 从“预测下一个时间点”升级到“预测未来多个时间点”,数据格式要变什么?模型结构要动哪里?多变量输入(也就是同时用好几个特征)和多步输出(一下子预测好几天)叠加在一起时,哪些细节最容易出幺蛾子?适合谁来读?我觉得两类人最需要:一是已经跑通单一时间序列模型,想把预测能力往“未来多步”扩展的人;二是做多门店、多品类销量预测,或者做能源负荷预测,需要同时塞进多个相关特征并一次输出多步预测结果的工程师和分析师。

为了让你有个直观概念,我先画一个“业务需求”和“模型能力”的对应关系表:

业务需求模型输出形式常见场景
明天是多少单步,单变量单一指标短期预警
未来7天趋势多步,单变量产量预测、库存规划
多个指标一起预测未来N天多步,多变量多门店销量、多路段流量

这篇的核心价值不只是告诉你“加个 Dense 层就能多输出几个数”,而是把多步预测背后的数据切片逻辑、训练策略(尤其是 Direct 策略和 Recursive 策略的取舍)、以及多变量输入下的维度坑讲清楚。因为我在实际项目里踩过不少这类坑,所以文中会穿插一些“为什么这样做”的解释,而不是只给代码。

2. Multi-step CNN 的核心:不是改网络,而是先改数据切片方式

2.1 预测目标变了,数据窗口也要跟着变

先说一个很多人容易忽略的点:多步预测的关键,其实首先不在模型结构,而在你如何构造训练样本。

单步预测时,我们有时间序列 $[x_1, x_2, ..., x_T]$,用过去 $n$ 个点预测 $n+1$ 时刻的值。数据切分出来是这样的:

样本1: 输入 [x1, x2, x3, x4, x5] -> 输出 [x6] 样本2: 输入 [x2, x3, x4, x5, x6] -> 输出 [x7]

等到做多步预测,假设你要预测未来3个时间点,那么样本标签就不只是“下一个点”,而是“接下来3个点”。切片逻辑变成:

样本1: 输入 [x1, x2, x3, x4, x5] -> 输出 [x6, x7, x8] 样本2: 输入 [x2, x3, x4, x5, x6] -> 输出 [x7, x8, x9]

注意看,这里窗口滑动的步长仍然是1,标签从1个扩展成3个。很多初学者在这里会踩第一个坑:他们习惯用“输入5个点,标签1个点”的方式做滑动窗口,然后到多步预测时直接改模型的输出维度,结果发现训练出来的模型预测曲线一片平坦,几乎就是“用上一个值顺延”。

问题出在哪?出在标签构造。如果你的标签还是单步的,模型压根没学过“未来连续多步”是什么样的,你把最终输出的 Dense 单元从1改成3也没用。数据决定了模型学习的上限,网络结构只是在逼近这个上限。

这里我建议用一个明确的数据切片函数,把输入序列、标签序列一次切好:

import numpy as np def create_multistep_dataset(series, n_input=5, n_output=3): X, y = list(), list() for i in range(len(series)): end_ix = i + n_input out_end_ix = end_ix + n_output if out_end_ix > len(series): break seq_x = series[i:end_ix] seq_y = series[end_ix:out_end_ix] X.append(seq_x) y.append(seq_y) return np.array(X), np.array(y)

这里有个参数值得注意:out_end_ix如果越界,样本直接丢弃。这种处理方式很常见,代价是序列末尾的一部分数据无法参与训练。如果数据量本身不大,可以考虑用 padding 或者把末尾数据单独留作验证集,而不是硬切。

2.2 CNN 处理时间序列的维度逻辑:不要把模式想复杂了

CNN 原本是为图像设计的,图像数据是(height, width, channels)三维结构。时间序列拿到 CNN 里,通常变成一个二维输入:(time_steps, n_features)。在 Keras 里,Conv1D的输入要求是(batch_size, steps, features)。

对于单变量多步预测,n_features = 1,所以每个样本三维形状是(n_input, 1)。如果你直接把上一节切出来的seq_x(形状是(n_input,))塞进 Conv1D,大概率会报维度错误,因为它缺少了特征维度。

解决方式是在切好数据后做一个 reshape:

X = X.reshape((X.shape[0], X.shape[1], 1))

这一步很基础,但也是新手报错高频区。我自己早期写代码的时候,经常忘了 reshape,报错以后又去查 Conv1D 文档,浪费不少时间。

接下来看看 Multi-step CNN 的模型结构。这里给出一个基础版:

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, Flatten, Dense model = Sequential() model.add(Conv1D(filters=64, kernel_size=2, activation='relu', input_shape=(n_input, 1))) model.add(Flatten()) model.add(Dense(64, activation='relu')) model.add(Dense(n_output)) model.compile(optimizer='adam', loss='mse')

结构很简单:一个 Conv1D 提取局部时序模式,Flatten 把特征压平,两个全连接层做回归输出。注意最后一层Dense(n_output),输出维度等于未来步数,比如n_output=3就是一次输出3个未来值。

但这里我必须提醒一个容易误会的点:Conv1D 并不是直接把输入时间窗口“翻译”成未来序列。它的本质是提取局部模式,比如上升趋势、拐点、周期性片段。真正负责把“模式”映射到“多个未来值”的,是后面的全连接层。所以如果你发现模型效果不好,先不要急着增加卷积层数,有时候问题出在全连接层的容量或者数据切片的合理性上。

2.3 一个简单但能跑通的多步预测示例

光说不练假把式,我完整跑一个示例。数据集就用自带的正弦波加一点点噪声,模拟一个带周期性的序列。这类数据在验证模型结构阶段很有用,能快速看出模型是否学到了周期结构。

import numpy as np import matplotlib.pyplot as plt from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, Flatten, Dense # 生成模拟时间序列:正弦波 + 噪声 t = np.arange(0, 1000, 0.1) series = np.sin(t) + np.random.normal(scale=0.1, size=len(t)) n_input = 20 n_output = 5 X, y = create_multistep_dataset(series, n_input, n_output) X = X.reshape((X.shape[0], X.shape[1], 1)) train_size = int(len(X) * 0.8) X_train, y_train = X[:train_size], y[:train_size] X_test, y_test = X[train_size:], y[train_size:] model = Sequential() model.add(Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=(n_input, 1))) model.add(Flatten()) model.add(Dense(32, activation='relu')) model.add(Dense(n_output)) model.compile(optimizer='adam', loss='mse') history = model.fit(X_train, y_train, epochs=30, validation_data=(X_test, y_test), verbose=0) # 预测并对比 preds = model.predict(X_test[:5], verbose=0) for i in range(5): plt.plot(range(n_output), y_test[i], 'o-', label='true') plt.plot(range(n_output), preds[i], 'x-', label='pred') plt.legend() plt.show()

跑完之后你大概率会看到:预测的前一两个点接近真实值,后面几个点逐渐偏离。这个现象很典型,后面第3节会详细解释原因。但至少从结果来看,多步 CNN 的基础框架已经work了。

3. 多步预测的两种策略:Direct 与 Recursive,各有各的坑

3.1 Recursive(递归)策略:用预测值当输入,误差会滚雪球

多步预测有两种经典策略,实际操作中很多人不知道它们的名字,却在代码里不自觉地混着用。

Recursive策略是:先训练一个单步预测模型,然后用它的预测值作为下一步的输入,不断迭代得到未来多步的预测。举个例子,假设输入窗口是过去5个点,模型输出未来第1个点。预测完第6个点后,把窗口滑成“第2到第6个点”,再预测第7个点,循环往复,直到得到所有未来点。

这种方式的好处是模型结构简单,单步预测的模型库直接复用;坏处是误差会累积。第6个点的预测不是完全准确的,把它当输入以后,第7个点的输入本身就带偏差,这个偏差会一步步被放大。在信号强、周期稳的数据上可能还能看,但一旦数据噪声大,递归预测到后面基本就是一条平滑的直线或偏移严重的曲线。

我在一个电力负荷项目里试过这种策略,前3步预测效果良好,从第4步开始偏差明显加大,到第7步已经看不出原始曲线的形状了。这不是模型训练得不好,而是递归策略的固有缺陷,特别是在噪声主导的数据里更明显。

3.2 Direct(直接)策略:一次输出多个步长,训练更稳

Direct策略就是我上面示例里写的,输入固定长度的历史窗口,模型一次输出未来多个步长的预测值。比如输入20个历史点,一次输出未来5个点。它的好处是训练时每个步长的误差都直接和真实验签比较,不会出现“用预测值做输入”导致的误差滚雪球。

坏处也有:模型需要同时拟合多个步长,如果步长之间存在不同的变化规律,单靠一个输出层可能不够灵活。解决办法一般是增加全连接层的宽度、增加模型容量,或者把多步输出拆成多个独立的输出分支(多任务结构),但这就是进阶玩法了,基础版暂时用不到。

从工程实践来说,我更推荐先尝试 Direct 策略。原因有三:

  1. 训练和预测的逻辑最直观,不容易出 bug。
  2. 训练时所有步长共享同一个历史窗口信息,模型通常能学到更稳定的短期模式。
  3. 在多变量场景下,Direct 策略天然适配,不需要额外的迭代逻辑。

当然,Recursive 也不是一无是处。如果你的业务需要非常长的预测步数(比如预测未来30天),而 Direct 模型的输出维度太大导致训练困难,可以考虑 Recursive 或者 Direct + Recursive 混合。但那种场景下,我更建议换 LSTM 或 Transformer 这类专门处理长期依赖的模型,而不是硬用 CNN。

3.3 两种策略的对比表格

直接把两种策略放在一起看:

对比维度Recursive 策略Direct 策略
模型结构单步模型多步输出模型
输入数据预测值会回流到输入固定历史窗口
误差传递逐步累积无累积误差
训练难度低中等
预测稳定性步长越长越差相对稳定
适用步数长步数(但有风险)中短步数
模型复用性可复用单步模型需重训多步模型

选择哪个,取决于你实际预测的步长和数据质量。如果是日级别的销量预测,预测未来7天,Direct 完全够用,没必要去碰递归那块烫手山芋。

4. Multivariate Multi-step CNN:多个特征怎么组织输入

4.1 多变量时间序列的数据形态:从目录到矩阵的变化

如果说多步预测只是“标签从1个变成N个”,那多变量预测就是“输入从1列变成M列”。这两件事单独看都不难,但合在一起,数据组织方式就变得关键了。

假设我们要预测某产品未来3天的销量,可用的历史特征包括:

  • 销量本身(目标变量)
  • 价格
  • 促销活动标记
  • 平均客流量

那么输入窗口就有4个特征列,每个时间点上有4个数值。这个数据形态很像一个矩阵:行是时间步,列是特征。输入到 Conv1D 以后,每个样本的维度是(n_input, n_features),其中n_features=4。

数据切片的逻辑和单变量时类似,但标签通常还是只取目标变量那列(比如销量),或者你也可以同时预测多个变量的未来值。后者属于“多变量输出多步预测”,在有些文献里也叫序列到序列的变体。我这里先从最常见的场景讲起:输入多变量,输出单目标变量的多步预测。

切片代码可以这样写:

def create_multivariate_multistep_dataset(data, n_input=10, n_output=3, target_col=0): X, y = list(), list() for i in range(len(data)): end_ix = i + n_input out_end_ix = end_ix + n_output if out_end_ix > len(data): break seq_x = data[i:end_ix, :] # 输入多特征 seq_y = data[end_ix:out_end_ix, target_col] # 输出目标列未来值 X.append(seq_x) y.append(seq_y) return np.array(X), np.array(y)

注意,data是一个二维数组,形状是(总时间点数, 特征数)。切出来的X形状是(样本数, n_input, n_features),正好可以直接喂给 Conv1D,不需要像单变量那样额外 reshape。标签y的形状是(样本数, n_output)。

4.2 多变量多步 CNN 的模型配置:所有输入列都进了卷积层

多变量多步 CNN 模型和之前的结构很相似,唯一本质区别是input_shape的第二维改为n_features:

n_features = 4 n_input = 10 n_output = 3 model = Sequential() model.add(Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=(n_input, n_features))) model.add(Flatten()) model.add(Dense(64, activation='relu')) model.add(Dense(n_output)) model.compile(optimizer='adam', loss='mse')

看到这里,你可能会问:那卷积层是不是把4个特征当成图像的4个通道来处理了?这是个很关键的理解问题。在 Conv1D 里,卷积核是在steps维度上滑动的,n_features维度的处理方式是每个卷积核都对全部特征做加权组合。也就是说,它不会像图像卷积那样把通道分别处理后再融合,而是直接在特征维度上做全连接式融合。这意味着,不管你有多少个特征列,卷积核都会把它们一起卷进来,提取的模式实际上是一种“多特征联合模式”。

举个例子,如果kernel_size=3,一个卷积核看到的就是“过去3个时间点,每个点上所有特征”的小块。它能学到的模式可能是“过去3天,有促销且客流量上升,销量大概率也上升”。这种跨特征的局部模式,正是多变量 CNN 的核心优势。

但这里有个重要提醒:多变量输入并不一定总是能提升模型效果。如果某个特征和目标变量之间没有明确关联,甚至带有大量噪声,模型不仅要学习有效信息,还要费力忽略无用的特征。所以多变量建模前,最好先做一个简单的相关性分析或特征重要性粗筛。我在实际项目里,用过一个很土但有效的方法:先分别用单变量跑一版,再把想加的特征逐个加上,对比验证集误差,缺点只是耗时,但结论可靠。

4.3 完整示例:两个变量预测一个变量的未来5个时间点

下面给一个完整可复现的多变量多步 CNN 示例。数据用两个相关的正弦函数模拟,模拟“两个特征共同影响一个目标”的场景:

import numpy as np import matplotlib.pyplot as plt from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, Flatten, Dense # 生成模拟多变量数据:特征1和特征2,目标由两者组合而成 N = 2000 t = np.arange(N) f1 = np.sin(t / 20) + np.random.normal(scale=0.2, size=N) f2 = np.cos(t / 30) + np.random.normal(scale=0.2, size=N) target = 0.6 * f1 + 0.4 * f2 + np.random.normal(scale=0.1, size=N) data = np.column_stack([target, f1, f2]) # 3列:第一列是目标 n_input = 15 n_output = 5 target_col = 0 X, y = create_multivariate_multistep_dataset(data, n_input, n_output, target_col) print(X.shape, y.shape) # 你应该看到 (样本数, 15, 3) 和 (样本数, 5) train_size = int(len(X) * 0.8) X_train, y_train = X[:train_size], y[:train_size] X_test, y_test = X[train_size:], y[train_size:] model = Sequential() model.add(Conv1D(filters=64, kernel_size=3, activation='relu', input_shape=(n_input, data.shape[1]))) model.add(Flatten()) model.add(Dense(64, activation='relu')) model.add(Dense(n_output)) model.compile(optimizer='adam', loss='mse') history = model.fit(X_train, y_train, epochs=40, validation_data=(X_test, y_test), verbose=0) preds = model.predict(X_test[:3], verbose=0) for i in range(3): plt.plot(range(n_output), y_test[i], 'o-', label='true') plt.plot(range(n_output), preds[i], 'x-', label='pred') plt.legend() plt.show()

这个示例里有几个细节值得注意:

  1. 输入数据data的第一列是目标变量,但模型并不知道这一点,它把三列都当作普通输入。目标变量的历史值也在帮助预测未来。
  2. 模型输出的是target_col这一列的未来值,预测的是同一个变量,但建模时加入了另外两个相关特征的信息。
  3. 卷积层的input_shape动态取data.shape[1],这样即使后续增加特征列,代码也能自适应。

如果你跑完这个示例,会发现训练曲线下降得还算平滑,预测值和真实值在前几个步长对得也比较齐。这是因为两个特征函数都有周期性,CNN 学到了“目标变量受这两个特征影响”的组合模式。

5. 实战中的坑:我踩过的那些和维度、loss、预测偏移相关的坑

5.1 维度错位:样本数、时间步、特征数的三角关系

多变量多步 CNN 最容易出的 bug 就是维度错位。Keras 的 Conv1D 期望输入形状是(batch_size, steps, features)。如果你传入一个二维数组(样本数, n_input),它会直接报错;如果你传入(样本数, n_input, 1),但实际特征是4个,它也会报错。

我的建议是,每次切完数据后,立刻打印X.shape确认形状。不要等到训练报错再回查,那个过程相当浪费时间。

还有一个隐蔽的坑:当你使用flatten()时,模型会把所有时间步和特征全部拉平成一个大向量。假设n_input=15,n_features=3,Flatten 后得到的维度是45。如果你加了一个Dense(32),连接数就是45 * 32 = 1440,这个计算量不大,但如果你的filters设得很大、n_input也很长,Flatten 后可能瞬间变成一个超大的全连接层,导致参数爆炸。这种情况建议用GlobalAveragePooling1D或GlobalMaxPooling1D来替代 Flatten,它们会自动聚合时间维度,输出维度只和filters有关,能有效控制参数量。

5.2 预测结果的“后段偏移”现象

不管是单变量还是多变量的多步预测,我几乎每次都会遇到同一个现象:预测的前一两个点比较准,越往后越差。原因不复杂:距离当前时间越远的未来,不确定性本来就越大。而 CNN 通过卷积核看的是局部窗口,它最强的能力是捕捉“紧挨着的未来”,对更远的未来缺乏额外的长期依赖信息。

这个现象可以通过几个办法缓解:

  1. 增加输入窗口长度n_input,让模型看到更长的历史,从而有更多上下文推断远期趋势。
  2. 在数据预处理阶段做差分或归一化,让序列平稳性更好。CNN 对平稳序列的学习能力通常优于非平稳序列。
  3. 把损失函数从 MSE 改成 MAE 或 Huber loss,减少远步长预测的大误差对梯度的影响。

我特别想强调归一化。时间序列进 CNN 之前,一定要做归一化,而且最好按特征的尺度单独归一化,不要让量纲差异特别大的特征扰乱卷积核的权重学习。我在一个销量预测项目里,因为把“销量”和“促销金额”直接放在一起,促销金额的数值范围是销量的几十倍,训练出来的模型几乎完全忽略了销量特征。归一化之后,权重重新分配,效果立刻好了很多。

5.3 训练集、验证集划分的时间顺序问题

时间序列切分不同于普通机器学习里的随机打乱划分。随机打乱会破坏时序依赖,导致模型在训练时“偷看”未来信息。正确的做法是按时间顺序切割,前80%训练,后20%验证。

我在代码里就是这么做的。但要注意一个问题:切分后,如果你在训练集里用了滚动窗口切片,最后一个训练样本的标签部分可能在未来,而验证集第一个样本的输入部分可能和它重叠。这种重叠不会导致直接的泄露,但会让验证集效果略微乐观。严谨的做法是,在训练集末尾留出与验证集窗口相同长度的数据作为隔离带。实际操作中,如果数据量够大,这种重叠的影响可以接受。

还有一个更细微的坑:如果时间序列存在明显的趋势或季节性,按80/20直接切分,训练集和验证集的分布可能不一致。举个例子,训练集是1月到8月,验证集是9月到12月,如果业务有“年底促销”模式,验证集可能完全没见过这种模式,测试误差会比实际使用时的误差更差。这种情况下,可以考虑用滚动预测评估——在验证集上逐个时间点滚动预测并计算误差,而不是一次性预测大段区间。

5.4 模型评估不要只看损失值,画出预测曲线很关键

我见过不少人训练完只看loss降了多少,觉得模型效果不错,一画图才发现预测曲线跟真实曲线几乎错位。尤其是多步预测任务,MSE 可能被某一步的巨大误差主导,其他步的表现反而被掩盖。

所以我的习惯是:训练结束后,至少抽取3-5个测试样本,画出“真实未来值 vs 预测未来值”的曲线。看图比看数字快得多,也直观得多。如果图形上出现整体滞后一个步长的现象,通常说明模型学到的更多是“上一时刻的值延续”,而不是真正的趋势判断,这时候需要考虑增加输入窗口长度或者调整卷积核大小。

另外一个常被忽略的评估指标是各步长分别计算误差。把未来第1步、第2步、第3步的 MSE 分开统计,你能清楚看到模型在哪一步开始崩坏。这不仅是评估手段,更是后续模型改进的指引。

6. 进阶思路:从单输出多步到多输出多步

如果你已经跑通单目标多步预测,下一个自然的问题是:能不能让模型同时预测多个目标变量的未来多步值?

这个在工程上很有用。比如做多门店预测时,每个门店就是一个目标变量,它们之间可能存在联动效应(比如客流相互影响)。与其分别训练每个门店的模型,不如用同一个模型同时输出多个门店的未来值。这就是典型的Multivariate Multi-step Multi-output问题。

实现的时候,标签y的形状要变成(样本数, n_output, n_targets),模型输出层的维度也要相应调整,通常做法是把 Flatten 后接入一个Dense(n_output * n_targets),然后 Reshape 回(n_output, n_targets)。或者用多个输出头的函数式模型(Functional API)分别处理每个目标变量。

这里我不展开完整代码,因为一经展开又是一篇新文章的长度。但我想提一个重要的设计原则:当多个输出之间相关性较强时,共享底层的卷积特征提取层通常是有效的;如果相关性很弱,共享层反而会互相干扰,此时建议使用多个独立分支。这个原则在多任务学习里经常被讨论,时间序列预测同样适用。

7. 最后分享一点我对 CNN 做时间序列预测的感受

我把从单步到多步、从单变量到多变量这条路完整走了一遍,最大的体会是:CNN 做时间序列预测不是用来替代 LSTM 的,尤其在长周期序列上,CNN 的全局建模能力并不占优。但在中短步长预测、局部模式提取、多变量特征融合这些场景里,CNN 有自己的独特优势,尤其训练速度快、结构简洁、调参直观,特别适合作为第一版可用的预测模型。

我在实际项目里,通常会先用 Multi-step CNN 快速搭建一个 baseline,如果效果不达标,再切换到 LSTM 或 Transformer 做对比。很多情况下,CNN baseline 的效果已经足够业务使用,没必要一上来就动用复杂模型。

如果你想继续深入,我建议顺着这几个方向探索:

  • 把多步预测的 Direct 模型升级为序列到序列结构(Encoder-Decoder),让 CNN 作为 Encoder 提取特征,LSTM 或全连接层作为 Decoder 生成未来序列。
  • 尝试不同的卷积核大小组合,比如用两个并行的 Conv1D 分别捕捉短期模式和中期模式,再拼接特征,这个过程类似于图像里的多尺度卷积。
  • 加入时间特征,比如星期几、是否节假日、月份等,作为额外的输入特征列。这些离散特征在销量预测这类业务场景里往往比历史数值本身更有用。

时间序列预测这条路,永远有下一个坑等在前面。但只要你搞清楚数据切片、输入维度和误差评估这三件事,Multi-step CNN 和 Multivariate Multi-step CNN 基本就能稳稳跑起来了。后续的优化都是在这些基础之上添砖加瓦。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询