「点积是注意力的灵魂。」——这句话在 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 完全一致。
两个定义等价,这件事可以用余弦定理证明(任何线性代数教材都有),但本篇不展开。重要的是接受这个等价,把它当成一个事实,然后基于它思考点积的「意义」。
五、为什么这个等价如此重要
代数定义告诉我们「怎么算」,几何定义告诉我们「算出来代表什么」。两者合起来才是完整的点积。
从几何定义看,点积大小由三件事决定:
- \(\mathbf{a}\) 的长度。
- \(\mathbf{b}\) 的长度。
- 它们的夹角余弦。
如果两支箭都很长,且方向接近同向,点积就大。如果有一支很短,或者两者夹角接近 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」。一个常见模式是:
- 对角线明显:token 主要关注自己(identity attention)。
- 下三角填充:因果掩码导致只能看到自己和前面的 token。
- 垂直线条:某个 token 被很多其他 token 关注(比如句子开头的特殊 token、句号、CLS 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}\),称为「线性核」。
更复杂的核包括:
- 多项式核 \(K(\mathbf{x}, \mathbf{y}) = (\mathbf{x} \cdot \mathbf{y} + c)^d\)。
- RBF 核(高斯核) \(K(\mathbf{x}, \mathbf{y}) = \exp(-\|\mathbf{x} - \mathbf{y}\|^2 / 2\sigma^2)\)。
- Sigmoid 核 \(K(\mathbf{x}, \mathbf{y}) = \tanh(\alpha \mathbf{x} \cdot \mathbf{y} + c)\)。
这些核都可以替代点积来衡量相似度。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。理论上比点积表达力更强,因为多了一层非线性。
但点积的优势:
- 没有额外参数(除了 W_Q, W_K 已经在的)。
- 批量计算只需一次矩阵乘法,加性需要先加再过 tanh,无法用单次 GEMM。
- 效果实测和加性接近(Luong 2015 论文的实验)。
所以「点积」赢的不是「表达力」,而是「在效果接近的前提下,速度快得多」。这种 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 个是「工程性」的,做完能让你对真实模型实现有感性认识。
三十四、本篇与后面的关系
本篇我们建立了「点积」这一个基本砖块。后面:
- 03 篇:把「单次点积」扩展为「批量点积 = 矩阵乘法」,并展示矩阵乘法的两种视角。
- 04 篇:把矩阵乘法纳入「神经网络 = 函数复合」的框架,理解 W_Q, W_K, W_V 这些线性变换在做什么。
- 05 篇:激活函数。回答「为什么神经网络只用线性是不够的」。
- 08 篇:softmax。把「点积输出」转换为「概率分布」的桥梁。
- 13 篇:缩放点积注意力。把本篇所有概念组合成完整的注意力公式。
- 17 篇:RoPE 旋转位置编码。利用「点积在正交变换下不变」这个性质,把位置信息编码成相对旋转。
如果你读到这里仍然觉得「点积只是一个数值运算」,没关系,继续往下读。等你看到 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 里,至少有以下几种等价写法:
torch.matmul(a, b.transpose(-2, -1)):标准矩阵乘法。torch.einsum('...id,...jd->...ij', a, b):用 einsum 显式表达。(a.unsqueeze(-2) * b.unsqueeze(-3)).sum(-1):广播 + 求和,可读性差但能写。
三者数值上相等,但性能差异巨大。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.dot。np.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 和
contiguous。K.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.T、softmax、attn @ 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 等。
参考文献
经典教材
- Gilbert Strang. Introduction to Linear Algebra (5th ed.). Wellesley-Cambridge Press, 2016. Strang 的线代教材是公认最好的入门书之一,第 1-3 章涵盖了向量、点积、几何。配套 MIT OCW 视频(18.06)建议看一遍。
- Sheldon Axler. Linear Algebra Done Right (3rd ed.). Springer, 2015. 这本书风格不同,先讲抽象向量空间再讲坐标,对希望深入数学的读者更合适。
- Stephen Boyd, Lieven Vandenberghe. Introduction to Applied Linear Algebra: Vectors, Matrices, and Least Squares. Cambridge University Press, 2018. 偏工程导向,公开 PDF 在 https://stanford.edu/~boyd/vmls/。
论文
- Ashish Vaswani et al. “Attention Is All You Need.” NeurIPS, 2017. 第 3 节缩放点积注意力的公式就是把本篇的点积扩展到矩阵规模。
- Dzmitry Bahdanau et al. “Neural Machine Translation by Jointly Learning to Align and Translate.” ICLR, 2015. 第一次在 seq2seq 里使用 attention,使用的是「加性注意力」而非点积。论文里讨论了为何选择加性形式,是理解「点积 vs 加性」对比的好材料。
- Minh-Thang Luong, Hieu Pham, Christopher D. Manning. “Effective Approaches to Attention-based Neural Machine Translation.” EMNLP, 2015. 这篇 paper 系统对比了多种 attention 评分函数(点积、加性、双线性),是「为什么是点积」问题的实证基础之一。
博客与可视化资源
- 3Blue1Brown 的「Essence of Linear Algebra」系列。https://www.3blue1brown.com/topics/linear-algebra。 把向量、点积、矩阵的几何直觉讲得最好的视频系列。强烈推荐第 9 集「Dot products and duality」。
- 苏剑林。「再谈最小熵原理:飞行棋之上的注意力」。https://kexue.fm。 用大量例子讲注意力评分函数的选择。
- Jay Alammar. “The Illustrated Transformer.” http://jalammar.github.io/illustrated-transformer/。 经典的 Transformer 可视化博客,对点积部分有清晰的图示。
- Lilian Weng. “Attention? Attention!” https://lilianweng.github.io/posts/2018-06-24-attention/。 attention 历史与变种的综述,对评分函数家族有完整梳理。
上一篇:01 序章:你将获得什么 下一篇:03 矩阵乘法的两种视角
如果你看完本篇仍然觉得「我没有完全把点积和 attention 联起来」,不要焦虑。下一篇会给你看「批量点积长什么样」「为什么矩阵乘法对 GPU 是天作之合」。等你读到第 13 篇的时候,所有这些零散的砖块会被拼成一个完整的注意力公式。
那是值得期待的时刻。
读完本篇请休息几分钟,再开始下一篇。把脑子里的画面沉淀一下——两支箭、夹角余弦、批量计算、\(QK^\top\)。这些画面会陪伴你走完整个系列。
我们下一篇见。
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【Transformer 与注意力机制】04. 函数与神经网络:从 y=f(x) 到一台可学习的拟合机器
如果你问我「神经网络到底是什么」,我会先把所有教材合上,然后给你一句朴素得近乎敷衍的话——神经网络就是一个函数。
【Transformer 与注意力机制】05. 激活函数:让网络「弯下来」的非线性魔法
上一篇我们论证了一件事——纯线性的网络再深,也只是一个线性变换。把 $W2(W1\mathbf{x} + \mathbf{b}1) + \mathbf{b}2$ 展开就是 $W'\mathbf{x} + \mathbf{b}'$。线性的复合还是线性,这是线性代数的铁律。
【Transformer 与注意力机制】03 矩阵乘法的两种视角
把矩阵乘法掰开成两种等价但风格不同的视角——『行 × 列』的点积视角和『列的线性组合』视角,最终落到 QK^T 的形状分析。
【Transformer 与注意力机制】01|为什么要从这里开始
这是【Transformer 与注意力机制】系列的第一篇,承担两件事:一是把这套五十多篇文章为谁写、解决什么问题、彼此之间是什么关系交代清楚;二是为完全没基础的读者画出一条从向量、点积、矩阵乘法走到自注意力、再走到大语言模型的爬升路径,让你在投入时间之前先知道终点在哪、路上要经过哪些坎、读完之后你会、还不会做什么事。