时间序列预训练新思路:ST-MTM分解掩码建模实践指南
刚把 ST-MTMSeasonal-Trend Masked Time-series Modeling这套流程完整跑通的时候我最大的感受不是“效果又涨了几个点”而是“原来掩码预训练可以这么自然地落到时序任务上”。这几年时间序列预训练的说法很多但大多数方案要么是照搬 NLP 的 token masking要么是只做 Transformer 套壳很少从时序数据本身的组成结构入手。ST-MTM 的做法很直接——先把序列拆成季节分量和趋势残差分量再基于分解后的成分做掩码重建。这个思路既不玄学也不依赖额外的外部数据更像是对“时序数据该怎么自监督”这件事的一次正面回答。这篇文章就把我在研究和复现 ST-MTM 过程中的完整理解写出来包括它到底解决了什么问题、分解和掩码是怎么结合的、关键模块怎么实现、超参怎么调、训练和下游任务怎么衔接以及我实际踩过的那些坑。适合已经在做时间序列预测、自监督学习或者刚接触时序基础模型但想找一个靠谱切入点的人。1. 整体设计思路为什么非要把“分解”和“掩码”放一起ST-MTM 最核心的认知是时间序列不是一堆随机 token 的堆叠它内部有明确的结构层次——宏观的趋势、周期性重复的季节模式、以及剔除了这两者之后剩下的残差。这三个成分的统计特性完全不同直接拿原始序列做掩码重建等于逼模型同时学习三种不同尺度的规律任务难度大而且容易让小周期模式被大趋势淹没。1.1 掩码建模到底在让模型学什么掩码时间序列建模的基本流程大家应该不陌生把一段序列的一部分遮盖掉让模型根据剩下可见的部分去预测被遮盖的内容。这个任务看起来简单但它迫使编码器学会两个能力第一从可见 token 中提取上下文依赖第二根据周期性先验去补全缺失区间。前者对应的是局部相关性后者对应的是全局规律。但在原始序列上直接做这件事有个隐蔽的问题。假设你有一段用电量数据整体趋势在缓慢上升同时又带明显的日内周期性。如果模型被要求重建一个被掩盖的晚上 8 点它要同时猜出“这一天整体比上周高一点”和“晚上 8 点通常是个高峰”这两件事。对模型来说这是两个混合在一起的目标损失函数也被这两个不同尺度的误差同时驱动。训练初期梯度方向很容易被幅值更大的趋势项主导季节模式的学习效率就会被拖慢。季节-趋势分解的动机就在这里。把序列拆开之后趋势分量是平滑低变的季节分量是规律波动的残差分量是相对平稳的。每个分量内部的数据分布更单纯掩码重建任务在每个分量上也就更容易学。更重要的是模型可以在不同分量上分别捕捉不同的依赖模式而不是在一锅粥里瞎猜。1.2 分解如何降低掩码重建的难度这里可以用一个生活化的类比。让一个新员工直接去负责整个项目的成本控制他得同时理解人力趋势、材料价格周期和突发风险很容易顾此失彼。但如果先让三个人分别盯趋势、盯周期、盯异常再让 leader 把三份结论合起来每个人任务边界清晰模型收敛自然更快结果也可解释。ST-MTM 把分解作为一种前置结构而不是一个独立的数据处理步骤。这意味着分解模块不仅服务于训练前的预处理它本身可以参与到反向传播中或者至少是固定但明确的特征变换让整个网络从输入开始就沿着“趋势 季节 残差”的路径组织信息。相比让 Transformer 自己隐式地从原始输入里学出季节和趋势这种显式的结构先验要高效得多。1.3 ST-MTM 想要解决的实际问题我理解 ST-MTM 的设计目标有两个层次。短期目标是让掩码预训练阶段的重建损失收敛得更快、更稳学到的表征对下游预测任务更有帮助。长期目标其实是想探索一条不依赖海量数据、不依赖跨领域预训练模型的道路——只靠单条序列自身内部的结构就能训练出一个不错的时序编码器。这在很多真实场景里非常有价值。比如工业传感器数据、零售销售数据这类数据往往没有足够多的相似序列来构造大规模预训练语料但每条序列本身的长度和周期规律是够的。ST-MTM 这类方法让“单序列自监督”成为可能你不需要拿几千条序列做 pre-train一条足够长的历史序列就能把编码器训得像样。2. 核心模块解析分解层、掩码策略与重建目标整个 ST-MTM 的流程可以拆成三大块季节-趋势分解、掩码采样、重建与损失计算。每一块的实现细节都直接影响最终效果不能只按论文默认参数照抄。2.1 季节-趋势分解的三种落地方式在实际落地的时候分解模块大致有三条路线可以选我分别说下优劣。第一种是经典统计分解就是 STL、X13-ARIMA-SEATS 那套。先估计趋势通常用局部回归或移动平均再从原序列里减去趋势得到 detrended 序列最后按周期长度做平均或滤波得到季节分量剩下的就是残差。这个方案最大优势是稳定、可解释不需要训练周期固定。缺点是它是离线操作难以在 batch 训练中动态适配而且对非整数周期和时变季节无能为力。第二种是可微分的移动平均分解。用卷积或池化实现一个平滑趋势提取器让趋势 移动平均(输入)季节 输入 - 趋势。这与 Autoformer 里的 SeriesDecomp 思路一致可以嵌入到网络里端到端训练。它灵活能跟着梯度更新但容易把高频信息误分到残差里需要控制 kernel size 的大小否则趋势会过度拟合噪声。第三种是对趋势和季节分别建模的 learnable 分解。比如用多层感知机直接回归趋势项或者用傅里叶基函数拟合季节项。这种做法的表达能力强但可控性下降调参成本高在小规模数据上容易过拟合。ST-MTM 里比较稳妥的组合是趋势分量用滑动平均提取季节分量通过差分或周期基函数提取残差直接等于输入减两者。具体实现上可以写成def seasonal_trend_decompose(x, kernel_size25): # x: [batch, seq_len, num_vars] padding kernel_size // 2 x_pad F.pad(x, (0, 0, padding, padding), modereplicate) # 趋势滑动平均 trend F.avg_pool1d(x_pad.transpose(1, 2), kernel_sizekernel_size, stride1, padding0).transpose(1, 2) trend trend[:, :x.size(1), :] seasonal x - trend return seasonal, trend这里 kernel_size 不能乱设太小趋势里会混入季节波动太大趋势会延迟响应。一般我会先通过快速傅里叶变换或自相关函数确定主周期然后把 kernel 设为周期长度的 1.5 到 2 倍效果比较稳定。2.2 掩码策略在哪个维度上做掩码更合理掩码策略是 ST-MTM 里最容易被低估的环节。掩码的对象、粒度、比例共同决定了预训练任务的信息瓶颈长什么样。从掩码粒度来说无非三种point masking单点掩码、patch masking块掩码、span masking连续片段掩码。直接对原始时间步做随机点掩码对模型来说太简单了因为相邻时间步的高度相关性让它可以靠插值蒙混过关根本不需要学真正的周期规律。Patch masking 把相邻若干时间步当成一个 token 整体掩掉迫使模型从全局结构上推断缺失块这才是比较合理的任务难度。Span masking 进一步要求模型重建一段连续缺失区域几乎等价于多步预测难度最大也更接近下游预测场景。从作用维度来看多变量序列上还需要决定掩码是否跨变量。同一个时间步把全部变量都盖住模型只能靠纯时间依赖去猜难度太大如果每个 patch 随机盖掉一部分变量保留另一部分变量作为条件模型就能利用变量之间的相关性这通常能学出更丰富的表征。我实际跑下来比较顺手的配置是patch size 取 8如果序列特别长可以取 16patch stride 等于 patch size不重叠把每个 patch 看作一个 token掩码比例控制在 40% 到 60% 之间掩码时按 patch 维度随机采样不按变量维度整体盖死。这个配置下重建难度适中既不会因为信息泄漏导致任务失去意义也不会因为上下文太少导致训练不稳定。2.3 重建目标设计回归原始值还是残差值这是 ST-MTM 设计与普通掩码模型差异最大的地方。因为分解后季节分量和残差分量的尺度完全不同重建目标的选择会直接决定梯度分布是否均衡。如果你选择重建原始序列那么季节分量的掩码重建误差会被趋势分量的幅值主导。尤其是当数据存在明显的长期上行趋势时模型几乎不需要理解季节细节只需要把趋势延续对就能把整体 loss 刷到很低。这样学出来的表征大概率对预测没有真正的帮助。更好的做法是对分解后的分量分别计算重建损失。对季节分量使用它自己的均值和方差做标准化对残差分量也做标准化让两部分的损失贡献可比。如果你想让模型学到更具判别性的表征甚至可以额外要求模型预测被掩码 patch 的季节分量的傅里叶系数而不仅是时域数值。不过这个做法的收益在中小数据集上不太明显我通常只在长序列大实验中才加。最终的损失形式可以写成loss mse(season_pred, season_target) mse(resid_pred, resid_target)其中season_target和resid_target是在掩码位置上的季节分量和残差分量并且各自减掉均值除以标准差。这样设计之后模型的优化目标从“学一个混合分布”变成了“分别学两个相对一致的模式”收敛速度会有肉眼可见的提升。3. 实操过程数据准备、模型搭建与训练调参这一部分我按照一个完整复现流程来写包括数据怎么切、网络怎么搭、训练怎么配以及最后怎么接到下游任务上。3.1 数据预处理先定周期再做归一化数据预处理这一步直接决定分解能否发挥作用。我踩过的最大一个坑是没有先确定周期就凭感觉设了分解窗口结果季节分量里混入了大量噪声掩码重建任务变成了噪声拟合。处理顺序推荐如下先对原始序列做一次快速傅里叶变换找到幅度谱中最显著的频率换算成周期长度。比如采样频率是每小时一个点FFT 峰值出现在频率 0.0417 附近对应周期大概 24 小时这就是日周期。如果同时存在周周期和日周期可以考虑用 MSTL 的思路做多季节分解但这种场景 ST-MTM 的实现复杂度会上一个台阶建议先把单周期跑通。归一化方面如果用全局数据算 mean 和 std 来做归一化在训练和测试之间很容易造成数据泄漏。我习惯的做法是只拿训练集的前一段作为校准窗口计算统计量然后固定住测试和验证都沿用这套统计量保证评估诚实可靠。对长期趋势较强的序列建议在进入模型前先做一步差分或者 log 变换这能明显提升季节分量的平稳性。3.2 网络结构Patch 嵌入 Transformer 编码器 双头重建ST-MTM 的骨架很简单一个标准的 Transformer encoder 就够用没必要上特别复杂的结构。输入先经过一个 patch embedding 层把原始长度 L 映射成 L/P 个 token每个 token 维度是 d_model。可以加一个小的可学习的位置编码但要注意时序数据的位置编码不一定非要采用正弦编码很多场景下可学习位置编码表现更好。编码器按常规配置就行6 层、8 头、d_model 512前馈网络维度设成 2048。层数不必过多因为输入只是单条序列的 patch 序列token 数量有限太深的网络反而过拟合。重建头是两个并行的线性层一个负责预测季节分量 patch 的数值一个负责预测残差分量。如果将来要接分类或回归任务这个双头还可以继续接一个池化层取所有可见 patch 的编码输出的均值池化作为序列表征。建好之后的流程大概是输入原始序列分解模块输出季节分量和残差分量对两个分量分别做 patch 化选择一部分 patch 掩码掉可见 patch 进入编码器双头预测被掩码位置的对应分量最后算两个 MSE 的和。这里的核心依赖是掩码要同时作用在季节分量和残差分量的同一批 patch 位置上否则会破坏两个分量之间的对齐关系。3.3 训练超参数与掩码比例实验训练超参里影响最大的是掩码比例、patch size 和学习率。我给一个我在多个数据集上验证过的起点配置参数推荐值说明patch size8如果序列周期很长可以加大到 16mask ratio0.5我测过 0.4 到 0.6 区间差别不大encoder layers6小数据集用 4 层也能跑d_model512token 数量少时 256 也够learning rate1e-3 到 3e-3配合 warmup 使用batch size128取决于序列长度epochs40早停 patience 设为 5掩码比例这块我做过一个对比实验从 0.3 到 0.8 都试过0.3 的时候预训练 loss 很低但下游预测表现平平0.7 以上预训练变难可下游效果也没有显著提升稳定在 0.5 左右收益最大。这个结果符合预期——掩码太少模型不费力气就能看到足够多的邻居 patch学到的不是深层的周期结构掩码太多可见上下文不足重建本身都变成难题反而顾不上学可迁移的表征。学习率方面建议用带 warmup 的余弦退火。warmup 步数设 5% 到 10% 的总训练步数这能让 Transformer 在训练早期不因为掩码重建任务的剧烈梯度变化而崩溃。3.4 下游任务适配预训练特征如何迁移ST-MTM 预训练出来的编码器有两种下游使用方式。第一种是 linear probing把编码器冻结取输出表征去接一个线性预测头用少量样本训几轮适合快速评估预训练质量。第二种是 full fine-tuning把预训练权重作为初始化整个模型和预测头一起在下游训练集上微调适合追求最终预测精度的场景。在时空序列预测里我比较推荐先把预训练编码器和预测头一起做几轮全量微调再用验证集早停最后再在测试集上评估。这样迁移效果会明显好于直接 linear probing因为预测任务和重建任务之间毕竟有差异全量微调能让上半层特征更贴合任务目标。4. 常见问题与排查技巧这部分我总结几个我复现这类模型时高频遇到的问题基本都不在论文里写属于自己跑出来的经验。4.1 分解窗口怎么选才靠谱最常犯的错就是把 kernel size 和周期长度搞混。如果你用滑动平均提取趋势kernel 太小趋势线会跟着季节一起起伏kernel 太大趋势线反应迟钝在拐点区域会出现很大的分解残差。建议先用 ACF 或 FFT 确定主周期 P然后让 kernel 落在 [P, 2P] 这个区间再根据验证集重建 loss 微调。另外需要注意 padding 的影响我习惯用 replicate 填充而不是 zero padding因为零填充会在边界制造出虚假的大梯度。4.2 掩码重建训练不收敛怎么办如果预训练 loss 出现震荡或者初期下降很慢先检查两点。一是可见 patch 的数量是否足够如果序列长度是 96patch size 8总共也只有 12 个 patch掩码一半后只剩 6 个条件 token信息量太匮乏了。这种情况要么把 patch size 调小要么把序列窗口拉长。二是看残差分量是否包含大量尖峰如果数据的异常值比较多残差项的 MSE 会被个别点主导建议对残差损失用 Huber loss 替代。4.3 多头注意力在不同分量上是否应该共享这是个值得思考的实现细节。既然我们已经把序列分解成季节和残差两个输入分支两个分支是共用同一个 Transformer encoder还是各用各的我实测下来共享 encoder 的效果更好。原因是掩码重建任务的前几层本身需要理解序列的局部上下文这个能力在两个分量上是通用的。共用参数还能减少参数量防止小数据过拟合。如果你实在想分开那至少要保证 d_model 和层数保持一致否则分支间的信息融合会变得很麻烦。4.4 长序列训练显存不足的几个解法面对 512 甚至 1024 以上的长序列ST-MTM 照样会撞上显存瓶颈。梯度累计是最简单的手段把 batch size 降到 32每个 step 累计 4 次再更新等效 batch size 不变。第二个手段是对 attention 做窗口限制也就是局部注意力只在相邻的比如 16 个 patch 内计算注意力这个改动对大多数时序任务几乎没有精度损失。第三个手段是减少 encoder 层数把 6 层降到 4 层通常不会太影响表征质量但训练速度能提升近一半。4.5 季节分解对缺失值的敏感性工业数据经常有缺失点如果直接用插值法填充后再分解这些缺失位置会变成人造异常值季节性分量会在缺失点附近出现奇怪的“凹陷”。我建议先做线性插值然后在分解后得到的残差分量中把这些位置的梯度降权也就是在损失函数里对缺失点附近的残差项乘以一个较小的权重让模型不刻意去拟合这些位置。这个方法简单有效比专门设计复杂缺失值处理模块要划算得多。4.6 下游预测时要不要重新分解一个容易让人困惑的问题是下游 fine-tuning 时是否还需要保留分解模块。我的答案是保留但预测头只接残差分量和解纠缠后的季节特征。因为下游预测的核心目标是未来值未来值 未来趋势 未来季节 未来残差。趋势部分可以让预测头自己外推季节部分由预训练编码器提供模式先验残差部分交给局部注意力去建模。如果完全去掉分解模块模型相当于重新退回原始序列预测预训练阶段学到的结构优势就被浪费了。5. 进一步扩展的方向ST-MTM 这套框架目前还有不少可以继续玩的空间。多周期分解是第一个我想提的现实世界中许多序列不止一个季节周期比如电力数据既有日周期又有周周期。把这个框架扩展成多路分解每路对应一个周期分别做掩码重建理论上能捕获更丰富的时序结构。第二个方向是跨序列预训练目前 ST-MTM 更多是单序列自监督但如果能把同场景下的多条序列拼成一个大的 token 集合让模型同时看到多个样本的 patch这可能是一条通往轻量时序基础模型的路径。第三个方向是跟扩散模型结合既然重建目标已经从原始值换成了分解后的分量那完全可以把季节分量和残差分量的生成交给扩散头让模型先学分布再采样这对随机性较强的序列会更友好。回到 ST-MTM 本身我觉得这个方法的精髓不在于某个模块有多精巧而在于它终于把时间序列自身的结构变成了一种自监督信号。相比堆数据和堆参数这种从数据本身挖掘先验的做法可能才是时序预训练更稳妥的出路。如果你正在考虑给自己的时序项目引入预训练我建议你别急着上超大模型先拿 ST-MTM 这套“分解 掩码”的组合跑一版对比看看在同样数据量下显式结构先验到底能帮你把效果抬多高。这套流程的复现成本很低一两天就能跑通但我认为它带给你的启发远比那零点几个点的指标提升更值得。