☰
多变量时间序列预测新思路:FACT细粒度跨变量卷积动态建模变量交互
2026/10/2 8:53:08 网站建设 项目流程

做多变量时间序列预测的时候,你有没有遇到过这种情况:模型在验证集上跑得好好的,一上线就频繁“翻车”。尤其当某些变量之间的关系发生变化时,比如某个传感器坏了、某项业务指标开始提前或滞后于其他指标,预测结果很快就会走样。过去几年的主流做法,要么把每个变量当成独立通道处理,要么用一个固定的相关性矩阵概括所有变量在整个时间段上的依赖关系。我越来越觉得,问题恰恰出在这里——变量之间的交互,本身是动态的、分层的、会随着外部状态改变的。

FACT(Fine-grained Across-variable Convolutional Transformer)这个名字,细粒度跨变量卷积建模动态变量交互,针对的正是这个痛点。它不再用静态相关矩阵或全局注意力来打包变量关系,而是把变量之间的协同变化细化到不同时间片段和不同频率层级,用跨变量的卷积结构去抽取这些交互,同时让模型感知“这种联动在什么情况下变紧、什么情况下变松”。这篇文章我会把FACT背后的设计逻辑、关键模块、实现方法和工程中踩过的坑都梳理一遍,适合正在做多变量预测、也愿意跳出“注意力至上”框架的团队参考。

1. 先搞清楚:为什么变量交互需要单独建模

1.1 独立通道建模的局限

先看一个极其常见的baseline设定:把输入整理成[batch, time, variables]的张量,然后丢进LSTM或者TCN。网络内部对时间维做递归或者卷积,变量的每一个维度只是特征通道里的一个分量。很多模型在整个前向过程中,变量之间几乎不发生真实的相互作用,各自的特征只是在同一个隐空间里“拼车”而已。

这种设计在变量少、关系简单的场景下问题不大。比如预测单台设备的温度,就算把电压、电流、转速都丢进去,模型其实只需要利用每个序列自身的趋势就够了。但一旦变量之间存在强耦合,比如电力负荷预测里温度、湿度、节假日、电价四者互相影响,独立通道编码就明显力不从心。模型如果要学会“高温叠加工作日导致负荷抬升”这样的规则,就必须隐式地从数据中自己挖掘出跨变量的组合模式来。

在样本量充足时,深网络确实有可能硬记住这类组合;可一旦业务场景变化,比如进入冬季,原来的组合规则完全改写了,独立通道模型就需要重新训练大量参数来适应。它的泛化空间被限制在每个变量各自的时间模式里,变量间涌现出的结构关系却没有被显式编码。

1.2 静态相关性假设的坑

另一类做法是显式建模变量关系,典型代表是站在变量层面的图神经网络、VAR模型,或者直接计算一个Pearson相关矩阵。它们把整段历史数据压缩成一个静态结构,再把这个结构“焊死”在模型里。这种假设在稳定系统中还勉强成立,在真实业务数据里却非常脆弱。

举个实际例子。我做过一个空调能耗预测项目,夏天的时候,温度和负荷高度正相关,相关系数能到0.85以上;到了十月,空调负荷逐渐退出主导地位,温度和负荷的相关性一路跌到接近零;一旦进入冬季电采暖阶段,温度和负荷不仅相关,而且是负相关。如果你在整个训练集上算一个“全局相关性矩阵”,得到的结果基本上夏冬互相抵消,变成一个毫无区分度的中间值。模型拿着这个中间值去做预测,冷天热天都用同一套交互逻辑,不崩才怪。

更隐蔽的是相位滞后问题。变量之间的影响往往不是同步的,上游变量先动,下游变量延后几小时甚至几天。一个静态矩阵本质上只能描述同时刻的线性相关,把滞后关系压扁成了一个数值,远不能表达变量在时间尺度上的前后影响。

1.3 从全局关系到细粒度交互的转变

既然问题出在“把交互写死”,那自然的改进方向就是让交互结构变得可学习、可变化。注意力机制在理论上能够做到这一点:让模型在每个时间步动态决定变量之间的权重。但全局注意力有一个很难绕开的代价:它的计算复杂度是序列长度的平方,而且需要非常大的样本量才能收敛出一个可靠的变量关系权重。

细粒度跨变量卷积提供了一个折中:它把建模对象从“整个时间范围上的变量关系”拆解成“局部窗口内、特定频率尺度下的变量组合关系”。用卷积核在变量维度和时间维度上同时滑动,既可以捕捉变量间的局部联动,又不需要承担全局注意力那样的参数压力和计算压力。更重要的是,卷积核天然具备平移不变性,这意味着模型学会一种交互模式后,可以很方便地复用到不同时间段,这一点更贴合现实场景中变量交互“反复出现但形态相似”的特点。

2. FACT的核心思路:用卷积表达变量交互的动态性

2.1 时间卷积与跨变量卷积的分工

如果你熟悉一维卷积做时间序列,那你一定知道时间卷积是怎么回事:卷积核沿着时间轴滑动,每个输出位置是附近若干时间步的加权组合。时间卷积解决的问题是“这个变量自身的过去如何影响现在”。

跨变量卷积则是另外一个维度的操作:卷积核不再只沿时间滑动,而是同时覆盖变量维度和时间维度。一个kh×kw的跨变量卷积核,在一次操作里就会把相邻时间步上的多个变量混合起来。从直观意义上看,每个卷积核学到的是一个“局部交互模板”:在这个模板里,变量A上升一个单位、变量B下降两个单位、变量C滞后三个小时,共同构成一个特定的状态模式。

我个人的理解是,FACT没有把时间卷积和跨变量卷积做成互斥关系,而是做了明确分工。时间卷积负责编码单变量动态特征,跨变量卷积负责编码变量间的组合关系,两层交替堆叠,让信息在两种维度之间反复交换。这比直接用一个大卷积核同时混洗时间和变量更可控,也更容易调试。

2.2 细粒度到底细在哪里

细粒度这个词很容易被当成营销话术,但在FACT的语境里,它其实对应着三个维度的细化。

第一个维度是时间窗口的细化。不是把整段历史平均看待,而是把历史切分成不同长度的局部窗口:短窗口负责捕捉突变式的交互,比如雷暴天气下风速和湿度的同步跃升;长窗口负责捕捉缓慢的耦合关系,比如经济指数对消费指标的连续影响。多组不同尺寸的卷积核并行存在,相当于同时用显微镜和望远镜观察变量关系。

第二个维度是变量分组的细化。不是把所有变量一视同仁地做全连接混合,而是先把变量按语义或先验知识分组,在组内做细粒度跨变量卷积,再跨组做信息交换。例如在气象预测中,温度、湿度、气压分为一组,污染物浓度分为一组;在金融风控中,资产价格、波动率、成交量为一组,宏观指标单独一组。分组减少了参数规模,也防止不相关的变量之间产生虚假交互。

第三个维度是卷积核权重的动态化。普通的卷积核一旦训练完成就固定不变了,FACT的细粒度还体现在它会根据当前输入的内容生成调制参数,对基础卷积核进行加权或偏移。相当于每个时间窗口里的卷积核不是千篇一律的,而是由上下文动态调制出来的,这正是建模动态变量交互的关键所在。

2.3 跨变量卷积相比注意力的独特优势

我们团队做过一个很直观的对比实验:同一份多变量数据,用标准的Transformer来做变量attention,和用跨变量卷积做交互建模,在参数量相近的情况下,卷积版本收敛更快、训练曲线更稳。原因不复杂,注意力要学习的是一张完整的变量×变量权重矩阵,数据稀疏时很容易学到噪声;跨变量卷积则因为参数共享和局部感受野的限制,天然带了一点正则效果,不太容易在一个小窗口上把变量关系彻底记住。

还有一个优势是延迟表达。变量之间经常会存在相位差,注意力机制的权重是点对点的,必须叠加多层才能隐式表达时间上的滞后关系;跨变量卷积的不同卷积核可以在不同时间偏移上被激活,显式地表达“变量A在t-3时刻的变化会引发变量B在t时刻的反应”。这个特性在交通流量预测里非常有用,上游路口的车流往往要15到20分钟才会传导到下游路口,跨变量卷积能在几步之内把这种延时联动学好。

当然,这绝不意味着attention没有用。在实际模型里,attention仍然可以作为高层特征融合器,在跨变量卷积提取出交互模式后,进一步捕捉长距离依赖和全局上下文。

3. 实操:FACT子模块的搭建与训练

3.1 数据预处理:变量独立归一化

在进入FACT之前,有个基础工作最容易忽略却又至关重要,那就是变量归一化。多变量数据里各特征量纲差异极大,温度可能是30,湿度可能是80,负荷可能是几千。如果做全局归一化,数值小的变量在跨变量卷积核中的贡献会被直接淹没。

我的建议是每个变量各自做标准化,保存各自的均值方差,预测完成后再逆变换回原尺度。跨变量卷积在处理标准化后的数据时,卷积核学习到的是标准化空间里的相关性,数值范围基本一致,梯度更新也更均衡。还有一个细节:训练集和验证集的归一化参数必须分开计算,不能在验证集上混入训练集统计量,否则模型在离线评估时会被轻微“剧透”。

3.2 一个可用的跨变量卷积模块实现

下面给出一个FACT跨变量卷积模块的参考结构。这里我用PyTorch写,本质上就是把输入调整成适合二维卷积的形状,在时间和变量两个维度上做局部混合。

import torch import torch.nn as nn class CrossVariableConv2d(nn.Module): def __init__(self, in_channels, out_channels, kernel_time, kernel_vars, groups=1): super().__init__() self.conv = nn.Conv2d( in_channels=in_channels, out_channels=out_channels, kernel_size=(kernel_time, kernel_vars), padding=(kernel_time // 2, kernel_vars // 2), groups=groups, ) self.norm = nn.LayerNorm(out_channels) def forward(self, x): # x: (B, T, V) 时间步、变量维度 x = x.unsqueeze(1) # (B, 1, T, V) h = self.conv(x) # (B, C, T, V) h = h.transpose(1, 2) # (B, T, C, V) return self.norm(h)

如果你希望卷积核能动态响应输入状态,关键改动是让卷积权重带一个调制门。可以先用一个GlobalPooling提取当前窗口的整体状态,再通过一个小网络生成一组缩放因子,叠加到基础卷积核上。伪代码大概是:

class DynamicCrossVariableConv(nn.Module): def __init__(self, in_channels, out_channels, num_vars, reduction=4): super().__init__() self.conv = nn.Conv2d( in_channels, out_channels, kernel_size=(3, 3), padding=(1, 1) ) self.gate = nn.Sequential( nn.AdaptiveAvgPool2d(1), nn.Conv2d(in_channels, in_channels // reduction, 1), nn.ReLU(), nn.Conv2d(in_channels // reduction, out_channels, 1), nn.Sigmoid() ) def forward(self, x): base = self.conv(x) gate = self.gate(x) return base * gate

这个动态门是捕捉“变量交互强度变化”的直接手段。比如在电力预测里,模型学会在温度极端时放大温度-负荷卷积核的响应,在温和天气里把权重压低,效果会比静态卷积核明显很多。

3.3 残差与多尺度堆叠

实验里我一般会堆三到四层跨变量卷积,每层卷积核的时间宽度从3、5、7这样递增,相当于在不同尺度上提取交互。记住每一层都要做残差连接,否则梯度很难流到浅层,网络深了以后极其容易退化。

残差连接的结构是这样:输入先过一个跨变量卷积,再经过LayerNorm,然后与原输入相加,最后过一个前馈网络。整体上遵循Transformer的Block风格,但把注意力替换成了跨变量卷积。这样做的好处是工程上可以继续复用已有的Transformer训练技巧,包括学习率预热、Warmup、梯度裁剪等。

3.4 损失函数与评估指标的选择

预测任务我通常用分位数损失或Huber损失,前者可以帮助你输出预测区间,后者对异常点更鲁棒。如果是点预测为主,简单一点用MSE也够。不过评估建议尽量拆分到变量维度算,不要只出一个整体MAE。因为模型可能把主要变量拟合得非常好,却在两个小变量上完全失真,整体指标掩盖了局部问题。我在项目里会把每个变量的MSE列成一张表,专门观察交互变化最活跃的那几个变量。

还有一个实用技巧:在训练初期给跨变量卷积的输出加一点Dropout,比例在0.1到0.2之间。这不是为了防过拟合,主要是为了让模型不要过早依赖某一个变量,迫使它学会多变量之间的冗余表达。

4. 实验复盘:FACT在实际数据上的表现

4.1 实验设置与baseline选择

我在公开数据集和自己业务数据上都做过验证。公开数据选的是ETT电力变压器数据集和Traffic交通流量数据集,baseline选了三类,一类是独立通道模型LSTM和TCN,一类是静态关系模型VAR,还有一类是带变量注意力机制的Transformer。统一设置是:用过去24个时间步的数据预测未来12个时间步,批次大小64,初始学习率1e-3,Cosine退火,训100轮。

我自己的实际业务数据是某城市区域供暖负荷预测,变量包含室外温度、湿度、风速、太阳辐射、前一天负荷、小时刻、节假日标记和楼宇入住率。这类数据的变量交互极其不稳定,供暖季和非供暖季的逻辑截然不同,非常适合测试动态交互建模的效果。

4.2 跨变量卷积带来的主要收益

从验证集效果看,FACT结构相比LSTM和TCN,在MSE上大致有6%到9%的提升,相比带变量注意力的Transformer也有2到3个点的提升。更有意思的是样本效率。我只用十分之一的训练数据重新训了一次,FACT的指标下降幅度明显小于Transformer,这说明跨变量卷积确实有更强的结构先验,不会像注意力那样需要大量数据去“死磕”变量关系。

在供暖负荷这个业务场景里,模型学到的一个交互模式是:太阳辐射上升时,室内负荷会滞后下降,延迟大约3小时。但如果在阴天,这种辐射-负荷交互几乎不存在。动态跨变量卷积核把这个模式编码成了两层结构:基础卷积核负责太阳辐射与负荷的时滞相关性,动态门负责判断当前是否存在日照足够强的条件。当门控值低时,卷积核输出被自然抑制,整条交互链路失效,模型就不会误判。

4.3 从卷积核可视化中看到的变化

我们把训练好的卷积核定位到不同时间段,做了简单的可视化。方法是把每个窗口内的输入切片输入到某个跨变量卷积层,记录卷积核激活强度,再按时间聚类。结果很清晰:冬季样本中“温度-负荷”核激活强度显著高于夏季;工作日与周末,“节假日标记-负荷”核的激活模式也完全不同。这从侧面验证了细粒度跨变量卷积确实不是学了一个静态模式,而是真的在跟着上下文动态切换。

另一个有意思的现象是,变量分组的作用比预期更大。我把气象变量和经济变量混在一起不分组训练时,模型指标略有下降且训练更慢。分组后,气象组内部交互明显更紧凑,组间交互由高层网络统一融合,整个模型的收敛速度和最终效果都有改善。

5. 工程中常见的坑与排查方法

5.1 跨变量卷积变成“变量全连接”

有个坑我在项目里踩过不止一次:跨变量卷积核的变量维度过大,比如一次覆盖全部20个变量,结果模型退化成了在每个时间步对变量做全连接,完全没有“细粒度”可言。感知野太大时,卷积核反而无法区分局部变量组合。

解决方法有两个。一是把变量维度上的kernel_vars设小一些,比如4到8,让每个卷积核只看一小部分变量;二是把变量分组功能用起来,让每组内部各自卷积,层间再混合。我们最后在供暖负荷项目里把kernel_vars设成了5,效果比全变量混合好很多。

5.2 动态门控失效或震荡

动态门控不是随便加个Sigmoid就有用的。如果门控网络的输入统计量波动太大,训练初期门控值会在0和1之间疯狂震荡,导致模型很难收敛。我的经验是在门控网络的输入端加归一化,并用一个温度系数把Sigmoid的输入压缩到比较平缓的区域。比如在Sigmoid之前乘一个0.5的缩放系数,让门控值的变化更平滑。

如果训练集比较小,还可以给门控网络加一点L2正则,或者把门控的初始偏置设为较大正值,让模型一开始接近“门开着”的状态,之后再逐步学习关闭哪些交互路径。

5.3 长期预测退化成“复制粘贴”

堆叠太多层时间卷积之后,模型容易学到一种偷懒策略:把输入序列末尾的值直接平移到输出。这在短预测里表现很好,但一旦预测长度拉长,误差会指数级放大。排查方式是在训练时关闭序列末端的直接残差,或者把预测头改成分层输出,每一层负责不同尺度的预测,让模型不能只靠复制。

更直接的办法是在损失函数里加一个预测值差分惩罚项,让模型在未来时刻的变化幅度更贴近真实训练数据的变化分布,而不是过度平滑。

5.4 常见问题速查表

现象特征可能原因处理方式
变量维度卷积无效果kernel_vars过大,核退化为全连接调小变量核宽度,增加分组卷积
训练损失震荡不收敛动态门控无归一化,门值跳变在门控输入端加归一化,初始化偏置
变量相关性强的时段预测差归一化方式不对,低量纲变量被淹没改为每变量独立标准化
长期预测误差放大模型采用末端复制策略削弱末端残差,添加差分损失
离线指标好、上线变差静态相关矩阵参与推理确保线上使用与训练一致的动态门控

6. 一点个人体会

我在实际项目中最大的感受是,FACT这类“细粒度跨变量卷积”真正改变的不是模型结构,而是我们看待变量关系的方式。过去我们总想着把关系“测准”,算一个相关矩阵、画一张热力图就完事;但真实系统里的关系是会呼吸的,有时强有时弱,有时快有时慢,有时方向还会翻转。把问题切换成“在什么样的上下文条件下,哪些变量会以何种方式联动”,模型的表达能力和可解释性都会上一个台阶。

如果你现在的多变量预测项目已经走到了瓶颈,与其继续堆更多层的Transformer,不如先审视一下变量交互是不是被过度简化了。从跨变量卷积开始,加上动态门控,先用小规模实验验证效果,再逐步扩大,这个过程会给你带来不少意想不到的惊喜。

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

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

立即咨询