1. 什么是余弦相似度? #

1.1 生活中的例子 #

想象一下,你在网上购物,想找和你兴趣相似的人来参考他们的购买记录:

虽然用户A买的书是用户B的2倍,但他们的购买偏好方向是相同的(都喜欢书,不太喜欢衣服)。余弦相似度就能识别出这种相似性,而欧氏距离可能会认为他们差异很大(因为数量差异大)。

1.2 余弦相似度的核心思想 #

余弦相似度通过计算两个向量夹角的余弦值来衡量相似程度:

关键优势:余弦相似度不受向量大小(长度)的影响,只关注方向。这使得它特别适合处理:

1.3 为什么需要余弦相似度? #

问题场景:假设你要比较两篇文章的相似度:

如果用欧氏距离,文章A和B会显得差异很大(因为字数不同)。但实际上,两篇文章的主题比例是相同的(都是Python:编程 = 2:1)。

余弦相似度的优势:

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 $$

形象理解:

2.4 什么是向量的点积(内积)? #

点积

点积(Dot Product),也叫内积,是向量运算中最基础、最核心的概念之一。

点积衡量了两个向量在多大程度上指向同一个方向。 或者说:它计算了一个向量在另一个向量方向上的“有效贡献”或“投影”。

我们可以从三个层面来通俗理解它:

2.4.1. 直观的生活例子:赛车加速带 #

想象你在玩赛车游戏,地上有一个加速带(箭头指引加速方向,这是一个向量 $\mathbf{B}$)。你的赛车正朝某个方向行驶(这是向量 $\mathbf{A}$)。

点积 $\mathbf{A} \cdot \mathbf{B}$ 就是你实际获得的加速效果:

结论: 点积告诉你这两个向量配合得有多好。

2.4.2. 几何视角:投影(影子的长度) #

这是最经典的解释。想象有一束光垂直照下来。

公式:$\mathbf{A} \cdot \mathbf{B} = |\mathbf{A}| |\mathbf{B}| \cos\theta$

我们可以把它拆解成:$|\mathbf{A}| \times (\underbrace{|\mathbf{B}| \cos\theta}_{\text{投影}})$

如果影子和 $\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}|} $$

其中:

3.2 数学证明 #

公式推导

3.2.1. 设定 #

在三角形 OAB 中(O 是原点):

角 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]:

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)")