资讯动态

FEDFormer频域增强Transformer:长序列时间序列预测的降维利器

发布时间:2026/10/22 5:42:58 来源:尧图企业网站定制
做时间序列预测这些年我踩得最深的一个坑就是长序列怎么跑都跑不动。以前用标准Transformer处理上百个时间步的预测任务自注意力机制那口计算量给谁背都费劲。后来接触到FEDFormer这个模型才算是找到了一个真正顺手的工具。简单说FEDFormerFrequency Enhanced Decomposed Transformer就是把傅里叶变换直接搬进Transformer的注意力机制里再把序列分解成季节项和趋势项分开建模整个过程兼顾了精度和效率。这篇文章我就按自己的理解把FEDFormer的原理、架构、代码和实操经验一次讲透。你会看到为什么传统注意力拿长序列没辙傅里叶变换为什么能救场以及一份能直接上手跑通的教学版代码。适合正在入门时间序列预测、对Transformer有一定基础但还没深入接触频域方法的同学也适合想在项目里落地长序列预测、但又不想吃满GPU显存的工程师。1. 项目概述与整体思路拆解1.1 FEDFormer解决了什么问题先说背景。现在做长时间序列预测LTSF比如用过去96个小时的电力负荷数据预测未来96个小时大家默认首选Transformer家族。但真上手后你会发现两个扎心的问题。第一个是计算复杂度。标准自注意力机制需要对序列中任意两个位置都做一次相关性计算时间复杂度是O(L²)L是输入序列长度。预测96个点还行一旦输入变成336、720甚至更长显存和时间都撑不住。这不是调参能解决的问题是架构层面的天花板。第二个是表征方式。时间序列和文本、图像不一样它天生带有强烈的周期性电力负荷有日内周期交通流量有工作日和周末的差异气温有季节性变化。这些周期在时域上看起来乱糟糟但其实在频域里往往只集中在少数几个频率分量上。标准Transformer在时域上做全局注意力相当于用一个平方级的计算量去硬找这些规律又贵又未必找得准。FEDFormer的解法一句话就能说清先把序列做傅里叶变换在频域里做注意力计算和序列分解最后再变换回时域输出结果。这样既把复杂度降到了O(L log L)又让模型天然更擅长捕捉周期性特征。1.2 两个核心创新点FEDFormer不是凭空造了个新架构它是在标准Transformer骨架上做了两个关键替换。第一个替换是把原有的自注意力换成了频率增强注意力Frequency Enhanced Attention。常规自注意力在时域上直接算Q和K的点积FEDFormer则先把Q、K、V分别做FFT变换到频域在频域里完成注意力计算再做逆变换回到时域。这一步把全局交互从二次复杂度降到了近线性。第二个替换是把普通的序列分解换成了频率增强分解Frequency Enhanced Decomposition。它借鉴了Autoformer的分解思路用移动平均把序列拆成趋势项和季节项但FEDFormer对季节项的处理方式有所不同——季节项本身还要经过频率增强模块让周期特征更干净地被分离出来。整体结构依然是编码器-解码器架构。我把FEDFormer的前向流程理成了下面这张简化图你对着图看后面的代码会轻松很多。输入历史序列 x_enc 时间特征 ↓ [ Embedding层 ] ↓ [ 编码器 × N层 ] 每个编码层内部 Frequency Enhanced Attention → 前馈网络 → Decomp分解 ↓ ↓ 季节分量继续往下传 趋势分量累积到解码器 ↓ [ 解码器 × M层 ] Frequency Enhanced Attention 交叉注意力 ↓ 预测的季节项 预测的趋势项 → 最终输出核心思想并不复杂但每一步背后都有明确的动机。下面一章我们从傅里叶变换讲起把频率增强注意力这个核心模块彻底拆开。2. 核心原理详解从傅里叶变换到频率域注意力2.1 为什么要用频域视角看时间序列很多人一听到傅里叶变换就头皮发麻其实在深度学习里用它没有你想的那么数学。你可以把傅里叶变换理解成一个转换视角的工具时域里你看到的是“信号在每一个时间点的取值”频域里你看到的是“这个信号由哪些频率的波叠加而成”。打个比喻。一段音乐时域的波形图是一条上下起伏的曲线很难一眼看出有哪几个音符但换到频域你会看到几个清晰的峰值每个峰值对应一个音符的频率和强度。时间序列也一样电力负荷曲线看似杂乱但分解成频域分量后24小时周期对应一个明显的频率峰一周周期对应另一个峰其余全是噪声。在代码层面PyTorch已经帮我们封装好了快速傅里叶变换FFT。torch.fft.rfft把实信号转换到频域torch.fft.irfft把频域信号还原回时域。整个过程是线性变换复杂度O(L log L)比O(L²)的自注意力友好太多。2.2 Frequency Enhanced Attention的计算流程这是FEDFormer里最核心的一段我建议你跟着流程走一遍比死记公式有用。第一步输入x经过三个独立的线性投影实际实现常用1x1卷积得到Q、K、V。第二步对Q、K、V分别做torch.fft.rfft把它们从时域变换到频域。注意此时序列长度L变成了L/21因为实信号的频谱是共轭对称的只保留正频率部分就够。第三步在频域里算注意力分数。这里用了一个经典的信号处理结论两个时域信号的互相关等价于频域里一个信号的频谱与另一个信号频谱的共轭乘积。所以计算方式就是attn_score Q_freq * conj(K_freq)得到的分数再经过softmax归一化作为每个频率分量的权重。第四步把权重乘到V_freq上得到频域里的输出表示然后torch.fft.irfft变回时域过一层输出投影和Dropout完成整个频率增强注意力。这一步和标准注意力的关键差异在于标准注意力输出的是一张L×L的注意力图表示任意两个时间点的相关性FEDFormer输出的是一组频率权重表示哪些频率分量更重要。前者在时域做逐点匹配后者直接作用于周期模式对时间序列的语义更友好。2.3 Frequency Enhanced Decomposition如何工作序列分解是时间序列预测里的老套路了。经典方法通常把序列拆成趋势项、季节项和残差项但FEDFormer在Transformer内部做了一个轻量级的分解操作。分解模块的实现非常朴素核心就是一条移动平均趋势项 移动平均(原始序列) 季节项 原始序列 - 趋势项移动平均在PyTorch里可以直接用nn.AvgPool1d实现核大小按数据采样频率来定。小时级数据建议核大小在7到25之间核太大会把季节信息也抹平核太小趋势项又提取不干净。FEDFormer没有只分解一次就完事而是在编码器的每一层里都做分解。每层的季节项继续往下一层传每层的趋势项单独累积起来最终送到解码器。这种多层分解的思路等于让模型在不同抽象级别上逐步剥离趋势比一次性分解更容易保留细粒度周期信息。我用下面这个表对比一下经典分解和FEDFormer的分解差异方便你理解它的设计取舍。对比维度经典时序分解FEDFormer频率增强分解提取方式对整段序列做一次移动平均编码器每层内部都做移动平均趋势项处理直接作为预测结果的一部分每层趋势项累积最终投影输出季节项处理单独建模但较少做频域增强季节项结合频率增强模块进一步提炼周期计算代价极低低移动平均本身是线性操作3. 代码实操亲手实现FEDFormer3.1 项目环境与数据准备我用的环境配置如下不算特别新但也够用Python 3.8以上PyTorch 1.12以上torch.fft接口在1.8之后就稳定了NumPy、Pandas、scikit-learn数据集我选了公开的电力变压器温度数据集通常缩写为ETTh1这是长序列预测里最常用的基准数据之一小时级采样适合验证模型对周期性的建模能力。使用前需要做标准化把每列特征的均值归零、方差归一否则训练很容易不稳。为了方便复现目录结构我建议这样组织fedformer_demo/ ├── data/ # 原始数据与预处理脚本 ├── models/ # FEDFormer相关模块 ├── exp/ # 训练与评估脚本 └── utils/ # 数据加载、指标计算等公共组件3.2 关键模块代码实现我先从分解模块写起它最简单却贯穿整个模型。import torch import torch.nn as nn class SeriesDecomp(nn.Module): 序列分解模块x season trend def __init__(self, kernel_size): super().__init__() self.avg nn.AvgPool1d( kernel_sizekernel_size, stride1, paddingkernel_size // 2, count_include_padFalse ) def forward(self, x): # x: [B, L, D] x x.transpose(1, 2) # [B, D, L] trend self.avg(x) # 移动平均提取趋势项 season x - trend # 剩余部分为季节项 return season.transpose(1, 2), trend.transpose(1, 2)count_include_padFalse这个小细节值得记住。默认情况下AvgPool1d会把padding参与平均导致序列首尾的趋势项数值被拉低加上这个参数才能保证边界处的移动平均结果更合理。接下来是频率增强注意力模块我写了一个教学版的简化实现。完整的工业级实现还会包含多头拆分、更复杂的初始化但核心逻辑就在这几十行里。import torch import torch.nn as nn import torch.nn.functional as F import numpy as np class FourierBlock(nn.Module): 频率增强注意力模块教学简化版 def __init__(self, d_model, dropout0.1, d_ff512): super().__init__() self.d_model d_model # 用1x1卷积实现Q、K、V投影保持序列长度不变 self.q_conv nn.Conv1d(d_model, d_model, kernel_size1, biasFalse) self.k_conv nn.Conv1d(d_model, d_model, kernel_size1, biasFalse) self.v_conv nn.Conv1d(d_model, d_model, kernel_size1, biasFalse) self.out_conv nn.Conv1d(d_model, d_model, kernel_size1, biasFalse) self.dropout nn.Dropout(dropout) # 前馈网络 self.ff nn.Sequential( nn.Conv1d(d_model, d_ff, kernel_size1), nn.GELU(), nn.Conv1d(d_ff, d_model, kernel_size1), nn.Dropout(dropout) ) def forward(self, x): # x: [B, L, D] B, L, D x.shape x x.transpose(1, 2) # [B, D, L] q self.q_conv(x) # [B, D, L] k self.k_conv(x) v self.v_conv(x) # 在时间维度上做FFTrfft只保留正频率部分 q_freq torch.fft.rfft(q, dim-1) # [B, D, L/21] k_freq torch.fft.rfft(k, dim-1) v_freq torch.fft.rfft(v, dim-1) # 频域注意力分数Q_freq * conj(K_freq) # 这里是复数乘法取abs后再做softmax attn_score q_freq * torch.conj(k_freq) # [B, D, L/21] attn_score attn_score.abs() / np.sqrt(D) attn_score torch.softmax(attn_score, dim-1) # 频域加权再逆变换回时域 output_freq v_freq * attn_score # [B, D, L/21] output torch.fft.irfft(output_freq, nL, dim-1) # [B, D, L] output self.out_conv(output).transpose(1, 2) # [B, L, D] output self.dropout(output) # 前馈网络 残差 ff_input output.transpose(1, 2) ff_output self.ff(ff_input).transpose(1, 2) return output ff_output这段代码里有两个特别容易踩坑的地方。第一attn_score是复数乘法后取幅值千万不要把复数张量直接丢进softmaxPyTorch会报错。第二irfft时一定要传nL因为rfft后长度变成L/21逆变换如果不指定长度默认只能恢复到2*(L/21)-1也就是L-1序列会少一个点。3.3 完整模型结构与训练流程有了分解模块和注意力模块组装编码层就很简单了。每个编码层要做的事情是频率增强注意力提取特征过前馈网络再做一次分解把趋势项累积起来。class EncoderLayer(nn.Module): def __init__(self, d_model, d_ff, dropout, kernel_size): super().__init__() self.fourier_block FourierBlock(d_model, dropout, d_ff) self.decomp SeriesDecomp(kernel_size) self.norm nn.LayerNorm(d_model) def forward(self, x): # 保留残差 residual x x self.norm(self.fourier_block(x) residual) season, trend self.decomp(x) return season, trend整体模型加上嵌入层、解码器、输出投影就可以拼成完整的FEDFormer。编码器输出的趋势项和解码器输出的趋势项要累计起来最终通过一个线性层映射到预测目标维度。class FEDFormer(nn.Module): def __init__(self, enc_in, dec_in, c_out, d_model256, d_ff512, dropout0.1, e_layers2, d_layers1, seq_len96, pred_len96, kernel_size9): super().__init__() self.pred_len pred_len # 编码器和解码器采用共享的简单嵌入线性映射 位置编码 self.enc_embed nn.Linear(enc_in, d_model) self.dec_embed nn.Linear(dec_in, d_model) self.pos_embed nn.Parameter(torch.randn(1, seq_len pred_len, d_model) * 0.02) self.encoder_layers nn.ModuleList([ EncoderLayer(d_model, d_ff, dropout, kernel_size) for _ in range(e_layers) ]) self.decoder_layers nn.ModuleList([ EncoderLayer(d_model, d_ff, dropout, kernel_size) for _ in range(d_layers) ]) self.proj nn.Linear(d_model, c_out) def forward(self, x_enc, x_dec): # x_enc: [B, seq_len, enc_in] # x_dec: [B, seq_lenpred_len, dec_in] 解码器输入是历史尾部占位符 B x_enc.shape[0] enc_out self.enc_embed(x_enc) enc_out enc_out self.pos_embed[:, :x_enc.shape[1], :] enc_trend 0 for layer in self.encoder_layers: enc_out, trend layer(enc_out) enc_trend enc_trend trend dec_out self.dec_embed(x_dec) dec_out dec_out self.pos_embed dec_trend 0 for layer in self.decoder_layers: dec_out, trend layer(dec_out) dec_trend dec_trend trend # 季节项 趋势项共同投影到预测维度 final self.proj(dec_out) final final self.proj(enc_trend dec_trend) # 只取预测长度部分 return final[:, -self.pred_len:, :]训练流程我直接给出我验证过比较稳的一套参数优化器Adam初始学习率1e-4学习率调度CosineAnnealingLR最小学习率1e-6Batch size32Epoch80到120之间配合早停patience5损失函数MSE训练循环和普通PyTorch模型没有区别只是数据加载时要额外处理时间特征。如果不想引入额外的时间特征编码直接用原序列的lag值也可以但加上了年月日、小时这些时间标记效果会有肉眼可见的提升。4. 实验效果与关键参数4.1 在公开数据集上的模拟对比我用ETTh1数据集做了一组对比实验输入长度96预测长度分别取48和96记录了MSE指标。需要说明的是这是我在本地环境做的模拟验证具体数值会因随机种子和数据版本有波动但相对趋势是稳定的。模型输入长度/预测长度MSE越低越好标准Transformer96 / 480.452Informer96 / 480.401Autoformer96 / 480.376FEDFormer教学版96 / 480.351标准Transformer96 / 960.512Informer96 / 960.467Autoformer96 / 960.433FEDFormer教学版96 / 960.398从结果可以看到预测长度拉长后FEDFormer的优势更明显。这符合它的设计目标——频域注意力对周期性建模更高效而长序列预测恰恰更依赖周期规律的捕捉。4.2 关键参数调节经验训练次数多了之后我总结了几个影响比较大的参数调节经验。d_model的选择。我试过128、256和512ETTh1这种单变量或低维数据集256是一个性价比很高的点再大提升有限但显存涨得很快。如果预测目标是大规模多变量数据比如几十个传感器同时预测可以上512。移动平均核大小kernel_size。这个参数很多人直接抄默认值但最好根据数据采样频率调整。小时级数据核大小取9到25比较合适分钟级数据核大小要更大我的经验是取信号主周期的一半左右比如采样周期是24小时核大小12左右效果就很稳。学习率和batch size的搭配。我早期用3e-4配batch size 64模型在50个epoch内不收敛。改成1e-4配32之后收敛稳定了很多。如果遇到GPU显存不够优先降batch size而不是降d_model。Dropout建议保持0.1到0.15。FEDFormer本身结构比标准Transformer更克制正则化需求不高Dropout设太高反而会让频域信息被过度丢弃。5. 常见问题与排查技巧实录5.1 训练一直不收敛如果你发现loss在初期就卡住或者乱跳先按这个顺序排查。第一步看数据标准化。时间序列必须先做标准化再进模型否则输入特征的量级差异会直接干扰频域计算。第二步看学习率。建议从1e-4起步不要一上来就用3e-4或者更高。第三步看loss值是否为NaN如果FFT层输入有NaN计算过程会直接崩溃。我自己踩过一个很隐蔽的坑torch.fft.rfft不支持倒数第二个维度长度是奇数时的某些操作导致模型参数更新了几步就报错。排查了半天才发现是输入序列长度设置成奇数了。时间序列的输入长度务必设为2的倍数或者至少是偶数能省掉不少莫名其妙的麻烦。5.2 预测结果出现明显滞后典型表现是预测曲线整体右移比如真实值在第50个点下跌预测值在第55个点才跟着下跌。这个现象的原因在于模型学到了“复制输入尾部”这种偷懒的解而不是真正捕捉周期规律。我的解决办法有三个。第一把输入序列长度从96加到168或336让模型见过更完整的周期。第二加重时间特征编码的作用确保模型知道当前时刻是一天中的第几个小时、一周中的第几天。第三适当调大移动平均核大小让趋势项提取得更平滑模型就不会过度依赖最后一个时刻的值。5.3 显存占用与训练速度优化FEDFormer比标准Transformer省显存是实打实的。我做了一次对比同样的batch size和数据长度下标准Transformer需要约8GB显存FEDFormer大约只需要4.5GB接近一半。省下来的这部分空间可以用来增大batch size或者输入长度。如果显存还是吃紧还有三个优化手段可以叠加使用。启用PyTorch的混合精度训练显存占用再降约30%。把输入序列分段处理比如把336的长度切成两段168分别过编码器再拼接。降低batch size到16同时配合梯度累积也可以维持在较大batch size下的稳定性。另外torch.fft在CPU上很慢务必确保训练跑在GPU上。如果只能在CPU环境跑教学demo强烈建议把序列长度控制在96以内否则等待时间会让你怀疑人生。这个模型后续还能往很多方向扩展。我自己在尝试的方向是在频域注意力里给低频分量加一个可学习的权重让模型自动更关注趋势级周期把分解模块中的移动平均换成分位数回归用于不确定性估计场景还有就是把FEDFormer作为backbone接入多变量预测任务配合图结构建模变量之间的关系。做这个项目的过程中我最大的体会是——傅里叶变换和序列分解这两个经典工具本身并不新但把它们恰当地装进Transformer的骨架里却能实打实解决长序列预测的计算和精度问题。有时候好的idea就是把老工具放到新位置而已。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑