我的知识记录

digSelf

告别循环,拥抱并行:一步步构建你自己的Transformer

2025-07-13
告别循环,拥抱并行:一步步构建你自己的Transformer

序列之思:从零推导循环神经网络与注意力机制 一节中,已经探讨了如何通过交叉注意力解决RNN的“信息瓶颈”问题,但是并未解决RNN的序贯式的处理方式,这是限制处理速度的另一大关键问题。现在关键问题变为如何打破这种序贯式的处理方式,增加神经网络处理信息的速率和吞吐量

如果要打破RNN的这种序贯式处理方式,就面临解决如下2个关键问题:

  • RNN通过序贯式处理方式,隐含了位置信息的概念,但如果打破了这种处理方式,那么如何嵌入序列的位置信息
  • 在解码时,为了保证生成序列的自回归特性(即预测第t个元素只能依赖于前t-1个元素),RNN通过其序贯结构天然地实现了这一点。如果打破了这种结构,如何用一种并行化的方式,来强制模型在预测时只能关注到已经生成的部分,而屏蔽未来的信息

由于注意力机制(Attention Mechanism)可以建模复杂的依赖关系(即上下文向量),那么是否可以采用纯注意力机制来解决上述两个关键问题呢?答案是肯定的。

核心机制:自注意力

自注意力与多头注意力

由于现在打破了RNN的序贯式处理方式,采用并行的方式进行处理,编码器或解码器在时间步t做出预测时,无法依赖之前在RNN中存在的工作记忆状态向量\pmb s_t,因此,编码器或解码器不止需要交叉注意力来获取参考向量(即上下文向量\pmb c_t),为完成自身任务(即编码器总结归纳信息,解码器根据归纳信息做信息预测/生成),各位置的依赖关系是什么?

为了解决自身内部的依赖关系建模问题,引入自注意力机制(Self-Attention Mechanism)。其核心思想是,对于序列中的每一个位置,都允许它直接与所有其他位置进行交互,从而计算出这个位置富含上下文的新表示。

可以从单个位置i的角度来理解这个过程,即“站在一个位置上来进行观测”:其构建依赖关系的过程与获取交叉注意力的过程是一致的,仍然是概率性的数据库检索。在开始检索之前,编解码器根据当前位置i的输入,采用非线性函数计算决策依据\pmb h_i,需注意的是计算决策依据\pmb h_i时采用的是共享参数,即:

\pmb h_i = \sigma(W\pmb x_i + \pmb b)

根据\pmb h_i分别计算对应的\pmb q_i = \mathrm{Query}(\pmb h_i), \pmb k_i = \mathrm{Key}(\pmb h_i), \pmb v_i = \mathrm{Value}(\pmb h_i)。随后,计算第i个位置针对第j个位置的注意力得分e_{ij},并将当前序列中的所有决策依据\pmb h_\cdot视为记录,通过计算数学期望的方式获取相对于自身的上下文向量\pmb c_i

在第i个位置,该节点自身也需要与自身求自注意力,这个其实就是对自己审视自己的建模,其回答的是:当我有了一个“查询/疑问”时,我自身能为这个“疑问”解答多少信息。如果自身不与自身求解注意力,根据注意力计算方法,在求加权平均获取上下文向量时由于未参考自身,会导致忽略自身所掌握的知识/经验,进而导致被其他位置处的信息所淹没。因此,在第i个位置处,自身与自身之间也需要计算注意力。

上述各位置之间计算注意力得分,进而获取上下文向量的过程可以类比为CNN中的卷积核提取特征,而为了能够提取更丰富的特征,引入多个Query, Key和Value函数,即多头注意力机制,以提取到足够丰富的特征表示。在实践中,大约8个注意力头就能取得不错的性能。

非线性与前馈网络

到目前为止,多头注意力机制本身仍然是线性的。如果只是简单地堆叠多个注意力层,其表示能力等价于一个复杂的线性变换。为了增加模型的表示能力,我们必须引入非线性。因此,在每个多头注意力模块之后,都会接一个简单的位置前馈网络(Position-wise Feed-Forward Network)。这个网络通常由两个线性层和一个ReLU激活函数组成,它对每个位置的输出进行一次独立的非线性变换,极大地增强了模型的特征提取和表示能力。

解决顺序问题:位置编码

问题的提出

自注意力机制解决了序列内部之间的复杂相互依赖关系的问题,但是并没有解决序列的“顺序”问题。上述的多头注意力机制是并行的,只要序列中的元素不变,其并不能感知顺序的问题,即同一个内容集合的不同顺序对于多头注意力而言所代表的含义是相同的。例如对于纯自注意力模型来说,“I love you”和“You love I”其含义是一样的(虽然这两者的含义具有本质的不同)。

造成这种情况的本质原因是当前的输入并不包含位置信息,因此,需要将位置信息与输入的内容进行融合。一个最朴素的想法是直接将当前元素在序列中的位置直接输入到模型中,但这种方法有致命缺点:

  • 缺乏相对位置信息:模型很难从23,与99100这两对数字中,学到它们都代表着“相邻”这个相同的相对关系。
  • 泛化能力差:如果输入的序列长度超过了模型在训练时见过的最大长度,模型将不知道如何处理这些全新的位置索引。

正余弦绝对位置编码

为了解决朴素位置编码存在的问题,需要找到一种编码方式,该方式具有:

  1. 能为每个位置输出一个唯一的编码;
  2. 无论序列多长,都能计算出编码;
  3. 能让模型轻易地学习到“相对位置”关系。

一个很自然的想法就是利用三角函数的周期性来建模位置编码,而采用正余弦的位置编码方式称为正余弦绝对位置编码,其核心思想是用不同频率的正弦和余弦波来共同表示一个位置

令位置编码(Positional Encoding)的维度为d_{model},序列中的位置为pos,假设位置编码中每两个维度共享一个频率的波,则其位置编码PE \in\mathbb{R}^{d_{model}}的计算方式如下:

\begin{align} PE(pos, 2i) &= \sin(\frac{pos}{w^{2i/d_{model}}}) \\ PE(pos, 2i + 1) &= \cos(\frac{pos}{w^{2i/d_{model}}}) \end{align}

其中:

  • pos \in \{0, 1, 2, \cdots\}是元素在序列中的绝对位置
  • i \in \{0,1 , \cdots, 2^{d_{model}} - 1\}是位置编码的维度索引
  • w\in \mathbb R,是一个超参数,一旦选定则为常数

在三角函数中,其波长是指正弦函数和余弦函数图像的周期,也就是在x轴上重复一个完整波形所需的距离,因此在正余弦位置编码中,其波长为2\pi \times w^{2i/d_{model}}。当w很小时,则该维度频率越快,那么这个维度用来编码非常精细、高频的位置变化;当w越大时,则该维度频率越慢,则可以用来编码非常粗糙、长距离的宏观位置信息

此外,w用来设定最长波长的基准。由于输入的序列长度可能很长,为了尽可能保证近似唯一性,w控制了输入序列的最大长度。其原理类似于哈希表,为了减少冲突的概率,通常会设置成一个较大的数。

d_{model}在这里的作用是作为一个“缩放因子”,它将一个固定的波长范围(从2\pi2\pi \times w^{2i/d_{model}})平滑地、优雅地分布到整个d_model维度的向量上。它让波长的变化形成了一个平滑的几何级数(Geometric Progression)。从高频(短波长)到低频(长波长),每个维度上的频率都是上一个维度的固定倍数。

综上,d_model 在这里起到了一个**“归一化”或“插值”**的作用。它将一个固定的、设计好的频率范围(由基准值w决定),根据你模型的维度d_model的大小,自动地、平滑地分配给每一个维度。这使得位置编码的生成方式与模型的维度解耦,成为一种非常优雅和通用的设计。它确保了无论模型胖瘦,位置编码的频谱分布都是合理且一致的。

此外,正余弦绝对位置编码具有良好的性质,令\theta_i = \frac{1}{10000^{2i/d_{model}}},则利用三角函数的和角公式,有:

\begin{align} \sin(A + B) &= \sin A \cos B + \cos A \sin B \\ \cos(A+B) &= \cos A \cos B - \sin A \sin B \end{align}

则对于pos + k的位置,其第i个维度的编码向量,有下式:

\begin{align} PE(pos + k, 2i) &= \sin((pos + k)\theta_i) \\ &= \sin(pos \times \theta_i) \cos (k\theta_i) + \cos(pos\times \theta_i) \sin(k\theta_i) \\ &= PE(pos, 2i) \cos(k \theta_i) + PE(pos, 2i + 1) \sin(k\theta_i) \end{align}

同理有:

PE(pos + k, 2i + 1) = PE(pos, 2i + 1) \times \cos(k\theta_i) - PE(pos, 2i) \times \sin(k\theta_i)

采用矩阵形式表示,有:

\begin{bmatrix} PE(pos +k, 2i) \\ PE(pos + k, 2i+1) \end{bmatrix} = \begin{bmatrix} \cos(k\theta_i) & \sin(k\theta_i) \\ -\sin(k\theta_i) & \cos(k\theta_i) \end{bmatrix} \begin{bmatrix} PE(pos, 2i) \\ PE(pos, 2i + 1) \end{bmatrix}

上式中的系数矩阵其实是一个二维旋转矩阵,这意味着,在每一个频率的子空间中,将一元素的位置向前移动k步,等价于将其位置编码向量旋转一个固定的角度。模型可以通过学习一个简单的线性变换(矩阵乘法),来轻易地理解和利用相对位置信息。它不需要去学习复杂的非线性函数来推断“这个元素在另一个元素3个位置后”。

当有了绝对位置编码后,只需要保证元素的嵌入向量维度和位置编码维度一致,即可通过向量加法的形式来融合位置信息,这就解决了上述模型对位置不敏感的问题。

解决生成问题:掩码自注意力

解码器的困境

在解码(生成)过程中,模型必须遵循自回归特性,即在预测第t个词时,只能看到它在前面t-1步已经生成的词,绝不能“作弊”地看到未来的信息。RNN的序贯结构天然满足这一点,但并行的自注意力机制却会一下子看到所有位置,破坏了这一规则。

掩码机制

为了在并行计算的同时强制实现自回归,我们引入了带掩码的自注意力(Masked Self-Attention)。由于自注意力机制需要对序列中各元素都进行注意力检索,而在解码过程中,由于还没有生成未来的元素,为了防止作弊,需要屏蔽掉模型对未来信息的检索,即避免注意力机制对未来信息进行检索。

一个朴素的想法就是借鉴计算机网络中子网掩码的方式,对于未来的信息,通过掩码的方式屏蔽掉。具体而言,利用softmax函数的性质,当输入接近负无穷时,其值越接近于0,进而在加权平均计算上下文向量时,屏蔽掉这些未来信息对于模型解码时的影响。具体的操作方法如下:

  1. 在计算完注意力得分矩阵之后,但在送入softmax函数之前,模型会生成一个“上三角”掩码矩阵。对于所有代表“未来”位置的元素,这个掩码矩阵的值是一个非常大的负数(比如-1e9-\infty),而对于其他位置则是0
  2. 将这个掩码矩阵加到注意力得分矩阵上;
  3. 当这个被修改过的得分矩阵通过softmax时,那些被加上了巨大负数的位置,其exp的值会趋近于0,最终的注意力权重也几乎为0

这样,在最后的加权求和中,未来位置的值向量v_j的权重就是0,它们的信息就被彻底“屏蔽”了。

通过这种方式,我们既保留了并行计算带来的高速,又通过掩码保证了解码过程的正确性。

  • 0