13

自注意力——一个词怎么「看见」前面的词

阅读约 8 分钟第 13/60 课

嵌入层给每个 token 一个初始向量。但这个向量是固定的——无论「银行」出现在什么语境下,嵌入层查出来的向量都一样。

这显然不够。「去银行取钱」的「银行」是金融机构,「河岸银行」的「银行」是地理概念。同一个词在不同上下文中的含义不同,它的向量表示也应该不同。

自注意力(self-attention)就是解决这个问题的机制。它让每个 token 能够「看到」序列中其他 token 的信息,然后根据看到的内容修正自己的向量。

核心直觉

想象你在读一篇文章,读到「她非常开心」这四个字。当你处理「她」这个字时,你会自动回忆前文——谁是「她」?如果前面说的是「小明的妈妈走进来」,你就知道「她」指的是小明的妈妈。

你做这件事时并不是均匀地看前面每一个字。你更关注「小明的妈妈」而不是「走进来」——因为前者才是「她」所指代的对象。

自注意力做的就是同样的事:对于序列中的每个 token,计算它和前面每个 token 的相关程度,然后把更相关的 token 的信息融合进自己的表示中。

三个角色:Q、K、V

自注意力的计算用到了三个矩阵变换,产生三组向量,分别叫 Query(查询)、Key(键)和 Value(值)。

一个便于理解的类比是图书馆检索:

  • Query(查询):你想找什么。「我想找关于猫的书。」
  • Key(键):图书馆里每本书的索引标签。「动物」「历史」「数学」「猫科动物」……
  • Value(值):书的实际内容。

检索的过程是:你拿着 Query 去跟每本书的 Key 做匹配(计算点积),找到最匹配的几本书(最相关的 token),然后把它们的 Value(实际内容)取出来汇总。

在自注意力中,每个 token 同时扮演三个角色:它作为 Query 去查询别人,它的 Key 被别人查询,它的 Value 被别人取走。

计算步骤

假设输入序列有 5 个 token,嵌入层之后是一个 5×1,536 的矩阵 X。自注意力的计算分四步。

第一步:生成 Q、K、V。

用三个不同的权重矩阵(W_Q、W_K、W_V)把 X 分别变换成 Q、K、V:

Q = X × W_Q     (5×1536) × (1536×1536) = (5×1536)
K = X × W_K     (5×1536) × (1536×1536) = (5×1536)
V = X × W_V     (5×1536) × (1536×1536) = (5×1536)

三次矩阵乘法,得到三个 5×1,536 的矩阵。每个 token 现在有了三个不同的向量——Q 向量、K 向量、V 向量。

为什么要变换三次,而不是直接用 X 本身?因为一个 token「想找什么」(Query)和它「被别人找时的标签」(Key)以及它「提供给别人的内容」(Value)是三种不同的语义角色。把同一个向量通过三个不同的变换,让模型能够学会把这三种角色区分开来。

第二步:计算注意力分数。

对于每个 token,计算它的 Q 向量和所有 token 的 K 向量的点积:

分数矩阵 = Q × K 的转置     (5×1536) × (1536×5) = (5×5)

得到一个 5×5 的矩阵。第 i 行、第 j 列的值代表:第 i 个 token 对第 j 个 token 的关注程度。

这个值越大,说明两个 token 的 Q 和 K 越匹配——第 i 个 token 越想关注第 j 个 token。

第三步:归一化(softmax)。

把分数矩阵的每一行通过 softmax 函数,转换成概率分布——每一行的所有值加起来等于 1,且都是非负的。下一章节会详细讲 softmax 的原理。

注意力权重 = softmax(分数矩阵的每一行)     仍然是 5×5

归一化之后,每一行变成了一个概率分布:第 i 个 token 把多少比例的注意力分配给了第 1、2、3、4、5 个 token。

第四步:加权汇总。

用注意力权重对 V 向量做加权求和:

输出 = 注意力权重 × V     (5×5) × (5×1536) = (5×1536)

每个 token 的输出向量 = 所有 token 的 V 向量按注意力权重加权平均。一个 token 关注谁越多,谁的 V 向量在它的输出中占比就越大。

一个数字例子

为了直观感受,用一个极简化的例子。假设只有 3 个 token,维度只有 2(不是 1,536):

token 1 ("她"):  Q = [1, 0]    K = [0, 1]    V = [1, 0]
token 2 ("妈妈"): Q = [0, 1]    K = [1, 0]    V = [0, 1]
token 3 ("开心"): Q = [1, 1]    K = [1, 1]    V = [1, 1]

token 3(「开心」)的 Q 向量是 [1, 1]。计算它和每个 token 的 K 向量的点积:

与 token 1 的 K [0,1]:  1×0 + 1×1 = 1
与 token 2 的 K [1,0]:  1×1 + 1×0 = 1
与 token 3 的 K [1,1]:  1×1 + 1×1 = 2

softmax([1, 1, 2]) ≈ [0.21, 0.21, 0.58]

token 3 把 58% 的注意力给了自己(K 和 Q 最匹配),21% 给了 token 1,21% 给了 token 2。

输出向量 = 0.21 × [1,0] + 0.21 × [0,1] + 0.58 × [1,1] = [0.79, 0.79]

token 3 的新向量不再是原来的嵌入向量,而是融合了上下文信息之后的结果。

因果掩码:只看前面

在语言模型中,有一个约束:每个 token 只能看到它自己和它前面的 token,不能看到后面的。 这是因为语言模型的任务是预测下一个词——如果它能看到后面的词,就等于看到了答案。

这个约束通过因果掩码(causal mask)实现:在计算分数矩阵之后、softmax 之前,把所有「看到后面的 token」的位置设为负无穷。softmax 会把负无穷变成 0——那个 token 就被完全屏蔽了。

token 1 可以看到:[token 1]
token 2 可以看到:[token 1, token 2]
token 3 可以看到:[token 1, token 2, token 3]
token 4 可以看到:[token 1, token 2, token 3, token 4]
token 5 可以看到:[token 1, token 2, token 3, token 4, token 5]

这就是为什么叫「因果」——因为信息只能从过去流向未来,不能反过来。

下一章节来看两个细节:为什么点积要除以一个数(缩放),以及 softmax 到底是怎么把任意数字变成概率分布的。