使用 PyTorch 创建的多步时间序列预测的 Encoder-Decoder 模型

本文涉及的产品
实时数仓Hologres,5000CU*H 100GB 3个月
实时计算 Flink 版,5000CU*H 3个月
检索分析服务 Elasticsearch 版,2核4GB开发者规格 1个月
简介: 本文提供了一个用于解决 Kaggle 时间序列预测任务的 encoder-decoder 模型,并介绍了获得前 10% 结果所涉及的步骤。

Encoder-decoder 模型在序列到序列的自然语言处理任务(如语言翻译等)中提供了最先进的结果。多步时间序列预测也可以被视为一个 seq2seq 任务,可以使用 encoder-decoder 模型来处理。本文提供了一个用于解决 Kaggle 时间序列预测任务的 encoder-decoder 模型,并介绍了获得前 10% 结果所涉及的步骤。

数据集

所使用的数据集来自过去的 Kaggle 竞赛 —— Store Item demand forecasting challenge,给定过去 5 年的销售数据(从 2013 年到 2017 年)的 50 个商品来自 10 家不同的商店,预测接下来 3 个月(2018 年 1 月 1 日至 2018 年 3 月 31 日)每个商品的销售情况。这是一个多步多元的时间序列预测问题。

特征也非常的少

有500个商店组合,这意味着要预测500个时间序列。

数据预处理

深度学习模型擅长自行发现特征,因此可以将特征工程简化到最少。

从图表中可以看出,我们的数据具有每周和每月的季节性以及每年的趋势,为了捕捉这些特性,可以向模型提供DateTime 特征。为了更好地捕捉每个商品销售的年度趋势,还提供了年度自相关性。

时间的特征是有周期性的,为了将这些信息提供给模型,对 DateTime 特征应用了正弦和余弦变换。

最终的特征如下所示。

神经网络期望所有特征的值都在相同的尺度上,因此数据缩放变得必不可少。每个时间序列的值都是独立归一化的。年度自相关和年份也进行了归一化。

Encoder-decoder 模型接受一个序列作为输入并返回一个序列作为输出,所以需要将数据转为序列

输出序列的长度固定为 90 天,而输入序列的长度必须根据问题的复杂性和可用的计算资源来选择。对于这个问题,可以选择 180 天(6 个月)的输入序列长度。通过在数据集中的每个时间序列上应用滑动窗口来构建序列数据。

数据集和数据加载器

Pytorch 提供了方便的抽象 —— Dataset 和 Dataloader —— 用于将数据输入模型。Dataset 接受序列数据作为输入,并负责构建每个数据点以输入到模型中。Dataloader 则可以读取Dataset 生成批量的数据

 class StoreItemDataset(Dataset):
     def __init__(self, cat_columns=[], num_columns=[], embed_vector_size=None, decoder_input=True, ohe_cat_columns=False):
         super().__init__()
         self.sequence_data = None
         self.cat_columns = cat_columns
         self.num_columns = num_columns
         self.cat_classes = {}
         self.cat_embed_shape = []
         self.cat_embed_vector_size = embed_vector_size if embed_vector_size is not None else {}
         self.pass_decoder_input=decoder_input
         self.ohe_cat_columns = ohe_cat_columns
         self.cat_columns_to_decoder = False

     def get_embedding_shape(self):
         return self.cat_embed_shape

     def load_sequence_data(self, processed_data):
         self.sequence_data = processed_data

     def process_cat_columns(self, column_map=None):
         column_map = column_map if column_map is not None else {}
         for col in self.cat_columns:
             self.sequence_data[col] = self.sequence_data[col].astype('category')
             if col in column_map:
                 self.sequence_data[col] = self.sequence_data[col].cat.set_categories(column_map[col]).fillna('#NA#')
             else:
                 self.sequence_data[col].cat.add_categories('#NA#', inplace=True)
             self.cat_embed_shape.append((len(self.sequence_data[col].cat.categories), self.cat_embed_vector_size.get(col, 50)))

     def __len__(self):
         return len(self.sequence_data)

     def __getitem__(self, idx):
         row = self.sequence_data.iloc[[idx]]
         x_inputs = [torch.tensor(row['x_sequence'].values[0], dtype=torch.float32)]
         y = torch.tensor(row['y_sequence'].values[0], dtype=torch.float32)
         if self.pass_decoder_input:
             decoder_input = torch.tensor(row['y_sequence'].values[0][:, 1:], dtype=torch.float32)
         if len(self.num_columns) > 0:
             for col in self.num_columns:
                 num_tensor = torch.tensor([row[col].values[0]], dtype=torch.float32)
                 x_inputs[0] = torch.cat((x_inputs[0], num_tensor.repeat(x_inputs[0].size(0)).unsqueeze(1)), axis=1)
                 decoder_input = torch.cat((decoder_input, num_tensor.repeat(decoder_input.size(0)).unsqueeze(1)), axis=1)
         if len(self.cat_columns) > 0:
             if self.ohe_cat_columns:
                 for ci, (num_classes, _) in enumerate(self.cat_embed_shape):
                     col_tensor = torch.zeros(num_classes, dtype=torch.float32)
                     col_tensor[row[self.cat_columns[ci]].cat.codes.values[0]] = 1.0
                     col_tensor_x = col_tensor.repeat(x_inputs[0].size(0), 1)
                     x_inputs[0] = torch.cat((x_inputs[0], col_tensor_x), axis=1)
                     if self.pass_decoder_input and self.cat_columns_to_decoder:
                         col_tensor_y = col_tensor.repeat(decoder_input.size(0), 1)
                         decoder_input = torch.cat((decoder_input, col_tensor_y), axis=1)
             else:
                 cat_tensor = torch.tensor(
                     [row[col].cat.codes.values[0] for col in self.cat_columns],
                     dtype=torch.long
                 )
                 x_inputs.append(cat_tensor)
         if self.pass_decoder_input:
             x_inputs.append(decoder_input)
             y = torch.tensor(row['y_sequence'].values[0][:, 0], dtype=torch.float32)
         if len(x_inputs) > 1:
             return tuple(x_inputs), y
         return x_inputs[0], y

模型架构

Encoder-decoder 模型是一种用于解决序列到序列问题的循环神经网络(RNN)。

Encoder-decoder 模型由两个网络组成——编码器(Encoder)和解码器(Decoder)。编码器网络学习(编码)输入序列的表示,捕捉其特征或上下文,并输出一个向量。这个向量被称为上下文向量。解码器网络接收上下文向量,并学习读取并提取(解码)输出序列。

在编码器和解码器中,编码和解码序列的任务由一系列循环单元处理。

编码器

编码器网络的输入形状为(序列长度,特征维度),因此序列中的每个项目由 n 个值组成。在构建这些值时,不同类型的特征被不同对待。

时间依赖特征 — 这些是随时间变化的特征,如销售和 DateTime 特征。在编码器中,每个连续的时间依赖值被输入到一个 RNN 单元中。

数值特征 — 不随时间变化的静态特征,如序列的年度自相关。这些特征在序列的长度中重复,并被输入到 RNN 中。重复和合并值的过程在 Dataset 中处理。

分类特征 — 如商店 ID 和商品 ID 等特征,可以通过多种方式处理,每种方法的实现可以在 encoders.py 中找到。对于最终模型,分类变量进行了独热编码,跨序列重复,并被输入到 RNN 中,这也在 Dataset 中处理。

带有这些特征的输入序列被输入到循环网络 — GRU 中。下面给出了使用的编码器网络的代码。

 class RNNEncoder(nn.Module):
     def __init__(self, rnn_num_layers=1, input_feature_len=1, sequence_len=168, hidden_size=100, bidirectional=False, device='cpu', rnn_dropout=0.2):
         super().__init__()
         self.sequence_len = sequence_len
         self.hidden_size = hidden_size
         self.input_feature_len = input_feature_len
         self.num_layers = rnn_num_layers
         self.rnn_directions = 2 if bidirectional else 1
         self.gru = nn.GRU(
             num_layers=rnn_num_layers,
             input_size=input_feature_len,
             hidden_size=hidden_size,
             batch_first=True,
             bidirectional=bidirectional,
             dropout=rnn_dropout
         )
         self.device = device

     def forward(self, input_seq):
         ht = torch.zeros(self.num_layers * self.rnn_directions, input_seq.size(0), self.hidden_size, device=self.device)
         if input_seq.ndim < 3:
             input_seq.unsqueeze_(2)
         gru_out, hidden = self.gru(input_seq, ht)
         print(gru_out.shape)
         print(hidden.shape)
         if self.rnn_directions * self.num_layers > 1:
             num_layers = self.rnn_directions * self.num_layers
             if self.rnn_directions > 1:
                 gru_out = gru_out.view(input_seq.size(0), self.sequence_len, self.rnn_directions, self.hidden_size)
                 gru_out = torch.sum(gru_out, axis=2)
             hidden = hidden.view(self.num_layers, self.rnn_directions, input_seq.size(0), self.hidden_size)
             if self.num_layers > 0:
                 hidden = hidden[-1]
             else:
                 hidden = hidden.squeeze(0)
             hidden = hidden.sum(axis=0)
         else:
             hidden.squeeze_(0)
         return gru_out, hidden

解码器

解码器接收来自编码器的上下文向量,解码器的输入还包括未来的 DateTime 特征和滞后特征。模型中使用的滞后特征是前一年的值。使用滞后特征的原因是,鉴于输入序列仅限于 180 天,提供超出此时间的重要数据点将有助于模型。

不同于直接使用循环网络(GRU)的编码器,解码器是通过循环一个解码器单元来构建的。这是因为从每个解码器单元获得的预测作为输入传递给下一个解码器单元。每个解码器单元由一个 GRUCell 组成,其输出被输入到一个全连接层,该层提供预测。每个解码器单元的预测被组合形成输出序列。

 class DecoderCell(nn.Module):
     def __init__(self, input_feature_len, hidden_size, dropout=0.2):
         super().__init__()
         self.decoder_rnn_cell = nn.GRUCell(
             input_size=input_feature_len,
             hidden_size=hidden_size,
         )
         self.out = nn.Linear(hidden_size, 1)
         self.attention = False
         self.dropout = nn.Dropout(dropout)

     def forward(self, prev_hidden, y):
         rnn_hidden = self.decoder_rnn_cell(y, prev_hidden)
         output = self.out(rnn_hidden)
         return output, self.dropout(rnn_hidden)

Encoder-Decoder模型

下面代码将上面2个模型整合完成完整的seq2seq模型

 class EncoderDecoderWrapper(nn.Module):
     def __init__(self, encoder, decoder_cell, output_size=3, teacher_forcing=0.3, sequence_len=336, decoder_input=True, device='cpu'):
         super().__init__()
         self.encoder = encoder
         self.decoder_cell = decoder_cell
         self.output_size = output_size
         self.teacher_forcing = teacher_forcing
         self.sequence_length = sequence_len
         self.decoder_input = decoder_input
         self.device = device

     def forward(self, xb, yb=None):
         if self.decoder_input:
             decoder_input = xb[-1]
             input_seq = xb[0]
             if len(xb) > 2:
                 encoder_output, encoder_hidden = self.encoder(input_seq, *xb[1:-1])
             else:
                 encoder_output, encoder_hidden = self.encoder(input_seq)
         else:
             if type(xb) is list and len(xb) > 1:
                 input_seq = xb[0]
                 encoder_output, encoder_hidden = self.encoder(*xb)
             else:
                 input_seq = xb
                 encoder_output, encoder_hidden = self.encoder(input_seq)
         prev_hidden = encoder_hidden
         outputs = torch.zeros(input_seq.size(0), self.output_size, device=self.device)
         y_prev = input_seq[:, -1, 0].unsqueeze(1)
         for i in range(self.output_size):
             step_decoder_input = torch.cat((y_prev, decoder_input[:, i]), axis=1)
             if (yb is not None) and (i > 0) and (torch.rand(1) < self.teacher_forcing):
                 step_decoder_input = torch.cat((yb[:, i].unsqueeze(1), decoder_input[:, i]), axis=1)
             rnn_output, prev_hidden = self.decoder_cell(prev_hidden, step_decoder_input)
             y_prev = rnn_output
             outputs[:, i] = rnn_output.squeeze(1)
         return outputs

训练

模型的性能高度依赖于优化、学习率等超参数和策略

  1. 验证策略 —— 由于我们的数据是时间依赖的,交叉的训练-验证-测试分割不适用。时间依赖的训练-验证-测试分割存在一个问题,即模型没有在最近的验证数据上进行训练,这影响了模型在测试数据上的表现。为了解决这个问题,模型在过去 3 年的数据(2014 到 2016 年)上进行训练,并预测 2017 年的前 3 个月,这用于验证和实验。最终模型在 2014 到 2017 年的数据上进行训练,并预测 2018 年的前 3 个月。最终模型基于验证模型训练的学习成果,以盲模式(无验证)进行训练。
  2. 优化器 —— 使用的优化器是 AdamW,它在许多学习任务中提供了最佳结果。另一个探索的优化器是 COCOBOptimizer,它不显式设置学习率。在使用 COCOBOptimizer 训练时,我观察到它比 AdamW 尤其是在初始迭代时收敛更快。但使用 AdamW 和单周期学习得到了最佳结果。
  3. 学习率调度 —— 使用了 1cycle 学习率调度器。通过使用循环学习的学习率查找器确定了周期中的最大学习率。
  4. 损失函数 —— 使用的损失函数是均方误差损失(MSE),这与最终测试的损失 —— SMAPE 不同。MSE 损失提供了更稳定的收敛性,优于使用 SMAPE。
  5. 为编码器和解码器网络使用了不同的优化器和调度器,这带来了结果的改进。
  6. 除了权重衰减外,还在编码器和解码器中使用了 dropout 来对抗过拟合。

结果

下图显示了该模型对2018年前3个月某家商店单品的预测。

通过绘制所有商品的平均销售额,以及均值预测来去除噪声,可以更好地评估模型。下图来自验证模型对特定日期的预测,可以与实际销售数据进行比较。

这个结果在竞赛排行榜中提供前10%的排名。

总结

本文演示了使用Encoder-Decoder 模型创建多步时间序列预测的完整步骤,但是为了达到这个结果(10%),作者还做了超参数调优。并且这个模型还没有增加注意力机制,所以还可以通过探索注意机制来进一步改进模型,进一步提高模型的记忆能力,应该能获得更好的分数。

本文代码:

https://avoid.overfit.cn/post/242a897692244172ae44adc15a569647

作者:Gautham Kumaran

目录
相关文章
|
22天前
|
机器学习/深度学习 PyTorch 算法框架/工具
【从零开始学习深度学习】26.卷积神经网络之AlexNet模型介绍及其Pytorch实现【含完整代码】
【从零开始学习深度学习】26.卷积神经网络之AlexNet模型介绍及其Pytorch实现【含完整代码】
|
22天前
|
机器学习/深度学习 PyTorch 算法框架/工具
【从零开始学习深度学习】28.卷积神经网络之NiN模型介绍及其Pytorch实现【含完整代码】
【从零开始学习深度学习】28.卷积神经网络之NiN模型介绍及其Pytorch实现【含完整代码】
|
23天前
|
机器学习/深度学习 并行计算 PyTorch
使用PyTorch Profiler进行模型性能分析,改善并加速PyTorch训练
加速机器学习模型训练是工程师的关键需求。PyTorch Profiler提供了一种分析工具,用于测量CPU和CUDA时间,以及内存使用情况。通过在训练代码中嵌入分析器并使用tensorboard查看结果,工程师可以识别性能瓶颈。Profiler的`record_function`功能允许为特定操作命名,便于跟踪。优化策略包括使用FlashAttention或FSDP减少内存使用,以及通过torch.compile提升速度。监控CUDA内核执行和内存分配,尤其是避免频繁的cudaMalloc,能有效提升GPU效率。内存历史记录分析有助于检测内存泄漏和优化批处理大小。
47 1
|
13天前
|
机器学习/深度学习 算法 PyTorch
Pytorch实现线性回归模型
在机器学习和深度学习领域,线性回归是一种基本且广泛应用的算法,它简单易懂但功能强大,常作为更复杂模型的基础。使用PyTorch实现线性回归,不仅帮助初学者理解模型概念,还为探索高级模型奠定了基础。代码示例中,`creat_data()` 函数生成线性回归数据,包括噪声,`linear_regression()` 定义了线性模型,`square_loss()` 计算损失,而 `sgd()` 实现了梯度下降优化。
|
13天前
|
机器学习/深度学习 PyTorch 算法框架/工具
PyTorch中的模型创建(一)
最全最详细的PyTorch神经网络创建
|
13天前
|
机器学习/深度学习 PyTorch 算法框架/工具
|
24天前
|
机器学习/深度学习 人工智能 PyTorch
人工智能平台PAI产品使用合集之Alink是否加载预训练好的pytorch模型
阿里云人工智能平台PAI是一个功能强大、易于使用的AI开发平台,旨在降低AI开发门槛,加速创新,助力企业和开发者高效构建、部署和管理人工智能应用。其中包含了一系列相互协同的产品与服务,共同构成一个完整的人工智能开发与应用生态系统。以下是对PAI产品使用合集的概述,涵盖数据处理、模型开发、训练加速、模型部署及管理等多个环节。
|
22天前
|
机器学习/深度学习 PyTorch 算法框架/工具
【从零开始学习深度学习】27.卷积神经网络之VGG11模型介绍及其Pytorch实现【含完整代码】
【从零开始学习深度学习】27.卷积神经网络之VGG11模型介绍及其Pytorch实现【含完整代码】
|
22天前
|
机器学习/深度学习 算法 PyTorch
【从零开始学习深度学习】25.卷积神经网络之LeNet模型介绍及其Pytorch实现【含完整代码】
【从零开始学习深度学习】25.卷积神经网络之LeNet模型介绍及其Pytorch实现【含完整代码】
|
22天前
|
机器学习/深度学习 并行计算 PyTorch
【从零开始学习深度学习】20. Pytorch中如何让参数与模型在GPU上进行计算
【从零开始学习深度学习】20. Pytorch中如何让参数与模型在GPU上进行计算