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

简介: 本文提供了一个用于解决 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

目录
相关文章
|
机器学习/深度学习 JavaScript PyTorch
9个主流GAN损失函数的数学原理和Pytorch代码实现:从经典模型到现代变体
生成对抗网络(GAN)的训练效果高度依赖于损失函数的选择。本文介绍了经典GAN损失函数理论,并用PyTorch实现多种变体,包括原始GAN、LS-GAN、WGAN及WGAN-GP等。通过分析其原理与优劣,如LS-GAN提升训练稳定性、WGAN-GP改善图像质量,展示了不同场景下损失函数的设计思路。代码实现覆盖生成器与判别器的核心逻辑,为实际应用提供了重要参考。未来可探索组合优化与自适应设计以提升性能。
1281 7
9个主流GAN损失函数的数学原理和Pytorch代码实现:从经典模型到现代变体
|
11月前
|
机器学习/深度学习 存储 PyTorch
Neural ODE原理与PyTorch实现:深度学习模型的自适应深度调节
Neural ODE将神经网络与微分方程结合,用连续思维建模数据演化,突破传统离散层的限制,实现自适应深度与高效连续学习。
1019 3
Neural ODE原理与PyTorch实现:深度学习模型的自适应深度调节
|
12月前
|
PyTorch 算法框架/工具 异构计算
PyTorch 2.0性能优化实战:4种常见代码错误严重拖慢模型
我们将深入探讨图中断(graph breaks)和多图问题对性能的负面影响,并分析PyTorch模型开发中应当避免的常见错误模式。
589 9
|
10月前
|
边缘计算 人工智能 PyTorch
130_知识蒸馏技术:温度参数与损失函数设计 - 教师-学生模型的优化策略与PyTorch实现
随着大型语言模型(LLM)的规模不断增长,部署这些模型面临着巨大的计算和资源挑战。以DeepSeek-R1为例,其671B参数的规模即使经过INT4量化后,仍需要至少6张高端GPU才能运行,这对于大多数中小型企业和研究机构来说成本过高。知识蒸馏作为一种有效的模型压缩技术,通过将大型教师模型的知识迁移到小型学生模型中,在显著降低模型复杂度的同时保留核心性能,成为解决这一问题的关键技术之一。
783 6
|
算法 PyTorch 算法框架/工具
Pytorch学习笔记(九):Pytorch模型的FLOPs、模型参数量等信息输出(torchstat、thop、ptflops、torchsummary)
本文介绍了如何使用torchstat、thop、ptflops和torchsummary等工具来计算Pytorch模型的FLOPs、模型参数量等信息。
3423 2
|
机器学习/深度学习 存储 PyTorch
PyTorch + MLFlow 实战:从零构建可追踪的深度学习模型训练系统
本文通过使用 Kaggle 数据集训练情感分析模型的实例,详细演示了如何将 PyTorch 与 MLFlow 进行深度集成,实现完整的实验跟踪、模型记录和结果可复现性管理。文章将系统性地介绍训练代码的核心组件,展示指标和工件的记录方法,并提供 MLFlow UI 的详细界面截图。
623 2
PyTorch + MLFlow 实战:从零构建可追踪的深度学习模型训练系统
|
机器学习/深度学习 PyTorch 算法框架/工具
提升模型泛化能力:PyTorch的L1、L2、ElasticNet正则化技术深度解析与代码实现
本文将深入探讨L1、L2和ElasticNet正则化技术,重点关注其在PyTorch框架中的具体实现。关于这些技术的理论基础,建议读者参考相关理论文献以获得更深入的理解。
441 4
提升模型泛化能力:PyTorch的L1、L2、ElasticNet正则化技术深度解析与代码实现
|
机器学习/深度学习 数据可视化 机器人
比扩散策略更高效的生成模型:流匹配的理论基础与Pytorch代码实现
扩散模型和流匹配是生成高分辨率数据(如图像和机器人轨迹)的先进技术。扩散模型通过逐步去噪生成数据,其代表应用Stable Diffusion已扩展至机器人学领域形成“扩散策略”。流匹配作为更通用的方法,通过学习时间依赖的速度场将噪声转化为目标分布,适用于图像生成和机器人轨迹生成,且通常以较少资源实现更快生成。 本文深入解析流匹配在图像生成中的应用,核心思想是将图像视为随机变量的实现,并通过速度场将源分布转换为目标分布。文中提供了一维模型训练实例,展示了如何用神经网络学习速度场,以及使用最大均值差异(MMD)改进训练效果。与扩散模型相比,流匹配结构简单,资源需求低,适合多模态分布生成。
1538 17
比扩散策略更高效的生成模型:流匹配的理论基础与Pytorch代码实现

推荐镜像

更多