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

本文涉及的产品
实时计算 Flink 版,5000CU*H 3个月
智能开放搜索 OpenSearch行业算法版,1GB 20LCU 1个月
实时数仓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

目录
相关文章
|
2月前
|
机器学习/深度学习 JavaScript PyTorch
9个主流GAN损失函数的数学原理和Pytorch代码实现:从经典模型到现代变体
生成对抗网络(GAN)的训练效果高度依赖于损失函数的选择。本文介绍了经典GAN损失函数理论,并用PyTorch实现多种变体,包括原始GAN、LS-GAN、WGAN及WGAN-GP等。通过分析其原理与优劣,如LS-GAN提升训练稳定性、WGAN-GP改善图像质量,展示了不同场景下损失函数的设计思路。代码实现覆盖生成器与判别器的核心逻辑,为实际应用提供了重要参考。未来可探索组合优化与自适应设计以提升性能。
135 7
9个主流GAN损失函数的数学原理和Pytorch代码实现:从经典模型到现代变体
|
25天前
|
存储 自然语言处理 PyTorch
从零开始用Pytorch实现LLaMA 4的混合专家(MoE)模型
近期发布的LLaMA 4模型引入混合专家(MoE)架构,以提升效率与性能。尽管社区对其实际表现存在讨论,但MoE作为重要设计范式再次受到关注。本文通过Pytorch从零实现简化版LLaMA 4 MoE模型,涵盖数据准备、分词、模型构建(含词元嵌入、RoPE、RMSNorm、多头注意力及MoE层)到训练与文本生成全流程。关键点包括MoE层实现(路由器、专家与共享专家)、RoPE处理位置信息及RMSNorm归一化。虽规模小于实际LLaMA 4,但清晰展示MoE核心机制:动态路由与稀疏激活专家,在控制计算成本的同时提升性能。完整代码见链接,基于FareedKhan-dev的Github代码修改而成。
59 9
从零开始用Pytorch实现LLaMA 4的混合专家(MoE)模型
|
1月前
|
机器学习/深度学习 数据可视化 机器人
比扩散策略更高效的生成模型:流匹配的理论基础与Pytorch代码实现
扩散模型和流匹配是生成高分辨率数据(如图像和机器人轨迹)的先进技术。扩散模型通过逐步去噪生成数据,其代表应用Stable Diffusion已扩展至机器人学领域形成“扩散策略”。流匹配作为更通用的方法,通过学习时间依赖的速度场将噪声转化为目标分布,适用于图像生成和机器人轨迹生成,且通常以较少资源实现更快生成。 本文深入解析流匹配在图像生成中的应用,核心思想是将图像视为随机变量的实现,并通过速度场将源分布转换为目标分布。文中提供了一维模型训练实例,展示了如何用神经网络学习速度场,以及使用最大均值差异(MMD)改进训练效果。与扩散模型相比,流匹配结构简单,资源需求低,适合多模态分布生成。
82 13
比扩散策略更高效的生成模型:流匹配的理论基础与Pytorch代码实现
|
1月前
|
机器学习/深度学习 编解码 PyTorch
从零实现基于扩散模型的文本到视频生成系统:技术详解与Pytorch代码实现
本文介绍了一种基于扩散模型的文本到视频生成系统,详细展示了模型架构、训练流程及生成效果。通过3D U-Net结构和多头注意力机制,模型能够根据文本提示生成高质量视频。
70 1
从零实现基于扩散模型的文本到视频生成系统:技术详解与Pytorch代码实现
|
4月前
|
机器学习/深度学习 搜索推荐 PyTorch
基于昇腾用PyTorch实现传统CTR模型WideDeep网络
本文介绍了如何在昇腾平台上使用PyTorch实现经典的WideDeep网络模型,以处理推荐系统中的点击率(CTR)预测问题。
305 66
|
3月前
|
机器学习/深度学习 算法 安全
用PyTorch从零构建 DeepSeek R1:模型架构和分步训练详解
本文详细介绍了DeepSeek R1模型的构建过程,涵盖从基础模型选型到多阶段训练流程,再到关键技术如强化学习、拒绝采样和知识蒸馏的应用。
406 3
用PyTorch从零构建 DeepSeek R1:模型架构和分步训练详解
|
7月前
|
算法 PyTorch 算法框架/工具
Pytorch学习笔记(九):Pytorch模型的FLOPs、模型参数量等信息输出(torchstat、thop、ptflops、torchsummary)
本文介绍了如何使用torchstat、thop、ptflops和torchsummary等工具来计算Pytorch模型的FLOPs、模型参数量等信息。
986 2
|
4月前
|
机器学习/深度学习 数据可视化 PyTorch
PyTorch FlexAttention技术实践:基于BlockMask实现因果注意力与变长序列处理
本文介绍了如何使用PyTorch 2.5及以上版本中的FlexAttention和BlockMask功能,实现因果注意力机制与填充输入的处理。通过attention-gym仓库安装相关工具,并详细展示了MultiheadFlexAttention类的实现,包括前向传播函数、因果掩码和填充掩码的生成方法。实验设置部分演示了如何组合这两种掩码并应用于多头注意力模块,最终通过可视化工具验证了实现的正确性。该方法适用于处理变长序列和屏蔽未来信息的任务。
171 17
|
9月前
|
机器学习/深度学习 并行计算 PyTorch
优化技巧与策略:提高 PyTorch 模型训练效率
【8月更文第29天】在深度学习领域中,PyTorch 是一个非常流行的框架,被广泛应用于各种机器学习任务中。然而,随着模型复杂度的增加以及数据集规模的增长,如何有效地训练这些模型成为了一个重要的问题。本文将介绍一系列优化技巧和策略,帮助提高 PyTorch 模型训练的效率。
780 0
|
5月前
|
机器学习/深度学习 人工智能 PyTorch
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers
本文探讨了Transformer模型中变长输入序列的优化策略,旨在解决深度学习中常见的计算效率问题。文章首先介绍了批处理变长输入的技术挑战,特别是填充方法导致的资源浪费。随后,提出了多种优化技术,包括动态填充、PyTorch NestedTensors、FlashAttention2和XFormers的memory_efficient_attention。这些技术通过减少冗余计算、优化内存管理和改进计算模式,显著提升了模型的性能。实验结果显示,使用FlashAttention2和无填充策略的组合可以将步骤时间减少至323毫秒,相比未优化版本提升了约2.5倍。
171 3
Transformer模型变长序列优化:解析PyTorch上的FlashAttention2与xFormers