Advertisement

时间序列:Unlocking the Power of Patch: Patch-Based MLP for Long-Term Time Series Forecasting

阅读量:

摘要:

近期的研究尝试通过改进 Transformer 架构,来验证其在长期时间序列预测(Long-Term Time Series Forecasting, LTSF)任务中的有效性。尽管这类模型在性能上不断进步,并在多个方面超越了传统线性预测模型,我们对 Transformer 是否适合 LTSF 仍持保留态度。我们认为,这些模型的有效性主要归功于其采用的 Patch 机制 ,该机制在一定程度上增强了序列的局部性,但仍无法充分解决自注意力机制(self-attention)中固有的时间信息丢失问题 ,这种问题源自其**置换不变性(permutation-invariance)的特性。进一步的研究表明,相比复杂的 Transformer 架构,结合 Patch 机制的简单线性层在某些情况下可能表现更优 。此外,与以往采用“通道独立(channel independence)”策略的模型不同,我们的研究强调了多变量时间序列中变量间交互(cross-variable interactions)**的重要性。这种交互信息在过去的研究中常被错误处理,导致跨变量模型效果不佳。

基于上述见解,我们提出了一种新颖而简洁的 Patch 机制多层感知机模型(Patch-based MLP,简称 PatchMLP) ,用于解决 LTSF 任务。具体而言,模型使用简单的移动平均 方法从时间序列中提取平滑成分包含噪声的残差 ,通过通道混合(channel mixing)进行语义信息交换,并对噪声部分采用通道独立策略进行建模。PatchMLP 在多个真实世界数据集上持续取得了最先进的性能(SOTA) 。我们希望这一出人意料的发现能激发 LTSF 领域新的研究方向,并为开发更高效、结构更简洁的解决方案 铺平道路。

代码:https://github.com/TangPeiwang/PatchMLP

论文:https://arxiv.org/abs/2405.13575v3

总结:

这篇论文是非Transformer的,MLP实现。论文写的某些见解 要比代码实现要好,

先看文章,摘要里就提到了patch 很关键。

图 1:展示了在 ETTh1 数据集上训练的一个 2 层 Transformer,在不同 Patch 尺寸下的自注意力得分(self-attention scores)。我们采用了 PatchTST(Nie 等,2022)的方法设置,仅保留了编码器(Encoder)部分将解码器(Decoder)替换为一个简单的 MLP ,并使用了通道独立(channel independent)策略 。当 Patch 大小为 1 时,该模型等同于原始的 Transformer,这表明时间序列数据往往具有被划分为 patch 的趋势 (Zhang 和 Yan,2022;Tang 和 Zhang,2023)。增大 patch 大小可以在一定程度上缓解这一问题 。(图例

这里说的划分为patch的趋势是什么意思呢?因为attention matrix 其实是权重,那就是说里面更大的数对应黄色的 表示权重更大 更主要,同时attention matrix 本质上是对信息的筛选,也就是说我们选择的权重信息越集中,矩阵越稀疏越好。

首先看patch=1时,可以看到attention的权重有一部分颜色会更亮一些,但是分布比较均匀。但是patch变大后,可以看到 黄色慢慢变得更加集中。但是patch 变的过于大时,会导致 权重压缩过度,导致模型失去细粒度的时间信息。

后面又做了patch长度 与 输入长度 、预测长度 d_model长度 的关系。Patch Transformer 在 ETTh2 数据集上的实验结果

在保持所有其他参数不变的情况下,仅更改输入序列长度,展示四种预测长度下的 MSE(均方误差)结果。 (pred=96 时候一定最好,越大越差 符合正常认知,,随着输入长度变大,,结果是先变好再变差,其实也能理解,输入越来越长,信息就越来越多,导致结果会变差,但是在一定长度内,会变好的原因 我觉得是因为输入长度 > 预测长度 导致信息非常充足)

同样保持其他参数不变,仅更改 patch 大小,在预测长度为 720 的情况下,展示五种不同输入长度下的 MSE 结果。(首先可以看到输入长度变长 预测长度固定时,趋势是先降低再升高 刚才的,对应到图里就是某一patch ,比如1的时候,最好的时候是288,,但是patch的变化【类似于前面分析attention matrix】 也是先降低再升高,而且对应于不同的输入长度,峰值会不一样,而且还能发现出规律,对于输入长度越长,patch的峰值也越大,表示patch确实能够减小噪声影响,因为输入长度越长 噪声越大,patch越大,信息越模糊)

保持所有其他参数不变,仅更改 patch 大小,在输入长度和预测长度均为 720 的条件下,展示五种不同 dmodel(隐藏层维度)值对应的 MSE 结果。(这个部分)

“正如图 2c 所示,随着 dmodel 的增加,模型性能逐渐提升,这表明较大的 patch 在更大的 dmodel 下表现更好。在极端情况下,如果将整个输入序列视为一个 patch 并映射为一个向量,这与 iTransformer(Liu 等人,2023)的方法类似。然而,更大的 dmodel 也意味着模型需要学习的参数数量更多,这可能导致欠拟合,从而使模型性能下降。”

其实可以看出来,随着patch 变大,整体性能应该是先变好再变差,跟输入长度,预测长度 有关,当修改d_model大小时,可以看到16,32,64时,交错比较复杂,patch小的时候,d_model越小,性能越好;但是随着patch变大,就会出现反转。而d_model 在超过一定范围时 会导致整体性能急剧下降。而该论文的代码 d_model 设置为1024。虽然通过不同的patch 调整为256 最后进行concat,而patch_len=[48, 24, 12, 6] 。 所以这里可能会有点问题。。

后面的话呢 就是介绍介绍模型框架,很一般了

整体框架如上图, 文章的方法里也是按照这个框架去进行的划分。主要有四块,多尺度 Patch 嵌入、特征分解、MLP层、投影层

首先是多尺度Patch嵌入,这个就是他这篇文章的重点,但实际上代码是

复制代码
 class Emb(nn.Module):

    
  
    
     def __init__(self, seq_len, d_model, patch_len=[48, 24, 12, 6]):
    
     super().__init__()
    
     patch_step = patch_len
    
     d_model = d_model//4
    
     # print('d_model',d_model)        ## 256
    
     self.EmbLayer_1 = EmbLayer(patch_len[0], patch_step[0] // 2, seq_len, d_model)
    
     self.EmbLayer_2 = EmbLayer(patch_len[1], patch_step[1] // 2, seq_len, d_model)
    
     self.EmbLayer_3 = EmbLayer(patch_len[2], patch_step[2] // 2, seq_len, d_model)
    
     self.EmbLayer_4 = EmbLayer(patch_len[3], patch_step[3] // 2, seq_len, d_model)
    
  
    
     def forward(self, x):
    
     s_x1 = self.EmbLayer_1(x)               ###  [batch, features, d_model/4]
    
     s_x2 = self.EmbLayer_2(x)
    
     s_x3 = self.EmbLayer_3(x)
    
     s_x4 = self.EmbLayer_4(x)
    
     s_out = torch.cat([s_x1, s_x2, s_x3, s_x4], -1)     ###  [batch, features, d_model]
    
     return s_out
    
    
    
    
    python
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-08-16/T5NdnlOfk6Msmuyiw1zaHrEoPQC2.png)

patch固定划分成四个,然后d_model = 1024 划分成几组,这样看起来,是实现想法,但是没有那么落实,这个位置可以稍稍改改,对比数据。

第二个是特征分解。这里就没什么意思,还是按之前的拆成整体趋势trend和局部seasonal。

复制代码
 class series_decomp(nn.Module):

    
     """
    
     Series decomposition block
    
     """
    
  
    
     def __init__(self, kernel_size):
    
     super(series_decomp, self).__init__()
    
     self.moving_avg = moving_avg(kernel_size, stride=1)
    
  
    
     def forward(self, x):
    
     moving_mean = self.moving_avg(x)
    
     res = x - moving_mean
    
     return res, moving_mean
    
    
    
    
    python
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-08-16/MPnwLS8gai1W9T3zC5EvoyxbNk2t.png)

这里不懂为什么kernel_size = 13 可能超参吧,但是这个kernel_size感觉也可以用上面的patch思想带入。

第三个是MLP层,本来觉得最屌的地方,感觉就比较敷衍。说是channel mix

对着图看着花里胡哨。一看代码,实现就很简单,可能因为本身就是mlp。只能做一下转置的小操作

复制代码
 class Encoder(nn.Module):

    
  
    
     def __init__(self, d_model, enc_in):
    
     super().__init__()
    
     ## d_model = 1024
    
     ## enc_in = 21
    
     self.norm1 = nn.LayerNorm(d_model)
    
     self.norm2 = nn.LayerNorm(d_model)
    
  
    
     self.ff1 = nn.Sequential(
    
         nn.Linear(d_model, d_model),
    
         nn.GELU(),
    
         nn.Dropout(0.1)
    
     )
    
  
    
     self.ff2 = nn.Sequential(
    
         nn.Linear(enc_in, enc_in),
    
         nn.GELU(),
    
         nn.Dropout(0.1)
    
     )
    
  
    
     def forward(self, x):
    
  
    
     # print(x.shape)      ##torch.Size([32, 21, 1024])
    
     
    
     y_0 = self.ff1(x)
    
     y_0 = y_0 + x
    
     y_0 = self.norm1(y_0)
    
     y_1 = y_0.permute(0, 2, 1)
    
     y_1 = self.ff2(y_1)
    
     y_1 = y_1.permute(0, 2, 1)
    
     y_2 = y_1 * y_0 + x
    
     y_2 = self.norm1(y_2)
    
  
    
     return y_2
    
    
    
    
    python
    
    
![](https://ad.itadn.com/c/weblog/blog-img/images/2025-08-16/DthWjKgMdbCXurLf7HJ1Oio3aSvI.png)

但是这里的 y_0 = y_0 + x 就没有很看懂了。可能就是相当残差层 方便训练。不过非常奇怪的是跟attention 的思路非常的相似非常的相似,可以把类似的做法带回attention

第四个就是prediction,💩 就mlp 从 d_model 映射回 pred_len

文章说完了代码也讲完了。

全部评论 (0)

还没有任何评论哟~