向量检索:余弦相似度与内积

黎 浩然/ 9 10 月, 2026/ 大语言模型/LARGELANGUAGEMODEL/LLM, 机器学习/MACHINELEARNING, 研究生/POSTGRADUATE, 计算机/COMPUTER/ 0 comments

同一批向量,改一下检索指标,第一名就可能变了。原因未必是索引坏了:内积同时受方向和长度影响,余弦相似度把长度除掉。只有明确归一化条件,才能说它们等价。

文章目录
  1. 一个二维例子就能看出区别
  2. 归一化后怎样联系起来
  3. L2距离的等价也有前提
  4. 不用模型也能复算
  5. 改指标之前先查三件事
  6. 参考资料

这是RAG 混合检索之前的一项基础配置检查。English version

一个二维例子就能看出区别

设查询 q=(1,0),候选 A=(1,0),B=(80,60)。它们是人为构造的教学向量,没有对应真实文本。A与查询完全同向;B的长度为100,方向则偏离查询。

内积 q·A=1,q·B=80,所以按内积降序时B在前。余弦为 cos(q,d)=(q·d)/(‖q‖‖d‖):A得到1,B得到0.8,顺序变成A在前。这个例子只说明两个目标不同,不能证明哪个更适合实际语义检索。

原始内积:B排在前面
A: 1
B: 80
余弦相似度:A排在前面
A: 1
B: 0.8
原创分数图,使用人工构造向量。两栏尺度不同,只在各栏内部比较条形长度。

归一化后怎样联系起来

对非零向量,定义 x̂=x/‖x‖。把两个向量都变成单位长度以后,x̂·ŷ就是原向量的余弦相似度。Faiss官方文档给出的余弦检索方法,是归一化入库向量和查询向量,再做最大内积搜索。

这里还有一个容易忽略的细节:针对固定的非零查询,仅改变查询长度不会改变原始内积排序,因为所有分数乘了同一个正数。但它会改变分数值和阈值含义。候选长度各不相同,不能用同样理由忽略它们;如果要让返回数值也是余弦,就应两端一致归一化。

L2距离的等价也有前提

展开平方距离可得 ‖x−y‖²=‖x‖²+‖y‖²−2x·y。只有两端都是单位向量时,才化成 2−2x·y。因此,在单位向量上最大内积与最小平方L2的精确排序一致;对未归一化向量,不能直接套用这个结论。

量 选择方向 单位向量关系
内积 越大越近 等于余弦
余弦相似度 越大越近 范围−1到1
平方L2距离 越小越近 2−2×内积

Faiss的L2返回平方欧氏距离。排序时开不开平方不改变先后,但把返回值用于距离阈值或展示时,要确认自己用的是哪一个量。

不用模型也能复算

以下标准库Python示例已经执行,验证了排序变化、单位向量恒等式和零向量处理。没有调用嵌入模型,也没有运行Faiss索引,不能把它称为真实召回测试。

from math import sqrt, isclose

def dot(a, b):
    if len(a) != len(b):
        raise ValueError('dimension mismatch')
    return sum(x*y for x,y in zip(a,b))

def unit(v):
    norm = sqrt(dot(v,v))
    if norm == 0:
        raise ValueError('zero vector has no cosine direction')
    return [x/norm for x in v]

q = [1., 0.]
docs = {'A': [1., 0.], 'B': [80., 60.]}
raw = {k: dot(q,v) for k,v in docs.items()}
cos = {k: dot(unit(q),unit(v)) for k,v in docs.items()}
assert raw == {'A': 1., 'B': 80.}
assert isclose(cos['A'], 1.) and isclose(cos['B'], .8)
for v in docs.values():
    x,y = unit(q),unit(v)
    squared_l2 = sum((a-b)**2 for a,b in zip(x,y))
    assert isclose(squared_l2, 2-2*dot(x,y), abs_tol=1e-12)
try:
    unit([0.,0.])
except ValueError:
    pass
else:
    raise AssertionError('zero vector must be rejected')
print('inner product:', raw)
print('cosine:', cos)
print('normalized squared L2:', {k:2-2*s for k,s in cos.items()})

内积输出A=1、B=80;余弦输出A=1、B=0.8;单位向量平方L2约为0与0.4。末尾的浮点数0.3999999999999999是表示误差,因此恒等式用容差比较。零向量没有可定义的余弦方向,代码显式拒绝;真实系统也应确定异常策略。

改指标之前先查三件事

第一,查嵌入模型要求什么打分方式,输出是否已经归一化。向量长度可能包含模型设计中的信号,归一化并不是对所有模型都合适的无损处理。

第二,查入库和查询是否使用相同版本、维度和预处理。只把新查询归一化,却保留旧的非单位文档向量,并不会得到一般的余弦分数。需要变更时,应同步考虑已有索引与线上阈值。

第三,用精确计算建立一小份基准,再比较实际近似索引。数学上的排序等价,不保证不同近似搜索配置、量化误差与并列值处理返回完全相同的列表。把分数含义和搜索误差分开检查,才能判断是配置问题还是近似召回损失。

参考资料

Faiss: MetricType and distances(2024-12-03)

补发说明:本文于北京时间2026年10月9日补发,文章日期保留原计划09:00。示例为原创数学验证,不包含模型性能结论。

支持

如果这篇文章对你有帮助,欢迎支持本站。

微信支持二维码,点击查看大图
微信
支付宝支持二维码,点击查看大图
支付宝
Buy Me a Coffee,点击支持本站
Buy Me a Coffee

二维码可点击放大。更多支持方式见支持页面。

Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*