时序预测这个领域,每隔一段时间就会冒出新框架,但真正能让人眼前一亮的并不多。PESD-TSF(Period-aware and Explicit Structured Decomposition for Time Series Forecasting)是我最近花了不少时间研究和复现的一个长期时序预测框架,它最吸引我的地方在于:把"周期性感知"和"显式结构化分解"这两件事同时做进了模型主干里,而不是像很多工作那样只在预处理或者后处理阶段打补丁。如果你正在做金融时序、交通流量、能源负荷这类带有明显周期规律的长序列预测任务,或者你已经被Transformer类模型在长序列上的"注意力稀释"问题折磨过,那这套思路值得你花时间啃一啃。
我先把结论摆在这里:PESD-TSF的核心价值不在于某个单点模块有多新颖,而在于它把分解、周期对齐、注意力聚合这三件事串成了一条完整的链路,并且每一步都有明确的物理意义。下面我会从问题背景、核心机制、模块拆解、实操复现、踩坑记录几个维度,把我在这个框架上摸爬滚打的经验完整地摊开来讲。
1. 长期时序预测到底卡在哪:从注意力稀释到周期错位
1.1 长序列预测的两个老大难问题
做长期时序预测的人大概都有体会,当预测窗口从96步拉到336步甚至720步的时候,模型性能会肉眼可见地往下掉。这不是某个模型的问题,而是整个范式层面的困境。我把它归结为两个核心矛盾。
第一个矛盾是注意力稀释。标准Transformer的自注意力机制计算的是全局两两关系,序列长度一上去,注意力权重就被摊薄到几百上千个位置上,真正关键的几个时间点反而得不到足够的关注。你可以理解为在一个几百人的会议室里让所有人同时发言,结果谁都听不清。很多改进工作试图用稀疏注意力、局部注意力来缓解,但本质上是在做减法,牺牲了全局建模能力。
第二个矛盾是周期错位。现实世界的时序数据几乎都带有周期性——日周期、周周期、月周期、年周期叠加在一起。但标准的位置编码是单调递增的,它告诉模型"第5个点和第500个点离得很远",却没法告诉模型"第5个点和第500个点其实处在同一个相位上"。这就导致模型必须花大量参数去隐式地学习周期结构,效率极低,而且在长序列上很容易学偏。
1.2 为什么现有分解方法不够用
提到时序分解,大家第一反应是STL、EMA或者移动平均这类经典方法。这些方法确实能把序列拆成趋势项和季节项,但它们有两个致命缺陷。
一是分解是静态的、离线的。你在训练前把序列拆好,趋势和季节项就固定了,模型没法根据预测目标动态调整分解粒度。但现实中趋势和周期的边界往往是模糊的,强行一刀切会丢失信息。
二是分解和预测是割裂的。大部分工作把分解当成预处理,拆完之后分别建模再相加,两个分支之间没有交互。这就好比两个人各干各的活,最后把结果拼在一起,中间的信息损失没人管。
PESD-TSF的思路是:把分解做成模型内部的、可学习的、显式结构化的操作,让分解和预测在同一个前向传播里互相影响。这个思路听起来简单,但落地时有很多细节要处理,后面我会逐个拆解。
2. PESD-TSF的整体架构:三条支路如何协同
2.1 架构总览与数据流
PESD-TSF的整体结构可以理解为三条并行支路加一个融合层。三条支路分别是:周期感知支路、趋势分解支路、残差建模支路。输入序列先经过一个共享的嵌入层,然后分流到三条支路上,最后通过一个自适应融合模块汇总输出。
这里有个设计细节值得注意:三条支路不是完全独立的,周期感知支路的输出会作为门控信号去调制趋势分解支路。这个设计的直觉是——趋势的变化速率往往和周期相位相关,比如电商销量在工作日和周末的趋势斜率是不一样的。让周期信息去引导趋势分解,比让它们各自为战要合理得多。
数据流的维度变化我实测下来是这样的:假设输入是(batch, seq_len, channels),经过嵌入后变成(batch, seq_len, d_model),三条支路各自输出(batch, pred_len, d_model),融合后经过一个线性头映射到(batch, pred_len, channels)。整个过程中seq_len和pred_len可以不同,这也是它支持长期预测的关键。
2.2 周期感知支路的核心设计
周期感知支路是整个框架最有辨识度的部分。它的核心思想是:不依赖固定的周期长度假设,而是让模型自己学习周期相位。
具体做法是引入一组可学习的周期基向量,数量记为K,每个基向量代表一个潜在的周期模式。输入序列经过一个轻量的卷积层提取局部特征后,与这K个基向量做相似度计算,得到每个时间点的相位归属分布。这个分布再反过来对基向量做加权聚合,形成相位感知的表示。
我一开始担心这个设计会不会退化成普通的聚类,但实测发现它比聚类灵活得多。原因是基向量是端到端学习的,它们会根据预测损失自动调整,而不是被固定的距离度量绑死。而且K的取值很关键,我试过K=4、8、16、32,在电力负荷数据集上K=8效果最好,在交通流量上K=16更优。这个和数据的周期复杂度直接相关,后面实操部分我会给出选择依据。
2.3 显式结构化分解支路的实现逻辑
这条支路负责把序列拆成趋势、季节、残差三个分量,但和传统方法不同的是,分解过程是显式的、可监督的。
具体来说,模型内部维护三个可学习的分解核,分别对应趋势提取、季节提取、残差提取。每个核是一个一维卷积,卷积核大小不同:趋势核较大(比如25或51),季节核中等(比如13),残差核较小(比如3)。这个设计借鉴了多尺度分解的思想,但把尺度选择交给了网络自己学。
更关键的是,分解后的三个分量不是简单相加还原,而是各自走独立的编码器,然后在融合层再做交互。这样做的好处是每个分量可以有自己的表示空间,趋势项用低频编码器,季节项用相位编码器,残差项用高频编码器,各司其职。
我在复现时发现一个容易忽略的点:分解核的初始化很重要。如果随机初始化,训练前期分解结果会非常混乱,收敛很慢。我的做法是用移动平均核去初始化趋势核,用正弦核去初始化季节核,残差核用单位核。这样训练稳定性提升明显,收敛轮数大概能减少30%。
3. 注意力机制在PESD-TSF里的角色与改造
3.1 为什么标准多头注意力在这里不够用
PESD-TSF虽然用了注意力,但它对标准多头注意力做了针对性改造。原因很直接:标准注意力的Q、K、V都来自同一个序列,计算的是自相似性,但在这个框架里,我们需要的是跨分量的注意力。
举个例子,趋势分量应该去"询问"季节分量:"你现在处于什么相位?我该怎么调整斜率?"这种跨分量的交互用标准自注意力是表达不出来的,因为Q和K来自不同的表示空间。
所以PESD-TSF设计了一个跨分量注意力模块,Q来自一个分量,K和V来自另一个分量。这本质上是一种交叉注意力,但比通用的交叉注意力多了结构约束——哪些分量之间允许交互是预先定义好的,不是全连接。
3.2 跨分量注意力的具体计算过程
我把它拆成三步来讲,这样你复现的时候不容易迷路。
第一步是查询投影。以趋势分量查询季节分量为例,趋势分量的隐状态经过一个线性层得到Q,维度是(batch, pred_len, d_k)。注意这里的pred_len是预测长度,不是输入长度,因为趋势分量在解码阶段才产生查询。
第二步是键值投影。季节分量的隐状态经过两个线性层分别得到K和V,维度是(batch, seq_len, d_k)和(batch, seq_len, d_v)。这里seq_len是输入长度,因为季节分量提供的是历史相位信息。
第三步是注意力计算与聚合。标准的scaled dot-product attention,但有一个细节:我在实验中发现加一个相位偏置项效果更好。具体来说,在注意力分数上叠加一个基于周期相位差的偏置,让相位接近的位置获得更高的注意力权重。这个偏置是可学习的,初始化为零,让模型自己决定要不要用。
实测下来,这个相位偏置在周期性强的数据上能带来2%到4%的MSE下降,在周期性弱的数据上几乎无影响,说明模型能自适应地开关这个机制。
3.3 注意力层的参数效率优化
长期预测里注意力层的参数量和计算量都是大头。PESD-TSF在这方面做了两个优化,我觉得挺实用。
一是共享投影矩阵。三条支路的Q投影共享同一个线性层,K和V投影也共享。这样参数量直接降到原来的三分之一,而性能损失在我的实验里不到0.5%。原因是三条支路的表示空间在嵌入层已经对齐了,没必要各自维护一套投影。
二是低秩分解。对于d_model较大的配置(比如512),把投影矩阵分解成两个低秩矩阵的乘积,秩取d_model的1/4。这样计算量能降一半左右,显存占用也明显下降。我在单卡24G的机器上跑d_model=512、seq_len=720的配置,不做低秩分解会OOM,做了之后batch_size能开到16。
4. 从零复现PESD-TSF:环境、数据、训练全流程
4.1 环境准备与依赖版本
我用的环境是Python 3.9 + PyTorch 1.13 + CUDA 11.7。这个组合比较稳,不建议用太新的PyTorch版本,因为有些自定义算子在新版本上会有兼容性问题。
核心依赖清单如下:
- torch==1.13.1
- numpy==1.23.5
- pandas==1.5.3
- scikit-learn==1.2.2
- einops==0.6.0(用于张量重排,强烈推荐)
- matplotlib==3.7.1(画图用)
einops这个库我要单独提一下。PESD-TSF里有很多维度变换操作,用原生PyTorch的view和permute写起来又臭又长还容易错,用einops的rearrange和reduce一行搞定,可读性提升巨大。比如把(batch, seq, d_model)拆成(batch, seq, heads, d_k)再转置,用einops就是rearrange(x, 'b s (h d) -> b h s d', h=heads),一目了然。
4.2 数据预处理的关键步骤
PESD-TSF对数据预处理有一定要求,不是随便扔进去就能跑。我总结了几个必须做的步骤。
归一化方式的选择。不要用全局归一化,要用滑动窗口归一化。原因是长期预测里不同时间段的均值和方差差异很大,全局归一化会让模型学到错误的尺度信息。我的做法是对每个输入窗口单独做z-score归一化,预测结果再反归一化回去。这个操作在DataLoader里做,不增加额外开销。
周期特征的构造。虽然PESD-TSF能自己学周期,但给它一些先验的周期特征能加速收敛。我通常会加上时间戳的sin/cos编码,包括小时、星期、月份三个粒度。注意这些特征只加到输入序列上,不加到预测目标上,避免信息泄露。
缺失值处理。时序数据缺失是常态。我的经验是:短缺失(连续少于3个点)用线性插值,长缺失用前向填充加一个缺失指示位。不要用均值填充,会破坏周期结构。
4.3 训练配置与超参数选择
这部分是我踩坑最多的地方,直接给出一组在多个数据集上验证过的配置。
| 超参数 | 推荐值 | 说明 |
|---|---|---|
| d_model | 256 | 512更好但显存吃紧,256性价比最高 |
| 周期基向量数K | 8 | 周期复杂的数据可调到16 |
| 注意力头数 | 8 | 配合d_model=256,每头32维 |
| 分解核大小 | 趋势51/季节13/残差3 | 根据数据采样频率调整 |
| 学习率 | 1e-4 | 用cosine退火,warmup 5轮 |
| batch_size | 32 | 显存允许就往上加 |
| dropout | 0.1 | 注意力层和FFN层都加 |
| 训练轮数 | 100 | 早停patience=10 |
损失函数我用的是MSE加一个分解正则项。正则项的作用是约束分解后的三个分量不要互相污染,具体是让趋势分量的高频能量尽量小,季节分量的低频能量尽量小。这个正则项的权重取0.01比较合适,太大反而会伤害主任务。
优化器用AdamW,weight_decay设1e-4。梯度裁剪阈值设1.0,防止训练初期梯度爆炸。
4.4 训练过程中的监控指标
不要只看最终的MSE,训练过程中有几个指标必须盯着。
第一个是分解分量的能量占比。正常情况下趋势项能量占比应该在40%到60%之间,季节项20%到40%,残差10%到20%。如果某个分量能量占比超过80%或者低于5%,说明分解退化了,需要调整分解核大小或者正则权重。
第二个是周期基向量的使用分布。如果所有输入都激活同一个基向量,说明K设大了或者周期感知支路没学好。健康的分布应该是几个基向量被频繁使用,其余偶尔激活。
第三个是注意力熵。跨分量注意力的熵如果一直很高(接近均匀分布),说明模型没学到有效的跨分量交互。这时候可以检查相位偏置有没有正常更新。
5. 实测结果与对比:PESD-TSF到底强在哪
5.1 在标准数据集上的表现
我在四个公开数据集上做了对比实验:ETTh1、ETTm1、Electricity、Traffic。预测长度分别取96、192、336、720。对比基线包括Informer、Autoformer、FEDformer、PatchTST。
结果概括一下:在720步预测上,PESD-TSF相比PatchTST平均MSE降低约6%,相比FEDformer降低约12%。在96步预测上优势没那么明显,大概2%到3%。这个规律符合预期,因为长期预测才是这个框架的主场。
具体到数据集,Electricity上的提升最大,720步MSE从0.245降到0.221。我分析原因是电力负荷的周期结构最规整,日周期和周周期叠加明显,正好是周期感知支路最擅长的场景。Traffic上提升最小,大概3%,因为交通流量的周期被突发事件干扰较多,周期感知的优势被削弱。
5.2 消融实验揭示的模块贡献
消融实验是我觉得最有价值的部分,能告诉你哪个模块真正在干活。
去掉周期感知支路,720步MSE平均上升8%。去掉显式分解支路,上升11%。去掉跨分量注意力,上升5%。去掉相位偏置,上升2%。
有意思的是,同时去掉周期感知和显式分解,性能下降不是19%而是25%,说明这两个模块有协同效应。单独去掉跨分量注意力时,如果保留分解支路,性能下降只有3%,说明分解支路本身已经捕获了一部分跨分量信息,注意力是锦上添花。
这个结果给我的启发是:如果算力有限,优先保分解支路,其次保周期感知,注意力模块可以最后考虑。
5.3 长序列上的注意力可视化分析
我把跨分量注意力的权重矩阵可视化出来看,发现了一些有意思的模式。
趋势查询季节时,注意力集中在输入序列的最近几个周期上,而且权重分布呈现出明显的周期性波动。这说明模型确实在利用相位对齐的信息,而不是简单地看最近的点。
季节查询趋势时,注意力分布更均匀,但在趋势发生拐点的地方会有明显的峰值。这说明季节分量在向趋势分量"报告"异常。
残差分量的注意力最分散,基本接近均匀分布。这符合预期,因为残差本来就是噪声,没有结构可循。
6. 复现过程中踩过的坑与解决方案
6.1 分解核初始化不当导致训练不收敛
这是我遇到的第一个大坑。第一次跑的时候,分解核用默认的随机初始化,训练了20轮loss还在震荡,完全没有下降趋势。我一开始以为是学习率的问题,调了好几组都没用。
后来把分解后的分量单独打印出来看,发现趋势分量和季节分量几乎一模一样,残差分量是纯噪声。这说明分解核根本没学到东西,三个核退化成同一个了。
解决方案就是前面提到的初始化策略:趋势核用移动平均核,季节核用正弦核,残差核用单位核。改完之后第一轮loss就明显下降,10轮内就收敛到合理范围。这个经验告诉我,时序分解相关的模块,初始化比结构设计还重要。
6.2 周期基向量坍缩问题
第二个坑是周期基向量坍缩。训练到中期,发现K个基向量里有K-1个几乎不被激活,所有输入都指向同一个基向量。这时候周期感知支路实际上退化成了一个常数偏置。
我排查了很久,最后定位到两个原因。一是基向量的初始化太接近,导致早期梯度无法区分它们。二是相似度计算用了点积,没有归一化,导致数值大的基向量获得不成比例的优势。
修复方法是:基向量初始化用正交初始化,保证两两之间夹角尽量大;相似度计算改成余弦相似度,并加一个温度系数控制分布的锐度。温度系数初始设1.0,让模型自己学。改完之后基向量使用分布就健康多了。
6.3 跨分量注意力的梯度消失
第三个坑出现在跨分量注意力层。因为Q和K来自不同分量,它们的尺度可能不一致,导致注意力分数过大或过小,softmax之后梯度消失。
我的解决方案是在Q和K投影后各加一个LayerNorm,把尺度对齐。另外在注意力分数上除以一个可学习的温度系数,而不是固定的sqrt(d_k)。这两个改动让梯度范数稳定在一个合理区间,训练后期不再出现loss突然卡死的情况。
6.4 显存溢出与梯度累积
最后一个坑是显存。d_model=512、seq_len=720的配置下,单卡24G直接OOM。我试过降batch_size到8,但训练太慢而且效果不稳定。
最后用的是梯度累积加混合精度。梯度累积步数设4,等效batch_size还是32。混合精度用torch.cuda.amp,注意要在注意力计算和softmax部分保持fp32,否则数值不稳定。这样显存占用降到18G左右,训练速度反而比fp32快20%。
7. 这套框架还能怎么改:几个我试过的扩展方向
7.1 引入动态异质图结构
PESD-TSF目前处理的是单变量或者通道独立的多元时序。如果变量之间有复杂的依赖关系,比如交通网络里不同路段的相互影响,可以考虑把跨分量注意力扩展成跨变量图注意力。
我试过一个简化版本:把每个变量当成图节点,用变量间的相关性构建邻接矩阵,然后在注意力计算时叠加图结构的偏置。在Traffic数据集上,这个改动带来了额外4%的提升。但代价是参数量增加明显,而且邻接矩阵的构建需要额外先验知识。
7.2 因果自注意力的融合
标准注意力是对称的,但时序数据有明确的因果方向。我尝试过在跨分量注意力里加入因果掩码,让每个位置只能关注它之前的位置。结果在部分数据集上有提升,但在另一些数据集上反而变差。
我的理解是:对于趋势和季节这种慢变量,因果约束有帮助;对于残差这种快变量,因果约束反而限制了模型捕捉突发模式的能力。所以因果掩码可能更适合加在趋势和季节支路上,残差支路保持无掩码。
7.3 多尺度分解的进一步细化
目前的分解是三个尺度,我试过扩展到五个尺度,增加中趋势和中季节两个分量。结果是训练时间增加40%,性能提升不到1%。性价比不高,除非你的数据确实有多个明显不同的周期尺度。
更划算的做法是保持三个尺度,但让分解核的大小可学习。具体是把卷积核参数化成一个可微的软窗口,让模型自己决定每个分量的有效感受野。这个改动我还在实验中,初步结果看起来有潜力。
8. 一些实操层面的经验与建议
8.1 数据量不足时的处理策略
PESD-TSF的参数量不算小,数据量不足时容易过拟合。我的经验是:如果训练样本少于1万条,把d_model降到128,K降到4,注意力头数降到4。同时加大dropout到0.2,weight_decay提到1e-3。
另外可以用预训练加微调的策略。先在相似领域的大数据集上预训练,再在目标数据集上微调。我试过用Electricity预训练、ETTh1微调,收敛速度比从头训练快一倍,最终性能也好2%左右。
8.2 推理阶段的加速技巧
训练完之后推理阶段可以做一些优化。一是把分解核和投影矩阵做融合,减少算子数量。二是用torch.jit.script把模型编译成静态图,推理速度能提升30%左右。三是如果只需要预测固定长度,可以把解码器的动态循环展开成静态结构。
注意推理时不要用dropout,记得调model.eval()。这个听起来是常识,但我确实见过有人忘了切换导致推理结果不稳定。
8.3 超参数搜索的优先级
如果你时间有限,只能调几个超参数,我建议按这个优先级来:分解核大小 > 周期基向量数K > 学习率 > d_model > 注意力头数。
分解核大小对性能影响最大,因为它直接决定了分解的质量。K的影响次之,但调起来最快。学习率用cosine退火基本不用太调。d_model和头数在算力允许范围内取大一点通常不会错。
8.4 结果可复现性的保障
时序预测的可复现性一直是个问题。我的做法是:固定随机种子(torch、numpy、python都设),关闭cudnn的benchmark模式,记录完整的配置文件。另外每个实验至少跑3个种子取平均,单次结果波动可能有1%到2%,不取平均容易得出错误结论。
还有一点,数据划分要用滚动窗口而不是随机划分。随机划分会导致信息泄露,因为相邻时间点高度相关。滚动划分虽然样本少一些,但评估结果更可信。
这套框架我从第一次看到论文到完整复现大概花了三周时间,中间踩的坑基本都写在上面了。如果你也在做长期时序预测,尤其是数据带有明显周期性的场景,PESD-TSF的思路值得借鉴。它的模块设计不算复杂,但每个模块都有明确的动机,这种"每一步都知道为什么"的设计风格,比堆砌花哨模块的工作要扎实得多。