1. 什么是余弦相似度? #
1.1 生活中的例子 #
想象一下,你在网上购物,想找和你兴趣相似的人来参考他们的购买记录:
- 用户A:喜欢买很多书(100本),偶尔买几件衣服(2件)
- 用户B:也喜欢买很多书(50本),偶尔买几件衣服(1件)
虽然用户A买的书是用户B的2倍,但他们的购买偏好方向是相同的(都喜欢书,不太喜欢衣服)。余弦相似度就能识别出这种相似性,而欧氏距离可能会认为他们差异很大(因为数量差异大)。
1.2 余弦相似度的核心思想 #
余弦相似度通过计算两个向量夹角的余弦值来衡量相似程度:
- 夹角为0度:两个向量方向完全相同,相似度为1(最相似)
- 夹角为90度:两个向量垂直(正交),相似度为0(不相关)
- 夹角为180度:两个向量方向完全相反,相似度为-1(最不相似)
关键优势:余弦相似度不受向量大小(长度)的影响,只关注方向。这使得它特别适合处理:
- 文本相似度(不同长度的文档)
- 推荐系统(不同用户的行为强度不同)
- 特征比较(关注特征的比例而非绝对值)
1.3 为什么需要余弦相似度? #
问题场景:假设你要比较两篇文章的相似度:
- 文章A:1000字,提到"Python"10次,"编程"5次
- 文章B:500字,提到"Python"5次,"编程"2.5次
如果用欧氏距离,文章A和B会显得差异很大(因为字数不同)。但实际上,两篇文章的主题比例是相同的(都是Python:编程 = 2:1)。
余弦相似度的优势:
- 不受文档长度影响
- 关注内容的比例和方向
- 对稀疏数据友好(很多值为0的情况)
2. 前置知识 #
在学习余弦相似度之前,我们需要了解一些基础概念。
2.1 什么是向量? #
向量就是一组有序的数字,比如 [1, 2, 3] 就是一个3维向量。在数学和编程中,向量用来表示具有多个特征的对象。
# 导入 numpy 库用于处理向量
import numpy as np
# 创建一个3维向量,表示某个对象的特征
# 比如:[身高, 体重, 年龄] 或者 [喜欢Python的程度, 喜欢Java的程度, 喜欢C++的程度]
vector = np.array([1.2, 3.4, 5.6])
# 打印向量的维度(形状)
print(f"向量维度: {vector.shape}")
# 打印向量内容
print(f"向量内容: {vector}")
# 打印向量的长度(元素个数)
print(f"向量长度: {len(vector)}")2.2 什么是余弦 #
余弦(Cosine)是一个三角函数,表示一个角的邻边长度与斜边长度之比。在余弦相似度中,我们利用余弦值来衡量两个向量夹角的大小。
数学公式:
对于一个角 $\theta$,其余弦值为:
$$ \cos\theta = \frac{\text{邻边}}{\text{斜边}} $$
2.3 什么是余弦定理 #
余弦定理(Cosine Law) 是平面几何中的一个重要公式,用于描述三角形三边和夹角之间的关系。它是勾股定理的扩展,适用于任意三角形(不只是直角三角形)。
公式:
对于三角形 $ABC$,设 $a,b,c$ 分别为角 $A,B,C$ 对应的三边长度,则有:
$$ c^2 = a^2 + b^2 - 2ab\cos C $$
或者类似地:
$$ a^2 = b^2 + c^2 - 2bc\cos A $$
形象理解:
- 当角 $C = 90^\circ$,$\cos C = 0$,就变成了勾股定理 $c^2 = a^2 + b^2$。
- 当角 $C$ 不为直角时,余弦定理可以用来计算未知的边或角。
2.4 什么是向量的点积(内积)? #
点积(Dot Product),也叫内积,是向量运算中最基础、最核心的概念之一。
点积衡量了两个向量在多大程度上指向同一个方向。 或者说:它计算了一个向量在另一个向量方向上的“有效贡献”或“投影”。
我们可以从三个层面来通俗理解它:
2.4.1. 直观的生活例子:赛车加速带 #
想象你在玩赛车游戏,地上有一个加速带(箭头指引加速方向,这是一个向量 $\mathbf{B}$)。你的赛车正朝某个方向行驶(这是向量 $\mathbf{A}$)。
点积 $\mathbf{A} \cdot \mathbf{B}$ 就是你实际获得的加速效果:
- 情况一(完全同向): 你正对着加速带箭头冲上去。
- 你的方向和加速带完全一致(夹角 $0^\circ$)。
- 结果: 获得最大加速!点积是正的最大值。
- 情况二(侧面路过): 你垂直于加速带箭头开过去。
- 你完全没有利用到它的推力(夹角 $90^\circ$)。
- 结果: 没有加速,也没减速。点积等于 0。
- 情况三(完全逆向): 你逆着箭头开。
- 你和它对着干(夹角 $180^\circ$)。
- 结果: 被减速或弹回。点积是负数。
结论: 点积告诉你这两个向量配合得有多好。
2.4.2. 几何视角:投影(影子的长度) #
这是最经典的解释。想象有一束光垂直照下来。
公式:$\mathbf{A} \cdot \mathbf{B} = |\mathbf{A}| |\mathbf{B}| \cos\theta$
我们可以把它拆解成:$|\mathbf{A}| \times (\underbrace{|\mathbf{B}| \cos\theta}_{\text{投影}})$
- 这就相当于:把向量 $\mathbf{B}$ 投影到向量 $\mathbf{A}$ 上(想象 $\mathbf{B}$ 在 $\mathbf{A}$ 身上投下的影子)。
- 点积 = $\mathbf{A}$ 的长度 乘以 $\mathbf{B}$ 在 $\mathbf{A}$ 上的影子长度。
如果影子和 $\mathbf{A}$ 同向,乘积为正;如果影子是反向的,乘积为负;如果没有影子(垂直),乘积为 0。
2.4.3. 代数视角:计算(程序员怎么算) #
在计算机代码里我们用坐标直接算。
如果 $\mathbf{A} = (x_1, y_1)$,$\mathbf{B} = (x_2, y_2)$。 那么点积就是: $$\mathbf{A} \cdot \mathbf{B} = x_1 x_2 + y_1 y_2$$
简单来说就是:x 乘 x,y 乘 y,然后加起来。 (如果是 3D,就是 $x_1x_2 + y_1y_2 + z_1z_2$)。
2.5 什么是向量的模长(长度)? #
向量的模长就是向量的"长度",就像在坐标系中计算从原点到点的距离。
数学公式:对于向量 A = [a₁, a₂, ..., aₙ] $$ |\mathbf{A}| = \sqrt{a_1^2 + a_2^2 + \cdots + a_n^2} $$
# 导入 numpy 库用于数学计算
import numpy as np
# 定义一个向量
vector = np.array([3, 4])
# 方法1:使用 numpy 的 linalg.norm 函数计算模长
norm_v1 = np.linalg.norm(vector)
print(f"方法1(使用norm): {norm_v1}")
# 方法2:手动计算模长
# 先计算每个元素的平方
squared = vector ** 2
print(f"各元素平方: {squared}")
# 求和
sum_squared = np.sum(squared)
print(f"平方和: {sum_squared}")
# 开平方根
norm_v2 = np.sqrt(sum_squared)
print(f"方法2(手动计算): {norm_v2}")
# 验证:3² + 4² = 9 + 16 = 25,√25 = 5
print(f"验证: 3² + 4² = {3**2 + 4**2}, √{3**2 + 4**2} = {norm_v2}")
# 可视化:在二维坐标系中,向量 [3, 4] 的长度就是 5
print(f"\n在二维坐标系中,从原点到点 (3, 4) 的距离是: {norm_v1}")3. 余弦相似度原理 #
3.1 数学公式 #
对于两个向量 A 和 B,余弦相似度的计算公式为:
$$ \cos\theta = \frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}|\; |\mathbf{B}|} $$
其中:
- A · B:两个向量的点积(内积)
- ||A||:向量 A 的模长(长度)
- ||B||:向量 B 的模长(长度)
- θ:两个向量之间的夹角
3.2 数学证明 #
3.2.1. 设定 #
在三角形 OAB 中(O 是原点):
- $ a = |\mathbf{A}| $ (对角 A 的边,即 OA 的长度)
- $ b = |\mathbf{B}| $ (对角 B 的边,即 OB 的长度)
- $ c = |\mathbf{B} - \mathbf{A}| $ (对角 C 的边,即 AB 的长度)
角 C 在点 O,就是向量 A 与 B 的夹角 θ。
3.2.2. 代入余弦定理 #
余弦定理(对顶点 O 处的角 θ): $$ c^2 = a^2 + b^2 - 2ab\cos\theta $$ 代入: $$ |\mathbf{B} - \mathbf{A}|^2 = |\mathbf{A}|^2 + |\mathbf{B}|^2 - 2|\mathbf{A}|\,|\mathbf{B}|\cos\theta $$
3.2.3. 用坐标展开 #
左边: $$ |\mathbf{B} - \mathbf{A}|^2 = (B_x - A_x)^2 + (B_y - A_y)^2 $$ $$ = A_x^2 + A_y^2 + B_x^2 + B_y^2 - 2(A_x B_x + A_y B_y) $$ $$ = |\mathbf{A}|^2 + |\mathbf{B}|^2 - 2(\mathbf{A} \cdot \mathbf{B}) $$
右边: $$ |\mathbf{A}|^2 + |\mathbf{B}|^2 - 2|\mathbf{A}|\,|\mathbf{B}|\cos\theta $$
3.2.4. 化简 #
两边都有 $|\mathbf{A}|^2 + |\mathbf{B}|^2$,可以抵消,得到:
$$ -2(\mathbf{A} \cdot \mathbf{B}) = -2 |\mathbf{A}| |\mathbf{B}| \cos\theta $$
消去两边的 $-2$,得到:
$$ \mathbf{A} \cdot \mathbf{B} = |\mathbf{A}|\,|\mathbf{B}|\,\cos\theta $$
两边同时除以 $|\mathbf{A}| |\mathbf{B}|$,得到余弦公式:
$$ \cos\theta = \frac{\mathbf{A} \cdot \mathbf{B}}{|\mathbf{A}|\,|\mathbf{B}|} $$
3.2.5. 最终结果 #
$$ \cos\theta = \frac{A_x B_x + A_y B_y}{\sqrt{A_x^2 + A_y^2} \cdot \sqrt{B_x^2 + B_y^2}} $$
3.3 取值范围和含义 #
余弦相似度的取值范围是 [-1, 1]:
- 1:两个向量方向完全相同(夹角0度),最相似
- 接近1:两个向量方向非常相似
- 0:两个向量垂直(夹角90度),不相关
- 接近-1:两个向量方向相反
- -1:两个向量方向完全相反(夹角180度),最不相似
3.4 代码实现 #
# 导入 numpy 库用于数学计算
import numpy as np
# 定义函数,计算两个向量的余弦相似度
def cosine_similarity(a, b):
# 计算两个向量的点积(内积)
dot_product = np.dot(a, b)
# 计算第一个向量的模长(长度)
norm_a = np.linalg.norm(a)
# 计算第二个向量的模长(长度)
norm_b = np.linalg.norm(b)
# 如果模长为0,返回0(避免除以0的错误)
if norm_a == 0 or norm_b == 0:
return 0.0
# 返回点积除以两个模长的乘积,即余弦相似度
return dot_product / (norm_a * norm_b)
# 示例1:方向相同的向量(相似度应该接近1)
vector_a = np.array([1, 2, 3])
vector_b = np.array([2, 4, 6]) # vector_a 的2倍,方向相同
similarity_1 = cosine_similarity(vector_a, vector_b)
print(f"示例1 - 方向相同的向量:")
print(f" 向量A: {vector_a}")
print(f" 向量B: {vector_b}")
print(f" 余弦相似度: {similarity_1:.4f} (应该接近1.0)")
# 示例2:方向相反的向量(相似度应该接近-1)
vector_c = np.array([1, 2, 3])
vector_d = np.array([-1, -2, -3]) # 完全相反的方向
similarity_2 = cosine_similarity(vector_c, vector_d)
print(f"\n示例2 - 方向相反的向量:")
print(f" 向量C: {vector_c}")
print(f" 向量D: {vector_d}")
print(f" 余弦相似度: {similarity_2:.4f} (应该接近-1.0)")
# 示例3:垂直的向量(相似度应该接近0)
vector_e = np.array([1, 0])
vector_f = np.array([0, 1]) # 在二维空间中垂直
similarity_3 = cosine_similarity(vector_e, vector_f)
print(f"\n示例3 - 垂直的向量:")
print(f" 向量E: {vector_e}")
print(f" 向量F: {vector_f}")
print(f" 余弦相似度: {similarity_3:.4f} (应该接近0.0)")