ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FACT模型:细粒度跨变量卷积如何攻克动态时序预测

FACT模型:细粒度跨变量卷积如何攻克动态时序预测 多变量时间序列预测做到后面你会发现最折磨人的不是单条序列的趋势拟合而是多条序列之间那只可意会不可言传的交互关系。我前期做一个园区光伏与负荷联合预测的项目光伏出力、空调负荷、楼宇用电几条曲线摆在一起相关性就像夏天的云风向一变关系就变今天还能用的加权组合明天就失灵。后来跳出来换个思路用细粒度跨变量卷积去建模动态变量交互才把很多之前说不清楚的现象逼到了墙角。这篇博文就围绕 FACT 展开把问题背景、模型拆解、工程实现和应用场景完全讲透。适合正在做多序列联合预测、对比 Transformer 或 MLP 类时序方案的读者也适合刚接触跨变量建模、想搞清楚“变量交互”到底该怎么落地的朋友。1. 问题拆解为什么“动态变量交互”才是多变量预测的核心矛盾1.1 静态交互假设的隐患先说一个常见场景。做能耗预测时大家习惯把外部温度、湿度、节假日标记、历史负荷全部拼成一个特征向量喂给模型。模型学到的本质上是一组固定的加权关系温度升高多少度负荷大约增加多少千瓦。这种线性或固定权重的交互在数据平稳的月份里很准确但一旦进入过渡季节早晚温差大、人流量变化明显温度与负荷之间的敏感程度会在一天内反复横跳。再用一个全局固定的相关系数去描述结果必然是一部分时段预测偏大、另一部分偏小。这个痛点不止存在于传统回归和 VAR 模型很多神经网络同样掉进这个坑。MLP 或者 TCN 虽然能拟合非线性映射但它们的权重在训练完以后就固定了。输入变量的排列组合再复杂模型也只能按训练阶段统计出来的“平均交互强度”去推断。我们把这种建模方式叫做静态交互假设假设变量之间的关系在整个使用期间是恒定不变的。在实际业务中这个假设极少成立。拿交通流量来说早高峰时路段 A 和路段 B 的拥堵存在强传导关系但到了平峰期这种传导几乎可以忽略遇到交通事故这种关系又会突然增强。要把这种变化捕捉进来就得把“交互”本身当作一个随时间变化的量而不是一个固定参数。1.2 动态交互的两个主要来源变量交互随时间变化我认为主要来自两个来源。第一个来源是系统自身运行状态的变化。比如一个空调系统制冷模式下电压与功率的关系很直接但在压缩机启停切换的瞬间功率和电压之间会出现短暂的滞后和反弹这种交互模式是随工况切换的。经济系统里更常见牛市里板块联动强熊市里又变成避险资产吸引资金相关性结构跟着市场状态走。第二个来源是外部条件的变化。很多外部变量不会直接进入模型而是通过改变内生变量之间的关系来发挥作用。比如风速影响风电出力但风速对电网频率的影响还取决于此刻负荷的高低同一个外部干预在不同背景下产生的变量联动效果完全不同。这种“被隐藏状态调节的交互”如果模型不显式建模就只能靠海量数据硬扛扛不下来的部分就成了预测误差。当交互变得如此不稳定时简单地把所有变量扔进一个全局映射里等于用一张渔网去兜瀑布怎么兜都兜不干净。1.3 注意力机制依然不够“细”Transformer 在时序领域火了以后大家自然想到用注意力去建模变量相关关系让每个变量去查询其他变量的状态把相似度当权重。这个思路比固定参数灵活但它有两个容易被忽略的问题。第一标准的自注意力作用在时间维度上不同变量通过 embedding 投影进同一个空间再计算相似度。这种方式把“变量身份”和“时间状态”混在一起很容易出现两个变量数值接近但语义无关的误匹配。第二注意力权重是全局归一化的它对局部模式的敏感性不足。动态交互往往发生在很短的窗口里——比如异常事件发生后的十几分钟内变量间突然出现强耦合过了这段时间又消失。全局注意力对这种瞬时局部变化是钝感的它会把这十几分钟的特殊耦合稀释在整个序列的平均注意力里。FACT 的切入点是既然交互是局部的、且随时间变化的那就用卷积来建模。卷积天然处理局部窗口跨变量卷积又能在变量维度上做组合如果把卷积核的动态生成和时序滑动结合起来就能得到一种既能感知局部、又能随上下文变化的交互建模方式。2. 细粒度跨变量卷积设计思路与核心动机2.1 “细粒度”到底细在哪里细粒度这个词不少论文都用但 FACT 语境下的细粒度我理解是三个层面。第一层是时间粒度。不是把整个历史序列编码成一个全局向量后再算交互而是在每个局部窗口内部计算变量之间的交互结构。窗口滑到哪里交互就算到哪里这样交互模式可以随窗口漂移。第二层是变量对的粒度。不是对所有变量做一个全局混合而是显式建模每一对变量之间的局部耦合关系。说白了变量 A 和变量 B 之间的关系统一建模变量 A 和变量 C 之间单独建模它们的演化节奏可以不一样。第三层是卷积核的参数粒度。同一组卷积核不一定要在整条序列上完全共享而是可以根据局部上下文动态生成。这样模型既能享受卷积的参数共享优势又不会因为核固化而丢失交互的动态性。这三点组合起来就是我们说的“细粒度跨变量卷积”在时间维上滑窗在变量维度上对通道分组或逐对建模在核参数上按需生成。2.2 为什么是卷积不是全连接也不是注意力把变量交互建模成卷积有几个很实际的好处。首先是参数效率。假设有 C 个变量如果用全连接层直接建模变量间的交互需要 C×C 个参数而且每个时间步都要单独算一次。用卷积核心参数是卷积核的大小假设核宽为 K跨变量卷积的参数量大致是 C×C×K但因为核在时间维上滑动共享实际需要学习的独立参数远小于在每个时间步都使用全连接的情况。其次是局部归纳偏置。变量之间的交互通常不是瞬间完成的而是有一个传导过程。以电价和负荷的关系为例负荷上升不会立刻推高电价中间有十几分钟到几小时的市场响应延迟。卷积核天然覆盖一个时间窗口通过窗口内的加权组合可以直接建模这种“一个变量的历史信息如何影响另一变量的当前状态”的过程。注意力也能做到这一点但它的问题是计算复杂度随序列长度平方级增长而且注意力没有显式的“核”概念你很难解释变量 A 在滞后 2 步和滞后 3 步时对变量 B 的贡献分别是多少。卷积核给出的解释要直接得多核的每一格就是一阶滞后的影响系数这在调试和做可解释分析时非常有用。2.3 把“变量”当通道跨变量卷积的基本形态在信号处理中卷积通常作用于单通道或多通道信号。把多变量时间序列看作多通道信号是非常自然的操作时间轴是卷积的滑动维每个变量就是通道。传统的一维卷积在做通道融合时会通过输出通道的加权组合把输入通道混合起来这其实就是一种跨变量交互。FACT 的做法更进一步。它不只做一次静态的通道混合而是让这个混合权重随局部上下文变化。具体来说输入是一个形状为 (B, T, C) 的张量B 是批次T 是时间长度C 是变量数。卷积核的形状是 (C_out, C_in, K)其中 K 是时间窗口长度。过一遍卷积输出变量 j 在时间 t 的激活值等于输入变量 i 在 t-K1 到 t 窗口内的值乘以核权重后的累加。这个计算过程把两件事同时做了沿时间方向聚合局部历史信息沿变量方向做通道混合。如果核权重是静态的这就是一个普通的 1D 卷积如果核权重根据输入动态生成那就是 FACT 的核心组件——动态跨变量卷积。3. FACT 框架核心模块拆解3.1 输入预处理与变量嵌入真正动手搭 FACT 时第一步不是接卷积层而是把输入整理成适合卷积操作的结构。最常见的方式是对原始序列做实例归一化。多变量序列里不同变量的量纲可能差很多负荷的数值是兆瓦级温度是十几到几十度如果不做归一化卷积核会倾向于忽略数值小的变量导致某些交互永远学不出来。我一般对每个样本单独做标准化而不是在整个训练集上做全局统计这样可以避免分布漂移带来的问题。做完标准化还要考虑是否做 patch 化。PatchTST 这类模型告诉我们把相邻时间步拼成一个 patch 可以减少序列长度、扩大感受野还能降低噪声干扰。FACT 也可以采用类似思路但要注意 patch 的尺寸不宜太大否则局部交互的细粒度会被磨掉。我常用的配置是 patch 步长为 4 到 8patch 长度为 8 到 16。这个参数需要根据数据的采样频率来调整高频数据可以小一点低频数据可以大一些。3.2 核心模块动态跨变量卷积块动态跨变量卷积块是整个 FACT 框架的心脏。它的输入是经过归一化和 patch 化后的张量输出是同等形状的张量。块内部可以拆成三条支路上下文提取、动态核生成、卷积作用。上下文提取负责从输入张量中提炼与当前时间步相关的“环境信息”。它不需要复杂的编码器一个小的多层感知机加上平均池化就够用。提取出来的上下文向量代表的是当前局部状态下变量之间交互的倾向比如当前处于高负荷时段还是低谷时段这决定了交互应该呈现出哪种模式。动态核生成拿到上下文向量后通过一个生成网络输出卷积核的权重。注意这一步是动态卷积的关键也是最容易失控的地方。如果直接生成全部 C×C×K 个参数变量数稍多就会让生成网络的输出维度爆炸而且很难训练。实践中更稳妥的做法是采用低秩分解或者超网络加缩放的设计要么让生成网络输出两组低秩矩阵相乘来近似整个核要么让生成网络输出一个缩放系数去调制一个静态基础核。最后一步是把生成的核作用到输入张量上。可以用 PyTorch 的 conv1d 或者 F.conv1d 实现。如果动态核是逐样本不同的那就需要利用分组卷积或者逐样本循环来处理这一点在工程实现时容易忽略后面细说。3.3 堆叠、残差与感受野一个动态跨变量卷积块的感受野毕竟有限。FACT 一般通过堆叠多个块来扩展感受野让上层的块有机会看到更长的历史窗口从而建模更长期的交互变化。堆叠的时候残差连接几乎是必须的。原因很好理解动态核生成是个非线性的高阶过程如果每个块都直接覆盖原始输入梯度传递路径过长很容易出现梯度消失。加上残差连接每个块只需要学习相对输入的增量变化训练稳定性会好很多。在堆叠过程中还有一个值得注意的设计选择是否在每个块之后插入时间维的降采样。降采样可以扩大后续块的感受野但也可能丢失细粒度信息。我的经验是前一到两个块保持原始分辨率后续块再逐步降采样这样既照顾了局部模式也保证了长程依赖。3.4 输出头与损失函数预测头的设计取决于预测目标。做单步预测的话直接把最后一个时间步的隐状态接一个线性层输出即可做多步预测可以选择直接多输出也可以采用自回归解码。损失函数方面时序预测通常用 MSE 或 MAE。如果是概率预测可以在输出头同时输出均值和方差然后用负对数似然作为损失。还有一个容易被忽略的细节在多变量预测中不同变量的量纲差异意味着它们的损失天然会有不同的尺度。如果不做加权模型会自动偏向尺度大的变量。我建议在计算损失之前对每个变量的误差做标准化或者直接使用除以真实值绝对值的百分比误差类指标。4. 实操过程从头实现一个可用的 FACT 简化版4.1 简化版动态核生成的代码实现理论讲了这么多还是要落到代码上。我给出一个可以在小数据集上直接起跑的 FACT 核心模块用 PyTorch 实现尽量精简但保留了关键设计。import torch import torch.nn as nn import torch.nn.functional as F class DynamicCrossConv1d(nn.Module): def __init__(self, C, K3, hidden64, low_rank8): super().__init__() self.C C self.K K self.low_rank low_rank # 基础静态核作为动态调制的基底 self.base_weight nn.Parameter(torch.randn(C, C, K) * 0.02) # 从局部上下文生成动态调制系数 self.context_encoder nn.Sequential( nn.Linear(C * K, hidden), nn.ReLU(), nn.Linear(hidden, (C low_rank) * K 1) ) def forward(self, x): # x: (B, T, C) B, T, C x.shape K self.K # 用窗口均值作为上下文这里简化只取前K步 context x[:, :K, :].reshape(B, -1) params self.context_encoder(context) # 从参数中切分出缩放系数、行低秩矩阵、列低秩矩阵 scale torch.tanh(params[:, :1]) * 0.5 1.0 row params[:, 1:1 self.low_rank * K].reshape(B, self.low_rank, K) col params[:, 1 self.low_rank * K:].reshape(B, self.low_rank, C * C) # 生成逐样本核: base_weight 被行/列低秩结构调制 # 这里使用外积近似: 核 base_weight sigma(row,col) kernel self.base_weight.unsqueeze(0) * scale # (1, C, C, K) # 简化的低秩更新实际可以再精细 low_rank_update torch.einsum(blk,blc-bck, row, col.view(B, self.low_rank, C, C).sum(dim1)) kernel kernel low_rank_update.unsqueeze(-1) * 0.1 # 逐样本进行卷积 x_perm x.permute(0, 2, 1) # (B, C, T) outs [] for b in range(B): w kernel[b] # (C, C, K) out F.conv1d(x_perm[b:b1], w, paddingK // 2) outs.append(out) return torch.stack(outs, dim0).permute(0, 2, 1)这个实现是教学性质的不是直接上生产的高性能版本。几个关键点第一用缩放系数加低秩更新的方式生成动态核而不是直接生成一个巨大的 C×C×K 张量。我实测过直接生成的方式在 C 大于 16 时训练速度明显下降而且容易出现 NaN。低秩近似牺牲了一小部分表达能力换来了训练稳定性和生成网络的小体积。第二逐样本循环卷积在 B 和 C 较大时效率不高。如果要上大规模数据建议用 grouped convolution 或者把生成权重 batch 起来一次性计算。对原型验证来说循环是最直观、最不容易出错的写法。第三上下文编码器只用了前 K 个时间步这是个极大的简化。真实使用中可以用一个 1D 卷积或者 GRU 把更长的上下文压缩成一个向量这样核能感知的变化范围会更大。4.2 训练稳定性的三个实用技巧动态卷积模型最让人头疼的问题是训练不稳定。我踩过几次坑之后总结了三件必做的小事。第一件实例归一化放在第一位。动态核生成依赖输入上下文如果上下文的统计特征在不同样本间差异过大生成网络就很难收敛。对每个样本做标准化相当于把上下文的分布压到一个相对窄的范围生成网络只需要学“相对模式”而不是“绝对数值”。第二件对生成的核做约束。我见过最典型的问题核权重在训练后期无规律放大导致预测值爆炸。解决方式是每步生成后做一次 clip 或规范化比如把核的 L2 范数限制在一个上限内。这个上限设 3 到 5 就够用太大起不到约束作用太小又会限制表达能力。第三件先用小学习率跑一个静态核版本的 FACT再打开动态生成分支。逻辑很简单静态版本相当于动态版本的一个特例如果特例都训不好动态版本大概率也白搭。把静态版本训到一定指标后冻结静态核和输出头只训练生成网络这个课程式的训练顺序能显著降低训练初期的发散概率。4.3 参数效率和变量数扩展关于参数效率我做过一个很有意思的对比。同样是 20 个变量的时序数据直接用全连接跨变量层单层参数量是 20×20400用一个核宽为 5 的跨变量卷积核心参数是 20×20×52000看似更大但因为核在时间维上滑动共享实际每个时间步摊下来的参数量是 2000/TT 是序列长度序列越长反而越划算。注意力机制的参数主要在投影空间但计算量随长度平方上涨而且可解释性差。如果变量数继续增长到 100 以上逐对卷积核的规模也会失控。这时有两种工程化的降维思路。一种是在输入侧先做变量嵌入把 100 个变量映射到 32 维的隐空间再在这个隐空间上做跨变量卷积另一种是用稀疏连接或聚类先根据历史相关性把变量分组组内做密集跨变量卷积组间只保留低频交互。后一种方案更接近细粒度的本意把力气花在真正有强交互的变量对上。5. 应用场景不同预测任务里的变量交互模式5.1 能源电力光伏、负荷与电价能源调度是我接触最多的场景也是动态变量交互最显著的领域。光伏出力和负荷之间交互模式随天气和人类活动变化晴天中午光伏峰值和空调负荷峰值叠在一起阴天光伏骤降负荷需求却可能不变甚至上升。用 FACT 建模时动态核可以捕捉到“太阳辐射变化一个单位在不同云量、不同日期下对净负荷的边际影响不同”这种细微的关系。实际操作中我把辐射、云量、温度、历史负荷、历史光伏出力作为输入变量。最终模型在预测净负荷时读出的卷积核权重确实会随时间发生明显的结构化变化上午时段辐射与光伏出力的核权重高傍晚时段温度和负荷的交互权重抬升。这个可解释性让调度人员更容易信任模型的结果。5.2 交通流上下游路段与事件联动交通预测中的变量交互动态性同样突出。上下游路段的车流传导方向是确定的但强度受时段、天气、事故和信号控制策略的影响。用 FACT 把多个路段的历史流量同时作为变量卷积核能学到事故发生后大概 3 到 5 个时间步内下游路段流量才开始呈现异常抬升这个滞后的信号会体现在核的不同滞后期位置。我在交通场景中特别注意了输入变量的顺序。卷积核对变量的排列顺序是敏感的如果把空间位置相近的路段放在相邻的变量位置上核就更容易学到空间局部性。这种“变量重排先验”在一般模型中常被忽略但在跨变量卷积模型里是一个免费的提升手段。5.3 金融与经济相关性结构的切换金融场景的变量交互更多表现为状态切换。股票、债券、商品之间的相关性在风险偏好变化时会整体切换不是说某个变量对的交互逐渐变化而是全矩阵在阶段之间突变。FACT 的动态核生成天然适合这种任务生成网络接收到的上下文不同输出的核就可能落在不同的参数区域从而形成类似于“状态感知的交互模式”。这里要提醒一句金融时间序列信噪比极低FACT 很容易过拟合到噪声交互上。我建议在损失函数中加入核权重的正则项并增加验证集上的早停策略。宁可交互学得保守一点也不要去追逐那些无法复现的瞬时相关性。下面用一个表把几类典型场景的核心变量、交互特性和 FACT 的优势概括出来场景典型变量交互特性FACT 建模优势能源电力光伏出力、负荷、温度、辐射受天气和时段影响交互强度随工况变化动态核直接刻画边际影响的时变性交通流各路段流量、速度、天气空间传导存在滞后事件引发局部增强卷积窗口显式建模滞后效应金融多资产收益、波动率相关性存在状态切换和突变生成网络输出核随上下文状态切换工业过程温度、压力、流量、转速设备工况切换导致耦合关系变化局部窗口感知工况变化不依赖全局假设6. 常见问题与排查技巧实录6.1 训练初期损失下降缓慢如果你发现 FACT 的训练损失几乎没有下降先检查是不是动态核生成部分学得太慢。一个很有效的排查方法把动态生成的核输出打印出来看不同样本之间的核差异是否在训练初期就很明显。如果一开始就差异巨大那大概率是生成网络过拟合到噪声上如果几乎没差异则是生成网络没有接收到有效梯度。修复办法通常是调整上下文编码器的容量。太小的编码器学不到有用的上下文表征太大的编码器又容易让核的变化幅度失控。建议从两层 MLP、隐藏层 64 开始观察损失变化再逐步增加。6.2 预测值整体偏小或偏大整体偏移一般不是动态核的问题而是输出头的问题。跨变量卷积建模的是交互增量输出的均值信息如果被卷积的局部差分性质削弱预测就会系统性偏离。解决方法是输出头加上一个可学习的 bias 项或者在最终预测前做一个水平校正比如把预测结果的均值对齐到最近一段真实值的均值。6.3 感受野不够导致交互建模失真变量之间长周期的交互比如月度级别的需求关联如果卷积窗口只有几个时间步模型无论如何都捕捉不到。排查方法是做一个简单的敏感性测试延长输入序列的长度看验证集指标是否明显提升。如果提升了说明感受野不够可以增加卷积块层数、扩大核宽或者插入降采样层。如果没提升那问题多半在特征或数据质量而不在模型结构。6.4 动态核退化为静态核调试时我习惯把训练后动态核的实际变化幅度统计出来计算每个核在不同样本间的方差。如果方差趋近于零说明模型找到了一个“偷懒”的解直接用静态模式就够拟合训练集了。这未必是坏事说明当前数据里的动态交互本来就不强。但如果验证集表现差那很可能是模型猜错了方向——真实数据有动态交互但生成网络没学会触发条件。此时可以把生成网络的输入范围扩大比如塞进更多上下文信息让核的变化对整个输入更敏感。7. 最后再分享一个实践经验在我自己的项目里真正让 FACT 类方法发挥作用的其实不是模型本身有多复杂而是我花了很多时间去理解数据里的交互机制。每一次调整卷积核大小、修改上下文编码器、改变正则化强度都是在和数据里的物理过程做对话。如果你正在做的项目也有明显的变量联动建议先画一张交互热度图把各个变量对在不同时段的相关系数变化趋势画出来。这张图会直接告诉你动态交互到底存在不存在、存在于哪些变量上、变化周期大概是多长。拿着这张图再去定 FACT 的核宽、层级和是否启用动态分支会比盲目套用论文配置高效得多。这就是我用了很久的建模前检查流程也希望大家能从中找到适合自己的调试节奏。
返回列表