I. 前言
前面已经写了很多关于时间序列预测的文章:
- 深入理解PyTorch中LSTM的输入和输出(从input输入到Linear输出)
- PyTorch搭建LSTM实现时间序列预测(负荷预测)
- PyTorch中利用LSTMCell搭建多层LSTM实现时间序列预测
- PyTorch搭建LSTM实现多变量时间序列预测(负荷预测)
- PyTorch搭建双向LSTM实现时间序列预测(负荷预测)
- PyTorch搭建LSTM实现多变量多步长时间序列预测(一):直接多输出
- PyTorch搭建LSTM实现多变量多步长时间序列预测(二):单步滚动预测
- PyTorch搭建LSTM实现多变量多步长时间序列预测(三):多模型单步预测
- PyTorch搭建LSTM实现多变量多步长时间序列预测(四):多模型滚动预测
- PyTorch搭建LSTM实现多变量多步长时间序列预测(五):seq2seq
- PyTorch中实现LSTM多步长时间序列预测的几种方法总结(负荷预测)
- PyTorch-LSTM时间序列预测中如何预测真正的未来值
- PyTorch搭建LSTM实现多变量输入多变量输出时间序列预测(多任务学习)
- PyTorch搭建ANN实现时间序列预测(风速预测)
- PyTorch搭建CNN实现时间序列预测(风速预测)
- PyTorch搭建CNN-LSTM混合模型实现多变量多步长时间序列预测(负荷预测)
- PyTorch搭建Transformer实现多变量多步长时间序列预测(负荷预测)
- PyTorch时间序列预测系列文章总结(代码使用方法)
- TensorFlow搭建LSTM实现时间序列预测(负荷预测)
- TensorFlow搭建LSTM实现多变量时间序列预测(负荷预测)
- TensorFlow搭建双向LSTM实现时间序列预测(负荷预测)
- TensorFlow搭建LSTM实现多变量多步长时间序列预测(一):直接多输出
- TensorFlow搭建LSTM实现多变量多步长时间序列预测(二):单步滚动预测
- TensorFlow搭建LSTM实现多变量多步长时间序列预测(三):多模型单步预测
- TensorFlow搭建LSTM实现多变量多步长时间序列预测(四):多模型滚动预测
- TensorFlow搭建LSTM实现多变量多步长时间序列预测(五):seq2seq
- TensorFlow搭建LSTM实现多变量输入多变量输出时间序列预测(多任务学习)
- TensorFlow搭建ANN实现时间序列预测(风速预测)
- TensorFlow搭建CNN实现时间序列预测(风速预测)
- TensorFlow搭建CNN-LSTM混合模型实现多变量多步长时间序列预测(负荷预测)
- PyG搭建图神经网络实现多变量输入多变量输出时间序列预测
- PyTorch搭建GNN-LSTM和LSTM-GNN模型实现多变量输入多变量输出时间序列预测
- PyG Temporal搭建STGCN实现多变量输入多变量输出时间序列预测
- 时序预测中Attention机制是否真的有效?盘点LSTM/RNN中24种Attention机制+效果对比
- 详解Transformer在时序预测中的Encoder和Decoder过程:以负荷预测为例
- (PyTorch)TCN和RNN/LSTM/GRU结合实现时间序列预测
- PyTorch搭建Informer实现长序列时间序列预测
- PyTorch搭建Autoformer实现长序列时间序列预测
- PyTorch搭建GNN(GCN、GraphSAGE和GAT)实现多节点、单节点内多变量输入多变量输出时空预测
在PyTorch搭建Transformer实现多变量多步长时间序列预测(负荷预测)中我们仅仅使用了Transformer的encoder进行编码,然后直接flatten再使用一个MLP得到预测结果,而不是使用decoder来进行解码得到输出。
在这篇文章中,将详细讲解Transformer完整的Encoder-Decoder架构在时间序列预测上的应用。
II. Transformer
先给出完整的模型定义代码:
class TransformerModel(nn.Module):
def __init__(self, args):
super(TransformerModel, self).__init__()
self.args = args
self.trans = nn.Linear(args.input_size, args.d_model)
self.pos_emb = PositionalEncoding(args.d_model)
encoder_layer = nn.TransformerEncoderLayer(
d_model=args.d_model,
nhead=4,
dim_feedforward=4 * args.d_model,
batch_first=True,
dropout=0.2,
device=device
)
decoder_layer = nn.TransformerDecoderLayer(
d_model=args.d_model,
nhead=4,
dropout=0.2,
dim_feedforward=4 * args.d_model,
batch_first=True,
device=device
)
self.encoder = torch.nn.TransformerEncoder(encoder_layer, num_layers=3)
self.decoder = torch.nn.TransformerDecoder(decoder_layer, num_layers=3)
self.output_fc = nn.Sequential(
nn.Linear(args.d_model, 64),
nn.ReLU(),
nn.Dropout(),
nn.Linear(64, args.input_size)
)
def encode(self, src):
src = self.trans(src)
src = self.pos_emb(src)
memory = self.encoder(src)
return memory
def decode(self, tgt, memory, tgt_mask):
tgt = self.trans(tgt)
tgt = self.pos_emb(tgt)
out = self.decoder(tgt=tgt, memory=memory, tgt_mask=tgt_mask)
out = self.output_fc(out)
return out
def forward(self, src, tgt, tgt_mask):
memory = self.encode(src)
out = self.decode(tgt, memory, tgt_mask)
return out
2.1 Encode
Encode的作用是将时间序列进行编码以得到上下文信息。在NLP中,假设我们输入的句子长度为s
,即句子中包含s
个单词,我们需要将s
单词每一个都进行编码得到大小为(s, e)
的矩阵。如果我们想要一次性输入b
个句子,最终将得到一个大小为(b, s, e)
的矩阵,这与我们前面讲的时间序列需要的输入维度(batch_size, seq_len, input_size)
一致。
在NLP中,当我们想要一次性输入b
个句子时,我们不得不考虑一个问题:b
个句子的长度是否一致?答案是否定的。因此,我们需要进行padding操作,给定一个最大的句子长度s
,如果某一条句子长度不足,则将其长度扩充为s
。
以下面3个句子为例:
[
[‘I’,'love','China','<PAD>'],
[‘Machine’,'Learning','is','interesting'],
[‘a’,'b','<PAD>','<PAD>']
]
最大长度我们设置为4,如果某个句子长度不为4,则填充pad。需要注意的是,在时间序列中,由于数据一般以表格形式存在,我们可以保证每一次都输入seq_len个时间段的所有变量,所以不需要上述操作。
因此,Encoder部分的代码可以表示为:
def encode(self, src):
src = self.trans(src)
src = self.pos_emb(src)
memory = self.encoder(src)
return memory
其中src的大小为(batch_size, seq_len, input_size)
,由于encoder要求输入的大小为(batch_size, seq_len, d_model)
,因此我们首先利用一个nn.Linear操作进行维度变换,然后进行位置编码,最后送入encoder层得到大小为(batch_size, seq_len, d_model)
的memory。
2.2 Decode
Decode的过程定义如下:
def decode(self, tgt, memory, tgt_mask):
tgt = self.trans(tgt)
tgt = self.pos_emb(tgt)
out = self.decoder(tgt=tgt, memory=memory, tgt_mask=tgt_mask)
out = self.output_fc(out)
return out
其中tgt的大小为(batch_size, tgt_len, input_size)
,由于decoder要求的输入为(batch_size, tgt_len, d_model)
,因此首先进行一个简单变换+位置编码,然后送入decoder中解码,最后经过一个简单变换得到大小为(batch_size, tgt_len, input_size)
,表示所有input_size
个变量未来tgt_len
个预测值。
在Transformer中,如果我们需要将中文句子“我/爱/机器/学习”翻译为英文句子"i/ love /machine/ learning",正常的操作过程为:
- 把“我/爱/机器/学习”embedding后输入到encoder里去进行编码。
- 将<bos>也就是开始符号作为decoder的初始输入,然后与前面encoder的输出编码做注意力机制,最终得到一个最大概率输出词A1,然后A1和‘i’做cross entropy计算error。
- 将<bos>,“i” 作为decoder的输入,将decoder的最大概率输出词A2和‘love’做cross entropy计算error。
- 以此类推,最终将,“i”,"love ",“machine”,“learning” 作为decoder的输入,将decoder最大概率输出词A5和终止符做cross entropy计算error。
显然,这个过程是串行的,也就是每次输入的句子的长度都是不一样的。然而,在训练阶段,为了并行化,我们可以一次性输入5句话,然后采用类似于前面mask的技巧,将第一句话的后四个位置mask掉,这样<bos>只能与自身计算注意力分数;对于第2句话,我们将后三个位置mask掉,这样前两个单词<bos>和"i"只能互相计算注意力分数,依次类推,最终mask矩阵为一个上三角矩阵,为1的地方表示需要mask的位置。
训练中我们可以一次性输入5个句子,因为所有句子的信息都是已知的。然而在测试时,我们只能串行操作,因为我们并不知道未来的句子信息。
因此,针对训练和测试,decode过程并不一样。
2.2.1 Teacher Forcing训练
训练时,decode过程如下:
for epoch in tqdm(range(epochs)):
train_loss = []
for batch_idx, (src, tgt) in enumerate(Dtr, 0):
src, tgt = src.to(args.device), tgt.to(args.device)
tgt = torch.cat([src[:, -1:, :], tgt], dim=1)
tgt_mask = torch.tril(torch.ones(tgt.size(1), tgt.size(1)), diagonal=0) == 0
#
tgt_mask = tgt_mask.to(args.device)
optimizer.zero_grad()
y_pred = model(src, tgt, tgt_mask)
loss = loss_function(y_pred[:, :-1, 0], tgt[:, 1:, 0])
train_loss.append(loss.item())
loss.backward()
optimizer.step()
其中src和tgt为我们构造的样本。src大小为(batch_size, seq_len, input_size)
,表示过去seq_len
个时刻的input_size
个变量值;tgt大小为(batch_size, output_size, input_size)
,表示未来output_size
个时刻的input_size
个变量值。我们的目标是利用过去seq_len
的所有变量信息预测未来output_size
个时刻的所有变量的信息。
在Transformer中,一开始需要输入一个起始符。在NLP中我们可以输入<bos>,在时间序列预测中,我们直接将当前tgt上一时刻的信息当做起始信息:
tgt = torch.cat([src[:, -1:, :], tgt], dim=1)
通过这一步操作后,tgt的大小变为(batch_size, tgt_len=output_size+1, input_size)
,其第一个时刻的值为src最后一个时刻也就是原始tgt上一个时刻的值。
接着,由于decode过程中,tgt中的前t个时刻只需要内部进行计算而不涉及到后续的其他时刻,因此我们需要构造tgt_mask:
tgt_mask = torch.tril(torch.ones(tgt.size(1), tgt.size(1)), diagonal=0) == 0
构造完后的tgt_mask为:
tensor([[False, True, True, True, True],
[False, False, True, True, True],
[False, False, False, True, True],
[False, False, False, False, True],
[False, False, False, False, False]], device='cuda:0')
为True的位置表示需要mask掉的位置,tgt_mask的大小为(tgt_len, tgt_len)
接着,将所有信息送入模型中即可:
y_pred = model(src, tgt, tgt_mask)
得到的y_pred大小为(batch_size, tgt_len, input_size)
。这里tgt_len=output_size+1
,最后一个位置表示对终止符的预测,需要去掉。此外,所有input_size
个变量中,第1个为我们需要预测的负荷变量,因此我们只取第一个位置:
loss = loss_function(y_pred[:, :-1, 0], tgt[:, 1:, 0])
当然,由于前边我们让tgt和src最后一个位置进行了concat,因此真正的真实值应该为tgt[:, 1:, 0]
。
值得注意的是,在Decode的过程中,我们每次都是强制输入真实值而不是让预测值接着参与预测,这种策略又叫teacher forcing,它是指在每一轮预测时,不使用上一轮预测的输出,而强制使用正确的单词。teacher forcing可以有效的避免因中间预测错误而对后续序列的预测,从而加快训练速度。而Transformer采用这个方法,为并行化训练提供了可能,因为每个时刻的输入不再依赖上一时刻的输出,而是依赖正确的样本,而正确的样本在训练集中已经全部提供了。
2.2.2 测试
测试阶段与训练不同,我们没法使用teacher forcing策略,因为我们没有正确的目标语句,t时刻的输入必然依赖t-1时刻的输出。
代码如下:
for batch_idx, (src, tgt) in enumerate(Dte, 0):
target = list(chain.from_iterable(tgt[:, :, 0].numpy().tolist()))
y.extend(target)
# greedy decode
src, tgt = src.to(args.device), tgt.to(args.device)
memory = model.encode(src)
start_symbol = src[:, -1, :]
ys = start_symbol.unsqueeze(1)
for k in range(max_len):
len_tgt = ys.shape[1]
tgt_mask = torch.tril(torch.ones(len_tgt, len_tgt), diagonal=0) == 0
tgt_mask = tgt_mask.to(args.device)
out = model.decode(tgt=ys, memory=memory, tgt_mask=tgt_mask)
out = out[:, -1:, :]
ys = torch.cat([ys, out], dim=1)
y_pred = ys[:, 1:, 0]
y_pred = list(chain.from_iterable(y_pred.data.tolist()))
pred.extend(y_pred)
y, pred = np.array(y), np.array(pred)
对于测试阶段,我们首先将src也就是前seq_len时刻的数据编码得到memory:
memory = model.encode(src)
memory大小为(batch_size, seq_len, d_model)
。接着,我们输入<bos>来得到第一个位置的预测输出,而<bos>我们使用当前tgt上一时刻的值,即src最后一个时刻的值:
start_symbol = src[:, -1, :]
ys = start_symbol.unsqueeze(1)
其中start_symbol的shape为(batch_size, input_size)
,然后ys为(batch_size, 1, input_size)
表示<bos>。
接着,我们同样需要构造tgt_mask:
tgt_mask = torch.tril(torch.ones(len_tgt, len_tgt), diagonal=0) == 0
tgt_mask的大小为(len_tgt, len_tgt)
,其中上三角的位置需要被mask掉。这是因为当我们输入,“i”,"love ","machine"来预测下一个单词时,我们只希望在预测"love"位置时,前三个单词只在内部进行attention,而不能与"machine"进行交互。一切数据都准备妥当后进行decode:
out = model.decode(tgt=ys, memory=memory, tgt_mask=tgt_mask)
得到的out大小为(batch_size, 1, input_size)
,表示第一个时刻的所有变量的预测值。然后,我们将预测值放到ys后边,当做下一轮解码时的tgt:
out = out[:, -1:, :]
ys = torch.cat([ys, out], dim=1)
此时ys的shape为(batch_size, 2, input_size)
,其中的2表示<bos>和第一个时刻的预测值。然后循环往复:
out = model.decode(tgt=ys, memory=memory, tgt_mask=tgt_mask)
此时得到的out大小为(batch_size, 2, input_size)
,表示未来2个时刻的预测值,我们只取第2个时刻的预测值out[:, -1:, :]
加入到ys中,此时ys的shape为(batch_size, 3, input_size)
,其中的3表示<bos>、第一个时刻以及第二个时刻的预测值。
重复上述过程,最后一个循环得到的out大小为(batch_size, max_len=output_size, input_size)
,表示未来output_size
个时刻的所有变量的预测值,也就是我们需要的值。当然,我们也可以选择将out的最后一个时刻的值concat到ys中,然后再将ys中start_symbol给排除掉,也可以当做预测值:
y_pred = ys[:, 1:, 0]
y_pred的大小为(batch_size, output_size)
,0表示我们只使用负荷变量的预测值。
III. 实验结果
我们使用负荷数据集,然后使用前24个时刻的所有变量未来4个时刻的负荷值。
我们使用PyTorch搭建Transformer实现多变量多步长时间序列预测(负荷预测)中的方法作为对照,其在将src进行encode后直接将memory进行flatten,然后送入一个简单的全连接层得到所有输出:
class TransformerModel(nn.Module):
def __init__(self, args):
super(TransformerModel, self).__init__()
self.args = args
self.input_fc = nn.Linear(args.input_size, args.d_model)
self.output_fc = nn.Linear(args.input_size, args.d_model)
self.pos_emb = PositionalEncoding(args.d_model)
encoder_layer = nn.TransformerEncoderLayer(
d_model=args.d_model,
nhead=4,
dim_feedforward=4 * args.input_size,
batch_first=True,
dropout=0.2,
device=device
)
decoder_layer = nn.TransformerDecoderLayer(
d_model=args.d_model,
nhead=4,
dropout=0.2,
dim_feedforward=4 * args.input_size,
batch_first=True,
device=device
)
self.encoder = torch.nn.TransformerEncoder(encoder_layer, num_layers=3)
self.decoder = torch.nn.TransformerDecoder(decoder_layer, num_layers=3)
self.fc = nn.Linear(args.output_size * args.d_model, args.output_size)
self.fc1 = nn.Linear(args.seq_len * args.d_model, args.d_model)
self.fc2 = nn.Linear(args.d_model, args.output_size)
def forward(self, x):
x = self.input_fc(x)
x = self.pos_emb(x)
x = self.encoder(x)
x = x.flatten(start_dim=1)
x = self.fc1(x)
out = self.fc2(x)
return out
最终两个结果的MAPE如下:
Encoder-only | Encoder-Decoder |
---|---|
3.51% | 5.29% |
可以发现仅仅使用Encoder效果好一点,说明Transformer的复杂结构在简单的时间序列数据上可能不太适用。