AI 数学课程 · 第 44 课,共 180

角度与余弦相似度

从向量坐标计算角度和余弦相似度,负责任地归一化嵌入,并在检索代码中处理零向量。

分享这篇文章

两个文档可以表示为向量 x=(1,2)x=(1,2) 和 y=(10,20)y=(10,20)。它们的原始点积是 5050,因此点积搜索会把第二个向量看得更大。不过,它们的方向完全相同:分别除以两个向量的长度后,余弦相似度是 11。评分规则改变了“相似”的含义。

学完本课后,你将能够从坐标计算角度和余弦相似度,解释为什么正数缩放不会改变余弦相似度,并在面对零向量时不为它赋予它并不存在的方向。

**学习成果:**计算角度和余弦相似度,并处理零向量边界。

本课结合了两个更早的概念:第 039 课:范数衡量向量大小提供欧几里得长度,第 041 课:内积衡量对齐程度提供点积。第 043 课:距离取决于所选几何随后说明了为什么距离会把大小保留在比较中。官方 Mathematics for Machine Learning 书籍的第 3.4 节展开了角度关系。这里的示例、代码、视觉、练习题均为原创。

角度将方向与长度分开

对于两个非零向量 x,y∈Rnx,y\in\mathbb{R}^n,用 θ\theta 表示它们之间的角度。通常的点积和欧几里得范数为

第一个表达式把对应坐标组合成一个标量。第二个表达式衡量每个向量的欧几里得长度。第三个表达式用两个长度归一化对齐分数,留下方向比较。对于非零向量,柯西–施瓦茨不等式保证最后的比值处在 −1-1 与 11 之间,因此可以在区间 [0,π][0,\pi] 内唯一恢复角度:

θ=arccos⁡ ⁣(xTy∥x∥2 ∥y∥2).\theta = \arccos\!\left(\frac{x^\mathsf{T}y}{\lVert x\rVert_2\,\lVert y\rVert_2}\right).

这个归一化值就是余弦相似度。接近 11 表示向量几乎指向同一方向;00 表示在通常的点积几何中两者成直角;−1-1 表示方向相反。这些解释假设两个向量都非零,并且使用标准欧几里得内积。

从坐标计算一个角度

取 x=(3,1)x=(3,1) 和 y=(1,2)y=(1,2)。它们是二维向量,因此各自有两个坐标,并不暗示任何物理单位。在应用反余弦之前,我们可以先计算每一部分。

中间值很重要。仅凭点积 55 无法告诉我们角度,因为另一对向量可能有相同的点积但长度不同。除以两个范数后,结果才能在不同向量大小之间进行比较。

正数缩放不会改变余弦相似度

假设用正数 cc 拉伸一个非零向量。它的方向不会改变,代数也说明了余弦分数为何保持不变:

cos⁡(cx,y)=(cx)Ty∥cx∥2∥y∥2=c xTyc∥x∥2∥y∥2=cos⁡(x,y),c>0.\cos(cx,y)=\frac{(cx)^\mathsf{T}y}{\lVert cx\rVert_2\lVert y\rVert_2} =\frac{c\,x^\mathsf{T}y}{c\lVert x\rVert_2\lVert y\rVert_2} =\cos(x,y),\qquad c>0.

当 yy 被正数缩放时,同样的约消也会发生。这是方向性嵌入比较的有用特性:方向相同但范数更大的文档向量会得到相同的余弦分数。这同时也是一种建模选择。如果大小承载着关于置信度、频率或表示的其他属性的信号,去掉它就可能丢失信息。

一起探索角度、分数与长度

这个探索器从两个非零向量开始。用鼠标、触摸输入或方向键移动任一坐标。图像从原点显示相同的向量,表格则以文字重复点积、两个范数、余弦相似度以及恢复出的角度。将任一向量设为 (0,0)(0,0),即可看到零向量边界。

向量方向改变时,余弦相似度也会改变
选择两个向量的坐标

每个范围控件会以 0.5 为步长改变一个坐标。表格是所绘几何图形的完整文字替代。

向量 x

向量 y

x = (1, 0.5);y = (0.5, 1.5);点积 = 1.25;x 的范数 = 1.118;y 的范数 = 1.581;余弦相似度 = 0.707;角度 = 0.785 弧度(45 度)。解释:分数反映了部分方向对齐。

共享原点上的两个向量及其夹角图中显示从原点出发的向量 x = (1, 0.5) 和 y = (0.5, 1.5)。x = (1, 0.5);y = (0.5, 1.5);点积 = 1.25;x 的范数 = 1.118;y 的范数 = 1.581;余弦相似度 = 0.707;角度 = 0.785 弧度(45 度)。解释:分数反映了部分方向对齐。-2-2-1-101122x = (1, 0.5)y = (0.5, 1.5)第一个坐标第二个坐标
所选向量的文字数值
量值
点积 x · y1.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 文档用向量范数说明了这一轴约定。

会改变解释的边界

零向量没有角度

如果 x=0x=0,那么 ∥x∥2=0\lVert x\rVert_2=0,余弦公式会除以零。点积 0Ty=00^\mathsf{T}y=0 仍然有定义,但这并不意味着零向量与 yy 成直角。零向量没有方向,因此从数学上说,角度和余弦相似度未定义。

库可能会为这个输入选择一种策略。当前的 cosine_similarity 示例包含一个零行,并显示分数 0.0;这是需要针对所用版本和数据路径检查的 API 约定,而不是新定义出的方向。生产检索管线应决定是拒绝、过滤还是单独路由零嵌入,并明确测试这一策略。

相反向量并非“互不相关”

对于非零的 xx,向量对 (x,−x)(x,-x) 满足

cos⁡(x,−x)=−∥x∥22∥x∥22=−1,θ=π.\cos(x,-x)=\frac{-\lVert x\rVert_2^2}{\lVert x\rVert_2^2}=-1, \qquad \theta=\pi.

由于向量方向相反,分数达到最小值。按照这种几何关系,接近 00 的分数表示正交;它并不会自动表示两个项目在语义上互不相关。解释取决于表示和任务。

余弦不等同于距离或原始点积

余弦相似度忽略正数缩放,而欧几里得距离不会忽略。对于 x=(1,1)x=(1,1) 和 y=(10,10)y=(10,10),余弦相似度是 11,但

∥x−y∥2=(−9)2+(−9)2=92≈12.728.\lVert x-y\rVert_2=\sqrt{(-9)^2+(-9)^2}=9\sqrt{2}\approx12.728.

这一对向量作为点的方向完全相同,却仍然相距很远。反过来,原始点积会把方向与两个长度混合在一起。当方向一致是预期信号时选择余弦;当大小也应参与决策时,选择由范数诱导的距离或原始分数。scikit-learn 的 cosine_distances API将其值定义为 11 减去余弦相似度,因此它保留了相同的方向性选择,而不会恢复欧几里得分离程度。

下一课第 045 课:正交意味着没有线性重叠将聚焦于内积为零这一特殊情况,并解释“没有线性重叠”的含义(该链接打开英文内容)。Math for AI 课程中心会在每一课开放时持续显示依赖顺序。

检查你的理解

Question 1

对于 x = (3, 1) 和 y = (1, 2),计算点积、两个 L2 范数、余弦相似度和角度。

Show the step-by-step solution

点积是 3⋅1+1⋅2=53\cdot1+1\cdot2=5。范数是 ∥x∥2=3⋅3+1⋅1=10\lVert x\rVert_2=\sqrt{3\cdot3+1\cdot1}=\sqrt{10} 和 ∥y∥2=1⋅1+2⋅2=5\lVert y\rVert_2=\sqrt{1\cdot1+2\cdot2}=\sqrt{5}。因此 cos⁡(θ)=5/(105)=1/2≈0.7071\cos(\theta)=5/(\sqrt{10}\sqrt{5})=1/\sqrt{2}\approx0.7071。 应用反余弦得到 θ=π/4\theta=\pi/4 弧度,即 45∘45^\circ。每一步都使用了两个坐标向量和范数非零这一假设。

Question 2

为什么 x = (1, 2) 和 y = (10, 20) 的点积与自身长度不同,却有相同的余弦相似度?

Show the step-by-step solution

这两个向量满足 y=10xy=10x,所以 xTy=1⋅10+2⋅20=50x^\mathsf{T}y=1\cdot10+2\cdot20=50。它们的 范数是 ∥x∥2=5\lVert x\rVert_2=\sqrt{5} 和 ∥y∥2=105\lVert y\rVert_2=10\sqrt{5} 。归一化分数是 50/(5 105)=150/(\sqrt{5}\,10\sqrt{5})=1。点积随 yy 的缩放而增大,但相同的缩放也出现在它的范数中,并在余弦相似度里约去。

Question 3

令 x = (1, 0) 且 y = (0, 2)。它们的余弦相似度和角度是多少?这显示了什么几何关系?

Show the step-by-step solution

点积是 1⋅0+0⋅2=01\cdot0+0\cdot2=0。范数是 11 和 22,所以余弦相似度是 0/(1⋅2)=00/(1\cdot2)=0。由于 arccos⁡(0)=π/2\arccos(0)=\pi/2,角度是 90∘90^\circ。因此,在标准点积几何中,这两个向量正交,尽管它们都不是零向量。

Question 4

对于 x = (1, 0) 和 y = (−4, 0),计算余弦相似度和角度。为什么分数不是零?

Show the step-by-step solution

点积是 1⋅(−4)+0⋅0=−41\cdot(-4)+0\cdot0=-4,范数是 11 和 44。因此 cos⁡(θ)=−4/(1⋅4)=−1\cos(\theta)=-4/(1\cdot4)=-1。反余弦得到 θ=π=180∘\theta=\pi=180^\circ。两个向量位于同一直线上但方向相反,因此它们是最大程度的负对齐,而不是垂直。

Question 5

对于 x = (0, 0) 和 y = (2, 3),余弦相似度实现应该做什么?为什么返回 0 不是一个数学角度?

Show the step-by-step solution

xx 的范数是 00,所以分母 ∥x∥2∥y∥2\lVert x\rVert_2\lVert y\rVert_2 为零。公式未定义,而且 xx 没有方向。实现可以拒绝这一对、过滤零向量,或者采用一个有文档说明的 API 约定(例如返回 00);这一约定必须经过测试,也不能被描述成直角。

Question 6

如果 x = (1, 2)、y = (2, 1),并且 x 被替换为 −3x,余弦相似度会怎样变化?展示符号计算。

Show the step-by-step solution

首先,xTy=1⋅2+2⋅1=4x^\mathsf{T}y=1\cdot2+2\cdot1=4,且 ∥x∥2∥y∥2=55=5\lVert x\rVert_2\lVert y\rVert_2=\sqrt5\sqrt5=5,所以原余弦相似度为 4/5=0.84/5=0.8。令 c=−3c=-3 后,分子乘以 −3-3,而 cxcx 的范数为 ∣c∣∥x∥2=3∥x∥2|c|\lVert x\rVert_2=3\lVert x\rVert_2。因此新分数是 (−3)/3⋅0.8=−0.8(-3)/3\cdot0.8=-0.8:负数缩放反转了方向。

Question 7

为什么 NumPy 实现会在调用 arccos 前截断归一化点积?较大的超界应该说明什么?

Show the step-by-step solution

从数学上讲,柯西–施瓦茨不等式把余弦限制在 [−1,1][-1,1],但浮点数运算可能在很长或几乎平行的计算后产生类似 1.00000000000000021.0000000000000002 的值。由于 arccos⁡\arccos 只有在该区间内才是实值,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

原始点积是 qTa=10q^\mathsf{T}a=10、qTb=5q^\mathsf{T}b=5 和 qTc=100q^\mathsf{T} c=100,因此原始点积排序为 cc、aa、bb。向量 c=10ac=10a,所以 cos⁡(q,c)=cos⁡(q,a)=1\cos(q,c)=\cos(q,a)=1。对于 bb,cos⁡(q,b)=5/(105)≈0.7071\cos(q,b)=5/(\sqrt{10}\sqrt{5} )\approx0.7071。因此余弦排序将 aa 与 cc 并列第一,随后是 bb。这种差异揭示了建模决策:点积同时奖励大小和方向,而余弦会去除正数缩放。

资料来源

  1. Mathematics for Machine Learning book PDF, Section 3.4
  2. NumPy documentation: numpy.dot
  3. NumPy documentation: numpy.linalg.norm
  4. NumPy documentation: numpy.arccos
  5. scikit-learn documentation: Pairwise metrics, affinities and kernels
  6. scikit-learn documentation: cosine_similarity
  7. scikit-learn documentation: cosine_distances