Transformer 实现机制 · 第 5 课 · 补充课

余弦相似度 —— 把"像不像"变成一个数

⏱ 约 20 分钟 · 前置:第 4 课(词嵌入) · 本课是数学补充,不在 Vizuara 主干里,但第 7 课(注意力打分与缩放)离不开它 · 本课目标:手算余弦;说清"点积的缺陷"与"余弦为什么公平";玩会拖拽实验室

第 4 课我们用余弦相似度把"词义像不像"变成了一个数,还见证了 king−man+woman≈queen。但当时公式只是脚注。这课把它拆开——它总共涉及两个运算(点积、长度),10 分钟手算就能吃透,回报却极大:注意力机制的打分公式就是它的近亲。

一、点积:最自然的相似度,和它的缺陷

两个向量有多"像"?最直接的想法是点积:对应位置相乘,再全部加起来:

手算:点积 [3, 4] · [1, 2] = 3×1 + 4×2 = 11
同方向越对齐、数值越大 → 点积越大越"像"。看起来完美?

缺陷藏在下面这组对比里。[6, 8] 和 [3, 4] 方向完全相同(它就是 [3,4] 的两倍长),但:

向量对点积直觉上应该
[3,4] 和 [6,8](同方向,B 长 2 倍)3×6 + 4×8 = 50"最像"——数值也最大 ✓
[3,4] 和 [3,4](完全相同)3×3 + 4×4 = 25"最像"——但只有 50 的一半 ✗
[3,4] 和 [-4,3](恰好垂直)3×(−4) + 4×3 = 0"无关"——0 ✓
点积的缺陷:长度污染 [6,8] 和 [3,4] 明明是同一个意思(同方向),点积却是自身与自身比较的 2 倍——分数被向量长度绑架了。放在词嵌入里:向量长度往往反映词频高低这类"音量"因素,而方向才编码语义。用点积排名,长向量会霸榜。

二、余弦:先除掉长度,只比方向

修法顺理成章:把点积除以两个向量各自的长度,把"音量"归一掉:

A = [3,4] |A| = 5 B = [6,8] |B| = 10(同方向,长 2 倍) θ = 0° cos θ = 50 / (5 × 10) = 1 → 方向完全一致 长度不同(5 vs 10),方向相同 → 余弦 = 1。余弦只看夹角,不看长度。
余弦 = 点积除以两个长度。A 与 B 同方向,余弦 = 1;垂直 = 0;反向 = −1。
公式(唯一需要记的)+ 三个锚点 $$\cos\theta = \frac{A \cdot B}{\lVert A \rVert \times \lVert B \rVert}$$ 其中 $\lVert A \rVert$ = 向量长度(各分量平方和开根号)。
手算:$\lVert[3,4]\rVert = \sqrt{3^2+4^2} = 5$,$\lVert[6,8]\rVert = 10$,于是 $\cos = 50/(5\times10) = $ 1(同向)。
三个锚点:1 = 同方向(最像);0 = 垂直(无关);−1 = 反方向(相反)。其余角度落在中间,越锐越像。

三、玩会它:拖拽实验室

下面的实验室里,拖动 A、B 两个箭头的端点,读数实时变化。先点「垂直(无关)」看余弦归零,再拖 B 转到和 A 同方向看它爬向 1——注意拖长向量时余弦几乎不变,这就是"只看方向":

四、回到词嵌入:为什么必须用余弦

现在能准确回答第 4 课留下的问题了。词向量由训练产生,长度没有语义含义(高频词的向量往往偏长),方向才有。用点积排名,"长向量"会霸榜;用余弦排名,才轮到真正语义相近的词。看一组故意把 king 造成超长向量的演示:

# ============================================================ # 【演示目标】同一个"和 cat 最像"的问题:点积榜 vs 余弦榜,答案完全不同 # ——证明点积会被长度污染,余弦只认方向 # 【思路】给 4 个词造向量,故意把 king 的向量做成超长(示意高频词的长度偏差), # 但它的方向和 cat 毫不相似。然后分别用点积、余弦给"谁最像 cat"排名: # 点积榜:king 靠长度霸榜(错!) 余弦榜:dog 第一(对!) # ============================================================ import numpy as np # ---- 向量:cat/dog/fish 方向相近(都是动物);king 故意超长但方向无关 ---- emb = { "cat": [0.30, 0.95, 0.10], "dog": [0.25, 0.85, 0.15], "fish": [0.15, 0.80, 0.20], "king": [2.80, 0.30, 0.10], # 长度 ≈ 2.83,是 cat 的近 3 倍(长度偏差的示意) } def dot(a, b): # 点积:对应位相乘求和 return float(np.dot(a, b)) def cos(a, b): # 余弦:点积除以两个长度——只比方向 a, b = np.array(a), np.array(b) return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) # ---- 两个榜单:"谁最像 cat?" ---- dot_rank = sorted(((dot(emb["cat"], emb[w]), w) for w in emb if w != "cat"), reverse=True) cos_rank = sorted(((round(cos(emb["cat"], emb[w]), 3), w) for w in emb if w != "cat"), reverse=True) print("点积榜(被长度污染):") for s, w in dot_rank: print(" %-5s %.2f" % (w, s)) print("余弦榜(只看方向):") for s, w in cos_rank: print(" %-5s %.3f" % (w, s)) # 读法:点积榜第一名是 king(错!它和 cat 毫不相似,只是长); # 余弦榜第一名是 dog(对!)。这就是嵌入相似度都用余弦的原因。

五、往后的路:它和注意力机制是什么关系

记住一句话就够:注意力机制的打分,就是查询向量和键向量的点积(第 7 课正式拆)。而点积有本课讲的长度污染 + 维度变大后数值膨胀两个问题,所以原论文在点积后除以 √d(键维度)——那是"缩放"版的点积;如果把向量先归一化成单位长度再点积,得到的就是余弦。三兄弟是一家人:点积 → 缩放点积 → 余弦。第 7 课你会再见到它们。

检索练习

本课主读材料

去复习(约 10 分钟)

回看第 4 课第三节的语义地图:现在你完全拥有读懂那张图的两个工具——共现(语境)+ 余弦(方向比较)。试着在它的代码块里「单步」到 SIM 计算那一行,「求值」SIM[0,1]、SIM[idx['cat']] 看看。

视频(可选,睡前看):3Blue1Brown 深度学习第 5 章前 20 分钟——注意他多次强调"嵌入的方向/夹角承载语义",就是本课的内容。

我是你的老师,别客气 卡在哪一步(比如"|A| 为什么要开根号"或者 lab 拖不动),原样贴回来问我。下一课回到主干:位置编码。