对patch深入理解下篇:Patch+LSTM实现以及改进策略整理
我在去年11月份写了patch深入理解的上篇,主要介绍patch的原理和代码实现过程。文章发布后很多朋友催更下篇,其实一直在积累素材,因为介绍完原理和实现之后,下一步肯定是要考虑如何改进。在这之前,首先,我们接着上一篇的内容,实现了一个LSTM+patch的例子,结果表明加上Patch之后确实对LSTM在各指标和预测长度上均有明显的效果提升。然后,本篇文章还重点介绍了最近一段时间我读到的对patch的几种改进策略,以及自己的一些思考。
这里再挖个坑吧,下一篇我计划写一下如何在LSTM+Patch的基础上,通过傅立叶进行降噪,并探索实现动态切分patch的方法。欢迎关注~
实现LSTM+Patch
我的这份代码是在Are Transformers Effective for Time Series Forecasting? (AAAI 2023)这篇文章代码的基础上写的。主要在model文件夹添加了LSTM和patch_LSTM两个类,大家复制下面的代码,把文件放入到model文件夹即可运行。此外,为了简化,没有做patch切分时候的补齐。包括hidden_size等一些参数也是直接写到了模型里面,所以严格来说,代码并不是很规范,以实现效果为主。
01 LSTM 代码实现
下面是原始LSTM模型的代码实现,实现过程已经重复过多次,直接看代码,就不在展开细致讲解。
代码语言:javascript代码运行次数:0运行复制class Model(nn.Module):
"""
Just one Linear layer
"""
def __init__(self, configs):
super(Model, self).__init__()
self.seq_len = configs.seq_len
self.pred_len = configs.pred_len
self.hidden_size = 64
self.num_layers = 2
self.enc_in = configs.enc_in
self.lstm = nn.LSTM(self.enc_in, self.hidden_size, self.num_layers, batch_first=True)
self.fc = nn.Linear(self.hidden_size, self.pred_len)
self.fc2 = nn.Linear(self.seq_len, self.enc_in)
def forward(self, x):
# x: [Batch, Input length, Channel]
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) # 初始化隐藏状态h0
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) # 初始化记忆状态c0
out, _ = self.lstm(x, (h0, c0)) # out:[bs,seq,hid]
out = self.fc(out) # out:[bs,seq,pred_len]
out = self.fc2(out.permute(0, 2, 1)) # out: [bs, pred_len, channel]
return out # [Batch, Output length, Channel]
代码语言:javascript代码运行次数:0运行复制
02 LSTM+patch
下面是代码实现的LSTM+patch的实现过程。这里我设置了步长为12、切分长度也为12,这样相当于没有重复的切分。此外,为了简化很多超参数也没有特意调参寻优,我们还是把重点放到patch实现上,特别要关注数据维度的变化:
- 进入forward函数时x的维度: [Batch, seq_len, Channel]
- 置换后两个维度,并用unfold函数切分:[bs, ch, sql]=>[bs, ch, pum, plen],pnum是切分后块的数量。
- 之前讲过,放回到LSTM、Transformer时,数据还是要变回三维,因此这里我们把batch和channel合并到一起,数据维度变成了:[(bs*ch), pnum, plen]
- 此时已经可以放入到模型建模,LSTM输出结果的维度是:[(bs*ch), plen, hidden],然后我们通过线性层和reshape操作,把维度调整回[(bs,pred_len, ch]
代码如下,其实就是维度拆分、合并。另外欢迎大家纠错
class Model(nn.Module):
"""
Just one Linear layer
"""
def __init__(self, configs):
super(Model, self).__init__()
self.seq_len = configs.seq_len #336
self.pred_len = configs.pred_len
self.hidden_size = 64
self.num_layers = 2
self.enc_in = configs.enc_in
#patch
self.plen = 12
self.pnum = 28 # seq_len/plen
# LSTM 这里 self.enc_in => self.pnum
self.lstm = nn.LSTM(self.pnum, self.hidden_size, self.num_layers, batch_first=True)
self.fc = nn.Linear(self.hidden_size, self.pnum)
self.fc2 = nn.Linear(self.seq_len, self.pred_len)
# self.fc3 = nn.Linear(self.seq_len, self.enc_in)
def forward(self, x): # x: [Batch, seq_len, Channel]
x_shape = x.size()
# patching
if x.size(1) == self.seq_len: # 这里为了简单,就直接定义patch的切分步长为12,不重合,注意336恰好能整除12
x = x.permute(0, 2, 1) # [bs, ch, sql]=>[16,21,336]
x = x.unfold(dimension=-1, size=12, step=12) # [bs, ch, pum, plen]=>[16,21,28,12]
x = torch.reshape(x,(x.shape[0]*x.shape[1], x.shape[2], x.shape[3])) # [(bs*ch), pnum, plen]=>[336,28,12] 相当于原来的[bs, ch, sql]
x = x.permute(0, 2, 1)
h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) # 初始化隐藏状态h0
c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size).to(x.device) # 初始化记忆状态c0
out, _ = self.lstm(x, (h0, c0)) # out:[(bs*ch), plen, hidden]
out = self.fc(out)
out = torch.reshape(out, (x_shape[0], x_shape[1], -1)) # out: [bs, ch, ()]
out = self.fc2(out.permute(0, 2, 1)) # out: []
return out.permute(0, 2, 1) # [Batch, Output length, Channel]
下面是我跑的几组实验,验证了在不同的预测长度下,原始LSTM和Patch+LSTM的实验结果对比。首先大家能看到,添加Patch后,各项指标在不同的预测长度上提升都是非常明显的。其次,线性模型比LSTM效果好很多~
你肯定也能想到现在自己定义patch的长度为12,滑动步长为12,是没有依据的,没错!这些都是可以改进的地方。这就接到下文了,我们来阅读近期三篇对patch的一些改进工作。
原始Patch的不足
还是先回顾一下patch的具体操作,是把一条序列切分成片段,然后按照片段进行建模。好处在于:1、保留更长的前后信息;2、节约transformer计算成本。
那原始的patch是否有不足呢?有的,首先原始patch是对所有的数据集按照相同的片段长度进行切分。但实际情况是不同数据集的采样频率有巨大差距。如下图所示,像汇率数据集、疾病数据集、交通数据集的数据模式显然是不一样的,那么采样频率是分钟的和小时都用同样的长度切显然不合理。
所以对patch的第一种改进策略就是:按照数据集的差异进行动态切分,学术一点的名字就是自适应切分