证明单位向量下三种度量等价,详细推导各度量适用场景与量级差异,指明阈值不可跨度量迁移。
当向量是单位长度时,Euclidean distance、cosine similarity 和 dot product 产生的排序结果完全相同,证明只有两行代数。当它们不是单位长度时,三者会产生分歧,而索引使用哪一个则成为一个有后果的决策。
取 a = [1, 2, 3] 和 b = [4, 6, 3]。差值 a - b = [-3, -4, 0]。
Euclidean (L2) sqrt(3^2 + 4^2 + 0^2) = sqrt(25) = 5.0000
Manhattan (L1) |3| + |4| + |0| = 7.0000
Chebyshev (Linf) max(3, 4, 0) = 4.0000
Cosine:
a . b = 1*4 + 2*6 + 3*3 = 4 + 12 + 9 = 25
||a|| = sqrt(1 + 4 + 9) = sqrt(14) = 3.741657
||b|| = sqrt(16 + 36 + 9)= sqrt(61) = 7.810250
cos = 25 / (3.741657 * 7.810250) = 25 / 29.223278
= 0.855477
cosine distance = 1 - 0.855477 = 0.144523
同一对向量得到四个不同的数值,而且它们不在同一量级上。Manhattan 永远不小于 Euclidean;Chebyshev 永远不大于 Euclidean。Cosine distance 被限制在 [0, 2] 范围内,而其他的是无界的。为其中一个设定的阈值对另一个毫无意义。
展开 squared Euclidean distance。完整推导如下:
||a - b||^2 = (a - b) . (a - b)
= a.a - 2(a.b) + b.b
= ||a||^2 - 2(a.b) + ||b||^2
If ||a|| = ||b|| = 1:
||a - b||^2 = 1 - 2(a.b) + 1
= 2 - 2(a.b)
= 2 - 2*cos(a, b)
Squared Euclidean distance 是 cosine similarity 的严格递减函数。不是近似、不是通常如此——对所有单位向量而言,精确成立。因此按升序排列 Euclidean distance 和按降序排列 cosine similarity 产生的顺序完全相同,而由于单位向量的 dot product 就是 cosine,所以实际上是三种方法一致。
cos = 1.00 -> ||a-b||^2 = 0.00 -> ||a-b|| = 0.0000
cos = 0.90 -> ||a-b||^2 = 0.20 -> ||a-b|| = 0.4472
cos = 0.50 -> ||a-b||^2 = 1.00 -> ||a-b|| = 1.0000
cos = 0.00 -> ||a-b||^2 = 2.00 -> ||a-b|| = 1.4142
cos =-1.00 -> ||a-b||^2 = 4.00 -> ||a-b|| = 2.0000
这有实际的收益。如果你的向量数据库只提供 Euclidean 或只提供 inner product,你仍然可以通过在插入前和查询前对向量做归一化来获得 cosine 的行为。索引本身不需要知道这件事。
现在是失败案例,这种情况很常见但很隐蔽。查询 q = [1, 0],三个存储的向量:
d1 = [0.60, 0.80] norm 1.00 direction 53.1 degrees off q
d2 = [0.99, 0.14] norm 1.00 direction 8.0 degrees off q
d3 = [2.00, 1.50] norm 2.50 direction 36.9 degrees off q
Inner product against q:
d1: 1*0.60 + 0*0.80 = 0.60
d2: 1*0.99 + 0*0.14 = 0.99
d3: 1*2.00 + 0*1.50 = 2.00 <- ranks first
Cosine against q:
d1: 0.60 / 1.00 = 0.60
d2: 0.99 / 1.00 = 0.99 <- ranks first
d3: 2.00 / 2.50 = 0.80
同一数据产生两个不同的最优结果。d3 在 inner product 上获胜仅仅因为它长了 2.5 倍,尽管它指向的方向偏离查询 37 度,而 d2 只偏离 8 度。将 d3 归一化为 [0.8, 0.6],它的 inner product 就变成 0.80,排名与 cosine 一致,这个 bug 用一行代码就消失了。
这之所以难以发现的原因是:结果并非明显损坏。它们是看似合理的、相关的文档——只是系统性地偏向更长的那些。Recall 下降几个百分点但不会报错。检查方法是测量一百个存储向量的 norm;如果它们不全是 1.0,而且你用的是 inner product,那就是这个问题。
"Distance" 有一个技术定义,但人们称为 distance 的许多东西并不满足它。一个函数 d 是 metric,当且仅当它满足四个条件:
非负性。d(a, b) >= 0。
同一性。d(a, b) = 0 当且仅当 a = b。
对称性。d(a, b) = d(b, a)。
三角不等式。d(a, c) <= d(a, b) + d(b, c)。绕道第三点永远不会更近。
Euclidean、Manhattan 和 Hamming 满足全部四条。Cosine distance,定义为 1 - cos,不满足最后一条,而且很容易证明:
a = [1, 0]
b = [0.7071, 0.7071]
c = [0, 1]
cos(a,b) = 0.7071 -> d(a,b) = 0.2929
cos(b,c) = 0.7071 -> d(b,c) = 0.2929
cos(a,c) = 0.0000 -> d(a,c) = 1.0000
三角不等式要求:
1.0000 <= 0.2929 + 0.2929 = 0.5858
不成立。Cosine distance 不是 metric。
Angular distance 解决了这个问题。使用角度本身(归一化后),而不是余弦减一:
d_ang(a, b) = arccos(cos_sim(a, b)) / pi
a 到 b: 45 度 -> 0.25
b 到 c: 45 度 -> 0.25
a 到 c: 90 度 -> 0.50
0.50 <= 0.25 + 0.25 成立,等号成立。
Squared Euclidean 同样失败,原因相同:
a=[0,0] b=[1,0] c=[2,0]
d(a,c)^2 = 4, d(a,b)^2 + d(b,c)^2 = 1 + 1 = 2
4 <= 2 为假。如果需要 metric,始终取平方根;
排序不受影响。
这是否重要取决于索引类型。基于图的近似索引如 HNSW,以及聚类索引如 IVF,只用该度量来排序,对 cosine 也能良好运行——它们本身就是近似的,排序是它们所需的全部。精确的 metric-tree 结构,包括 ball trees、VP-trees 和 cover trees,使用三角不等式来剪枝整个分支;给它们一个非 metric,它们返回错误结果而非变慢。
安全做法来自前一节:对向量做归一化然后使用 Euclidean distance。它是一个真正的 metric,排序结果与 cosine 完全相同,而且下游无需知道 cosine 才是你真正想要的。
Hamming distance 统计两个等长序列中不相同的位置。它适用于比特、字符或类别,不适用于连续值。
a = 1 0 1 1
b = 1 1 0 1
- x x - distance 2
XOR 后 popcount,这就是它快的原因:
1011 XOR 1101 = 0110
popcount(0110) = 2
关注它的原因:嵌入的二进制量化。取一个 1,024 维的 float 向量,只保留每个分量的符号。向量变成 1,024 bits——128 bytes 而非 4,096 bytes——比较变成 XOR 加 popcount,现代 CPU 以极高速率完成这些操作。
存储: fp32 1024 * 4 = 4096 bytes
int8 1024 * 1 = 1024 bytes
binary 1024 / 8 = 128 bytes 小 32 倍
与 cosine 的关系,对于近似随机的向量:
hamming_fraction ~= theta / pi
cos = 1.00 -> theta = 0 -> 0% 的位不同
cos = 0.70 -> theta = 0.795 -> 25% 的位不同
cos = 0.00 -> theta = 1.571 -> 50% 的位不同
二进制向量丢失了真实精度,所以标准模式是两阶段搜索:用 Hamming 在二进制向量上检索几百个候选,然后用完整精度重新评分。昂贵的度量只在 0.01% 的语料上运行。
向量的 magnitude 是否有意义?对于文本嵌入,几乎从来没有——所以用 cosine,或者归一化后用任何度量都可以。对于 learned recommender 向量,magnitude 编码了流行度,dot product 是模型训练时用的度量,换成 cosine 会悄然丢失一个信号。
模型是用什么训练的?这覆盖其他一切考虑。用 cosine objective 训练的嵌入模型没有理由在其 norm 中放置含义。使用模型卡片指定的度量;如果它没有指定,cosine 是文本的安全默认。
向量是离散的吗?那就用 Hamming,Euclidean 与 cosine 的问题不会产生。
你检查过 norm 了吗?一行代码:np.linalg.norm(V, axis=1)。如果它们全是 1.0,L2、cosine 和 inner product 之间的选择就是纯 cosmetic 的,你可以用索引运行最快的那一个。
Manhattan、Chebyshev 和其余 Minkowski 家族成员出现在经典机器学习中,在嵌入检索中很少见。如果你在为神经嵌入的最近邻索引在它们之间做选择,召回问题几乎肯定不在这里——检索策略比度量选择重要得多。