土法炼钢兴趣小组的算法知识备份

【Transformer 与注意力机制】02 向量与点积的几何直觉

文章导航

分类入口
transformer
标签入口
#向量#点积#内积#余弦相似度#注意力#Transformer

目录

「点积是注意力的灵魂。」——这句话在 Transformer 火起来之后变成了一句俗话。但很多人会用点积,会写代码 torch.matmul(Q, K.transpose(-2, -1)),却说不清楚为什么是点积——为什么不是欧氏距离、不是绝对值差、不是 KL 散度。这一篇就是为这个「为什么」准备的。


一、从一支箭开始

在二维平面上画一支从原点出发的箭,箭的尾巴在 \((0, 0)\),箭的头落在 \((3, 2)\)。这支箭就是一个向量(vector),记作 \(\mathbf{a} = (3, 2)\)

向量比「点」多了一个东西:方向。点 \((3, 2)\) 和点 \((0, 0)\) 之间的关系是「位置不同」;而向量 \((3, 2)\) 不仅有位置,还隐含「从 (0, 0) 指向 (3, 2)」这条路径。这种「位置 + 方向」的双重身份,是后面所有讨论的基础。

我特意从「箭」这个比喻开始,是因为它能避免一个常见误解:很多初学者把向量等同于「一列数字」。代码里看到的 [3, 2, 1, 5, 8] 是向量在某个坐标系下的表示,但向量本身是更抽象的「带方向的量」。这种区分在前两年看起来无所谓,但当你后面遇到「同一个向量在不同基底下有不同坐标」「向量空间没有自然坐标」之类的话题时,没有这个区分会让你混乱很久。

点和向量的区别

不过对本篇,我们就停留在「向量 ≈ 一列数字 + 一个箭头比喻」的层面。这个层面足够用很久。

我们再画两支箭。一支是 \(\mathbf{a} = (3, 2)\),另一支是 \(\mathbf{b} = (1, 1)\)。它们都从原点出发,但方向不同、长度不同。把它们画在同一个坐标系里,肉眼可以看到:它们『差不多指着同一个方向』,但又不完全重合。这种「差不多」的程度,正是我们想要量化的东西。

两支箭的相对方向

如果你现在还没有动笔在草稿纸上画这两支箭,请放下手机/合上电脑屏幕,找一张纸,画一下。我说的不是开玩笑。本篇后面所有关于「点积、夹角、投影」的讨论,都建立在你脑子里有「两支箭」这个画面之上。如果没有这个画面,公式只是符号;有了这个画面,公式是可视的事实。

二、向量的三个量:长度、方向、夹角

一支箭最直接的属性是它的长度,记作 \(|\mathbf{a}|\)\(\|\mathbf{a}\|\)。在二维里,长度由勾股定理给出:

\[ |\mathbf{a}| = \sqrt{a_1^2 + a_2^2} \]

\(\mathbf{a} = (3, 2)\),长度是 \(\sqrt{9 + 4} = \sqrt{13} \approx 3.61\)。这个长度告诉我们「这支箭有多长」,但不告诉「指向哪里」。

方向这件事,在数学上有一个干净的处理方式:单位化(normalize)。把向量除以自己的长度,就得到一个长度为 1 但方向不变的向量:

\[ \hat{\mathbf{a}} = \frac{\mathbf{a}}{|\mathbf{a}|} \]

\(\mathbf{a} = (3, 2)\),单位化后是 \((3/\sqrt{13}, 2/\sqrt{13}) \approx (0.832, 0.555)\)。这个 \(\hat{\mathbf{a}}\) 就是「纯方向」,剥掉了长度信息。

第三个量是夹角。两支箭之间总有一个夹角 \(\theta\),从一支转到另一支需要旋转的角度。在二维里,夹角的范围是 \([0, \pi]\)(不区分顺时针逆时针时)。\(\theta = 0\) 表示完全同向,\(\theta = \pi/2\) 表示垂直,\(\theta = \pi\) 表示完全反向。

夹角是一个比「方向」更精炼的概念。两支箭可能各自指着不同方向,但「它们之间相差多少」是一个单一的标量,比比较两个方向向量更紧凑。

长度、方向与夹角

到这里我们有三个量:长度、方向、夹角。点积,将以一种意想不到的方式把这三者串起来。

三、点积的代数定义:最朴素的版本

点积(dot product,又称内积 inner product) 在二维里的定义简单到让人怀疑它的力量:

\[ \mathbf{a} \cdot \mathbf{b} = a_1 b_1 + a_2 b_2 \]

\(\mathbf{a} = (3, 2)\)\(\mathbf{b} = (1, 1)\),点积等于 \(3 \cdot 1 + 2 \cdot 1 = 5\)

推广到 \(n\) 维,定义一样朴素:

\[ \mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^{n} a_i b_i = a_1 b_1 + a_2 b_2 + \cdots + a_n b_n \]

这就是代码里一行 np.dot(a, b)(a * b).sum() 做的事。

为什么这个定义重要? 因为它把两个向量「乘出来」之后变成一个标量——不再是向量,而是一个数。这种「降维」是点积的关键特性之一:它把「两个方向上的对齐程度」浓缩成一个数字,方便比较、排序、用作权重。

但代数定义本身不直观。\(3 \cdot 1 + 2 \cdot 1 = 5\) 这个 5 代表什么?为什么是「对齐程度」?光看公式说不出来。要看出意义,必须配合几何定义。

四、点积的几何定义:两个等价的视角

点积有第二个定义,叫几何定义

\[ \mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| \cdot |\mathbf{b}| \cdot \cos\theta \]

这里 \(\theta\)\(\mathbf{a}\)\(\mathbf{b}\) 之间的夹角。

让我们验证一下。\(\mathbf{a} = (3, 2)\)\(\mathbf{b} = (1, 1)\)

\(|\mathbf{a}| = \sqrt{13} \approx 3.606\)

\(|\mathbf{b}| = \sqrt{2} \approx 1.414\)

夹角 \(\theta\)\(\mathbf{a}\) 与 x 轴夹角 \(\arctan(2/3) \approx 33.69°\)\(\mathbf{b}\) 与 x 轴夹角 \(\arctan(1/1) = 45°\),所以 \(\theta \approx 11.31°\)\(\cos(11.31°) \approx 0.981\)

代入几何定义:\(3.606 \cdot 1.414 \cdot 0.981 \approx 5.000\)

和代数定义算出来的 5 完全一致。

两个定义等价,这件事可以用余弦定理证明(任何线性代数教材都有),但本篇不展开。重要的是接受这个等价,把它当成一个事实,然后基于它思考点积的「意义」。

五、为什么这个等价如此重要

代数定义告诉我们「怎么算」,几何定义告诉我们「算出来代表什么」。两者合起来才是完整的点积。

从几何定义看,点积大小由三件事决定:

  1. \(\mathbf{a}\) 的长度。
  2. \(\mathbf{b}\) 的长度。
  3. 它们的夹角余弦。

如果两支箭都很长,且方向接近同向,点积就大。如果有一支很短,或者两者夹角接近 90°,点积就接近 0。如果两者夹角接近 180°(指相反方向),点积是负数。

这就是「点积衡量对齐程度」的真正含义:它综合了长度和方向,对长度加权后乘上方向的对齐度

回到 \(\mathbf{a} = (3, 2)\)\(\mathbf{b} = (1, 1)\) 的例子。点积是 5。如果我们把 \(\mathbf{b}\) 拉长 10 倍变成 \((10, 10)\),点积就变成 50;方向没变,但因为长度变了 10 倍,点积也变了 10 倍。如果我们旋转 \(\mathbf{b}\) 让它变成 \((-1, -1)\)(反向),点积变成 \(3 \cdot (-1) + 2 \cdot (-1) = -5\);长度没变,但方向反了。

注意点积对长度敏感。这有时是缺点(我们不希望「相似度」被向量长度污染),有时是优点(我们希望「重要的、强的信号」权重更大)。具体哪种,取决于场景。在 Transformer 里两种都有,需要分别理解。

点积的几何视角

六、投影:点积的另一个解读

几何定义还有第三个等价说法。把 \(\mathbf{a}\) 投影到 \(\mathbf{b}\) 的方向上,得到一个标量 \(|\mathbf{a}|\cos\theta\),这是 \(\mathbf{a}\)\(\mathbf{b}\) 方向的「分量长度」。点积可以写成:

\[ \mathbf{a} \cdot \mathbf{b} = (|\mathbf{a}|\cos\theta) \cdot |\mathbf{b}| \]

也就是「\(\mathbf{a}\)\(\mathbf{b}\) 方向上的投影长度」乘以「\(\mathbf{b}\) 的长度」。

这个视角对理解很多物理和工程量都有用:「力在位移方向上的功」「电场强度在面元法向上的通量」「光线在镜面法向上的分量」——所有这些都是某种点积。

在注意力机制里,这个视角也帮助我们理解:当我们用查询向量 \(\mathbf{q}\) 去「问」键向量 \(\mathbf{k}\),本质是在问「\(\mathbf{k}\)\(\mathbf{q}\) 方向上有多大的分量」。如果 \(\mathbf{k}\) 主要不在 \(\mathbf{q}\) 的方向上,那么 \(\mathbf{q}\) 不关心 \(\mathbf{k}\)

七、余弦相似度:剥掉长度后的点积

如果我们把两个向量都先单位化再做点积,就得到 余弦相似度(cosine similarity)

\[ \cos\theta = \frac{\mathbf{a} \cdot \mathbf{b}}{|\mathbf{a}| \cdot |\mathbf{b}|} = \hat{\mathbf{a}} \cdot \hat{\mathbf{b}} \]

这个量的取值范围是 \([-1, 1]\),与向量长度完全无关,只反映方向的对齐度。

余弦相似度是 NLP 早期最常用的「语义相似」度量。给两个文档各自表示成词频向量,求余弦相似度,就能得到「这两份文档讲的内容像不像」的初步估计。这个做法直到今天还在搜索引擎、推荐系统、向量检索里大量使用。

但 Transformer 的注意力没有用余弦相似度,而是直接用点积(再除以 \(\sqrt{d_k}\) 做缩放)。这是有意为之的。原因有几个:

第一,学起来的向量本来就有合适的尺度。神经网络通过梯度下降学权重 \(W_Q, W_K\),会自然把 \(\mathbf{q} = W_Q \mathbf{x}\)\(\mathbf{k} = W_K \mathbf{x}\) 调整到合适的长度,让点积大小处于合适范围。强行单位化等于砍掉了网络自由调整尺度的能力。

第二,单位化引入除法,除法在反向传播时会带来梯度数值不稳定。能不除就不除是工程惯例。

第三,长度本身可能携带信息。如果某个键 \(\mathbf{k}_i\) 长度特别大,可能模型在告诉你「这个位置的信息特别强,应该被多关注」。单位化后这个信号就没了。

这三个理由在「点积 vs 余弦相似度」的选型上,几乎所有现代大模型都站在「点积」这边。

不过,单位化在向量检索里仍然是主流。当你用 FAISS、Milvus 之类的库做相似度搜索时,常常先把所有向量单位化,然后用点积当余弦相似度查。这两个场景的诉求不同:注意力是端到端学的,向量检索是离线建好的索引,需要的是稳定可解释的距离度量。

八、点积的对称性、双线性、可分配

点积有几个代数性质,简单但重要:

对称性\(\mathbf{a} \cdot \mathbf{b} = \mathbf{b} \cdot \mathbf{a}\)。从代数定义直接看出(求和顺序不影响结果)。

双线性\((\alpha \mathbf{a}) \cdot \mathbf{b} = \alpha (\mathbf{a} \cdot \mathbf{b})\)\((\mathbf{a}_1 + \mathbf{a}_2) \cdot \mathbf{b} = \mathbf{a}_1 \cdot \mathbf{b} + \mathbf{a}_2 \cdot \mathbf{b}\)。也就是说点积对每个向量参数都是线性的。

正定\(\mathbf{a} \cdot \mathbf{a} = |\mathbf{a}|^2 \ge 0\),且等号成立当且仅当 \(\mathbf{a} = \mathbf{0}\)

与零向量的点积:任何向量与零向量的点积都是 0。

这些性质看起来朴素,但每一条在后面都会用到。比如「双线性」让我们可以把矩阵乘法理解为「批量做点积」(下一篇 03 篇会展开);「正定」让我们能用 \(\mathbf{a} \cdot \mathbf{a}\) 当成「向量长度的平方」。

九、超越二维:为什么点积在高维仍然有意义

到这里我们一直在二维空间里讨论。但 Transformer 里的向量是几百到几千维的(GPT-3 的 hidden size 是 12288,Llama 3-70B 的 hidden size 是 8192)。在这样的高维空间里讨论「夹角」还有意义吗?

答案是有,但需要小心

二维或三维的「夹角」我们能直接画出来。到了 \(n\) 维之后,画不出来了,但几何定义依然成立

\[ \mathbf{a} \cdot \mathbf{b} = |\mathbf{a}| \cdot |\mathbf{b}| \cdot \cos\theta \]

这里的 \(\theta\) 不再是「平面上看到的角度」,而是用这个等式反推出来的一个量:

\[ \theta = \arccos\frac{\mathbf{a} \cdot \mathbf{b}}{|\mathbf{a}| \cdot |\mathbf{b}|} \]

只要 \(|\mathbf{a}|, |\mathbf{b}|\) 都非零,这个 \(\theta\) 就是良定义的,落在 \([0, \pi]\) 之内。它不依赖于空间是几维。

但高维有一个反直觉的现象:两个随机向量的夹角倾向于接近 \(\pi/2\)(也就是接近垂直)。具体地说,从单位球面均匀采样两个向量,其余弦相似度的分布在维度 \(d\) 越大时越集中在 0 附近,方差大约是 \(1/d\)

这意味着什么?意味着在高维空间里,「相似」是一件相对的事:所有相似度都不会很大(0.5 已经很高),但这些不大的差异里仍然蕴含信息。所以注意力公式里点积的输出值看起来都很小(softmax 之前),但 softmax 会把这些「小」的差异放大成有意义的概率分布。这是高维 + softmax 组合的一个微妙之处,第 13 篇缩放点积注意力会再展开。

这也是为什么注意力公式里要除以 \(\sqrt{d_k}\):高维下点积的方差是 \(O(d_k)\)(如果 \(\mathbf{q}, \mathbf{k}\) 各分量独立同分布),不缩放的话 softmax 会进入饱和区,梯度消失。除以 \(\sqrt{d_k}\) 让方差稳定在 \(O(1)\)

十、和其他相似度量的对比

为了真正理解「为什么是点积」,必须把它和其他选项对比。下面是几种常见的相似 / 距离度量。

度量 公式 取值范围 性质 注意力里用吗
点积 \(\mathbf{a} \cdot \mathbf{b}\) \(\mathbb{R}\) 长度敏感、方向敏感
余弦相似度 \(\frac{\mathbf{a} \cdot \mathbf{b}}{\|\mathbf{a}\|\|\mathbf{b}\|}\) \([-1, 1]\) 长度不敏感 不直接用
欧氏距离 \(\|\mathbf{a} - \mathbf{b}\|\) \([0, \infty)\) 距离,越小越相似 不用
曼哈顿距离 \(\sum_i \|a_i - b_i\|\) \([0, \infty)\) L1 距离 不用
切比雪夫距离 \(\max_i \|a_i - b_i\|\) \([0, \infty)\) L∞ 距离 不用
KL 散度 \(\sum_i p_i \log(p_i/q_i)\) \([0, \infty)\) 不对称,分布间 损失里用
JS 散度 KL 的对称化 \([0, \log 2]\) 对称分布距离 不用
内积(带权) \(\mathbf{a}^\top M \mathbf{b}\) \(\mathbb{R}\) 一般化的点积 隐含使用

为什么是点积,不是欧氏距离?

欧氏距离衡量「差异」,点积衡量「对齐」。注意力的目的是「找到与查询最相关的内容」,而「相关」在分布式表示里更接近「方向对齐」,所以点积更合适。再者,欧氏距离需要计算 \(\sqrt{\sum (a_i - b_i)^2}\),需要平方、求和、开方,开方在反向传播时引入数值问题。点积只需要乘加,速度和稳定性都更好。

为什么是点积,不是绝对值差或 L1 距离?

L1 距离对异常值不敏感(稳健性好),但它和神经网络的线性结构不兼容。点积可以直接由矩阵乘法实现(高度并行化),L1 距离不能。在工程层面,「能用矩阵乘法」就是巨大的优势。

为什么是点积,不是 KL 散度?

KL 散度是分布间的距离,需要两个向量都是合法的概率分布(非负、和为 1)。注意力计算的查询和键不是概率分布,是任意实数向量。强加概率约束会限制表达能力。

为什么是点积,不是带权点积 \(\mathbf{a}^\top M \mathbf{b}\)

实际上注意力里隐含使用了带权点积\(\mathbf{q}^\top \mathbf{k} = \mathbf{x}_q^\top W_Q^\top W_K \mathbf{x}_k = \mathbf{x}_q^\top (W_Q^\top W_K) \mathbf{x}_k\)。中间那个 \(W_Q^\top W_K\) 就是「权重矩阵 \(M\)」。所以注意力学的就是「在原 embedding 上加什么样的权重,让点积成为合适的对齐度」。这个观察在第 14 篇 Q、K、V 矩阵的来历会再展开。

十一、一个具体的相似度计算例子

把抽象的东西落到具体数字上。

设有 6 个词,各自有 4 维 embedding(实际模型里是几千维,4 维只是为了演示):

国王  = [ 0.9,  0.4,  0.7,  0.5]
王后  = [ 0.8,  0.5,  0.7,  0.7]
男人  = [ 0.7,  0.3,  0.5,  0.2]
女人  = [ 0.6,  0.4,  0.5,  0.4]
苹果  = [-0.2,  0.8, -0.5,  0.6]
汽车  = [-0.4, -0.6,  0.8, -0.3]

我们计算「国王」和其他 5 个词的点积:

「国王」·「王后」 \(= 0.9 \cdot 0.8 + 0.4 \cdot 0.5 + 0.7 \cdot 0.7 + 0.5 \cdot 0.7 = 0.72 + 0.20 + 0.49 + 0.35 = 1.76\)

「国王」·「男人」 \(= 0.9 \cdot 0.7 + 0.4 \cdot 0.3 + 0.7 \cdot 0.5 + 0.5 \cdot 0.2 = 0.63 + 0.12 + 0.35 + 0.10 = 1.20\)

「国王」·「女人」 \(= 0.9 \cdot 0.6 + 0.4 \cdot 0.4 + 0.7 \cdot 0.5 + 0.5 \cdot 0.4 = 0.54 + 0.16 + 0.35 + 0.20 = 1.25\)

「国王」·「苹果」 \(= 0.9 \cdot (-0.2) + 0.4 \cdot 0.8 + 0.7 \cdot (-0.5) + 0.5 \cdot 0.6 = -0.18 + 0.32 - 0.35 + 0.30 = 0.09\)

「国王」·「汽车」 \(= 0.9 \cdot (-0.4) + 0.4 \cdot (-0.6) + 0.7 \cdot 0.8 + 0.5 \cdot (-0.3) = -0.36 - 0.24 + 0.56 - 0.15 = -0.19\)

排序:王后 (1.76) > 女人 (1.25) > 男人 (1.20) > 苹果 (0.09) > 汽车 (-0.19)。

这个排序符合直觉:和「国王」最像的是「王后」,其次是同样表示人的「女人」「男人」,最不像的是「苹果」「汽车」。

注意「女人」比「男人」点积稍高,这是数据决定的,不是哲学决定的。如果我们用不同的 embedding,排序可能会变。我特意构造这个例子是为了展示「点积排序的结果取决于 embedding 的具体数值」。

把这个对比扩展到所有 6 个词两两组合,得到一个 \(6 \times 6\) 的相似度矩阵:

相似度热力图

这就是注意力机制里 \(QK^\top\) 矩阵的小型预演。把「6 个词」换成「一句话里的所有 token」,把「人工 4 维 embedding」换成「学出来的 \(\mathbf{q}, \mathbf{k}\) 向量」,就得到了真实的注意力分数矩阵。

十二、点积的不变性与不变量

数学上,点积有一个非常重要的性质:它在正交变换下不变

什么是正交变换?简单地说,就是「保长度、保夹角」的变换,旋转和镜像都是。形式上,矩阵 \(R\) 是正交的,当且仅当 \(R^\top R = I\)

正交变换下点积不变的意思是:

\[ (R\mathbf{a}) \cdot (R\mathbf{b}) = \mathbf{a} \cdot \mathbf{b} \]

证明很短:\((R\mathbf{a})^\top (R\mathbf{b}) = \mathbf{a}^\top R^\top R \mathbf{b} = \mathbf{a}^\top I \mathbf{b} = \mathbf{a}^\top \mathbf{b}\)

这个性质在 Transformer 里至关重要。它告诉我们,只要我们把所有向量同步旋转一下(同一个旋转 \(R\)),点积保持不变。换句话说,注意力对「整体坐标系的选择」是不敏感的。这对于位置编码、特别是 RoPE(旋转位置编码)的设计有直接影响。RoPE 的核心思想就是「让不同位置的向量被不同角度的旋转编码,而点积反映的是相对旋转角度,也就是相对位置」。

这个性质我们在第 17 篇 RoPE 旋转位置编码会深入。这里只需要先记住:「点积关心方向的相对关系,不关心绝对方向」。

十三、负点积是什么意思

如果两个向量夹角大于 90°,它们的点积是负的。这在物理上对应「方向相反」,在语义上有什么意义?

在 word embedding 时代(Word2Vec、GloVe),负点积常常对应「反义」或「弱相关」。比如「热」和「冷」可能点积是负的。

在 Transformer 里就更复杂了。\(\mathbf{q}\)\(\mathbf{k}\) 的负点积表示「这个键不应该被这个查询关注」。经过 softmax 后,负的注意力分数会被压成接近零的概率,所以负点积本质是「主动避开」。

不过 softmax 是单调的,最大的注意力总会落在点积最大的那个键上。负点积只是「相对于其他键的劣势」,不是绝对的禁止。如果一句话所有键和查询的点积都是负的(极端情况),softmax 仍然会把概率分给某个键——只是分布更平均一些。

十四、点积 = 0:正交

夹角是 90° 时,点积是 0,称为正交(orthogonal)。在二维和三维,正交对应「垂直」。在更高维,正交是「最不相关」的几何含义。

正交在线性代数里是一个非常重要的概念。一组互相正交且各自单位长度的向量叫标准正交基(orthonormal basis)。任何向量都可以唯一分解成标准正交基的线性组合,组合系数就是点积。

在神经网络里,初始化有时会用到正交基(特别是 RNN 时代)。让权重矩阵的列向量初始为正交,可以避免初期信号在网络中经过乘法后爆炸或消失。在现代大模型里这种技巧用得少了,但「正交基」这个概念在 PCA、SVD、注意力的可视化里反复出现。

十五、点积与协方差、相关系数

如果我们把向量 \(\mathbf{a}\) 视为「一组数据样本」(比如某个特征在 \(n\) 个样本上的取值),那么点积 \(\mathbf{a} \cdot \mathbf{b}\) 减去均值再归一化后,就是皮尔逊相关系数

\[ \rho(\mathbf{a}, \mathbf{b}) = \frac{(\mathbf{a} - \bar{\mathbf{a}}) \cdot (\mathbf{b} - \bar{\mathbf{b}})}{|\mathbf{a} - \bar{\mathbf{a}}| \cdot |\mathbf{b} - \bar{\mathbf{b}}|} \]

这就是「两个变量的线性相关性」。

这个观察让点积和统计学接上了线:点积 = 不去均值不归一的相关性。这在「向量是不是在同一组特征上同步变化」的问题上提供了直觉。

在注意力机制里我们不去均值也不归一(前面解释过原因),所以注意力里的点积是「未中心化、未归一化的相关性」。这种「粗糙版相关性」在大量数据上仍然能学出有意义的模式,是经验事实。

十六、铺垫:从点积到 \(QK^\top\)

现在我们已经知道: - 点积衡量两个向量的对齐程度。 - 点积可以高度并行化(矩阵乘法)。 - 点积的几何意义在高维仍然成立。 - 点积在正交变换下不变。

把这些性质拼起来:

注意力的输入是一句话,每个 token 经过 embedding 变成一个向量 \(\mathbf{x}_i\)。每个 \(\mathbf{x}_i\) 经过两个线性变换变成查询 \(\mathbf{q}_i\) 和键 \(\mathbf{k}_i\)

我们想知道:第 \(i\) 个 token 应该关注第 \(j\) 个 token 多少?答案是:\(\mathbf{q}_i \cdot \mathbf{k}_j\)

把所有 token 的 \(\mathbf{q}\) 拼成矩阵 \(Q\)\(n\)\(d\) 列),所有 token 的 \(\mathbf{k}\) 拼成矩阵 \(K\)\(n\)\(d\) 列)。所有的两两点积可以一次性算出来:\(QK^\top\),得到一个 \(n \times n\) 矩阵,第 \((i, j)\) 项就是 \(\mathbf{q}_i \cdot \mathbf{k}_j\)

这就是注意力公式 \(\mathrm{softmax}(QK^\top / \sqrt{d_k}) V\)\(QK^\top\) 的来历。

我们花了一整篇讲点积,最终落在「注意力里就是在做大量点积」。这种「把整篇用大量点积表达,再用矩阵乘法批量算」的范式,是 Transformer 高效的根本原因。下一篇 03 矩阵乘法的两种视角会专门讲这个「批量化」过程。

十七、点积的实现注意事项

在代码里,点积有多种实现方式,性能差异巨大:

最朴素:Python for 循环。

def dot(a, b):
    s = 0.0
    for x, y in zip(a, b):
        s += x * y
    return s

这种写法只能用来教学,实际不可用——Python 解释开销让它比下面的方法慢 100-1000 倍。

NumPy 向量化

import numpy as np
np.dot(a, b)        # 推荐
a @ b               # Python 3.5+ 的语法糖
(a * b).sum()       # 等价但稍慢

np.dot 底层调用 BLAS(Basic Linear Algebra Subprograms),在 CPU 上是 SIMD 优化的。

PyTorch 上的点积(GPU 友好):

import torch
torch.dot(a, b)             # 一维向量
torch.matmul(a, b)          # 一般矩阵乘法
a @ b                       # 同 matmul
torch.einsum('i,i->', a, b) # 显式但灵活

在大语言模型里,几乎从来不会出现「单个向量点积」,永远是批量矩阵乘法

# Q: (B, H, T, D)  -- batch, heads, time, head_dim
# K: (B, H, T, D)
scores = Q @ K.transpose(-2, -1)   # (B, H, T, T)

这一行就是几亿次点积同时进行。GPU 上 cuBLAS 会把它编译成几个 GEMM 调用,跑在 Tensor Core 上。第 03 篇会展开矩阵乘法的批量化。

十八、数值稳定性陷阱

点积虽然简单,但在工程里有几个数值陷阱:

陷阱一:FP16/BF16 的累加精度。FP16 只有 10 位尾数,BF16 只有 7 位。两个向量长度都是几千维,逐项相乘再求和,累加误差可能让最终点积失准 1% 以上。所以现代框架常常用 FP32 做累加,输入是 FP16/BF16,但 accumulator 保留 FP32:

out_fp32 = sum(a_fp16 * b_fp16)   # 累加用 fp32
out_final = out_fp32.to(fp16)     # 输出再转回去

NVIDIA 的 Tensor Core 自动处理这种「混合精度」。

陷阱二:极大值导致溢出。如果 \(\mathbf{q}, \mathbf{k}\) 数值很大,点积可能超过 FP16 的最大值(65504)。这是为什么 LayerNorm 通常放在 Q, K 之前,让数值在合理范围。

陷阱三:极小值导致下溢。在很高维(几千)下,如果点积的绝对值很小(几个 1e-3),FP16 的表达能力可能丢失精度。这种情况比上溢罕见,但调试时要注意。

陷阱四:点积后接 softmax 的数值稳定\(\mathrm{softmax}(QK^\top / \sqrt{d_k})\) 里如果 \(QK^\top\) 太大,\(\exp(\cdot)\) 会爆炸。所以要除以 \(\sqrt{d_k}\),并且 softmax 内部要做 \(-\max\) 减法。这个细节第 13 篇会专门讲。

十九、点积的可视化:注意力热力图

注意力的可视化经常使用「热力图(heatmap)」形式。横轴和纵轴都是 token 序列(一句话),格子的颜色代表点积的大小(softmax 之前)或概率(softmax 之后)。

热力图能让我们直观看到「哪个 token 关注了哪个 token」。一个常见模式是:

第 13 篇会展示真实模型的注意力可视化,那时再回来对照。本篇先建立「点积 = 一个数 = 一个格子的颜色」这个直觉。

二十、关键概念回顾

向量、长度、夹角:向量是带方向的量,二维下可以画成箭头。它有长度(勾股定理)、方向(单位化)、夹角(两个向量之间的关系)三个属性。这三者在所有维度都有意义,虽然到了高维我们只能依靠公式而无法直接画图。

点积的两种定义等价:代数定义是 \(\sum a_i b_i\),几何定义是 \(|a||b|\cos\theta\)。两者通过余弦定理可以严格证明等价。代数定义易于计算,几何定义提供意义。理解点积就是把这两个视角同时握在手里——看到公式时能想起箭头,看到箭头时能写出公式。

点积衡量「对齐度」:综合了长度和方向。两支箭都长且同向,点积大;垂直则点积为零;反向则点积为负。这种「一个数综合两件事」的能力是点积成为相似度度量的根本原因。

为什么注意力用点积:长度敏感是优点(强信号有更大权重);可矩阵化是巨大优势(GPU 友好);几何意义清晰(对齐度);正交变换不变(位置编码可以用旋转实现)。综合起来点积击败了欧氏距离、L1、KL 散度等所有候选方案。

点积是注意力的「核心算子」:整个注意力机制可以归约为「大量点积 + softmax + 加权求和」。把这三步看作一个组合算子,Transformer 的每一层就是这个组合算子的若干次应用。读到这里你已经理解了 Transformer 的最底层操作的一半(另一半是矩阵乘法的批量化,下一篇)。

高维下点积仍然有效:几何定义在任何维度都成立,但需要警惕「随机两个向量倾向于近正交」的现象。注意力的缩放因子 \(\sqrt{d_k}\) 就是为了对抗这个现象。

二十一、常见误解

误解一:「点积越大表示两个向量越相似。」 这是不严谨的说法。准确说法是「点积越大表示两个向量在长度加权后越对齐」。如果两个向量长度差异很大,长的那个会主导点积,看起来「相似」实际只是「长」。在做向量检索之前先单位化,是为了避免这种「长度偏见」。在注意力里我们不单位化,因为模型可以自己学到合适的尺度。

误解二:「余弦相似度比点积更好,因为它取值在 \([-1, 1]\) 更直观。」 直观和有效是两回事。余弦相似度在向量检索里更好,因为我们希望对长度不敏感(每个向量的「重要性」由它出现的次数或语义决定,不是由长度决定)。但在端到端训练的注意力里,点积让模型自己控制尺度,反而更好。「更好」必须放在具体场景里讨论。

误解三:「点积是『加权平均』。」 不是。点积是「逐项乘积之和」,没有归一化。加权平均是 \(\sum w_i x_i / \sum w_i\),分母会让权重之和为 1。把点积叫做加权和(weighted sum)更准确,叫做加权平均是不对的。

误解四:「在高维空间里点积没意义,因为所有向量都接近正交。」 这是把「随机向量近似正交」推广到了「学出来的向量近似正交」。不对。神经网络通过梯度下降学到的向量不是随机的——它们是在某个语义结构下排列的,相似含义的向量会接近,不相似的会远离。所以即使在 4096 维,「国王」和「王后」的点积仍然显著大于「国王」和「汽车」。

误解五:「点积可以替代任何相似度度量。」 不能。当我们关心的「相似」是分布层面的(两个概率分布的差异),用 KL/JS;当我们关心的是几何距离(两个点的远近),用欧氏距离;当我们关心的是序列相似(编辑次数),用编辑距离。点积只是一种相似度,恰好在「分布式表示的对齐」这个问题上特别合适。

误解六:「点积是 Transformer 的发明。」 不是。点积在线性代数里几百年了,在机器学习里几十年了(核方法 SVM 的核心就是点积)。Transformer 的贡献不是「使用点积」,而是「把点积嵌入到一个端到端可训练的注意力机制里,并把它扩展到长序列、多头、堆叠多层」。


二十一、点积的历史小掌故

「点积」这个词的历史比大多数人想象的要长。

线性代数的早期。十九世纪上半叶,Hamilton 在研究四元数时引入了「scalar product」这个概念,是点积的早期形式。Gibbs 和 Heaviside 进一步整理了向量代数,把点积和叉积分别定义出来,这套体系基本就是今天物理和工程里使用的标准向量运算。所以「点积」这个名字大约从 1880 年代开始稳定。

统计学的承接。1900 年前后,Karl Pearson 在研究两个变量相关性时,得到了「皮尔逊相关系数」,本质就是去均值标准化后的点积。这是点积第一次被用来「衡量两个东西的相似」。

信息检索的承接。1960-70 年代,Gerard Salton 等人把点积用到文档检索:把文档表示成词频向量,用点积衡量「查询和文档的相关性」,这就是 TF-IDF + 余弦相似度的原型。这个范式直到今天搜索引擎仍然在底层使用。

机器学习的承接。1990 年代核方法兴起,SVM 的「核技巧」本质就是「在某个高维空间里做点积」。点积在这里被推广成「内积空间」(任何满足对称性、双线性、正定性的二元运算)。这种泛化让点积成了机器学习的通用算子。

深度学习的承接。2014 年 Bahdanau 注意力首先用了加性注意力(不是点积),但 2015 年 Luong 的论文系统对比后,发现点积注意力效果不差但计算更快。2017 年 Transformer 直接采用了缩放点积注意力,确立了点积在深度学习中的地位。

所以点积的故事是:一个 19 世纪的几何概念,被 20 世纪的统计学和信息检索借用,再被 21 世纪的深度学习推到舞台中央。这种「数学概念被反复借用,每次借用都贡献一点新意义」的故事,在科学史上其实是常态。

二十二、点积与「核函数」的世系

进一步深入一层:点积属于「核函数(kernel function)」家族。

核函数 \(K(\mathbf{x}, \mathbf{y})\) 是一个二元函数,满足某些性质(对称、半正定)。最简单的核就是点积 \(K(\mathbf{x}, \mathbf{y}) = \mathbf{x} \cdot \mathbf{y}\),称为「线性核」。

更复杂的核包括:

这些核都可以替代点积来衡量相似度。SVM 时代有大量研究在比较哪种核最好。但是注意力机制基本只用线性核,原因还是那一个:矩阵乘法实现性能极佳,其他核形式无法用普通矩阵乘法批量化

近几年有一些工作试图把 attention 改造成「kernel attention」,比如 Performer(用随机特征近似 RBF 核)、Linformer(低秩近似)等。这些工作的目的是降低复杂度(从 \(O(n^2)\)\(O(n)\)),但实测效果不如标准 attention 稳定。所以点积仍然是默认选择。这部分内容会在第 33 篇线性注意力展开。

二十三、点积的对偶视角:内积空间与 Hilbert 空间

数学上的进一步推广:内积可以定义在「内积空间(inner product space)」上,远不止 \(\mathbb{R}^n\)

例如,函数空间也可以有内积:

\[ \langle f, g \rangle = \int_a^b f(x) g(x) \, dx \]

这就是「两个函数的内积」。泛函分析里整个一章都在讲这种推广。著名的 Hilbert 空间 就是「完备的内积空间」。

在机器学习里,RKHS(再生核 Hilbert 空间) 把核函数和内积空间联系起来:每个合法核对应一个 Hilbert 空间,核函数就是这个空间里两个函数的内积。这个观点让 SVM 的理论分析变得优雅。

对于本系列,我们不深入泛函分析,但记住「点积」是「内积」的特例,「内积」可以发生在任何具有良好结构的空间里。这种抽象在深度学习里偶尔会用到(比如 score-based generative models 里的「Stein 算子」就用了 RKHS)。

二十四、向量与「特征」的关系

在工程语境里,向量经常和「特征(feature)」混用。它们不完全等同。

特征(feature):从原始数据中提取的、有代表性的量。比如对一张图片,特征可以是「亮度均值、边缘数量、颜色直方图」。这些特征拼起来形成一个向量。

向量(vector):纯数学对象,是「一组数 + 加法和数乘的代数结构」。

特征通常表示为向量,但向量不一定是特征。一个 token 的 embedding 是向量,但它是「学出来的」,不是「人为提取的」,所以叫「学习的特征(learned feature)」更准确。

在 Transformer 时代,「特征工程」这个词几乎消失了,取而代之的是「让模型自己学特征」。但「向量」这个数学结构仍然贯穿一切。理解「特征是向量的一种用法」,能帮助你跨越「传统机器学习 → 深度学习」的语境差异。

二十五、点积的代码实战:手写一个最小注意力

我们用 NumPy 实现一个最小的「点积注意力」,看看从向量到注意力的全过程。

import numpy as np

# 6 个 token,每个 4 维 embedding
X = np.array([
    [ 0.9,  0.4,  0.7,  0.5],   # 国王
    [ 0.8,  0.5,  0.7,  0.7],   # 王后
    [ 0.7,  0.3,  0.5,  0.2],   # 男人
    [ 0.6,  0.4,  0.5,  0.4],   # 女人
    [-0.2,  0.8, -0.5,  0.6],   # 苹果
    [-0.4, -0.6,  0.8, -0.3],   # 汽车
])

# 简化:直接用 X 自己作为 Q 和 K(真实 Transformer 会先经过 W_Q, W_K 变换)
Q = X
K = X

# 一次性计算所有 token 两两点积,得到 6x6 矩阵
scores = Q @ K.T

# 缩放
d_k = X.shape[-1]
scores = scores / np.sqrt(d_k)

# softmax 沿着每一行
def softmax(x, axis=-1):
    x = x - x.max(axis=axis, keepdims=True)
    ex = np.exp(x)
    return ex / ex.sum(axis=axis, keepdims=True)

attn = softmax(scores, axis=-1)

print("Attention matrix (rows sum to 1):")
print(np.round(attn, 3))

跑一下能看到:每一行加起来是 1(softmax 的性质),「国王」那一行在「国王、王后、男人、女人」位置上分配较大权重,在「苹果、汽车」位置上权重很小。这就是注意力机制最赤裸的版本。

整个流程的核心就是 Q @ K.T 这一行:批量做了 36 次点积。理解了这一行,你就理解了 Transformer 的 50%。

二十六、用 PyTorch 实现并加上批量维度

NumPy 版本只能处理一句话。真实训练时一次处理几十甚至几千句话,需要加 batch 维度。PyTorch 的写法:

import torch
import torch.nn.functional as F

B, T, D = 32, 64, 512   # batch=32, 序列长度 64, embedding 维度 512

X = torch.randn(B, T, D)
W_Q = torch.randn(D, D) / D**0.5
W_K = torch.randn(D, D) / D**0.5
W_V = torch.randn(D, D) / D**0.5

Q = X @ W_Q          # (B, T, D)
K = X @ W_K          # (B, T, D)
V = X @ W_V          # (B, T, D)

scores = Q @ K.transpose(-2, -1)   # (B, T, T)
scores = scores / D**0.5

attn = F.softmax(scores, dim=-1)   # (B, T, T)
out  = attn @ V                    # (B, T, D)

print(out.shape)   # torch.Size([32, 64, 512])

这就是「单头注意力」的全部代码。把它扩展到多头,只需要把 D 拆成 H * d_h,再 reshape 一下,留到第 15 篇多头注意力详谈。

注意 Q @ K.transpose(-2, -1) 这行:在 PyTorch 里,@torch.matmul,对最后两个维度做矩阵乘法,前面的维度作为 batch。所以这一行实际上是 32 次「64×512 矩阵 与 512×64 矩阵」的乘法。

二十七、点积的 GPU 加速:为什么注意力如此擅长用 GPU

点积是 GPU 最擅长的算子之一。原因:

1. 高度并行。不同位置的乘法之间没有依赖,可以同时计算。

2. 数据局部性好。两个向量在内存里是连续存储的,缓存命中率高。

3. 适合 SIMD 和 Tensor Core。SIMD 一次处理多个数;NVIDIA 从 Volta 开始引入 Tensor Core,可以一次完成 4×4×4 的矩阵乘法。

4. cuBLAS 的 GEMM 已经优化到极致。GEMM(general matrix-matrix multiply)是科学计算几十年磨出来的算子,cuBLAS 上的 GEMM 接近 GPU 理论峰值的 70-90%。

注意力公式 \(\mathrm{softmax}(QK^\top / \sqrt{d_k})V\) 里,两次矩阵乘法(\(QK^\top\)\(\cdot V\))就占了 80% 以上的算力。softmax 和缩放都是「廉价的」逐元素操作。所以「Transformer 训练快」的本质是「Transformer 里点积特别多,而点积 GPU 极擅长」。

这也是为什么 RNN 在 GPU 时代逐渐被 Transformer 取代:RNN 的递归结构有时间维度的依赖,无法并行;Transformer 的注意力把所有 token 一起处理,并行度天然高。这部分在第 27 篇「Transformer 为何能并行训练」会专门讨论。

二十八、点积的复杂度

对长度 \(d\) 的向量,单次点积是 \(O(d)\)

\(n \times d\) 的矩阵 \(Q\)\(n \times d\) 的矩阵 \(K\),计算 \(QK^\top\)\(O(n^2 d)\)(一共 \(n^2\) 个点积,每个 \(O(d)\))。

这就是著名的「注意力的二次复杂度」。当序列长度 \(n\) 很大时(比如 100K 甚至 1M tokens),\(n^2\) 会爆炸。这是 Transformer 在长上下文场景下的最大瓶颈,也是 FlashAttention、Linformer、Mamba 等工作的动机。

不过对中等长度序列(4K-32K),\(n^2\) 仍然可控。Llama 3 的训练上下文是 8K,Llama 3.1 扩展到 128K,需要专门优化。GPT-4 的 128K 上下文也是工程挑战的产物。

第 31 篇「长上下文挑战」会专门讨论,这里只指出:点积本身廉价,但点积的数量是序列长度的平方——这才是问题所在。

二十九、向量的「平均」与「加权和」

注意力机制的输出形式是 \(\mathrm{softmax}(\text{scores}) \cdot V\),等价于「对 V 做加权平均,权重由 scores 决定」。这个加权平均的本质是什么?

设有 \(n\) 个向量 \(\mathbf{v}_1, \ldots, \mathbf{v}_n\)\(n\) 个权重 \(\alpha_1, \ldots, \alpha_n\)\(\sum \alpha_i = 1\)\(\alpha_i \ge 0\))。它们的加权和:

\[ \bar{\mathbf{v}} = \sum_{i=1}^n \alpha_i \mathbf{v}_i \]

这是一个新向量,每个分量是对应分量的加权平均。几何上看,\(\bar{\mathbf{v}}\) 落在 \(\mathbf{v}_1, \ldots, \mathbf{v}_n\)凸包里——也就是「这些向量围成的多面体内部的某点」。

注意力的输出,本质就是对所有候选信息源做凸组合,权重由查询与每个键的点积决定。这种「凸组合 + 学习的权重」是注意力的几何骨架。

理解这一点之后,注意力就不再神秘:它是一个「带学习权重的加权平均」,权重通过点积 + softmax 得到。所有的复杂度都集中在「怎么从查询和键算出好权重」这一步。

三十、点积之外:再问一次「为什么是它」

有人会问:既然 attention 的本质是「加权平均」,为什么权重必须用点积来算?为什么不用别的可学函数(比如一个小 MLP)?

答案:有人试过,效果可比,但速度慢得多

Bahdanau 2014 用的是加性注意力:score(q, k) = v^T tanh(W_q q + W_k k)。这是一个小 MLP。理论上比点积表达力更强,因为多了一层非线性。

但点积的优势:

所以「点积」赢的不是「表达力」,而是「在效果接近的前提下,速度快得多」。这种 trade-off 在工程里反复出现:「最优」往往不是数学上最强的方案,而是「足够好且能跑得起来」的方案。

三十一、术语对照表

中文 English 备注
向量 vector 一般指 \(\mathbb{R}^n\) 中的元素
标量 scalar 单个数
矩阵 matrix 二维数组
张量 tensor 任意维度数组
点积 dot product 名字来自记号「·」
内积 inner product 推广概念,包括点积
外积 outer product 两个向量得到一个矩阵
叉积 cross product 仅在 3 维有效,本系列基本不用
范数 norm 长度的推广
单位向量 unit vector 长度为 1 的向量
单位化 normalize 把向量除以自己的长度
正交 orthogonal 点积为 0
标准正交基 orthonormal basis 互相正交且单位长度的一组向量
投影 projection 一个向量在另一个方向上的分量
余弦相似度 cosine similarity 单位化后的点积
欧氏距离 Euclidean distance \(\|\mathbf{a} - \mathbf{b}\|\)
曼哈顿距离 Manhattan distance L1 距离
双线性形式 bilinear form 对两个参数都线性
核函数 kernel function 推广的内积
内积空间 inner product space 带内积的向量空间
Hilbert 空间 Hilbert space 完备的内积空间
RKHS reproducing kernel Hilbert space 再生核 Hilbert 空间

三十二、FAQ

Q1:为什么 \(|\mathbf{a}|\)\(\|\mathbf{a}\|\) 都用?有什么区别?

A:两种记号都常见。\(|\cdot|\) 在物理和工程文献里更普遍;\(\|\cdot\|\) 在数学和泛函分析里更普遍(因为可以加下标 \(\|\cdot\|_p\) 表示不同的 p 范数)。本系列我两种都会用,含义相同(除非特别说明)。

Q2:点积和外积是同一回事吗?

A:不是。点积输入两个向量输出一个标量;外积输入两个向量输出一个矩阵。\(\mathbf{a} \otimes \mathbf{b} = \mathbf{a} \mathbf{b}^\top\)。在 attention 里的 \(V\) 加权和实际上含有一些外积的影子,但是直接出现的是点积。

Q3:叉积在 attention 里用吗?

A:不用。叉积只在三维有定义,注意力里向量是高维的,叉积没法直接应用。叉积主要用在物理(角动量、磁场)和图形学(法线、旋转)。

Q4:点积和卷积有关系吗?

A:有间接关系。卷积可以看成「滑动窗口的点积」——把卷积核和输入的局部窗口做点积,再滑动到下一个位置。但卷积的「平移不变性」和点积本身无关,是窗口移动的方式带来的。

Q5:为什么深度学习里几乎所有相似度算子都基于点积?

A:因为点积可以高度并行化,工程上是 GPU 最擅长的算子。其他相似度(欧氏距离、KL 等)能做但慢。在「足够好 + 极快」之间,工程界几乎总是选后者。

Q6:高维下点积值的范围怎么估计?

A:如果 \(\mathbf{q}, \mathbf{k}\) 各分量独立同分布,期望为 0、方差为 1,那么点积 \(\sum q_i k_i\) 的方差大约是 \(d\)(维度)。所以典型量级是 \(\sqrt{d}\)。这就是 attention 公式里除以 \(\sqrt{d_k}\) 的依据:让点积值标准化到 \(O(1)\)

Q7:可以用矩阵的 Frobenius 内积吗?

A:Frobenius 内积是 \(\langle A, B \rangle_F = \mathrm{tr}(A^\top B) = \sum_{ij} A_{ij} B_{ij}\),等于把矩阵展平成向量后的点积。在 attention 里,每次的 score 是「向量 vs 向量」,而不是「矩阵 vs 矩阵」,所以 Frobenius 内积本身不直接出现,但它的概念在「计算两个 attention map 的相似度」时偶尔会用。

Q8:余弦相似度大于 0 的两个向量,是不是一定夹角小于 90°?

A:是。\(\cos\theta > 0 \iff \theta \in [0, \pi/2)\)

Q9:点积满足三角不等式吗?

A:点积本身不是「距离」,所以不直接讨论三角不等式。但它满足 Cauchy-Schwarz 不等式\(|\mathbf{a} \cdot \mathbf{b}| \le |\mathbf{a}| \cdot |\mathbf{b}|\)。等号成立当且仅当两向量平行。

Q10:为什么 attention 用点积,但 word2vec 用「负采样 + 点积」,不直接用 softmax?

A:word2vec 时代算力有限,全 vocabulary 的 softmax 太贵(vocab 量级 \(10^5\)),所以用负采样近似。今天的 LLM 在输出层依然有 softmax 但用了各种工程优化(fused softmax、近似 softmax 在解码时使用)。

Q11:可以用复数向量做点积吗?

A:可以,但要小心。复数向量的「内积」标准定义是 \(\langle \mathbf{a}, \mathbf{b} \rangle = \sum_i \overline{a_i} b_i\)(共轭一个再相乘)。RoPE 里实际上用的就是复数视角,但工程实现会拆成实部虚部两个实数向量。第 17 篇 RoPE 详谈。

Q12:attention 一定要用点积吗,能不能换?

A:理论上可以,工程上不划算。Performer、Linear Transformer 等用「核近似」在长序列下有优势,但训练效果常常逊色于标准 attention。在主流大模型里,点积 + softmax 是默认。

Q13:在量化推理里点积的精度怎么控制?

A:常见做法是 INT8 输入、INT32 累加(避免溢出),最后反量化回 FP16。这是 GPU 上 INT8 GEMM 的标准做法。详细见第 50 篇量化推理。

Q14:稀疏向量的点积怎么算?

A:只算两个向量都非零的那些位置。代码实现常用「哈希表交集」或「双指针」。在向量检索中(特别是关键词检索 BM25)很常用。在 dense Transformer attention 里基本不出现,因为向量是稠密的。

Q15:点积可以用来做 clustering 吗?

A:可以。球面 K-means 就是用余弦相似度(单位化后的点积)取代欧氏距离的 K-means。在文本聚类里效果常常优于普通 K-means。

三十三、实践小练习

为了巩固,建议你做下面几个练习:

练习 1。手算 \(\mathbf{a} = (1, 2, 3)\)\(\mathbf{b} = (4, 0, -1)\) 的点积,再单位化两个向量算余弦相似度。验证 \(|\mathbf{a}| \cdot |\mathbf{b}| \cdot \cos\theta\) 等于点积。

练习 2。用 NumPy 写一个函数 pairwise_dot(X),输入 \(n \times d\) 矩阵,输出 \(n \times n\) 矩阵,每个元素是对应两行的点积。要求 1 行代码(不能用 for 循环)。

练习 3。用 PyTorch 写一个最小的注意力函数 attention(Q, K, V),返回输出。要求支持 batch 维度。

练习 4。把上一题的注意力函数加上「causal mask」(只允许关注自己和之前的位置)。提示:用 torch.tril 生成下三角矩阵,把上三角设成 \(-\infty\) 再过 softmax。

练习 5。比较 torch.matmul(Q, K.transpose(-2, -1))torch.einsum('bld,bmd->blm', Q, K) 的速度(在 GPU 上、足够大的输入下)。einsum 的可读性如何?

练习 6。验证「随机两个高维向量近似正交」:从单位球面均匀采样 1000 对 1024 维向量,画出它们点积的直方图。看到分布集中在 0 附近吗?方差大约多少?是否接近 \(1/d\)

练习 7。读 nanoGPT 的 model.py,找到 CausalSelfAttention 类,把那一段代码和本篇的所有公式对应起来。

这些练习里前 4 个是「概念性」的,做完能让你确信自己理解了;后 3 个是「工程性」的,做完能让你对真实模型实现有感性认识。

三十四、本篇与后面的关系

本篇我们建立了「点积」这一个基本砖块。后面:

如果你读到这里仍然觉得「点积只是一个数值运算」,没关系,继续往下读。等你看到 RoPE 用旋转编码相对位置、看到 GQA 用键值共享降低内存、看到 FlashAttention 用分块计算提升速度时,会突然意识到:所有这些技巧的核心都是「点积」这一行代码周围的优化。点积是 Transformer 的圆心。

三十五、再深一步:点积的「能量」直觉

如果你有物理背景,点积有一个很有用的「能量」直觉。

考虑一根弹簧,受力 \(\mathbf{F}\) 让它产生位移 \(\mathbf{d}\)。这个过程做了多少?答案是 \(W = \mathbf{F} \cdot \mathbf{d}\)

为什么是点积?因为只有「力沿着位移方向的分量」才做正功;垂直于位移的力(比如重力作用在一个水平推动的物体上)不做功。点积自然地把「方向不一致」的影响排除了。

这个「能量」类比对理解 attention 也有启发:当查询 \(\mathbf{q}\) 与键 \(\mathbf{k}\) 方向一致,「信息流」就大;当方向不一致,信息流被压制。注意力分数可以理解为「这个键给这个查询贡献了多少信息能量」。当然这只是一个比喻,不是严格物理对应,但它能帮助你建立直觉。

三十六、几何陷阱:在高维直觉容易出错

二维三维的几何直觉,到了高维有时会失效。下面是几个典型的「高维反常」:

反常一:高维球的体积主要集中在「赤道附近」。在 \(d\) 维球面上均匀采样的点,几乎都集中在某条赤道附近(任何固定的赤道)。

反常二:高维立方体的「体积」几乎全部在「角落附近」。在 \([0,1]^d\) 中,「中心附近」的体积占比随 \(d\) 增大趋于 0。

反常三:高维下「最近邻」和「最远邻」差异很小。这让传统的 KNN 在高维下失效。

反常四:随机两个向量趋于正交。我们前面讨论过,余弦相似度方差 \(\sim 1/d\)

这些「反常」的共同根源是:高维空间「太大了」,二维三维的体积/距离/角度直觉根本捕捉不到它的庞大。但是,学习的向量不是均匀分布的——它们沿着语义结构紧密排列,所以学习出来的向量之间仍有显著的相似度差异。这是为什么深度学习在高维下仍然有效。

理解「学习让高维变得可控」是深度学习的一个重要哲学。我们不靠「均匀分布的高维统计」,而靠「学到的低维结构嵌入在高维里」。

三十七、本篇的小结

我们一开始问「为什么注意力用点积」。走到这里,答案分几层:

第一层(几何):点积衡量两个向量的对齐程度,综合长度和方向。

第二层(工程):点积可以用矩阵乘法批量化,GPU 上极快。

第三层(数学):点积是内积空间的基本操作,正交不变性让位置编码有清晰几何含义。

第四层(学习):点积让模型自由学习尺度,不强加单位化约束。

第五层(历史):点积在物理、统计、机器学习里反复出现,到了 Transformer 这一棒被推到了主舞台。

这五层答案互相支撑,缺一不可。读完本篇,你应该能对「为什么是点积」给出至少其中三层的解释。

下一篇我们把单次点积升级为矩阵乘法的批量视角,正式进入「为什么 Transformer 适合 GPU」的讨论。

三十八、点积之外的细节延伸

下面这些是点积主线之外、但读者经常会问起的几个细节。我没法在每篇都展开,但本篇先把大致的「这件事存在」标出来,避免你以后遇到时陷入疑惑。

关于 batched dot product 的不同写法。在 PyTorch 里,至少有以下几种等价写法:

三者数值上相等,但性能差异巨大matmul 直接走 cuBLAS GEMM,最快;einsum 在简单情况下被优化器识别后会转成 matmul,复杂情况下慢;广播写法最慢,因为多了一个中间张量。

关于 fp32 累加的工程实践。在 NVIDIA H100 上,FP16 的 matmul 默认 FP32 累加;在 A100 上,BF16 的 matmul 默认 FP32 累加。你不需要手动配置,但要意识到这个事实——它能解释为什么模型在不同硬件上数值有微小差异。

关于 attention dropout。原始 Transformer 在注意力分数 softmax 之后加了 dropout:

attn = F.softmax(scores, dim=-1)
attn = F.dropout(attn, p=0.1)   # 随机丢一些权重
out = attn @ V

这个 dropout 不影响点积本身,但影响最终输出。现代大模型(GPT-3 之后)很少在 attention 上用 dropout,因为足够大的模型不太会过拟合。

关于 attention 的 bias。早期 Transformer 在 \(W_Q, W_K, W_V\) 后面有 bias 项(线性层的 \(b\))。现代大模型(PaLM、Llama)大多去掉了 attention 里的 bias,因为发现去掉之后效果不变甚至略好,还能省一点参数。

关于 attention 的对称性\(\mathbf{q}_i \cdot \mathbf{k}_j\) 一般不等于 \(\mathbf{q}_j \cdot \mathbf{k}_i\),因为 \(W_Q \ne W_K\)。所以注意力不是对称的:\(i\) 关注 \(j\) 不等于 \(j\) 关注 \(i\)。这种不对称是 attention 表达力的来源——如果对称,能表达的关系就少一半。

三十九、小坑大坑十则

坑 1:把点积写成 np.sum(a * b) 在大向量上慢于 np.dotnp.dot 调 BLAS,速度差几倍到几十倍。

坑 2:在 PyTorch 里 a @ b 当 a 和 b 都是 1 维向量时,返回标量;但当 a 是 2D 时,需要 b 也是 2D 或更高维,否则维度会出错。常见错误是 Q @ K.T 写成 Q @ K

坑 3:FP16 下点积溢出torch.matmul 在 FP16 模式下,如果某个乘积绝对值超过 65504,就会变成 inf。LayerNorm 通常能避免,但调试时要打印中间值。

坑 4:mask 用 -inf 不是 -1e9。softmax 里 mask 位置应该是 float('-inf'),让 softmax 后概率精确等于 0。-1e9 在 FP16 下不一定足够小(因为 FP16 的 -1e9 会被截断为 -65504)。

坑 5:transpose 和 contiguousK.transpose(-2, -1) 返回的不是 contiguous tensor,某些操作会要求先 .contiguous()。否则会报错。

坑 6:把点积和外积搞混\(\mathbf{a} \otimes \mathbf{b}\)(外积)输出矩阵;\(\mathbf{a} \cdot \mathbf{b}\)(点积)输出标量。维度对不上是常见 bug。

坑 7:广播下点积的语义混乱(a * b).sum(-1) 在 a 和 b 形状不同时会先广播再相乘,这未必是你想要的。建议写出明确的 reshape。

坑 8:在量化下点积不再精确。INT8 点积有舍入误差,特别是当向量长度大时(误差累加)。模型移植到 INT8 推理时要重新校准。

坑 9:注意力的 mask 加在缩放之后还是之前。标准做法是「先除 \(\sqrt{d_k}\),再加 mask,再 softmax」。如果顺序反了,mask 的 -inf 不会被缩放破坏,但其他实现细节可能受影响。

坑 10:跨 batch 的点积维度顺序。PyTorch 的约定是 (B, T, D)(batch first);TensorFlow 早期是 (T, B, D)(time first)。在跨框架移植时常见 bug。

四十、再次回到「为什么」

我们绕了一大圈,最终还是回到那个最初的问题:为什么注意力用点积?

如果你必须用一句话回答:

因为点积是「衡量两个向量对齐程度的最简、最快、可批量、可学习的算子」

「最简」:定义只有一个求和。 「最快」:GEMM 实现,GPU 极擅长。 「可批量」:\(QK^\top\) 一次算完。 「可学习」:\(W_Q, W_K\) 让模型自调尺度。

任何替代方案都至少在一个维度上输给点积。所以工程界选择了它,并不打算换。

四十一、对工程师的实用建议

如果你是工程师,读完本篇之后想立刻把它用起来,下面是几条具体建议:

建议一:每当你看到 Q @ K.T 这样的代码,停下来问自己「这是在做什么形状的运算?输出的每个元素代表什么?」。养成「看见矩阵乘法就分析形状」的习惯,能在 debug 时省下大量时间。

建议二:在写新模型代码之前,先用「shape comments」把每一行的张量形状写在旁边:

Q = X @ W_Q          # (B, T, D)
K = X @ W_K          # (B, T, D)
scores = Q @ K.transpose(-2, -1)   # (B, T, T)

这种注释看起来啰嗦,但能预防 99% 的 shape mismatch bug。

建议三:调试时打印 attention map(softmax 之后的 (T, T) 矩阵)。看它是不是「对角线显著」「下三角填充」「均匀分布」等。模式不对说明哪里写错了。

建议四:性能调优时优先优化 attention 部分,因为它是大头。Q @ K.Tsoftmaxattn @ V 这三步的总和往往占模型运行时间的 50%+。FlashAttention 把这三步融合优化,能加速 2-4 倍。

建议五:理解了点积之后,去看 nanoGPT 的源码(特别是 model.py 中的 CausalSelfAttention 类)。把每一行和本篇对应起来。看完之后你对 Transformer 的核心实现就不再陌生。

建议六:在面试中如果被问到「attention 为什么用点积」,按照本篇给出的「五层答案」回答。这是一个能展示你深度的问题。

四十二、对学习者的实用建议

如果你是学习者(学生、转行者、自学者),读完本篇之后想继续提升,下面是几条建议:

建议一:在脑子里建立「点积 = 两支箭对齐度」的画面。每次看到点积公式就想这个画面。

建议二:在草稿纸上手算 5-10 个不同的二维点积,用代数和几何两种方法。让两个定义在脑子里融为一体。

建议三:去 3Blue1Brown 看「Dot products and duality」那期视频。它会从一个完全不同的角度(线性变换的视角)解释点积,和本篇互补。

建议四:尝试写一个不用 NumPy/PyTorch 的纯 Python 点积函数,并把它和 NumPy 比较速度。感受一下「向量化运算」的力量差异。

建议五:如果你觉得本篇某些段落看不懂,先继续往下读,不要卡在某一句。理解是螺旋上升的,第一遍懂 60%,第二遍懂 80%,第三遍懂 95%。绝大多数读者都是这样。

建议六:找一个朋友讲解「为什么 attention 用点积」。能讲清楚才是真懂。如果没有合适的朋友,也可以用录音/写文章的方式自己讲给自己听。「输出」是检验「输入」的唯一可靠标准。

四十三、最后一个画面

我想用一个画面给本篇收尾。

想象你站在一个巨大的星空下。每颗星星代表一个词的 embedding,方向各异。你拿起一支「查询箭」指向某颗特定的星,问「哪些星星和你最像?」

每颗星星会和你的箭做一次点积。点积大的,是和你「方向最对齐」的,最该被关注;点积小的,是和你方向不一致的,可以忽略;点积负的,是和你方向相反的,应该刻意避开。

这就是注意力。

Transformer 的全部魔法,是把一句话里每一个词,都变成一支「查询箭」,去问其他所有词「你像我吗?」。问得多了,每个词都有了被「看见」和「看见别人」的关系,整句话的语义就在这种关系网里浮现出来。

下一篇,我们把「问一次」变成「同时问所有」——这就是矩阵乘法。

四十四、读到这里你应该获得了什么

如果让我用最朴素的语言总结本篇的「读后收获」,我会说:

第一:你应该能在看到 \(\mathbf{a} \cdot \mathbf{b}\) 这个符号时,脑子里同时浮现两个画面——「\(\sum a_i b_i\)」的计算过程和「两支箭夹角余弦」的几何画面。这两个画面应该像左右手一样自然切换。

第二:你应该能解释「为什么注意力用点积」至少给出三条不同层面的理由,而不是仅仅说「论文这样写」。

第三:你应该能在代码里看到 Q @ K.transpose(-2, -1) 时,立刻知道它是 (B, T, T) 矩阵,每一行经过 softmax 是一个概率分布。

第四:你应该对「高维空间里随机向量近似正交」这件事有所警觉,知道学习出来的向量不是随机的,所以并不会真的全部正交。

第五:你应该明白「点积」是一个跨数学—物理—统计—工程的概念,每个领域都有它的用法。这种「概念跨界使用」是数学的力量来源。

如果以上五条你都能自然做到,本篇的目标就达成了。

四十五、写在篇末

这一篇我写了好几遍。最早的版本只讲到第十一节就结束,大约 3000 字。但读了几次发现「仅仅讲完点积是不够的,必须把读者带到 attention 公式的边缘」,否则读者读完不知道这个知识点要去往何处。

所以我把整篇推到了「能看见 \(QK^\top\) 影子」的程度,并铺垫了 RoPE、量化、FlashAttention 等后续话题。这种「为后续埋伏笔」的写法在本系列里会反复出现——每一篇都不是孤立的,都为下一组主题做准备。

最后一句话:这一篇的所有努力,都是为了让你看到第 13 篇 \(\mathrm{softmax}(QK^\top/\sqrt{d_k})V\) 这条公式时,每个符号都能说出它的几何含义、工程含义、历史含义。如果到那一刻你能做到,这一篇就值了。


下一步

下一篇 03 矩阵乘法的两种视角 会把「单次点积」扩展为「批量点积 = 矩阵乘法」,并展示矩阵乘法的两种等价视角(行视图 vs 列视图),最后落到 \(QK^\top\) 的形状分析。

如果你想直接看到注意力公式怎么把这些拼起来,可以先扫一眼第 13 缩放点积注意力,再回到第 03 篇补「批量化」的工程视角。

如果你对「点积之外有没有别的注意力」感兴趣,可以提前看第 16 注意力的其他形式,那里讨论 additive attention、bilinear attention、cross-attention 等。


参考文献

经典教材

论文

博客与可视化资源


上一篇01 序章:你将获得什么  下一篇03 矩阵乘法的两种视角


如果你看完本篇仍然觉得「我没有完全把点积和 attention 联起来」,不要焦虑。下一篇会给你看「批量点积长什么样」「为什么矩阵乘法对 GPU 是天作之合」。等你读到第 13 篇的时候,所有这些零散的砖块会被拼成一个完整的注意力公式。

那是值得期待的时刻。

读完本篇请休息几分钟,再开始下一篇。把脑子里的画面沉淀一下——两支箭、夹角余弦、批量计算、\(QK^\top\)。这些画面会陪伴你走完整个系列。

我们下一篇见。

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-15 · transformer

【Transformer 与注意力机制】01|为什么要从这里开始

这是【Transformer 与注意力机制】系列的第一篇,承担两件事:一是把这套五十多篇文章为谁写、解决什么问题、彼此之间是什么关系交代清楚;二是为完全没基础的读者画出一条从向量、点积、矩阵乘法走到自注意力、再走到大语言模型的爬升路径,让你在投入时间之前先知道终点在哪、路上要经过哪些坎、读完之后你会、还不会做什么事。


By .