详解Transformer在时序预测中的Encoder和Decoder过程:以负荷预测为例

I. 前言

前面已经写了很多关于时间序列预测的文章:

  1. 深入理解PyTorch中LSTM的输入和输出(从input输入到Linear输出)
  2. PyTorch搭建LSTM实现时间序列预测(负荷预测)
  3. PyTorch中利用LSTMCell搭建多层LSTM实现时间序列预测
  4. PyTorch搭建LSTM实现多变量时间序列预测(负荷预测)
  5. PyTorch搭建双向LSTM实现时间序列预测(负荷预测)
  6. PyTorch搭建LSTM实现多变量多步长时间序列预测(一):直接多输出
  7. PyTorch搭建LSTM实现多变量多步长时间序列预测(二):单步滚动预测
  8. PyTorch搭建LSTM实现多变量多步长时间序列预测(三):多模型单步预测
  9. PyTorch搭建LSTM实现多变量多步长时间序列预测(四):多模型滚动预测
  10. PyTorch搭建LSTM实现多变量多步长时间序列预测(五):seq2seq
  11. PyTorch中实现LSTM多步长时间序列预测的几种方法总结(负荷预测)
  12. PyTorch-LSTM时间序列预测中如何预测真正的未来值
  13. PyTorch搭建LSTM实现多变量输入多变量输出时间序列预测(多任务学习)
  14. PyTorch搭建ANN实现时间序列预测(风速预测)
  15. PyTorch搭建CNN实现时间序列预测(风速预测)
  16. PyTorch搭建CNN-LSTM混合模型实现多变量多步长时间序列预测(负荷预测)
  17. PyTorch搭建Transformer实现多变量多步长时间序列预测(负荷预测)
  18. PyTorch时间序列预测系列文章总结(代码使用方法)
  19. TensorFlow搭建LSTM实现时间序列预测(负荷预测)
  20. TensorFlow搭建LSTM实现多变量时间序列预测(负荷预测)
  21. TensorFlow搭建双向LSTM实现时间序列预测(负荷预测)
  22. TensorFlow搭建LSTM实现多变量多步长时间序列预测(一):直接多输出
  23. TensorFlow搭建LSTM实现多变量多步长时间序列预测(二):单步滚动预测
  24. TensorFlow搭建LSTM实现多变量多步长时间序列预测(三):多模型单步预测
  25. TensorFlow搭建LSTM实现多变量多步长时间序列预测(四):多模型滚动预测
  26. TensorFlow搭建LSTM实现多变量多步长时间序列预测(五):seq2seq
  27. TensorFlow搭建LSTM实现多变量输入多变量输出时间序列预测(多任务学习)
  28. TensorFlow搭建ANN实现时间序列预测(风速预测)
  29. TensorFlow搭建CNN实现时间序列预测(风速预测)
  30. TensorFlow搭建CNN-LSTM混合模型实现多变量多步长时间序列预测(负荷预测)
  31. PyG搭建图神经网络实现多变量输入多变量输出时间序列预测
  32. PyTorch搭建GNN-LSTM和LSTM-GNN模型实现多变量输入多变量输出时间序列预测
  33. PyG Temporal搭建STGCN实现多变量输入多变量输出时间序列预测
  34. 时序预测中Attention机制是否真的有效?盘点LSTM/RNN中24种Attention机制+效果对比
  35. 详解Transformer在时序预测中的Encoder和Decoder过程:以负荷预测为例
  36. (PyTorch)TCN和RNN/LSTM/GRU结合实现时间序列预测
  37. PyTorch搭建Informer实现长序列时间序列预测
  38. PyTorch搭建Autoformer实现长序列时间序列预测
  39. 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",正常的操作过程为:

  1. 把“我/爱/机器/学习”embedding后输入到encoder里去进行编码。
  2. 将<bos>也就是开始符号作为decoder的初始输入,然后与前面encoder的输出编码做注意力机制,最终得到一个最大概率输出词A1,然后A1和‘i’做cross entropy计算error。
  3. 将<bos>,“i” 作为decoder的输入,将decoder的最大概率输出词A2和‘love’做cross entropy计算error。
  4. 以此类推,最终将,“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-onlyEncoder-Decoder
3.51%5.29%

可以发现仅仅使用Encoder效果好一点,说明Transformer的复杂结构在简单的时间序列数据上可能不太适用。

Transformer时序预测有一些特点和挑战。时间序列具有自相关性或周期性,而且预测任务可能涉及到周期非常长的序列。这些特点给Transformer时序预测的应用带来了新的挑战。为了解决这些问题,研究者们提出了一些改进的Transformer模型。 一种改进的方法是将Transformer和seasonal-trend decomposition相结合。传统的Transformer预测每个时间点时是独立的利用attention进行预测,可能会忽略时间序列整体的属性。为了解决这个问题,一种方法是在基础的Transformer引入seasonal-trend decomposition。另一种方法是引入傅里叶变换,在频域使用Transformer,帮助Transformer更好地学习全局信息。这些方法可以提高Transformer时序预测的性能和准确性。\[2\] 此外,还有一篇综述类文章《Transformers in Time Series: A Survey》介绍了Transformer在时间序列的应用。这篇文章发表于2022年,比较新,可以给大家提供更全面的了解和参考。\[3\] #### 引用[.reference_title] - *1* *2* *3* [如何搭建适合时间序列预测Transformer模型?](https://blog.csdn.net/qq_33431368/article/details/124811340)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Cyril_KI

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值