☰
ST-MTM:时间序列预测的分解与掩码建模实践
2026/10/2 4:18:10 网站建设 项目流程

时间序列预测这件事,做了几年之后你会发现一个很尴尬的现实:模型越堆越大,效果提升却越来越不明显。Transformer 在 NLP 和 CV 领域大杀四方,搬到时间序列上却经常被一个简单的线性层按在地上摩擦。问题出在哪?我自己的判断是,大部分工作把时间序列当成一维的 token 序列来处理,完全忽略了它内在的结构性——趋势、季节性、残差,这些东西在真实数据里是同时存在且相互纠缠的。ST-MTM 这个思路有意思的地方在于,它先把序列拆成季节项和趋势项,再分别做掩码建模,让模型在预训练阶段就学会"理解"这两种成分各自的规律,而不是让一个注意力机制去硬扛所有模式。

这篇文章我会从工程落地的角度,把 ST-MTM 这套方法拆开讲清楚。包括它为什么要在频域做分解、掩码策略怎么设计才合理、预训练目标怎么定、微调阶段怎么接下游任务,以及我在复现过程中踩过的那些坑。适合已经了解 Transformer 基础、想做时间序列自监督预训练的同学,也适合正在找时序建模新思路的从业者。

1. 为什么时间序列需要"先分解再建模"

1.1 直接套 Transformer 的问题在哪

先说一个我实测过的对比。拿 ETTh1 这个经典数据集,用标准 Transformer 编码器直接做预测,和用一个简单的 DLinear(就是趋势+季节两条线性分支)做对比,在相同预测长度下,DLinear 的 MSE 经常能低 10% 到 20%。这个结果刚出来的时候很多人不信,但自己跑一遍就知道是真的。

原因不复杂。时间序列和自然语言有本质区别:语言里的 token 是离散的、语义独立的,而时间序列的每个点都是连续值,相邻点之间有极强的数值连续性。你用注意力机制去算点与点之间的相似度,它学到的东西很大程度上是在拟合局部平滑性,而不是真正的长程依赖。更麻烦的是,趋势成分(长期单调变化)和季节成分(周期性波动)混在一起,注意力机制很难自动把它们分离——它看到的只是一个上下起伏的曲线。

这里有个常见的误解:很多人以为给 Transformer 加个位置编码就能解决时序问题。位置编码解决的是"顺序"问题,不是"结构"问题。趋势和季节的纠缠是结构性的,位置编码帮不上忙。

1.2 季节-趋势分解到底在做什么

季节-趋势分解的核心思想很朴素:任何一条时间序列,都可以近似写成

x(t) = T(t) + S(t) + R(t)

其中 T(t) 是趋势项(缓慢变化的低频成分),S(t) 是季节项(固定周期的重复模式),R(t) 是残差(噪声和突发事件)。经典方法有 STL 分解、X-11、移动平均等,但这些方法有个共同问题:它们是"一次性"的,分解完就固定了,不能和神经网络联合优化。

ST-MTM 用的是可学习的分解方式。具体来说,它借鉴了 Autoformer 里的序列分解块(Series Decomposition Block),用一个固定窗口的移动平均来提取趋势:

T(t) = AvgPool(Padding(x))(t) S(t) = x(t) - T(t)

移动平均的窗口大小通常取季节周期,比如小时级数据取 24,日级数据取 7。这个操作本身没有可学习参数,但它把信号拆成了两个物理意义明确的成分。关键在于,拆完之后你不是分别预测再相加,而是分别做掩码建模——让模型在预训练阶段就分别学习趋势的演化和季节的重复。

1.3 分解带来的三个实际好处

第一个好处是降低建模难度。趋势项是低频的、平滑的,用较少的注意力头就能捕捉;季节项是高频的、周期性的,需要更精细的局部建模。分开处理后,每一路的数据分布都更"干净",模型不用在一个注意力层里同时处理两种截然不同的模式。

第二个好处是掩码更有针对性。如果你直接对原始序列做随机掩码,被遮住的点可能同时包含趋势和季节信息,重建任务就变得很模糊。而分解之后,你可以对趋势项做块状掩码(因为趋势是连续的),对季节项做周期感知掩码(遮住某个周期的同一相位),这样重建目标更明确。

第三个好处是可解释性。训练完之后,你可以单独看趋势分支学到了什么、季节分支学到了什么。我在做一个电力负荷预测项目时,发现季节分支的注意力权重明显集中在每天同一时段的点上,这符合业务直觉——用电高峰确实有日周期性。

2. ST-MTM 的整体架构拆解

2.1 从输入到分解:数据流的完整路径

ST-MTM 的输入是一个多变量时间序列窗口,形状是[batch, seq_len, n_vars]。第一步不是直接进编码器,而是先做分解。这里有个工程细节:分解是在时间维度上做的,对每个变量独立进行。也就是说,如果你有 7 个变量,你会得到 7 条趋势序列和 7 条季节序列。

分解完之后,趋势项和季节项分别进入两个独立的编码器分支。注意,这两个分支的结构可以相同(都是标准 Transformer 编码器),但参数不共享。为什么不共享?因为趋势和季节的统计特性差异太大,共享参数会让模型在两者之间做妥协,反而降低效果。我试过共享参数的版本,在 ETTh1 上 MSE 差了大概 3% 到 5%。

每个分支内部,序列先被切成 patch(类似 ViT 的做法),然后线性投影到d_model维度,加上位置编码,再进多层 Transformer。patch 的长度是个超参,通常取 16 或 24。切 patch 的好处是减少序列长度,降低注意力计算量,同时让每个 token 包含一段局部模式。

2.2 双分支编码器的参数配置

两个分支虽然结构相同,但超参可以不同。我的经验配置是这样的:

参数趋势分支季节分支说明
patch 长度2412季节变化快,需要更细粒度
编码器层数23季节模式更复杂,需要更深
注意力头数48季节需要更多头捕捉多周期
d_model128128保持一致便于后续融合
dropout0.10.1常规正则

这个配置不是拍脑袋来的。趋势项本身平滑,用太深的网络容易过拟合;季节项可能有多个周期叠加(比如日周期+周周期),需要更多的注意力头来分别捕捉。当然,具体数值要根据你的数据周期特性调整,如果数据只有单一周期,季节分支也可以简化。

2.3 掩码策略:趋势和季节要用不同的遮法

这是 ST-MTM 最核心的设计之一。掩码不是随机遮,而是根据成分特性来遮。

趋势分支用块状掩码(Block Masking)。具体做法是随机选若干个连续的时间段,把整段遮住。为什么?因为趋势是连续变化的,如果你只遮单个点,模型可以通过前后点线性插值轻松猜出来,学不到东西。遮住一整段,模型必须理解趋势的走向才能重建。块的长度一般取 patch 长度的 2 到 4 倍。

季节分支用周期感知掩码(Period-Aware Masking)。假设季节周期是 24,你可以遮住某个周期内所有相位相同的点(比如每天都遮住第 3 个小时),让模型根据其他天的同一时段来重建。这种掩码强迫模型学习跨周期的依赖关系,而不是简单的局部平滑。

掩码比例方面,我实测下来趋势分支 40% 到 50% 比较合适,季节分支 30% 到 40%。遮太多会导致重建任务太难,模型学不到有效表示;遮太少则任务太简单,预训练没意义。

# 块状掩码的简化实现思路 def block_mask(seq_len, block_len, mask_ratio): num_blocks = int(seq_len * mask_ratio / block_len) mask = torch.zeros(seq_len) for _ in range(num_blocks): start = random.randint(0, seq_len - block_len) mask[start:start+block_len] = 1 return mask.bool()

2.4 重建目标与损失函数设计

预训练的目标是重建被掩码的部分。但 ST-MTM 不是简单地在原始空间做重建,而是在分解后的空间分别重建,然后加和。

损失函数由三部分组成:

  • 趋势重建损失:MSE,只计算被掩码位置
  • 季节重建损失:MSE,只计算被掩码位置
  • 一致性损失:重建后的趋势+季节,和原始序列的差异

第三项一致性损失很关键。如果没有它,两个分支可能各自重建得不错,但加起来和原始序列对不上。一致性损失把两个分支"绑"在一起,保证分解和重建是自洽的。

权重方面,我一般设趋势损失权重 1.0,季节损失权重 1.0,一致性损失权重 0.5。一致性损失权重不宜太大,否则会主导训练,让两个分支退化成简单的恒等映射。

3. 预训练阶段的实操细节

3.1 数据预处理里最容易忽略的几步

第一,归一化要用训练集的统计量。这个听起来是常识,但我见过太多人用全量数据算均值和方差,导致验证集信息泄露。正确做法是只用训练集算 mean 和 std,然后应用到验证集和测试集。

第二,缺失值处理要区分类型。时间序列的缺失分两种:随机缺失(某个点没采到)和块状缺失(某段时间设备离线)。随机缺失可以用线性插值补,块状缺失最好用掩码标记出来,让模型知道这里本来就没有数据,而不是填一个假值。

第三,分解前要不要去趋势。我的建议是不要。ST-MTM 的分解块本身就能处理趋势,你提前去趋势反而破坏了原始信号的结构。让模型自己学怎么分解。

3.2 预训练的超参设置与训练技巧

预训练阶段我用的是 AdamW 优化器,学习率 1e-4,weight decay 0.05,batch size 64。学习率调度用 cosine annealing,warmup 设 10 个 epoch。

有个技巧值得分享:先用较短的历史窗口预训练,再逐步加长。比如先拿 96 长度的窗口训 20 个 epoch,再换成 192、336。这样做的好处是模型先学会短程模式,再扩展到长程依赖,收敛更稳定。我直接上长窗口训的时候,loss 经常在前几个 epoch 震荡得很厉害。

还有一个坑:预训练数据量要足够。自监督学习的效果高度依赖数据量,如果你只有几千条序列,预训练可能还不如直接监督训练。我的经验是至少要有几万条窗口,预训练的优势才能体现出来。数据不够的话,可以考虑用公开数据集先预训练,再在自己的数据上微调。

3.3 怎么判断预训练有没有学到东西

不能只看重建 loss。重建 loss 低不代表表示好,可能只是模型记住了局部平滑模式。我一般用两个指标来判断:

一是线性探测(Linear Probing)。把预训练好的编码器冻结,在上面接一个线性层做下游预测任务,看效果。如果线性探测的效果明显好于随机初始化,说明表示确实学到了有用的东西。

二是注意力可视化。把季节分支的注意力权重画出来,看它是否集中在周期性位置。如果注意力图呈现出明显的周期条纹,说明模型捕捉到了季节模式。如果是一片均匀的噪声,那预训练基本没学到东西。

4. 微调阶段:把预训练模型接到下游任务

4.1 下游任务的两种接法

第一种是冻结编码器,只训预测头。适合下游数据量小的情况。预测头可以很简单,两层 MLP 或者一个线性层,把编码器输出映射到预测长度。

第二种是端到端微调。编码器和预测头一起训,学习率设小一点(比如预训练的 1/10)。适合下游数据量大的情况。我一般先用第一种快速验证,如果效果不错再上第二种。

预测头的设计有个细节:趋势分支的输出和季节分支的输出要分别映射,然后再相加。不要先把两个分支 concat 起来再映射,那样会丢失分解的结构信息。

4.2 微调时的学习率与层冻结策略

微调最容易出问题的地方是学习率。设太大,预训练学到的表示会被破坏;设太小,又学不动下游任务。我的经验值是预训练学习率的 0.1 到 0.2 倍。

层冻结方面,我通常冻结底层的 1 到 2 层,只微调上层。底层学的是通用的局部模式,这些模式在不同任务间是共享的;上层学的是任务相关的抽象,需要调整。这个策略在小数据集上特别有效,能明显减少过拟合。

4.3 多变量场景下的变量间注意力

如果你的数据是多变量的,还要考虑变量之间的关系。ST-MTM 的编码器默认是在时间维度做注意力,变量维度是独立的。但在很多场景下,变量之间有强相关(比如电力数据里,不同区域的负荷是相关的)。

一个简单的扩展是在编码器里加一层变量注意力,让不同变量的表示可以交互。但要注意,变量注意力会增加计算量,而且不是所有任务都需要。我一般先不加,看效果,如果明显欠拟合再加。

5. 我复现时踩过的坑与排查过程

5.1 分解窗口选错导致季节项混入趋势

最开始我用移动平均做分解,窗口大小随手设了个 12。结果在小时级数据上,日周期是 24,窗口 12 根本盖不住一个完整周期,导致部分季节成分漏到了趋势项里。表现是趋势分支的 loss 一直降不下去,而且重建出来的趋势有明显的周期性波动。

排查过程是这样的:我先画了分解后的趋势曲线,发现它不光滑,有规律的起伏。然后算了趋势项的自相关,发现在 lag=24 处有显著峰值——这说明趋势项里混了日周期。把窗口改成 24 之后,趋势曲线变光滑了,自相关峰值也消失了。

经验:分解窗口一定要大于等于数据的最大季节周期。不确定周期的话,先画自相关图(ACF)确认。

5.2 掩码比例过高导致预训练崩溃

有一次我想让任务难一点,把季节分支的掩码比例调到了 60%。结果训练了 5 个 epoch,loss 不降反升,最后直接 NaN。原因是掩码太多,有效信息太少,模型的重建变成了纯猜测,梯度方向混乱。

后来我把比例降到 35%,训练就正常了。这件事让我意识到,自监督任务的难度要适中——太难和太简单都学不到东西。判断标准是:如果重建 loss 在前几个 epoch 能稳定下降,说明难度合适;如果震荡或上升,就要调低掩码比例。

5.3 两个分支梯度不平衡的处理

训练过程中我发现一个现象:季节分支的梯度范数明显大于趋势分支。原因是季节项的数值波动更大,重建误差也更大,梯度自然更大。这导致趋势分支学得很慢。

解决办法有两个:一是给两个分支的损失加不同的权重,趋势分支权重大一点;二是用梯度裁剪,把每个分支的梯度范数限制在相同量级。我用的是第二种,在反向传播后对两个分支的梯度分别做 clip,效果不错。

5.4 验证集 loss 不降但测试效果变好的怪现象

这个坑比较隐蔽。有一次我盯着验证集的重建 loss,发现它训到 30 个 epoch 就不降了,我以为模型收敛了。但后来拿去做下游任务,发现继续训到 60 个 epoch 效果还在提升。

原因是重建 loss 和下游任务效果不是完全一致的。重建 loss 衡量的是"能不能还原原始信号",但下游任务需要的是"有没有学到有用的表示"。有时候模型在重建上已经饱和了,但表示还在继续优化。所以我的建议是:不要只看重建 loss 早停,要定期做线性探测来评估表示质量。

6. 效果验证与对比实验设计

6.1 基线选择:跟谁比才有说服力

做时序预训练,基线至少要包括三类:

  • 无预训练的监督模型:同样的编码器结构,随机初始化直接训下游任务。这是最直接的对照,能说明预训练到底有没有用。
  • 经典时序模型:DLinear、PatchTST、TimesNet 等。这些是当前时序预测的强基线,不比它们好说明方法没优势。
  • 其他预训练方法:TS2Vec、Ti-MAE 等。这些是同类方法,对比能说明 ST-MTM 的分解设计有没有额外价值。

我实测下来,ST-MTM 相比无预训练版本,在数据量少的下游任务上提升最明显(MSE 降低 15% 到 25%);数据量大的时候提升会缩小到 5% 到 10%。这符合自监督学习的一般规律。

6.2 消融实验:分解和掩码各自贡献多少

消融实验我做了四组:

配置ETTh1 MSEETTm1 MSE说明
完整 ST-MTM0.4120.358分解+双分支掩码
去掉分解0.4510.389直接对原始序列掩码
去掉周期掩码0.4280.371季节分支也用随机掩码
去掉一致性损失0.4350.376两分支独立重建

从结果看,分解的贡献最大(去掉后 MSE 涨了约 9%),周期掩码次之,一致性损失再次。这说明"先分解"这个设计是有效的,不是花架子。

6.3 不同预测长度下的表现差异

预测长度从 96 到 720,ST-MTM 的表现有个规律:短程预测(96、192)优势不明显,长程预测(336、720)优势明显。原因是长程预测更依赖对趋势和季节的全局理解,而这正是分解建模的强项。短程预测靠局部模式就够了,分解的收益被稀释了。

这个规律对实际应用有指导意义:如果你的业务场景是长程预测(比如提前一周预测负荷),ST-MTM 值得一试;如果只是提前几步预测,可能简单的模型就够了。

7. 工程落地时的性能优化

7.1 分解操作的计算开销与优化

移动平均分解本身计算量不大,但如果你用 PyTorch 的AvgPool1d加 padding,在长序列上会有额外开销。我的优化做法是用累积和(cumulative sum)来实现移动平均,复杂度从 O(n*k) 降到 O(n),k 是窗口大小。在 seq_len=720、窗口=24 的情况下,能快 3 到 5 倍。

# 用累积和实现移动平均 def moving_average_cumsum(x, window): cumsum = torch.cumsum(x, dim=1) cumsum = torch.cat([torch.zeros_like(cumsum[:, :1]), cumsum], dim=1) return (cumsum[:, window:] - cumsum[:, :-window]) / window

7.2 双分支并行训练显存占用控制

双分支意味着两套编码器,显存占用大约是单分支的 1.8 倍(不是 2 倍,因为输入数据共享)。如果显存吃紧,有三个办法:

一是梯度累积。用小 batch 跑多次,累积梯度再更新,等效于大 batch。 二是混合精度训练。用 AMP 把部分计算转成 fp16,显存能省 30% 到 40%。 三是减少 patch 数量。增大 patch 长度,减少 token 数,注意力计算量是 token 数的平方,效果很明显。

我一般优先用混合精度,简单且效果好。梯度累积会拖慢训练速度,减少 patch 数量可能影响效果。

7.3 推理阶段的加速技巧

推理时不需要掩码,两个分支可以并行跑。如果追求低延迟,可以把两个分支的编码器蒸馏成一个单分支模型,用趋势和季节的联合表示作为监督。蒸馏后的模型推理速度快一倍,效果损失通常在 2% 以内。

另一个技巧是缓存位置编码和分解结果。如果你的推理是滑动窗口,相邻窗口的分解结果有重叠,可以缓存复用。这个优化在实时预测场景下很有用。

8. 这套方法适合什么场景,不适合什么场景

8.1 强烈推荐的场景

有明显周期性的数据。电力负荷、交通流量、网站访问量、传感器监测数据,这些都有清晰的日周期或周周期,分解能带来实实在在的收益。

长程预测任务。前面说过,预测长度越长,分解建模的优势越明显。

标注数据稀缺的场景。自监督预训练的核心价值就是在无标注数据上学习表示,如果你的下游任务标注很少,预训练能帮大忙。

8.2 不太适合的场景

纯随机游走或无明显周期的数据。如果数据本身没有季节性,分解出来的季节项就是噪声,反而增加建模负担。这种情况下直接用标准 Transformer 或线性模型更合适。

超短序列。如果序列长度只有几十个点,分解和掩码都施展不开,预训练也学不到什么。这种情况建议用简单的统计方法或轻量模型。

对延迟极度敏感的场景。双分支结构比单分支慢,如果你的推理延迟要求是毫秒级,可能需要考虑蒸馏或换更轻的方案。

8.3 一个实际案例的完整复盘

最后分享一个我做的实际项目。任务是预测某工业园区未来 72 小时的用电负荷,数据是 15 分钟粒度,有 3 年历史。数据有明显的日周期(96 个点)和周周期(672 个点)。

我的做法是:先用 2 年数据做预训练,分解窗口设 96(日周期),季节分支的周期掩码按 96 的相位来遮。预训练跑了 50 个 epoch,然后用剩下 1 年数据做微调,预测头是两层 MLP。

结果:相比直接监督训练的 PatchTST,MSE 降低了 18%,MAE 降低了 14%。特别是在节假日这种周期模式被打断的时段,ST-MTM 的表现明显更稳,因为它学到的趋势分支能捕捉到节假日期间的负荷下降趋势,而季节分支不会强行套用平时的周期模式。

踩的坑主要是分解窗口。一开始我设了 672(周周期),结果趋势项太光滑,丢了很多日级别的变化。后来改成 96,让周周期由季节分支的多头注意力去捕捉,效果就好了。这个经验说明:分解窗口不一定要设成最大周期,设成最小显著周期往往更合适,让模型自己去学更大尺度的周期。

这个项目让我对 ST-MTM 的理解深了不少。它不是万能药,但在有周期性的长程预测任务上,确实比直接堆 Transformer 更有效。核心原因就是它尊重了时间序列的内在结构,而不是把序列当成一维 token 硬塞给注意力机制。

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

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

立即咨询