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

本文涉及的产品
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时计算 Flink 版,5000CU*H 3个月
实时数仓Hologres,5000CU*H 100GB 3个月
简介: 本文提供了一个用于解决 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

目录
相关文章
|
28天前
|
算法 PyTorch 算法框架/工具
Pytorch学习笔记(九):Pytorch模型的FLOPs、模型参数量等信息输出(torchstat、thop、ptflops、torchsummary)
本文介绍了如何使用torchstat、thop、ptflops和torchsummary等工具来计算Pytorch模型的FLOPs、模型参数量等信息。
132 2
|
30天前
|
机器学习/深度学习 自然语言处理 监控
利用 PyTorch Lightning 搭建一个文本分类模型
利用 PyTorch Lightning 搭建一个文本分类模型
51 8
利用 PyTorch Lightning 搭建一个文本分类模型
|
1月前
|
机器学习/深度学习 自然语言处理 数据建模
三种Transformer模型中的注意力机制介绍及Pytorch实现:从自注意力到因果自注意力
本文深入探讨了Transformer模型中的三种关键注意力机制:自注意力、交叉注意力和因果自注意力,这些机制是GPT-4、Llama等大型语言模型的核心。文章不仅讲解了理论概念,还通过Python和PyTorch从零开始实现这些机制,帮助读者深入理解其内部工作原理。自注意力机制通过整合上下文信息增强了输入嵌入,多头注意力则通过多个并行的注意力头捕捉不同类型的依赖关系。交叉注意力则允许模型在两个不同输入序列间传递信息,适用于机器翻译和图像描述等任务。因果自注意力确保模型在生成文本时仅考虑先前的上下文,适用于解码器风格的模型。通过本文的详细解析和代码实现,读者可以全面掌握这些机制的应用潜力。
47 3
三种Transformer模型中的注意力机制介绍及Pytorch实现:从自注意力到因果自注意力
|
2月前
|
机器学习/深度学习 PyTorch 调度
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
在深度学习中,学习率作为关键超参数对模型收敛速度和性能至关重要。传统方法采用统一学习率,但研究表明为不同层设置差异化学习率能显著提升性能。本文探讨了这一策略的理论基础及PyTorch实现方法,包括模型定义、参数分组、优化器配置及训练流程。通过示例展示了如何为ResNet18设置不同层的学习率,并介绍了渐进式解冻和层适应学习率等高级技巧,帮助研究者更好地优化模型训练。
128 4
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
|
2月前
|
机器学习/深度学习 监控 PyTorch
PyTorch 模型调试与故障排除指南
在深度学习领域,PyTorch 成为开发和训练神经网络的主要框架之一。本文为 PyTorch 开发者提供全面的调试指南,涵盖从基础概念到高级技术的内容。目标读者包括初学者、中级开发者和高级工程师。本文探讨常见问题及解决方案,帮助读者理解 PyTorch 的核心概念、掌握调试策略、识别性能瓶颈,并通过实际案例获得实践经验。无论是在构建简单神经网络还是复杂模型,本文都将提供宝贵的洞察和实用技巧,帮助开发者更高效地开发和优化 PyTorch 模型。
39 3
PyTorch 模型调试与故障排除指南
|
1月前
|
存储 并行计算 PyTorch
探索PyTorch:模型的定义和保存方法
探索PyTorch:模型的定义和保存方法
|
3月前
|
机器学习/深度学习 PyTorch 编译器
PyTorch 与 TorchScript:模型的序列化与加速
【8月更文第27天】PyTorch 是一个非常流行的深度学习框架,它以其灵活性和易用性而著称。然而,当涉及到模型的部署和性能优化时,PyTorch 的动态计算图可能会带来一些挑战。为了解决这些问题,PyTorch 引入了 TorchScript,这是一个用于序列化和优化 PyTorch 模型的工具。本文将详细介绍如何使用 TorchScript 来序列化 PyTorch 模型以及如何加速模型的执行。
112 4
|
3月前
|
机器学习/深度学习 边缘计算 PyTorch
PyTorch 与边缘计算:将深度学习模型部署到嵌入式设备
【8月更文第29天】随着物联网技术的发展,越来越多的数据处理任务开始在边缘设备上执行,以减少网络延迟、降低带宽成本并提高隐私保护水平。PyTorch 是一个广泛使用的深度学习框架,它不仅支持高效的模型训练,还提供了多种工具帮助开发者将模型部署到边缘设备。本文将探讨如何将PyTorch模型高效地部署到嵌入式设备上,并通过一个具体的示例来展示整个流程。
453 1
|
3月前
|
机器学习/深度学习 自然语言处理 PyTorch
PyTorch与Hugging Face Transformers:快速构建先进的NLP模型
【8月更文第27天】随着自然语言处理(NLP)技术的快速发展,深度学习模型已经成为了构建高质量NLP应用程序的关键。PyTorch 作为一种强大的深度学习框架,提供了灵活的 API 和高效的性能,非常适合于构建复杂的 NLP 模型。Hugging Face Transformers 库则是目前最流行的预训练模型库之一,它为 PyTorch 提供了大量的预训练模型和工具,极大地简化了模型训练和部署的过程。
157 2
|
3月前
|
机器学习/深度学习 边缘计算 PyTorch
PyTorch 与 ONNX:模型的跨平台部署策略
【8月更文第27天】深度学习模型的训练通常是在具有强大计算能力的平台上完成的,比如配备有高性能 GPU 的服务器。然而,为了将这些模型应用到实际产品中,往往需要将其部署到各种不同的设备上,包括移动设备、边缘计算设备甚至是嵌入式系统。这就需要一种能够在多种平台上运行的模型格式。ONNX(Open Neural Network Exchange)作为一种开放的标准,旨在解决模型的可移植性问题,使得开发者可以在不同的框架之间无缝迁移模型。本文将介绍如何使用 PyTorch 将训练好的模型导出为 ONNX 格式,并进一步探讨如何在不同平台上部署这些模型。
186 2