Python 现代时间序列预测第二版(四)
原文:
annas-archive.org/md5/22eab741fce9c15dfad894ecf37bdd51译者:飞龙
第十四章:时间序列中的注意力与变换器
在上一章中,我们卷起袖子,实施了一些用于时间序列预测的深度学习(DL)系统。我们使用了在第十二章中讨论的常见构建块,深度学习时间序列的构建块,将它们组合成一个编码器-解码器架构,并训练它们产生我们期望的预测。
现在,让我们谈谈深度学习(DL)中的另一个关键概念,它在过去几年迅速席卷了这个领域——注意力。注意力有着悠久的历史,并最终成为了 DL 工具箱中最受欢迎的工具之一。本章将带领你从理论的角度理解注意力和变换器模型,并通过实际示例巩固这一理解。
在本章中,我们将涵盖以下主要内容:
-
什么是注意力?
-
广义注意力模型
-
使用序列到序列模型和注意力进行预测
-
变换器——注意力就是你所需要的一切
-
使用变换器进行预测
技术要求
你需要通过遵循本书前言中的说明来设置Anaconda环境,以便获得一个包含本书中所需所有库和数据集的工作环境。任何额外的库将在运行笔记本时自动安装。
本章你需要运行以下笔记本:
-
02-Preprocessing_London_Smart_Meter_Dataset.ipynb在Chapter02中 -
01-Setting_up_Experiment_Harness.ipynb在Chapter04中 -
01-Feature_Engineering.ipynb在Chapter06中 -
02-One-Step_RNN.ipynb和03-Seq2Seq_RNN.ipynb在Chapter13中(用于基准测试) -
00-Single_Step_Backtesting_Baselines.ipynb和01-Forecasting_with_ML.ipynb在Chapter08中
本章相关的代码可以在github.com/PacktPublishing/Modern-Time-Series-Forecasting-with-Python-/tree/main/notebooks/Chapter14找到。
什么是注意力?
注意力的概念灵感来自于人类的认知功能。我们眼中的视神经、鼻中的嗅神经以及耳中的听神经,时刻向大脑传送大量的感官信息。这些信息量过于庞大,显然超过了大脑的处理能力。但我们的大脑已经发展出一种机制,帮助我们只对那些重要的刺激物注意——比如一个不属于的声音或气味。经过多年的进化,我们的大脑已经被训练去挑出异常的声音或气味,因为这对于我们在野外生存至关重要,那时捕食者自由漫游。在认知科学中,注意力被定义为一种认知过程,允许个体选择性地专注于特定信息,同时忽略其他无关的刺激。
除了这种本能的注意力外,我们还可以通过所谓的集中注意力来控制我们的注意力。你现在正在做的就是通过选择忽视所有其他的刺激,专注于本书的内容。当你阅读时,手机响了,屏幕亮了,尽管书本仍然摆在你面前,你的大脑决定将注意力集中到手机屏幕上。这种人类认知功能的特性正是深度学习中注意力机制的灵感来源。赋予学习机器这种注意力的能力,已经带来了今天 AI 各个领域的巨大突破。
这个理念最早应用于深度学习中的 Seq2Seq 模型,我们在第十三章《时间序列的常见建模模式》中学习了这一点。在那一章中,我们看到了编码器和解码器之间的“握手”是如何进行的。对于递归神经网络(RNN)系列模型,我们使用序列末尾的编码器隐藏状态作为解码器的初始隐藏状态。我们把这种“握手”称为上下文。这里的假设是,解码任务所需的所有信息都被编码在上下文中,并且这是按时间步进行的。因此,对于较长的上下文窗口,第一时间步的信息必须通过多次写入和重写才能在最后一个时间步使用。这就形成了一种信息瓶颈(图 14.1),模型可能会在这个有限的上下文中难以保持重要信息。先前的隐藏状态中可能包含对解码任务有用的信息。2015 年,Bahdanau 等人(参考文献1)提出了深度学习领域中第一个已知的注意力模型。他们提出为每个对应于输入序列的隐藏状态学习注意力权重,,并在解码时将这些权重合并成一个单一的上下文向量。
在每个解码步骤中,这些注意力权重会根据解码过程中隐藏状态与输入序列中所有隐藏状态之间的相似度重新计算(图 14.2):
图 14.1:传统模型(上)与注意力模型(下)在 Seq2Seq 模型中的对比
为了更清楚地说明这一机制,我们采用一种正式的描述方式。设为编码过程中生成的隐藏状态,
为解码过程中生成的隐藏状态。上下文向量将是c[j]:
图 14.2:使用注意力机制进行解码
现在,我们有了来自编码阶段的隐藏状态(H),我们需要一种方法在每个解码步骤中使用这些信息。关键在于,在每个解码步骤中,来自不同隐藏状态的信息可能是相关的。这正是注意力权重的作用。因此,在解码步骤 j 中,我们使用 s[j][-1] 并计算注意力权重(我们很快会详细看一下如何学习注意力权重),a[i],j,使用 s[j][-1] 与 H 中每个隐藏状态的相似度。现在,我们计算上下文向量,它以正确的方式结合 H 中的信息:
我们可以使用这个上下文向量的两种主要方式,我们将在本章稍后详细讨论。这打破了传统 Seq2Seq 模型中的信息瓶颈,使得模型可以访问更大的信息池,并决定在每个解码步骤中哪些信息是相关的。
现在,让我们来看一下这些注意力权重,,是如何计算的。
通用的注意力模型
多年来,研究人员提出了不同的计算注意力权重和在深度学习模型中使用注意力的方法。Sneha Chaudhari 等人(参考文献8)发布了一篇关于注意力模型的调查论文,提出了一种通用的注意力模型,旨在将所有变体融合到一个框架中。让我们围绕这个通用框架组织我们的讨论。
我们可以将注意力模型看作是使用一组查询 q 来为一组键 K 学习一个注意力分布()。在我们上一节讨论的示例中,查询将是 S[j][-1]——解码过程中最后一个时间步的隐藏状态——而键将是 H——使用输入序列生成的所有隐藏状态。在某些情况下,生成的注意力分布被应用到另一个称为值 V 的输入集合。在许多情况下,K 和 V 是相同的,但为了保持框架的通用形式,我们将它们分开考虑。使用这个术语,我们可以将注意力模型定义为 q、K 和 V 的一个函数:
在这里,a 是一个对齐函数,它计算查询(q)和键(k[i])之间的相似性或相似度的概念,而 v[i] 是与索引 对应的值。在我们在前面一节中讨论的示例中,这个对齐函数计算的是编码器隐藏状态与解码器隐藏状态的相关性,p 是一个分布函数,它将此分数转换为总和为 1 的注意力权重。
参考文献检查:
Sneha Choudhari 等人的研究论文在参考文献部分被引用为参考文献8。
现在我们已经有了通用的注意力模型,让我们看看如何在 PyTorch 中实现它。完整实现可以在src/dl/attention.py中的Attention类中找到,但我们在这里将重点介绍其中的关键部分。
初始化该模块之前,我们所需的唯一信息是查询和键(编码器和解码器)的隐藏维度。因此,类定义和__init__函数如下所示:
class Attention(nn.Module, metaclass=ABCMeta):
def __init__(self, encoder_dim: int, decoder_dim: int):
super().__init__()
self.encoder_dim = encoder_dim
self.decoder_dim = decoder_dim
现在,我们需要定义一个forward函数,该函数接受两个输入:
-
query:大小为(batch size,decoder dimension)的查询向量,我们将用它来计算注意力权重,并用这些权重来组合键值。这是A(q,K,V)中的q。 -
key:大小为(batch size,sequence length,encoder dimension)的键向量,这是我们将计算注意力权重的隐状态序列。这是A(q,K,V)中的K。
我们假设键和值是相同的,因为在大多数情况下,它们确实是。所以,根据通用的注意力模型,我们知道我们需要执行几个步骤:
-
计算一个对齐分数—a(k[i],q)—对于每个查询和键的组合。
-
通过应用一个函数将分数转换为权重—p(a(k[i],q))。
-
使用学习到的权重来组合数值—
。
那么,让我们看看在forward方法中的这些步骤:
def forward(
self,
query: torch.Tensor, # [batch_size, decoder_dim]
values: torch.Tensor, # [batch_size, seq_length, encoder_dim]
):
scores = self._get_scores(query, values) # [batch_size, seq_length]
weights = torch.nn.functional.softmax(scores, dim=-1)
return (values*weights.unsqueeze(-1)).sum(dim=1) # [batch_size, encoder_dim]
forward方法中的三行代码对应我们之前讨论的三个步骤。第一步是计算分数,这是关键步骤,促成了许多不同类型的注意力机制,因此我们将其抽象为一个_get_scores方法,任何继承Attention类的类都必须实现此方法。在第二行,我们使用了softmax函数将分数转换为权重,最后一行则是对权重和数值进行逐元素相乘(*),并沿序列长度求和,得到加权后的值。
现在,让我们将注意力转向对齐函数。
对齐函数
随着时间的推移,已经出现了许多不同版本的对齐函数。让我们回顾一下今天常用的几种。
点积
这可能是所有对齐函数中最简单的一种。Luong 等人于 2015 年提出了这种形式的注意力机制。从线性代数中我们知道,两个向量的点积可以告诉我们一个向量在另一个向量方向上的投影量。它衡量了两个向量之间的相似性,这种相似性同时考虑了向量的大小和它们在向量空间中的夹角。因此,当我们计算查询和键向量的点积时,我们得到了一种它们之间相似性的度量。需要注意的一点是,查询和键的隐藏维度应该相同,才能应用点积注意力。正式地,相似度函数可以定义如下:
我们需要为每个键中的元素K[i]计算这个分数,K中的每个元素可以通过使用一个巧妙的矩阵乘法技巧,一次性计算出所有键的分数,而无需遍历K中的每个元素。让我们看看如何为点积注意力定义_get_scores函数。
从上一节我们知道,查询和值(在我们的情况下与键相同)分别是(batch size,decoder dimension)和(batch size,sequence length,encoder dimension)维度,在_get_scores函数中它们将分别称为q和v。在这个特殊情况下,解码器维度和编码器维度是相同的,因此分数可以通过如下方式计算:
scores = (q @ v.transpose(1,2))
这里,@是torch.matmul的简写,用于进行矩阵乘法。整个实现的名称为DotProductAttention,可以在src/dl/attention.py中找到。
缩放点积注意力
在 2017 年,Vaswani 等人提出了这种注意力机制,在开创性的论文《Attention Is All You Need》中进行了介绍。我们将在本章稍后深入探讨这篇论文,但现在,让我们先理解他们对点积注意力提出的一个关键修改。这个修改是出于这样一个考虑:当输入非常大时,我们用来将分数转换为权重的softmax 函数可能会有非常小的梯度,这使得高效学习变得困难。
这是因为softmax函数不是尺度不变的。softmax函数中的指数函数是导致这种行为的原因。因此,当我们扩大输入的尺度时,最大的输入会更加主导输出,从而限制了网络中的梯度流动。如果我们假设q和v是具有零均值和方差为 1 的d[k]维向量,那么它们的点积将具有零均值和d[k]的方差。因此,如果我们通过来缩放点积的输出,那么我们就将点积的方差重新调整为 1。因此,通过控制softmax函数输入的尺度,我们能够在网络中维持健康的梯度流动。进一步阅读部分包含了一篇博客文章链接,详细讲解了这一内容。因此,缩放点积对齐函数可以定义如下:
因此,我们需要对PyTorch实现做的唯一更改就是再增加一行:
scores = scores/math.sqrt(encoder_dim)
这已经在src/dl/attention.py中的DotProductAttention参数中实现。如果在初始化类时传递scaled=True,它将执行缩放点积注意力。我们需要记住,与点积注意力类似,缩放变种也要求查询和数值具有相同的维度。
通用注意力
2015 年,Luong 等人(参考文献2)通过在计算中引入可学习的W矩阵,提出了点积注意力的轻微变种。他们称之为通用注意力。我们可以将其视为一种注意力机制,它允许查询在计算相似度分数时,先通过W矩阵投影到一个与值/键相同维度的学习平面上,再使用点积来计算相似度分数。对齐函数可以写成如下:
相应的PyTorch实现可以在src/dl/attention.py中找到,命名为GeneralAttention。计算注意力分数的关键代码如下:
scores = (q @ self.W) @ v.transpose(1,2)
这里,self.W是一个大小为(编码器隐藏维度 x 解码器隐藏维度)的张量。通用注意力可以用于查询和键/值维度不同的情况。
加性/拼接注意力
2015 年,Bahdanau 等人提出了加性注意力,这是首次将注意力引入深度学习系统的尝试之一。与使用定义好的相似度函数(如点积)不同,Bahdanau 等人提出相似度函数可以通过学习来获得,使得网络在判断什么是相似时更加灵活。他们建议我们可以将查询和键拼接成一个张量,并使用可学习矩阵W来计算注意力分数。这个对齐函数可以写成如下:
在这里,v[t]、W[q] 和 W[k] 是可学习的矩阵。如果查询和键具有不同的隐藏维度,我们可以使用 W[q] 和 W[k] 将它们投影到同一个维度,然后对它们进行相似度计算。如果查询和键具有相同的隐藏维度,这也等同于 Luong 等人使用的注意力变体,他们称之为 concat 注意力,表示如下:
通过简单的线性代数可以看出,这两者是相同的,并且为了工程简便性,进一步阅读 部分有一个指向 Stack Overflow 解答的链接,解释了两者的等价性。
我们在 src/dl/attention.py 中的 ConcatAttention 和 AdditiveAttention 下包含了这两种实现。
对于 AdditiveAttention,计算得分的关键行如下:
q = q.repeat(1, v.size(1), 1) # [batch_size, seq_length, decoder_dim]
scores = self.W_q(q) + self.W_v(v) # [batch_size, seq_length, decoder_dim]
torch.tanh(scores) @ self.v # [batch_size, seq_length]
第一行将查询向量重复到序列长度。这只是一个线性代数技巧,用于一次性计算所有编码器隐藏状态的得分,而不是通过它们进行循环。第 2 行 使用 self.W_q 和 self.W_v 将查询和数值投影到相同的维度,第 3 行 应用 tanh 激活函数并与 self.v 进行矩阵乘法以产生最终得分。self.W_q、self.W_v 和 self.v 是可学习的矩阵,定义如下:
self.W_q = torch.nn.Linear(self.decoder_dim, self.decoder_dim)
self.W_v = torch.nn.Linear(self.encoder_dim, self.decoder_dim)
self.v = torch.nn.Parameter(torch.FloatTensor(self.decoder_dim)
ConcatAttention 中唯一的不同之处是,我们没有两个独立的权重——self.W_q 和 self.W_v——而是只有一个权重——self.W,定义如下:
self.W = torch.nn.Linear(self.decoder_dim + self.encoder_dim, self.decoder_dim)
并且我们不再添加投影(第 2 行),而是使用以下一行:
scores = self.W(
torch.cat([q, v], dim=-1)
) # [batch_size, seq_length, decoder_dim]
因此,我们可以认为 AdditiveAttention 和 ConcatAttention 执行相同的操作,但 AdditiveAttention 被调整以处理不同的编码器和解码器维度。
参考检查:
Luong 等人、Badahnau 等人和 Vaswani 等人的研究论文在 参考文献 部分分别作为参考文献 2、1 和 5 被引用。
现在我们已经了解了一些流行的对齐函数,让我们将注意力转向注意力模型的分布函数。
分布函数
分布函数的主要目标是将对齐函数中学习到的得分转换为一组加起来为 1 的权重。softmax 函数是最常用的分布函数,它将得分转换为一组加起来为 1 的权重。这也让我们能够将学习到的权重解释为概率——即相应元素是最相关的概率。
虽然softmax是最常用的选择,但它并非没有缺点。softmax的权重通常是密集的。这意味着概率质量(某些权重)会分配给我们计算注意力时序列中的每个元素。这些权重可能很低,但仍然不为零。有时我们希望在分布函数中引入稀疏性。也许我们希望确保不为某些不太可能的选项分配任何权重。也许我们希望使注意力机制更加可解释。
还有其他分布函数,如sparsemax(Martins 等人 2016 年,参考文献3)和entmax(Peters 等人 2019 年,参考文献4),它们能够将概率质量分配给一些相关元素,并将其余元素的概率设置为零。当我们知道输出仅依赖于编码器中的某些时间步时,我们可以使用这样的分布函数将这些知识编码到模型中。像Sparsemax这样的空间激活函数具有可解释性的优势,因为它们提供了一个更清晰的区分,表明哪些元素是重要的(非零概率),哪些是无关的(零概率)。
参考检查:
Martins 等人和 Peters 等人的研究论文分别在参考文献部分被引用为参考文献3和4。
现在我们已经了解了一些注意力机制,是时候将它们付诸实践了。
使用序列到序列模型和注意力机制进行预测
让我们从第十三章,时间序列的常见建模模式中接着讲,那里我们使用 Seq2Seq 模型预测了一个示例家庭的情况(如果你还没有阅读前一章,我强烈建议你现在阅读)并修改了Seq2SeqModel类以包括注意力机制。
笔记本提醒:
若要跟进完整的代码,请使用Chapter14文件夹中的01-Seq2Seq_RNN_with_Attention.ipynb笔记本以及src文件夹中的代码。
我们仍然会继承在src/dl/models.py中定义的BaseModel类,并且整体结构将与Seq2SeqModel类非常相似。关键的区别在于,在我们的新模型中,使用注意力机制时,我们不接受全连接层作为解码器。并不是因为它不可行,而是出于实现的便利性和简洁性。事实上,实现一个带有全连接解码器的 Seq2Seq 模型是你可以自己做的事情,这样可以真正内化这个概念。
类似于Seq2SeqConfig类,我们定义了一个非常相似的Seq2SeqwAttnConfig类,它具有完全相同的一组参数,但增加了一些额外的验证检查。一个验证检查是禁止使用全连接的解码器。另一个验证检查是确保解码器输入的大小允许使用注意力机制。我们稍后会详细看到这些要求。
除了 Seq2SeqwAttnConfig,我们还定义了一个 Seq2SeqwAttnModel 类来启用支持注意力的解码。这里唯一的额外参数是 attention_type,这是一个字符串类型的参数,可以取以下值:
-
dot:点积注意力 -
scaled dot:缩放点积注意力 -
general:通用注意力 -
additive:加法注意力 -
concat:拼接注意力
整个代码可以在 src/dl/models.py 中找到。我们将在书中详细讲解 forward 函数,因为这是唯一一个有关键区别的地方。类的其余部分涉及根据输入参数等定义正确的注意力模型。
编码器部分与我们在上一章看到的 SeqSeqModel 完全相同。唯一的区别是在解码部分,我们将使用注意力。
现在,让我们讨论一下我们将如何在解码中使用注意力输出。
如我之前提到的,在解码时使用注意力有两种思路。我们使用的注意力术语来看,让我们看看它们之间的区别。
Luong 等人使用解码器在步骤 j 的隐藏状态 s[j],计算它与所有编码器隐藏状态 H 之间的相似度,从而计算上下文向量 c[j]。然后,将该上下文向量 c[j] 与解码器隐藏状态 s[j] 拼接在一起,这个组合后的张量被用作输入到生成输出的线性层。
Bahdanau 等人以另一种方式使用注意力。他们使用前一时间步解码器的隐藏状态 s[j-1],并计算它与所有编码器隐藏状态 H 之间的相似度,从而计算上下文向量 c[j]。然后,这个上下文向量 c[j] 会与解码步骤 j 的输入 x[j] 拼接在一起。正是这个拼接后的输入被用在使用 RNN 的解码步骤中。
我们可以在 图 14.3 中直观地看到它们之间的区别。进一步阅读 部分还提供了关于注意力的另一精彩动画——Attn: Illustrated Attention。这也能帮助你更好地理解机制:
图 14.3:基于注意力的解码:Bahdanau 与 Luong
在我们的实现中,我们选择了 Bahdanau 解码方式,在这种方式下,我们将拼接的上下文向量和输入作为解码的输入。因此,解码器必须满足一个条件:解码器的 input_size 参数应当等于编码器的 input_size 参数与编码器的 hidden_size 参数之和。这个验证被内建在 Seq2SeqwAttnConfig 中。
以下代码块包含了所有必要的注意力解码代码,并且带有行号,这样我们可以逐行解释我们在做什么:
01 y_hat = torch.zeros_like(y, device=y.device)
02 dec_input = x[:, -1:, :]
03 for i in range(y.size(1)):
04 top_h = self._get_top_layer_hidden_state(h)
05 context = self.attention(
06 top_h.unsqueeze(1), o
07 )
08 dec_input = torch.cat((dec_input, context.unsqueeze(1)), dim=-1)
09 out, h = self.decoder(dec_input, h)
10 out = self.fc(out)
11 y_hat[:, i, :] = out.squeeze(1)
12 teacher_force = random.random() < self.hparams.teacher_forcing_ratio
13 if teacher_force:
14 dec_input = y[:, i, :].unsqueeze(1)
15 else:
16 dec_input = out
第 1 行和第 2 行与Seq2SeqModel类中的设置相同,我们在其中设置变量来存储预测,并提取传递给解码器的起始输入,第 3 行开始逐步进行解码循环。
现在,在每一步中,我们需要使用前一时间步的隐藏状态来计算上下文向量。如果你记得 RNN 的输出形状(第十二章,构建时间序列深度学习的基础),我们知道它是(层数,批量大小,隐藏大小)。但我们需要的查询隐藏状态的维度是(批量大小,隐藏大小)。Luong 等人建议使用堆叠 RNN 模型顶部层的隐藏状态作为查询,这正是我们在这里所做的:
hidden_state[-1, :, :]
如果 RNN 是双向的,我们需要稍微调整检索过程,因为现在,张量的最后两行将是来自最后一层的输出(一前一后)。有很多方式可以将它们合并为一个单一张量——我们可以将它们连接起来,或者对它们求和,甚至可以通过线性层将它们混合。这里,我们只是将它们连接起来:
torch.cat((hidden_state[-1, :, :], hidden_state[-2, :, :]), dim=-1)
现在我们有了隐藏状态,我们将其作为查询传入注意力层(第 5 行)。在第 8 行,我们将上下文与输入连接起来。第 9到第 16 行以类似于Seq2SeqModel的方式完成剩余的解码。
该笔记本训练了一个多步骤的 Seq2Seq 模型(最佳表现的变体使用教师强制)以及本章中介绍的所有不同类型的注意力机制,使用我们在上一章中开发的相同设置。结果总结如下表所示:
图 14.4:带注意力机制的 Seq2Seq 模型汇总表
我们可以看到,通过引入注意力机制,MAE、MSE和MASE都有了显著的改善,在所有注意力变体中,简单的点积注意力表现最好,其次是加性注意力。此时,可能有些人心中会有一个问题——为什么缩放点积没有比点积注意力表现得更好? 缩放应该能使点积效果更好,不是吗?
这里有一个教训(适用于所有机器学习(ML))。无论某种技术在理论上有多好,你总能找到一些例子证明它表现更差。在这里,我们只看到了一个家庭,并不奇怪我们看到缩放点积注意力没有比普通点积注意力更好。但如果我们在大规模评估中发现这是跨多个数据集的模式,那么就值得关注了。
所以,我们已经看到,注意力机制确实使得模型变得更好。大量的研究都在探讨如何利用不同形式的注意力来增强神经网络(NN)模型的性能。大部分这类研究都集中在自然语言处理(NLP)领域,特别是在语言翻译和语言建模方面。不久后,研究人员偶然发现了一个惊人的结果,这一发现极大地改变了深度学习(DL)发展的轨迹——Transformer 模型。
Transformer——注意力就是你所需要的一切
虽然引入注意力机制对 RNN 和 Seq2Seq 模型来说是一针强心剂,但它们仍然存在一个问题。RNN 是递归的,这意味着它们需要按顺序处理句子中的每个单词。
对于流行的 Seq2Seq 模型应用,如语言翻译,这意味着处理长序列的单词变得非常耗时。简而言之,很难将它们扩展到大规模的数据语料库。在 2017 年,Vaswani 等人(参考文献5)发表了一篇具有里程碑意义的论文,题为Attention Is All You Need。正如论文标题所暗示的,他们探讨了一种使用注意力(缩放点积注意力)的架构,并完全抛弃了递归网络。令全球 NLP 研究人员惊讶的是,这些被称为 Transformer 的模型在语言翻译任务中超过了当时最先进的 Seq2Seq 模型。
这激发了围绕这一新型模型类别的研究热潮,没过多久,在 2018 年,Google 的 Devlin 等人(参考文献6)开发了一种双向 Transformer,并训练了现在著名的语言模型BERT(即Bidirectional Encoder Representations from Transformers),并在多个任务中突破了许多最新的技术成果。这被认为是 Transformer 作为模型类别真正登上舞台的时刻。快进到 2022 年——Transformer 模型已经无处不在。它们几乎被应用于 NLP 中的所有任务,并且在许多其他基于序列的任务中也有所应用,比如时间序列预测和强化学习(RL)。它们还成功应用于计算机视觉(CV)任务中。
对原始 Transformer 模型进行了许多修改和适应,使其更适合时间序列预测。但让我们先讨论 Vaswani 等人 2017 年提出的原始 Transformer 架构。
注意力就是你所需要的一切
Vaswani 等人提出的模型(以下简称“原始 Transformer”)也是一个编码器-解码器模型,但编码器和解码器都是非递归的。它们完全由注意力机制和前馈网络组成。由于 Transformer 模型最初是为文本序列开发的,我们就用相同的例子来理解,然后再适应到时间序列的上下文。
为了将整个模型组合起来,需要理解模型中的几个关键组件。我们逐一来看。
自注意力
我们之前在本章中看到了缩放点积注意力是如何工作的(在对齐函数部分),但在那里,我们计算的是编码器和解码器隐藏状态之间的注意力。当我们有一个输入序列并计算该输入序列本身之间的注意力分数时,这就是自注意力。直观地说,我们可以将这个操作视为增强上下文信息,并使下游组件能够利用这些增强的信息进行进一步处理。
我们之前看过PyTorch中编码器-解码器注意力的实现,但那个实现更偏向于逐步解码 RNN。通过标准矩阵乘法一次性计算每个查询-键对的注意力分数是非常简单且对计算效率至关重要的事情。
笔记本提示:
要跟随完整的代码,请使用位于Chapter14文件夹中的笔记本02-Self-Attention_and_Multi-Headed_Attention.ipynb。
在自然语言处理中,将每个单词表示为称为嵌入的可学习向量是标准做法。这是因为文本或字符串在数学模型中没有位置。为了我们的示例,假设我们为每个单词使用大小为 512 的嵌入向量,并假设注意机制具有 64 的内部维度。让我们通过一个包含 10 个单词的句子来阐明注意机制。
在嵌入后,句子将成为一个维度为(10, 512)的张量。我们需要三个可学习的权重矩阵W[q]、W[k]和W[v]来将输入嵌入投影到注意力维度(64)。参见图 14.5:
图 14.5:自注意力层:输入句子和可学习权重
第一步操作将句子张量投影到查询、键和值,其维度等于(序列长度,注意力维度)。这是通过使用句子张量和可学习矩阵之间的矩阵乘法来实现的。参见图 14.6:
图 14.6:自注意力层:查询、键和值投影
现在我们有了查询、键和值,我们可以使用查询与键的转置之间的矩阵乘法来计算每个查询-键对的注意力权重。矩阵乘法实际上就是每个查询与每个值的点积,给出了一个大小为(序列长度,序列长度)的方阵。参见图 14.7:
图 14.7:自注意力层:查询和键之间的注意力分数
将注意力分数转换为注意力权重只是简单地进行缩放并应用softmax函数,正如我们在缩放点积注意力部分讨论过的那样。
现在我们已经得到了注意力权重,可以利用它们来结合值。通过元素级的乘法然后在权重上求和,可以通过另一种矩阵乘法高效完成。见图 14.8:
图 14.8:自注意力层:使用学习到的注意力权重结合 V
现在,我们已经看到注意力如何应用于所有查询-键对的整体矩阵运算,而不是以顺序方式对每个查询进行相同的操作。但Attention Is All You Need提出了一个更好的方法。
多头注意力
由于 Transformers 旨在摒弃整个递归架构,它们需要增强注意力机制,因为那是模型的主力。因此,论文的作者提出了多个注意力头共同作用于不同子空间。我们知道,注意力帮助模型专注于众多元素中的少数几个。多头注意力(MHA)做了同样的事情,但它关注的是不同的方面或不同的元素集,从而增加了模型的容量。如果我们想用人类思维来做个类比,我们在做决策前会考虑情况的多个方面。
比如,如果我们决定走出家门,我们会关注天气,关注时间,以确保无论我们想完成什么,都是可能的,我们会关注和你约好见面的朋友过去的守时情况,然后根据这些去决定何时离开家。你可以把这些看作是注意力的每一个头。因此,MHA 使得 Transformers 能够同时关注多个方面。
通常情况下,如果有八个头,我们会认为我们必须做上节中看到的计算八次。但幸运的是,事实并非如此。通过使用相同类型的矩阵乘法,但现在使用更大的矩阵,有巧妙的方法完成这个 MHA。让我们来看一下是如何做到的。
我们将继续使用相同的例子,看看在我们有八个注意力头的情况下会发生什么。有一个条件需要满足,以便高效计算 MHA——注意力维度应该能够被我们使用的头数整除。
最初的步骤完全相同。我们将输入句子的张量传入,并将其投影到查询、键和值。现在,我们通过进行一些基本的张量重排,将查询、键和值分割成每个头的独立查询、键和值子空间。见图 14.9:
图 14.9:多头注意力:将 Q、K 和 V 重塑为每个头的子空间
现在,我们对每个头计算注意力得分并将其与值结合,以获取每个头的注意力输出。请参见图 14.10:
图 14.10:多头注意力:计算注意力权重并结合值
我们得到了每个头的注意力输出,保存在attn_output变量中。现在,我们只需要重塑数组,将所有注意力头的输出堆叠在一个维度上。请参见图 14.11:
图 14.11:多头注意力:重塑并堆叠所有注意力头输出
通过这种方式,我们可以快速高效地执行多头注意力(MHA)。现在,让我们来看一下另一项关键创新,它使得 Transformers 能够工作。
位置信息编码
Transformer 成功地避免了递归,突破了顺序操作的性能瓶颈。这也意味着 Transformer 模型对序列的顺序不敏感。从数学角度来看,如果 RNNs 考虑将序列视为一个序列,Transformers 则将其视为一组值。对于 Transformer 来说,每个位置彼此独立,因此我们期望从处理序列的模型中获得的一个关键特征是缺失的。原始作者确实提出了一种方法,确保我们不会丢失这些信息——位置信息编码。
在后续几年的研究中,出现了许多位置信息编码的变种,但最常见的仍然是原始 Transformer 中使用的变种。
Vaswani 等人提出的解决方案是,在处理输入标记通过自注意力层之前,向每个输入标记添加一个特殊的向量,该向量通过正弦和余弦函数对位置进行数学编码。如果输入X是一个n个标记的d[model]维嵌入,位置信息编码P是一个相同大小的矩阵(n x d[model])。矩阵中pos^(行)和 2i^(列)或(2i + 1)^(列)的元素定义如下:
尽管这看起来有点复杂且违背直觉,但让我们分解一下,便于更好地理解。
从 20,000 英尺的高度来看,我们知道这些位置信息编码捕捉了位置信息,并将其添加到输入嵌入中。但为什么我们要将它们添加到输入嵌入中呢?让我们来澄清一下。假设嵌入维度只有 2(这是为了便于可视化和更好地理解概念),并且我们有一个单词,A,用这个标记表示。为了方便实验,假设在我们的序列中多次重复相同的单词,A。如果我们将位置信息编码添加到它上面会发生什么呢?
我们知道正弦或余弦函数的值在 0 和 1 之间变化。因此,我们添加到单词嵌入中的每个编码只是扰动了单词嵌入在单位圆内的位置。随着pos的增加,我们可以看到位置编码的单词嵌入在原始嵌入周围描绘一个单位圆(见图 14.12):
图 14.12:位置编码:直观理解
在图 14.12中,我们假设了一个单词A(由交叉标记表示)的随机嵌入,并且假设A处于不同的位置,添加了位置嵌入。这些位置编码向量由星号标记表示,并在旁边用数字标注了对应的位置。我们可以看到,每个位置是原始向量的一个稍微扰动的点,并且这种扰动是以顺时针方向周期性进行的。我们可以看到位置 0 位于最上方,接下来是 1、2、3,依此类推,按顺时针方向排列。通过这种表示,模型能够识别单词在不同位置的含义,并且仍然保持语义空间中的整体位置。
现在我们知道为什么要将位置编码添加到输入嵌入中,并且了解了它为何有效,让我们深入了解细节,看看正弦和余弦函数中的各个项是如何计算的。pos表示标记在序列中的位置。如果序列的最大长度是 128,pos的值从 0 到 127 变化。i表示嵌入维度中的位置,由于公式的定义方式,对于每个i值,我们有两个值——一个正弦和一个余弦。因此,i将是维度数量的一半,d[model],并且从 0 到d[model]/2 变化。
有了这些信息,我们知道正弦和余弦函数内部的项在我们接近嵌入维度的末端时趋向于 0。它还从 0 开始随着序列维度的推进而增加。对于嵌入维度中每一对(2i 和 2i+1)的位置,我们都有一个互补的正弦和余弦波,如图 14.13所示:
图 14.13:位置编码:正弦和余弦项
我们可以看到,嵌入维度40和41是具有相同频率的正弦和余弦波,而嵌入维度40和42是正弦波,频率略有增加。通过使用频率不同的正弦和余弦波组合,位置编码可以将丰富的位置信息编码为一个向量。如果我们绘制整个位置编码向量的热图(参考颜色图像文件:packt.link/gbp/9781835883181),我们可以看到值的变化及其如何编码位置信息:
图 14.14:位置编码:整个向量的热图
另一个有趣的观察是,随着我们在嵌入维度中前进,位置编码会迅速收敛到 0/1,因为正弦或余弦函数中的项(弧度角度)会由于分母过大而迅速变为零。放大的图表清晰地显示了颜色差异。
现在,让我们来看一下 Transformer 模型中的最后一个组件。
位置-wise 前馈层
我们已经在第十二章《时间序列深度学习的构建块》中讨论过前馈网络。这里唯一需要注意的是,位置-wise 前馈层是指我们在每个位置上独立地应用相同的前馈层。如果我们有 12 个位置(或单词),那么我们将有一个前馈网络来处理每个位置。
Vaswani 等人将其定义为一个两层的前馈网络,其中转换操作被定义为将输入维度扩展到四倍的输入维度,应用 ReLU 激活函数后,再将其转换回原输入维度。具体操作可以写成如下数学公式:
FFN(x) = max(0, W[1]x + b[1]) W[2] + b[2]
这里,W[1]是一个维度为(输入大小,4输入大小)的矩阵,W[2]是一个维度为(4输入大小,输入大小)的矩阵,b[1]和b[2]是相应的偏置,max(0, x)是标准的 ReLU 操作符。
有一些研究尝试将 ReLU 替换为其他激活函数,特别是门控线性单元(GLUs),这在实验中显示出了潜力。来自谷歌的 Noam Shazeer 在此方面有一篇论文,如果你想了解更多关于这些新激活函数的信息,我建议查阅他在进一步阅读部分的论文。
现在我们已经了解了 Transformer 模型的所有必要组件,接下来看看它们是如何组合在一起的。
编码器
原始的 Transformer 模型是一个编码器-解码器模型。模型包含 N 个编码器块,每个编码器块内含有一个 MHA 层,并且在其间有一个带残差连接的位置-wise 前馈层(图 14.15):
图 14.15:Vaswani 等人《Attention Is All You Need》中的 Transformer 模型
现在,让我们关注一下图 14.15的左侧部分,即编码器。编码器接收输入嵌入,并将位置编码向量加到输入中作为输入。进入 MHA 的三叉箭头表示查询(query)、键(key)和值(value)分割。MHA 的输出进入一个名为Add and Norm的块。让我们快速了解一下这个操作。
这里有两个关键操作——残差连接和层归一化。
残差连接
残差连接(或跳跃连接)是一系列引入深度学习的技术,旨在使深度网络的学习变得更加容易。该技术的主要优势在于它改善了网络中的梯度流动,从而促进了网络各部分的学习。它们在网络中引入了一个通过的记忆通道。我们已经看到一个实例,跳跃连接(尽管不是显而易见的)解决了梯度流动问题——长短时记忆网络(LSTM)。LSTM 中的细胞状态作为这个通道,让梯度能够顺利通过网络,避免了梯度消失问题。
但如今,当我们提到残差连接时,我们通常想到的是 ResNets,它通过一种卷积神经网络(CNN)架构,在深度学习历史上掀起了波澜,赢得了多个重要的图像分类挑战赛,包括 2015 年的 ImageNet 竞赛。他们引入了残差连接,以训练比当时流行的架构更深的网络。这个概念看似简单,我们来直观地理解它:
图 14.16:残差网络
假设我们有一个包含两层函数的深度学习模型,M[1]和M[2]。在常规的神经网络中,输入 x 会通过这两层,从而得到输出 y。这两个单独的函数可以看作一个将 x 转换为 y 的单一函数:y = F(x)。
在残差网络中,我们将这种范式改变为每个独立的函数(或层)仅学习输入与期望输出之间的差异。这就是残差连接名称的由来。因此,如果 h[1] 是期望输出,x 是输入,那么 M1 = h[1] - x。重写这一公式,我们得到 h[1] = M1 + x。这就是最常用的残差连接。
残差连接的诸多好处之一是它改善了梯度流动,此外它还使损失面更加平滑(Li 等,2018 年,参考文献 7),更适合基于梯度的优化。关于残差网络的更多细节和直觉,我建议你查看 Further reading 部分中的博客链接。
所以,Transformer 中的 Add and Norm 块中的 Add 实际上是残差连接。
层归一化
深度神经网络(DNNs)中的归一化一直是一个活跃的研究领域。在众多好处中,归一化能够加速训练、提高学习速率,甚至起到一定的正则化作用。批归一化是最常见的归一化技术,通常应用于计算机视觉(CV)中,它通过在当前批次中减去输入均值并除以标准差,使得输入数据的均值接近零,方差接近单位。
但在自然语言处理(NLP)中,研究人员更倾向于使用层归一化,其中归一化发生在每个特征上。可以在 图 14.17 中看到两者的区别:
图 14.17:批量归一化与层归一化
层归一化的偏好是通过经验得出的,但已经有研究探讨了这种偏好的原因。与计算机视觉(CV)数据相比,自然语言处理(NLP)数据通常具有更高的方差,而这种方差会导致批量归一化出现一些问题。另一方面,层归一化对此免疫,因为它不依赖于批量级别的方差。
无论如何,Vaswani 等人决定在他们的加法与归一化(Add and Norm)块中使用层归一化。
现在,我们知道加法与归一化块实际上就是一个残差连接,之后通过层归一化。因此,我们可以看到,位置编码的输入首先在多头自注意力(MHA)层中使用,MHA 的输出再次与位置编码的输入相加,并通过层归一化。接下来,这个输出通过位置-wise 前馈网络和另一个加法与归一化层,这就形成了编码器的一个块。一个重要的点是,编码器中所有元素的架构设计使得每个位置的输入维度在整个过程中得以保持。换句话说,如果嵌入向量的维度为 100,那么编码器的输出也将具有 100 的维度。这是一种便捷的方式,使得能够使用残差连接并尽可能堆叠多个层。现在,有多个这样的编码器块堆叠在一起,形成 Transformer 的编码器。
解码器
解码器块与编码器块非常相似,但有一个关键的区别。解码器块不仅包含单一的自注意力层,还包括一个自注意力层,该层作用于解码器输入,并且还有一个编码器-解码器注意力层。编码器-解码器注意力层在每个阶段从解码器获取查询(query),并从上层编码器块获取键(key)和值(value)。
解码器块中应用的自注意力有一些特别之处。让我们来看看到底是什么。
掩蔽自注意力
我们谈到了 Transformer 如何并行处理序列并且在计算上具有高效性。但解码的范式提出了另一个挑战。假设我们有一个输入序列,X = {x[1], x[2], …, x[n]},任务是预测下一个步骤。所以,在解码器中,如果我们给定序列X,由于并行处理架构,每个序列都会通过自注意力一次性处理。而且我们知道自注意力与序列顺序无关。如果不加限制,模型将通过使用未来时间步的信息来预测当前时间步。这就是掩蔽注意力变得重要的地方。
我们在 自注意力 部分中已经看过如何计算一个方阵(如果查询和键有相同的长度)的注意力权重,正是使用这些权重我们将信息从值向量中进行组合。这种自注意力没有时间性概念,所有的令牌都会关注所有其他令牌,而不管它们的位置。
让我们看看 图 14.18 来巩固我们的理解:
图 14.18:掩码自注意力
我们有序列,X = {x[1], x[2], …, x[5]},我们仍然尝试预测一步之遥。所以,解码器的期望输出是 。当我们使用自注意力时,学习到的注意力权重将是一个 5 X 5 的方阵。但是如果我们看方阵的上三角部分(图 14.18 中阴影部分),这些令牌组合会违反时间序列的独立性。
我们可以通过简单地添加一个预生成的掩码来解决这个问题,掩码中所有白色单元格为零,所有阴影单元格为 -inf,然后将其添加到生成的注意力能量中(即应用 softmax 之前的阶段)。这样可以确保阴影区域的注意力权重为零,从而确保在计算值向量的加权和时不使用未来的信息。
现在,为了总结所有内容,解码器的输出会传递给一个标准的任务特定头部,以生成我们期望的输出。
我们在讨论 Transformer 时是在 NLP 的背景下进行的,但将其适配到时间序列数据上是一个非常小的飞跃。
时间序列中的 Transformers
时间序列与 NLP 有很多相似之处,因为两者都涉及到序列中的信息,而且在这两种情况下,元素的顺序都很重要。在时间序列中,元素通常是按时间排序的数据点,而在 NLP 中,元素是构成句子或文档的令牌(如单词或字符)。这一点可以通过这样一个现象得到进一步验证:大多数流行的 NLP 技术很快就被适配到时间序列的上下文中,Transformers 也不例外。
我们不再查看每个位置的标记,而是每个位置都有实数。而且,我们不再谈论输入嵌入,而是可以谈论输入特征。每个时间步的特征向量可以视为 NLP 中嵌入向量的等效物。并且,我们对因果解码有严格要求,而在 NLP 中,因果解码通常是一个可选步骤(这实际上取决于任务)。因此,将 Transformer 适应时间序列是微不足道的,尽管实际上存在许多挑战,因为在时间序列中,我们通常遇到比 NLP 中更长的序列,这会带来问题,因为自注意力的复杂度随着输入序列长度的增加呈二次方增长。已经有许多替代性的自注意力提案使得在长序列中使用自注意力成为可能,我们将在第十六章《用于预测的专门化深度学习架构》中介绍其中的一些。
现在,让我们尝试将我们学到的关于 Transformer 的知识付诸实践。
使用 Transformer 进行预测
为了保持一致性,我们将使用之前用 RNN 和带注意力的 RNN 进行预测的相同家庭示例。
笔记本提醒:
要跟随完整代码,请使用 Chapter14 文件夹中的 03-Transformers.ipynb 笔记本,并使用 src 文件夹中的代码。
虽然我们学习了 vanilla Transformer 作为一个具有编码器-解码器架构的模型,但它最初是为语言翻译任务设计的。在语言翻译中,源序列和目标序列是完全不同的,因此编码器-解码器架构显得有意义。但很快,研究人员发现,仅使用 Transformer 的解码器部分也能取得良好的效果。文献中称之为解码器仅 Transformer。这个命名有点令人困惑,因为如果你思考一下,解码器和编码器有两个不同之处——掩码自注意力和编码器-解码器注意力。那么,在解码器仅 Transformer 中,我们如何弥补编码器-解码器注意力呢?简短的回答是我们不需要。解码器仅 Transformer 的架构更像是编码器块,但我们称其为解码器仅 Transformer,因为我们使用掩码自注意力来确保模型遵守序列的时间顺序。
我们还将实现一个解码器仅 Transformer。我们需要做的第一件事是定义一个配置类 TransformerConfig,并包含以下参数:
-
input_size:此参数定义了 Transformer 所期望的特征数量。 -
d_model:此参数定义了 Transformer 的隐藏维度,或所有注意力计算和后续操作发生的维度。 -
n_heads:此参数定义了 MHA 机制中有多少个头。 -
n_layers:此参数定义了我们要堆叠在一起的编码器块数量。 -
ff_multiplier:此参数定义了位置前馈层内扩展的尺度。 -
activation:此参数允许我们定义在位置前馈层中使用的激活函数,可以是relu或gelu。 -
multi_step_horizon:此参数让我们定义预测未来多少个时间步。 -
dropout:此参数允许我们定义在 Transformer 模型中应用的 dropout 正则化的大小。 -
learning_rate:定义优化过程的学习率。 -
optimizer_params、lr_scheduler、lr_scheduler_params:这些参数允许我们调整优化过程。暂时不需要担心这些,因为它们都已设置为智能默认值。
现在,我们将继承我们在src/dl/models.py中定义的BaseModel类,并定义一个TransformerModel类。
我们需要实现的第一个方法是_build_network。整个模型可以在src/dl/models.py中找到,但我们也将在这里介绍一些重要的部分。
我们需要定义的第一个模块是一个线性投影层,它接受input_size参数并将其投影到d_model:
self.input_projection = nn.Linear(
self.hparams.input_size, self.hparams.d_model, bias=False
)
这是我们为使 Transformer 适应时间序列预测范式所添加的额外步骤。在传统的 Transformer 中,这一步并不需要,因为每个词都由一个通常维度为 200 或 500 的嵌入向量表示。但是在进行时间序列预测时,我们可能需要仅使用一个特征(即历史数据)进行预测,这大大限制了我们为模型提供能力的方式,因为没有投影层时,d_model只能等于input_size。因此,我们引入了一个线性投影层,解耦了可用特征的数量和d_model。
现在,我们需要有一个模块来添加位置编码。我们已将之前看到的代码打包成一个PyTorch模块,并将其添加到src/dl/models.py中。我们只需使用该模块并定义我们的位置信息操作符,如下所示:
self.pos_encoder = PositionalEncoding(self.hparams.d_model)
我们之前说过,我们将使用仅解码器的方法来构建模型,为此,我们使用了TransformerEncoderLayer和TransformerEncoder模块,这些模块在PyTorch中已定义。只需要记住,当使用这些层时,我们将使用掩蔽自注意力,这使得它成为一个仅解码器的 Transformer。代码如下:
self.encoder_layer = nn.TransformerEncoderLayer(
d_model=self.hparams.d_model,
nhead=self.hparams.n_heads,
dropout=self.hparams.dropout,
dim_feedforward=self.hparams.d_model * self.hparams.ff_multiplier,
activation=self.hparams.activation,
batch_first=True,
)
self.transformer_encoder = nn.TransformerEncoder(
self.encoder_layer, num_layers=self.hparams.n_layers
)
我们需要定义的最后一个模块是一个线性层,它将 Transformer 的输出转换为我们预测的时间步数:
self.decoder = nn.Sequential(nn.Linear(self.hparams.d_model, 100),
nn.ReLU(),
nn.Linear(100, self.hparams.multi_step_horizon)
)
这就是模型定义的全部内容。接下来,让我们在forward方法中定义前向传播。
第一步是生成我们需要应用掩蔽自注意力的掩码:
mask = self._generate_square_subsequent_mask(x.shape[1]).to(x.device)
我们定义了掩码,使其与输入序列的长度相同。_generate_square_subsequent_mask是我们定义的方法,用于生成掩码。假设序列长度为 5,我们可以查看准备掩码的两个步骤:
mask = (torch.triu(torch.ones(5, 5)) == 1).transpose(0, 1)
torch.ones(sz,sz)会创建一个全是 1 的方阵,而torch.triu(torch.ones(sz,sz))会生成一个上三角矩阵(包括对角线),其余部分填充为 0。通过使用带有一个条件的等式运算符并进行转置,我们可以得到一个掩码,该掩码在所有下三角区域(包括对角线)中为True,其他地方为False。前面语句的输出将是这样的:
tensor([[ True, False, False, False, False],
[ True, True, False, False, False],
[ True, True, True, False, False],
[ True, True, True, True, False],
[ True, True, True, True, True]])
我们可以看到这个矩阵在所有需要掩蔽注意力的位置上是False。现在,我们只需要将所有True实例填充为0,将所有False实例填充为-inf:
mask = (
mask.float()
.masked_fill(mask == 0, float("-inf"))
.masked_fill(mask == 1, float(0.0))
)
这两行代码被封装到_generate_square_subsequent_mask方法中,我们可以在训练模型时使用它。
现在我们已经为掩蔽自注意力创建了掩码,接下来我们开始处理输入x:
# Projecting input dimension to d_model
x_ = self.input_projection(x)
# Adding positional encoding
x_ = self.pos_encoder(x_)
# Encoding the input
x_ = self.transformer_encoder(x_, mask)
# Decoding the input
y_hat = self.decoder(x_)
在这四行代码中,我们将输入投影到d_model维度,添加位置编码,通过 Transformer 模型处理,最后使用线性层将输出转换为我们想要的预测结果。
现在我们有了y_hat,它是模型的预测结果。现在我们需要思考的是如何训练这个输出,使其成为期望的输出。
我们知道 Transformer 模型一次性处理所有的 tokens,如果序列中有N个元素,那么也会有N个预测值(每个预测值对应下一个时间步)。如果每个预测值对应接下来的 H 个时间步,那么y_hat的形状将是(B, N, H),其中B是批量大小。我们可以通过几种方式使用这个输出与目标进行比较。最简单且最朴素的方法是直接使用最后一个位置的预测(它将有H个时间步)并将其与y(它也有H个时间步)进行比较。
但这并不是利用我们所有信息的最有效方式,对吧?我们丢弃了N-1个预测值,并且没有给模型提供关于这些N-1个预测的任何信号。因此,在训练时,使用这些N-1个预测是有意义的,这样模型在学习时可以得到更加丰富的反馈信号。
我们可以通过使用原始输入序列x,但是将其偏移一个位置来实现。当H=1时,我们可以将其视为一个简单的任务,其中每个位置的预测值与下一个位置(即前进一步)的目标进行比较。我们可以通过将x[:,1:,:](输入序列偏移 1)与y(原始目标)连接,并将其视为目标来轻松完成。但当H > 1 时,这变得稍微复杂,但我们仍然可以通过使用PyTorch中的一个有用函数unfold来做到这一点:
y = torch.cat([x[:, 1:, :], y], dim=1).squeeze(-1).unfold(1, y.size(1), 1)
我们首先将输入序列(偏移一个位置)与y连接起来,然后使用unfold创建大小 = H的滑动窗口。这样我们就得到了一个形状相同的目标(B,N,H)。
但是在推理过程中(当我们使用训练好的模型进行预测时),我们不需要所有其他位置的输出,因此我们会将它们丢弃,如下所示:
y_hat = y_hat[:, -1, :].unsqueeze(1)
我们定义的BaseModel类还允许我们通过使用predict方法来定义一个稍有不同的预测步骤。你可以再次查看src/dl/models.py中的完整模型,以巩固你的理解。
现在我们已经定义了模型,可以使用我们一直在使用的相同框架来训练TransformerModel。完整的代码可以在笔记本中找到,但我们将只查看一个总结表格,展示结果:
图 14.19:Transformer 模型在 MAC000193 家庭上的度量
我们可以看到模型的表现不如其 RNN 同行。造成这种情况的原因可能有很多,但最可能的原因是 Transformers 非常依赖数据。Transformers 的归纳偏差要少得多,因此只有在有大量数据可供学习时才能发挥其优势。当仅对一个家庭进行预测时,我们的模型可以访问的数据非常有限,可能效果不好。到目前为止,这对于我们看到的所有深度学习模型都在一定程度上是成立的。在第十章,全球预测模型中,我们讨论了如何训练一个可以同时处理多个家庭的模型,但那个讨论仅限于经典的机器学习模型。深度学习同样完全能够应对全球预测模型,这正是我们在下一章——第十五章,全球深度学习预测模型的策略中要讨论的内容。
现在,恭喜你完成了又一章充满概念和信息的章节。注意力机制这一席卷领域的概念,现在应该比开始时更清晰了。我建议你再花点时间重新阅读这一章,通读进一步阅读部分,如果有不清楚的地方,可以做一些自己的研究,因为未来的章节假设你理解这一内容。
总结
在过去的几章中,我们快速穿越了深度学习的世界。我们从深度学习的基本前提开始,了解了它是什么,为什么它变得如此流行。接着,我们看到了时间序列预测中常用的一些基本构件,并亲自实践了如何使用 PyTorch 将所学知识付诸实践。虽然我们讨论了 RNN、LSTM、GRU 等,但我们有意将注意力机制和 Transformers 留给了独立的章节。
本章开始时,我们学习了广义注意力模型,帮助你将所有不同的注意力方案框架化,然后详细讨论了几种常见的注意力方案,如缩放点积、加性和一般性注意力。在将注意力机制融入我们在 第十二章,时间序列深度学习构建模块 中使用的 Seq2Seq 模型后,我们开始研究 Transformer。
我们从自然语言处理的角度审视了原始 Transformer 模型中所有的构建模块和架构决策,并在理解了架构之后,将其适配到时间序列设置中。
最后,我们通过训练一个 Transformer 模型来对一个样本家庭进行预测,从而为本章画上了圆满的句号。现在,通过完成这一章,我们已经掌握了所有基本的要素,可以真正开始使用深度学习进行时间序列预测。
在下一章中,我们将提升我们一直在做的工作,并转向全球预测模型范式。
参考文献
以下是本章中使用的参考文献列表:
-
Dzmitry Bahdanau, KyungHyun Cho, 和 Yoshua Bengio (2015). 通过联合学习对齐与翻译的神经机器翻译。收录于 第三届国际学习表征会议。
arxiv.org/pdf/1409.0473.pdf -
Thang Luong, Hieu Pham, 和 Christopher D. Manning (2015). 基于注意力的神经机器翻译的有效方法。收录于 2015 年自然语言处理经验方法会议。
aclanthology.org/D15-1166/ -
André F. T. Martins, Ramón Fernandez Astudillo (2016). 从 Softmax 到 Sparsemax:一种稀疏的注意力模型及多标签分类。收录于 第 33 届国际机器学习会议论文集。
proceedings.mlr.press/v48/martins16.html -
Ben Peters, Vlad Niculae, André F. T. Martins (2019). 稀疏序列到序列模型。收录于 第 57 届计算语言学协会年会论文集。
aclanthology.org/P19-1146/ -
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser, 和 Illia Polosukhin (2017). 注意力即你所需的一切。收录于 神经信息处理系统进展。
papers.nips.cc/paper/2017/hash/3f5ee243547dee91fbd053c1c4a845aa-Abstract.html -
Jacob Devlin, Ming-Wei Chang, Kenton Lee, 和 Kristina Toutanova(2019)。BERT:深度双向 Transformer 的预训练用于语言理解。发表于 2019 年北美计算语言学协会年会论文集:人类语言技术,第 1 卷(长篇和短篇论文)。
aclanthology.org/N19-1423/ -
Hao Li, Zheng Xu, Gavin Taylor, Christoph Studer, 和 Tom Goldstein(2018)。可视化神经网络的损失景观。发表于 神经信息处理系统进展。
proceedings.neurips.cc/paper/2018/file/a41b3bb3e6b050b6c9067c67f663b915-Paper.pdf -
Sneha Chaudhari, Varun Mithal, Gungor Polatkan 和 Rohan Ramanath(2021)。注意力模型的细致调查。ACM 智能系统技术期刊,12 卷,第 5 期,第 53 号文章(2021 年 10 月)。
doi.org/10.1145/3465055
进一步阅读
以下是一些进一步阅读的资源:
-
图解 Transformer 作者:Jay Alammar:
jalammar.github.io/illustrated-transformer/ -
Transformer 网络:一个数学解释,为什么缩放点积会导致更稳定的梯度:
towardsdatascience.com/transformer-networks-a-mathematical-explanation-why-scaling-the-dot-products-leads-to-more-stable-414f87391500 -
为什么 Bahdanau 的注意力有时被称为拼接注意力?:
stats.stackexchange.com/a/524729 -
Noam Shazeer(2020)。GLU 变种改进 Transformer。arXiv 预印本:Arxiv-2002.05202。
arxiv.org/abs/2002.05202 -
什么是残差连接? 作者:Wanshun Wong:
towardsdatascience.com/what-is-residual-connection-efb07cab0d55 -
Attn: 图解注意力 作者:Raimi Karim:
towardsdatascience.com/attn-illustrated-attention-5ec4ad276ee3
加入我们的 Discord 社区
加入我们社区的 Discord 空间,与作者和其他读者讨论:
第十五章:全球深度学习预测模型策略
在过去的几章中,我们一直在建立时间序列预测的深度学习模型。我们从深度学习的基础开始,了解了不同的构建模块,并实际使用了其中一些构建模块来对一个样本家庭进行预测,最后讨论了注意力和 Transformer。现在,让我们稍微改变方向,看看全球深度学习模型。在第十章“全球预测模型”中,我们看到了为什么全球模型是有意义的,还看到了如何在机器学习背景下使用这些模型。我们在实验中甚至得到了良好的结果。在本章中,我们将探讨如何从深度学习的角度应用类似的概念。我们将探讨可以使全球深度学习模型更好运作的不同策略。
在本章中,我们将涵盖以下主要内容:
-
创建全球深度学习预测模型
-
使用时变信息
-
使用静态/元信息
-
使用时间序列的规模
-
平衡采样过程
技术要求
你需要按照前言中的说明设置Anaconda环境,以便获取本书代码所需的所有库和数据集的工作环境。在运行笔记本时,会安装任何额外的库。
你将需要运行这些笔记本:
-
02-Preprocessing_London_Smart_Meter_Dataset.ipynb在Chapter02 -
01-Setting_up_Experiment_Harness.ipynb在Chapter04 -
01-Feature_Engineering.ipynb在Chapter06
本章的相关代码可以在github.com/PacktPublishing/Modern-Time-Series-Forecasting-with-Python-/tree/main/notebooks/Chapter15找到。
创建全球深度学习预测模型
在第十章“全球预测模型”中,我们详细讨论了为什么全球模型是有意义的。我们详细讨论了增加样本大小、跨学习、多任务学习以及与之相关的正则化效应,以及减少的工程复杂性的好处。所有这些对深度学习模型同样适用。工程复杂性和样本大小变得更为重要,因为深度学习模型对数据需求量大,并且比其他机器学习模型需要更多的工程工作和训练时间。在深度学习背景下,我认为在大多数需要大规模预测的实际情况中,全球模型是唯一有意义的深度学习范式。
那么,为什么我们要花这么多时间研究单一模型呢?其实,从这个层面上理解概念更容易,而且我们在这个层面获得的技能和知识非常容易转移到全球建模的范式中。在第十三章,时间序列的常见建模模式中,我们看到如何使用数据加载器从单一时间序列中抽样窗口来训练模型。为了使模型成为一个全球模型,我们需要做的就是改变数据加载器,使其不再从单一时间序列中抽样窗口,而是从多个时间序列中抽样。这个抽样过程可以看作是一个两步走的过程(尽管在实践中我们是一气呵成的,但从直观上讲它是两个步骤)—首先,从需要选择窗口的时间序列中抽样,然后,从该时间序列中抽样窗口。通过这样做,我们正在训练一个单一的深度学习模型来一起预测所有时间序列。
为了让我们的生活更轻松,本文将使用开源库 PyTorch Forecasting 和 Nixtla 的 neuralforecast。我们将出于教学目的使用 PyTorch Forecasting,因为它提供了更多的灵活性,但 neuralforecast 更为现代且在积极维护,因此更近期的架构将添加到该库中。在第十六章中,我们将看到如何使用 neuralforecast 进行预测,但现在让我们选择 PyTorch Forecasting 继续前进。
PyTorch Forecasting 旨在使深度学习时间序列预测对于研究和实际应用都变得更加简便。PyTorch Forecasting 还实现了一些最先进的预测架构,我们将在第十六章中回顾这些架构,标题为专用深度学习架构用于预测。但现在,让我们使用 PyTorch Forecasting 的高级 API。这样可以大大减少我们在准备 PyTorch 数据集时的工作量。PyTorch Forecasting 中的 TimeSeriesDataset 类处理了许多样板代码,涉及不同的转换、缺失值、填充等问题。在本章中,我们将使用这个框架来探讨实现全球深度学习预测模型的不同策略。
笔记本提示:
要完整跟随代码,请使用 Chapter15 文件夹中的名为 01-Global_Deep_Learning_Models.ipynb 的笔记本。笔记本中有两个变量作为开关—TRAIN_SUBSAMPLE = True 会使笔记本仅在 10 个家庭的子集上运行。train_model = True 会使笔记本训练不同的模型(警告:在完整数据集上训练模型需要超过 3 小时)。train_model = False 会加载训练好的模型权重并进行预测。
数据预处理
我们从加载必要的库和数据集开始。我们使用的是在第六章中创建的经过预处理和特征工程处理的数据集,时间序列预测的特征工程。数据集中有不同种类的特征,为了使我们的特征分配标准化,我们使用namedtuple。namedtuple()是collections中的一个工厂方法,允许你创建带有命名字段的tuple子类。这些命名字段可以通过点表示法进行访问。我们这样定义namedtuple:
from collections import namedtuple
FeatureConfig = namedtuple(
"FeatureConfig",
[
"target",
"index_cols",
"static_categoricals",
"static_reals",
"time_varying_known_categoricals",
"time_varying_known_reals",
"time_varying_unknown_reals",
"group_ids"
],
)
让我们快速了解一下这些名称的含义:
-
target:我们尝试预测的目标的列名。 -
index_cols:我们需要将这些列设置为索引,以便快速访问数据。 -
static_categoricals:这些列是分类性质的,并且不会随着时间变化。它们是每个时间序列特有的。例如,我们数据集中的Acorn 组是static_categorical,因为它是分类性质的,并且是一个与家庭相关的值。 -
static_reals:这些列是数值型的,并且随着时间的推移不会变化。它们是每个时间序列特有的。例如,我们数据集中的平均能耗是数值型的,且只适用于单个家庭。 -
time_varying_known_categoricals:这些列是分类性质的,并且随着时间变化,并且我们知道未来的值。它们可以视为随着时间不断变化的量。一个典型的例子是节假日,它是分类的,且随时间变化,我们知道未来的节假日。 -
time_varying_known_reals:这些列是数值型的,会随着时间变化,并且我们知道未来的值。一个典型的例子是温度,它是数值型的,随着时间变化,并且我们知道未来的值(前提是我们获取天气数据的来源也提供了未来天气的预报数据)。 -
time_varying_unknown_reals:这些列是数值型的,会随着时间变化,并且我们不知道未来的值。我们尝试预测的目标就是一个很好的例子。 -
group_ids:这些列唯一标识数据框中的每个时间序列。
一旦定义好,我们可以为这些名称分配不同的值,如下所示:
feat_config = FeatureConfig(
target="energy_consumption",
index_cols=["LCLid", "timestamp"],
static_categoricals=[
"LCLid",
"stdorToU",
"Acorn",
"Acorn_grouped",
"file",
],
static_reals=[],
time_varying_known_categoricals=[
"holidays",
"timestamp_Dayofweek",
],
time_varying_known_reals=["apparentTemperature"],
time_varying_unknown_reals=["energy_consumption"],
group_ids=["LCLid"],
)
在neuralforecast中,问题设置的方式略有不同,但原理是相同的。我们定义的不同类型的变量在概念上保持一致,只是我们用来定义它们的参数名称不同。PyTorch Forecasting 需要将目标包含在time_varying_unknown_reals中,而neuralforecast则不需要。这些细微的差异将在我们使用neuralforecast生成预测时进行详细说明。
我们可以看到,我们没有像在机器学习模型中那样使用所有特征(第十章,全球预测模型)。这样做有两个原因:
-
由于我们使用的是顺序深度学习模型,因此我们尝试通过滚动特征等捕捉的许多信息,模型已经能够自动获取。
-
与强大的梯度提升决策树模型不同,深度学习模型对噪声的鲁棒性较差。因此,无关特征会使模型表现变差。
为了使我们的数据集与 PyTorch Forecasting 兼容,有几个预处理步骤是必需的。PyTorch Forecasting 需要一个连续的时间索引作为时间的代理。虽然我们有一个 timestamp 列,但它包含的是日期时间。因此,我们需要将其转换为一个新的列 time_idx。完整的代码可以在笔记本中找到,但代码的核心思想很简单。我们将训练集和测试集的 DataFrame 合并,并使用 timestamp 列中的公式推导出新的 time_idx 列。这个公式是这样的:每个连续的时间戳递增 1,并且在 train 和 test 之间保持一致。例如,train 中最后一个时间步的 time_idx 是 256,而 test 中第一个时间步的 time_idx 将是 257。此外,我们还需要将类别列转换为 object 数据类型,以便与 PyTorch Forecasting 中的 TimeSeriesDataset 良好配合。
对于我们的实验,我们选择了 2 天(96 个时间步)作为窗口,并预测一个时间步的未来。为了启用提前停止,我们还需要一个验证集。提前停止是一种正则化方法(防止过拟合的技术,第五章),它通过监控验证损失,当验证损失开始上升时停止训练。我们选择了训练的最后一天(48 个时间步)作为验证数据,并选择了 1 个月作为最终测试数据。但在准备这些 DataFrame 时,我们需要注意一些问题:我们选择了两天作为历史数据,而为了预测验证集或测试集中的第一个时间步,我们需要将过去两天的历史数据一同考虑进去。因此,我们按照下图所示的方式划分 DataFrame(具体代码在笔记本中):
图 15.1:训练-验证-测试集划分
现在,在使用 TimeSeriesDataset 处理我们的数据之前,让我们先了解它的作用以及涉及的不同参数。
理解 PyTorch Forecasting 中的 TimeSeriesDataset
TimeSeriesDataset 自动化以下任务及更多:
-
对数值特征进行缩放并编码类别特征:
-
对数值特征进行缩放,使其具有相同的均值和方差,有助于基于梯度下降的优化方法更快且更好地收敛。
-
类别特征需要编码为数字,以便我们能够在深度学习模型中正确处理它们。
-
-
归一化目标变量:
- 在全局模型上下文中,目标变量对于不同的时间序列可能具有不同的尺度。例如,某个家庭通常有较高的能源消耗,而其他一些家庭可能是空置的,几乎没有能源消耗。将目标变量缩放到一个单一的尺度有助于深度学习模型专注于学习模式,而不是捕捉尺度上的方差。
-
高效地将 DataFrame 转换为 PyTorch 张量字典:
- 数据集还会接受关于不同列的信息,并将 DataFrame 转换为 PyTorch 张量的字典,分别处理静态信息和随时间变化的信息。
这些是TimeSeriesDataset的主要参数:
-
data:这是包含所有数据的 pandas DataFrame,每一行通过time_idx和group_ids进行唯一标识。 -
time_idx:这指的是我们之前创建的连续时间索引的列名。 -
target、group_ids、static_categoricals、static_reals、time_varying_known_categoricals、time_varying_known_reals、time_varying_unknown_categoricals和time_varying_unknown_reals:我们已经在数据预处理部分讨论过所有这些参数,它们的含义相同。 -
max_encoder_length:设置给定编码器的最大窗口长度。 -
min_decoder_length:设置解码上下文中给定的最小窗口长度。 -
target_normalizer:这是一个 Transformer,用于对目标进行标准化。PyTorch Forecasting 内置了几种标准化器——TorchNormalizer、GroupNormalizer和EncoderNormalizer。TorchNormalizer对整个目标进行标准化和鲁棒性缩放,而GroupNormalizer则对每个组分别进行相同的处理(组是由group_ids定义的)。EncoderNormalizer在运行时根据每个窗口中的值进行标准化。 -
categorical_encoders:该参数接受一个字典,字典中的值是 scikit-learn 的 Transformer,作为类别编码器。默认情况下,类别编码类似于LabelEncoder,它将每个独特的类别值替换为一个数字,并为未知值和NaN值添加额外的类别。
完整文档请参阅 pytorch-forecasting.readthedocs.io/en/stable/data.html#time-series-data-set。
初始化 TimeSeriesDataset
现在我们知道了主要参数,接下来用我们的数据初始化一个时间序列数据集:
training = TimeSeriesDataSet(
train_df,
time_idx="time_idx",
target=feat_config.target,
group_ids=feat_config.group_ids,
max_encoder_length=max_encoder_length,
max_prediction_length=max_prediction_length,
time_varying_unknown_reals=[
"energy_consumption",
],
target_normalizer=GroupNormalizer(
groups=feat_config.group_ids, transformation=None
)
)
请注意,我们使用了GroupNormalizer,使得每个家庭根据其自身的均值和标准差分别进行缩放,使用的是以下著名的公式:
TimeSeriesDataset还使得声明验证和测试数据集变得更加容易,通过工厂方法from_dataset。它接受另一个时间序列数据集作为参数,并使用相同的参数、标准化器等,创建新的数据集:
# Defining the validation dataset with the same parameters as training
validation = TimeSeriesDataSet.from_dataset(training, pd.concat([val_history,val_df]).reset_index(drop=True), stop_randomization=True)
# Defining the test dataset with the same parameters as training
test = TimeSeriesDataSet.from_dataset(training, pd.concat([hist_df, test_df]).reset_index(drop=True), stop_randomization=True)
请注意,我们将历史数据连接到val_df和test_df中,以确保可以在整个验证和测试期间进行预测。
创建数据加载器
剩下的工作就是从TimeSeriesDataset创建数据加载器:
train_dataloader = training.to_dataloader(train=True, batch_size=batch_size, num_workers=0)
val_dataloader = validation.to_dataloader(train=False, batch_size=batch_size, num_workers=0)
在我们继续之前,让我们通过一个示例巩固我们对 PyTorch Forecasting 数据加载器的理解。我们刚创建的train数据加载器已经将数据框拆分成了一个 PyTorch 张量的字典。我们选择了512作为批次大小,并可以使用以下代码检查数据加载器:
# Testing the dataloader
x, y = next(iter(train_dataloader))
print("\nsizes of x =")
for key, value in x.items():
print(f"\t{key} = {value.size()}")
print("\nsize of y =")
print(f"\ty = {y[0].size()}")
我们将得到如下的输出:
图 15.2:批量数据加载器中张量的形状
我们可以看到,数据加载器和TimeSeriesDataset已经将数据框拆分为 PyTorch 张量,并将它们打包进一个字典中,编码器和解码器序列被分开。我们还可以看到,类别特征和连续特征也被分开了。
我们将使用这个字典中的主要键是encoder_cat、encoder_cont、decoder_cat和decoder_cont。encoder_cat和decoder_cat这两个键的维度为零,因为我们没有声明任何类别特征。
可视化数据加载器的工作原理
让我们尝试更深入地剖析这里发生了什么,并通过视觉化的方式理解TimeSeriesDataset所做的事情:
图 15.3:TimeSeriesDataset——它是如何工作的示意图
假设我们有一个时间序列,x[1] 到 x[6](这将是目标以及TimeSeriesDataset术语中的time_varying_unknown)。我们有一个时间变化的实数,f[1] 到 f[6],和一个时间变化的类别,c[1] 到 c[2]。除此之外,我们还拥有一个静态实数,r,和一个静态类别,s。如果我们将编码器和解码器的长度设置为3,那么我们将得到如图 15.3所示的张量。请注意,静态类别和实数在所有时间步长上都被重复。这些不同的张量构造是为了让模型的编码器能够使用编码器张量进行训练,而解码器张量则在解码过程中使用。
现在,让我们开始构建我们的第一个全局模型。
构建第一个全局深度学习预测模型
PyTorch Forecasting 使用 PyTorch 和 PyTorch Lightning 在后台定义和训练深度学习模型。可以与 PyTorch Forecasting 无缝配合使用的模型本质上是 PyTorch Lightning 模型。然而,推荐的做法是从 PyTorch Forecasting 继承 BaseModel。PyTorch Forecasting 的开发者提供了出色的文档和教程,帮助新用户按照自己的需求使用它。这里值得一提的一个教程名为 如何使用自定义数据并实现自定义模型和指标(链接在 进一步阅读 部分)。
我对教程中的基础模型做了一些修改,使其更加灵活。实现代码可以在 src/dl/ptf_models.py 文件中找到,名为 SingleStepRNNModel。该类接收两个参数:
-
network_callable:这是一个可调用对象,当初始化时,它将成为一个 PyTorch 模型(继承自nn.Module)。 -
model_params:这是一个字典,包含初始化network_callable所需的所有参数。
结构相当简单。__init__ 函数将 network_callable 初始化为一个 PyTorch 模型,并将其存储在 network 属性下。forward 函数将输入传递给网络,格式化返回的输出,使其符合 PyTorch Forecasting 的要求,并返回结果。这个模型非常简短,因为大部分工作都由 BaseModel 完成,它负责处理损失计算、日志记录、梯度下降等任务。我们通过这种方式定义模型的好处是,现在我们可以定义标准的 PyTorch 模型,并将其传递给这个模型,使其能够与 PyTorch Forecasting 配合得很好。
除此之外,我们还定义了一个抽象类 SingleStepRNN,它接收一组参数并初始化由这些参数指定的相应网络。如果参数指定了一个两层的 LSTM,那么它将会被初始化,并保存在 rnn 属性下。它还在 fc 属性下定义了一个全连接层,将 RNN 的输出转化为预测结果。forward 方法是一个抽象方法,任何继承该类的子类都需要重写这个方法。
定义我们的第一个 RNN 模型
现在我们已经完成了必要的设置,让我们定义第一个继承 SingleStepRNN 类的模型:
class SimpleRNNModel(SingleStepRNN):
def __init__(
self,
rnn_type: str,
input_size: int,
hidden_size: int,
num_layers: int,
bidirectional: bool,
):
super().__init__(rnn_type, input_size, hidden_size, num_layers, bidirectional)
def forward(self, x: Dict):
# Using the encoder continuous which has the history window
x = x["encoder_cont"] # x --> (batch_size, seq_len, input_size)
# Processing through the RNN
x, _ = self.rnn(x) # --> (batch_size, seq_len, hidden_size)
# Using a FC layer on last hidden state
x = self.fc(x[:,-1,:]) # --> (batch_size, seq_len, 1)
return x
这是最直接的实现方式。我们从字典中取出 encoder_cont,并将其传递给 RNN,然后在 RNN 的最后一个隐藏状态上使用全连接层来生成预测。如果我们以 图 15.3 中的示例为例,我们使用 x[1] 到 x[3] 作为历史数据,并训练模型预测 x[4](因为我们使用了 min_decoder_length=1,所以解码器和目标中只有一个时间步)。
初始化 RNN 模型
现在,让我们使用一些参数初始化模型。我为参数定义了两个字典:
-
model_params:这包含了初始化SingleStepRNN模型所需的所有参数。 -
other_params:这些是我们传递给SingleStepRNNModel的所有参数,如learning_rate、loss等。
现在,我们可以使用 PyTorch Forecasting 模型支持的工厂方法from_dataset进行初始化。这个工厂方法允许我们传入数据集,并从数据集中推断一些参数,而不需要每次都填入所有内容:
model = SingleStepRNNModel.from_dataset(
training,
network_callable=SimpleRNNModel,
model_params=model_params,
**other_params
)
训练 RNN 模型
训练模型就像我们在前几章中所做的那样,因为这是一个 PyTorch Lightning 模型。我们可以按照以下步骤进行:
-
使用早期停止和模型检查点初始化训练器:
trainer = pl.Trainer( auto_select_gpus=True, gpus=-1, min_epochs=1, max_epochs=20, callbacks=[ pl.callbacks.EarlyStopping(monitor="val_loss", patience=4*3), pl.callbacks.ModelCheckpoint( monitor="val_loss", save_last=True, mode="min", auto_insert_metric_name=True ), ], val_check_interval=2000, log_every_n_steps=2000, ) -
拟合模型:
trainer.fit( model, train_dataloaders=train_dataloader, val_dataloaders=val_dataloader, ) -
训练完成后加载最佳模型:
best_model_path = trainer.checkpoint_callback.best_model_path best_model = SingleStepRNNModel.load_from_checkpoint(best_model_path)
训练可能需要一些时间。为了节省您的时间,我已包含了我们使用的每个模型的训练权重,如果train_model标志为False,则会跳过训练并加载保存的权重。
使用训练好的模型进行预测
现在,训练完成后,我们可以在test数据集上进行预测,方法如下:
pred, index = best_model.predict(test, return_index=True, show_progress_bar=True)
我们将预测结果存储在一个 DataFrame 中,并使用我们的标准指标进行评估:MAE、MSE、meanMASE和Forecast Bias。让我们看看结果:
图 15.4:使用基线全局模型汇总结果
这是一个不太好的模型,因为我们从第十章《全局预测模型》中知道,基线全局模型使用 LightGBM 的结果如下:
-
MAE = 0.079581
-
MSE = 0.027326
-
meanMASE = 1.013393
-
预测偏差 = 28.718087
除了预测偏差,我们的全局模型与最佳模型相差甚远。我们将全局机器学习模型称为GFM(ML),将当前模型称为GFM(DL),并在接下来的讨论中使用这两个术语。现在,让我们开始探索一些策略,改善全局模型。
使用时变信息
GFM(ML)使用了所有可用的特征。因此,显然,该模型比我们目前构建的 GFM(DL)访问了更多的信息。我们刚刚构建的 GFM(DL)只使用了历史数据,其他的都没有。让我们通过加入时变信息来改变这一点。这次我们只使用时变的真实特征,因为处理类别特征是我希望留到下一节讨论的话题。
我们以与之前相同的方式初始化训练数据集,但在初始化参数中加入了time_varying_known_reals=feat_config.time_varying_known_reals。现在我们已经创建了所有数据集,让我们继续设置模型。
为了设置模型,我们需要理解一个概念。我们现在使用目标的历史数据和时变已知特征。在图 15.3中,我们看到TimeSeriesDataset如何将不同类型的变量安排成 PyTorch 张量。在上一节中,我们只使用了encoder_cont,因为没有其他变量需要考虑。但现在,我们有了时变变量,这增加了复杂性。如果我们退一步思考,在单步预测的上下文中,我们可以看到时变变量和目标的历史数据不能具有相同的时间步。
让我们使用一个视觉示例来阐明:
图 15.5:使用时变变量进行训练
延续图 15.3示例的精神,但将其简化以适应我们的上下文,我们有一个时间序列,x[1] 到 x[4],以及一个时变的真实变量,f[1] 到 f[4]。所以,对于max_encoder_length=3和min_decoder_length=1,我们会让TimeSeriesDataset生成张量,如图 15.5中的步骤 1所示。
现在,对于每个时间步,我们有时变变量f和历史数据x在encoder_cont中。时变变量f是我们也知道未来值的变量,因此对该变量没有因果约束。这意味着对于预测时间步t,我们可以使用f[t],因为它是已知的。然而,目标变量的历史数据则不是。我们无法知道未来的值,因为它正是我们想要预测的量。这意味着x上有因果约束,因此我们不能使用x[t]来预测时间步t。但是目前张量的形成方式是,f和x在时间步上是对齐的,如果我们将它们输入到模型中,就相当于作弊,因为我们会使用x[t]来预测时间步t。理想情况下,历史数据x和时变特征f之间应该有一个偏移量,这样在时间步t时,模型看到的是x[t][-1],然后看到f[t],然后预测x[t]。
为了实现这一点,我们执行以下操作:
-
将
encoder_cont和decoder_cont连接起来,因为我们需要使用f[4]来预测时间步t = 4(图 15.5中的步骤 2)。 -
将目标历史数据x向前移动一个时间步,使得f[t]和x[t][-1]对齐(图 15.5中的步骤 3)。
-
去掉第一个时间步,因为我们没有与第一个时间步相关的历史数据(在图 15.5中的步骤 4)。
这正是我们在新模型DynamicFeatureRNNModel的forward方法中需要实现的内容:
def forward(self, x: Dict):
# Step 2 in Figure 15.5
x_cont = torch.cat([x["encoder_cont"],x["decoder_cont"]], dim=1)
# Step 3 in Figure 15.5
x_cont[:,:,-1] = torch.roll(x_cont[:,:,-1], 1, dims=1)
x = x_cont
# Step 4 in Figure 15.5
x = x[:,1:,:] # x -> (batch_size, seq_len, input_size)
# Processing through the RNN
x, _ = self.rnn(x) # --> (batch_size, seq_len, hidden_size)
# Using a FC layer on last hidden state
x = self.fc(x[:,-1,:]) # --> (batch_size, seq_len, 1)
return x
现在,让我们训练这个新模型,看看它的表现。具体的代码在笔记本中,和之前完全相同:
图 15.6:使用时变特征汇总结果
看起来温度作为特征确实使模型稍微改善了一些,但还有很长的路要走。别担心,我们还有其他特征可以使用。
使用静态/元信息
有些特征,如橡子组、是否启用动态定价等,特定于某个家庭,这将帮助模型学习特定于这些组的模式。自然地,包含这些信息是有直觉意义的。
然而,正如我们在第十章《全球预测模型》中讨论的,分类特征与机器学习模型的配合不太好,因为它们不是数值型的。在那一章中,我们讨论了几种将分类特征编码为数值表示的方法。这些方法同样适用于深度学习模型。但有一种处理分类特征的方法是深度学习模型特有的——嵌入向量。
独热编码及其为何不理想
将分类特征转换为数值表示的方法之一是独热编码。它将分类特征编码为一个更高维度,将分类值等距地放置在该空间中。它需要的维度大小等于分类变量的基数。有关独热编码的详细讨论,请参阅第十章《全球预测模型》。
在对分类特征进行独热编码后,我们得到的表示被称为稀疏表示。如果分类特征的基数(唯一值的数量)是C,那么每一行代表分类特征的一个值时,将有C - 1 个零。因此,该表示大部分是零,因此称为稀疏表示。这导致有效编码一个分类特征所需的总体维度等于向量的基数。因此,对一个拥有 5,000 个唯一值的分类特征进行独热编码会立刻给你要解决的问题添加 5,000 个维度。
除此之外,独热编码也是完全没有信息的。它将每个分类值放置在相等的距离之内,而没有考虑这些值之间可能的相似性。例如,如果我们要对一周的每一天进行编码,独热编码会将每一天放在一个完全不同的维度中,使它们彼此之间距离相等。但如果我们仔细想想,周六和周日应该比其他工作日更接近,因为它们是周末,对吧?这种信息在独热编码中并没有被捕捉到。
嵌入向量和密集表示
嵌入向量是一种类似的表示方式,但它不是稀疏表示,而是努力为我们提供类别特征的密集表示。我们可以通过使用嵌入层来实现这一点。嵌入层可以被视为每个类别值与一个数值向量之间的映射,而这个向量的维度可以远小于类别特征的基数。唯一剩下的问题是“我们怎么知道为每个类别值选择哪个向量?”
好消息是我们不需要知道,因为嵌入层与网络的其余部分一起训练。因此,在训练模型执行某项任务时,模型会自动找出每个类别值的最佳向量表示。这种方法在自然语言处理领域非常流行,在那里数千个单词被嵌入到维度只有 200 或 300 的空间中。在 PyTorch 中,我们可以通过使用nn.Embedding来实现这一点,它是一个简单的查找表,存储固定离散值和大小的嵌入。
初始化时有两个必需的参数:
-
num_embeddings:这是嵌入字典的大小。换句话说,这是类别特征的基数。 -
embedding_dim:这是每个嵌入向量的大小。
现在,让我们回到全局建模。首先介绍静态类别特征。请注意,我们还包括了时间变化的类别特征,因为现在我们已经知道如何在深度学习模型中处理类别特征。初始化数据集的代码是一样的,只是添加了以下两个参数:
-
static_categoricals=feat_config.static_categoricals -
time_varying_known_categoricals=feat_config.time_varying_known_categoricals
定义带有类别特征的模型
现在我们有了数据集,让我们看看如何在新模型StaticDynamicFeatureRNNModel中定义__init__函数。除了调用父模型来设置标准的 RNN 和全连接层外,我们还使用输入embedding_sizes设置嵌入层。embedding_sizes是一个包含每个类别特征的元组列表(基数和嵌入大小):
def __init__(
self,
rnn_type: str,
input_size: int,
hidden_size: int,
num_layers: int,
bidirectional: bool,
embedding_sizes = []
):
super().__init__(rnn_type, input_size, hidden_size, num_layers, bidirectional)
self.embeddings = torch.nn.ModuleList(
[torch.nn.Embedding(card, size) for card, size in embedding_sizes]
)
我们使用nn.ModuleList来存储nn.Embedding模块的列表,每个类别特征一个。在初始化该模型时,我们需要提供embedding_sizes作为输入。每个类别特征所需的嵌入大小在技术上是一个超参数,我们可以进行调优。但是有一些经验法则可以帮助你入门。这些经验法则的核心思想是,类别特征的基数越大,编码这些信息所需的嵌入大小也越大。此外,嵌入大小可以远小于类别特征的基数。我们采用的经验法则如下:
因此,我们使用以下代码创建embedding_sizes元组列表:
# Finding the cardinality using the categorical encoders in the dataset
cardinality = [len(training.categorical_encoders[c].classes_) for c in training.categoricals]
# using the cardinality list to create embedding sizes
embedding_sizes = [
(x, min(50, (x + 1) // 2))
for x in cardinality
]
现在,转向forward方法,它将类似于之前的模型,但增加了一个部分来处理类别特征。我们本质上使用嵌入层将类别特征转换为嵌入,并将它们与连续特征拼接在一起:
def forward(self, x: Dict):
# Using the encoder and decoder sequence
x_cont = torch.cat([x["encoder_cont"],x["decoder_cont"]], dim=1)
# Roll target by 1
x_cont[:,:,-1] = torch.roll(x_cont[:,:,-1], 1, dims=1)
# Combine the encoder and decoder categoricals
cat = torch.cat([x["encoder_cat"],x["decoder_cat"]], dim=1)
# if there are categorical features
if cat.size(-1)>0:
# concatenating all the embedding vectors
x_cat = torch.cat([emb(cat[:,:,i]) for i, emb in enumerate(self.embeddings)], dim=-1)
# concatenating continuous and categorical
x = torch.cat([x_cont, x_cat], dim=-1)
else:
x = x_cont
# dropping first timestep
x = x[:,1:,:] # x --> (batch_size, seq_len, input_size)
# Processing through the RNN
x, _ = self.rnn(x) # --> (batch_size, seq_len, hidden_size)
# Using a FC layer on last hidden state
x = self.fc(x[:,-1,:]) # --> (batch_size, seq_len, 1)
return x
现在,让我们用静态特征来训练这个新模型,并看看它的表现如何:
图 15.7:使用静态和时间变化特征的汇总结果
添加静态变量也改善了我们的模型。现在,让我们来看另一种策略,它向模型添加了一个关键的信息。
使用时间序列的规模
我们在TimeSeriesDataset中使用了GroupNormlizer来对每个家庭进行缩放,使用它们各自的均值和标准差。这样做是因为我们希望使目标具有零均值和单位方差,以便模型不必浪费精力调整其参数来捕捉单个家庭消费的规模。虽然这是一种很好的策略,但我们确实在这里丢失了一些信息。可能有一些模式是特定于消费较大家庭的,而另外一些模式则是特定于消费较少的家庭的。但现在,这些模式被混在一起,模型试图学习共同的模式。在这种情况下,这些独特的模式对模型来说就像噪音一样,因为没有变量来解释它们。
关键是我们移除的尺度中包含了信息,将这些信息加回来将会是有益的。那么,我们该如何加回来呢?绝对不是通过包括未缩放的目标,这样会带回我们一开始想要避免的缺点。实现这一点的一种方式是将尺度信息作为静态真实特征添加到模型中。当我们最初进行缩放时,我们会记录每个家庭的均值和标准差(因为我们需要它们进行反向变换,并恢复原始目标)。我们需要做的就是确保将它们作为静态真实变量包含在内,这样模型在学习时间序列数据集中的模式时就能访问到尺度信息。
PyTorch Forecasting 通过在TimeSeriesDataset中提供一个方便的参数add_target_scales,使这变得更简单。如果将其设置为True,那么encoder_cont和decoder_cont也将包含各个时间序列的均值和标准差。
我们现有的模型没有变化;我们只需要在初始化时将这个参数添加到TimeSeriesDataset中,然后使用模型进行训练和预测。让我们看看它是如何为我们工作的:
图 15.8:使用静态、时间变化和尺度特征的聚合结果
尺度信息再次改善了模型。有了这些,我们来看看本书最后将讨论的一种策略。
平衡采样过程
我们已经看到了几种通过添加新特征类型来改进全球深度学习模型的策略。现在,让我们看一下全球建模上下文中相关的另一个方面。在前面的章节中,当我们谈到全球深度学习模型时,我们讨论了将一个序列窗口采样输入模型的过程可以被看作是一个两步过程:
-
从一组时间序列中采样一个时间序列。
-
从时间序列中采样一个窗口。
让我们用一个类比来使这个概念更清晰。想象我们有一个大碗,里面填满了N个球。碗中的每个球代表数据集中的一个时间序列(我们数据集中的一个家庭)。现在,每个球,i,都有M[i]张纸片,表示我们可以从中抽取的所有不同样本窗口。
在我们默认使用的批量采样中,我们打开所有的球,将所有纸片倒入碗中,然后丢弃这些球。现在,闭上眼睛,我们从碗中随机挑选B张纸片,将它们放到一边。这就是我们从数据集中采样的一个批次。我们没有任何信息来区分纸片之间的差异,所以抽到任何纸片的概率是相等的,这可以表示为:
现在,让我们在数据类比中加入一些内容。我们知道,时间序列有不同的种类——不同的长度、不同的消费水平等等。我们选择其中一个方面,即序列的长度,作为我们的例子(尽管它同样适用于其他方面)。所以,如果我们将时间序列的长度离散化,我们就会得到不同的区间;我们为每个区间分配一个颜色。现在,我们有C种不同颜色的球在碗里,纸片也会按相应的颜色来标记。
在我们当前的采样策略中(我们将所有纸片都倒入碗中,并随机抽取B张纸片),我们最终会在一个批次中复制碗的概率分布。可以理解的是,如果碗中包含更多的长时间序列而不是短时间序列,那么我们抽到的纸片也会偏向于这一点。因此,批次也会偏向长时间序列。那会发生什么呢?
在小批量随机梯度下降(我们在第十一章,《深度学习导论》中看到过)中,我们在每个小批次后进行一次梯度更新,并使用该梯度来更新模型参数,从而使得模型更接近损失函数的最小值。因此,如果一个小批次偏向某一类型的样本,那么梯度更新将会偏向一个对这些样本效果更好的解。这和不平衡学习有着很好的类比。较长的时间序列和较短的时间序列可能有不同的模式,而这种采样不平衡导致模型学到的模式可能对长时间序列效果较好,而对短时间序列的效果不太理想。
可视化数据分布
我们计算了每个家庭的长度(LCLid),并将它们分到 10 个区间中——bin_0代表最短的区间,bin_9代表最长的区间:
n_bins= 10
# Calculating the length of each LCLid
counts = train_df.groupby("LCLid")['timestamp'].count()
# Binning the counts and renaming
out, bins = pd.cut(counts, bins=n_bins, retbins=True)
out = out.cat.rename_categories({
c:f"bin_{i}" for i, c in enumerate(out.cat.categories)
})
让我们可视化原始数据中区间的分布:
图 15.9:时间序列长度分布
我们可以看到,bin_5和bin_6是最常见的长度,而bin_0是最不常见的。现在,让我们从数据加载器中获取前 50 个批次,并将它们绘制为堆叠柱状图,以检查每个批次的分布:
图 15.10:批次分布的堆叠柱状图
我们可以看到,在批次分布中,和图 15.9中看到的相同的分布也得到了复现,bin_5和bin_6占据了领先位置。bin_0几乎没有出现,而在bin_0中的 LCLid 将不会被学得很好。
调整采样过程
那么接下来我们该怎么办?让我们暂时进入一个装有纸条的碗的类比。我们在随机挑选一个球,结果发现分布和原始的颜色分布完全一致。因此,为了让批次中的颜色分布更加平衡,我们需要按不同的概率从不同颜色的纸条中抽取。换句话说,我们应该从原始分布中低频的颜色上抽取更多,而从占主导地位的颜色中抽取更少。
让我们从另一个角度来看选择碗中筹码的过程。我们知道选择碗中每个筹码的概率是相等的。所以,另一种选择筹码的方法是使用均匀随机数生成器。我们从碗中抽取一个筹码,生成一个介于 0 和 1 之间的随机数(p),如果随机数小于 0.5(p < 0.5),则选择该筹码。所以,我们选择或拒绝筹码的概率是相等的。我们继续进行,直到得到B个样本。虽然这个过程比前一个过程稍微低效一些,但它与原始过程非常接近。这里的优势在于,我们现在有了一个阈值,可以调整我们的采样以适应需求。较低的阈值使得在该采样过程中更难接受筹码,而较高的阈值则使其更容易被接受。
现在我们有了一个可以调整采样过程的阈值,我们需要做的就是找到每个筹码的合适阈值,以便最终的批次能够均匀地代表所有颜色。
换句话说,我们需要找到并为每个 LCLid 分配正确的权重,以使最终的批次能够均匀分布所有长度区间。
我们该如何做呢?有一个非常简单的策略。我们希望对于样本多的长度区间,权重较低;对于样本少的长度区间,权重较高。我们可以通过取每个区间样本数量的倒数来得到这种权重。如果一个区间中有C个 LCLid,则该区间的权重可以是 1/C。进一步阅读部分有一个链接,您可以通过它了解更多关于加权随机采样和为此目的使用的不同算法。
TimeSeriesDataset有一个内部索引,这是一个包含它可以从数据集抽取的所有样本的 DataFrame。我们可以使用它来构建我们的权重数组:
# TimeSeriesDataset stores a df as the index over which it samples
df = training.index.copy()
# Adding a bin column to it to represent the bins we have created
df['bins'] = [f"bin_{i}" for i in np.digitize(df["count"].values, bins)]
# Calculate Weights as inverse counts of the bins
weights = 1/df['bins'].value_counts(normalize=True)
# Assigning the weights back to the df so that we have an array of
# weights in the same shape as the index over which we are going to sample
weights = weights.reset_index().rename(columns={"index":"bins", "bins":"weight"})
df = df.merge(weights, on='bins', how='left')
probabilities = df.weight.values
这样可以确保probabilities数组的长度与TimeSeriesDataset进行采样时的内部索引长度一致,这是使用这种技术时的强制要求——每个可能的窗口应该有一个对应的权重。
现在我们有了这个权重,有一个简单的方法可以将其付诸实践。我们可以使用 PyTorch 中的WeightedRandomSampler,它是专门为此目的创建的:
from torch.utils.data import WeightedRandomSampler
sampler = WeightedRandomSampler(probabilities, len(probabilities))
使用并可视化带有WeightedRandomSampler的 dataloader
现在,我们可以在我们从TimeSeriesDataset创建的 dataloader 中使用这个采样器:
train_dataloader = training.to_dataloader(train=True, batch_size=batch_size, num_workers=0, sampler=sampler)
让我们像之前一样可视化前 50 个批次,看看有什么不同:
图 15.11:带有加权随机采样的批次分布堆叠柱状图
现在,我们可以看到每个批次中更均匀的区间分布。让我们也看看使用这个新 dataloader 训练模型后的结果:
图 15.12:使用静态、时间变化和规模特征以及批量采样器聚合结果
看起来,采样器在所有指标上对模型的改进都很大,除了预测偏差之外。虽然我们没有比 GFM(ML)(它的 MAE 为 0.079581)取得更好的结果,但我们已经足够接近了。也许通过一些超参数调整、数据划分或更强的模型,我们能够更接近那个数值,或者可能不能。我们使用了自定义采样选项,使得每批数据中的时间序列长度保持平衡。我们可以使用相同的技术在其他方面进行平衡,比如消费水平、地区,或任何其他相关的方面。像机器学习中的所有事情一样,我们需要通过实验来确定一切,而我们需要做的就是根据问题陈述形成假设,并构建实验来验证这个假设。
至此,我们已完成又一章以实践为主(且计算量大)的内容。恭喜你顺利完成本章;如果有任何概念尚未完全理解,随时可以回去查阅。
小结
在过去几章中,我们已经打下了深度学习模型的坚实基础,现在开始探讨在深度学习模型背景下的全球模型新范式。我们学习了如何使用 PyTorch Forecasting,这是一个用于深度学习预测的开源库,并利用功能丰富的TimeSeriesDataset开始开发自己的模型。
我们从一个非常简单的 LSTM 开始,在全球背景下看到了如何将时间变化信息、静态信息和单个时间序列的规模添加到特征中,以改善模型性能。最后,我们讨论了交替采样程序,它帮助我们在每个批次中提供问题的更平衡视图。本章绝不是列出所有使预测模型更好的技术的详尽清单。相反,本章旨在培养一种正确的思维方式,这是在自己模型上工作并使其比之前更好地运行所必需的。
现在,我们已经建立了深度学习和全球模型的坚实基础,是时候在下一章中探索一些多年来为时间序列预测提出的专用深度学习架构了。
进一步阅读
你可以查阅以下资料进行进一步阅读:
-
如何使用自定义数据并实现自定义模型和指标(PyTorch Forecasting):
pytorch-forecasting.readthedocs.io/en/stable/tutorials/building.html -
《数据库中的随机抽样》作者:Frank Olken,第 22-23 页:
dsf.berkeley.edu/papers/UCB-PhD-olken.pdf
加入我们在 Discord 上的社区
加入我们社区的 Discord 空间,与作者和其他读者进行讨论:
第十六章:专用深度学习架构用于预测
我们在深度学习(DL)世界中的旅程即将结束。在上一章中,我们介绍了全球预测范式,并看到如何使一个简单的模型,如递归神经网络(RNN),表现接近全球机器学习模型设定的高基准。在本章中,我们将回顾一些专门为时间序列预测设计的流行深度学习架构。借助这些更复杂的模型架构,我们将更好地应对现实世界中需要比普通 RNN 和 LSTM 更强大模型的问题。
在本章中,我们将涵盖以下主要主题:
-
对专用架构的需求
-
NeuralForecast 简介
-
可解释的时间序列预测的神经基础扩展分析
-
带外生变量的可解释的时间序列预测的神经基础扩展分析
-
用于时间序列预测的神经层次插值
-
Autoformer
-
LTSF-Linear 家族
-
补丁时间序列预测
-
iTransformer
-
时序融合变换器
-
TSMixer
-
时间序列密集编码器
技术要求
你需要按照前言中的说明设置 Anaconda 环境,以获取包含本书代码所需的所有包和数据集的工作环境。
本章相关的代码可以在github.com/PacktPublishing/Modern-Time-Series-Forecasting-with-Python/tree/main/notebooks/Chapter16找到。
你需要运行以下笔记本文件来进行本章学习:
-
02-Preprocessing_London_Smart_Meter_Dataset.ipynb在Chapter02 -
01-Setting_up_Experiment_Harness.ipynb在Chapter04 -
01-Feature_Engineering.ipynb在Chapter06
对专用架构的需求
偏倚归纳(或学习偏倚)指的是学习算法在将其在训练数据上学到的函数推广到未见数据时所做的一系列假设。偏倚归纳本身并不是一件坏事,它不同于学习理论中的“偏倚”和“方差”中的“偏倚”。我们通过模型架构或特征工程来使用和设计偏倚归纳。例如,卷积神经网络(CNN)在图像上表现优于标准的前馈神经网络(FFN)在纯像素输入上的表现,因为 CNN 具有 FFN 所没有的局部性和空间偏倚。虽然 FFN 理论上是一个通用逼近器,但我们可以利用 CNN 所具有的偏倚归纳来学习更好的模型。
深度学习被认为是一种完全数据驱动的方法,特征工程和最终任务都是端到端学习的,从而避免了模型设计者在特征设计时所植入的归纳偏差。但这种看法并不完全正确。过去通过特征引入的归纳偏差,如今通过架构设计的方式融入其中。每种深度学习架构都有自己的归纳偏差,这就是为什么某些类型的模型在某些类型的数据上表现更好。例如,卷积神经网络(CNN)在图像上表现良好,但在序列数据上的表现则不如图像,因为 CNN 所带来的空间归纳偏差和平移等价性对图像最为有效。
在理想的世界里,我们会有无限的好数据,并且能够学习完全数据驱动的网络,没有强烈的归纳偏差。但遗憾的是,在现实世界中,我们永远不会有足够的数据来学习如此复杂的函数。这就是设计正确类型的归纳偏差成败的关键。我们曾经在序列建模中大量依赖 RNN,它们内置了强烈的自回归归纳偏差。但后来,Transformer 的出现改变了这一局面,尽管它对序列的归纳偏差较弱,但在大数据的支持下,它们能够更好地学习序列的函数。因此,如何决定将多强的归纳偏差融入模型,是设计深度学习架构时的重要问题。
多年来,许多深度学习架构专门用于时间序列预测,每种架构都有其独特的归纳偏差。我们无法一一回顾所有这些模型,但我们会介绍一些对该领域产生深远影响的主要模型。我们还将探讨如何使用一些开源库在我们的数据上训练这些模型。
我们将专注于能够处理全局建模范式的模型,无论是直接还是间接。这是因为在大规模预测时,为每个时间序列训练独立模型是不现实的。
我们将介绍几种用于时间序列预测的流行架构。影响模型选择的一个主要因素是是否有支持这些模型的稳定开源框架。这里列出的并不是完整的架构清单,因为有许多架构我们没有涵盖。我会在进一步阅读部分尝试分享一些链接,帮助你开始探索之旅。
在我们深入本章的核心内容之前,让我们先了解一下我们将要使用的库。
NeuralForecast 简介
NeuralForecast 是 NIXTLA 团队的又一款库。你可能还记得在第四章,设置强基准预测中,我们使用了statsforecast来处理经典的时间序列模型,如 ARIMA、ETS 等。他们有一整套用于时间序列预测的开源库(如mlforecast用于基于机器学习的预测,hierarchicalforecast用于层级数据的预测整合,utilsforecast包含一些预测工具,datasetsforecast提供一些现成的数据集,以及TimeGPT,他们的时间序列基础模型)。
由于我们已经学习了如何使用statsforecast,因此将其扩展到neuralforecast将变得非常容易,因为这两个库在 API、结构和工作方式上非常相似。neuralforecast提供了经典和前沿的深度学习模型,并且具有易于使用的 API,非常适合本章的实际应用。
NeuralForecast 的结构旨在提供一个直观且灵活的 API,能够与现代数据科学工作流无缝集成。该包包括多个著名模型的实现,每个模型都针对时间序列预测的不同方面。
常见的参数和配置
类似于statsforecast,neuralforecast也期望输入数据具有特定的格式:
-
ds:此列应包含时间索引。它可以是一个日期时间列,也可以是表示时间的整数列。 -
y:此列应包含我们要预测的时间序列。 -
unique_id:此列使我们能够通过选择的唯一 ID 来区分不同的时间序列。它可以是我们数据中的家庭 ID,或者是我们指定的任何其他唯一标识符。
neuralforecast包中的大多数模型共享一组控制各种方面的通用参数,例如:
-
stat_exog_list:这是一个列出静态连续列的列表。 -
hist_exog_list:这是一个列出历史可用的外生特征的列表。 -
futr_exog_list:这是一个列出未来可用的外生特征的列表。 -
learning_rate:这是决定模型学习速度的参数。较高的学习率可能更快收敛,但可能会超过最优权重,而较低的学习率则能保证更稳定的收敛,但速度较慢。 -
batch_size:此参数影响每次训练步骤中输入模型的数据量,进而影响内存使用和训练动态。 -
max_steps:此参数定义了最大训练轮数——即整个数据集通过神经网络正向和反向传递的次数。它是最大值,因为我们还可以添加早停机制,在这种情况下,轮数可能会少于这个值。 -
loss:这是用于衡量预测值与实际值之间差异的指标,指导优化过程。有关包含的损失函数的列表,请参阅 NIXTLA 文档:nixtlaverse.nixtla.io/neuralforecast/losses.pytorch.html -
scaler_type:这是一个字符串,表示使用的时间归一化类型。时间归一化会在每个批次的窗口级别对每个实例分别进行缩放。常见的类型包括['minmax,' 'robust,' 'standard']。此参数仅适用于基于窗口的模型,如 NBEATS 和 TimesNet,而不适用于递归模型如 RNN。如果您想查看所有归一化器,请查阅 NIXTLA 时间归一化器:nixtlaverse.nixtla.io/neuralforecast/common.scalers.html。 -
early_stop_patience_steps:如果定义了该参数,它设置了在验证得分没有改进的情况下,我们将在多少步后停止训练。 -
random_seed:此参数定义了随机种子,它对于结果的可复现性至关重要。
实践者小贴士:
参数stat_exog_list、hist_exog_list和futr_exog_list仅对支持这些参数的模型可用。在使用的模型文档中检查,查看该模型是否支持这些参数。有些模型支持所有三个,有些模型只支持futr_exog_list,等等。有关支持的模型和可用项的完整列表,请参阅:nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/overview.html。
如果某些特征只有历史数据,那么它们应该放在hist_exog_list中。如果某些特征既有历史数据又有未来数据,它们应同时放在hist_exog_list和futr_exog_list中。
除了这些常见的模型参数外,neuralforecast还具有一个核心类NeuralForecast,它协调训练过程(就像我们在statsforecast中使用StatsForecast一样)。与statsforecast类似,这里定义了我们需要预测的模型列表等参数。让我们也来看一下这个类中的一些参数:
-
models:该参数定义了我们需要拟合或预测的模型列表。 -
freq:此参数设置我们想要预测的时间序列的频率。它可以是一个字符串(一个有效的 pandas 或 polars 偏移别名)或一个整数。此参数用于生成用于预测的未来数据框,并应根据您所拥有的数据进行定义。
如果ds是一个日期时间列,那么freq可以是一个字符串,表示重复的频率(例如‘D’代表天,‘H’代表小时等),或者是一个整数,表示默认单位(通常为天)的倍数,用于确定每个日期之间的间隔。如果ds是数值列,那么freq也应是一个数值列,表示值之间的固定数值增量。
local_scaler_type:这是另一种缩放时间序列的方式。在基于 Windows 的模型中,scaler_type在每个窗口中缩放时间序列,而此方法在预处理步骤中缩放每个时间序列。对于每个unique_id,此步骤会单独缩放时间序列并存储缩放器,以便在预测时可以应用逆变换。
使用 neuralforecast 的典型工作流程如下所示:
from neuralforecast import NeuralForecast
from neuralforecast.models import LSTM
horizon = 12
models = [LSTM(h=horizon,
max_steps=500,
scaler_type='standard',
encoder_hidden_size=64,
decoder_hidden_size=64,),
]
nf = NeuralForecast(models=models, freq='M')
nf.fit(df=Y_df)
Y_hat_df = nf.predict()
“自动”模型
neuralforecast 包的一个突出特点是包含了“自动”模型。这些模型自动化了超参数调优和模型选择的过程,简化了用户的工作流程。通过利用自动化机器学习(AutoML)技术,这些模型能够根据数据集自适应其架构和设置,大大减少了模型配置中的手动工作量。它们定义了智能的默认范围,因此即使你没有声明要调优的范围,它们也会采用默认范围并进行调优。更多信息请见:nixtlaverse.nixtla.io/neuralforecast/models.html。
外部特征
NeuralForecast 还可以轻松地将外部变量纳入预测过程中(具体取决于模型的能力)。外部特征是指能够影响目标变量的外部因素,对于提高预测准确性至关重要,尤其是当这些外部因素对结果产生显著影响时。neuralforecast 包中的许多模型可以整合这些特征,通过考虑时间序列数据中可能不存在的附加信息来优化预测。
例如,将假期效应、天气状况或经济指标作为外部变量纳入,可以提供纯历史数据无法提供的重要洞察。这个特性在像 NBEATSx、NHITS 和 TSMixerx 这样的模型中尤为有用,这些模型能够建模历史和未来外部输入之间的复杂交互。通过有效处理外部特征,NeuralForecast 提高了模型在现实场景中预测准确性的能力,因为外部因素在这些场景中起着关键作用。要查看哪些模型可以处理外部信息,请参考网站上的文档:nixtlaverse.nixtla.io/neuralforecast/docs/capabilities/overview.html
现在,让我们不再拖延,开始介绍列表中的第一个模型。
解释性时间序列预测的神经基础扩展分析(N-BEATS)
第一个使用深度学习(DL)一些组件的模型(我们不能称其为 DL,因为它本质上是 DL 与经典统计学的混合)并在该领域引起轰动的是在 2018 年赢得 M4 竞赛(单变量)的一个模型。这是由 Slawek Smyl(当时在 Uber)设计的模型,它是指数平滑和 RNN 的“怪物”式混合,名为ES-RNN(进一步阅读部分提供了使用 GPU 加速的该模型更新版的链接)。这促使 Makridakis 等人提出“混合方法和方法组合是未来的方向”这一观点。N-BEATS模型的创造者希望通过设计一个纯 DL 架构来挑战这一结论,用于时间序列预测。当他们创建出一个在 M4 竞赛中击败所有其他方法的模型时(尽管他们没有及时发布以参与竞赛),他们成功地实现了这一目标。这是一个非常独特的架构,深受信号处理的启发。让我们更深入地了解并理解这一架构。
参考检查:
Makridakis 等人的研究论文以及 Slawek Smyl 的博客文章在参考文献部分分别被引用为1和2。
在继续解释之前,我们需要先建立一些背景和术语。它们所解决的核心问题是单变量预测,这意味着它类似于经典方法,如指数平滑和 ARIMA,因为它只使用时间序列的历史数据来生成预测。模型中没有包含其他协变量的机制。模型展示一个历史窗口,并要求预测接下来的几个时间步。历史窗口被称为回溯期,而未来的时间步则是预测期。
N-BEATS 的架构
N-BEATS 架构在几个方面不同于当时的现有架构:
-
与常见的编码器-解码器(或序列到序列)形式不同,N-BEATS 将问题表述为多元回归问题。
-
当时大多数其他架构相对较浅(大约 5 层 LSTM)。然而,N-BEATS 使用残差原则堆叠了许多基础块(我们稍后会解释这一点),而且论文表明我们可以堆叠多达 150 层,仍然能够实现高效学习。
-
该模型让我们将其扩展为可人类解释的输出,仍然是以一种有原则的方式进行的。
让我们来看看架构,并深入分析:
图 16.1:N-BEATS 架构
我们可以看到三个矩形块的列,每一个都是另一个的爆炸视图。从最左侧开始(那是最细节的视图),然后逐步向上构建,直到完整的架构。在最顶部,有一个代表性的时间序列,它包含一个回溯窗口和一个预测期。
块
N-BEATS 中的基本学习单元是块。每个块,l,接受一个输入,(x[l]),其大小为回看期,并生成两个输出:一个预测,(),和一个反向预测,(
)。反向预测是块对回看期的最佳预测。它在经典意义上与拟合值同义;它们告诉我们堆栈如何使用它所学到的函数预测回看窗口。块的输入首先通过堆叠的四个标准完全连接层进行处理(包含偏置项和非线性激活),将输入转换为隐藏表示,h[l]。现在,这个隐藏表示通过两个独立的线性层(没有偏置或非线性激活)转换为论文中称之为扩展系数的内容,分别是反向预测和预测的系数,
和
。
块的最后部分将这些扩展系数映射到输出,使用一组基础层(和
)。稍后我们会详细讲解基础层,但现在,先理解它们将扩展系数转换为所需的输出(
和
)。
堆栈
现在,让我们向上一层抽象,转到图 16.1的中间列。它展示了不同块在一个堆栈(s)中的排列方式。堆栈中的所有块共享相同类型的基础层,因此它们被归为一个堆栈。如我们之前所见,每个块有两个输出,和
。这些块按残差方式排列,每个块一步步处理和清洗时间序列。块的输入,l,是
。在每个步骤中,块生成的反向预测会从输入中减去,然后再传递到下一层。堆栈中所有块的预测输出会被加起来形成堆栈预测:
堆栈中最后一个块的残差反向预测是堆栈残差(x^s)。
整体架构
接下来,我们可以转到图 16.1的最右侧列,该列展示了架构的顶层视图。我们看到每个堆栈有两个输出——一个是堆栈预测(y^s),另一个是堆栈残差(x^s)。可以有N个堆栈组成 N-BEATS 模型。每个堆栈是串联在一起的,因此对于任何堆栈(s),前一个堆栈的堆栈残差(x^(s-1))是输入,堆栈会生成两个输出:堆栈预测(y^s)和堆栈残差(x^s)。最后,N-BEATS 预测,,是所有堆栈预测的加和:
现在我们已经理解了模型在做什么,我们需要回到一个之前留到后面讲的点——基函数。
免责声明:
这里的解释主要是为了帮助理解,因此我们可能略过了一些数学概念。如果想深入理解这一主题,建议参考涉及该主题的数学书籍或文章。例如,《函数作为向量空间》在进一步阅读部分和函数空间(cns.gatech.edu/~predrag/courses/PHYS-6124-12/StGoChap2.pdf)。
基函数与可解释性
要理解什么是基函数,我们需要了解线性代数中的一个概念。我们在第十一章《深度学习导论》中讨论过向量空间,并给出了向量和向量空间的几何解释。我们谈到了向量是* n *维向量空间中的一个点。我们曾讨论过常规的欧几里得空间(R^n),它用于表示物理空间。欧几里得空间是通过一个原点和一个正交归一基来定义的。正交归一基是单位向量(大小=1),且它们彼此正交(直观理解为 90 度)。因此,一个向量,,可以写成!,其中
和
是正交归一基。你可能还记得这部分内容是高中学过的。
现在,有一个数学分支将函数视为向量空间中的一个点(此时我们称其为泛函空间)。这一观点源自于这样一个事实:向量空间需要满足的所有数学条件(如加法性、结合性等)在我们考虑函数而非点时依然有效。为了更好地理解这一点,让我们考虑一个函数,f(x) = 2x + 4x²。我们可以将这个函数视为在具有基函数x和x²的函数空间中的一个向量。现在,系数 2 和 4 可以变化,给我们不同的函数;它们可以是从-到+
的任何实数。所有能够以x和x²为基的函数的集合就是泛函空间,泛函空间中的每个函数都可以表示为基函数的线性组合。我们可以选择任意函数作为基函数,这给我们提供了极大的灵活性。从机器学习的角度来看,在这个泛函空间中寻找最佳函数,实际上意味着我们在限制函数的搜索范围,使其具备基函数所定义的某些性质。
回到 N-BEATS,我们讨论了扩展系数, 和
,这些系数通过一组基础层(
和
)映射到输出。一个基础层也可以被看作是一个基础函数,因为我们知道一个层仅仅是一个函数,它将输入映射到输出。因此,通过学习扩展系数,我们实际上是在寻找一个最佳的函数,这个函数能够表示输出,但受到我们选择的基础函数的限制。
N-BEATS 有两种操作模式:通用模式和可解释模式。N-BEATS 论文表明,在这两种模式下,N-BEATS 都能够在 M4 比赛中超越最优秀的模型。通用模式是指我们没有任何基础函数来限制函数搜索。我们也可以将此看作是将基础函数设置为恒等函数。因此,在这种模式下,我们让模型通过线性投影的方式,完全学习函数。这种模式缺乏人类可解释性,因为我们无法了解不同的函数是如何学习的,也无法理解每一层的意义。
但是,如果我们有固定的基础函数来限制函数空间,我们就可以引入更多的可解释性。例如,如果我们有一个基础函数来限制输出表示堆栈中所有模块的趋势,那么我们可以说该堆栈的预测输出代表趋势成分。同样,如果我们有另一个基础函数来限制输出表示堆栈中所有模块的季节性,那么我们可以说该堆栈的预测输出代表季节性。
这正是论文中提出的内容。他们定义了特定的基础函数来捕捉趋势和季节性,包含这些模块可以通过给出分解来使最终的预测更具可解释性。趋势基础函数是一个低阶多项式,p。因此,只要 p 较低,比如 1、2 或 3,就会迫使预测输出模仿趋势成分。对于季节性基础函数,作者选择了傅里叶基础(类似于我们在第六章,时间序列预测的特征工程中看到的)。这迫使预测输出成为这些正弦基础函数的组合,模仿季节性。换句话说,模型学习将这些正弦波与不同的系数组合,以尽可能好地重构季节性模式。
为了更深入理解这些基础函数及其结构,我在进一步阅读部分链接了一篇Kaggle 笔记本,其中提供了关于趋势和季节性基础函数的清晰解释。相关笔记本还包含一个额外的部分,展示了季节性基础函数的前几个可视化图像。结合原始论文,这些附加阅读材料将有助于加深你的理解。
N-BEATS 并非专为全球模型设计,但在全球环境中表现良好。M4 竞赛是一个包含无关时间序列的集合,N-BEATS 模型的训练方式使得模型能够接触到所有这些序列,并学习一种共同的函数来预测数据集中的每个时间序列。这种方法,再加上使用不同回溯窗口的多个 N-BEATS 模型的集成,构成了 M4 竞赛的成功公式。
参考检查:
Boris Oreshkin 等人的研究论文(N-BEATS)在参考文献部分被标注为3。
使用 N-BEATS 进行预测
N-BEATS 以及我们将在本章中探讨的许多其他专用架构,都已在 NIXTLA 的 NeuralForecast 包中实现。首先,我们来看一下该实现的初始化参数。
NeuralForecast 中的NBEATS类有很多参数,以下是最重要的一些:
-
stack_types:这定义了我们在 N-BEATS 模型中需要的堆叠数量。它应该是一个包含字符串的列表(generic、trend或seasonality),表示堆叠的数量和类型。示例包括["trend", "seasonality"]、["trend", "seasonality", "generic"]和["generic", "generic", "generic"]。不过,如果整个网络是通用的,我们也可以只使用一个通用堆叠,并添加更多的块。 -
n_blocks:这是一个整数列表,表示我们已定义的每个堆叠中块的数量。如果我们已将stack_types定义为["trend", "seasonality"],并希望每个堆叠有三个块,那么我们可以将n_blocks设置为[3,3]。 -
input_size:这是一个整数,表示要测试的自回归单位(滞后)。 -
shared_weights:这是一个布尔值列表,表示生成扩展系数的权重是否与堆叠中的其他块共享。建议在可解释的堆叠中共享权重,而在身份堆叠中则不共享。
还有一些其他参数,但这些参数不如上述重要。参数及其描述的完整列表可以在nixtlaverse.nixtla.io/neuralforecast/models.nbeats.html找到。
由于该模型的优势在于预测较长时间的跨度,我们只需设置预测时长参数h = 48,即可进行一次性的 48 步预测。
笔记本提示:
用于训练 N-BEATS 的完整代码可以在Chapter16文件夹中的01-NBEATS_NeuralForecast.ipynb笔记本中找到。
解释 N-BEATS 预测结果
N-BEATS,如果我们在可解释模型中运行它,也通过将预测分解为趋势和季节性,提供了更多的可解释性。要获得可解释的输出,我们可以调用decompose函数。我们必须确保在初始参数中包括趋势和季节性组件的堆叠类型:stack_types = ['trend','seasonality']。
model_interpretable = model_untuned.models[0]
dataset, *_ = TimeSeriesDataset.from_df(df = training_df, id_col='LCLid',time_col='timestamp',target_col='energy_consumption')
y_hat = model_interpretable.decompose(dataset=dataset)
这将返回一个array,从中可以访问趋势和季节性,比如y_hat =[0,1]。趋势或季节性的顺序取决于你在stack_types中如何包含它,但默认顺序是['seasonality','trend'],意味着季节性是y_hat =[0,1],趋势是y_hat =[0,1]。
让我们看看其中一个家庭预测是如何分解的:
图 16.2:N-BEATS 的分解预测(可解释)
尽管 N-BEATS 取得了巨大成功,但它仍然是一个单变量模型。它不能接受任何外部信息,除了它的历史数据。这在 M4 竞赛中是可以的,因为所有相关的时间序列都是单变量的。然而,许多实际世界中的时间序列问题会带有额外的解释变量(或外生变量)。让我们看看对 N-BEATS 做的一个小改动,如何使其能够处理外生变量。
带外生变量的可解释时间序列预测的神经基础扩展分析(N-BEATSx)
Olivares 等人提出了 N-BEATS 模型的扩展,通过使其与外生变量兼容。整体结构与 N-BEATS(图 16.1)相同(包含块、堆叠和残差连接),因此我们将仅关注N-BEATSx模型提出的关键差异和新增内容。
参考检查:
Olivares 等人的研究论文(N-BEATSx)在参考文献部分被引用为4。
处理外生变量
在 N-BEATS 中,一个块的输入是回溯窗口,y^b。但在这里,块的输入是回溯窗口,y^b,以及外生变量数组,x。这些外生变量可以分为两种类型:时间变化型和静态型。静态变量使用静态特征编码器进行编码。这个编码器实际上是一个单层全连接网络(FC),将静态信息编码为用户指定的维度。现在,编码后的静态信息、时间变化的外生变量和回溯窗口被连接在一起,形成一个块的输入,从而使得块l的隐藏状态表示,h[l],不再是像 N-BEATS 中的FC(y^b),而是FC([y^b;x]),其中[;]表示连接。这样,外生信息作为输入的一部分,与残差在每一步都被连接到每个块中。
外生块
此外,论文还提出了一种新型的模块——外生模块。外生模块接收串联的回顾窗口和外生变量(与任何其他模块相同)作为输入,并生成反向预测和预测:
这里,N[x]表示外生特征的数量。
在这里,我们可以看到外生预测是外生变量的线性组合,并且这个线性组合的权重由扩展系数学习。该论文将此配置称为可解释的外生模块,因为通过使用扩展权重,我们可以定义每个外生变量的重要性,甚至找出由特定外生变量引起的预测的确切部分。
N-BEATSx 还具有外生模块的通用版本(不可解释)。在此模块中,外生变量通过一个学习上下文向量C[l]的编码器传递,并且使用以下公式生成预测:
他们提出了两种编码器:时间卷积网络(TCN)和WaveNet(类似于 TCN 的网络,但通过扩张来扩展感受野)。进一步阅读部分包含了更多关于 WaveNet 的资源,这是一种起源于声音领域的架构。
N-BEATSx 也在 NIXTLA 的neuralforecast中实现,然而在撰写本文时,它尚不能处理分类数据。因此,我们需要将分类特征编码为数值表示(就像我们在第十章 全球预测模型中所做的)后再使用neuralforecast。
研究论文还表明,N-BEATSx 在电力价格预测方面明显优于 N-BEATS、ES-RNN 和其他基准模型。
在延续 N-BEATS 的基础上,我们现在将讨论另一种修改架构的方法,使其适用于长期预测。
神经层次插值用于时间序列预测(N-HiTS)
尽管深度学习在处理时间序列预测方面取得了相当多的成果,但对长期预测的关注仍然很少。尽管最近取得了进展,但由于两个原因,长期预测仍然是一个挑战:
-
真正捕捉变化所需的表达能力
-
计算复杂度
基于注意力的方法(Transformers)和类似 N-BEATS 的方法在内存和与预测时间范围相关的计算成本上呈二次扩展。
作者声称,与现有基于 Transformer 的架构相比,N-HiTS 大大降低了长期预测的计算成本,同时在大量多变量预测数据集上显示出 25%的准确率改进。
参考检查:
Challu 等人在N-HiTS上的研究论文被引用为5,详见参考文献部分。
N-HiTS 的架构
N-HiTS 可以被视为对 N-BEATS 的修改,因为它们两者在架构上有很大一部分相同。 图 16.1 展示了 N-BEATS 的架构,对于 N-HiTS 仍然有效。N-HiTS 也具有以残差方式排列的块堆叠;它们的不同之处仅在于所使用的块的种类。例如,N-HiTS 没有为可解释块提供规定。所有 N-HiTS 中的块都是通用的。虽然 N-BEATS 试图将信号分解为不同的模式(趋势、季节性等),但 N-HiTS 试图将信号分解为多个频率并分别进行预测。
为了实现这一点,提出了一些关键的改进:
-
多速率数据采样
-
分层插值
-
将输入采样率与各个块之间的输出插值尺度同步
多速率数据采样
N-HiTS 在完全连接块之前加入了子采样层,以便每个块的输入分辨率不同。这类似于用不同分辨率平滑信号,以便每个块查看发生在不同分辨率的模式,例如,如果一个块每天查看输入,另一个块每周查看输出等。这样,当用不同块查看不同分辨率时,模型将能够预测在这些分辨率下发生的模式。这显著减少了内存占用和所需的计算,因为我们不是查看所有H步的回顾窗口,而是查看较小的系列(例如 H/2、H/4 等)。
N-HiTS 使用了一个最大池化或平均池化层,内核大小为k[l],在回顾窗口上进行池化操作。池化操作类似于卷积操作,但使用的函数是非可学习的。在第十二章,时间序列深度学习的基本构建块中,我们学习了卷积、内核、步长等。而卷积使用从数据中学习的权重进行训练,池化操作使用非可学习和固定的函数来聚合内核接收场中的数据。这些函数的常见示例包括最大值、平均值、求和等。N-HiTS 在不同块中使用MaxPool1d或AvgPool1d(以PyTorch术语表示),每个池化操作的步长也等于内核,导致非重叠窗口上进行聚合操作。为了刷新我们的记忆,让我们看看kernel=2和stride=2的最大池化是什么样子:
图 16.3:一维最大池化 - 内核 = 2,步长 = 2
因此,较大的内核大小会倾向于削减输入中更多的高频(或小时间尺度)成分。这样,块被迫专注于更大尺度的模式。该论文将此称为多速率信号采样。
分层插值
在标准的多步预测设置中,模型必须预测H个时间步长。随着H的增大,计算要求增加,并导致模型所需的表现力爆炸性增长。
训练一个具有如此大表现力的模型而不发生过拟合本身就是一个挑战。为了应对这些问题,N-HiTS 提出了一种名为时间插值的技术((不是在两个已知时间点之间的简单插值,而是与架构特定的插值方法)`)。
汇聚的输入(我们在前一节中看到的)与常规机制一起进入区块,生成扩展系数,并最终转换为预测输出。但是在这里,N-HiTS 将扩展系数的维度设置为r[l] X H,而不是H,其中r[l]是表现力比率。这个参数本质上减少了预测输出的维度,从而控制了我们在前一段中讨论的问题。为了恢复原始采样率并预测预测范围内的所有H点,我们可以使用插值函数。插值函数有很多选择——线性插值、最近邻插值、三次插值等。所有这些选项都可以通过PyTorch中的interpolate函数轻松实现。
同步输入采样和输出插值
除了通过池化和输出插值提出输入采样外,N-HiTS 还提出了以特定方式将它们排列在不同区块中的方法。作者认为,层次化插值只有在表现力比率以与多速率采样同步的方式分布在区块之间时,才能正确进行。离输入较近的区块应该具有较小的表现力比率r[l],以及较大的卷积核大小k[l]。这意味着离输入较近的区块将生成更高分辨率的模式(因为进行更为积极的插值),同时被迫查看大幅度下采样的输入信号。论文提出了随着从初始区块到最后一个区块的移动,表现力比率呈指数增长,以处理广泛的频率带。官方的 N-HiTS 实现使用以下公式来设置表现力比率和池化卷积核:
pooling_sizes = np.exp2(
np.round(np.linspace(0.49, np.log2(prediction_length / 2), n_stacks))
)
pooling_sizes = [int(x) for x in pooling_sizes[::-1]]
downsample_frequencies = [
min(prediction_length, int(np.power(x, 1.5))) for x in pooling_sizes
]
我们还可以提供显式的pooling_sizes和downsampling_fequencies来反映已知的时间序列周期(例如每周季节性、每月季节性等)。N-BEATS 的核心原理(一个区块从信号中去除其捕捉的影响并传递给下一个区块)在这里也得到了应用,因此在每一层中,区块捕捉到的模式或频率会在传递给下一个区块之前从输入信号中去除。最终,最终的预测是所有这些单独区块预测的总和。
使用 N-HiTS 进行预测
N-HiTS 已在 NIXTLA 预测中实现。我们可以使用与 N-BEATS 相同的框架,并扩展其以便在我们的数据上训练 N-HiTS。更棒的是,该实现支持外生变量,就像 N-BEATSx 处理外生变量一样(尽管没有外生模块)。首先,让我们看看实现的初始化参数。
neuralforecast 中的 NHITS 类具有以下参数:
-
n_blocks:这是一个整数列表,表示每个堆叠中使用的块的数量。例如,[1,1,1]表示将有三个堆叠,每个堆叠中有一个块。 -
n_pool_kernel_size:这是一个整数列表,用于定义每个堆叠的池化大小(k[l])。这是一个可选参数,如果提供,我们可以更好地控制不同堆叠中池化的方式。使用从高到低的排序可以提高结果。 -
pooling_mode:定义要使用的池化类型。它应该是'MaxPool1d'或'AvgPool1d'。 -
n_freq_downsample:这是一个整数列表,用于定义每个堆叠的表现力比率(r[l])。这是一个可选参数,如果提供,我们可以更好地控制不同堆叠中插值的方式。
笔记本提示:
训练 N-HiTS 的完整代码可以在 Chapter16 文件夹中的 02-NHiTS_NeuralForecast.ipynb 笔记本中找到。
现在,让我们把注意力转向 Transformer 模型的一些修改,使其更适合时间序列预测。
Autoformer
最近,Transformer 模型在捕捉长期模式方面表现出比标准 RNN 更强的性能。其主要原因之一是自注意力机制,Transformer 正是依靠这一机制来减少相关序列信息在被用于预测之前必须保存的长度。换句话说,在 RNN 中,如果第 12 步之前的时间步包含重要信息,那么这些信息必须通过 12 次更新存储在 RNN 中,才能用于预测。但是,在 Transformer 中,由于结构中没有递归,模型可以自由地在滞后 12 步和当前步骤之间创建一个快捷通道。
但同样的自注意力机制也是我们无法将原生 Transformer 扩展到长序列的原因。在上一节中,我们讨论了长期预测面临的挑战,主要有两个原因:捕捉变化所需的表现力和计算复杂度。自注意力的二次计算复杂度就是第二个原因。
研究界已经意识到这个挑战,并投入了大量努力,通过许多技术,如下采样、低秩近似、稀疏注意力等,来设计高效的变换器。有关这些技术的详细介绍,请参阅进一步阅读部分中的高效变换器:调查链接。
Autoformer是另一种为长期预测设计的模型。Autoformer 发明了一种新的注意力机制,并将其与时间序列分解的各个方面结合。让我们来看一下是什么使得 Autoformer 如此特别。
Autoformer 模型的架构
Autoformer 模型是变换器的改进版。以下是其主要贡献:
-
一致性输入表示:一种系统化的方法,将序列的历史信息与其他信息结合,有助于捕获长期信号,如周、月、节假日等。
-
生成式解码器:用于在一次前向传播中生成长期预测,而不是通过动态递归生成。
-
自相关机制:一种替代标准点积注意力的方法,考虑的是子序列相似性,而非点对点相似性。
-
分解架构:一种专门设计的架构,在建模时间序列时将季节性、趋势和残差分离。
参考检查:
吴等人在 Autoformer 上的研究论文在参考文献部分被引用为9。
一致性输入表示
RNN 通过其递归结构捕获时间序列模式,因此只需要序列;它们不需要时间戳信息来提取模式。然而,变换器中的自注意力是通过点对点操作来完成的,这些操作在一组内执行(顺序在一组内不重要)。通常,我们会包含位置编码来捕获序列的顺序。我们可以不使用位置编码,而是使用更丰富的信息,如层次化时间戳信息(例如周、月、年等)。这正是作者通过一致性输入表示提出的。
一致性输入表示使用三种类型的嵌入来捕获时间序列的历史、时间序列中的值序列和全局时间戳信息。时间序列中的值序列通过d_model维度的标准位置嵌入来捕获。
一致性输入表示使用一个一维卷积层,kernel=3,stride=1,将历史数据(它是标量或一维的)投影到d_model维度的嵌入中。这被称为值嵌入。
全球时间戳信息通过一个可学习的嵌入(d_model维度,有限词汇表)嵌入机制来实现,这个机制与将类别变量嵌入到固定大小的向量中的方法相同(第十五章,全球深度学习预测模型策略)。这被称为时间嵌入。
现在我们有三个相同维度的嵌入,d_model,我们需要做的就是将它们加在一起,得到统一输入表示。
生成式解码器
推理 Transformer 模型的标准方式是一次解码一个标记。这个自回归过程非常耗时,并且每一步都会重复很多计算。为了解决这个问题,Autoformer 模型采用了更具生成性的方式,在一次前向传播中生成整个预测区间。
在自然语言处理(NLP)中,使用特殊标记(START)来开始动态解码过程是一种流行的技术。Autoformer 模型没有选择为此目的使用特殊标记,而是从输入序列中选择一个样本,例如输出窗口之前的一个较早的切片。例如,假设输入窗口是t[1]到t[w],我们将从输入中采样一个长度为C的序列,t[w-c]到t[w],并将该序列作为解码器的起始序列。为了使模型在一次前向传播中预测整个预测区间,我们可以扩展解码器输入张量,使其长度为C + H,其中H是预测区间的长度。初始的C个标记由输入的样本序列填充,剩余部分填充为零——即,。这只是目标。尽管
的预测区间填充了零,但这仅仅是目标。其他信息,例如全球时间戳,包含在
中。还采用了充分的注意力矩阵遮蔽,使得每个位置不会关注未来的位置,从而保持预测的自回归特性。
现在,让我们来看一下时间序列分解架构。
分解架构
我们在第三章,时间序列数据的分析与可视化中曾看到过分解的思想,甚至在本章(N-BEATS)中也有所提及。Autoformer 成功地将 Transformer 架构改造成了一种深度分解架构:
图 16.4:Autoformer 架构
首先理解整体架构,然后再深入了解细节会更容易。在图 16.4中,有标记为自相关和系列分解的框。现在,只需知道自相关是一种注意力机制,而系列分解是一个将信号分解为趋势-周期性和季节性分量的特定模块。
编码器
通过前面章节讨论的抽象级别,让我们理解编码器中发生了什么:
-
时间序列的均匀表示x[en]是编码器的输入。输入通过一个自相关块(用于自注意力),其输出是x[ac]。
-
均匀表示x[en]作为残差连接加回x[ac],x[ac] = x[ac] + x[en]。
-
现在,x[ac]通过一个系列分解块,该块将信号分解为趋势-周期性成分(x[T])和季节性成分x[seas]。
-
我们丢弃x[T]并将x[seas]传递给前馈网络,该网络输出x[FF]。
-
x[seas]再次作为残差连接加回x[FF],x[seas] = x[FF] + x[seas]。
-
可能会有N个编码器块堆叠在一起,每个编码器块都将前一个编码器的输出作为输入。
现在,让我们将注意力转向解码器块。
解码器
Autoformer 模型通过包含来自输入序列的采样窗口,使用类似 START 标记的机制。但 Autoformer 不仅仅取序列,而是对其进行了一些特殊处理。Autoformer 将其学习能力的重点放在了季节性上。变换器的输出也是季节性。因此,Autoformer 不包含完整的输入序列窗口,而是将信号分解,并仅在 START 标记中包含季节性成分。让我们一步步地来看这个过程:
-
现在,我们从
的末尾采样C个时间步,并添加H个零,其中H是预测视野,构造x[ds]。
-
这个x[ds]接着用于创建一个均匀表示x[dec]。
-
与此同时,我们从
的末尾采样C个时间步,并附加H个时间步与序列的均值(mean(x)),其中H是预测视野,构造x[dt]。
然后,这个x[dec]作为输入用于解码器。这就是解码器中发生的事情:
-
输入x[dec]首先通过一个
自相关(用于自注意力)块,其输出是x[dac]。 -
均匀表示x[dec]作为残差连接加回x[dac],x[dac] = x[dac] + x[dec]。
-
现在,x[dac]通过一个系列分解块,该块将信号分解为趋势-周期性成分(x[dT][1])和季节性成分x[dseas]。
-
在解码器中,我们不会丢弃趋势组件;相反,我们会将其保存。这是因为我们将会将所有包含趋势的趋势组件(x[dt])加在一起,形成整体的趋势部分(T)。
-
序列分解块的季节性输出(x[dseas]),以及来自编码器的输出(
),然后传入另一个自相关块,在这里计算解码器序列和编码器序列之间的交叉注意力。让这个块的输出是x[cross]。
-
现在,x[dseas]作为残差连接被加回到x[cross]上,x[cross] = x[cross] + x[dseas]。
-
x[cross]再次通过序列分解块,该块将x[cross]分解为两个组件—x[dT2]和x[dseas2]。
-
x[dseas]随后通过前馈网络转换为x[dff],并通过残差连接将x[dseas]加到其中,x[dff] = x[dff] + x[dseas]。
-
最后,x[dff]通过另一个序列分解块,该块将其分解为两个组件—x[dT3]和x[dseas3]。x[dseas3]是解码器的最终输出,捕捉季节性。
-
另一个输出是残差趋势,
,它是通过解码器的序列分解块提取的所有趋势组件的总和的投影。投影层是一个Conv1d层,它将提取的趋势投射到所需的输出维度:
。
-
M个这样的解码器层被依次堆叠,每个层将其输出作为下一个层的输入。
-
每个解码器层的残差趋势,
,会加到趋势初始化部分x[dt]上,以建模整体的趋势组件(T)。
-
最终解码器层的x[dseas3]被认为是整体的季节性组件,并通过一个线性层投射到所需的输出维度(S)。
-
最终,预测或预测结果 X[out] = T + S。
整个架构巧妙地设计,以便将时间序列中相对稳定且易于预测的部分(趋势-周期性)移除,从而能够很好地建模难以捕捉的季节性。
现在,序列分解块是如何分解序列的呢?这个机制你可能已经熟悉了:AvgPool1d配合一定的填充,这样它可以保持与输入相同的大小。这就像是在指定的核宽度上做一个移动平均。
我们在整个解释过程中都在讨论自相关块。现在,让我们来理解一下自相关块的巧妙之处。
自相关机制
Autoformer 使用自相关机制替代标准的缩放点积注意力机制。该机制基于周期性发现子序列的相似性,并利用这种相似性聚合相似的子序列。这个巧妙的机制通过将标准缩放点积注意力的逐点操作扩展到子序列级操作,从而打破了信息瓶颈。整体机制的初始部分类似于标准的注意力过程,其中我们使用权重矩阵将查询、键和值投影到相同的维度。关键区别在于注意力权重的计算方式以及它们如何被用来计算值。这个机制通过使用两个显著的子机制来实现这一点:发现基于周期的依赖关系和时间延迟聚合。
基于周期的依赖关系
Autoformer 使用自相关作为相似性度量。正如我们所知,自相关表示给定时间序列 X[t] 与其滞后序列之间的相似性。例如, 是时间序列 X[t] 与
之间的自相关。Autoformer 将这个自相关视为特定滞后的未归一化置信度。因此,从所有
中,我们选择 k 个最可能的滞后并使用 softmax 将这些未归一化的置信度转换为概率。我们使用这些概率作为权重来聚合相关子序列(我们将在下一节讨论这一点)。
自相关计算并不是最高效的操作,Autoformer 提出了一个替代方案,使得计算更快。根据 Wiener–Khinchin 定理(该内容在随机过程中涉及,超出了本书的范围,但对于感兴趣的读者,我在进一步阅读部分提供了链接),自相关也可以通过 快速傅里叶变换 (FFT) 进行计算。过程如下所示:
这里, 表示 FFT,
表示共轭操作(复数的共轭是具有相同实部和虚部符号相反的数值,大小相等。有关这一部分的数学推导超出了本书的范围)。
这可以很容易地用 PyTorch 编写如下:
# calculating the FFT of Query and Key
q_fft = torch.fft.rfft(queries.permute(0, 2, 3, 1).contiguous(), dim=-1)
k_fft = torch.fft.rfft(keys.permute(0, 2, 3, 1).contiguous(), dim=-1)
# Multiplying the FFT of Query with Conjugate FFT of Key
res = q_fft * torch.conj(k_fft)
现在, 处于频谱域。为了将其带回实数域,我们需要做一个逆 FFT:
这里, 表示逆 FFT。在 PyTorch 中,我们可以轻松实现:
corr = torch.fft.irfft(res, dim=-1)
当查询和键相同,计算的是自注意力;当它们不同,计算的是交叉注意力。
现在,我们所需要做的就是从 corr 中取出前 k 个值,并用它们来聚合子序列。
时间延迟聚合
我们通过 FFT 和逆 FFT 确定了自相关的主要滞后。例如,我们使用的数据集(伦敦智能电表数据集)具有半小时的频率,并且具有强烈的日常和每周季节性。因此,自相关识别可能已将 48 和 48*7 选为最重要的两个滞后。在标准的注意力机制中,我们使用计算出的概率作为权重来聚合值。Autoformer 也做了类似的事情,但它不是将权重应用于单个点,而是将它们应用于子序列。
Autoformer 通过将时间序列按滞后进行平移来实现此功能,,然后使用滞后权重对其进行聚合:
这里, 是 softmax 归一化后的 top-k 自相关概率。
在我们的示例中,我们可以将其理解为将序列平移 48 个时间步长,以使前一天的时间步长与当前日对齐,然后使用 48 个滞后的权重进行缩放。然后,我们可以转到 487 的滞后,将前一周的时间步长与当前周对齐,再使用 487 滞后的权重进行缩放。因此,最终我们将获得一个加权的季节性模式混合,这些模式可以在日常和每周中观察到。由于这些权重是由模型学习的,我们可以假设不同的模块学习专注于不同的季节性,因此整体上,模块学习了时间序列中的总体模式。
使用 Autoformer 进行预测
Autoformer 实现在 NIXTLA 预测中。我们可以使用之前用于 NBEATS 的相同框架,并扩展它以在我们的数据上训练 Autoformer。首先,让我们看看实现中的初始化参数。
我们必须记住,Autoformer 模型不支持外生变量。它官方支持的唯一附加信息是全局时间戳信息,如周、月等,以及假期信息。我们可以从技术上将其扩展到任何类别特征(静态或动态),但目前不支持任何实值信息。
让我们看看实现中的初始化参数。
Autoformer 类具有以下主要参数:
-
distil:这是一个布尔标志,用于开启或关闭注意力蒸馏。 -
encoder_layers:这是一个整数,表示编码器层的数量。 -
decoder_layers:这是一个整数,表示解码器层的数量。 -
n_head:这是一个整数,表示注意力头的数量。 -
conv_hidden_size:这是一个整数参数,指定卷积编码器的通道数,可以类似于控制卷积层中内核或滤波器的数量。通道的数量有效地决定了应用于输入数据的不同滤波器的数量,每个滤波器捕捉不同的特征。 -
activation:这是一个字符串值,可以是relu或gelu中的一个。它是用于编码器和解码器层的激活函数。 -
factor:这是一个整数值,帮助我们控制在我们讨论的自相关机制中将要选择的 top-k 值。top_k = int(self.factor * math.log(length))是使用的准确公式,但我们可以将 k 看作一个因子来控制前 K 的选择。 -
dropout:这是一个介于 0 和 1 之间的浮动值,决定网络中 dropout 的强度。
笔记本警告:
训练 Autoformer 模型的完整代码可以在 Chapter16 文件夹中的 03-Autoformer_NeuralForecast.ipynb 笔记本中找到。
让我们换个话题,来看一下提出的用于挑战 Transformer 的一类简单线性模型,用于长期时间序列预测(LTSF)。
LTSF-线性系列模型
关于 Transformer 是否适合用于预测问题,许多论文讨论了 Transformer 论文普遍没有使用强基线来展示其优越性、顺序不敏感的注意力机制可能不是处理强序列时间序列的最佳方法等等。对于长期时间序列预测的批评更为明显,因为它更多依赖于提取强趋势和季节性。2023 年,曾爱玲等人决定对 Transformer 模型进行测试,并通过使用 5 个多元数据集进行广泛研究,将五种 Transformer 模型(FEDFormer、Autoformer、Informer、Pyraformer 和 LogTrans)与他们提出的一组简单线性模型进行对比。令人惊讶的是,他们提出的简单线性模型轻松超过了所有 Transformer 模型。
参考检查:
曾爱玲等人的研究论文和不同的 Transformer 模型,FEDFormer、Autoformer、Informer、Pyraformer 和 LogTrans,分别在 参考文献 部分被引用为 14、16、9、8、15 和 17。
在 LTSF 模型系列中,作者提出了三种模型:
-
线性
-
D-线性
-
N-线性
这些模型非常简单,以至于它们能超越 Transformer 模型简直有些让人羞愧。但一旦你更了解它们,你可能会欣赏到这些模型内建的简单但有效的归纳偏置。让我们一一来看。
线性
正如名字所示,这是一个简单的线性模型。它取上下文窗口并应用一个线性层来预测预测视野。它还将不同的时间序列视为独立通道,并对每个通道应用不同的线性层。在 PyTorch 中,我们只需要为每个通道拥有一个 nn.Linear 层:
# Declare nn.Linear for each channel
layers = nn.ModuleList([nn.Linear(context_window, forecast_horizon) for _ in range(n_timeseries)])
## Forward Method ##
# Now use these layers once you get the input (Batch, Context Length, Channel)
forecast = layers[i for i in range(n_timeseries)]
这个令人尴尬的简单模型能够超越一些 Transformer 模型,如 Informer、LogTrans 等等。
D-线性
D-Linear 将简单的线性模型与一个分解先验结合起来。我们在第三章中已经看到如何将时间序列分解为趋势、季节性和残差。D-Linear 正是这样做的,并使用移动平均(窗口或核大小是一个超参数)将输入时间序列x分解为趋势t(移动平均)和剩余部分r(季节性+残差)。接下来,它对t和r分别应用独立的线性层,最后将它们加在一起得到最终的预测。让我们来看一下简化的PyTorch实现:
# Declare nn.Linear for each channel, trend and seasonality separately
trend_layers = nn.ModuleList([nn.Linear(context_window, forecast_horizon) for _ in range(n_timeseries)])
seasonality_layers = nn.ModuleList([nn.Linear(context_window, forecast_horizon) for _ in range(n_timeseries)])
## Forward Method ##
# Now use these layers once you get the input (Batch, Context Length, Channel)
# series_decomp is a function extracting trend using moving aveages
trend, seasonality = series_decomp(input)
trend_forecast = trend_layers[i for i in range(n_timeseries)]
seasonality_forecast = seasonality_layers[i for i in range(n_timeseries)]
forecast = [trend_forecast[i] + seasonality_forecast[i] for i in range(n_timeseries)]
模型中的分解先验使其比简单的线性模型表现得更好,并且它在研究中几乎所有数据集上都超过了所有 Transformer 模型。
N-Linear
作者还提出了另一种模型,向线性模型添加了另一个非常简单的修改。这个修改是用来处理时间序列数据中固有的分布性变化。在 N-Linear 中,我们只需提取输入上下文中的最后一个值,并将其从整个序列中减去(进行一种规范化处理),然后使用线性层进行预测。现在,一旦线性层的输出可用,我们再将之前减去的最后一个值加回去。在 PyTorch 中,一个简单的实现可能是这样的:
# Declare nn.Linear for each channel
layers = nn.ModuleList([nn.Linear(context_window, forecast_horizon) for _ in range(n_timeseries)])
## Forward Method ##
# Extract the last value once you get the input (Batch, Context Length, Channel)
# Get the last value of time series
last_value = sample_data[:,-1:,:]
# Normalize the time series
norm_ts = sample_data - last_value
# Use the linear layers
output = layers[i for i in range(n_timeseries)]
# Add back the last value
forecast = [o + last_value[:,:,i] for i, o in enumerate(output)]
与研究中的其他 Transformer 模型相比,N-Linear 模型表现得也相当优秀。在研究中大多数数据集里,N-Linear 或 D-Linear 都成为了表现最好的模型,这一点非常值得注意。
本文揭示了我们在使用 Transformer 模型进行时间序列预测时存在的一些重大缺陷,特别是在多变量时间序列问题上。Transformer 的典型输入形式是(Batch x Time steps x Embedding)。预测多变量时间序列的最常见方法是将所有时间序列或其他特征作为 embedding 传入每个时间步。这会导致看似不相关的值被嵌入到一个单一的 token 中,并在注意力机制中混合在一起(而注意力机制本身并不强制按顺序排列)。这就导致了“混乱”的表示,进而使得 Transformer 模型可能在从数据中提取真实模式时遇到困难。
本文产生了如此大的影响,以至于许多新模型,包括 PatchTST 和 iTransformer(我们将在本章后面看到这些模型),都将这些模型作为基准,并且表现优于它们。这强调了需要保留强大而简单的方法作为可靠的基准,以避免被任何算法的“炫酷性”所误导。
现在让我们看看如何使用这些简单的线性模型并获得良好的长期预测。
使用 LTSF-Linear 家族进行预测
NLinear 和 DLinear 在 NIXTLA 预测中实现,采用了我们在前述模型中看到的相同框架。
让我们看看实现中的初始化参数。
DLinear 类与许多其他模型具有类似的参数。以下是一些主要参数:
-
moving_avg_window:这是一个整数值,表示用于趋势季节性分解的窗口大小。此值应该是一个奇数。 -
exclude_insample_y:这是一个布尔值,用于跳过自回归特征。
NLinear 类没有额外的参数,因为它只是一个输入窗口到输出窗口的映射。
笔记本提醒:
训练 D-Linear 模型的完整代码可以在04-DLinear_NeuralForecast.ipynb笔记本中找到,N-Linear 模型的代码则位于05-NLinear_NeuralForecast.ipynb笔记本中的Chapter16文件夹里。
从业者提示:
这个辩论尚无定论,因为 Transformer 模型在不断修改以适应时间序列预测。可能总有某些数据集,在这些数据集上使用基于 Transformer 的模型能够比其他类型的模型获得更好的性能。作为从业者,我们应该能够怀疑一切,并尝试不同类别的模型,看看哪种模型适合我们的应用场景。毕竟,我们关心的只是我们正在预测的数据集。
现在,让我们看看如何根据 LTSF-Linear 论文的见解修改 Transformer 来用于时间序列,并展示它如何优于我们刚才看到的简单线性模型。
Patch 时间序列 Transformer(PatchTST)
2021 年,Alexey Dosovitskiy 等人提出了 Vision Transformer,该架构将 Transformer 架构广泛应用于自然语言处理领域,并取得了巨大成功,随后也被引入到计算机视觉中。尽管不是第一个引入 patch 技术的模型,但他们在视觉任务中的应用方式非常成功。该设计将图像分成多个 patch,并依次将每个 patch 输入到 Transformer 中。
参考检查:
Alexey Dosovitskiy 等人关于 Vision Transformers 的研究论文和 Yuqi Nie 等人关于 PatchTST 的研究论文分别在参考文献部分中标记为12和13。
快进到 2023 年,我们看到同样的 patch 设计被应用于时间序列预测。Yuqi Nie 等人提出了Patch 时间序列 Transformer(PatchTST),通过为时间序列采用 patch 设计。它们的动机是更复杂的 Transformer 设计(如 Autoformer 和 Informer)在时间序列预测中的效果并不明显。
2023 年,Zheng 等人通过将多个 Transformer 模型与一个简单的线性模型进行比较,展示了该线性模型在常见基准测试中优于大多数 Transformer 模型。论文的一个关键洞察是,时间序列点对点地应用到 Transformer 架构无法捕捉时间序列数据中的局部信息和强排序性。因此,作者提出了一种更简单的替代方案,它比线性模型表现更好,并解决了在不增加内存和计算需求的情况下将长时间窗口引入 Transformer 的问题。
PatchTST 模型的架构
PatchTST 模型是对 Transformers 的修改。以下是其主要贡献:
-
分片:一种有条理的方法,用于将序列的历史信息与其他信息结合,这有助于捕捉长期信号,如周、月、节假日等。
-
通道独立性:一种将多变量时间序列作为独立时间序列进行处理的概念方式。虽然我不认为这是一项重大贡献,但这确实是我们需要注意的地方。
让我们更详细地看看这些参数。
分片
我们在本章早些时候看到了一些针对时间序列预测的 Transformer 改进方法。它们都集中在使注意力机制适应时间序列预测和更长的上下文窗口。但它们都以逐点的方式使用注意力。让我们通过一个图示来澄清这一点,并引入分片的概念。
图 16.5:输入 Transformer 的分片与非分片时间序列
在图 16.5中,我们以一个有 8 个时间步长的时间序列为例。在左侧,我们可以看到我们讨论过的所有其他 Transformer 架构如何处理时间序列。它们使用一些机制,如 AutoFormer 中的统一表示,将时间序列的每个点转换为 k 维嵌入,然后将其逐点输入到 Transformer 架构中。每个点的注意力机制是通过查看上下文窗口中所有其他点来计算的。
PatchTST 论文声称,这种点对点的注意力机制并不能有效捕捉时间序列的局部性,提出将时间序列转换为片段,并将这些片段馈送到 Transformer 中。分片不过是将时间序列变成更短的时间序列,这一过程与我们之前在书中看到的滑动窗口操作非常相似(或者几乎相同)。主要的不同之处在于,这种分片是在我们已经从更大的时间序列中采样一个窗口后进行的。
分片通常由几个参数定义:
-
片段长度(P)是每个子时间序列或片段的长度。
-
步幅(S)是两个连续片段之间不重叠区域的长度。更直观地说,这就是每次分片迭代时我们移动的时间步数。这与卷积中的步幅具有完全相同的意义。
固定这两个参数后,一个长度为L的时间序列将会产生 个片段。在这里,我们还将最后一个值的重复数字填充到原始序列的末尾,以确保每个片段的大小相同。
在图 16.5中,我们可以看到我们已经说明了一个时间序列补丁处理的过程,长度为!,包含!,和!。使用刚才看到的公式,我们可以计算出!。我们还可以看到,最后一个值 8 已经在末尾重复,作为填充,使得最后一个补丁的长度也为 4。
现在,考虑到每个补丁可以看作一种嵌入,并被传入并由 Transformer 架构处理。通过这种输入补丁,对于给定的上下文!,Transformer 的输入令牌数量大约可以减少到!。这意味着计算复杂度和内存使用也减少了约!。这使得模型在相同的硬件限制下能够处理更长的上下文窗口,从而可能提高模型的预测性能。
现在,让我们来看一下通道独立性。
通道独立性
多变量时间序列可以看作是一个多通道信号。Transformer 输入可以是单个通道或多个通道。大多数基于 Transformer 的多变量预测模型采用的是将通道混合在一起并处理的方法。换句话说,输入令牌从所有时间序列中获取信息,并将其投影到共享的嵌入空间,混合信息。而其他更简单的方法则分别处理每个通道,PatchTST 的作者将这种独立性引入到了 Transformer 中。
实际上,这非常简单。让我们通过一个例子来理解。考虑一个数据集,其中有!个时间序列,形成一个多变量时间序列。因此,PatchTST 的输入将是!,其中!是批量大小,!是上下文窗口的长度。补丁处理后,它变成了!,其中!是补丁的数量,!是补丁的长度。现在,为了以通道独立的方式处理这个多变量信号,我们只需要将张量重塑,使得每个 M 个时间序列变成批次中的一个样本,即!,其中!。
虽然这种独立性为模型带来了一些理想的特性,但这也意味着不同时间序列之间的任何交互都被忽略,因为它们被视为完全独立。模型仍然是在全局模型范式下训练的,并将从跨领域学习中受益,但不同时间序列之间的任何显式交互(如两个时间序列一起变化)并未被捕捉。
除了这些主要组件外,架构与标准 Transformer 架构非常相似。现在,让我们来看看如何使用 PatchTST 进行实际的预测。
使用 PatchTST 进行预测
PatchTST 在 NIXTLA 预测中得到了实现。与之前使用的框架相同,这里也可以使用相同的框架来实现 PatchTST。
让我们来看一下实现的初始化参数。
PatchTST 类具有以下主要参数:
-
encoder_layers:这是一个整数,表示编码器层的数量。 -
hidden_size:该参数设置嵌入和编码器的大小,直接影响模型的能力及其从数据中捕获信息的能力。这是编码器和解码器层使用的激活函数。 -
patch_len和stride:这些参数定义了输入序列如何被划分成补丁,进而影响模型如何感知时间依赖性。patch_len控制每个段的长度,而stride则影响这些段之间的重叠部分。 -
stride:该参数设置嵌入和编码器的大小,直接影响模型的能力及其从数据中捕获信息的能力。这是编码器和解码器层使用的激活函数。
正则化参数:
-
dropout:这是一个介于 0 和 1 之间的浮动值,用于确定网络中的丢弃强度。 -
fc_dropout:这是一个浮动值,表示线性层的丢弃率。 -
head_dropout:这是一个浮动值,表示扁平化层的丢弃率。 -
attn_dropout:这是一个浮动值,表示注意力层的丢弃率。笔记本提示:
训练 PatchTST 模型的完整代码可以在
Chapter16文件夹中的06-PatchTST_NeuralForecast.ipynb笔记本中找到。
现在,让我们看一个基于 Transformer 的模型,它从 PatchTST 中获得创新,并反过来加以改进,取得了良好的效果,超越了 LTSF-Linear 模型。
iTransformer
我们已经详细讨论了 Transformer 架构在处理多变量时间序列时的不足之处,即无法高效捕获局部性,基于顺序无关的注意力机制使得跨时间步的信息混乱等问题。在 2024 年,Yong Liu 等人对这个问题持有稍微不同的看法,并用他们自己的话说,“一种极端的补丁处理方式。”
iTransformer 的架构
他们提出,问题不在于 Transformer 架构在时间序列预测中无效,而是其使用不当。作者建议,我们可以翻转输入到 Transformer 架构,使得注意力机制不再跨时间步应用,而是跨不同的变量或时间序列的不同特征进行应用。图 16.6 清楚地展示了这种差异。
图 16.6:Transformers 与 iTransformers 的差异
在原始 Transformer 中,我们使用输入为(批次 x 时间步 x 嵌入(特征)),注意力机制在时间步之间应用,最终,按位置的前馈网络将不同的特征混合成一个变元混合表示。但当你将输入反转为(批次 x 嵌入(特征) x 时间步)时,注意力机制会在变量之间进行计算,按位置的前馈网络会混合时间并保持变元在变元未混合表示中的独立性。
这种“反转”带来了一些额外的好处。由于注意力机制不再跨时间进行计算,我们可以在计算和内存约束较小的情况下包含非常大的上下文窗口(记住,计算和内存复杂度来自注意力机制的 O(N²))。事实上,论文建议将整个时间序列历史作为上下文窗口。另一方面,我们需要注意包含在模型中的特征数或并发时间序列的数量。
一个典型的 Transformer 架构包含以下主要组件:
-
注意力机制
-
前馈网络
-
层归一化
在倒转版本中,我们已经看到,注意力机制是跨变元应用的,前馈网络(FFN)学习了用于最终预测的回溯窗口的可泛化表示。层归一化在倒转版本中也表现得很好。在标准 Transformer 中,层归一化通常用于归一化每个时间步的多变量表示。但在倒转版本中,我们对每个变元在时间维度上分别进行归一化。这类似于我们在 N-Linear 模型中做的归一化,并且已被证明对非平稳时间序列问题有效。
参考检查:
Yong Liu 等人关于 iTransformer 的研究论文在参考文献部分被引用为18。
使用 iTransformer 进行预测
iTransformer 在 NIXTLA 预测中实现。可以使用与之前相同的框架,也可以在这里使用 iTransformer。
iTransformer类有以下主要参数:
-
n_series:这是一个整数,表示时间序列的数量。 -
e_layers:这是一个整数,表示编码器层的数量。 -
d_layers:这是一个整数,表示解码器层的数量。 -
d_ff:这是一个整数,表示在编码器和解码器层中使用的 1 维卷积层的核数。
笔记本提醒:
训练 iTransformer 模型的完整代码可以在Chapter16文件夹中的07-iTransformer_NeuralForecast.ipynb笔记本中找到。
现在,让我们来看一下另一种非常成功的架构,它设计得很好,能够在全局上下文中利用各种信息。
时间融合 Transformer(TFT)
TFT 是一种从全局建模角度精心设计的模型,以最有效地利用各种静态和动态变量信息为特点。TFT 在所有设计决策中都注重解释性。其结果是一个高性能、可解释和全局的深度学习模型。
参考检查:
Lim 等人关于 TFT 的研究论文在参考文献部分被引用为10。
乍一看,模型架构看起来复杂且令人望而生畏。但一旦你剥开这层外皮,它其实相当简单和巧妙。我们将一级一级地深入模型,以帮助您更好地理解。在这个过程中,会有很多黑箱我会请您默认接受,但不用担心——我们会逐一打开它们,深入探讨。
TFT 的架构
在我们开始之前,让我们先建立一些符号和设置。我们有一个包含I个唯一时间序列的数据集,每个实体i都有一些静态变量(s[i])。所有实体的所有静态变量的集合可以用S表示。我们还有长度为k的上下文窗口。除此之外,我们还有时间变化的变量,这些变量有一个区别——对于某些变量,我们没有未来数据(未知),而对于其他变量,我们知道未来(已知)。让我们用输入的上下文窗口x[t-k]…x[t]来表示所有时间变化信息(上下文窗口、已知和未知的时间变化变量)。未来的已知时间变化变量用表示,其中
是预测的时间跨度。有了这些符号,我们准备好来看第一层抽象化了:
图 16.7:TFT—高层概述
这里有很多内容需要分析。让我们从静态变量S开始。首先,静态变量通过变量选择网络(VSN)传递。VSN 对实例级特征进行选择,并对输入进行一些非线性处理。处理后的输入被馈送到一组静态协变量编码器(SEs)。SE 块被设计为以一种原则性的方式整合静态元数据。
如果你按照图 16.6中 SE 模块的箭头,你会看到静态协变量在架构中三个(四个不同输出)不同的地方被使用。我们将在讨论这些模块时看到它们是如何在这些地方使用的。但这些不同的地方可能在关注静态信息的不同方面。为了让模型具有这种灵活性,处理过并经过变量选择的输出被输入到四个不同的门控残差网络(GRNs)中,这四个 GRN 会分别生成四个输出——c[s]、c[e]、c[c]和c[h]。我们稍后会解释什么是 GRN,但现在只需要理解它是一个可以进行非线性处理的模块,并带有残差连接,这使得它在需要时可以跳过非线性处理。
过去的输入,x[t-k]…x[t],以及未来已知的输入,,也会通过各自的 VSN,并且这些处理后的输出会被输入到局部增强(LE)的 Seq2Seq 层。我们可以把 LE 看作是一种将局部上下文和时间顺序编码到每个时间步的嵌入中的方式。这类似于普通 Transformer 中的位置编码。我们还可以看到在
Conv1d层中也有类似的尝试,这些层用于在 Autoformer 模型中对历史进行编码。我们稍后会探讨 LE 内部的工作原理,但现在只需要理解它捕捉了基于其他观测变量和静态信息的局部上下文。我们将这个模块的输出称为局部编码上下文向量( 和
)。
术语、符号和主要模块的分组与原始论文中不同。我对这些进行了修改,以使它们更易于理解。
现在,这些 LE 上下文向量被输入到时间融合解码器(TFD)中。TFD 以 Transformer 模型中类似的方式应用了多头自注意力的微小变体,并生成解码表示()。最后,这个解码表示通过门控线性单元(GLU)和一个Add & Norm模块,后者将 LE 上下文向量作为残差连接添加进去。
GLU 是一个帮助模型决定需要允许多少信息流过的单元。我们可以将其视为一个学习的信息节流装置,它在自然语言处理(NLP)架构中广泛应用。公式非常简单:
这里,W和V是可学习的权重矩阵,b和c是可学习的偏置,是激活函数,
是哈达玛积运算符(元素逐个相乘)。
Add & Norm模块与普通 Transformer 中的相同;我们在第十四章,时间序列的注意力和 Transformer中讨论过这一点。
现在,为了完美收尾,我们有一个Dense层(带偏置的线性层),它将Add & Norm模块的输出投影到所需的输出维度。
现在是我们在抽象层次上进一步下沉的时候了。
局部增强 Seq2Seq 层
让我们剥开洋葱,看看 LE Seq2Seq 层内部发生了什么。让我们从一张图开始:
图 16.8:TFT—LE Seq2Seq 层
LE 使用 Seq2Seq 架构来捕获局部上下文。处理过程从过去的输入开始。LSTM 编码器接收这些过去的输入,x[t-k]…x[t]。c[h] c[c]来自静态协变量编码器,作为 LSTM 的初始隐藏状态。编码器逐步处理每个时间步,产生每个时间步的隐藏状态,H[t-k]…H[t]。最后的隐藏状态(上下文向量)被传递给 LSTM 解码器,解码器处理已知的未来输入,,并在每个未来时间步生成隐藏状态,
。最后,所有这些隐藏状态都通过一个GLU + AddNorm模块,带有来自 LSTM 处理前的残差连接。输出是 LE 上下文向量(
和
)。
现在,让我们看看下一个模块:TFD。
时间融合解码器
让我们从另一张图开始讨论:
图 16.9:时间融合变换器—时间融合解码器
来自过去输入和已知未来输入的 LE 上下文向量被连接成一个单一的 LE 上下文向量。现在,这可以被视为在 Transformer 范式中的位置编码标记。TFD 首先做的是用来自静态协变量编码器创建的静态信息c[e]来丰富这些编码。它与嵌入一起连接。一个逐位置的 GRN 用于丰富嵌入。这些增强的嵌入现在作为掩码可解释多头注意力模块的查询、键和值。
论文提出,掩码可解释多头注意力模块学习了跨时间步的长期依赖关系。局部依赖关系已经通过 LE Seq2Seq 层在嵌入中捕获,但逐点的长期依赖关系则通过掩码可解释多头注意力捕获。该模块还增强了架构的可解释性。生成的注意力权重为我们提供了一些关于过程涉及的主要时间步的信息。然而,从可解释性的角度来看,多头注意力有一个缺点。
在传统的多头注意力中,我们为值使用了独立的投影权重,这意味着每个头的值是不同的,因此注意力权重并不容易解释。
TFT 通过使用一个共享权重矩阵将值投影到注意力维度上,克服了这一限制。即使有共享的值投影权重,由于每个查询和键的投影权重不同,每个头部仍然可以学习不同的时间模式。除了这一点,TFT 还使用了掩蔽机制,确保在操作中不会使用来自未来的信息。我们在第十四章,时间序列的注意力和变换器中讨论了这种因果掩蔽。通过这两项修改,TFT 将这一层命名为掩蔽可解释的多头注意力。
到这里,我们可以打开我们所使用的最后一个也是最精细的抽象层级了。
门控残差网络
我们已经讨论 GRN 有一段时间了;到目前为止,我们只是从表面上看待它们。现在让我们来理解一下 GRN 内部发生了什么——这是 TFT 中最基础的构建块之一。
让我们来看一个 GRN 的示意图,以更好地理解它:
图 16.10:TFT—GRN(左)和 VSN(右)
GRN 接收两个输入:主输入a和外部上下文c。上下文c是一个可选输入,如果不存在,则视为零。首先,两个输入a和c通过单独的密集层和随后的激活函数进行变换——指数线性单元(ELU)(pytorch.org/docs/stable/generated/torch.nn.ELU.html)。
现在,变换后的a和c输入被加在一起,然后通过另一个Dense层再次进行变换。最后,这通过一个带有来自原始a的残差连接的GLU+加法与归一化层进行处理。这个结构结合了足够的非线性,能够学习输入之间的复杂交互,但同时通过残差连接让模型能够忽略这些非线性。因此,这样的模块可以让模型根据数据规模调整所需的计算量。
变量选择网络
TFT 的最后一个构建块是 VSN。VSN 使得 TFT 能够进行实例级的变量选择。大多数真实世界的时间序列数据集包含许多预测能力较弱的变量,因此能够自动选择那些具有预测能力的变量,将有助于模型挑选出相关的模式。图 16.9(右)展示了这个 VSN。
这些附加变量可以是分类的也可以是连续的。TFT 使用实体嵌入将分类特征转换为我们所需的数值向量维度(d[model])。我们在第十五章《全球深度学习预测模型策略》中讨论过这个问题。连续特征则被线性转换(独立地)为相同维度的向量,d[model]。这为我们提供了变换后的输入,,其中m是特征数量,t是时间步长。我们可以将所有这些嵌入(扁平化)拼接在一起,得到的扁平化表示可以表示为!。
现在,这些嵌入被处理的方式有两个平行流:一个用于对嵌入进行非线性处理,另一个用于特征选择。每个嵌入通过独立的 GRN 进行处理(但所有时间步共享),以得到非线性处理后的嵌入,。在另一个流中,VSN 处理扁平化表示,
,以及可选的上下文信息c,并通过带有 softmax 激活函数的 GRN 进行处理。这为我们提供了一个权重向量v[t],它的长度为m。现在,v[t]被用于对所有非线性处理后的特征嵌入进行加权求和,
,该加权和计算如下:
使用 TFT 进行预测
TFT在 NIXTLA 预报中得到了实现。我们可以使用与 NBEATS 相同的框架,并将其扩展以在我们的数据上训练TFT。此外,NIXTLA 支持外生变量,就像 N-BEATSx 处理外生变量一样。首先,让我们看一下实现的初始化参数。
NIXTLA 中的TFT类有以下主要参数:
-
hidden_size:这是一个整数,表示模型中的隐藏维度。在这个维度中,所有的 GRN、VSN、LSTM 隐藏层、self-attention 隐藏层等都会进行计算。可以说,这是模型中最重要的超参数。 -
n_head:这是一个整数,表示注意力头的数量。 -
dropout:这是一个介于 0 和 1 之间的浮动值,决定变量选择网络中的 dropout 强度。 -
attn_dropout:这是一个介于 0 和 1 之间的浮动值,决定解码器注意力层中 dropout 的强度。
笔记本提醒:
完整的 TFT 训练代码可以在Chapter16文件夹中的08-TFT_NeuralForecast.ipynb笔记本中找到。
解释 TFT
TFT 从一个稍微不同的角度来探讨可解释性,与 N-BEATS 不同。N-BEATS 为我们提供了解构输出以实现可解释性,而 TFT 则让我们看清模型是如何解释其使用的变量的。由于 VSN,我们可以轻松访问特征权重。类似于树模型中的特征重要性,TFT 也能提供类似的评分。由于自注意力层,注意力权重也可以被解释,帮助我们理解哪些时间步骤在注意力机制中占据较大权重。
PyTorch Forecasting 通过执行几个步骤使这一切成为可能。首先,我们通过 predict 函数中的 mode="raw" 获取原始预测。然后,我们将这些原始预测用于 interpret_output 函数。interpret_output 函数中有一个名为 reduction 的参数,用于决定如何聚合不同实例的权重。我们知道 TFT 在 VSN 中进行实例级的特征选择,且注意力机制也是按实例进行的。'mean' 是查看全局可解释性的一个不错选择:
raw_predictions, x = best_model.predict(val_dataloader, mode="raw", return_x=True)
interpretation = best_model.interpret_output(raw_predictions, reduction="sum")
这个 interpretation 变量是一个字典,包含模型中不同方面的权重,例如 attention、static_variables、encoder_variables 和 decoder_variables。PyTorch Forecasting 还为我们提供了一个简单的方式来可视化这些重要性:
best_model.plot_interpretation(interpretation)
这会生成四个图表:
图 16.11:解释 TFT
我们还可以查看每个实例,并为每个预测绘制类似的可视化图表。我们只需要使用 reduction="none",然后自行绘制即可。附带的笔记本探讨了如何实现这一点及更多内容。
现在,让我们换个角度,看看一些模型,这些模型证明了简单的 MLP 同样能够匹敌甚至超越基于 Transformer 的模型。
TSMixer
当基于 Transformer 的模型如火如荼地推进时,一条平行的研究轨迹开始使用多层感知机(MLPs)代替 Transformer 作为关键学习单元。这一趋势始于 2021 年,当时 MLP-Mixer 展示了仅使用 MLP 就能在视觉问题上达到最先进的性能,取代了卷积神经网络。于是,类似的 MLP 混合架构开始在各个领域出现。2023 年,Google 的 Si-An Chen 等人将 MLP 混合引入了时间序列预测。
参考检查:
Si-An 等人关于 TSMixer 的研究论文在参考文献部分被引用为19。
TSMixer 模型的架构
TSMixer 确实从 Transformer 模型中汲取了灵感,但它试图通过 MLP 模拟类似的过程。让我们通过图 16.10来理解其相似性与差异。
图 16.12:Transformer 与 TSMixer 对比
如果你观察 Transformer 块,我们可以看到有一个多头注意力机制,它会跨时间步长进行观察,并利用注意力机制“混合”它们。然后,这些输出会传递给位置逐步前馈网络,它们将不同的特征混合在一起。从这些灵感中,TSMixer 也在混合器块中包含了一个时间混合组件和一个特征混合组件。时间投影层将混合器块的输出投影到输出空间。
让我们逐层解释。图 16.11展示了整个架构。
图 16.13:TSMixer 架构
输入是一个多变量时间序列,它被送入 N 个混合器层,逐个处理,最终混合器层的输出被送入时间投影层,时间投影层将学到的表示转换为实际的预测。尽管图表和论文中提到了“特征”,但它们并不是我们在本书中讨论的特征。这里的“特征”指的是多变量设置中的其他时间序列。
现在,让我们深入研究混合器层,看看时间混合和特征混合是如何在一个块内工作的。
混合器层
图 16.14:TSMixer—混合器块
输入的形式为(批量大小 x 特征 x 时间步长),首先通过时间混合块。输入首先被转置为(批量大小 x 时间步长 x 特征)的形式,这样,时间混合 MLP 中的权重就能混合时间步长。现在,这个“时间混合”的输出被传递到特征混合 MLP,后者使用其权重来混合不同的特征,得到最终的学习表示。批量归一化层和残差连接被加入其中,以使模型更加健壮,并学习更深层次和更平滑的连接。
给定输入矩阵 ,时间混合可以用数学公式表示为:
特征混合实际上是一个两层的 MLP,其中一层将数据投影到隐藏维度H[inner],下一层从H[inner]投影到输出维度H。如果没有明确指定,这将默认使用原始特征数量(或时间序列数量)C。
因此,整个混合器层可以表示为:
现在,这个输出被传递到时间投影层以得到预测。
时间投影层
图 16.15:TSMixer—时间投影层
时间投影层只是一个应用于时间域的全连接层。这与我们之前看到的简单线性模型相同,在这个模型中,我们将全连接层应用于输入上下文以得到预测。TSMixer 并不是将该层应用于输入,而是将其应用于来自混合器层的“混合”输出。
前一层的输出形式为(批量大小 x 时间步长 x 特征)。它被转置为(批量大小 x 特征 x 时间步长),然后通过一个全连接层,该层将输入投影到(批量大小 x 特征 x 预测视野),从而得到最终的预测结果。
给定作为第k个 Mixer Layer 的输出和预测视野,T:
那么,如何加入额外的特征呢?许多时间序列问题有静态特征和动态(面向未来的)特征,这为问题增加了相当多的信息。到目前为止,我们讨论的架构并未让你包含这些信息。为此,作者提出了一个小的调整来包括这些额外信息,即 TSMixerx。
TSMixerx—带辅助信息的 TSMixer
按照之前的符号约定,考虑我们有输入的时间序列(或时间序列集合),。现在,我们将有一些历史特征,
,一些面向未来的特征,
,以及一些静态特征,
。为了有效地包含所有这些额外信息,作者定义了另一个学习单元,称为条件特征混合层(Conditional Feature Mixing layer),并以一种能够同化所有信息的方式使用它。
条件特征混合(CFM)层几乎与特征混合层相同,不同之处在于增加了一个处理静态信息和特征的额外层。静态信息首先被重复跨越时间步长,并通过线性层投影到输出维度。然后,它与输入特征连接,连接后的输入再被“混合”在一起,并投影到输出维度。
数学上,它可以表示为:
其中,表示连接操作,而Expand表示将静态信息重复到所有时间步长中。
现在,让我们看看 CFM 层如何在整体 TSMixerx 架构中使用。
图 16.16:TSMixerx—使用外生变量的架构
首先,我们有X和,它们有L个时间步长,即上下文窗口的长度。因此,我们将它们连接,并使用一个简单的时间投影层将组合张量投影到
,其中T是预测视野的长度,这也是面向未来特征Z的长度。接着,我们使用 CFM 层将其与静态信息结合,并将它们投影到一个隐藏维度,H。形式上,这一步表示为:
现在,我们希望有条件地混合面向未来的特征,Z,以及静态信息,S。因此,我们使用 CFM 层来实现这一点,并将这些组合信息投影到一个隐藏维度,H。
在这一点上,我们有! 和 ,它们都在!维度下。因此,我们使用另一个 CFM 层进一步混合这些特征,并根据!进行条件处理。这给了我们第一个混合特征的潜在表示,!。
现在,这个潜在表示通过!后续的 CFM(类似于常规的 TSMixer 架构),其中!是总的 Mixer 层数,最终得到!,即最终的潜在表示。共有!层,因为第一个 Mixer 层已经定义并且在输入维度上与其他层不同。
现在,我们可以使用一个简单的线性层将这个输出投影到所需的输出维度。如果它是单一时间序列的点预测,我们可以将其投影到!。如果我们预测的是 M 个时间序列,那么我们可以将其投影到!。
使用 TSMixer 和 TSMixerx 进行预测
TSMixer 在 NIXTLA 预测中实现,使用的是我们在之前模型中看到的相同框架。
让我们看一下实现的初始化参数。
TSMixer类具有以下主要参数:
-
n_series:这是一个整数值,表示时间序列的数量。 -
n_block:这是一个整数值,表示模型中使用的混合层数量。 -
ff_dim:这是一个整数值,表示第二个前馈层中要使用的单元数量。 -
revin:这是一个布尔值,如果为 True,则使用可逆实例归一化来处理输入和输出(ICLR 2022 论文:openreview.net/forum?id=cGDAkQo1C0p)。
类似于 NBEATX,TSMixerx类可以处理外生信息。要使用外生信息进行预测,您需要将适当的信息添加到下面的参数中:
-
futr_exog_list:这是一个未来外生列的列表。 -
hist_exog_list:这是一个历史外生列的列表。 -
stag_exog_list:这是一个外生列的列表。
笔记本提醒:
用于训练 TSMixer 模型的完整代码可以在Chapter16文件夹中的09-TSMixer_NeuralForecast.ipynb笔记本中找到。
现在让我们看一下另一种基于 MLP 的架构,它已经证明比我们之前看到的 PatchTST 和线性系列模型表现得更好。
时间序列稠密编码器(TiDE)
我们在本章早些时候看到,线性模型家族的表现超越了许多 Transformer 模型。2023 年,Google 的 Das 等人提出了一种将这一思想扩展到非线性的模型。他们认为,当未来与过去之间存在内在的非线性依赖关系时,线性模型会失效。而协变量的加入加剧了这个问题。
参考检查:
Das 等人关于 TiDE 的研究论文在参考文献部分被引用为20。
因此,他们提出了一种简单高效的基于多层感知机(MLP)的架构,用于长期时间序列预测。该模型本质上通过密集的 MLP 编码时间序列的过去及其协变量,然后将这个潜在表示解码为一个预测。该模型假设通道独立性(类似于 PatchTST),并将多元问题中不同相关的时间序列视为单独的时间序列。
TiDE 模型的架构
该架构有两个主要组件——编码器和解码器。但在整个架构中,他们称之为残差块的一个学习组件被重复使用。让我们先看看残差块。
残差块
残差块是一个带有 ReLU 和后续线性投影的 MLP,允许残差连接。图 16.14显示了一个残差块。
图 16.17:TiDE:残差块
我们通过设置隐藏维度和输出维度来定义该层。第一层 Dense 将输入转换为隐藏维度,然后对输出应用 ReLU 非线性。然后,该输出被线性投影到输出维度,并在其上堆叠一个丢弃层。残差连接随后通过将输入投影到输出维度并使用另一个线性投影来添加到输出中。最后,输出通过层归一化进行处理。
设为该块的输入,h为隐藏维度,o为输出维度。那么,残差块可以表示为:
让我们建立一些符号,以帮助我们理解接下来的解释。数据集中有N个时间序列,L是回溯窗口的长度,H是预测的长度。因此,第 i^(th)个时间序列的回溯可以表示为,而它的预测是
。在时间
的 r 维动态协变量表示为
。第 i^(th)个时间序列的静态特征是
。
现在,让我们看一下图 16.15中的更大架构。
图 16.18:TiDE:整体架构
编码器
编码器的任务是将回溯窗口及其相应的协变量映射到一个稠密的潜在表示。第一步是对动态协变量进行线性投影,,映射到
,其中
,称为时间宽度,远小于r。我们使用残差块来完成这个投影。
从程序的角度来看(其中B是批量大小),如果动态协变量的输入维度是,我们将其投影到
。
这样做是为了当我们将时间序列及其协变量展平后输入编码器时,结果张量的维度不会爆炸。这就引出了下一步,即张量的展平和拼接操作。展平和拼接操作类似于这样:
-
回溯窗口:
-
动态协变量:
-
静态信息:
-
拼接表示:
现在,这个拼接后的表示会通过一组n[e]个残差块进行编码,转换成稠密的潜在表示。
从程序的角度来看,维度从转换到
,其中H是潜在表示的隐藏层大小。
现在我们已经得到了潜在表示,让我们来看一下如何从中解码预测结果。
解码器
和编码器一样,解码器也有两个独立的步骤。在第一步中,我们使用一组n[d]个残差块将潜在表示解码成维度为的解码向量,其中
是解码器输出维度。这个解码向量被重新形状为
维的向量。
现在,我们使用时间解码器将这个解码向量转换成预测。时间解码器仅是一个残差块,它接受拼接后的解码向量和编码后的未来外生向量
。作者认为,这个残差连接允许某些未来的协变量在预测中产生更强的影响。例如,如果在零售预测问题中,未来的协变量之一是节假日,那么你希望这个变量对预测有很强的影响。
这个残差连接帮助模型在需要时启用“高速公路”功能。
最后,我们添加一个全局残差连接,将回溯窗口线性映射到预测结果,经过线性映射到正确的维度。这确保了我们在本章前面看到的线性模型变成了 TiDE 模型的一个子类。
使用 TiDE 进行预测
TiDE 在 NIXTLA 预测中实现,采用了我们在之前的模型中看到的相同框架。
让我们来看一下实现的初始化参数。
TIDE 类有以下主要参数:
-
decoder_output_dim:一个整数,控制解码器输出中的单元数量。它直接影响解码序列的维度,并且可能会影响模型重建目标序列的能力。
-
temporal_decoder_dim:一个整数,定义了时间解码器的输出大小。尽管我们讨论过时间解码器的输出是最终预测结果,NeuralfForecast通过网络输出到期望输出维度的统一映射。因此,temporal_decoder_dim表示倒数第二层的维度,最终将被转换为最终输出。该维度的大小决定了你允许传递到最终预测层的信息量。 -
num_encoder_layers:堆叠在一起的编码器层的数量。 -
num_decoder_layers:堆叠在一起的解码器层的数量 -
temporal_width:一个整数,影响较低时间投影维度,影响外生数据如何被投影和处理。它在模型如何整合和学习外生信息方面起着重要作用。
-
layernorm:该布尔标志决定是否应用层归一化。层归一化可以稳定并加速训练,可能会提高性能,特别是在更深的网络中。
此外,TIDE 可以处理外生信息,这些信息可以包含在以下参数中:
-
futr_exog_list:接受一个未来外生列的列表 -
hist_exog_list:接受一个历史外生列的列表 -
stag_exog_list:这是一个外生列的列表笔记本提醒:
用于训练 TIDE 模型的完整代码可以在
Chapter16文件夹中的10-TIDE_NeuralForecast.ipynb笔记本中找到。
我们已经介绍了几种常见的时间序列预测专用架构,但这绝不是完整的列表。外面有很多模型架构和技术。我在进一步阅读部分列出了一些,供你参考。
恭喜你成功完成了本书中可能最具挑战性和最密集的章节之一。给自己拍拍背,坐下来放松一下吧。
总结
我们关于时间序列的深度学习之旅终于迎来了结尾,我们回顾了一些时间序列预测的专门架构。我们理解了为何在时间序列和预测中采用专门架构是合理的,并且进一步了解了如何使不同的模型,如 N-BEATS、N-BEATSx、N-HiTS、Autoformer、TFT、PatchTST、TiDE 和 TSMixer 等工作。除了介绍这些架构背后的理论,我们还探讨了如何使用 neuralforecast(由 NIXTLA 开发)在实际数据集上应用这些模型。我们无法预知哪个模型在我们的数据集上效果最好,但作为从业者,我们需要培养出能够指导实验的直觉。了解这些模型的工作原理对于培养这种直觉至关重要,也将帮助我们更高效地进行实验。
本书的这一部分到此结束。到目前为止,你应该已经更熟悉如何使用深度学习来解决时间序列预测问题。
在下一章中,我们将讨论另一个在预测中非常重要的主题——概率预测。
参考文献
以下是我们在本章中使用的参考文献列表:
-
Spyros Makridakis, Evangelos Spiliotis, 和 Vassilios Assimakopoulos. (2020). M4 竞赛:100,000 个时间序列和 61 种预测方法. 《国际预测期刊》,第 36 卷,第 1 期。页码 54–74。
doi.org/10.1016/j.ijforecast.2019.04.014。 -
Slawek Smyl. (2018). M4 预测竞赛:引入一种新的混合 ES-RNN 模型。
www.uber.com/blog/m4-forecasting-competition/。 -
Boris N. Oreshkin, Dmitri Carpov, Nicolas Chapados, 和 Yoshua Bengio. (2020). N-BEATS: 可解释时间序列预测的神经基础扩展分析. 第 8 届国际学习表示大会(ICLR)。
openreview.net/forum?id=r1ecqn4YwB。 -
Kin G. Olivares, Cristian Challu, Grzegorz Marcjasz, R. Weron, 和 A. Dubrawski. (2022). 带外生变量的神经基础扩展分析:使用 NBEATSx 预测电力价格. 《国际预测期刊》,2022 年。
www.sciencedirect.com/science/article/pii/S0169207022000413。 -
Cristian Challu, Kin G. Olivares, Boris N. Oreshkin, Federico Garza, Max Mergenthaler-Canseco, 和 Artur Dubrawski. (2022). N-HiTS: 用于时间序列预测的神经层次插值. arXiv 预印本 Arxiv: Arxiv-2201.12886。
arxiv.org/abs/2201.12886。 -
Vaswani, Ashish, Shazeer, Noam, Parmar, Niki, Uszkoreit, Jakob, Jones, Llion, Gomez, Aidan N, Kaiser, Lukasz, 和 Polosukhin, Illia. (2017). 注意力即你所需要的。神经信息处理系统进展.
papers.nips.cc/paper/2017/hash/3f5ee243547dee91fbd053c1c4a845aa-Abstract.html. -
Haoyi Zhou, Shanghang Zhang, Jieqi Peng, Shuai Zhang, Jianxin Li, Hui Xiong, 和 Wancai Zhang. (2021). Informer: 超越高效的 Transformer 用于长序列时间序列预测。第三十五届{AAAI}人工智能会议,{AAAI} 2021.
ojs.aaai.org/index.php/AAAI/article/view/17325. -
Haixu Wu, Jiehui Xu, Jianmin Wang, 和 Mingsheng Long. (2021). Autoformer: 带有自动相关性的分解 Transformer 用于长期序列预测。神经信息处理系统进展 34: 2021 年神经信息处理系统年会,NeurIPS 2021, 2021 年 12 月 6 日至 14 日.
proceedings.neurips.cc/paper/2021/hash/bcc0d400288793e8bdcd7c19a8ac0c2b-Abstract.html. -
Bryan Lim, Sercan Ö. Arik, Nicolas Loeff, 和 Tomas Pfister. (2019). 时间融合 Transformer 用于可解释的多视角时间序列预测。国际预测期刊,第 37 卷,第 4 期,2021 年,第 1,748–1,764 页.
www.sciencedirect.com/science/article/pii/S0169207021000637. -
Alexey Dosovitskiy, Lucas Beyer, Alexander Kolesnikov, Dirk Weissenborn, Xiaohua Zhai, Thomas Unterthiner, Mostafa Dehghani, Matthias Minderer, Georg Heigold, Sylvain Gelly, Jakob Uszkoreit, 和 Neil Houlsby. (2021). 一张图片值 16x16 个词:用于大规模图像识别的 Transformer。第九届国际学习表征大会,ICLR 2021.
openreview.net/forum?id=YicbFdNTTy. -
Yuqi Nie, Nam H. Nguyen, 和 Phanwadee Sinthong 和 J. Kalagnanam. (2022). 一条时间序列值 64 个词:使用 Transformer 的长期预测。第十届国际学习表征大会,ICLR 2022.
openreview.net/forum?id=Jbdc0vTOcol. -
Ailing Zeng 和 Mu-Hwa Chen, L. Zhang, 和 Qiang Xu. (2023). Transformer 是否对时间序列预测有效? AAAI 人工智能会议.
ojs.aaai.org/index.php/AAAI/article/view/26317. -
Liu, Shizhan 和 Yu, Hang 和 Liao, Cong 和 Li, Jianguo 和 Lin, Weiyao 和 Liu, Alex X 和 Dustdar, Schahram. (2022). Pyraformer: 低复杂度的金字塔注意力机制用于长时间序列建模与预测. 国际学习表征会议 (ICLR)。
openreview.net/pdf?id=0EXmFzUn5I -
Zhou, Tian 和 Ma, Ziqing 和 Wen, Qingsong 和 Wang, Xue 和 Sun, Liang 和 Jin, Rong. (2022). {FEDformer}: 增强频率的分解变换器用于长期时间序列预测。第 39 届国际机器学习会议 (ICML 2022)。
proceedings.mlr.press/v162/zhou22g.html -
Shiyang Li, Xiaoyong Jin, Yao Xuan, Xiyou Zhou, Wenhu Chen, Yu-Xiang Wang 和 Xifeng Yan. (2019). 增强局部性并打破变换器在时间序列预测中的记忆瓶颈. 神经信息处理系统进展(Advances in Neural Information Processing Systems)。
proceedings.neurips.cc/paper_files/paper/2019/file/6775a0635c302542da2c32aa19d86be0-Paper.pdf -
Yong Liu, Tengge Hu, Haoran Zhang, Haixu Wu, Shiyu Wang, Lintao Ma 和 Mingsheng Long. (2024). iTransformer:倒转变换器在时间序列预测中的有效性。第 12 届国际学习表征会议 (ICLR 2024)。
openreview.net/forum?id=JePfAI8fah -
Si-An Chen, Chun-Liang Li, Sercan O Arik, Nathanael Christian Yoder 和 Tomas Pfister. (2023). TSMixer: 一种全 MLP 架构的时间序列预测模型。机器学习研究期刊(Transactions on Machine Learning Research)。
openreview.net/forum?id=wbpxTuXgm0 -
Abhimanyu Das, Weihao Kong, Andrew Leach, Shaan Mathur, Rajat Sen 和 Rose Yu. (2023). 使用 TiDE 进行长期预测:时间序列稠密编码器. 机器学习研究期刊(Transactions on Machine Learning Research)。
openreview.net/forum?id=pCbC3aQB5W
进一步阅读
你可以查阅以下资源进行进一步阅读:
-
Fast ES-RNN: ES-RNN 算法的 GPU 实现:
arxiv.org/abs/1907.03329和github.com/damitkwr/ESRNN-GPU -
作为向量空间的函数:
www.youtube.com/watch?v=NvEZol2Q8rs -
使用 N-BEATS 进行预测,由 Gaetan Dubuc 提供:
www.kaggle.com/code/gatandubuc/forecast-with-n-beats-interpretable-model/notebook -
WaveNet: 一种用于音频生成的模型,由 DeepMind 提供:
www.deepmind.com/blog/wavenet-a-generative-model-for-raw-audio -
什么是残差连接?,由 Wanshun Wong 撰写:
towardsdatascience.com/what-is-residual-connection-efb07cab0d55 -
高效的 Transformer:一项综述,由 Tay 等人撰写:
arxiv.org/abs/2009.06732 -
自相关与 Wiener-Khinchin 定理:
www.itp.tu-berlin.de/fileadmin/a3233/grk/pototskyLectures2012/pototsky_lectures_part1.pdf -
使用深度神经网络建模长短期时间模式,由 Lai 等人撰写:
dl.acm.org/doi/10.1145/3209978.3210006和github.com/cure-lab/SCINet -
全球思考,本地行动:一种基于深度神经网络的高维时间序列预测方法,由 Sen 等人撰写:
proceedings.neurips.cc/paper/2019/hash/3a0844cee4fcf57de0c71e9ad3035478-Abstract.html和github.com/rajatsen91/deepglo
加入我们社区的 Discord
加入我们社区的 Discord 空间,与作者和其他读者进行讨论: