从词元化到Transformer中的注意力机制

17 min

最近我正在学习Transformer的基础知识。如果你和我一样——在这个看似崭新的领域里挣扎,或是被细节搞得头疼——但仍想为下一个ASR/NLP项目收集灵感并稍作理解机制,那么这篇文章正适合你。我不擅长数学,因此会尝试用简单的方式解释。

原文

介绍

注意力机制出现在Transformer的多个部分。目前,我将重点放在编码层内部的机制。

暂时忘掉这张图。我们来谈谈句子中的语义:“我在桌子上吃面包。” (I eat bread on the table) 作为英语学习者,你大概能感觉到这些词之间的关系。例如:

  • “eat”与”bread”的相关性高于与”桌子”,因为我是正常人,尽管从技术上讲桌子也能吃
  • “bread”与”table”存在某种关联,因为
  • “I”与两者都有关系,但在这个句子中,动词”eat”和宾语”bread”比”bread”和”table”更重要。去掉”table”句子仍然成立
  • “I”与自身存在关联

现在仅选取”eat”、“bread”和”table”来观察它们之间的关系。我们将跳过词元化过程,将每个单词视为一个词元(故下文中的”token”= “单词”,“句子”指输入序列)。

预备知识:NumPy中的线性代数

为找出词元间的关系,我将通过计算每个词元间的注意力分数来模拟这个过程。这需要一些线性代数基础。你不需要深入数学原理,但应该知道如何使用。

线性代数是处理向量和矩阵的数学分支。在Python中,我们可以使用numpy库执行线性代数运算。以下是numpy处理基础线性代数任务的简单示例:

  • 点积(一维数组)
import numpy as np
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
c = np.multiply(a, b)
print(c) # -> 1*4 + 2*5 + 3*6 = 32 
print(a@b) # -> 1*4 + 2*5 + 3*6 = 32 
  • 矩阵乘法(二维数组)
A = np.array([[1, 2],
              [3, 4]])

B = np.array([[5, 6],
              [7, 8]])
C = A @ B
print("A:\n", A)
print("B:\n", B)
print("A @ B:\n", C) # ->  [[19 22] [43 50]]

以上就是后续章节所需的全部知识。

你不需要打开IDE进行计算,只需在终端输入python即可获得Python交互环境。

词元化与编码

众所周知,句子中的每个单词都与其他单词存在某种关联。例如在”我吃面包而不是桌子”这句话中,“吃”显然与”面包”的关联度高于”桌子”。但如何从数学和计算角度衡量这些关系呢?

在原始Transformer论文中,定义了d_model=512作为模型的维度。该维度用于描述词嵌入——包含单词特征(如身份标识、上下文、句法角色和语义)的向量表示(我将在其他文章中详细解释)。

但词嵌入本身无法捕获词语的上下文关系,尤其是在像Transformer编码器这样的非自回归模型中(并行处理输入序列,而非像RNN那样逐步或序列接续处理)。Transformer中的词元通过相互”关注”建立联系,我们称之为”成对注意力”(或自注意力)。

为简化起见,我们将从示例矩阵入手,设 d_model=4。(实际应用中维度可能为8,但在引入多头机制前,我们将持续使用 d_model=4。)同时设定 tokens=3 以代表句子中的3个单词(此处假设 1token=1单词1token=1单词),每个单词具有三个不同的特征。假设已完成位置编码等预处理步骤,则使用一个形状为 (3,4)(3,4)XX 矩阵来表示这三个编码后的词元。

X=[101002021111]X=\begin{bmatrix}1&0&1&0\\0&2&0&2\\1&1&1&1\end{bmatrix}

每行代表1个词元

自注意力机制

Q, K, V(它们是什么?)

通过词嵌入和位置编码中的余弦相似度,我们已了解单词在句子中的特性及其位置关系。但由于仍需以自回归方式处理词元(如您所知,大语言模型逐词生成句子),若不知如何预测下一个词元,便无法预先训练词元间的关系。此时注意力得分便发挥作用。

==完整的注意力得分公式为:QKT(dk)\dfrac{Q*K^T}{\sqrt(d_{k})},因此在开始前需明确:(1) 为何需要注意力得分 (2) 如何计算 Q, K, V 及其本质==

注意力得分与注意力机制

词嵌入中的余弦相似度与注意力层中的注意力得分存在差异:

  • 注意力机制评估两个词元间语义关系的概率,确保关系以相对重要性权重而非任意原始得分的形式表达。
  • 虽然得分随输入动态变化,但转换过程(点积+缩放+softmax)是固定的,且可学习权重控制着数值的调整方式。
  • 为确保机制数学稳定、易于训练和解释,需将概率范围限制在特定阈值内。

但问题在于:如何实现这三点?答案不仅在于 Q, K, V 的推导方式,更关键的是它们如何通过可训练权重矩阵塑形。科学家选择将其抽象为查询(Query)、键(Key)和值(Value),而非简单的词元对词元矩阵,因为这种抽象使机制兼具灵活性与可扩展性——Q 聚焦于查询主体,K 定义信息索引方式,V 决定实际承载的内容。相比之下,直接的词元对比矩阵会将模型禁锢于僵化的相似性检查,降低训练过程中的控制力与适应性。

  • Q=查询(Query),代表寻找的主体或目标,它提问:“我是谁?”
  • K=键(Key),代表被审视的对象,它提问:“我在看什么?”
  • V=值(Value),揭示 K 携带的信息,它提问:“我所见为何物?”

可将输入嵌入 XX 视作原始食材,后续将提及的权重矩阵 WQ,WK,WVW_Q, W_K, W_V 则是食谱,而生成的 Q, K, V 便是准备就绪、可供注意力机制享用的菜肴。

可训练权重 W_Q, W_K 与 W_V

或许有人会说,即使没有训练过的权重,仍可计算 Q, K, V 并用于衡量词元间相似性。但实际上,若缺乏定制的 WQ,WK,WVW_{Q}, W_{K}, W_{V},模型无法优化该相似性,也无法决定应强调并传递哪些信息(值)。

此外,与FFN(前馈神经网络,即小型MLP)、层归一化和嵌入层一样,这些权重矩阵是可训练参数。它们通过优化器逐步反向传播更新。在推理阶段(训练后),它们固定不变,但每个编码器层(及该层内的每个注意力头)均保留其独有的 WQ,WK,WVW_{Q}, W_{K}, W_{V} 集合。此设计使模型能学习不同层和头中的多样化注意力模式,并在网络多个层级影响信息流动。

既然已理解 WQ,WK,WVW_{Q}, W_{K}, W_{V} 的重要性及其训练方式,下一步便是观察其实际应用。实践中,每个词元嵌入 XX 会先经这些权重投影生成 Q, K, V,继而作为计算注意力得分的基础。我们将定义Q、K和V,使它们各自与输入XX的形状保持一致,以确保架构中各层的一致性,并在该层处理完成后得到相同形状的输出ZZ

WQ=X@WQW_Q = X @ W_Q

为了计算形状均为(4,4)的Q、K和V(即输入矩阵的形状),显然我们需要一个(4,3)(4,3)的矩阵来进行推导。

X=[101002021111],WQ=[101100001011],WK=[001110010110],WV=[020030103110]X = \begin{bmatrix} 1 & 0 & 1 & 0 \\ 0 & 2 & 0 & 2 \\ 1 & 1 & 1 & 1 \end{bmatrix}, \qquad W_Q = \begin{bmatrix} 1 & 0 & 1 \\ 1 & 0 & 0 \\ 0 & 0 & 1 \\ 0 & 1 & 1 \end{bmatrix}, \qquad W_K = \begin{bmatrix} 0 & 0 & 1 \\ 1 & 1 & 0 \\ 0 & 1 & 0 \\ 1 & 1 & 0 \end{bmatrix}, \qquad W_V = \begin{bmatrix} 0 & 2 & 0 \\ 0 & 3 & 0 \\ 1 & 0 & 3 \\ 1 & 1 & 0 \end{bmatrix}

X * W_Q = Q

了解了训练权重所代表的内容后,首要任务是计算Q、K、V。

从图中可以看出,QbreadK(all)Q_{bread} * K_{(all)}将生成一个新的概率向量,显然“吃”与“面包”之间的关系最具潜力。

Q=XWQ,K=XWK,V=XWV.Q = X W_Q, \qquad K = X W_K, \qquad V = X W_V.

Q=[101002021111][101100001011]=[102222213]Q = \begin{bmatrix} 1 & 0 & 1 & 0\\ 0 & 2 & 0 & 2\\ 1 & 1 & 1 & 1 \end{bmatrix} \cdot \begin{bmatrix} 1 & 0 & 1\\ 1 & 0 & 0\\ 0 & 0 & 1\\ 0 & 1 & 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 & 2\\ 2 & 2 & 2\\ 2 & 1 & 3 \end{bmatrix}

原始注意力分数

S=QKdk(第i行给出了查询i与所有键的匹配分数).S = \frac{Q K^\top}{\sqrt{d_k}} \quad\text{(第i行给出了查询i与所有键的匹配分数)}.

💡为什么是1k\dfrac{1}{\sqrt{k}}
(1) 将分数转化为权重(输出为概率分布)
(2) 因为点积注意力在实践中更快且更节省空间,它可以利用高度优化的矩阵乘法代码实现,但会显著增长并使softmax结果失衡。[[^1]]

S=11[102222213][042143101]=[244416241210]S = \frac{1}{1} \begin{bmatrix} 1 & 0 & 2\\ 2 & 2 & 2\\ 2 & 1 & 3 \end{bmatrix} \begin{bmatrix} 0 & 4 & 2\\ 1 & 4 & 3\\ 1 & 0 & 1 \end{bmatrix} = \begin{bmatrix} 2 & 4 & 4\\ 4 & 16 & 2\\ 4 & 12 & 10 \end{bmatrix}

在计算原始注意力分数后,我们了解了一个查询与每个键的“对齐”强度。然而,这些分数无界且缺乏一致的尺度,使其不适合作为注意力机制中的最终权重。为解决此问题,我们引入softmax函数:

softmax(sj)=exp(sj)kexp(sk).\mathrm{softmax}(s_j) = \frac{\exp(s_j)}{\sum_k \exp(s_k)}.

Softmax将任意分数向量转换为概率分布:每个权重变为正数,且每行总和为1。这种归一化确保注意力权重稳定、可解释,并在不同标记间具有可比性。因此,每个标记的表示成为其他标记的混合体,混合比例由这些动态的概率权重决定。

Softmax归一化

得到原始注意力分数SS后,我们逐行应用softmax进行归一化:

A=softmax(S)(S的每一行应用softmax)A = \mathrm{softmax}(S) \quad (\text{对}S\text{的每一行应用softmax})

我们将通过代码实现此过程,而非深入探讨Softmax算法细节。attention_scores[0]代表标记1与所有标记的关系,标记2和标记3同理。

这确保每个查询的注意力权重形成概率分布(全部为正,每行总和为1)。

可直接实现如下:

attention_scores[0] = softmax(attention_scores[0]) # Q1与所有标记的注意力权重 attention_scores[1] = softmax(attention_scores[1]) # Q2与所有标记的注意力权重 attention_scores[2] = softmax(attention_scores[2]) # Q3与所有标记的注意力权重

此时,attention_scores的每一行以归一化概率形式,展示了查询对所有标记键的关注程度。

拼接并恢复至原始输入尺寸(n, d_512)

接下来,使用归一化后的注意力权重计算值向量的加权和:

Z=AWoZ = A ⋅ W_o

其中A=(n,n)(分数经行向softmax处理);W_o=(n, d_model)为同样经过预训练的权重矩阵。

此处ZZ代表经过注意力处理后的新嵌入序列——每个标记现在是整个序列值向量的混合体,按注意力权重进行缩放。

多头注意力中,我们并行执行相同步骤于kk个头。每个头ii沿特征维度拼接后得到:

Zmulti=Concat(Z1,Z2,,ZH)Rn×(H,dk),dk=dmodelHZ_{\text{multi}} = \mathrm{Concat}(Z_1, Z_2, \dots, Z_H) \in \mathbb{R}^{n\times (H,d_k)},\quad d_k=\dfrac{d_{model}}{H}

由于设计上满足Hdk=dmodelH d_k = d_{model},拼接后的张量形状为Rn×dmodel\mathbb{R}^{n\times d_{model}}。 在我们的示例中,n=3n=3dmodel=8d_{model}=8,H=2,每个头有dk=4d_k=4,且

Z1,Z2R3×4,ZmultiR3×8.Z_1, Z_2 \in \mathbb{R}^{3\times 4},\quad Z_{\text{multi}} \in \mathbb{R}^{3\times 8}.

拼接后,通常应用输出投影:

Z~=ZmultiWO,WORdmodel×dmodel\tilde Z = Z_{\text{multi}} W_O, \qquad W_O \in \mathbb{R}^{d_{\text{model}}\times d_{\text{model}}}

此步骤确保最终表示与原始输入形状相同,即(n,dmodel)(n, d_{\text{model}}),保持模型层间的一致性。

太长不看

下图展示了自注意力层(设定维度d=512、头数h=8、序列长度s=20)的逐步计算流程:

我还绘制了流程示意图。为保持清晰度,初始阶段保持注意力计算未拆分状态,仅在最后引入多头拆分机制:

了解单层自注意力的逐步计算后,自然会产生疑问:为何要将机制拆分为多个注意力头?

多头注意力并非随意设计,而是经过实践验证的方案。若不进行拆分,注意力机制仍可运作,但模型会丧失多样性视角(如语法、语义、长距离依赖等词元关系的不同层面)与计算效率。原始Transformer采用h=8且d_model=512(每个头维度d_k=64)的配置,实现了平衡——每个头保持较小维度,整体表征丰富度高,且训练稳定性强。

  1. 输入X (s, d_model) 与参数矩阵W_Q, W_K, W_V (d_model, d_model) 相乘 → 得到Q,K,V (s, d_model)
  2. 维度重塑 → Q,K,V (s, h, d_k) → 分离出每个头的Q_i,K_i,V_i (s, d_k)
  3. 单头注意力计算:softmax(QiKiT/sqrt(dk))softmax(Q_i K_i^T / sqrt(d_k)) → 生成(s, s)注意力图
  4. 单头输出:(s, s)矩阵与(s, d_k)矩阵相乘 → 得到(s, d_k)
  5. 多头拼接:(s, h * d_k) = (s, d_model) → 可选W_O投影 → 最终输出(s, d_model)

最终输出的注意力矩阵是n×64n \times 64矩阵,每行代表单个词元的注意力矩阵。由于词元向量尚未完整,需将(n, 64)组合为(n, 512)维度。

通过完整执行该流程,我们不仅对序列施加可训练权重,还将输入投射到多个子空间:在每个子空间内进行注意力计算后重新整合视角——既为模型提供多样化的上下文信号,又保持d_model维度以供下一层使用。

参考文献

^1: Vaswani, Ashish, Noam Shazeer, Niki Parmar, et al. ‘Attention Is All You Need’. arXiv

.03762. Preprint, arXiv, 2 August 2023. https://doi.org/10.48550/arXiv.1706.03762.↩︎