余弦相似度为什么常用于 Embedding 检索
做 Embedding 检索时,经常会遇到“余弦相似度”这个词。
把问题转换成向量,再和知识库里的向量算一下余弦相似度,挑出分数最高的几条,检索就完成了。
流程看着不复杂,但这里有个问题:比较两个向量,直接算它们之间的距离不就行了吗?为什么偏偏要算夹角?
要说清楚这件事,先得看看,我们到底想比较向量的什么。
一、都是向量,“接近”却有不同的含义
假设用一个特别简化的二维向量表示文本:
A = [1, 2]
B = [2, 4]
C = [2, 1]
暂时不用管每个数字代表什么,只把它们当成从原点出发的箭头。
B 的两个数都是 A 的两倍,所以它们指向同一个方向,只是 B 更长。
C 的长度和 A 相同,但方向不同。
这时候,如果问“谁和 A 更像”,答案取决于你怎么定义“像”。
按坐标之间的直线距离算:
- A 到 B 的距离约为 2.24。
- A 到 C 的距离约为 1.41。
所以 C 更近。
但如果只看方向,显然是 B 更接近 A,因为它们根本就指向同一个方向。
欧氏距离关心两个点相隔多远,余弦相似度关心两个向量指向是否一致。
这两种比较都有道理,只是关注的东西不同。
二、为什么文本检索经常更关心方向
Embedding 模型会把文本映射成一串数字,也就是一个高维向量。训练得合适的模型,会让语义相关的文本在向量空间里呈现出某种接近关系。
比如:
怎么修改服务器的 SSH 端口?
和:
Linux 如何更改远程登录端口?
用词不完全一样,但讨论的事情接近。我们希望它们的向量也足够相似,检索时能互相找到。
问题在于,向量除了方向,还有长度,也叫模长。
不同模型中,模长的含义可能不同:它可能携带有用信息,也可能受到训练方式等因素影响。不能简单地认为“文本越长,向量就越长”,更不能默认模长越大,文本就越相关。
当我们希望比较语义关系,同时减少模长差异的影响时,余弦相似度就很方便。
它会把长度因素除掉,主要比较方向。
不过,这里有个前提:模型得把我们需要的语义关系学进这种几何关系里。 余弦相似度只是计算工具,不会自己理解句子。
拿一个不适合文本检索的模型生成向量,再认真计算夹角,也未必能检索出好结果。
三、余弦相似度到底怎么算
两个非零向量的余弦相似度,公式是:
[
\operatorname{sim}(A,B)=\frac{A\cdot B}{|A||B|}
]
公式看着有点抽象,拆开其实就是:
先把对应位置的数字相乘后求和,再除以两个向量的长度之积。
其中,分子里的 (A\cdot B) 叫点积。
还是用前面的例子:
A = [1, 2]
B = [2, 4]
它们的点积是:
[
1\times2+2\times4=10
]
两个向量的长度分别为:
[
|A|=\sqrt{1^2+2^2}=\sqrt{5}
]
[
|B|=\sqrt{2^2+4^2}=\sqrt{20}
]
所以:
[
\operatorname{sim}(A,B)
=\frac{10}{\sqrt{5}\times\sqrt{20}}
=1
]
只要两个非零向量方向相同,即使长度不同,余弦相似度也是 1。
再看 A 和 C:
A = [1, 2]
C = [2, 1]
它们的余弦相似度是:
[
\operatorname{sim}(A,C)
=\frac{1\times2+2\times1}{\sqrt{5}\times\sqrt{5}}
=0.8
]
这样,B 就比 C 更像 A。
余弦相似度的理论范围是 ([-1,1]):同向时为 1,垂直时为 0,反向时为 -1。但映射到文本之后,不能直接把负数理解成“语义相反”,也不能把 0 理解成绝对无关。具体分数的含义,还要看模型的训练和实际数据。
四、为什么不直接用点积
点积也能比较向量,而且计算更简单。
还是上面的数据:
A = [1, 2]
B = [2, 4]
点积为 10。
如果把 B 放大十倍:
B₂ = [20, 40]
点积就变成了 100。
方向完全没变,分数却涨了十倍。这说明点积同时受到方向和模长的影响。
可以把它的关系写成:
[
A\cdot B=|A||B|\cos\theta
]
因此,用点积检索时,一个向量可能因为模长较大而获得更高分。
这并不意味着点积不好。如果模型在训练时就让模长承载了有用信息,使用点积可能更合适。反过来,如果这些长度差异会干扰检索,余弦相似度更容易满足需求。
选择哪一种,应该看模型的训练目标和使用说明。
五、归一化之后,事情就简单了
实际项目里,还有一个经常和余弦相似度一起出现的操作:归一化。
这里通常指 L2 归一化,也就是把向量除以自己的长度:
[
\hat A=\frac{A}{|A|}
]
处理后,向量的方向不变,长度变成 1。
想象一下,把长短不一的箭头全部缩放成相同长度,就可以更直接地比较它们的方向。
两个向量都归一化之后:
[
|\hat A|=|\hat B|=1
]
余弦相似度就变成了:
[
\operatorname{sim}(\hat A,\hat B)=\hat A\cdot\hat B
]
也就是说,归一化后的向量,直接计算点积,就能得到余弦相似度。
这也是为什么有些检索代码明明在做“余弦检索”,配置里却出现了 inner product,也就是内积。如果入库向量和查询向量都已经归一化,这样做是说得通的。
归一化还有一个有意思的结果:
[
|\hat A-\hat B|^2=2-2(\hat A\cdot\hat B)
]
右边的点积就是余弦相似度。于是,对单位向量来说,余弦相似度越高,欧氏距离就越小。
所以,在所有向量都经过 L2 归一化的前提下,用余弦相似度、点积或欧氏距离进行精确排序,得到的先后顺序是一致的。 如果使用近似索引,实际返回结果仍可能受索引配置等因素影响。
看到这里就能发现,这几种计算方式并没有想象中那么割裂。向量有没有归一化,是一个很关键的条件。
六、分数 0.8,等于有八成相似吗
不等于。
余弦相似度是一个几何计算结果,不是概率,也不是人类对两句话“相似程度”的百分比打分。
某个模型算出 0.8,可能已经是很相关的结果;换一个模型,0.8 也许只是比较普通的分数。
因此,不能随手规定:
大于 0.8 就相关,小于 0.8 就无关。
更靠谱的做法,是准备一些真实查询,观察相关文档和不相关文档各自的得分,再根据业务需求确定阈值。
还有个容易忽略的问题:排在第一,不代表值得使用。
假如知识库里根本没有答案,检索仍然可以找出分数最高的几条。这就像让一群没复习的人参加考试,总会有人拿第一,但第一名也可能不及格。
所以,检索系统既要考虑排名,也要考虑结果是否足够相关。
七、算得很准,也可能找错内容
余弦相似度只能比较已有向量。向量没表达好的信息,它也没办法补回来。
比如:
如何开启远程访问?
和:
如何关闭远程访问?
两句话用词高度重合,但操作方向相反。模型如果没有充分区分这种差异,算出的相似度仍可能很高。
型号、版本号、错误码也有类似问题。用户查的是某个具体错误,语义检索可能找到几篇“讨论类似故障”的文章,却漏掉真正包含那个错误码的文档。
因此,实际检索经常会把几种方法一起用:语义检索负责找意思接近的内容,关键词检索负责照顾精确词项,再通过重排进一步判断候选内容是否能回答问题。
余弦相似度很适合做其中的一步,但不能包办整个检索过程。
八、为什么它这么常用
余弦相似度常用于 Embedding 检索,主要是因为它提供了一种直接的比较方式:在模型适合这种度量的前提下,通过向量方向衡量相关程度,同时减少模长差异的影响。
计算也不复杂。向量提前归一化后,比较时只需要做点积,很方便接入向量检索系统。
不过,真正决定效果的,仍然是模型、文本切分、数据质量和检索流程。实际使用时,先确认模型推荐什么度量、输出是否已经归一化,再选择索引配置,通常比纠结“哪种公式天生更好”更有用。
- 点赞
- 收藏
- 关注作者
评论(0)