距离度量不是玄学:余弦相似度与内积在归一化后的本质
在向量数据库建表(Create Index)的时候,控制台或 API 通常会弹出一个必选参数:Distance Metric(距离度量方式)。下拉菜单里排着三张老面孔:
L2(欧几里得距离,Euclidean Distance)COSINE(余弦相似度,Cosine Similarity)IP(内积 / 点积,Inner Product)
很多刚入行做 RAG 的同学在选这个参数时,往往带着几分看风水的玄学心态:“听说做语义搜索一定要选 COSINE,做推荐系统才选 IP,算图像相似度要选 L2。”
直到某一天,他看到资深系统架构师在所有生产向量库里一律配置了metric_type: "IP",但召回出来的文档相似度排序却和COSINE分毫不差,而且检索速度还快了 20%!
为什么会有这种看似违背直觉的现象?余弦、内积与欧氏距离之间,到底藏着怎样的几何与代数真相?
三种距离的几何直觉
让我们把高维空间(如 768 维)想象成初中几何平面上的两个从原点出发的箭头(向量 $\vec{A}$ 和 $\vec{B}$):
- 欧氏距离(L2)—— 尺子量直线:
$$D_{L2}(\vec{A}, \vec{B}) = \sqrt{\sum_{i=1}^n (A_i - B_i)^2}$$
它关心的是箭头两个尖端之间的物理直线距离。如果向量 $\vec{A}$ 的方向和 $\vec{B}$ 完全一致,但 $\vec{A}$ 的长度是 100,$\vec{B}$ 的长度是 1,它们的 L2 距离依然会非常大。 - 余弦相似度(Cosine)—— 量角器测夹角:
$$S_{cos}(\vec{A}, \vec{B}) = \frac{\vec{A} \cdot \vec{B}}{|\vec{A}| |\vec{B}|} = \frac{\sum A_i B_i}{\sqrt{\sum A_i^2} \sqrt{\sum B_i^2}}$$
它用量角器去量两个箭头之间的夹角 $\theta$。它完全无视箭头的物理长度,只关心它们的指向是否一致。夹角为 0 时 $\cos(0) = 1$(完全相同);夹角为 90 度时 $\cos(90^\circ) = 0$(正交无关);夹角为 180 度时 $\cos(180^\circ) = -1$(完全相反)。 - 内积(Inner Product / IP)—— 投影与乘积:
$$S_{IP}(\vec{A}, \vec{B}) = \vec{A} \cdot \vec{B} = \sum_{i=1}^n A_i B_i = |\vec{A}| |\vec{B}| \cos\theta$$
它是向量 $\vec{A}$ 在向量 $\vec{B}$ 方向上的投影长度乘以 $\vec{B}$ 自身的长度。它既受夹角影响,又受向量模长(Magnitude)的严重支配。
关键魔法:L2 单位归一化(Unit Normalization)
现在,让我们施加一个极其普遍的数学变换——向量 L2 归一化(把每个向量除以它自身的模长,使其长度严格等于 1):
$$\vec{u} = \frac{\vec{A}}{|\vec{A}|}, \quad |\vec{u}| = 1$$
当所有的向量在存入数据库之前都被拉伸或压缩到了**单位超球面的表面(Unit Hypersphere)**时,奇迹发生了:
1. 余弦相似度退化为内积
代入余弦公式:
$$S_{cos}(\vec{u}, \vec{v}) = \frac{\vec{u} \cdot \vec{v}}{|\vec{u}| |\vec{v}|} = \frac{\vec{u} \cdot \vec{v}}{1 \times 1} = \vec{u} \cdot \vec{v} = S_{IP}(\vec{u}, \vec{v})$$
在归一化后,余弦相似度在数值上与内积完全等价!
2. 欧氏距离与内积的单调对偶关系
展开欧氏距离的平方:
$$D_{L2}^2(\vec{u}, \vec{v}) = |\vec{u} - \vec{v}|^2 = |\vec{u}|^2 + |\vec{v}|^2 - 2(\vec{u} \cdot \vec{v}) = 1 + 1 - 2(\vec{u} \cdot \vec{v}) = 2 - 2 S_{IP}(\vec{u}, \vec{v})$$
这意味着:内积越大,欧氏距离越小,两者的排名顺序严格保序单调!
为什么生产环境必须“先归一化,再选 IP”?
既然数学上完全等价,为什么资深工程师一定要在写入时做归一化,并在向量库选IP,而不是直接选COSINE?
答案全在底层 CPU 计算效率与硬件指令集:
- 计算
COSINE的代价:
向量库在比较两个 1536 维向量时,必须计算:$1536$ 次乘加运算(算点积) + $1536$ 次平方加运算(算模长 A) + $1536$ 次平方加运算(算模长 B) + $2$ 次开平方根 + $1$ 次浮点数除法。
在海量检索时,频繁的开方和除法运算是 CPU 流水线的杀手。 - 计算归一化后
IP的代价:
因为写入时模长已经是 1,查询时只需对 Query 归一化一次,底层比对完全退化为纯粹的向量点积($\sum A_i B_i$)。
现代 CPU 的 AVX-512 / ARM Neon 指令集提供了一条极速硬件指令:FMA(Fused Multiply-Add),可以在单个时钟周期内并发完成多个浮点数的乘加融合运算!
实测表明,在单核 CPU 上计算 1000 万次向量距离,归一化后的 IP 计算耗时比原生 COSINE 快 2.4 倍,CPU 功耗下降 35%!
生产落地三步法
import numpy as np def normalize_vector(vec: np.ndarray) -> np.ndarray: """写入或查询前,显式进行 L2 归一化""" norm = np.linalg.norm(vec) if norm == 0: return vec return (vec / norm).astype(np.float32) # 1. 业务写入向量时:做 normalize_vector(doc_vec) # 2. 向量库建索引时:一律配置 metric_type="IP" # 3. 线上检索 Query 时:做 normalize_vector(query_vec)搞清楚这个代数本质,你就能彻底撕掉“距离度量玄学”的标签:在应用层做一次 L2 归一化,在存储层坚定选择 IP(点积)索引,这就是兼顾最高检索精度与极限硬件算力榨取的最佳工程法则。