Autoformer解析:用自相关与序列分解攻克长序列预测难题 Autoformer这个模型我前前后后啃了三遍才算真正看懂。第一次看名字以为是普通的Transformer变体第二次看代码才发现它把注意力机制换成了自相关还把序列分解直接搬进了深度模型。这篇学习日记33想把我在这段时间里对Autoformer的理解、复现踩坑和实际使用经验一次性说清楚。如果你也在做长序列预测或者想搞明白为什么Autoformer在长时间序列问题上比标准Transformer稳那这篇内容应该能帮你省不少时间。我会从解决什么问题讲起拆核心机制再给一份能直接参考的PyTorch实现思路和训练全流程。1. 学Autoformer之前先搞懂它要解决什么问题1.1 时间序列预测里最头疼的两个老问题我在做某电力负荷预测项目时第一版直接用标准Transformer做长序列预测结果非常不理想。预测长度稍微拉长到48步之后输出曲线几乎变成一条平线峰值全部被磨平连基本的日周期都保持不住。当时我一度以为是数据预处理的问题调了很久才发现问题出在模型本身对时间序列的结构特征不敏感。标准Transformer擅长捕捉任意两个位置之间的依赖关系但这种依赖是通过点积计算出来的本质上是“值匹配”不是“模式匹配”。对于文本这种特性很合适因为词与词之间的相关性确实可以通过上下文语义来度量。但对于时间序列相邻位置的差异很小点积注意力很容易被噪声主导导致模型只在局部拼凑信息无法提取出真正影响长期趋势的周期性成分。另一个问题是长期预测stability。序列越长误差不断累积模型越倾向于输出均值附近的值也就是常说的“过平滑”和“预测滞后”。我在实验里看到真实曲线已经出现上升拐点时模型预测还在缓慢下降。这说明Transformer根本没有建立起“趋势外推”的能力只是在做某种程度上的相似性搜索。1.2 Autoformer到底改了什么Autoformer是这一类时序Transformer模型里相当有代表性的一种思路。它不搞复杂堆叠是在标准Transformer骨架上做了三个关键改动。第一引入了序列分解模块。它把输入序列拆成趋势项和季节项分别建模。趋势项反映缓慢变化的方向季节项反映周期重复的波动。这个操作看起来简单但在深度模型里很有效相当于直接告诉模型你要分开学长期运动和短期波动别混在一起。第二把标准注意力机制换成了自相关机制。这是Autoformer最核心的创新。自相关计算的是同一序列在不同时间延迟下的相似度某个序列和延迟k后的自己有多像。这样模型能找到周期性很强的“子序列对”直接聚合多个周期的信息而不是两两位置之间盲目匹配。第三在复杂度上做了优化。自相关机制通过快速傅里叶变换来计算复杂度从标准注意力的O(L^2)降到了O(L log L)。这个对长序列来说极其重要因为输入长度翻倍时标准Transformer的显存和计算时间几乎是四倍增长Autoformer只有线性加对数的增长幅度。这三个改动合在一起才让模型真正适合长期时间序列预测而不是简单套用NLP的骨架。1.3 适合谁学、学完能得到什么这篇内容适合正在做时间序列预测、想复现前沿模型或者已经被标准Transformer在时序任务上的表现搞到怀疑人生的人。你不需要完全理解傅里叶变换的数学原理但至少要知道Transformer的基本结构编码器、解码器、注意力、前馈层这些概念。学完Autoformer你能获得的不只是一个模型的复现能力更重要的是理解一种设计思路如何把一个任务的结构性先验塞进模型架构而不是靠数据硬学。序列分解和自相关机制这两个思想后来也被很多模型借鉴比如把分解扩展成多尺度分解、把自相关换成别的周期提取方式等。搞懂Autoformer再去看近年来的时序模型会发现很多概念都串起来了。2. 核心机制拆解序列分解和自相关到底在干什么2.1 序列分解把趋势项和季节项拆开序列分解本身不是新概念传统时间序列分析里早就有STL、X13等方法。Autoformer做的事情是在神经网络内部嵌入一个可导的分解算子每一层都能做分解。具体的做法很简单趋势项用移动平均得到然后从原序列里减掉趋势项剩下的就是季节项。我在代码里看到的实现是这样对输入序列做一维平均池化池化窗口大小是一个可配置的kernel默认常常用奇数比如3、5、7也可以设为数据周期长度。池化之后再用padding把长度对齐保证输入输出长度一致。公式可以理解成趋势项 对输入序列做移动平均 季节项 输入序列 - 趋势项这个分解在每一层都会做而不只是在输入那里做一次。原因在于随着网络加深不同层可能捕捉到不同尺度的趋势。比如浅层可能分离出短期波动深层可能分离出更缓慢的长期趋势。训练过程中这些分解模块会学习到该保留什么样的基础趋势。实际使用中这个分解有一个不容易注意到的坑移动平均的窗口太小趋势项还残留大量噪声模型学起来困难窗口太大趋势项变得过度平滑季节项里会混入低频成分导致预测结果滞后。我在自己的数据上对比过窗口取周期一半左右通常比较稳但最好还是做一次超参数搜索。2.2 自相关机制从周期里找依赖自相关机制是Autoformer替换标准自注意力的核心模块。要理解它可以先想一个问题时间序列里今天的数据和哪一天的数据最像如果一个序列有明显的日周期那么上午9点的数据很可能跟昨天上午9点、前天上午9点的数据高度相关。这种相关不是由语义相似度决定的而是由时间间隔决定的。自相关机制做的事情就是计算一个序列和它自身延迟k个位置后的序列之间的相关性。相关性高说明k这个延迟长度对应着一种周期性模式。模型会提取出多个高相关的延迟值然后把对应位置的子序列聚合起来得到输出。计算方式上直接算所有延迟的自相关需要O(L^2)但可以借助傅里叶变换来加速。先把序列做FFT变成频域表示再在频域里做共轭相乘最后做逆傅里叶变换回到时域就得到了自相关序列。整个过程都是可微的可以正常反向传播。在选择依赖时只取自相关值最高的几个延迟而不是全部这样既能保持特征丰富性又控制了计算量。我对这个机制的感受是它比标准注意力更符合时间序列的直觉。标准注意力注意力权重是由query和key的语义相似度决定而时间序列不是靠语义相关的是靠模式重复相关的。自相关直接把“周期性”这个先验编码进模型效果自然更好。2.3 为什么比标准自注意力更适合长序列可以从三个角度理解。第一个角度是复杂度。我跑了两个对比实验输入长度256时两者差别不大输入长度1024时标准Transformer单次训练迭代时间和显存都让我被迫缩小batchAutoformer依然很平稳。复杂度O(L log L)的优势在长序列上会越来越明显。第二个角度是归纳偏置。时间序列的显著特点是规律性重复日周期、周周期、季节周期。自相关机制明确面向这种规律性在多个延迟上寻找周期片段并聚合。标准注意力没有这个偏置只能在大量时间步里靠数据自己找关系结果经常被局部噪声带偏。第三个角度是鲁棒性。自相关聚合的是以某个延迟为中心的子序列片段相当于多个周期的平均这天然抑制了单个周期的异常波动。而标准注意力很容易把某个时刻的异常值赋予过高的权重。我并不是说标准注意力一无是处对于没有明显周期性的序列或者需要依赖上下文语义的任务标准注意力可能更合适。但在长周期、有重复模式的时间序列上Autoformer的自相关机制确实有结构性优势。3. 源码级理解从模型结构到关键模块实现3.1 整体结构看一遍我在复现时参考的是Autoformer开源实现的整体思路结构可以简化成这样一个流程。输入序列 → Encoder: 每个编码层 序列分解 → 自相关模块 → 分解输出 前馈网络 → 再分解 残差连接与归一化 保留分解出的季节项 长期序列用于Decoder的起始输入→ Decoder: 每个解码层 交叉自相关连接Encoder输出 序列分解 → 自相关模块 前馈网络 → 再分解 累加所有分解出的趋势项作为最终趋势预测第一眼看着不像标准Transformer因为每一层都在做分解整个模型不断把“趋势信息”剥离出来、单独累积最后原始输入序列经过多层之后只剩下纯季节或者随机成分。最后预测结果是“累积趋势项 最后一层输出”这样就把趋势外推和季节预测拼接起来了。我自己在搭建时把Encoder和Decoder各设了两层隐藏维度设为512效果和特征维度256时差别不大但训练速度快了不少。如果你的数据量不算太大我建议不要一上来就上大模型Autoformer本身的结构已经带有很强的先验小模型往往就够用。3.2 SeriesDecomp模块的实现细节这个模块代码很简短核心就是一个1D平均池化加一个padding操作。我把代码写出来注释都放在里面。import torch import torch.nn as nn class SeriesDecomp(nn.Module): 把序列拆成趋势项和季节项。 趋势项 移动平均后的结果 季节项 原序列 - 趋势项 def __init__(self, kernel_size): super().__init__() # 用平均池化做移动平均 self.pool nn.AvgPool1d(kernel_sizekernel_size, stride1, padding0) # 确保池化后长度不变化 self.padding (kernel_size - 1) // 2 def forward(self, x): # x: [Batch, Length, Channels] 或者 [Batch, Channels, Length] # 这里假设输入是 [Batch, Length, Channels] x x.transpose(1, 2) # 变成 [Batch, Channels, Length] trend self.pool(x) # 手动补pad trend nn.functional.pad( trend, (self.padding, self.padding), modereplicate ) x x.transpose(1, 2) trend trend.transpose(1, 2) seasonal x - trend return seasonal, trend这里有个容易被忽略的细节padding方式不要用默认的0填充不然序列首尾趋势会有一个骤降造成边界失真。我一般用replicate填充让边界值延续下去。这个细节对预测结果的影响在短序列上特别明显我踩过这个坑。3.3 AutoCorrelation模块的实现细节自相关模块比SeriesDecomp复杂得多但核心逻辑可以拆成三步计算自相关、挑选top-k延迟、按延迟对齐并聚合。import torch import torch.nn as nn def time_delay_correlation(query, key): 通过FFT计算query和key的自相关序列。 query, key shape: [Batch, heads, Length, d_model] 返回: correlation value and index B, H, L, _ query.shape # 在最后一维做FFT转为频域 q_fft torch.fft.rfft(query, dim2) k_fft torch.fft.rfft(key, dim2) # 频域点乘相当于时域相关 s torch.conj(q_fft) * k_fft # 逆变换回时域得到相关结果 corr torch.fft.irfft(s, nL, dim2) return corr下面的代码是核心聚合逻辑按照延迟对齐后取平均再经过一个稀疏注意力加权的做法。这只是简化版说明思路不是完整原版。def auto_correlation_aggregate(corr, value, top_k): B, H, L, _ value.shape # 找出自相关最大的k个延迟 top_k min(top_k, L) top_corr, top_idx torch.topk(corr, top_k, dim2) # 对延迟做softmax得到权重让模型自己学习聚合比例 weights torch.softmax(top_corr, dim-1) # 对齐后加权求和 res torch.zeros_like(value) for i in range(top_k): delay top_idx[..., i].unsqueeze(-1) # [B, H, L, 1] # 把value按延迟roll再乘权重 rolled torch.roll(value, shifts-delay.squeeze(-1)[0].item(), dims2) # 上面的roll写法只是单步说明实际要按每个位置逐delay处理 res res weights[..., i:i1] * rolled return res上面这段代码里roll用的方式其实不够严谨因为每个位置的delay不同真实实现要逐位置、逐batch、逐head处理通常会用其他方式避免循环。但它把“按周期对齐并加权融合”这个意思表达出来了不直接对query和key做点积而是把周期性对齐后的value片段加权求和。我自己复现的时候并没有从头写完整版AutoCorrelation而是直接参考开源实现改参数。如果你也是以应用为主我建议不要死磕数学细节先跑通代码再反推公式理解速度会快很多。3.4 训练配置与损失函数选择训练Autoformer我常用的配置是优化器Adam学习率1e-4batch size 32损失函数用MSE。数据量如果不大可以做early stopping看验证集上的MSE何时不再下降就停下来。损失函数这里有一个值得讨论的点。MSE对异常值比较敏感会让模型变得更保守。如果你的序列波动大、有尖峰可以试试MSE和MAE的组合损失比如MSE 0.5 * MAE这样既保证预测值尽量贴近真实值又不至于被尖峰逼得太保守。但需要自己多实验我的经验是平稳序列用MSE就足够剧烈波动的用电负荷、交通流数据用混合损失会稍微平滑一些。还有一个很关键的细节Autoformer不是一次性输出所有预测步。解码器的输入包含部分真实序列和预测序列的拼接这一点在训练时和推理时不同。训练时要避免把真实未来值传入解码器造成数据泄露这一点我在下一节细讲。4. 实操记录用Autoformer跑一个长序列预测的完整流程4.1 环境与数据准备我推荐以下环境组合实测稳定Python 3.9 / 3.10PyTorch 2.0及以上numpy、pandas如果想要可视化加matplotlib我用的数据是某公开的电力负荷数据集时间范围大概一年采样间隔为1小时字段包括负荷值、温度、湿度、是否节假日等。做实验时我只用了负荷值单变量序列因为Autoformer可以单变量预测多变量特征只是在输入维度上叠加而已。数据准备时最大的问题是缺失值和异常值。我遇到过一个连续48小时的零值区间明显是采集设备故障如果不处理模型会把零值当成正常模式预测结果会周期性出现超低值。我的处理方式是用前后24小时的中位数做插值再用一个简单的规则把超过正常范围三倍方差的点标成异常并拉回边界。时间序列模型对异常值非常敏感预处理比调模型参数更重要。4.2 数据预处理与窗口构造我采用的窗口构造方式用过去96小时预测未来48小时也就是输入长度96预测长度48。实践证明这个配置在电力负荷数据上比较自然因为负荷有明显的日内周期预测未来两天需要至少四天的历史信息。缩放选择上我做的是z-score归一化。要注意的是归一化参数只能在训练集上计算然后用训练集的均值和标准差去转换验证集和测试集绝对不能用全量数据一起计算。最开始我没注意直接对全量数据做归一化结果测试集的MSE比实际应该的低很多属于典型的数据泄露。窗口构造用了一个滑动窗口函数为了避免未来信息混入训练样本每一步只使用当前时刻之前的数据。我还特意排查过确保训练集和验证集之间没有窗口重叠否则验证集评估会虚高。这个很多人都会忽略但真的影响模型选型的判断。4.3 训练脚本与参数设置我直接把训练循环的骨架贴在下面方便你照着自己改。model Autoformer( seq_len96, label_len48, # decoder起始部分长度 pred_len48, enc_in1, # 输入特征维数 dec_in1, d_model256, n_heads8, e_layers2, d_layers2, dropout0.05 ) optimizer torch.optim.Adam(model.parameters(), lr1e-4) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size10, gamma0.5) criterion nn.MSELoss() for epoch in range(50): model.train() total_loss 0 for batch_x, batch_y in train_loader: # batch_x: [batch, seq_len, enc_in] # batch_y: [batch, label_len pred_len, dec_in] optimizer.zero_grad() # decoder输入前label_len个用真实值后面补0或不给 dec_input torch.cat( [batch_y[:, :label_len, :], torch.zeros(batch_y.shape[0], pred_len, batch_y.shape[2])], dim1 ) outputs model(batch_x, dec_input) loss criterion(outputs, batch_y[:, -pred_len:, :]) loss.backward() optimizer.step() total_loss loss.item()这个脚本里最关键的是dec_input的构造。训练时decoder的起始部分用的是真实的label_len个值后面的预测部分填0模型输出会和真实未来对比计算损失。推理时这部分真实值就换成最新观测到的值后面照旧填0。这个细节很多人第一次复现会搞混导致训练时模型看到了未来的信息测试时表现跳水。4.4 评估指标怎么看我用两个指标MSE和MAE。MSE关注大误差MAE反映平均偏差水平。一般看MSE为主因为时序预测里大误差往往是更严重的问题。评估时要做滚动预测而不是只测一次。我通常把测试集分成多个窗口每天滚动预测一次最后把所有预测结果拼在一起和真实值对比计算整体MSE和MAE。单独测一个窗口容易受偶然因素影响尤其是碰到节假日这类特殊日期模型表现会有明显波动。我在项目里还额外看了分时段误差比如工作日上午和周末晚上的误差分布发现Autoformer对周末的预测误差比工作日大不少原因是训练集中周末天数少、模式不够充分。另一个值得关注的是预测结果的时间滞后情况。我画预测曲线的时候发现预测曲线和真实曲线形状很像但整体向右偏移了1到2个小时。这个现象在时序预测里很常见尤其是模型倾向于输出稳健的均值回归。如果滞后明显就要警惕是不是过拟合了或者输入序列不够长导致模型没有足够信息判断拐点位置。5. 踩坑排查训练Autoformer时我遇到过的几类问题5.1 数据泄露归一化没做对指标虚高这是我复现Autoformer时犯过的第一个严重错误。我当时为了图省事用pandas的describe函数看了全量数据的均值标准差然后直接做归一化。结果验证集和测试集的误差都低得惊人我还高兴地以为模型效果极好后来在真正上线前做回测时才发现预测一塌糊涂。正确做法是在训练集上计算均值和标准差保存下来然后对验证集和测试集套用同一个变换。这也意味着每次重新划分数据集或增加新数据后都要重新计算缩放参数。我后来把标准化封装成了一个小类fit和transform分成两个方法避免了这个问题。5.2 输入序列长度与内存的取舍Autoformer虽然复杂度是O(L log L)但也不是无限长都能跑。我在一次实验里把输入长度从168加到336batch size从32降到8显存才勉强放下。如果序列继续加长训练会非常痛苦。我的处理方法先把输入长度固定在预测长度的2到4倍比如预测48步输入96步或168步先跑通一个baseline。再逐步加大输入长度观察模型在验证集上的表现变化。如果增长不明显就不要一味追求长输入因为长输入意味着需要更多的历史数据对数据不足的场景并不友好。5.3 自相关延迟选择和周期估计的坑自相关机制的top-k选择直接影响模型能捕捉多少种周期成分。k太小周期模式不够丰富k太大噪声也被聚合进来。我在不同数据上试过k取1到5之间比较合理。当你发现预测周期形状单一、总是抓不住第二个主要周期时可以先检查这个参数。另外如果数据本身没有明显周期自相关机制能学到的有效延迟就很少。这时强行用Autoformer反而可能不如简单线性模型。我做过一个无周期噪声序列的实验Autoformer的MSE跟直接用历史均值预测差不多这给我一个教训不同的数据模式要配不同的模型Autoformer不是所有场景的通解。5.4 预测结果过度平滑和滞后过度平滑是指预测曲线波动小、峰谷幅度被压缩。滞后是指预测的上升和下降拐点比真实值晚了一段时间。这两个问题经常一起出现。我从实践里总结的排查顺序是先看数据预处理有没有做过激进的平滑比如把原始负荷值做了大窗口平均。如果有先改成轻度平滑。再检查SeriesDecomp的kernel_size窗口太大时趋势项过于平滑。试着调小到3或5。检查损失函数如果用了很多MAE可能会加重平滑。可以暂时只用MSE看是否改善。最后看数据泄露如果归一化参数用了未来数据模型会显得过于自信导致测试时滞后严重。整个过程用一个问题速查表概括问题现象可能原因解决办法指标虚高上线效果差归一化用了全量数据只用训练集计算均值和标准差预测曲线过度平滑分解窗口过大或损失不合适调小kernel_size改用MSE预测滞后明显输入长度不够或模型过拟合增加输入长度加早停训练时显存爆炸输入太长或batch太大减小batch使用梯度累积抓不住次要周期top-k设置太小增大自相关延迟数量周末/节假日误差大训练集中相关样本少补充历史数据或单独建模这个表格是我后面几轮实验用来排查的主要工具每次模型表现不对就按表里的顺序逐个对照。6. 个人的一点体会Autoformer让我重新认识了“模型架构应该服务于数据先验”这句话。它不是把Transformer改得花哨而是针对时间序列的两个核心特性趋势分解和周期相关做了直接而优雅的改动。这种设计思路比单纯堆注意力层要有效得多。在实际使用中我会再强调一遍先确认数据里有没有周期性。我在某项目里做流量预测数据明显有七天和一天两个周期Autoformer表现很好甚至比一些更复杂的近期时序模型还稳定。但在另一个可预测性很弱的传感器数据上Autoformer和简单线性回归的差距并不大训练成本却是后者的几十倍。模型选型不是越先进越好而是要对症下药。最后分享一个我后来一直在用的小技巧不管用什么时序模型先用一个简单线性模型或直接“用最后N个值的均值”做baseline。如果复杂模型连这个baseline都打不过考虑问题不在模型而在数据。在数据质量、归一化、窗口构造这些基础工作上花时间永远比盲目换模型回报更高。Autoformer是一个值得仔细学的模型但它同样需要建立在这个常识之上。