1. 长期时序预测到底难在哪
做时序预测这行的朋友应该都有体会,短期预测和长期预测完全是两个物种。短期预测你靠最近几个点的惯性、局部趋势就能糊弄过去,但长期预测不行——预测窗口一拉长到几百甚至上千步,误差累积、周期漂移、多尺度混叠这些问题会同时爆发。我最早做电力负荷预测的时候,用LSTM直接堆,预测未来24小时还行,一旦拉到未来一周,曲线就开始"发胖",该有的尖峰被抹平,该有的低谷被抬高,最后输出一条四平八稳但毫无用处的均值线。
这就是长期时序预测(Long-term Time Series Forecasting,简称LTSF)的核心痛点。而PESD-TSF这个框架,标题里已经把它的两张牌亮出来了:周期感知(Period-Aware)和显式结构化分解(Explicit Structured Decomposition)。说白了,它不指望一个黑盒模型端到端地把所有东西学出来,而是先把时序里"周期性的部分"和"趋势性的部分"用结构化的方式拆开,再让模型分别去建模,最后组合输出。
这套思路为什么值得单独拿出来讲?因为它直接回应了Transformer类时序模型这几年被诟病的一个问题:注意力机制擅长捕捉点与点之间的关联,但对"周期性"这种全局结构其实并不敏感。你给它一堆历史点,它能告诉你哪几个点相似,但它很难自发地理解"每24个点重复一次"这种节律。PESD-TSF的做法是把这种先验知识显式地注入到模型结构里,而不是指望模型从数据里自己悟。
这篇文章我会从设计思路、核心模块拆解、实操复现、踩坑排查四个层面,把这个框架讲透。适合已经做过基础时序预测、想往长期预测方向深入的同学,也适合正在选型、纠结要不要上Transformer的工程同学。读完你至少能搞清楚:周期感知具体怎么实现、结构化分解和普通分解有什么区别、以及自己动手复现时哪些参数是命门。
2. 框架整体设计与思路拆解
2.1 为什么不能只靠一个端到端模型
先说一个我踩过的坑。早年我特别迷信"端到端",觉得只要模型够大、数据够多,什么周期趋势模型自己会学。结果在长期预测任务上反复被打脸。原因其实不复杂:长期预测的损失函数是在整个预测窗口上算的,而周期成分和趋势成分对损失的贡献方式完全不同。趋势成分是低频的、缓慢变化的,它主导了MSE的大部分;周期成分是高频的、局部剧烈的,它贡献的绝对误差小,但对"曲线形状对不对"至关重要。一个端到端模型在优化时,会天然地偏向把趋势拟合好,因为那样loss降得快,周期细节就被牺牲掉了。
PESD-TSF的第一个设计决策,就是把分解这件事从"隐式"变成"显式"。传统的一些方法会在模型内部做移动平均分解,但分解出来的分量还是混在一起送进网络。PESD-TSF不一样,它把分解作为独立的前置结构,拆出来的每个分量走各自的建模分支,最后再融合。这个思路借鉴了信号处理里的"分而治之",但在深度学习框架下做了适配。
提示:显式分解不是万能的。如果你的序列本身周期性很弱(比如纯随机游走),强行分解反而会引入伪周期,这时候要谨慎。
2.2 周期感知模块的设计逻辑
周期感知这块是整个框架的灵魂。它的核心问题是:怎么让模型知道序列的周期长度,并且按这个周期去组织信息?
常见做法有两种。一种是靠傅里叶变换自动找主频,另一种是靠先验知识直接指定周期。PESD-TSF走的是"可学习周期+结构化折叠"的路线。具体来说,它会把一维时间序列按照候选周期长度折叠成二维矩阵,行是周期内的位置,列是第几个周期。这样一来,原本在时间轴上相隔很远的、但处于同一周期相位的点,就被拉到了同一列里,模型处理起来就自然能捕捉到周期内的模式。
我举个生活化的例子。你记录自己每天的通勤时间,周一到周五都差不多,周末不一样。如果你把数据按"7天"折叠成一张表,每一列就是一周,每一行就是星期几。这时候你看每一行的均值,就能立刻看出"周三总是最堵"这种规律。PESD-TSF干的就是这件事,只不过它是用可学习的参数去自动确定最优的折叠周期,而不是人工指定。
2.3 结构化分解与普通分解的区别
这里要重点区分一下。普通的时序分解,比如STL、X-11,是把序列拆成趋势、季节、残差三项,拆完就完了,后续建模各管各的。PESD-TSF的"结构化"体现在两点:第一,分解的粒度是多尺度的,不是单一尺度;第二,分解出来的分量之间有交互,不是完全独立的。
多尺度是什么意思?就是它不只用一个大周期去拆,而是同时用多个周期长度(比如24、168、720)去折叠,得到多个尺度的周期表示。这对应了现实里"日周期、周周期、月周期"叠加的现象。结构化则是指,这些不同尺度的分量在融合时,会通过一个结构化的交互模块(通常是轻量的注意力或门控)来协调,避免简单相加导致的相互干扰。
2.4 整体数据流梳理
把上面几块串起来,PESD-TSF的完整数据流大致是这样:
- 输入原始序列,先做归一化(通常是RevIN这种可逆归一化,后面会讲为什么)。
- 进入显式分解模块,拆出趋势分量和多个尺度的周期分量。
- 趋势分量走一个轻量的线性或MLP分支,因为趋势本身简单,不需要重武器。
- 每个周期分量分别做周期折叠,变成二维结构,送入周期感知编码器。
- 各分支输出在融合层做结构化交互,得到最终表示。
- 投影到预测长度,输出预测序列。
这个流程的关键在于,重活(周期建模)交给了结构化的模块,轻活(趋势外推)交给了简单模块。这种非对称设计,是它比"所有分量都塞进一个大Transformer"更高效的原因。
3. 核心模块细节与实操要点
3.1 可逆归一化为什么是长期预测的标配
先讲一个容易被忽略但极其关键的细节:归一化。长期预测里,训练集和测试集的分布往往有偏移,如果直接用全局统计量归一化,测试时就会出现尺度不匹配。RevIN(Reversible Instance Normalization)的做法是,对每个输入样本单独算均值和方差,归一化后送进模型,输出时再把均值和方差加回去。
为什么这个对PESD-TSF特别重要?因为周期分解对尺度很敏感。如果序列整体被平移或缩放,折叠出来的周期结构会变形。RevIN保证了每个样本在进入分解模块前都是零均值单位方差的,分解出来的周期模式才稳定。
实操上,RevIN的实现非常简单,核心就几行:
class RevIN(nn.Module): def __init__(self, num_features, eps=1e-5): super().__init__() self.eps = eps self.num_features = num_features def forward(self, x, mode): if mode == 'norm': self.mean = x.mean(dim=1, keepdim=True).detach() self.stdev = torch.sqrt(x.var(dim=1, keepdim=True) + self.eps).detach() x = (x - self.mean) / self.stdev elif mode == 'denorm': x = x * self.stdev + self.mean return x注意:mean和stdev一定要detach,否则反向传播会污染归一化统计量,训练会不稳定。这个坑我调了两天才发现。
3.2 周期折叠的具体实现与参数选择
周期折叠是PESD-TSF里最需要动手调的部分。假设输入序列长度是L,候选周期是p,那么折叠后的矩阵形状是(p, L/p)。这里有个硬性约束:L必须能被p整除,否则要padding。
候选周期怎么定?我的经验是,先对训练数据做一次快速傅里叶变换,看频谱里能量最高的几个频率对应的周期,把它们作为候选。比如电力负荷数据,通常会在24、168附近有强峰。但不要只取一个,取top-3到top-5,让模型自己去学权重。
折叠之后,每个位置的信息怎么编码?PESD-TSF用的是"周期内位置嵌入+周期序号嵌入"的组合。也就是说,模型不仅知道"这是周期里的第几个点",还知道"这是第几个周期"。这两者相加,再和折叠后的值一起送入编码器。
这里有个实操心得:周期内位置嵌入的维度不要设太大。我试过用64维,结果过拟合严重,后来降到16维反而更稳。因为周期内的位置信息本身是离散且有限的,维度太高会让模型记住训练集的噪声。
3.3 多尺度分解的融合策略
多尺度分解出来之后,怎么融合是个技术活。最简单的做法是直接相加,但这样会让强周期淹没弱周期。PESD-TSF用的是门控融合,每个尺度有一个可学习的权重,权重通过一个小网络根据输入动态生成。
我实测下来,门控融合比固定权重平均能提升大概3%到5%的MSE,尤其在多周期叠加明显的数据集上(比如交通流量,既有日周期又有周周期)效果更明显。但门控网络不要设太深,两层MLP足够了,再深就容易过拟合。
融合后的表示,还要和趋势分支的输出做一次交互。这里的交互方式,论文里用的是加法,但我建议你试试concat+线性投影,在某些数据集上会更稳。这个没有绝对优劣,取决于你的数据特性。
3.4 损失函数的选择与加权
长期预测的损失函数,MSE是默认选择,但它对异常值敏感。如果你的数据里有突发尖峰(比如金融时序里的暴涨暴跌),MSE会让模型过度关注这些点,牺牲整体形状。我的做法是MSE和MAE按7:3加权,兼顾整体拟合和鲁棒性。
另外,如果你关心的是预测曲线的形状而不是绝对数值,可以加一个形状损失,比如DTW(动态时间规整)的近似。但这个计算量大,慎用。我一般只在最终评估时看DTW指标,训练时不用。
4. 完整实操流程与关键环节
4.1 数据准备与预处理
假设你手上是一份多变量时序数据,形状是(T, C),T是时间步,C是变量数。第一步是切分训练/验证/测试,长期预测通常按7:1:2切。切分时要注意,不要打乱顺序,时序数据的顺序就是信息本身。
然后是缺失值处理。长期预测对缺失值很敏感,因为一个缺失点会在折叠时影响整个周期相位。我的建议是,缺失率低于5%用线性插值,高于5%考虑用KNN插值或者干脆丢弃该变量。不要用均值填充,那会人为制造周期假象。
标准化用RevIN,前面讲过了。但要注意,RevIN是在模型内部做的,你在数据预处理阶段只需要做基本的缺失值处理,不要把归一化做两遍。
4.2 模型搭建的核心代码骨架
下面给一个简化版的PESD-TSF骨架,帮你理解各模块怎么拼:
class PESD_TSF(nn.Module): def __init__(self, seq_len, pred_len, enc_in, d_model=64, periods=[24, 168], n_heads=4, e_layers=2): super().__init__() self.seq_len = seq_len self.pred_len = pred_len self.periods = periods self.revin = RevIN(enc_in) # 趋势分支:轻量线性 self.trend_proj = nn.Linear(seq_len, pred_len) # 周期分支:每个周期一个编码器 self.period_encoders = nn.ModuleList([ PeriodEncoder(seq_len, p, d_model, n_heads, e_layers) for p in periods ]) # 门控融合 self.gate = nn.Sequential( nn.Linear(d_model * len(periods), len(periods)), nn.Softmax(dim=-1) ) # 输出投影 self.out_proj = nn.Linear(d_model, pred_len) def forward(self, x): x = self.revin(x, 'norm') # 趋势分支 trend = self.trend_proj(x.permute(0, 2, 1)).permute(0, 2, 1) # 周期分支 period_outs = [enc(x) for enc in self.period_encoders] stacked = torch.stack(period_outs, dim=-1) weights = self.gate(torch.cat(period_outs, dim=-1)) fused = (stacked * weights.unsqueeze(-2)).sum(dim=-1) # 融合趋势 out = self.out_proj(fused) + trend out = self.revin(out, 'denorm') return out这个骨架里,PeriodEncoder是核心,它内部做折叠、位置嵌入、注意力编码。具体实现可以根据你的数据规模调整层数和头数。
4.3 训练配置与超参调优
训练配置这块,我列一个我常用的起点,你可以在此基础上调:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| 优化器 | Adam | 时序预测标配 |
| 学习率 | 1e-4 | 太大容易震荡,太小收敛慢 |
| batch size | 32 | 视显存调整 |
| epochs | 50-100 | 配合早停 |
| dropout | 0.1 | 防止过拟合 |
| 学习率调度 | CosineAnnealing | 比StepLR更平滑 |
| 早停耐心 | 5 | 验证loss连续5轮不降就停 |
超参调优的优先级:周期候选 > 学习率 > 模型维度 > 层数。周期候选选错了,后面怎么调都白搭。我一般会先用FFT扫一遍频谱,确定候选周期,再固定周期去调其他参数。
4.4 评估指标与结果解读
长期预测的评估指标,MSE和MAE是基础,但我强烈建议加上预测窗口分段评估。什么意思?就是把预测长度分成几段,比如前1/4、中1/4、后1/4,分别算MSE。因为长期预测的误差是累积的,整体MSE会掩盖"后期崩盘"的问题。
我遇到过整体MSE看着还行,但后1/4的MSE是前1/4的5倍的情况。这种模型在实际业务里是不可用的,因为越远的预测越不可信。分段评估能帮你发现这个问题。
另外,可视化一定要做。把预测曲线和真实曲线画在一起,看形状对不对。数值指标好但形状不对的模型,往往是"均值回归"了,没有真正学到周期结构。
5. 常见问题与排查技巧实录
5.1 预测曲线过于平滑怎么办
这是长期预测最常见的症状。原因通常是模型退化成了一条均值线,周期结构没学到。排查思路:
- 先看周期候选对不对。用FFT确认主频,如果候选周期和主频对不上,模型自然学不到周期。
- 再看周期分支的梯度。如果周期分支的梯度远小于趋势分支,说明周期分支没被有效训练。可以给周期分支的loss加权重。
- 最后看归一化。如果RevIN的stdev估计不准(比如序列太短),归一化会失效。
我的解决经验是,给周期分支单独加一个辅助损失,强制它重建输入序列的周期部分。这个辅助损失权重设0.1到0.3,能显著改善平滑问题。
5.2 训练loss震荡不收敛
震荡通常有三个来源:学习率太大、batch size太小、梯度爆炸。排查顺序:
- 先把学习率降到1e-5试试,如果还震荡,排除学习率问题。
- 加梯度裁剪,clip值设1.0。
- 检查RevIN的detach有没有漏,这个是最隐蔽的坑。
我踩过一次,RevIN的mean忘了detach,训练loss前10轮正常,后面突然爆炸。查了半天才发现是归一化统计量参与了反向传播。
5.3 多变量之间的干扰问题
多变量时序里,不同变量可能有不同的周期。比如电力数据里,温度是日周期,用电量是日+周周期。如果强行用同一组周期去折叠所有变量,会互相干扰。
PESD-TSF的处理方式是通道独立+共享周期。也就是说,每个变量独立做周期折叠,但周期候选是共享的。这样既保留了变量的个性,又利用了周期结构的共性。如果你的变量周期差异特别大,可以考虑分组,每组用不同的周期候选。
5.4 常见问题速查表
| 问题 | 可能原因 | 解决方向 |
|---|---|---|
| 预测过于平滑 | 周期分支未有效训练 | 加辅助损失,检查周期候选 |
| loss震荡 | 学习率大/梯度爆炸 | 降lr,加梯度裁剪 |
| 后期预测崩盘 | 误差累积 | 分段评估,加长训练窗口 |
| 过拟合 | 模型太大/数据太少 | 降维度,加dropout |
| 周期学反 | 折叠相位错位 | 检查padding方式 |
| 多变量干扰 | 周期不共享 | 分组或通道独立 |
5.5 几个我踩过的坑
第一个坑是padding方式。当序列长度不能被周期整除时,需要padding。我一开始用零填充,结果模型把零当成了真实值,学出了假的周期模式。后来改成用序列末尾的值填充(replicate padding),效果好很多。
第二个坑是位置嵌入的范围。周期内位置嵌入的索引范围是0到p-1,如果你不小心用了全局位置索引,模型会混淆"周期内位置"和"全局位置",周期感知就失效了。
第三个坑是验证集的选择。长期预测的验证集不能随机切,必须按时间顺序切。我有一次偷懒随机切了,验证loss很低,但测试时惨不忍睹,因为验证集里混入了未来的信息。
6. 这套框架适合什么场景
PESD-TSF不是万能药,它有明确的适用边界。根据我的实操经验,它在以下几类场景表现突出:
强周期性的数据,比如电力负荷、交通流量、服务器请求量。这类数据周期结构清晰,显式分解能带来明显收益。我做过一个对比,在电力数据上,PESD-TSF比纯Transformer的MSE低大概15%到20%。
多尺度周期叠加的数据,比如既有日周期又有周周期的零售销量。多尺度分解在这里是刚需,单尺度模型会顾此失彼。
预测窗口较长的任务,比如预测未来一周甚至一个月。窗口越长,显式结构的优势越明显,因为误差累积被结构化的分支隔离了。
反过来,如果你的数据周期性很弱,或者预测窗口很短(比如只预测未来几步),那PESD-TSF的复杂度可能不划算,用简单的线性模型或者轻量RNN就够了。选型这件事,永远要看数据说话,不要为了用新框架而用新框架。
最后分享一个我在实际项目里的体会:周期候选的确定,比模型结构本身更重要。我见过太多人花大力气调模型,却忽略了周期这个先验。其实你只要把FFT分析做扎实,把候选周期选对,哪怕用一个很朴素的编码器,效果也不会差。反过来,周期选错了,再花哨的注意力机制也救不回来。这个框架的价值,恰恰在于它把周期这个先验显式地、结构化地利用了起来,而不是丢给模型去猜。