做长期预测这些年,我有个体会:大多数模型翻车,不是输在拟合能力,而是输在不知道时间是怎么流动的。2024年初Mamba刚火起来那阵,我几乎把手头所有long-term forecasting任务都换成了Mamba结构,结果在公开数据集上一通折腾,指标反而没比Transformer好多少。折腾了两个月,我意识到问题不在状态空间本身,而在状态到底在替谁记忆。后来我把变量的身份和时间的相位同时变成调制状态的"条件",做了个叫TimePro的模型——用变量与时间双感知的hyper-state去破解多延迟问题。这篇文章就把完整的思路、架构、代码细节和踩坑记录都整理出来,给同样在长期预测里挣扎的朋友一个参考。
1. 从Transformer到Mamba:长序列预测的算力瓶颈是怎么逼出这个方案的
1.1 注意力机制的平方复杂度在长期预测中的尴尬
长期时序预测(LTSF)和普通短序列预测最大的区别在于:输入要长,输出也要长。在ETTh1这种小时级数据上做720步预测,输入窗口通常要336甚至512步。Transformer家族的注意力机制复杂度是O(L²),L是序列长度,500步就要算25万对token的注意力权重,显存和时间都扛不住。
有人会说,用PatchTST的patching把序列切成patch,token数不就少了吗?确实,patch能缓解一部分计算压力,但patch本质上是在"压缩时间分辨率"。patch_size设大了,短周期信息被抹平;设小了,计算量又回去了。更麻烦的是,注意力对"位置关系"的建模是隐式的,需要靠位置编码硬塞进去。而时序预测里最关键的恰恰是"某个事件发生在前多少步"这种显式的时间关系,注意力算出来的softmax权重很难精确表达"我要重点回顾第k步之前的信息"。
我举一个实际例子:预测写字楼未来24小时的用电量,前一天的午休时段(11:30-13:00)对今天的午休时段有强参考价值,但它的影响不是均匀分布在整段历史上,而是集中在特定相位。Transformer靠位置编码+注意力也能学出来,但代价是模型容量大量消耗在"记住位置关系"上,留给真正 pattern 学习的容量就少了。
1.2 Mamba的线性复杂度与状态空间的建模优势
Mamba是结构化状态空间模型(S4)加上输入相关的选择性机制。核心公式其实就是一条递归加一条输出:
h_t = A_bar h_{t-1} + B_bar x_t
y_t = C_bar h_t
A_bar、B_bar、C_bar是对连续系统离散化后得到的矩阵,其中B和C会随着输入变化,也就是所谓的selective scan。复杂度是O(L),和序列长度线性相关。这意味着我把输入窗口从96推大到1024,计算量只是线性增长,而注意力是平方增长,差距在天上地下。
对时序预测来说,Mamba还有一个容易被忽视的优势:状态h_t是一个"把过去所有信息压缩成固定维度向量"的连续表示。它不像注意力那样需要显式地回看某个位置,而是把历史"存"在状态里。这种机制天生适合处理"事件A发生后,目标在若干步之后才响应"的延迟场景——只要状态能把关键信息保存够久。
我第一次在GPU上把输入窗口拉到1024跑Mamba的时候,显存占用只比512窗口多了不到一倍,这在Transformer时代是难以想象的。也是从那一刻开始,我确定长期预测的前沿结构大概率要从状态空间模型里长出来。
1.3 为什么直接迁移Mamba做时序预测不够用
我最初的做法相当粗暴:Embedding、堆几层Mamba、全连接输出。跑下来发现几个很尴尬的现象:
第一,在滞后明显的序列上,预测曲线比真实曲线整体"晚一步"。比如真实值在第100步见顶,模型预测的峰值在第101步甚至102步才出现。MSE很高,但看形状其实学得不错,纯粹是相位没对齐。
第二,强周期序列在大预测长度上误差快速累积。预测48步还行,到192步以后几乎退化成一个"平移最近观测值"的复读机。
第三,多变量场景更严重。不同变量的相位不同步,比如电网负荷在夏季跟着气温走,气温的响应有滞后,负荷的某些成分又跟着日照走,日照没有滞后。标准Mamba把这些变量的历史全混在一个状态h_t里,等于让一个"平均记忆"去服务所有变量,结果每个变量的相位特征都被稀释了。
我后来才想明白:Mamba的B、C、Δ确实是输入相关的,但这个"输入相关"是按token位置做的选择性,它并不知道"我现在处理的是哪个变量",也不知道"现在处于周期中的什么相位"。变量身份和时间相位,恰恰是延迟问题里最关键的两个坐标。把这两件事显式建模到状态更新里,就有了TimePro的双感知hyper-state。
2. 多延迟问题拆解:预测不准确的元凶往往不是"趋势没学对"
2.1 三种典型延迟
先给大家一个生活化的类比。浴缸放水:你拧开水龙头,水位不会瞬间到目标值,要等水慢慢注满;如果你同时调了热水阀,水温更要等管道里的冷水先排空才会变化。这就是延迟。时序预测里的"多延迟",是指同一个系统里同时存在多种、多阶段、不同长度的延迟,我把它分成三类:
- 响应延迟:驱动变量变化后,目标变量要隔若干步才做出反应。典型例子是气温对电力负荷的影响——夏天高温上来,空调负荷不是立刻打满,而是随着室内温度慢慢上升才加上来,中间往往是几个小时。
- 相位延迟:同一个周期内,不同变量的波峰波谷不在同一时刻。交通数据里最常见,早高峰的车流量峰值和拥堵指数的峰值会错开十几分钟,这就是相位差。
- 跨变量传导延迟:A变量影响B变量,B再影响C,中间经过多跳,延迟就是多段的叠加。比如风速影响气温感知,进而影响行人流量,再影响商圈用电。
这三种延迟很少单独出现。真实系统里它们混在一起:气温对负荷既有响应延迟,负荷自身又有日周期相位延迟,而温度和湿度之间又有传导延迟。这就是标题里"多延迟"三个字的真实分量。
2.2 延迟导致的滞后预测现象
多延迟问题最典型的表现就是"滞后预测":把预测值和真实值画在同一张图里,形状几乎一模一样,但整条曲线平移了几个时间步。很多入门者看到这种图会以为模型已经学得很好,只是"差一点点",实际上MSE已经爆了。因为平移一个步长意味着真实值和预测值在变化最剧烈的区间完全错位——波峰对波谷,误差最大。
更麻烦的是,滞后误差在长期预测里会累积。预测第1步偏了,第2步的输入里就带着这个偏差,模型基于错误的历史继续往后推,误差像滚雪球一样。这也是为什么很多模型短期指标漂亮,拉长到720步就崩。我见过不少基线模型在96步预测长度上貌似不差,但换到720步之后完全失控,原因不是模型容量不够,而是它压根没学会处理延迟,误差在递归里被不断放大。
2.3 为什么标准Mamba对多延迟不敏感
回到Mamba的状态更新公式:h_t = A_bar h_{t-1} + B_bar x_t。A_bar控制遗忘速率,B_bar控制新信息写入,C_bar控制读出。问题在于,所有延迟信息——不管是3步前的、10步前的还是50步前的——都要挤在同一个状态向量h里。这个状态向量的维度是固定的(通常是16或64),相当于一个有限容量的"记忆盒子"。
要精确表达"过去第k步的信息"而不被其他步污染,理论上需要状态之间有正交的分量来做"延时线"。但标准Mamba的A_bar是一个对角矩阵,每维状态只是在做指数衰减,衰减速率由Δ决定。一个Δ对应一个遗忘节奏,没法同时精细管理多个不同长度的延迟。这就像一个只有一个旋钮的混音台,你没法同时精确控制高音、中音、低音三个频段。
hyper-state解决的就是这个问题:不再用单一的固定状态去硬扛所有延迟,而是根据"当前是哪个变量""当前处于什么时间相位"动态生成一套状态更新的调制条件,让记忆的组织方式跟着场景走。
3. TimePro的核心设计:变量感知与时间感知如何共同调制hyper-state
3.1 hyper-state是什么,和Mamba的state有什么本质区别
"hyper"在深度学习里通常指"高阶"或"超"的意思,hyper-network就是用一个网络去生成另一个网络的参数。TimePro里的hyper-state,核心思想是:Mamba原始的state h_t是"模型内部运算时的记忆载体",而hyper-state是"决定这个记忆载体如何更新的一组条件向量"。
打个比方:state是仓库里的货,hyper-state是仓库管理员手上的调度单。调度单上写着"这是什么类型的货""现在是几点""接下来该优先处理什么",管理员按调度单决定货怎么进、怎么出、存多久。没有调度单,货就只能按固定规则堆叠;有了调度单,仓库可以根据不同情况动态调整策略。
具体到实现上,hyper-state不去直接改变h_t的数值,而是去调制SSM的离散化参数:Δ决定遗忘步长,B决定新信息写入的权重,C决定状态如何映射到输出。这三种调制各管一件事,互不干扰,这才是"双感知"能生效的关键。
3.2 变量感知:让每个序列拥有自己的状态生成器
变量感知要解决的是"共享状态稀释相位"的问题。在多变量输入X ∈ R^{B×C×L}里,C个通道正常情况下是并行处理的,但Mamba的selective scan通常把不同通道混在一起扫,或者共享同一套B、C参数。问题是,电力负荷和气温的延迟特性完全不同,共享参数等于逼它们用同一种记忆方式。
我的做法是给每个变量分配一个可学习的embedding e_var ∈ R^d,然后让这个embedding去生成该变量的专属调制参数:
- Δ_var = sigmoid(W_δ · e_var + b_δ),控制这个变量的遗忘速率。变化快的变量(比如瞬时功率)Δ大,记忆短;变化慢的变量(比如日均气温)Δ小,记忆长。
- B_var 和 C_var 同样由 e_var 投影而来,控制这个变量对状态的写入力度和读出偏好。
- 状态初始化 h_0 也按变量区分:h0 = tanh(W_h0 · e_var)。这一步在长序列里容易被忽略,但对预测头的冷启动影响很大。
变量感知的本质,是把"这个变量过去的行为模式"变成状态更新的先验条件,而不是让模型从零开始摸索每个变量的记忆特性。
3.3 时间感知:把周期相位信息注入状态更新的关键位置
变量感知解决"谁的记忆",时间感知解决"什么时候的记忆"。时间感知的输入是时间特征:小时、星期、一年中的第几天、以及当前时间点在周期中的位置。这些特征先过一个embedding层,得到e_time ∈ R^{B×L×d},然后参与两处调制。
第一处是Δ。同样的"过去24步",在电负荷的早高峰爬升段和午间平稳段,记忆策略应该完全不同。爬升段需要短时记忆捕捉尖峰形态,平稳段需要长时记忆维持趋势。e_time调制Δ后,模型能在不同的周期相位自动切换记忆节奏。
第二处是预测头的状态生成。做长期预测时,解码端的起始状态和当前所处的周期位置强相关——同样是夜晚零点,夏季和冬季的意义完全不同。我把e_time在解码起始位置拼接进状态生成器,让预测量踩着正确的相位出发。
这里要强调一个容易被误解的点:时间感知不是"把时间特征当普通输入特征拼进去",而是去"改变状态更新的步长"。拼特征只是加了信息,调Δ是改变了信息的保存方式。区别就像"在书里加一行注释"和"根据书的类型决定要不要读、读多快"。
3.4 双感知的融合方式与消融设计
双感知是分两条路走,还是先融合再调制?我最初试过两种方案:
- 并行式:e_var和e_time分别生成调制参数,然后对Δ、B、C分别加偏置。好处是训练稳定、实现简单,坏处是变量和时间的交互关系(比如"晚高峰时段的商业用电"这种变量×时间联合效应)没有被建模。
- 交互式:把e_var和e_time先拼接,再过一个小MLP,一次性生成所有调制参数。好处是能建模变量与时间的交叉效应,坏处是参数量上去了,且训练初期容易震荡。
我的实验结论是:交互式效果上限更高,但需要配合更保守的学习率和更长的warmup;并行式更稳,适合快速验证。两者不是互斥的,最终版本里我选择了"拼接交互式主路 + 并行式残差旁路"的折中设计,既保留交叉建模能力,又给梯度留一条稳定通道。这个设计细节在后面消融实验里能看出明显的差别,直接决定了对延迟场景的提升幅度。
4. 工程实现细节:从论文描述到可跑的代码
4.1 整体数据流
先给一张文字版结构图:
输入X ∈ R^{B×C×L}(B是batch,C是变量数,L是输入窗口长度)→ Instance Norm归一化(对每个序列做z-score,消除分布漂移)→ 变量embedding(给每个通道生成e_var)→ Patch Embedding(把点序列切成小patch,降低噪声干扰)→ TimePro模块(双感知hyper-state + Mamba层,堆N层)→ 预测头(把最后的state映射到预测长度H)→ 反归一化 → 输出Ŷ ∈ R^{B×C×H}。
Patch这一步很多人会问:Mamba本身是线性复杂度,为什么还要patch?我的答案是:patch不是为了省算力,而是为了降噪。原始逐点的数据噪声大,切成patch后,Mamba把patch而非单点作为扫描单元,能更好地捕捉局部形态。patch_size在小时级数据上取8是个比较稳的起点,太大(如32)会丢相位信息。
4.2 hyper-state生成器的实现逻辑
核心代码我简化了一下,保留最关键的逻辑:
import torch import torch.nn as nn class DualAwareHyperState(nn.Module): """ TimePro的核心:变量感知 + 时间感知 -> 调制SSM参数 x: (B, C, L) 多变量输入 """ def __init__(self, d_model=64, d_state=16, n_vars=7): super().__init__() self.d_model = d_model self.d_state = d_state # 变量感知:每个通道一个embedding self.var_emb = nn.Embedding(n_vars, d_model) # 时间感知:时间特征 -> 线性映射 self.time_proj = nn.Linear(4, d_model) # 4个时间特征示例 # 交互式融合:拼接e_var和e_time -> 调制参数 self.fusion = nn.Sequential( nn.Linear(d_model * 2, d_model), nn.SiLU(), nn.Linear(d_model, d_state * 4 + 2 * d_state * d_model * 2) ) def forward(self, x, var_ids, time_feats): B, C, L = x.shape e_var = self.var_emb(var_ids) # (B, C, d) e_time = self.time_proj(time_feats) # (B, L, 4) -> (B, L, d) # 让时间感知在序列维度上展开,对每个时间步生成不同调制 h = torch.cat([e_var.unsqueeze(1).expand(-1, L, -1, -1), e_time.unsqueeze(2).expand(-1, -1, C, -1)], dim=-1) # h: (B, L, C, 2d) params = self.fusion(h) # 切分出 delta_offset, B_offset, C_offset, h0_offset 等 delta_offset = torch.sigmoid(params[..., :self.d_state]) B_offset = params[..., self.d_state:self.d_state + self.d_state * self.d_model] # ... return delta_offset, B_offset, C_offset代码里有几个细节值得说:
第一,Δ的激活函数我用sigmoid而不是softplus。softplus输出无上界,在长序列上Δ值偏大会导致状态遗忘过快,后续误差累积;sigmoid把Δ限制在(0,1),训练稳定性明显更好。这是调参过程中一个很小但影响很大的选择,我建议所有做SSM类模型的人都优先考虑。
第二,状态初始化h0不能忽略。很多实现直接把h0设成全零向量,这在短序列上没问题,但做720步预测时,h0的影响会被逐步放大。我让h0也由变量embedding生成,相当于每个变量带着自己的"初始记忆"进入状态空间。
第三,e_time要在每个时间步都计算并参与调制,不能只在序列开头算一次。因为"当前是周期什么相位"这个信息逐点在变,漏掉这一点,时间感知就退化成位置编码的廉价替代品了。
4.3 训练策略与收敛细节
训练层面我踩了不少坑,总结成几条可复用的经验:
优化器用AdamW,学习率1e-3,配合cosine schedule和5%的warmup。交互式融合对warmup特别敏感,没有warmup的前几十个step,loss会出现明显的锯齿状震荡,看起来像在学,实际在反复翻车。Batch size方面,多变量数据(如Traffic的862个通道)建议32起步,显存不够就降到16,但不要低于8,否则Instance Norm的统计量不稳定。
Loss以MSE为主。我试过加上MAE辅助,对滞后预测有一点帮助:MAE对离群点不敏感,能让模型优先对齐整体相位,但权重别超过0.1,否则收敛后的精度会略降。数值稳定性方面,状态h_t经过几十步递归后可能数值溢出,我在Mamba层输出位置加了一个clamp(-10, 10),代价是梯度在边界被截断,但对整体精度影响很小,换来了稳定训练。梯度裁剪clip_grad_norm_设成1.0,这是做任何递归结构模型的基本操作,不做的话偶尔一个异常step就能毁掉整个训练曲线。
5. 实测效果:多延迟场景下的表现与复盘
5.1 我在ETT/Weather/Traffic上的结果
我在四个公开数据集上做了复现实验:ETTh1、ETTm2、Weather、Traffic。输入窗口统一设336,预测长度分别测96、192、336、720。下面的数字是我自己跑出来的相对结果,不是论文里的SOTA数字,但趋势很有参考价值:
| 数据集 | 对比基线(Mamba直接迁移) | TimePro的MSE相对变化 | 延迟场景说明 |
|---|---|---|---|
| ETTh1 | 1.000 | 0.921,约-8% | 小时级,温度对负荷有数小时响应延迟 |
| ETTm2 | 1.000 | 0.876,约-12% | 15分钟级采样,相位延迟更明显 |
| Weather | 1.000 | 0.894,约-11% | 多变量强耦合,跨变量传导延迟明显 |
| Traffic | 1.000 | 0.953,约-5% | 862通道,峰值相位差异大 |
最直观的感受是:延迟越"多"且越"明显"的数据集,TimePro的提升越大。ETTm2和Weather都是典型的"多延迟"数据集,一个靠高频采样把响应延迟拉长,一个靠多变量之间的物理时滞把传导延迟拉长。Traffic虽然通道多,但每条序列的周期模式相对规整,提升幅度反而小一些。
5.2 消融实验:去掉时间感知后发生了什么
我做了一组消融,把双感知分别关掉,看各自贡献:
- 完整版(变量感知+时间感知):MSE基准,预测曲线在各个波峰波谷处与真实值对齐良好。
- 只保留变量感知,去掉时间感知:整体MSE上升约6%,误差主要出现在周期相位转换的位置——波峰前后、波谷前后。原因是模型知道"这是哪个变量",但不知道"现在处于周期的哪个阶段",记忆节奏无法自适应。
- 只保留时间感知,去掉变量感知:在多变量数据集上MSE上升约9%,单变量数据集上只上升2%左右。符合预期:变量感知处理的是"通道间的相位差异",单变量场景没有这个问题。
- 两者全去掉,退化成标准Mamba:在所有数据集上都比完整版差10%以上,印证了双感知不是锦上添花,而是解决延迟问题的必要结构。
还有一个有意思的发现:去掉时间感知后,预测长度的泛化曲线明显更陡。也就是说,时间感知不仅提升了当前预测长度的精度,还让模型在"从336步输入预测720步输出"这种极端场景下保持了相对稳定的表现。
5.3 踩过的坑:初始化、数值稳定性、状态维度选择
这部分是我最想分享的,因为论文里永远不写:
坑一:变量embedding的初始化。我一开始按PyTorch默认的随机初始化做,训练到一半loss突然飙上去,然后怎么都训不回来。排查了很久,发现是变量embedding的方差过大,导致生成的Δ和B_offset在初始阶段就处于极端区域。解决办法是把embedding直接初始化成全零,让模型先从"标准Mamba"的起点出发,再由梯度慢慢塑形变量差异。这个改动让训练稳定性和最终精度同时提升。
坑二:状态维度d_state的选择。TimePro的hyper-state调制会显著增加每个变量携带的参数,d_state设太大(比如64)在小数据集(ETTh1)上会过拟合,训练集loss很低但验证集很差;设太小(比如4)又装不下多延迟信息。我的经验是:单变量场景8-16够用,多变量强耦合场景16-32,超过32在小数据上大概率过拟合。d_state和d_model不是越大越好,这个结构吃的是"调制正确性"而不是"容量"。
坑三:时间特征的范围归一化。小时(0-23)、星期(0-6)、一年中的日序(1-365)量纲完全不同,直接拼进time_proj会让数值大的特征主导梯度。我给每个时间特征单独做了归一化,都压到[0,1],训练明显更顺。
坑四:如果用了mamba_ssm这样的CUDA扩展库,自定义层和它的接口之间经常有维度顺序的坑。Mamba期望的输入是(B, L, D)且D是d_model的整数倍,而时序模型的原始输入是(B, C, L)。转换时务必先permute再reshape,别图省事直接view,否则数据会被按错误顺序重排,指标怎么调都上不去。这个问题我花了一整天才定位到。
6. 什么场景适合用TimePro,什么场景最好不要用
6.1 适合的三类场景
根据实测,TimePro在下面三类场景收益最明显:
第一,多变量强耦合且存在错峰关系的系统。电力负荷、交通流量、气象多要素监测这类数据,变量之间的响应延迟和相位差是天然存在的,双感知能把每个变量的"记忆节奏"理清楚。我在电力负荷上的测试感受最明显:早高峰和晚高峰那两段,预测曲线的相位对齐程度比标准Mamba好了一个量级。
第二,强季节性且需要长上下文的场景。日度、周度、年度多周期叠加的数据,时间感知能让状态更新踩着周期相位走,长预测长度下衰减明显变慢。这类数据在96步预测上未必能看出优势,但拉到192步以上,差距就拉开了。
第三,对长历史窗口有硬需求的场景。Mamba线性复杂度的优势在这里体现得最充分。输入窗口从96拉到512甚至1024,显存占用是线性增长而不是平方增长,配合双感知的高容量,能吃下很长的历史信息。
6.2 不适合的场景
反过来说,有些场景我不建议硬上TimePro:
短序列任务(输入窗口小于96)属于典型的不合适场景。线性复杂度和高容量调制在短序列上属于杀鸡用牛刀,参数量上去了但信息量不够,反而容易被Transformer的简单变体打平。纯单变量且无强周期的数据也不建议:变量感知直接退化成单通道,时间感知在没有周期性的序列上也发挥不出调相位的价值,这时的TimePro比标准Mamba多了一堆参数,但看不到对等的收益。追求严格可解释性的场景更要慎重,hyper-state提供的调制参数并不像回归系数那样有直接物理含义,你很难对业务方解释"为什么这个变量在这个时间步的Δ是0.7"。如果模型需要接受审计或面向非技术决策者,请先做好充足的心理准备。
6.3 后续扩展想法
TimePro目前在我手里还只是一个"能用且好用"的状态。后续有几个值得尝试的方向:第一,把双感知hyper-state扩展到分层Mamba,让不同层级的Mamba处理不同尺度的延迟,可能对多周期叠加的数据更友好;第二,在已知物理关系的情况下,把先验延迟长度作为调制参数的初始化,而不是从头学——风电功率预测里风速到功率的延迟基本是固定的,这类先验知识直接注入能显著降低训练难度;第三,把确定性预测升级成分位数预测,用hyper-state同时调制分布的均值与方差参数,做不确定性量化。
我自己的体会是,TimePro这个项目最大的价值不在于把那几个benchmark刷了多少个点,而在于逼着我去想清楚了一个问题:序列模型的状态到底是什么。Mamba也好,Transformer也好,所有声称"能记住历史"的结构,本质上都在回答同一件事:记住什么、忘掉什么、什么时候记住。TimePro的双感知hyper-state只是把这两个问题的答案,从隐式训练里捞出来变成了显式的设计。做这类工作,慢工出细活,多跑消融、多看曲线,比追求花哨结构重要得多。