角度与余弦相似度
从向量坐标计算角度和余弦相似度,负责任地归一化嵌入,并在检索代码中处理零向量。
两个文档可以表示为向量 和 。它们的原始点积是 ,因此点积搜索会把第二个向量看得更大。不过,它们的方向完全相同:分别除以两个向量的长度后,余弦相似度是 。评分规则改变了“相似”的含义。
学完本课后,你将能够从坐标计算角度和余弦相似度,解释为什么正数缩放不会改变余弦相似度,并在面对零向量时不为它赋予它并不存在的方向。
**学习成果:**计算角度和余弦相似度,并处理零向量边界。
本课结合了两个更早的概念:第 039 课:范数衡量向量大小提供欧几里得长度,第 041 课:内积衡量对齐程度提供点积。第 043 课:距离取决于所选几何随后说明了为什么距离会把大小保留在比较中。官方 Mathematics for Machine Learning 书籍的第 3.4 节展开了角度关系。这里的示例、代码、视觉、练习题均为原创。
角度将方向与长度分开
对于两个非零向量 ,用 表示它们之间的角度。通常的点积和欧几里得范数为
第一个表达式把对应坐标组合成一个标量。第二个表达式衡量每个向量的欧几里得长度。第三个表达式用两个长度归一化对齐分数,留下方向比较。对于非零向量,柯西–施瓦茨不等式保证最后的比值处在 与 之间,因此可以在区间 内唯一恢复角度:
这个归一化值就是余弦相似度。接近 表示向量几乎指向同一方向; 表示在通常的点积几何中两者成直角; 表示方向相反。这些解释假设两个向量都非零,并且使用标准欧几里得内积。
从坐标计算一个角度
取 和 。它们是二维向量,因此各自有两个坐标,并不暗示任何物理单位。在应用反余弦之前,我们可以先计算每一部分。
中间值很重要。仅凭点积 无法告诉我们角度,因为另一对向量可能有相同的点积但长度不同。除以两个范数后,结果才能在不同向量大小之间进行比较。
正数缩放不会改变余弦相似度
假设用正数 拉伸一个非零向量。它的方向不会改变,代数也说明了余弦分数为何保持不变:
当 被正数缩放时,同样的约消也会发生。这是方向性嵌入比较的有用特性:方向相同但范数更大的文档向量会得到相同的余弦分数。这同时也是一种建模选择。如果大小承载着关于置信度、频率或表示的其他属性的信号,去掉它就可能丢失信息。
一起探索角度、分数与长度
这个探索器从两个非零向量开始。用鼠标、触摸输入或方向键移动任一坐标。图像从原点显示相同的向量,表格则以文字重复点积、两个范数、余弦相似度以及恢复出的角度。将任一向量设为 ,即可看到零向量边界。
x = (1, 0.5);y = (0.5, 1.5);点积 = 1.25;x 的范数 = 1.118;y 的范数 = 1.581;余弦相似度 = 0.707;角度 = 0.785 弧度(45 度)。解释:分数反映了部分方向对齐。
| 量 | 值 |
|---|---|
| 点积 x · y | 1.25 |
| x 的范数 | 1.118 |
| y 的范数 | 1.581 |
| 余弦相似度 | 0.707 |
| 角度 | 0.785 弧度 / 45 度 |
向量线使用不同的虚线模式和标签,而不只依靠颜色。即使图形不可用或减少了动画,数值表仍然有意义。
绘出的角度是帮助推理的辅助,而不是另一种定义。当控件改变时,数值表会重新计算上面展示的同一个公式。向量在不改变方向的情况下变长,余弦值会保持不变;而旋转任一向量会同时改变角度和分数。
谨慎地在嵌入检索中使用分数
嵌入是对某个项目的向量表示,例如文档、查询、图像或用户。如果预期的匹配依据是方向,余弦相似度就是自然的分数:它比较归一化向量,而不是比较原始长度。分数仍取决于嵌入模型、预处理、语料库和任务;它不会把底层文档的“相似”变成一个普适属性。
当前的 scikit-learn 成对指标指南将余弦相似度描述为 L2 归一化的点积,并指出它可用于文档向量。它的 cosine_similarity API把行视为样本,并返回成对分数矩阵。在单个向量和一个批次之间转换时,这个形状很容易丢失,因此要明确行的约定:
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
query = np.array([[3.0, 1.0]]) # shape (1 sample, 2 features)
documents = np.array([
[3.0, 1.0],
[1.0, 2.0],
[30.0, 10.0],
]) # shape (3 samples, 2 features)
scores = cosine_similarity(query, documents)
print(scores.round(4))
# [[1. 0.7071 1. ]]
对于透明的 NumPy 实现,一维点积和向量范数已经足够。NumPy 的 dot 文档将一维情况定义为两个向量的内积,而 numpy.linalg.norm在数组包含多个向量时接受 axis 参数。这个保护条件是实现的一部分,而不是装饰性的检查:
import numpy as np
def cosine_similarity(x, y):
x = np.asarray(x, dtype=float)
y = np.asarray(y, dtype=float)
x_norm = np.linalg.norm(x)
y_norm = np.linalg.norm(y)
if x_norm == 0 or y_norm == 0:
raise ValueError('cosine similarity needs two nonzero vectors')
raw = np.dot(x, y) / (x_norm * y_norm)
return np.clip(raw, -1.0, 1.0)
print(cosine_similarity(np.array([3.0, 1.0]), np.array([1.0, 2.0])))
# 0.7071067811865475
clip 可以防止微小的浮点数超界把 arccos 送出其定义域。它不应被用来隐藏更大的数值错误或数据形状错误。对于形状为 (number_of_samples, number_of_features) 的批次,要在 axis=1 上计算范数,并为每行保留一个分数;当前 NumPy 文档用向量范数说明了这一轴约定。
会改变解释的边界
零向量没有角度
如果 ,那么 ,余弦公式会除以零。点积 仍然有定义,但这并不意味着零向量与 成直角。零向量没有方向,因此从数学上说,角度和余弦相似度未定义。
库可能会为这个输入选择一种策略。当前的 cosine_similarity 示例包含一个零行,并显示分数 0.0;这是需要针对所用版本和数据路径检查的 API 约定,而不是新定义出的方向。生产检索管线应决定是拒绝、过滤还是单独路由零嵌入,并明确测试这一策略。
相反向量并非“互不相关”
对于非零的 ,向量对 满足
由于向量方向相反,分数达到最小值。按照这种几何关系,接近 的分数表示正交;它并不会自动表示两个项目在语义上互不相关。解释取决于表示和任务。
余弦不等同于距离或原始点积
余弦相似度忽略正数缩放,而欧几里得距离不会忽略。对于 和 ,余弦相似度是 ,但
这一对向量作为点的方向完全相同,却仍然相距很远。反过来,原始点积会把方向与两个长度混合在一起。当方向一致是预期信号时选择余弦;当大小也应参与决策时,选择由范数诱导的距离或原始分数。scikit-learn 的 cosine_distances API将其值定义为 减去余弦相似度,因此它保留了相同的方向性选择,而不会恢复欧几里得分离程度。
下一课第 045 课:正交意味着没有线性重叠将聚焦于内积为零这一特殊情况,并解释“没有线性重叠”的含义(该链接打开英文内容)。Math for AI 课程中心会在每一课开放时持续显示依赖顺序。
检查你的理解
Question 1
对于 x = (3, 1) 和 y = (1, 2),计算点积、两个 L2 范数、余弦相似度和角度。
Show the step-by-step solution
点积是 。范数是 和 。因此 。 应用反余弦得到 弧度,即 。每一步都使用了两个坐标向量和范数非零这一假设。
Question 2
为什么 x = (1, 2) 和 y = (10, 20) 的点积与自身长度不同,却有相同的余弦相似度?
Show the step-by-step solution
这两个向量满足 ,所以 。它们的 范数是 和 。归一化分数是 。点积随 的缩放而增大,但相同的缩放也出现在它的范数中,并在余弦相似度里约去。
Question 3
令 x = (1, 0) 且 y = (0, 2)。它们的余弦相似度和角度是多少?这显示了什么几何关系?
Show the step-by-step solution
点积是 。范数是 和 ,所以余弦相似度是 。由于 ,角度是 。因此,在标准点积几何中,这两个向量正交,尽管它们都不是零向量。
Question 4
对于 x = (1, 0) 和 y = (−4, 0),计算余弦相似度和角度。为什么分数不是零?
Show the step-by-step solution
点积是 ,范数是 和 。因此 。反余弦得到 。两个向量位于同一直线上但方向相反,因此它们是最大程度的负对齐,而不是垂直。
Question 5
对于 x = (0, 0) 和 y = (2, 3),余弦相似度实现应该做什么?为什么返回 0 不是一个数学角度?
Show the step-by-step solution
的范数是 ,所以分母 为零。公式未定义,而且 没有方向。实现可以拒绝这一对、过滤零向量,或者采用一个有文档说明的 API 约定(例如返回 );这一约定必须经过测试,也不能被描述成直角。
Question 6
如果 x = (1, 2)、y = (2, 1),并且 x 被替换为 −3x,余弦相似度会怎样变化?展示符号计算。
Show the step-by-step solution
首先,,且 ,所以原余弦相似度为 。令 后,分子乘以 ,而 的范数为 。因此新分数是 :负数缩放反转了方向。
Question 7
为什么 NumPy 实现会在调用 arccos 前截断归一化点积?较大的超界应该说明什么?
Show the step-by-step solution
从数学上讲,柯西–施瓦茨不等式把余弦限制在
,但浮点数运算可能在很长或几乎平行的计算后产生类似
的值。由于
只有在该区间内才是实值,np.clip(raw, -1.0, 1.0)
会去除微小的舍入超界。较大的超界说明数据、形状或数值条件存在问题;不应使用截断来掩盖它。
Question 8
查询向量是 q = (3, 1),三个文档向量是 a = (3, 1)、b = (1, 2) 和 c = (30, 10)。比较原始点积排序与余弦相似度排序。
Show the step-by-step solution
原始点积是 、 和 ,因此原始点积排序为 、、。向量 ,所以 。对于 ,。因此余弦排序将 与 并列第一,随后是 。这种差异揭示了建模决策:点积同时奖励大小和方向,而余弦会去除正数缩放。
资料来源
- Mathematics for Machine Learning book PDF, Section 3.4
- NumPy documentation: numpy.dot
- NumPy documentation: numpy.linalg.norm
- NumPy documentation: numpy.arccos
- scikit-learn documentation: Pairwise metrics, affinities and kernels
- scikit-learn documentation: cosine_similarity
- scikit-learn documentation: cosine_distances