Attention 机制解读

线性代数复习

数学思想

矩阵乘法的列视角

第一层:向量以及向量的运算

直接规定,一个 n 维向量,就是 n 个实数排成一列。

然后我们定义两种运算:

这是整座大厦唯一的、绝对的底层逻辑。后面的所有东西,全是用这两个动作拼出来的。

第二层:线性组合(Linear Combination)—— 真正的灵魂

现在,我们把地基里的两个动作连起来做:先缩放,再相加。

现在有个问题,给你几个向量,通过线性组合能"够到"哪些点?

直觉是,在二维平面上,如果给你两个不平行的箭头,你调系数(可正可负可零),能凑出平面上任何一个点。如果两个箭头平行,你只能凑出一条直线上的点。这个直觉给出两个概念:

到这里,初学者已经有了一个清楚的图像:向量 = 箭头 = 一列数:空间 = 所有箭头:基 = 一套"度量尺"。

第三层:线性变换

现在引入线性代数真正的主角。

第四层:矩阵

既然描述一个线性变换 T ,只需要记录 T ( e → 1 ) , T ( e → 2 ) , … , T ( e → n ) 这 n 个向量。我们可以把它们一列一列地排起来:

A = [ , T ( e → 1 )   |   T ( e → 2 )   |   ⋯   |   T ( e → n ) , ]

这张表就叫 T 的矩阵。所以矩阵的本质就是:每一列告诉你,这个线性变换把对应的基向量搬到了哪里。

这是初学者最该死记的一句话。理解了它,后面所有事情都顺。

第五层:矩阵乘向量

上面我已经得到,$T(\vec{v})=x_1 T(\vec{e}_1)+x_2 T(\vec{e}_2)+\dots+x_n T(\vec{e}_n)$,然后 T ( e → 1 ) … T ( e → n ) 存放在矩阵 A 中。

如果我们要算 T ( v → ) ,其中 v → = ( x 1 , … , x n ) ,只要算 $$ T(\vec{v})=x_1\cdot(\text{第 1 列})+x_2\cdot(\text{第 2 列})+\dots+x_n\cdot(\text{第 n 列}) $$

也就是说,矩阵乘向量 = 用向量的分量给矩阵的列加权,然后相加。换句话说,结果列向量是 A 里面列向量的线性组合。

这就是 A v → 的定义。学校里教的"行乘列"是同一件事的另一种算法,但列视角对理解更友好。

第六层:矩阵乘矩阵

现在最后一步。如果先做变换 S(矩阵 B),再做变换 T(矩阵 A),整体得到一个新的变换 T ∘ S 。它也是线性变换(很容易验证),所以它也有一个矩阵——记它为 C 。

C 长什么样?根据"矩阵第 j 列 = 变换把 e → j 送到哪里",$C$ 的每一列就是 T ( S ( e → j ) ) :

所以 C 的第 j 列就是 A 乘以 B 的第 j 列。

把所有列拼起来,就得到了矩阵乘法 C=AB 的规则。"行乘列"那个公式,只是把每一项展开写出来的样子。

关键体会:矩阵乘法之所以这么定义,是因为我们想让它对应"先做一个变换再做一个变换"。这也立刻解释了为什么 A B ≠ B A ——先转再拉伸,和先拉伸再转,结果当然不一样。

矩阵乘法的表达式

我们可以把上面的矩阵乘矩阵根据定义具体展开:

A ⋅ B = A [ | |   b 1 b 2   | | ] = [ | |   A b 1 A b 2   | | ]

然后再把矩阵乘向量根据上面说的定义展开:

A b 1 = b 11 a 1 + b 21 a 2 + b 31 a 3 = [ a 11 b 11 + a 12 b 21 + a 13 b 31   a 21 b 11 + a 22 b 21 + a 23 b 31 ]

所以,

$$ A \cdot B = \begin{bmatrix} a_{11}b_{11} + a_{12}b_{21} + a_{13}b_{31} & a_{11}b_{12} + a_{12}b_{22} + a_{13}b_{32} \

a_{21}b_{11} + a_{22}b_{21} + a_{23}b_{31} & a_{21}b_{12} + a_{22}b_{22} + a_{23}b_{32} \end{bmatrix} $$

即:

c i k = ∑ j = 1 n a i j , b j k

矩阵乘法的内积视角

然后引入内积定义,$a\cdot b = (a_1b_1 + a_2b_2 + a_3b_3)$。所以,

[ a 11 b 11 + a 12 b 21 + a 13 b 31   a 21 b 11 + a 22 b 21 + a 23 b 31 ] = [ a r 1 T ⋅ b 1   a r 2 T ⋅ b 1 ]

把拼好的列向量放回原处,就得到了完整的内积视角:

A ⋅ B = [ a r 1 T ⋅ b 1 a r 1 T ⋅ b 2   a r 2 T ⋅ b 1 a r 2 T ⋅ b 2 ]

矩阵乘法的行视角

继续推导。我们已经知道,

$$ A \cdot B = \begin{bmatrix} a_{11}b_{11} + a_{12}b_{21} + a_{13}b_{31} & a_{11}b_{12} + a_{12}b_{22} + a_{13}b_{32} \

a_{21}b_{11} + a_{22}b_{21} + a_{23}b_{31} & a_{21}b_{12} + a_{22}b_{22} + a_{23}b_{32} \end{bmatrix} $$

做一点变换。以下变换基于行向量的线性组合定义:

$$ A \cdot B = \begin{bmatrix} a_{11} \cdot b_{r1}^T + a_{12} \cdot b_{r2}^T + a_{13} \cdot b_{r3}^T \

a_{21} \cdot b_{r1}^T + a_{22} \cdot b_{r2}^T + a_{23} \cdot b_{r3}^T \end{bmatrix} $$

所以,我们可以得到,C 的每一行,都是 B 所有行的线性组合,权重就是 A 对应那一行的元素。

矩阵乘法的行视角补充

把行视角应用到 XW 上,设 X 是 N × d ,$W$ 是 d × h ,所以 X W 是 N × h 。直接套行视角:

X W  的第  i  行 = ∑ j = 1 d x i j ⋅ ( W  第  j  行 )

到这里还没有"批处理"概念,只是普通的行视角推论。

接下来引入解读:我们把 X 的第 i 行命名为"样本 i ",记作 $\vec{x}i$。注意 $\vec{x}i$ 的分量就是 $x{i1},x{i2},\dots,x_{id}$,正好就是上面求和里 x i j 当 i 固定、$j$ 变化时取到的那些数。

所以上式可以重写为:

X W  的第  i  行 = ∑ j = 1 d ( x → i ) j ⋅ ( W  第  j  行 )

关键一步:对于单个样本 x → i (看作 1 × d 矩阵)和 W ( d × h ),由矩阵乘法定义,$\vec{x}_i W$ 是一个 1 × h 矩阵。再套行视角(只有一行):

x → i W = ∑ j = 1 d ( x → i ) j ⋅ ( W  第  j  行 )

这和前面那个式子的右边完全相同。所以:

X W  的第  i  行 = x → i W

这就是数据矩阵视角的严格表述:$XW$ 的第 i 行,就等于"把第 i 个样本单独拿出来乘 W "的结果。一次大矩阵乘法 X W ,等于对每个样本独立做 x → i W 然后把结果拼回去。

矩阵乘法的外积视角

然后,我们对矩阵乘法进行变换:

$$ A \cdot B = \begin{bmatrix} a_{11}b_{11} + a_{12}b_{21} + a_{13}b_{31} & a_{11}b_{12} + a_{12}b_{22} + a_{13}b_{32} \

a_{21}b_{11} + a_{22}b_{21} + a_{23}b_{31} & a_{21}b_{12} + a_{22}b_{22} + a_{23}b_{32} \end{bmatrix} = \

\begin{bmatrix} a_{11}b_{11} & a_{11}b_{12} \ a_{21}b_{11} & a_{21}b_{12} \end{bmatrix} +

\begin{bmatrix} a_{12}b_{21} & a_{12}b_{22} \ a_{22}b_{21} & a_{22}b_{22} \end{bmatrix} +

\begin{bmatrix} a_{13}b_{31} & a_{13}b_{32} \ a_{23}b_{31} & a_{23}b_{32} \end{bmatrix} = \ \mathbf{a}1 \mathbf{b}{r1}^T + \mathbf{a}2 \mathbf{b}{r2}^T + \mathbf{a}3 \mathbf{b}{r3}^T $$

也就是说,

A B = ∑ j = 1 n ( A  的第  j  列 ) ⋅ ( B  的第  j  行 )

矩阵乘法视角总结

矩阵乘法的定义式 c i k = ∑ j a i j b j k 涉及三个下标 i , j , k 。一个三重求和的结构,你选择"先固定谁、让谁变"会得到不同视角:

视角 重组方式 看到什么
内积视角 固定 ( i , k ) ,只看 j C 的每个元素 = A 的某行和 B 的某列的内积
列视角 固定 k ,让 i 变,$j$ 隐含 C 的每列是 A 的列的线性组合
行视角 固定 i ,让 k 变,$j$ 隐含 C 的每行是 B 的行的线性组合
外积视角 固定 j ,让 i , k 变 C 是 n 个秩 1 矩阵之和

下面具体举例,

A = [ a 11 a 12 a 13   a 21 a 22 a 23 ] 2 × 3 , B = [ b 11 b 12   b 21 b 22   b 31 b 32 ] 3 × 2

为了便于后续视角的展开,我们将 A 按列和按行进行划分,将 B 同样按列和按行进行划分:

视角一:内积视角 (The Inner Product Perspective)

核心思想:逐个元素的标量计算(行 × 列)

这是最传统的计算法则,着眼于微观的标量结果。结果矩阵 C 中的每一个元素 c i j ,都是矩阵 A 的第 i 行与矩阵 B 的第 j 列的点积(Inner Product)。

数学表达:

c i j = a r i T ⋅ b j = ∑ k = 1 3 a i k b k j

展开示例:

C = [ a 11 b 11 + a 12 b 21 + a 13 b 31 a 11 b 12 + a 12 b 22 + a 13 b 32 a 21 b 11 + a 22 b 21 + a 23 b 31 a 21 b 12 + a 22 b 22 + a 23 b 32 ]

物理意义:衡量了 A 的特定行向量与 B 的特定列向量在三维空间中的投影或相似度。

视角二:列视角 (The Column Perspective)

核心思想:$C$ 的列是 A 的列的线性组合

在此视角下,我们不再关注单个元素,而是将矩阵 C 看作由两个列向量组成。矩阵 C 的每一列,本质上都是在矩阵 A 的列空间中,以矩阵 B 对应列的元素作为权重,进行线性组合的结果。

数学表达:

A ⋅ B = [ a 1 a 2 a 3 ] ⋅ [ b 11 b 12   b 21 b 22   b 31 b 32 ] = C = [ c 1 c 2 ] 2 × 2

其中:

c 1 = b 11 a 1 + b 21 a 2 + b 31 a 3 , ; c 2 = b 12 a 1 + b 22 a 2 + b 32 a 3

物理意义:矩阵 A 提供了三组“基底向量”,而矩阵 B 提供了“配方权重”。$C$ 的每一列,都是按照 B 给的配方,在 A 的列空间(Column Space)里调配出来的新向量。这揭示了矩阵乘法是一种空间映射,结果 C 的列向量永远落在 A 的列空间之内。

视角三:行视角 (The Row Perspective)

核心思想:$C$ 的行是 B 的行的线性组合

这是列视角的对偶形式。我们将矩阵 C 看作由两个行向量构成。矩阵 C 的每一行,本质上都是在矩阵 B 的行空间中,以矩阵 A 对应行的元素作为权重,进行线性组合的结果。

数学表达:

A ⋅ B = [ a 11 a 12 a 13   a 21 a 22 a 23 ] ⋅ [ b r 1 T   b r 2 T   b r 3 T ] = C = [ c r 1 T   c r 2 T ] 2 × 2

其中:

c r 1 T = a 11 b r 1 T + a 12 b r 2 T + a 13 b r 3 T

c r 2 T = a 21 b r 1 T + a 22 b r 2 T + a 23 b r 3 T

物理意义:矩阵 B 提供了行特征,矩阵 A 控制了特征的混合比例。结果 C 的行向量永远落在 B 的行空间之内。

行视角补充

行视角实际上有两种形式上不同、但数学完全等价的表述方式:展开形式和简洁形式。上面讲的是展开形式。

简洁形式核心思想:$C$ 的第 i 行 = A 的第 i 行 × 整个 B 矩阵。

数学表达:

A ⋅ B = [ a r 1 T   a r 2 T ] ⋅ B = C = [ c r 1 T   c r 2 T ] = [ a r 1 T ⋅ B   a r 2 T ⋅ B ]

物理意义:这个形式强调"每一行独立通过 B 做线性变换",简洁地表达了"逐行处理"的并行特性。

两者的关系:它们指向同一个数学对象,只是不同的写法。

a r 1 T ⋅ B = a r 1 T ⋅ [ b r 1 T   b r 2 T   b r 3 T ] = [ a 11 a 12 a 13 ] ⋅ [ b r 1 T   b r 2 T   b r 3 T ]

实际使用时——简洁形式适合表达整体计算流程,展开形式适合分析结果的内部构成。

视角四:外积视角 (The Outer Product Perspective)

核心思想:多个秩为 1 的矩阵之和(列 × 行)

这是最具宏观格局的视角。矩阵 C 被解构为矩阵 A 的每一列与矩阵 B 的对应行做张量外积(Outer Product),然后再将产生的所有同维度矩阵叠加。

数学表达:

C = a 1 b r 1 T + a 2 b r 2 T + a 3 b r 3 T

维度验证:每一次外积的计算都是一个 ( 2 × 1 ) 的列向量乘以一个 ( 1 × 2 ) 的行向量,生成一个 2 × 2 的矩阵。三个这样的矩阵相加,完美得到 2 × 2 的结果矩阵 C 。

C = [ a 11   a 21 ] [ b 11 b 12 ] + [ a 12   a 22 ] [ b 21 b 22 ] + [ a 13   a 23 ] [ b 31 b 32 ]

物理意义:每一项外积 $\mathbf{a}i\mathbf{b}{ri}^T$ 都会生成一个秩为 1 的子矩阵。该视角深刻表明:任何复杂的矩阵乘法,本质上都是将信息分解为若干个只包含单个维度信息(Rank-1)的信号矩阵,然后进行线性叠加(Superposition)。这一原理是主成分分析(PCA)和低秩近似算法的基石。

Embedding 基础

Embedding 定义

首先我们需要了解 embedding 到底是什么。

假设我们现在有一堆 token(比如 5 万个词),它们之间有某种"语义关系"(语义相近 vs 语义相远)。Embedding 做的事就是把这 5 万个 token 嵌入(embed)到一个 512 维的连续向量空间里,要求:

换句话说,我们把"词的世界"(一个离散的、有语义关系的集合)嵌入到"向量空间"里,并要求保持语义关系。

这就是为什么这个操作叫 "embedding"——它完全符合数学上"嵌入"的原本含义:

数学嵌入             -> 词嵌入
把空间 X 映射到空间 Y -> 把词汇表映射到向量空间
保持 X 的结构        -> 保持词汇间的语义关系
结构=拓扑、距离       -> 结构=语义相似度

需要注意,Embedding 的核心是"保持结构"。"变成向量"只是手段,"保持语义关系"才是目的。你可以给每个词随便分配一个向量(这也叫 "embedding",但是个烂 embedding)。但好的 embedding 必须满足:向量空间里的几何关系,真实反映词之间的语义关系。

语义蕴含在方向里

在真实的神经网络中,每种"语义特征"对应高维空间里的一个方向(一个向量),而不是一个坐标轴(维度)。

比如 "需要宾语" 这个特征,在 6 维空间里对应的方向可能是:[0.3,0.5,0.1,0.6,0.2,0.4]。

要判断一个 query 向量 q "是否在找宾语",不是看 q 的某一维是不是最大,而是看 q 和这个方向的点积:q⋅d需要宾语​。点积大 = q 在这个方向上投影长 = 想找宾语。

同一个向量里,可以同时沿多个不同方向"有分量",每个方向代表一种语义特征。这就是为什么 512 维能塞下远超 512 种信息。

512 维里能塞下多少语义特征

这涉及一个 "叠加假说 (Superposition Hypothesis)" 的重要理论,Anthropic 的可解释性团队发表过很著名的研究。

核心观察:在 n 维空间里,严格正交的方向只有 n 个,但"近似正交"的方向可以有指数级多个。

具体数字:

只要不同特征的方向之间夹角够大(不一定 90°),模型就能用点积"大致"区分它们——即使有点干扰(叠加噪声),也是可接受的。

所以神经网络实际上在用叠加的方式,把成千上万种语义特征"塞"进 512 维空间。

起点:输入矩阵 X

先考虑 batch 里只有一个 sequence 的情况。例子是 "I love GPU programming"(4 个 token,dmodel = 6,每个 token 6 维)。

经过 embedding + positional encoding 后,我们有形状为 ( 4 , 6 ) 的输入矩阵。

X = [ 1.0 0.2 0.3 0.5 0.1 0.4   0.4 0.9 0.1 0.2 0.3 0.5   0.3 0.1 0.8 0.6 0.2 0.1   0.2 0.5 0.7 0.9 0.4 0.3 ] 4 × 6 ← I   ← love   ← GPU   ← prog

每一行是一个 token 的向量,有 6 个维度。一共 4 个 token。

Step 1:生成 Q、K、V

1 矩阵具体如何相乘

三个权重矩阵的形状都是 ( d model , d model ) = ( 6 , 6 ) 。假设模型学到的权重是(数值随便编,为了算得出来):

W Q = [ 0.1 0.2 0.0 0.3 0.1 0.2   0.0 0.1 0.4 0.2 0.3 0.0   0.3 0.0 0.1 0.1 0.2 0.3   0.2 0.3 0.2 0.0 0.1 0.2   0.1 0.2 0.3 0.1 0.0 0.2   0.2 0.1 0.0 0.3 0.2 0.1 ] 6 × 6

W K , W V 也都是类似的 ( 6 , 6 ) 的矩阵,数值不同。

先计算 Q = X W Q ,形状变换为:$(4, 6) \times (6, 6) = (4, 6)$。

具体算一下第 1 行(token "I")的第 1 个元素:

Q I , 1 = 1.0 × 0.1 + 0.2 × 0.0 + 0.3 × 0.3 + 0.5 × 0.2 + 0.1 × 0.1 + 0.4 × 0.2 = 0.38

假设完整算下来:

Q = X W Q = [ 0.38 0.42 0.28 0.47 0.29 0.44   0.32 0.43 0.50 0.45 0.39 0.34   0.47 0.34 0.33 0.38 0.32 0.49   0.51 0.46 0.43 0.40 0.34 0.51 ] 4 × 6 ← q I   ← q love   ← q GPU   ← q prog

同理计算 K = X W K (假设结果):

K = [ 0.45 0.22 0.30 0.55 0.24 0.40   0.31 0.39 0.20 0.43 0.37 0.28   0.34 0.29 0.44 0.33 0.28 0.46   0.49 0.38 0.46 0.33 0.35 0.51 ] 4 × 6 ← k I   ← k love   ← k GPU   ← k prog

同理计算 V = X W V (假设结果):

V = [ 0.45 0.30 0.40 0.29 0.38 0.25   0.41 0.25 0.46 0.23 0.32 0.44   0.31 0.52 0.26 0.39 0.48 0.30   0.35 0.49 0.33 0.47 0.44 0.38 ] 4 × 6 ← v I   ← v love   ← v GPU   ← v prog

注意这一步的形状:$Q, K, V$ 都是 ( 4 , 6 ) ,和输入 X 的形状一样。每个 token 被"投影"成了三个不同角色的 6 维向量。

2.1 视角 1:内积/点积视角

如果把 X 看成多个 token 行向量纵向排列,把 WQ 看成多个列向量横向排列,那么实际进行的矩阵计算是:

[ X I   X l o v e   X G P U   X p r o g ] ⋅ [ W 1 Q W 2 Q W 3 Q W 4 Q W 5 Q W 6 Q ] = [ X I ⋅ W 1 Q X I ⋅ W 2 Q X I ⋅ W 3 Q X I ⋅ W 4 Q X I ⋅ W 5 Q X I ⋅ W 6 Q X l o v e ⋅ W 1 Q X l o v e ⋅ W 2 Q X l o v e ⋅ W 3 Q X l o v e ⋅ W 4 Q X l o v e ⋅ W 5 Q X l o v e ⋅ W 6 Q X G P U ⋅ W 1 Q X G P U ⋅ W 2 Q X G P U ⋅ W 3 Q X G P U ⋅ W 4 Q X G P U ⋅ W 5 Q X G P U ⋅ W 6 Q X p r o g ⋅ W 1 Q X p r o g ⋅ W 2 Q X p r o g ⋅ W 3 Q X p r o g ⋅ W 4 Q X p r o g ⋅ W 5 Q X p r o g ⋅ W 6 Q ]

从这个展开式里我们可以看到两个关键事实:

结果矩阵的维度最后是 ( 4 , 6 ) ——4 行对应 4 个 token,6 列对应每个 token 的 6 维 query 向量。

2.2 视角 2:X 到 Q 整行变换

我们可以再换个视角,把 X 看成多个 token 行向量纵向排列,把 WQ 看成一个整体,那么实际进行的矩阵计算是:

[ X I   X l o v e   X G P U   X p r o g ] ⋅ W Q = [ X I ⋅ W Q X l o v e ⋅ W Q X G P U ⋅ W Q X p r o g ⋅ W Q ] = [ q I q l o v e q G P U q p r o g ]

这种视角下,4 个 token,WQ,Q 这三者之间的关系非常清晰。每个 token 向量乘 WQ,会得到对应的 query 向量。

所以,和上个视角一样,每个 token 之间是独立的,每一行的变换不依赖其他行。每个 token 独立地生成自己的 query。而且,每个 token 都用同一个 WQ 做变换。

这解释了两件重要的事:

2.3 信息流视角

TODO

2.4 其他视角

另外,还有两个视角,列的线性组合,外积之和。暂时按下不表。

3 实际意义:Q K V 扮演的角色

我们已经了解了具体的矩阵计算和看待矩阵计算的一些视角。那么现在有一个问题,为什么要让输入矩阵 X 和 WQ WK WV 相乘,得到 Q K V?

因为每个 token 要扮演三种不同的信息角色:Query,Key,Value。

信息的定义

在 token 的 embedding 向量里,携带的信息大致分成几大类,这些都可能成为 attention 匹配的依据:

我们上面说过,512 维空间里可以塞下几万个近似正交的方向,所以一个 token 向量可以同时编码多种类型的信息。

Query

含义:token i 作为信息接收方时,表达"我需要什么样的上下文信息,来让我当前位置的表示变得更完整"。

用途:用来和其他 token 的 key 做点积,衡量"我的需求和他们的供给方向匹配度有多高"。

关键澄清:

Key

含义:token i 作为信息提供方时,表达"我这个位置能提供什么样的上下文信息,能匹配什么样的需求(query)"。

用途:被其他 token 的 query 扫描并匹配,决定"我应该被谁关注"。

关键澄清:

Value

含义:token i 作为信息提供方时,表达"如果我被选中了,我实际要贡献给对方的内容是什么"。

用途:当 token j 决定要关注 token i 时(即 Aji​ 较大),它会从 vi 中"提取内容",按权重加到自己的新表示里。

关键澄清:

为什么这么分工?

两个例子

实际 A 矩阵分析

              attend_to: I  love  GPU  prog
             ┌────────────────────────────────┐
I    asking  │   0.60   0.15   0.10   0.15    │  ← 和=1
love asking  │   0.25   0.15   0.10   0.50    │  ← 和=1
GPU  asking  │   0.10   0.10   0.30   0.50    │  ← 和=1
prog asking  │   0.10   0.20   0.55   0.15    │  ← 和=1
             └────────────────────────────────┘

可以看到,I 对应的那一行,I 把注意力大部分给力自己(0.6)。因为 I 作为主语,它的语义已经比较独立(就是指代说话者自身),不太需要从上下文吸收更多信息来完善自己的表示,所以它的 Query 向量和自己的 Key 向量最匹配("自足")。

love 对应的那一行,把 0.5 的注意力给了 programming。因为 love 作为动词,它的完整含义需要知道"爱什么"——它的 Query 向量里强烈编码了"找宾语"的需求方向,这个方向和 "programming" 的 Key 向量匹配度最高(因为 programming 的 Key 向量里编码了"我是名词""我能当宾语"等信息)。

programming 对应的那一行,把 0.55 给了 GPU,0.20 给了 love。这和它的 Query 卡完全一致:

programming 的 Query 在多个方向上都有分量,所以它的注意力也分散在多个 token 上,而不是像 I 那样集中。

Q K V 包含多层含义

通过上面的例子,我们可以看到,一个 Query 向量,可以同时编码多个需求(因为它是高维向量,可以在多个方向上有分量)。

programming 的 Query 向量里同时有这些方向的分量:

所有这些需求被编码在同一个向量里,通过点积在 attention 的一步中一起完成匹配。

同理,Key 和 Value 高维向量中,也编码了多层含义。

Q K V 的多层含义是人类的解读

前面说 "qlove​ 想找宾语"、"qprogramming​ 想找修饰词"——这些都是我们人类事后观察到的现象,不是模型的设计目标。

真实的情况是:

这些语言学上的"意图"是模型自己学出来的副产品,而不是我们告诉它该这么做的。

所以更严谨的说法是:programming 的 Query 卡上写的是"一堆模型学出来的向量分量",而这些分量恰好在功能上等价于"找修饰词、找支配动词、找动作发出者"等语言学需求。

WQ WK WV 的含义

q = X W Q 的本质是:

把 X 这个向量"旋转 + 拉伸"到一个新的空间,这个新空间的坐标方向,恰好对齐了"当下想找什么"这类语义特征。

通俗点说:

总结起来:

三个矩阵本质上都在做"从 X 的混合特征里,提取对某个任务有用的信息"。区别在于它们"提取的侧重点"不同——这是通过训练学出来的。

Step 2.1:算 Q × K^T(注意力分数)

Step 1 里我们得到了 Q, K, V 三个矩阵。现在进入 Step 2——用 Q 和 K 算出每个 token 对每个 token 的"关注度分数"。

首先计算 Q KT。把 K 转置,(4, 6) 变成 (6, 4):

K ⊤ = [ 0.45 0.31 0.34 0.49   0.22 0.39 0.29 0.38   0.30 0.20 0.44 0.46   0.55 0.43 0.33 0.33   0.24 0.37 0.28 0.35   0.40 0.28 0.46 0.51 ] 6 × 4

K 的每一行(一个 token 的 key)转置后变成了 K ⊤ 的每一列。

现在做 Q K ⊤ ,形状变换为 ( 4 , 6 ) × ( 6 , 4 ) = ( 4 , 4 ) 。注意,6 消失了(两个 6 相乘相加),剩下 ( 4 , 4 ) ——正好是"每个 token 对每个 token 的分数"。

具体算 scores 第 2 行第 4 列($q_{\text{love}} \cdot k_{\text{prog}}$),就是两个 6 维向量的点积:

q love ⋅ k prog = 0.32 × 0.49 + 0.43 × 0.38 + 0.50 × 0.46 + 0.45 × 0.33 + 0.39 × 0.35 + 0.34 × 0.51 ≈ 0.90

假设完整结果:

scores = Q K ⊤ = [ 0.85 0.68 0.72 0.88   0.78 0.72 0.75 0.90   0.82 0.70 0.76 0.91   0.93 0.75 0.82 1.02 ] 4 × 4

            k_I    k_love  k_GPU   k_prog
          ┌──────────────────────────────┐
q_I       │ 0.85   0.68    0.72    0.88  │
q_love    │ 0.78   0.72    0.75    0.90  │
q_GPU     │ 0.82   0.70    0.76    0.91  │
q_prog    │ 0.93   0.75    0.82    1.02  │
          └──────────────────────────────┘

scores 是 ( 4 , 4 ) ,每个位置 [ i , j ] 是 token i 的 query 和 token j 的 key 的点积。

Step 2.2:除以 √d_k(缩放)

d k = 6 ,$\sqrt{6} \approx 2.449$。每个元素除以它:

scaled = Q K ⊤ 6 ≈ [ 0.347 0.278 0.294 0.359   0.318 0.294 0.306 0.367   0.335 0.286 0.310 0.372   0.380 0.306 0.335 0.417 ] 4 × 4

当维度 dk​ 很大时(比如 512),即使方向几乎随机,两个向量点积的绝对值也会因为维度多而变得很大(每一维的小贡献加起来就很大)。这会让 softmax 里的 e^qk 出现极端值,分布变得一边倒(某个 token 权重接近 1,其他接近 0),失去了"软加权混合"的意义。

除以 √dk​​ 就是把方向匹配度标准化——让"方向接近程度"保持在一个合理的数值范围,无论维度多大。这个 scaling 不是工程上的 tweak,是数学上必要的归一化。

缩放之后,数值进入了一个更合理的范围。接下来就是 Softmax,即把这些原始分数转换成概率分布。

Step 2.3:对每一行做 Softmax

还是逐行操作,每行变成和为 1 的概率分布。

算第 2 行(love 的注意力分布)作为示例:

e 0.318 ≈ 1.374 , e 0.294 ≈ 1.342 , e 0.306 ≈ 1.358 , e 0.367 ≈ 1.443 , s u m ≈ 5.517 归 一 化 : [ 0.249 , 0.243 , 0.246 , 0.262 ]

可以看到,我的示例 Q,K 数值差异太小,算出来的注意力分布几乎是均匀的(0.243~0.262),这在真实训练好的模型里几乎不会出现。为了让后续步骤的注意力模式有教学价值,我手动设定一个更典型的注意力矩阵(真实模型通过训练会学出类似的有倾向性的分布):

A = softmax ! ( Q K ⊤ d k ) = [ 0.60 0.15 0.10 0.15   0.25 0.15 0.10 0.50   0.10 0.10 0.30 0.50   0.10 0.20 0.55 0.15 ] 4 × 4

              attend_to: I  love  GPU  prog
             ┌────────────────────────────────┐
I    asking  │   0.60   0.15   0.10   0.15    │  ← 和=1
love asking  │   0.25   0.15   0.10   0.50    │  ← 和=1
GPU  asking  │   0.10   0.10   0.30   0.50    │  ← 和=1
prog asking  │   0.10   0.20   0.55   0.15    │  ← 和=1
             └────────────────────────────────┘

这就是注意力矩阵 A ,形状 ( 4 , 4 ) ——4 个 token × 4 个 token 的注意力权重。

Step 3:A × V(输出)

形状变换:$(4, 4) \times (4, 6) = (4, 6)$。注意,这次两个矩阵的 "4" 消失了(相乘相加),剩下 ( 4 , 6 ) ——和输入 X 的形状完全一样!

视角 1 整行变换

AV 结果的每一行 = V 里所有行向量的线性组合(加权和)。具体计算如下:

s o f t m a x d k ( [ q I ⋅ k I q I ⋅ k l o v e q I ⋅ k G P U q I ⋅ k p r o g q l o v e ⋅ k I q l o v e ⋅ k l o v e q l o v e ⋅ k G P U q l o v e ⋅ k p r o g q G P U ⋅ k I q G P U ⋅ k l o v e q G P U ⋅ k G P U q G P U ⋅ k p r o g q p r o g ⋅ k I q p r o g ⋅ k l o v e q p r o g ⋅ k G P U q p r o g ⋅ k p r o g ] ) ⋅ [ v I v l o v e v G P U v p r o g ] = [ ( A I , I v I ) + ( A I , l o v e v l o v e ) + ( A I , G P U v G P U ) + ( A I , p r o g v p r o g ) ( A l o v e , I v I ) + ( A l o v e , l o v e v l o v e ) + ( A l o v e , G P U v G P U ) + ( A l o v e , p r o g v p r o g ) ( A G P U , I v I ) + ( A G P U , l o v e v l o v e ) + ( A G P U , G P U v G P U ) + ( A G P U , p r o g v p r o g ) ( A p r o g , I v I ) + ( A p r o g , l o v e v l o v e ) + ( A p r o g , G P U v G P U ) + ( A p r o g , p r o g v p r o g ) ]

以第 2 行(love)为例,详细算一下:

out love = 0.25 ⋅ v I + 0.15 ⋅ v love + 0.10 ⋅ v GPU + 0.50 ⋅ v prog

代入 V 的各行(每行 6 维):

$$\begin{aligned} \text{out}_{\text{love}} = ; & 0.25 \cdot [0.45, 0.30, 0.40, 0.29, 0.38, 0.25] \

最后算出来 out love = [ 0.380 , 0.410 , 0.360 , 0.381 , 0.411 , 0.349 ] (6 维,和输入一样)

对其他行做同样的计算,最终输出:

output = [ 0.42 0.32 0.40 0.30 0.38 0.29   0.38 0.41 0.36 0.38 0.41 0.35   0.36 0.43 0.34 0.40 0.42 0.35   0.35 0.44 0.32 0.38 0.44 0.32 ] 4 × 6

和输入 X 的形状完全一样:$(4, 6)$——所以这一层的输出可以直接喂给下一层。


意义:$\text{out}{\text{love}} = 0.25 \cdot v{\text{I}} + 0.15 \cdot v_{\text{love}} + 0.10 \cdot v_{\text{GPU}} + 0.50 \cdot v_{\text{prog}}$ 实际上,是按 love 对 I love GPU prog 的关注程度,对 I love GPU prog 提供的语义进行了加权和,生成最后 love 对应的结果。

具体来说,love 这个位置的新表示,是它从其他 token 那里按需"抄写"value的结果:

最后这个新 love 向量里,装着:

新 love 已经不是单纯的"喜爱"了,而是"在 I 这个主语下、对 GPU programming 这个技术对象的喜爱"——一个上下文化的 love。

视角 2 外积之和

A V 可以写成 4 个外积的和:

A V = A : , I ⊗ v I ⏟ 第 1 个外积 + A : , l o v e ⊗ v l o v e ⏟ 第 2 个外积 + A : , G P U ⊗ v G P U ⏟ 第 3 个外积 + A : , p r o g ⊗ v p r o g ⏟ 第 4 个外积

每一项是什么意思?我们看第 4 项(关于 prog):

A : , prog ⊗ v prog = [ 0.15   0.50   0.50   0.15 ] ⊗ [ v prog , 1 v prog , 2 ⋯ v prog , 6 ]

这是一个 ( 4 , 6 ) 矩阵,它代表"prog 的 value 在 output 矩阵上留下的足迹"。具体说:

这个矩阵就是"prog 这个 token 对最终 output 的全部贡献"!

同理,第 1 项是 "I 的全部贡献"、第 2 项是 "love 的全部贡献"、第 3 项是 "GPU 的全部贡献"。

最后:

output = (I 的贡献) + (love 的贡献) + (GPU 的贡献) + (prog 的贡献)

完整的形状流程图

现在 n = 4 (token 数)和 d = 6 (特征维度)不一样了,形状变化看得清清楚楚:

1. 通过输入和权重生成 Q K V
X (4, 6)     ← 4 个 token,每个 6 维
   │
   ├─ × W^Q (6, 6) ──→  Q  (4, 6)   ← W^Q 左边是 6,和 X 的 6 对齐
   ├─ × W^K (6, 6) ──→  K  (4, 6)
   └─ × W^V (6, 6) ──→  V  (4, 6)

2. Q 和 K 生成 A
Q (4, 6) × K^T (6, 4) ──→ scores  (4, 4)   ← "6" 消失,剩下 token×token
         │
         ÷ √6 ──→ scaled (4, 4)
         │
      softmax(每行) ──→ A (4, 4)      ← 每行和=1 的概率分布

3. A 和 V 相乘,得到结果
A (4, 4) × V (4, 6)  ──→ output (4, 6)   ← "4" 消失,回到 token×feature
                                          ← 和输入 X 同形状!

形状变换的核心直觉

Q K ⊤ :把特征维 (6) 消掉,得到 token 间的关系矩阵 (4×4)

A V :把 token 维 (4) 消掉,每个 token 得到新的特征向量 (4×6)

一句话记忆:

一句话总结

Self-attention 本质上就是两次矩阵乘法 + 一次 softmax:第一次算关系($n \times n$ 的注意力矩阵),第二次按关系混合信息(回到 n × d 的输出)。输入 ( n , d ) ,输出 ( n , d ) ——形状不变,但每个 token 都从"孤立词义"升级成了"上下文感知的表示"。

Self-Attention 和 Cross-Attention

Attention 的本质是,用 Q 扫描 K,按匹配度从 V 里取信息。

self-attention 的 Q K V 都来自同一个 sequence。句子里的 token 互相关注、互相吸收信息。大模型一般都是 decoder-only,只有 self-attention。

cross-attention 的 Q 来自一个序列,K V 来自另一个序列。这种用法常见于翻译模型——decoder 用目标语言的 token 作为 Q,去关注 encoder 输出的源语言信息(K 和 V)。

transformer 如何进行翻译任务 I love GPU programming -> 我爱 GPU 编程?两种 attention 都用到了。

Multi-Head Attention

我们前面讲的是 single-head attention。

但是 single-head 有个结构性局限:整个序列只产生一个 4x4 的注意力矩阵。意思是:所有 token 之间的关系,只能用一种"关注模式"来表达。

但实际语言里,token 之间的关系是多样的:

single-head 被迫把所有这些关系挤进一个注意力矩阵里——这导致它必须"取折中",每种关系都学不够好。

multi-head 的思路,用一句话概括就是,把特征维度切成 h 份,让 h 个独立的 attention 并行运行——每个头学一种关系。最后把结果拼起来。

为了画面清爽,我们用 h = 2 个头。所以每个头的维度是 d k = d model / h = 6 / 2 = 3 。即,把 6 维沿特征维度切成两半,每一半独立做 attention。

Step 1 生成 Q K V 然后切分

通过 X 和 WQ WK WV 生成 Q K V,这个操作和之前一样。但是现在我们需要对 Q K V 进行切分。

假设已经算出来:

Q = [ 0.39 0.42 0.28 0.47 0.29 0.44   0.32 0.43 0.50 0.45 0.39 0.34   0.47 0.34 0.33 0.38 0.32 0.49   0.51 0.46 0.43 0.40 0.34 0.51 ] 4 × 6

关键操作:把 Q 沿特征维度切成两半。

       前 3 维 (Head 1)        后 3 维 (Head 2)
       ┌────────────────┐    ┌────────────────┐
       │ 0.39 0.42 0.28 │    │ 0.47 0.29 0.44 │  ← q_I
       │ 0.32 0.43 0.50 │    │ 0.45 0.39 0.34 │  ← q_love
       │ 0.47 0.34 0.33 │    │ 0.38 0.32 0.49 │  ← q_GPU
       │ 0.51 0.46 0.43 │    │ 0.40 0.34 0.51 │  ← q_prog
       └────────────────┘    └────────────────┘
            Q^(1)                  Q^(2)
            (4, 3)                 (4, 3)

所以:

Q ( 1 ) = [ 0.39 0.42 0.28   0.32 0.43 0.50   0.47 0.34 0.33   0.51 0.46 0.43 ] 4 × 3 , Q ( 2 ) = [ 0.47 0.29 0.44   0.45 0.39 0.34   0.38 0.32 0.49   0.40 0.34 0.51 ] 4 × 3

同样地,把 K 和 V 也按 6 = 3 + 3 切开:

K = [ ⋯ ] 4 × 6 → K ( 1 ) , K ( 2 ) ∈ R 4 × 3

V = [ ⋯ ] 4 × 6 → V ( 1 ) , V ( 2 ) ∈ R 4 × 3

Step 2 每个头独立做 attention,得到独立输出

Head 1 拿到 Q ( 1 ) , K ( 1 ) , V ( 1 ) (都是 4 × 3 ),独立做完整的 attention 流程:

head 1 = softmax ! ( Q ( 1 ) K ( 1 ) ⊤ 3 ) V ( 1 )

注意:每个头的注意力矩阵 A 1 是 4 × 4 (token × token),输出 head_1 是 4 × 3 (token × 半 feature)。

Head 2 同样过一遍,用 Q ( 2 ) , K ( 2 ) , V ( 2 ) :

head 2 = softmax ! ( Q ( 2 ) K ( 2 ) ⊤ 3 ) V ( 2 )

得到 head_2 也是 4 × 3 。


注意,两个头会学到不同的关系——这是 multi-head 的精髓

因为 Q ( 1 ) 和 Q ( 2 ) 用的是输入向量的不同子空间(前 3 维 vs 后 3 维),它们捕捉的"匹配模式"会不同。

假设训练完后:

Head 1 的注意力矩阵(学到了"语法关系"):

A 1 = [ 0.5 0.2 0.1 0.2   0.2 0.1 0.1 0.6   0.1 0.1 0.3 0.5   0.1 0.2 0.5 0.2 ]

—— love 找宾语→prog(0.6)、GPU 修饰 prog(0.5)、prog 找修饰词→GPU(0.5)

Head 2 的注意力矩阵(学到了"主语-动作"关系):

A 2 = [ 0.3 0.3 0.2 0.2   0.5 0.2 0.1 0.2   0.3 0.2 0.3 0.2   0.4 0.2 0.2 0.2 ]

—— love 找主语→I(0.5),其他几行也都对 I 给了较多关注

两个头看到了不同的关系模式——这就是 multi-head 的力量。如果只有一个头,它只能在这两种模式里取折中,两种都学不好。

Step 3 拼接独立输出

每个头的输出是 4 × 3 。把它们沿特征维度拼起来:

   head_1 (4, 3)         head_2 (4, 3)
┌────────────────┐    ┌────────────────┐
│ h1_I (3 d)     │    │ h2_I (3 d)     │
│ h1_love        │    │ h2_love        │
│ h1_GPU         │    │ h2_GPU         │
│ h1_prog        │    │ h2_prog        │
└────────────────┘    └────────────────┘
拼接(concatenate):
┌─────────────────┬─────────────────┐
│ h1_I            │ h2_I            │   ← 6 d
│ h1_love         │ h2_love         │
│ h1_GPU          │ h2_GPU          │
│ h1_prog         │ h2_prog         │
└─────────────────┴─────────────────┘
            result (4, 6)

形状:$(4, 3) ;||; (4, 3) = (4, 6)$(沿特征维度拼接)。注意:拼接后的形状回到了 ( 4 , 6 ) ——和输入 X 的形状一致。

重要洞察——拼接后向量的"前 3 维"装着"语法关系信息","后 3 维"装着"主语-动作关系信息"。两种信息并排在同一个向量里,但还没有混合。

Step 4 混合拼接结果中的信息

拼接结果还需要乘一个矩阵 W O ∈ R 6 × 6 :

output = Concat ( head 1 , head 2 ) ⋅ W O

形状:$(4, 6) \times (6, 6) = (4, 6)$

W O 是干嘛的?

拼接后的向量里——"前 3 维"是 head_1 的产物(语法关系),"后 3 维"是 head_2 的产物(主语-动作关系)——它们还是"井水不犯河水"地并排放着。

W O 的作用是 让两个头的信息互相交流、融合——把"两个头独立学到的信息"整合成一个统一的表示。

具体说:

没有 W O ,multi-head 就只是 "几个独立小 attention 的结果硬拼"——不能形成真正的联合表示。$W^O$ 是 multi-head 的"融合器"。

形状流程图

X (4, 6)                                  ← 输入
    │
    ├── × W^Q ──→ Q (4, 6) ──┐
    ├── × W^K ──→ K (4, 6) ──┤── 沿特征维切成 h=2 份
    └── × W^V ──→ V (4, 6) ──┘
                     │
                     ↓
  ┌──────────────────┬──────────────────┐
  │                  │                  │
  │ Head 1           │ Head 2           │
  │ Q^1, K^1, V^1    │ Q^2, K^2, V^2    │
  │ (4, 3)           │ (4, 3)           │
  │                  │                  │
  │ Attention        │ Attention        │
  │   ↓              │   ↓              │
  │ head_1 (4, 3)    │ head_2 (4, 3)    │
  └────────┬─────────┴────────┬─────────┘
           │                  │
           └───── Concat ─────┘
                    │
              (4, 6) 拼接后
                    │
                × W^O (6, 6)
                    │
               output (4, 6)        ← 和输入同形状

每个头如何生成 Q K V

我们前面讲的是,先用 WQ 乘输入矩阵,然后把结果切两半。WQ 大小是 6 x 6。

论文里写的是,每个头有自己独立的矩阵 W i Q , W i K , W i V ,每个 W i Q 形状是 (6,3),从 6 维输入直接投到 3 维。

这两种实现数学上完全等价——一个 (6,6) 的大矩阵,按列切两半就是两个 (6,3) 的小矩阵。结果一模一样。实际工程实现中用大矩阵更高效(一次大矩阵乘法 vs 多次小矩阵乘法),所以代码里通常这么写。

和 single-head 区别总结

Q V 的真实语义

研究者事后观察 attention 模式时有以下发现:

这种"主体属性也参与 attention 匹配"的现象——在可解释性研究里有个名词叫 "conditional attention pattern"——意思是 attention 模式是"有条件激活"的——只在合适的 token 类型上激活。

Masked Multi-Head Attention

为什么需要 mask?

decoder 在做的事是自回归生成,即一个一个生成 token:

看到 "I"           → 预测 "love"
看到 "I love"      → 预测 "GPU"
看到 "I love GPU"  → 预测 "programming"

推理时这个机制天然成立,因为未来 token 还没生成,模型想偷看也没得看。

但训练时会有问题。为了高效,训练时把整个目标序列 "I love GPU programming" 一次性喂给模型,让它并行预测所有位置的下一个 token。

如果不加 mask,当模型在 "love" 这个位置预测下一个 token(应该是 "GPU")时——它的 attention 可以看到 "GPU" 和 "programming"!那它直接 attend 到 "GPU"——把它的信息复制过来,预测准确率 100%。但这是作弊,模型没学到真本事

结果:训练时 loss 极低,推理时一塌糊涂(推理时没有未来 token 可看)。

causal mask 具体怎么实现?

在 attention 的 softmax 之前,把 attention 矩阵的上三角(未来位置)设成 − ∞ :

未加 mask:           加 mask 后:
                        I    love  GPU  prog
   I    [✓ ✓ ✓ ✓]    I  [ ✓   -∞   -∞   -∞ ]
 love   [✓ ✓ ✓ ✓]  love [ ✓    ✓   -∞   -∞ ]
  GPU   [✓ ✓ ✓ ✓]   GPU [ ✓    ✓    ✓   -∞ ]
 prog   [✓ ✓ ✓ ✓]  prog [ ✓    ✓    ✓    ✓ ]

为什么是 − ∞ ?因为 e − ∞ = 0 ,softmax 之后那个位置的注意力权重变成 0,模型"看不到"那个位置。

加完 mask 之后,"love" 位置的 attention 只能看到 "I" 和自己,它真的需要从这里推出 "GPU"。这强迫模型学会真正的"基于上文预测"能力。

继续往下推,当我们计算 AV 的时候:

mask 带来的好处