时间序列的注意力与Transformer

在上一章中,我们卷起袖子实现了几套用于时间序列预测的深度学习DL)系统。我们使用了在第12章时间序列深度学习的构建模块中讨论的常见构建模块,将它们组合成编码器-解码器架构,并训练它们生成所需的预测。

现在,我们来聊聊深度学习中的另一个关键概念,它在过去几年席卷了整个领域——注意力。注意力有着悠久的历史,已成为深度学习工具箱中最受欢迎的工具之一。本章将带领你从理论角度从头理解注意力与Transformer模型,并通过实际例子巩固理解。

本章涵盖以下主要主题:

技术要求

你需要按照本书前言中的说明设置Anaconda环境,以获得一个包含本书代码所需的所有库和数据集的运行环境。任何额外的库将在运行notebooks时安装。

你需要为本章运行以下notebooks:

本章的关联代码可在 https://github.com/PacktPublishing/Modern-Time-Series-Forecasting-with-Python-/tree/main/notebooks/Chapter14 找到。

什么是注意力?

注意力的概念受人类认知功能的启发。在任何时刻,我们眼睛的视神经、鼻子的嗅神经和耳朵的听觉神经都会向大脑发送大量的感觉输入。这些信息量过于庞大,远超大脑的处理能力。但我们的大脑发展出了一种机制,帮助我们只注意那些重要的刺激——例如不属于当下环境的声音或气味。经过多年的进化,我们的大脑被训练出识别异常声音或气味的能力,因为这对我们在有捕食者游荡的野外生存至关重要。在认知科学中,注意力被定义为一种认知过程,它允许个体选择性地关注特定信息,同时忽略其他无关刺激。

除了这种本能式的注意力,我们还能通过所谓的聚焦来主动控制注意力。你现在就在这样做:选择忽略所有其他刺激,将注意力集中在这本书的内容上。当你阅读时,手机发出提示音,屏幕亮起,你的大脑决定将注意力转向手机屏幕,尽管书仍在你面前打开着。人类认知功能的这一特性启发了深度学习中的注意力机制。赋予学习机器这种注意能力,导致了当今人工智能所有领域的重大突破。

这一思想首次应用于深度学习是在Seq2Seq模型中,我们在第13章《时间序列的常见建模模式》中学习过。在那章中,我们看到了编码器和解码器之间如何传递信息。对于循环神经网络RNN)系列模型,我们使用序列末尾编码器的隐藏状态作为解码器的初始隐藏状态。我们将这种传递称为上下文。这里的假设是,解码任务所需的所有信息都编码在上下文中,并且以逐时间步的方式完成。因此,对于长上下文窗口,第一个时间步的信息必须通过多次写入和重写保留,直到在最后一个时间步使用。这会形成一种信息瓶颈(图14.1),模型可能难以通过这个有限的上下文保留重要信息。之前的隐藏状态中可能存在对解码任务有用的信息。2015年,Bahdanau等人(参考文献1)提出了深度学习背景下第一个已知的注意力模型。他们提出学习输入序列每个隐藏状态的注意力权重 $\alpha$,并在解码时将它们组合成一个单一的上下文向量。

这些注意力权重在每一步解码时重新计算,通过计算解码过程中的隐藏状态与输入序列中所有隐藏状态之间的相似度(图14.2):)

图14.1 – Seq2Seq模型中的传统模型(上)与注意力模型(下)

图14.1:Seq2Seq模型中的传统模型(上)与注意力模型(下)

为了使事情更清晰,我们采用一种正式的方式来描述该机制。令 $H = h_j, i \in \{1, 2, ..., T_s\}$ 为编码过程中生成的隐藏状态,且 $S = s_j, j \in \{1, 2, ..., T_t\}$ 为解码过程中生成的隐藏状态。上下文向量将为 cj

图14.2 – 使用注意力机制的解码

图14.2:使用注意力机制的解码

因此,现在我们有了来自编码阶段的隐藏状态(H),我们需要一种方法在每一步解码中使用这些信息。关键在于,在解码过程的每一步,不同隐藏状态的信息可能都相关。这正是注意力权重的作用。因此,对于解码步骤 j,我们使用 sj-1 并计算注意力权重(我们将很快详细了解注意力权重是如何学习的),ai, j,通过计算 sj-1H 中每个隐藏状态之间的相似度。现在,我们计算上下文向量,它以正确的方式组合了 H 中的信息:

$$c_j = \sum_{i=1}^{T_s} \alpha_{i,j} h_i$$

有两种主要方式可以使用这个上下文向量,我们将在本章后面更详细地讨论。这打破了传统Seq2Seq模型中存在的信息瓶颈,使模型能够访问更大的信息池,并决定在解码过程的每一步哪些信息是相关的。

现在,让我们看看这些注意力权重 $\alpha$ 是如何计算的。

通用注意力模型

多年来,研究人员提出了不同的计算注意力权重和在深度学习模型中使用注意力的方法。Sneha Chaudhari等人(参考文献 8)发表了一篇关于注意力模型的综述论文,提出了一个通用注意力模型,试图将所有变体整合到一个单一框架中。让我们围绕这个通用框架来组织我们的讨论。

我们可以将注意力模型视为学习一组键(K)的注意力分布($\alpha$),使用一组查询(q)。在上一节讨论的例子中,查询是 Sj-1——解码过程中上一个时间步的隐藏状态——而键是 H——使用输入序列生成的所有隐藏状态。在某些情况下,生成的注意力分布被应用于另一组输入,称为值(V)。在许多情况下,KV 是相同的,但为了保持框架的一般形式,我们将它们分开考虑。使用这些术语,我们可以将注意力模型定义为 qKV 的函数:

$$A(q, K, V) = \sum_{i} p(a(k_i, q)) \times v_i$$

这里,a 是一个对齐函数,用于计算查询(q)和键(ki)之间的相似度或相似度概念,而 vi 是对应于索引 $i$ 的值。在上一节讨论的例子中,这个对齐函数计算编码器隐藏状态与解码器隐藏状态的相关程度,而 p 是一个分布函数,它将这个分数转换为总和为1的注意力权重。

参考文献检查:

Sneha Choudhari 等人的研究论文在参考文献部分被引用为参考文献 8

现在我们有了广义的注意力模型,让我们看看如何在 PyTorch 中实现它。完整的实现可以在 Attention 类的 src/dl/attention.py 中找到,但我们将在这里介绍其中的关键部分。

我们事先初始化这样一个模块所需的唯一信息是查询和键(编码器和解码器)的隐藏维度。因此,类的定义和类的 __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 函数,它接受两个输入:

我们假设键和值是相同的,因为在大多数情况下它们确实相同。所以,从广义注意力模型我们知道需要执行以下几个步骤:

  1. 计算每个查询和键组合的对齐分数——akiq)。
  2. 通过应用函数——p((a((ki, , q)))) 将分数转换为权重。
  3. 使用学习到的权重来组合值——$\sum_i p(a(k_i, q)) \times v_i$..

那么,让我们在 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 年提出了这种注意力形式。从线性代数中,我们知道两个向量的点积告诉我们一个向量在另一个向量方向上有多少分量。它衡量两个向量之间的某种相似性,这种相似性同时考虑了向量的大小和它们在向量空间中的夹角。因此,当我们对查询和键向量取点积时,我们得到了它们之间的相似性概念。这里需要注意的一点是,查询和键的隐藏维度必须相同才能应用点积注意力。形式上,相似性函数可以定义如下:

$$a(k_i, q) = q^T \cdot k_i$$

我们需要为键 K 中的每个元素 Ki 计算这个分数,而不是对每个元素循环,我们可以使用巧妙的矩阵乘法技巧一次性计算 K 中所有键的分数。让我们看看如何定义点积注意力的 _get_scores 函数。

从前一节我们知道,查询和值(在本例中与键相同)的维度分别为(batch sizedecoder dimension)和(batch sizesequence lengthencoder dimension),并且在 _get_scores 函数中将分别称为 qv。在这种特殊情况下,解码器维度和编码器维度相同,因此分数可以计算如下:

scores = (q @ v.transpose(1,2))

这里,@torch.matmul的简写,用于执行矩阵乘法。整个实现命名为DotProductAttention,可以在src/dl/attention.py中找到。

缩放点积注意力

2017年,Vaswani等人在开创性论文Attention Is All You Need中提出了这种注意力机制。我们将在本章后面深入探讨该论文,但现在,让我们理解他们对点积注意力提出的一项关键修改。这一修改的动机是,当输入较大时,我们用于将分数转换为权重的softmax函数的梯度可能非常小,从而使高效学习变得困难。

这是因为softmax函数不是尺度不变的。函数中的指数函数是导致这种行为的原因。因此,我们对函数的输入缩放得越大,最大输入就越主导输出,这限制了网络中的梯度流动。如果我们假设qv是均值为0、方差为1的dk维向量,那么它们的点积的均值为0、方差为dk。因此,如果我们按$\sqrt{d_k}$缩放点积的输出,那么点积的方差就会回到1。因此,通过控制softmax函数输入的尺度,我们可以在网络中保持健康的梯度流。延伸阅读部分提供了一个链接,指向一篇更深入讨论此问题的博客文章。因此,缩放点积对齐函数可以定义如下:

$$a(k_i, q) = \frac{1}{\sqrt{d_k}} \times q^T k_i$$

因此,我们只需要对PyTorch的实现做一处改动:增加一行代码:

scores = scores/math.sqrt(encoder_dim)

这已在src/dl/attention.py中的DotProductAttention实现为一个参数。如果在初始化类时传递scaled=True,它将执行缩放点积注意力。我们需要记住,与点积注意力类似,缩放变体也要求查询和值具有相同的维度。

通用注意力

2015年,Luong等人(参考文献2)提出了点积注意力的一种微小变体,即在计算中引入一个可学习的W矩阵。他们称之为通用注意力。我们可以将其视为一种注意力机制,它允许查询在使用点积计算相似度分数之前,通过W矩阵投影到一个与值/键相同维度的学习平面上。对齐函数可以写成如下形式:

$$a(k_i, q) = q^T W k_i$$

相应的PyTorch实现在src/dl/attention.py中以GeneralAttention的名称提供。计算注意力分数的关键行可以写成如下形式:

scores = (q @ self.W) @ v.transpose(1,2)

这里,self.W是一个大小为(编码器隐藏维度×解码器隐藏维度)的张量。通用注意力可用于查询和键/值维度不同的情况。

加性/拼接注意力

2015年,Bahdanau等人提出了加性注意力,这是最早尝试将注意力引入深度学习系统的成果之一。Bahdanau等人没有使用诸如点积之类的定义好的相似度函数,而是提出相似度函数可以通过学习获得,从而让网络在决定什么内容相似时拥有更大的灵活性。他们建议将查询和键拼接成一个张量,并使用一个可学习的矩阵来计算注意力分数。这个对齐函数可以写成如下形式:

$$a(k_i, q) = w_{imp}^T \tanh(W_q q^T + W_k k_i + b)$$

这里,vtWqWk是可学习的矩阵。当查询和键的隐藏维度不同时,我们可以用WqWk将它们投影到同一维度,然后进行相似度计算。如果查询和键具有相同的隐藏维度,这也等价于Luong等人使用的注意力变体,他们称之为concat注意力,表示如下:

$$a(k_i, q) = w_{imp}^T \tanh(W[q^T; k_i] + b)$$

不难通过简单的线性代数看出两者是相同的,并且这样做是为了工程简便。进一步阅读部分提供了一个Stack Overflow答案的链接,解释了这种等价性。

我们在src/dl/attention.py中包含了两种实现,分别位于ConcatAttentionAdditiveAttention下。

对于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_qself.W_v将查询和值投影到相同的维度,并且第3行应用tanh激活函数,并与self.v进行矩阵乘法以生成最终分数。self.W_qself.W_vself.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_qself.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]

因此,我们可以认为AdditiveAttentionConcatAttention执行相同的操作,但是AdditiveAttention适应于不同的编码器和解码器维度。

参考文献检查:

Luong等人、Badahnau等人和Vaswani等人的研究论文分别在参考文献部分被引用为参考文献215

现在我们已经了解了几种流行的对齐函数,接下来让我们关注注意力模型的分布函数。

分布函数

分布函数的主要目标是将对齐函数学习到的分数转换为一组总和为1的权重。softmax函数是最流行的分布函数选择。它将分数转换为一组总和为1的权重。这也让我们能够将学习到的权重解释为概率——即对应元素最相关的概率。

尽管softmax是最流行的选择,但它并非没有缺点。softmax权重通常是稠密的。这意味着对于计算注意力的序列中的每个元素,都会分配一个概率质量(某个权重)。权重可能很低,但仍然不为0。在某些情况下,分布函数的稀疏性是理想的。也许我们希望确保不给某些不合理的选择分配任何权重。也许我们希望使注意力机制更具可解释性。

存在其他分布函数,如sparsemax(Martins等人,2016,参考文献3)和entmax(Peters等人,2019,参考文献4),它们能够将概率质量分配给少数几个相关元素,并将其余元素分配为零。当我们知道输出仅依赖于编码器中的少数时间步时,可以使用此类分布函数将该知识编码到模型中。类似Sparsemax的稀疏激活函数具有可解释性的优势,因为它们可以更清晰地区分重要元素(非零概率)和不重要元素(零概率)。

参考文献检查:

Martins等人和Peters等人的研究论文分别在参考文献部分被引用为参考文献34

现在我们已经了解了几种注意力机制,是时候将它们付诸实践了。

基于序列到序列模型与注意力的预测

让我们从第13章《时间序列的常见建模模式》中继续,我们在那一章使用了Seq2Seq模型来预测一个样本家庭(如果你没有阅读前一章,我强烈建议你现在去读),并修改Seq2SeqModel类以包含注意力机制。

笔记本提示:

要跟随完整代码,请使用 01-Seq2Seq_RNN_with_Attention.ipynb 文件夹中的笔记本 01-Seq2Seq_RNN_with_Attention.ipynb 以及 Chapter14 文件夹中的代码和 src 文件夹中的代码。

我们仍然要继承在src/dl/models.py中定义的BaseModel类,整体结构与Seq2SeqModel类非常相似。关键区别在于,在我们的新模型中,带有注意力机制,我们不接受全连接层作为解码器。这不是因为不可能,而是为了实现的便捷和简洁。实际上,实现一个带有全连接解码器的Seq2Seq模型是你自己可以做到的,以真正内化这个概念。

Seq2SeqConfig类类似,我们定义了一个非常相似的Seq2SeqwAttnConfig类,它具有完全相同的参数集,但增加了一些验证检查。其中一项验证检查是禁止使用全连接解码器。另一项验证检查是确保解码器输入大小也允许注意力机制。我们很快会详细看到这些要求。

除了Seq2SeqwAttnConfig,我们还定义了一个Seq2SeqwAttnModel类以实现启用注意力的解码。这里唯一的额外参数是attention_type,它是一个字符串参数,接受以下值:

完整代码见src/dl/models.py。我们只会在书中详细介绍forward函数,因为这是唯一存在关键区别的地方。类的其余部分涉及根据输入参数定义正确的注意力模型等。

编码器部分与上一章看到的SeqSeqModel完全相同。唯一的区别在于解码部分,我们将使用注意力机制。

现在,我们来讨论如何在解码时使用注意力输出。

如前所述,关于如何在解码时使用注意力,有两种不同的观点。沿用我们一直在使用的注意力术语,让我们来看看它们之间的区别。

Luong 等人使用解码器在第j步的隐藏状态sj来计算其与所有编码器隐藏状态H的相似度,从而得到上下文向量cj。然后,该上下文向量cj与解码器隐藏状态sj拼接,这个拼接后的张量被用作生成输出的线性层的输入。

Bahdanau 等人以另一种方式使用注意力。他们使用上一时间步的解码器隐藏状态sj-1来计算与所有编码器隐藏状态H的相似度,从而得到上下文向量cj。然后,该上下文向量cj与解码步骤j的输入xj拼接。正是这个拼接后的输入被用于使用 RNN 的解码步骤中。

我们可以在图14.3中直观地看到这些差异。延伸阅读部分还有一个关于注意力的精彩动画Attn: Illustrated Attention,这也能帮助理解该机制:

图14.3 – 基于注意力的解码:Bahdanau 与 Luong 对比

图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行Seq2SeqMode 类中相同,我们设置变量来存储预测,并提取要传递给解码器的起始输入;第3行开始循环,逐步进行解码。

现在,在每一步中,我们需要使用前一时间步的隐藏状态来计算上下文向量。如果你还记得 RNN 的输出形状(第12章时间序列深度学习构建模块),我们知道它是(层数批量大小隐藏大小)。但我们需要查询隐藏状态的维度为(批量大小隐藏大小)。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模型汇总表

图14.4:带注意力的Seq2Seq模型汇总表

我们可以看到,通过引入注意力机制,MAEMSEMASE都有显著改善,而在所有注意力变体中,简单点积注意力表现最好,紧随其后的是加性注意力。此时,你们中的一些人可能会产生疑问——为什么缩放点积注意力没有比点积注意力更好?缩放本应使点积注意力效果更好,不是吗?

这里有一个值得吸取的教训(适用于所有机器学习ML))。无论某种技术在理论上多么优越,你总能找到它表现较差的例子。在这里,我们只看到了一个家庭,缩放点积注意力没有比普通点积注意力更好并不奇怪。但如果我们在大规模上评估并意识到这在多个数据集上是一个模式,那才令人担忧。

因此,我们看到注意力确实使模型变得更好。大量研究致力于使用各种形式的注意力来增强神经网络NN)模型的性能。这些研究大部分在自然语言处理NLP)领域进行,特别是在语言翻译和语言建模中。很快,研究人员偶然发现了一个令人惊讶的结果,极大地改变了深度学习进程——Transformer模型。

Transformer——注意力即一切

虽然注意力的引入给RNN和Seq2Seq模型注入了强心剂,但它们仍然存在一个问题。RNN是循环的,这意味着它们需要按顺序处理句子中的每个单词。

对于流行的Seq2Seq模型应用,如语言翻译,这意味着处理长序列单词变得非常耗时。简而言之,难以将它们扩展到大规模语料库。2017年,Vaswani等人(参考文献5)发表了一篇开创性论文《注意力即一切》(Attention Is All You Need)。正如论文标题所暗示的,他们探索了一种使用注意力(缩放点积注意力)并完全抛弃循环网络的架构。令全世界的NLP研究人员惊讶的是,这些被称为Transformer的模型在语言翻译上超越了当时最先进的Seq2Seq模型。

这引发了对这类新模型的大量研究活动,很快,在2018年,Devlin等人(参考文献6)来自谷歌开发了Transformer的双向版本,并训练了现在著名的语言模型BERT(代表来自Transformer的双向编码器表示),在多个任务上打破了许多最先进的结果。这被认为是Transformer作为一个模型类真正到来的时刻。快进到2022年——Transformer模型无处不在。它们几乎被用于NLP中的所有任务,以及许多其他基于序列的任务,如时间序列预测和强化学习RL)。它们也已成功应用于计算机视觉CV)任务。

为了使其更适合时间序列预测,人们对原始Transformer模型进行了许多修改和调整。但我们首先讨论Vaswani等人于2017年提出的原始Transformer架构。

注意力即一切

Vaswani等人提出的模型(此处称为原始Transformer)也是一个编码器-解码器模型,但编码器和解码器都不是循环结构。它们完全由注意力机制和前馈网络组成。由于Transformer模型最初是为文本序列开发的,我们使用同样的例子来理解,然后将其适应到时间序列场景。

模型有几个关键组件需要理解,才能将它们组合在一起。我们逐一介绍。

自注意力

我们在本章前面看到了缩放点积注意力是如何工作的(在对齐函数部分),但在那里,我们计算的是编码器和解码器隐藏状态之间的注意力。自注意力是指我们有一个输入序列,并计算该输入序列自身的注意力分数。直观上,我们可以认为这个操作增强了上下文信息,并使下游组件能够使用这些增强的信息进行进一步处理。

我们之前看到了编码器-解码器注意力的PyTorch实现,但那个实现更偏向于RNN的逐步解码。一次性计算每个查询-键对的注意力分数是使用标准矩阵乘法非常容易实现的事情,并且对计算效率至关重要。

笔记本提示:

要跟随完整代码,请使用Chapter14文件夹中的名为02-Self-Attention_and_Multi-Headed_Attention.ipynb的笔记本。

在NLP中,标准做法是将每个单词表示为一个可学习的向量,称为嵌入。这是因为文本或字符串在数学模型中没有位置。为了举例,我们假设每个单词使用大小为512的嵌入向量,并假设注意力机制的内部维度为64。我们用一个包含10个单词的句子来阐明注意力机制。

嵌入后,句子将是一个维度为(10, 512)的张量。我们需要三个可学习的权重矩阵:WqWkWv,将输入嵌入投影到注意力维度(64)。请参见图14.5

图14.5 – 自注意力层:输入句子与可学习权重

图14.5: 自注意力层:输入句子与可学习权重

第一个操作将句子张量投影到查询、键和值中,其维度等于(序列长度注意力维度)。这是通过句子张量与可学习矩阵之间的矩阵乘法完成的。参见图14.6

图14.6 – 自注意力层:查询、键和值的投影

图14.6:自注意力层:查询、键和值的投影

现在我们有了查询、键和值,我们可以通过查询与键的转置之间的矩阵乘法来计算每个查询-键对的注意力权重。矩阵乘法不过是每个查询与每个值的点积,并给我们一个(序列长度序列长度)的方阵。参见图14.7

图14.7:自注意力层:Q与K之间的注意力分数

将注意力分数转换为注意力权重只需要进行缩放并应用softmax函数,正如我们在缩放点积注意力部分讨论的那样。

现在我们有了注意力权重,可以用它们来组合值。逐元素相乘然后跨权重求和可以通过另一个矩阵乘法高效完成。参见图14.8

图14.8 – 自注意力层:使用学习到的注意力权重组合V

图14.8:自注意力层:使用学习到的注意力权重组合V

现在,我们已经看到了如何以整体矩阵操作的方式将注意力应用于所有查询-键对,而不是以顺序方式对每个查询执行相同操作。但注意力就是一切提出了更好的方法。

多头注意力

由于Transformer试图完全取消循环架构,它们需要增强注意力机制,因为那是模型的主力。因此,论文作者没有使用单个注意力头,而是提出了在多个子空间中共同作用的多个注意力头。我们知道注意力帮助模型关注众多元素中的少数几个。多头注意力MHA)做同样的事情,但侧重于不同方面或不同的元素集,从而增加了模型的容量。如果我们想类比人类思维,我们在做决定之前会考虑情况的许多方面。

例如,如果我们决定出门,我们会关注天气,会关注时间,以确保我们想做的事情仍然可能完成,我们会关注过去与你约好的朋友有多准时,并相应地出门。你可以把每件事看作一个注意力头。因此,MHA使Transformer能够同时关注多个方面。

通常,如果有八个头,我们可能会认为必须将上一节中看到的计算重复八次。但幸运的是,事实并非如此。有一些巧妙的方法可以利用相同类型的矩阵乘法来实现MHA,只不过现在使用更大的矩阵。让我们看看这是如何完成的。

我们将继续使用相同的示例,并考察一个具有八个注意力头的情况。要进行这种高效的MHA计算,需要满足一个条件——注意力维度应能被我们使用的头数整除。

最初的步骤完全相同。我们接受输入句子张量,并将其投影为查询、键和值。现在,通过进行一些基本的张量重排,我们将查询、键和值拆分为每个头单独的查询、键和值子空间。参见图14.9

图14.9 – 多头注意力:将Q、K和V重塑为每个头的子空间

图14.9:多头注意力:将Q、K和V重塑为每个头的子空间

现在,我们在一次操作中计算每个头的注意力分数,并与值结合,得到每个头的注意力输出。参见图14.10

图14.10:多头注意力:计算注意力权重并与值结合

attn_output变量中,我们得到了每个头的注意力输出。现在,我们只需要重塑数组,以便将所有注意力头的输出堆叠到一个维度中。参见图14.11

图14.11 – 多头注意力:重塑并堆叠所有注意力头的输出

图14.11:多头注意力:重塑并堆叠所有注意力头的输出

通过这种方式,我们可以快速高效地进行MHA。现在,让我们看看另一项使Transformer奏效的关键创新。

位置编码

Transformer成功避免了循环,并解锁了序列操作的性能瓶颈。这也意味着Transformer模型对序列的顺序不敏感。从数学上讲,如果说RNN将序列视为序列,那么Transformer则将其视为一组值。对于Transformer来说,每个位置与其他位置独立,因此处理序列时我们所期望的一个关键方面缺失了。原始作者确实提出了一种方法来确保我们不会丢失这一信息——位置编码

在随后的研究中,出现了许多位置编码的变体,但最常见的仍然是原始Transformer中使用的变体。

Vaswani等人提出的解决方案是,在每个输入令牌通过自注意力层处理之前,向其添加一个特定的向量,该向量使用正弦和余弦函数以数学方式编码位置。如果输入X是一个dmodel维的嵌入,对应序列中的n个令牌,那么位置嵌入P是一个相同大小的矩阵(n x dmodel)。第posth行、第2ith或(2i+1)th列的元素定义如下:

$$p_{pos, 2i} = \sin\left(\frac{pos}{10000^{\frac{2i}{d_{model}}}}\right)$$

$$p_{pos, 2i+1} = \cos\left(\frac{pos}{10000^{\frac{2i}{d_{model}}}}\right)$$

虽然这看起来有点复杂且反直觉,但让我们分解一下以便更好地理解。

从宏观上看,我们知道这些位置编码捕获了位置信息,并将它们添加到输入嵌入中。但我们为什么要把它们添加到输入嵌入中呢?让我们更清楚地说明这一点。假设嵌入维度仅为2(这是为了便于可视化和更好地理解概念),我们有一个单词A,用这个令牌表示。在我们的实验中,假设同一个单词A在序列中重复多次。如果我们向它添加位置编码,会发生什么?

我们知道正弦或余弦函数在0和1之间变化。因此,我们添加到词嵌入中的每个编码只是在单位圆内扰动词嵌入。随着pos的增加,我们可以看到位置编码后的词嵌入围绕原始嵌入描绘出一个单位圆(图14.12):

图14.12 – 位置编码:直观理解

图14.12:位置编码:直观理解

图14.12中,我们假设了一个单词A的随机嵌入(用十字标记表示),并假设A在不同位置,添加了位置嵌入。这些位置编码后的向量用星形标记表示,旁边的数字标明了相应的位置。我们可以看到每个位置都是原始向量的一个轻微扰动点,并且以顺时针方向循环发生。我们可以看到位置0位于最上方,然后顺时针方向依次是1、2、3等。通过这种表示,模型可以识别出不同位置的单词,同时保留其在语义空间中的总体位置。

现在我们已经知道为什么将位置编码添加到输入嵌入中,并且明白了其工作原理,接下来让我们深入了解细节,看看正弦和余弦内部的项是如何计算的。pos 表示序列中令牌的位置。如果序列的最大长度为128,则 pos 的范围为0到127。i 表示沿嵌入维度的位置,根据公式的定义方式,对于每个 i 值,我们有两个值——一个正弦和一个余弦。因此,i 将是维度数 dmodel 的一半,其范围从0到 dmodel/2。

有了所有这些信息,我们知道正弦和余弦函数内部的项在接近嵌入维度末尾时趋近于0。同时,随着我们沿序列维度移动,它从0开始增加。对于嵌入维度中的每一对位置(2i 和 2i+1),我们有一个互补的正弦波和余弦波,如图14.13所示:

图14.13 – 位置编码:正弦与余弦项

图14.13: 位置编码:正弦与余弦项

我们可以看到,嵌入维度 4041 是相同频率的正弦波和余弦波,而嵌入维度 4042 是频率略有增加的正弦波。通过使用不同频率的正弦波和余弦波的组合,位置编码可以以向量的形式编码丰富的位置信息。如果我们绘制整个位置编码向量的热力图(参见彩色图像文件:https://packt.link/gbp/9781835883181)(图14.14),我们可以看到值如何变化并编码位置信息:

图14.14 – 位置编码:整个向量的热力图

图14.14: 位置编码:整个向量的热力图

另一个有趣的观察是,随着我们在嵌入维度中向前移动,位置编码迅速缩小到0/1,因为正弦或余弦函数内部的项(以弧度为单位的角度)由于分母很大而迅速变为零。放大后的图更清晰地显示了颜色差异。

现在,介绍Transformer模型中的最后一个组件。

逐位置前馈层

我们已经在第12章《时间序列深度学习构建模块》中介绍了什么是前馈网络。这里需要注意的一点是,逐位置前馈层是指我们在每个位置上独立地应用相同的前馈层。如果我们有12个位置(或单词),我们将会有一个单一的前馈网络来处理每个位置。

Vaswani等人将其定义为两层前馈网络,其中变换的定义使得输入维度扩展为输入维度的四倍,在该阶段应用ReLU激活函数,然后再次变换回输入维度。确切的操作可以写成数学公式:

FFN((x) = ) = max(0, (0, W1x + b1) ) W2 + b2

这里,W1 是一个维度为 (input size, 4*input size) 的矩阵,W2 是一个维度为 (4*input size, input size) 的矩阵,b1b2 是相应的偏置,并且 max(0, x) 是标准的 ReLU 运算符。

已有研究尝试用其他激活函数替代 ReLU,更具体地说是具体是 门控线性单元GLU),它们展现出潜力。Google 的 Noam Shazeer 有一篇相关论文,如果你想了解更多关于这些新激活函数的信息,我建议查阅他在《进一步阅读》部分中的论文。部分中的论文。

既然我们已经了解了 Transformer 模型的所有必要组件,我们来看看它们是如何组合在一起的。

编码器

原始的 Transformer 模型是一个编码器-解码器模型。有 N 个编码器块,并且每个块包含一个 MHA 层和一个位置前馈层,中间有残差连接(图14.15):):

图14.15 – 来自 Vaswani 等人的《Attention is All You Need》的 Transformer 模型

图14.15:来自 Vaswani 等人的《Attention Is All You Need》的 Transformer 模型

现在,我们聚焦于聚焦于 图14.15 的左侧,即编码器。编码器将输入嵌入加上位置编码向量作为输入。进入 MHA 的三叉箭头表示查询、键和值的分割。MHA 的输出进入一个名为Add and Norm 的块。我们快速看看它的作用

这里发生两个关键操作——残差连接层归一化

残差连接

残差连接(或跳跃连接)是一系列引入深度学习的技术,旨在使深度网络更易于学习。该技术的主要好处是使梯度更好地流经网络,从而促进网络各部分的学习。它们在网络中纳入了一条直通记忆高速公路。我们已经看到一个实例,其中跳跃连接(尽管不明显)解决了梯度流动问题——流动问题——长短期记忆网络LSTM)。LSTM 中的细胞状态充当了这条高速公路,使梯度流经网络而不会陷入梯度消失问题。

但如今,当我们谈到残差连接时,通常会想到深度学习中引起轰动的ResNet,它通过一个在主要图像分类挑战(包括2015年ImageNet)中获胜的卷积神经网络(CNN)架构在深度学习史上留下了浓墨重彩的一笔。ResNet引入残差连接来训练比当时主流架构更深的网络。这个概念看似简单。让我们可视化它:

图14.16 – 残差网络

图14.16:残差网络

假设一个深度学习模型有两个层,函数分别为M1M2。在常规神经网络中,输入x通过这两层得到输出y。这两个单独的函数可以看作一个将x转换为y的单一函数:y = F(x)。

在残差网络中,我们改变这一范式,认为每个单独的函数(或层)只学习输入与期望输出之间的差异。这就是残差连接名称的由来。因此,如果h1是期望输出,x是输入,那么M1(x) = h1 - x。改写后得到h1 = M1(x) + x。这就是最常用的残差连接形式。

除了改善梯度流等众多优点外,残差连接还能使损失曲面平滑(Li等,2018,参考文献7),更有利于基于梯度的优化。关于残差网络的更多细节和直观理解,我强烈建议您查看进一步阅读部分中的博客。

因此,Transformer中加与归一模块里的实际上就是残差连接。

层归一化

深度神经网络(DNN)中,归一化一直是一个活跃的研究领域。归一化具有众多好处,包括加快训练速度、支持更高的学习率,甚至还有一点正则化效果。批归一化是目前最常用的归一化技术,尤其在计算机视觉中,它通过减去当前批次输入的均值并除以标准差,使输入近似为零均值和单位方差。

但在自然语言处理中,研究者更偏好层归一化,其中归一化是在每个特征上进行的。区别可见于图14.17

图14.17 – 批归一化与层归一化

图14.17:批归一化与层归一化

对层归一化的偏好源于经验,但也有研究探讨这种偏好的原因。与计算机视觉数据相比,NLP 数据通常具有更高的方差,这种方差给批归一化带来了一些问题。而层归一化则不受此影响,因为它不依赖于批次级别的方差。

无论如何,Vaswani 等人决定在他们的 Add and Norm 模块中使用层归一化。模块。

现在,我们知道 Add and Norm 模块实际上就是一个残差连接,然后通过层归一化。因此,我们可以看出,经过位置编码的输入首先用于 MHA 层,MHA 的输出再次与Add and Norm 层,这就构成了编码器的一个模块。需要记住的一个要点是,编码器中所有元素的架构都设计为使每个位置的输入维度在整个过程中保持不变。换句话说,如果嵌入向量的维度是 100,那么编码器的输出也将是 100 维。这是一种便捷的方式,使得残差连接和尽可能多地堆叠层成为可能。现在,多个这样的编码器模块相互堆叠,形成了 Transformer 的编码器

解码器

解码器模块与编码器模块非常相似,

应用于解码器模块的自注意力有一些特殊之处。让我们看看它是什么。

掩码自注意力

我们之前讨论了 Transformer 如何并行处理序列并具有计算效率。但解码范式带来了另一个挑战。假设我们有一个输入序列,X = {x1, x2, …, xn},任务是预测下一步。因此,在解码器中,如果我们给出序列 X,由于并行处理架构,每个序列都使用自注意力一次性处理。我们知道自注意力对序列顺序不敏感。如果不加限制,模型就会通过使用未来时间步的信息来预测当前时间步,从而作弊。这就是掩码注意力变得重要的原因。

我们在前面的 自注意力 部分看到了如何计算一个方形注意力权重矩阵(如果查询和键具有相同长度),正是通过这个权重我们将值向量的信息组合起来。这种自注意力没有

让我们看看 图14.18 来巩固我们的理解:

图14.18 – 掩码自注意力

图14.18: 掩码自注意力

我们有一个序列 X = {x1, x2, …, x5},并且我们仍然在尝试预测下一步。因此,解码器的预期输出将是 $\hat{X} = \{\hat{x}_2, \hat{x}_3, ...\hat{x}_6\}$。当我们使用自注意力时,学习到的注意力权重将是一个 5×5 维的方阵。但如果观察该方阵的上三角(图14.18中阴影部分),这些词元组合违反了时间的神圣性。

我们可以通过添加一个预生成掩码来简单处理这个问题,该掩码在所有白色单元格中为0,在所有阴影单元格中为 -inf,然后将其加到生成的注意力能量上(应用 softmax 之前的阶段)。这确保了阴影区域的注意力权重为零,从而保证在计算价值向量的加权和时不使用任何未来信息。

现在,总结一下,解码器的输出被传递到一个标准的任务特定头部,以生成我们所需的输出。

我们在自然语言处理(NLP)的背景下讨论了 Transformer,但将其应用于时间序列数据是非常小的一步。

时间序列中的 Transformer

时间序列与 NLP 有很多相似之处,因为两者都处理序列中的信息,并且元素的顺序都很重要。在时间序列中,元素通常是按时间排序的数据点,而在 NLP 中,元素是构成句子或文档的词元(如单词或字符)。这可以通过以下现象进一步证明:NLP 中使用的大多数流行技术都迅速被应用到时间序列领域。Transformer 也不例外。

在每个位置,我们不是看词元,而是有实数。并且不是讨论输入嵌入,而是讨论输入特征。每个时间步的特征向量可以视为 NLP 中嵌入向量的等价物。此外,因果解码在 NLP 中可能是一个可选步骤(具体取决于任务),但在时间序列中我们有严格的要求。因此,将 Transformer 应用于时间序列是简单的,尽管在实践中存在许多挑战,因为时间序列中的序列通常比 NLP 中的序列长得多,这带来了一个问题,即自注意力的复杂度随输入序列长度呈二次方增长。已经有许多自注意力的替代方案使其也适用于长序列,我们将在第16章《用于预测的专用深度学习架构》中介绍其中一些。

现在,让我们尝试将我们学到的关于 Transformer 的所有知识付诸实践。

使用 Transformer 进行预测

为了保持连续性,我们将使用之前用 RNN 和带注意力的 RNN 进行预测的同一个家庭示例。

笔记本提示

要跟随完整代码,请使用 03-Transformers.ipynb 文件夹中的笔记本 03-Transformers.ipynb 以及 Chapter14 文件夹中的代码和 src 文件夹中的代码。

尽管我们了解到原始Transformer是一个具有编码器-解码器架构的模型,但它实际上是为语言翻译任务设计的。在语言翻译中,源序列和目标序列差异很大,因此编码器-解码器架构是合理的。但很快,研究人员发现仅使用Transformer的解码器部分也能表现出色。这在文献中被称为仅解码器Transformer。这种命名有点令人困惑,因为仔细想想,解码器在两个方面与编码器不同——掩码自注意力和编码器-解码器注意力。那么,在仅解码器Transformer中,我们如何弥补编码器-解码器注意力呢?简短的回答是:我们不弥补。仅解码器Transformer的架构更类似于编码器模块,但我们称之为仅解码器,是因为我们使用掩码自注意力来使模型尊重序列的时间神圣性。

我们还将实现一个仅解码器Transformer。我们需要做的第一件事是定义一个配置类TransformerConfig,其中包含以下参数:

现在,我们将继承在 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)

前面我们提到将采用仅解码器的方法构建模型,为此我们使用在 PyTorch 中定义的 TransformerEncoderLayerTransformerEncoder 模块。请记住,使用这些层时,我们将采用掩码自注意力机制,这使其成为仅解码器 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)) 创建一个上三角(包括对角线)全为1、其余全为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模型一次性处理所有token,如果序列中有N个元素,我们也会有N个预测(每个预测对应下一个时间步)。如果每个预测对应未来H个时间步,那么y_hat的形状将是(B, N, H),其中B是批量大小。有几种方法可以将此输出与目标进行比较。最简单直接的方法是只取最后一个位置的预测(它将有H个时间步)并将其与y(也有H个时间步)进行比较。

但这并不是利用我们所有信息的最有效方式,不是吗?我们丢弃了N-1个预测,并且没有给模型提供任何关于所有这些N-1个预测的信号。因此,在训练时,使用所有这些N-1个预测也是合理的,这样模型在学习时可以拥有更丰富的反馈信号。

我们可以通过使用原始输入序列x但将其偏移一个位置来实现。当H=1时,我们可以将其视为一个简单任务,其中每个位置的预测与下一个位置的目标(一步之后)进行比较。我们可以通过将x[:,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家庭上的指标

图14.19:Transformer模型在MAC000193家庭上的指标

我们可以看到,该模型的表现不如其RNN同类。可能有很多原因,但最可能的一个是Transformer确实非常依赖数据。Transformer的归纳偏置要少得多,因此只有在有大量数据可供学习时才能大放异彩。当仅预测一个家庭时,我们的模型可用的数据要少得多,可能无法很好地工作。在一定程度上,这对于我们目前看到的所有深度学习模型都是如此。在第10章《全局预测模型》中,我们讨论了如何为多个家庭一起训练单一模型,但该讨论仅限于经典机器学习模型。深度学习也完全能够处理全局预测模型,这正是我们在下一章——第15章《全局深度学习预测模型的策略》中将要讨论的内容。

现在,祝贺你完成了又一个概念多、信息量大的章节。注意力的概念已经席卷了该领域,现在你应该比本章开始时更加清晰。我强烈建议你再读一遍本章,阅读延伸阅读部分,如果还不清楚,做一些自己的研究,因为后续章节假设你已经理解这一点。

小结

在过去的几章中,我们一直在深度学习的世界中快速推进。我们从深度学习的基本前提开始,介绍了它是什么以及为什么它变得如此流行。然后,我们看到了时间序列预测中常用的一些基本构建模块,并通过实践学习了如何利用PyTorch将所学知识付诸实践。尽管我们讨论了RNN、LSTM、GRU等,但我们特意没有涉及注意力和Transformer,因为它们值得单独用一章来介绍。

我们在本章开始时学习了广义注意力模型,帮助你为所有不同的注意力方案建立一个框架,然后详细介绍了几个常见的注意力方案,如缩放点积注意力、加性注意力和通用注意力。在将注意力纳入我们在第12章《时间序列深度学习的构建模块》中使用的Seq2Seq模型之后,我们开始了Transformer的学习。

我们从自然语言处理的角度审视了原始Transformer中的所有构建模块和架构决策,在理解架构后,我们将其调整到时间序列设置中。

最后,我们通过训练一个Transformer模型来预测一个样本家庭,结束了本章。现在,通过完成本章,我们拥有了真正开始使用深度学习进行时间序列预测的所有基本要素。

在下一章中,我们将提升迄今为止所做的内容,转向全局预测模型范式。

参考文献

以下是本章使用的参考文献列表:

  1. Dzmitry Bahdanau, KyungHyun Cho, and Yoshua Bengio (2015). Neural Machine Translation by Jointly Learning to Align and Translate. In 3rd International Conference on Learning Representations. https://arxiv.org/pdf/1409.0473.pdf
  2. Thang Luong, Hieu Pham, and Christopher D. Manning (2015). 基于注意力的神经机器翻译的有效方法. 见 Proceedings of the 2015 Conference on Empirical Methods in Natural Language Processing. . https://aclanthology.org/D15-1166/
  3. André F. T. Martins, Ramón Fernandez Astudillo (2016). 从 Softmax 到 Sparsemax:一种稀疏的注意力与多标签分类模型. 见 Proceedings of the 33rd International Conference on Machine Learning. . http://proceedings.mlr.press/v48/martins16.html
  4. Ben Peters, Vlad Niculae, André F. T. Martins (2019). 稀疏的序列到序列模型. 见 Proceedings of the 57th Annual Meeting of the Association for Computational Linguistics. . https://aclanthology.org/P19-1146/
  5. Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Lukasz Kaiser, and Illia Polosukhin (2017). 注意力就是一切. 见 Advances in Neural Information Processing Systems. . https://papers.nips.cc/paper/2017/hash/3f5ee243547dee91fbd053c1c4a845aa-Abstract.html
  6. Jacob Devlin, Ming-Wei Chang, Kenton Lee, and Kristina Toutanova (2019). BERT:深度双向Transformer的语言理解预训练. 见 Proceedings of the 2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, Volume 1 (Long and Short Papers). . https://aclanthology.org/N19-1423/
  7. Hao Li, Zheng Xu, Gavin Taylor, Christoph Studer, and Tom Goldstein (2018). 可视化神经网络的损失景观. 见 Advances in Neural Information Processing Systems. . https://proceedings.neurips.cc/paper/2018/file/a41b3bb3e6b050b6c9067c67f663b915-Paper.pdf
  8. Sneha Chaudhari, Varun Mithal, Gungor Polatkan, and Rohan Ramanath (2021). 注意力模型的综述. ACM Trans. Intell. Syst. Technol. 12, 5, Article 53 (October 2021). . https://doi.org/10.1145/3465055

延伸阅读

以下是一些进一步阅读的资源: