Seq2Seq 与 Transformer 模型

Seq2Seq 的引入:机器翻译

Seq2Seq(Sequence to Sequence)模型指的是处理由一个序列数据转化为另一个序列数据的任务的模型。由于是序列的输入输出,传统的全连接神经网络无法直接处理这种不定长的输入输出。RNN 的引入为处理变长序列数据提供了可能。典型的 Seq2Seq 任务包括机器翻译,其生成的输出序列长度通常与输入序列长度不同且均不确定。为了实现这一点,Seq2Seq 模型通常采用编码器-解码器(Encoder-Decoder)架构。 编码器将输入序列编码为一个固定长度的上下文向量,解码器则根据该上下文向量生成输出序列。利用 RNN 11 或其变体,如 LSTM 和 GRU的一种典型实现可以如下图所示:

在这个架构中,编码器 RNN 读取输入序列的每个时间步,并将其隐藏状态传递给下一个时间步。最终的隐藏状态被视为输入序列的上下文表示。解码器 RNN 则使用该上下文表示作为初始隐藏状态,并逐步生成输出序列。在训练过程中,解码器通常会使用 “teacher forcing” 技术,即在每个时间步使用真实的前一个输出作为当前时间步的输入。在推理过程中,解码器则使用其自身生成的前一个输出作为输入,直到生成结束标志为止。

Attention

尽管 Seq2Seq 模型在处理变长序列任务上取得了一定的成功,但其性能在处理长序列时仍然受到限制。这是由于上述模型的信息量瓶颈被限制在了 Encoder 的单一最终隐藏状态上,而独独一个向量的维数与信息量有限,导致解码器在生成输出时无法充分利用输入序列的全部信息。为了解决这一问题,一种合理的思路便是在解码时可以更好地利用编码器的所有隐藏状态,而不是仅仅依赖于最后一个隐藏状态。这便是 Attention 机制的核心思想。

简单地理解,Attention 本身就是一个加权求和的过程。它通过计算输入序列中每个位置与当前解码位置的相关性(通常称为“注意力权重”),然后根据这些权重对编码器的隐藏状态进行加权求和,生成一个上下文向量。这个上下文向量包含了输入序列中与当前解码位置最相关的信息,从而帮助解码器更好地生成输出。数学上这一过程可以表示为:

这里我们采用了最简单的计算注意力权重的方法,即通过点积计算 query 和 key 的相关性。事实上这绝非唯一能够有效反映两个向量表示相关性的方法。更一般地,对于两个向量,他们的任意一个双线性型的函数都可以视作点积的扩展形式,也就是

其中 是一个可学习的参数矩阵, 是向量的维度。这是一个很好的扩展,但是问题在于往往计算复杂度太高:对于一个维度为 的向量,我们有 的参数需要学习,这对于 Scaling 来说是一个不小的负担。为此,可以简单地将 做低秩逼近(low-rank approximation),也就是将其分解为两个较小的矩阵的乘积。这是进行信息压缩的一种常用方法,可以显著减少参数数量,同时仍然保留足够的信息表达能力。具体来说,我们可以将 分解为两个矩阵 ,其中 。这样,注意力权重的计算可以表示为:

这样我们可以将矩阵 视作一种 Embedding,将原始的 维向量映射到 维空间中进行点积计算。这样一来,我们只需要学习 个参数,而不是 个参数,从而大大降低了计算复杂度。更进一步地有所谓加性注意力机制(Additive Attention),它通过一个前馈神经网络来计算注意力权重,而不是简单地使用点积。这种方法可以捕捉更复杂的相关性模式,通常在实践中有更高的表示能力,但是由于计算复杂度较高,训练速度较慢,因此在大规模应用中不如点积注意力常用。

注意 Attention 是一种通用机制,可以与各种神经网络架构结合使用。它不仅在 Seq2Seq 模型中得到了广泛应用,还被用于图像处理、语音识别等多个领域。Attention 机制的引入显著提升了模型在处理长序列任务时的性能,并为后续的 Transformer 模型奠定了基础。

我们真的还需要 RNN 吗?

抽象地来说,Attention 机制实现了一个先前序列 + 一个输入向量的信息整合过程,且这个过程综合用到了所有的序列信息。这其实和 RNN 的机制完全一致,理论上 RNN 能从序列中提取的信息都可以通过 Attention 来实现。此外,RNN 序列(即使使用 LSTM 改进)仍然在机制上依赖它循环的顺序处理,也就是在无论训练还是推理时都必须按时间步一个个地处理序列数据。随着计算能力与数据量的提升,这种顺序处理的方式逐渐暴露出其效率低下的问题。GPU 与 TPU 等硬件的并行计算能力无法被充分利用,导致训练时间过长,且难以扩展到更大的模型和数据集。由此,研究人员开始探索完全基于 Attention 机制的模型,这便是 Transformer 模型的诞生背景。

Self-Attention

2017 年,vbswani 等人的著名论文 Attention Is All You Need22 Original Paper 提出了 Transformer 模型。正如其标题所示,Transformer 完全基于 Attention 机制而完全摒弃了循序处理的 RNN 结构。在其核心既是所谓的“自注意力机制”(Self-Attention)。对于单个查询,它的核心原理可以由以下公式表示:

代表输入序列的词向量表示, 代表序列长度。对于每个位置 的词向量 ,我们计算其在较低维度 上的嵌入 ,其中 是对于词汇表 的嵌入矩阵。然后我们通过三个可学习的线性变换将其映射到查询(query)、键(key)和值(value)空间:

其中 是可学习的参数矩阵。接下来,我们计算位置 的查询向量与所有位置 的键向量之间的点积,以获得注意力权重:

可以看到这里的处理思路与前文介绍的 Attention 机制基本一致。不同之处在于这里的查询、键和值都是从同一个输入序列中生成的。

位置编码

这样的思路有一个显著的问题:不同于 RNN 那样天然地编码了序列的顺序信息,Self-Attention 机制本身并不包含任何关于序列中各个位置之间相对或绝对顺序的信息。如此一来,对于任意的输入序列的不同排列,Self-Attention 计算的结果是相同的,这显然不符合自然语言处理任务中对顺序敏感的需求。为了解决这个问题,我们必须在输入中包含除开语义信息以外的位置信息,也即位置编码

在最初的 Transformer 论文中,位置编码被设计为一组固定的正弦和余弦函数,其频率随着维度的增加而变化。具体来说,对于位置 和维度 ,位置编码的计算公式如下:

这的确从某种程度上有一定的可解释性:不同频率的正弦和余弦函数可以捕捉到不同尺度的位置信息,同时它们的周期性特征也有助于模型理解相对位置关系。另外也有使用 Linear Bias 的方法来编码位置信息,其秉持我们先前的思路,距离中心词越近的位置应该被赋予更高的权重。这听起来有点武断而粗暴,但实践中这种方法有时效果甚佳。

另外更加简单粗暴的方法是直接使用可学习的位置嵌入向量 ,与词向量直接相加,我们再在训练过程中一同学习这些位置嵌入向量。这种方法虽然缺乏明确的数学解释,但在实践中往往表现良好,且实现简单。这也是当前主流的做法。

你要训练一个 decoder-only Transformer(训练上下文 2k),但明确要求上线后在 8k 长度上尽量平稳退化。仅从“位置建模策略”看,哪种设计最稳妥?

非线性层

注意到单纯的 Self-Attention 机制实际上是单纯的线性变换(对于 的线性组合),这也就意味着这并不能独立构成一个强大的模型。为此,我们只需在每次 Self-Attention 之后添加一个前馈神经网络层一般来说对于所有位置的输出应用同样参数与结构的神经网络,引入非线性性即可。一般来说,可以通过以下公式表示:

掩码(Masking)

在训练语言模型中的 decoder 结构时,我们希望模型在预测下一个词时只能依赖于当前词之前的词,而不能看到未来的信息。 否则在训练过程中我们提前对于预测一个词就能“偷看”后续的词,这显然违背了语言模型的基本假设,不能起到好的训练效果。 为此,我们需要在计算注意力权重时引入掩码(masking)机制。具体来说,我们可以通过一个上三角矩阵来屏蔽掉未来位置的注意力权重,使得模型只能关注当前词及其之前的词。令

于是凡是 的位置,其注意力权重 都会被置为非常小的值,从而在计算上下文向量 时不会对其产生影响。

在 decoder-only 训练中,你怀疑“未来信息泄漏”。下面哪种实验最能直接验证 causal mask 是否被正确应用?

Transformer

Transformer decoder

Transformer 的 decoder 结构与我们前面介绍的简单的解码器最大的不同在于其拥有的(具有掩码的)多头自注意力机制(Masked Multi-Head Self-Attention)。所谓多头注意力机制,指的是我们并行地计算多个独立的注意力头(attention heads),每个头都有自己的一组查询、键和值的线性变换参数。这样做的好处在于不同的注意力头可以学习到输入序列中不同方面的相关性,从而提升模型的表达能力。具体来说,假设我们有 个注意力头,每个头的维度为 ,那么对于每个位置 ,我们计算每个头的注意力输出:

其中每个 embedding 的维度均为 ,每个 embedding matrix 的维度均为 。相应地我们可以计算每个头的注意力输出 。这样,我们就得到了 个不同的上下文向量,他们各自捕捉了输入序列中不同方面的信息。最后,我们将这些上下文向量进行拼接,并通过一个线性变换将其映射回原始的维度

这就是 Multi-Headed-Attention 机制的基本原理。但是正如先前所言,Attention 架构的优越性在于其并行计算的能力。因此,我们需要考虑如何将这一机制高效地实现。我们先考虑怎么使用矩阵运算来实现单头注意力机制。对于输入序列的所有位置 ,我们可以将它们的查询、键和值向量堆叠成矩阵:令 代表输入序列的嵌入矩阵,其中每一行代表一个位置的嵌入向量。然后我们可以计算查询、键和值矩阵:

其中 。 接下来,Attention 计算可以通过矩阵运算高效地实现:

这样一来我们通过容易并行的矩阵运算就实现了对所有位置的注意力计算。对于多头注意力机制,我们所做的就是将输入矩阵 分割成 个子矩阵,每个子矩阵的维度为 然后对于每个子矩阵,我们分别计算对应的查询、键和值矩阵,并进行注意力计算。最后,我们将所有头的输出拼接起来,并通过线性变换得到最终的输出矩阵。从并行计算的角度来看,我们所做的其实等价于将 三个矩阵视作三维张量 。为了进行不同头的注意力并行化计算,我们可以通过张量的转置将其视作 ,然后对于每个头 ,我们计算注意力输出:

随后我们将这个张量转置回去,得到 ,并将其拼接映射回 。随后我们便可以将其输入到前馈神经网络层中,完成 Transformer decoder 的一个 Self-Attention + Feed-Forward 层的计算过程。这种并行化的实现方式是 Transformer 模型高效训练的关键所在。

在实际训练中,Self-Attention 层在计算 softmax 之前通常会除以一个缩放因子 。这是由于随着 的增加,点积的值可能会变得非常大(这与本身训练的值无关,仅仅是由于维度的增加),从而导致 softmax 函数的梯度变得非常小,影响训练效果。我们做一个简单的数学假设,若 的元素均为独立同分布的随机变量,满足 ,那么点积 的期望和方差分别为:

此时方差随着 的增加而线性增加,使得点积的值变得非常大。为此,我们通过除以 来将其归一化为标准的 分布,从而稳定训练过程。因此最终的注意力计算公式为:

在实现多头注意力时,你误把缩放从 写成了 。其最可能带来的训练效应是?
两个 trick:Add & Norm

实际上,在 Transformer 的实现中还有两个常用的技巧:残差连接(Residual Connections)和层归一化(Layer Normalization)。残差连接通过将输入直接添加到输出,帮助缓解深层网络中的梯度消失问题,从而促进更深层次的模型训练。层归一化则通过对每个样本的特征进行归一化,稳定了训练过程,提高了模型的收敛速度和性能。这两个技巧在 Transformer 的成功应用中起到了重要作用。

残差连接的思路非常简单:对于神经网络中的任何一层,我们将其视作映射 ,那么残差连接便是将其改写为

显然引入这样的结构之后并没有削弱神经网络的表达能力;这样做的好处在于它允许梯度直接通过跳跃连接传递,从而缓解了深层网络中的梯度消失问题。这与先前 LSTM 的思路有些相似:纯粹的乘法结构连接梯度会带来指数级的影响,而引入加法结构则会使得梯度传递稳定得多。Resnet33 Original Paper 提出以来,残差连接已成为深度学习模型中的标准组件,其对于越来越深层次网络的训练起到了关键作用。

LayerNorm 的思想则是针对某一层的输出而言,我们想要标准化每一层的输出来使得训练更加稳定。注意 LayerNorm 是针对层与层之间的输出进行归一化处理的,也就是说这是网络的一部分,而不是像 BatchNorm 那样针对一个 batch 的数据进行归一化处理。具体来说,假设某一层的输出为 ,那么 LayerNorm 的计算过程如下: 是可学习的参数向量, 是一个小的常数,用于防止除零错误

我们发现如此处理过后, 符合 分布,而 则是经过缩放和平移后的结果。如此一来,模型可以更专注于学习有意义的特征,而不是被输入数据的分布所干扰44 一个有些令人困惑的点是,LayerNorm 在实际工作中往往是针对每个词独立的,也就是说我们是针对各个 维的向量分别进行 LayerNorm

每个 Add & Norm 模块实际上包含了一个残差连接和一个层归一化。但是这内部其实有两种不同的实现方式:Pre-Norm 和 Post-Norm。Post-Norm 是指我们先进行某一层的计算,然后将其输出与输入相加,最后进行层归一化;而 Pre-Norm 则是先对输入进行层归一化,然后再进行某一层的计算,最后将其输出与输入相加。具体来说,Post-Norm 的计算过程为 ,也就是

而 Pre-Norm 的计算过程为 ,也就是55 注意这里的残差链接加的是归一化之前的输入,这是为了保持梯度流的稳定性:

原始的 Transformer 论文(也是我们目前给出的结构)采用的是 Post-Norm 结构。然而在后续的研究中发现,Pre-Norm 结构在训练更深层次的 Transformer 模型时表现更好,且收敛速度更快。因此在实际应用中,Pre-Norm 已逐渐成为主流选择。

你把一个 decoder-only Transformer 从 24 层扩到 72 层,参数规模保持同量级。训练时 Post-Norm 版本反复出现深层梯度不稳(前层学习慢、后层震荡大),而注意力与 FFN 的实现检查都正常。最优先的结构性改动是?

Transformer encoder

事实上 Transformer 的 encoder 结构与 decoder 结构非常相似,唯一的区别在于 encoder 的 Self-Attention 机制不需要掩码(masking),因为 encoder 可以访问整个输入序列的信息。除此之外,encoder 同样包含多头自注意力机制和前馈神经网络层,并且同样采用残差连接和层归一化来稳定训练过程。

Encoder-Decoder 结构

在完整的 encoder-decoder transformer 结构中,我们需要将 encoder 的输出作为 decoder 的输入之一。具体来说,decoder 拥有两部分的输入:一部分是来自 decoder 自身的输入序列66 第一层 decoder 的输入序列通常是原始输入序列的位移版本,也就是在序列开头添加一个特殊的起始标志符号,这一部分会经过正常的 masked multi-head self-attention 层;另一部分则是来自 encoder 的输出,这一部分会经过一个额外的 multi-head attention 层,被称作 Cross-Attention 层。整体结构中,encoder 中的每个 attention + feed-forward 层会对应 decoder 中的一个 attention + feed-forward 层。

在 Cross-Attention 层中,decoder 的查询向量 来自 decoder 自身的前一层输出,而键和值向量 则来自 encoder 的输出。具体来说,用 代表 encoder 的输出,用 代表 decoder 前一层的输出,那么 Cross-Attention 的计算过程为:

随后我们便可以使用与 Self-Attention 相同的计算方式来计算注意力输出 77 此时并不需要 mask,因为 decoder 可以访问 encoder 的全部输出。这样一来,decoder 在生成每个位置的输出时,既可以利用自身的历史信息(通过 Self-Attention),也可以利用 encoder 提供的上下文信息(通过 Cross-Attention)。这种结构使得 Transformer 模型能够有效地捕捉输入序列与输出序列之间的复杂关系,从而在各种序列到序列的任务中表现出色。

训练与推理

在训练过程中,decoder 的输入序列通常是目标输出序列的位移版本,也就是在序列开头添加一个特殊的起始标志符号(例如 )。这样,模型在每个时间步都可以访问到正确的前一个词,从而更有效地学习生成下一个词的能力。这种技术被称为“teacher forcing”,它有助于加速训练过程并提高模型的性能。在推理过程中,decoder 则使用其自身生成的前一个输出作为输入,直到生成结束标志为止(例如 )。这种自回归的生成方式使得模型能够逐步构建输出序列,同时根据已经生成的内容进行调整和优化。