余弦相似度 (Cosine Similarity) 是一种衡量两个非零向量之间方向性的度量方法。它通过计算两个向量在多维空间中夹角的余弦值来评估它们的相似程度。当两个向量的方向越接近,它们的夹角越小,余弦值越大,表示它们越相似;反之,当两个向量方向越垂直,夹角越大,余弦值越小,表示它们相似度越低。余弦相似度广泛应用于文本挖掘、信息检索、推荐系统等领域,尤其适用于处理高维数据,并且对数据维度上的量级差异不敏感

核心思想:余弦相似度忽略了向量的长度(大小),只关注向量方向上的差异。它将向量间的相似性转化为其夹角的余弦值,夹角越小,方向越一致,相似度越高。这使得它在处理文本频率等只关注词语相对重要性而非绝对数量的场景中表现出色。


一、为什么需要余弦相似度?

在许多数据分析和机器学习任务中,我们需要量化两个数据对象之间的相似性。常见的方法是计算它们之间的距离,例如欧几里得距离 (Euclidean Distance)。然而,欧几里得距离在某些场景下存在局限性:

  • 对向量长度敏感:欧几里得距离会受到向量绝对大小的影响。如果两个向量的方向一致,但其中一个向量的模(长度)非常大,欧几里得距离也会很大,导致它们被认为是“不相似”的,即使它们在概念上或趋势上是高度相关的。
    • 示例:在文本分析中,一篇很长的文档和一篇很短的文档,即使它们主题高度相关(包含相同比例的关键词),如果用词频作为向量,长文档的词频总和会远大于短文档,导致欧几里得距离很大。
  • 无法捕捉方向上的相似性:有时我们更关心数据对象之间的“趋势”或“模式”是否相似,而不是它们在每个维度上的绝对差异。

余弦相似度正是为了解决这些问题而生。它通过将向量归一化到单位长度(或者说,直接计算夹角余弦值),从而消除了向量长度对相似度计算的影响,专注于评估它们的方向一致性。

二、余弦相似度的定义与数学公式

给定两个 $n$ 维向量 $A = (A_1, A_2, \dots, A_n)$ 和 $B = (B_1, B_2, \dots, B_n)$,它们的余弦相似度 $similarity(\mathbf{A}, \mathbf{B})$ 由以下公式定义:

$$
\text{similarity}(\mathbf{A}, \mathbf{B}) = \cos(\theta) = \frac{\mathbf{A} \cdot \mathbf{B}}{||\mathbf{A}|| \cdot ||\mathbf{B}||}
$$

其中:

  • $\mathbf{A} \cdot \mathbf{B}$ 是向量 $A$ 和 $B$ 的点积 (Dot Product)
    $$
    \mathbf{A} \cdot \mathbf{B} = \sum_{i=1}^{n} A_i B_i = A_1 B_1 + A_2 B_2 + \dots + A_n B_n
    $$
  • $||\mathbf{A}||$ 是向量 $A$ 的欧几里得范数 (Euclidean Norm)模 (Magnitude)
    $$
    ||\mathbf{A}|| = \sqrt{\sum_{i=1}^{n} A_i^2} = \sqrt{A_1^2 + A_2^2 + \dots + A_n^2}
    $$
  • $||\mathbf{B}||$ 是向量 $B$ 的欧几里得范数或模:
    $$
    ||\mathbf{B}|| = \sqrt{\sum_{i=1}^{n} B_i^2} = \sqrt{B_1^2 + B_2^2 + \dots + B_n^2}
    $$
  • $\theta$ 是向量 $A$ 和 $B$ 之间的夹角。

三、余弦相似度的值域与解释

余弦相似度的值介于 $[-1, 1]$ 之间:

  • $1$:表示两个向量的方向完全相同。它们指向同一个方向,是完全相似的。
    • 例如:向量 $(1, 2)$ 和 $(2, 4)$。
  • $0$:表示两个向量相互垂直,没有线性关系,即它们完全不相似。
    • 例如:向量 $(1, 0)$ 和 $(0, 1)$。
  • $-1$:表示两个向量的方向完全相反。它们指向相反方向,是完全不相似(甚至相反)的。
    • 例如:向量 $(1, 2)$ 和 $(-1, -2)$。
  • $0 < \text{similarity} < 1$:表示两个向量方向有一定程度的相似,夹角小于 $90^\circ$。
  • $-1 < \text{similarity} < 0$:表示两个向量方向有一定程度的相反,夹角大于 $90^\circ$ 但小于 $180^\circ$。

四、计算余弦相似度的步骤

计算两个向量 $A$ 和 $B$ 的余弦相似度可以分为以下步骤:

  1. 计算向量 $A$ 和 $B$ 的点积:将对应维度上的元素相乘,然后将所有乘积求和。
    • $A \cdot B = \sum_{i=1}^{n} A_i B_i$
  2. 计算向量 $A$ 的模 (长度):将向量 $A$ 的每个元素平方,求和,然后开方。
    • $||A|| = \sqrt{\sum_{i=1}^{n} A_i^2}$
  3. 计算向量 $B$ 的模 (长度):将向量 $B$ 的每个元素平方,求和,然后开方。
    • $||B|| = \sqrt{\sum_{i=1}^{n} B_i^2}$
  4. 将点积除以两个向量模的乘积:得到最终的余弦相似度。
    • $similarity(A, B) = \frac{A \cdot B}{||A|| \cdot ||B||}$

五、示例

假设我们有两个文档,分别用以下词频向量表示(例如,每个维度代表一个词语的频率):

  • 文档 A: $\mathbf{A} = [1, 1, 0, 1, 0, 1]$ (e.g., “apple”, “banana”, “orange”, “grape”, “kiwi”, “melon”)
  • 文档 B: $\mathbf{B} = [0, 1, 1, 0, 1, 1]$

1. 计算点积 $\mathbf{A} \cdot \mathbf{B}$:
$$
\mathbf{A} \cdot \mathbf{B} = (1 \times 0) + (1 \times 1) + (0 \times 1) + (1 \times 0) + (0 \times 1) + (1 \times 1) \
= 0 + 1 + 0 + 0 + 0 + 1 = 2
$$

2. 计算向量 $\mathbf{A}$ 的模 $||\mathbf{A}||$:
$$
||\mathbf{A}|| = \sqrt{1^2 + 1^2 + 0^2 + 1^2 + 0^2 + 1^2} \
= \sqrt{1 + 1 + 0 + 1 + 0 + 1} = \sqrt{4} = 2
$$

3. 计算向量 $\mathbf{B}$ 的模 $||\mathbf{B}||$:
$$
||\mathbf{B}|| = \sqrt{0^2 + 1^2 + 1^2 + 0^2 + 1^2 + 1^2} \
= \sqrt{0 + 1 + 1 + 0 + 1 + 1} = \sqrt{4} = 2
$$

4. 计算余弦相似度:
$$
\text{similarity}(\mathbf{A}, \mathbf{B}) = \frac{2}{2 \times 2} = \frac{2}{4} = 0.5
$$
因此,文档 A 和文档 B 的余弦相似度为 0.5。这表示它们之间存在一定的相似性,但并非完全相同或完全不相关。

六、优势与适用场景

6.1 优势

  • 对向量长度不敏感:这是余弦相似度最显著的优势。它能够有效处理因向量长度差异(如文本长短、购物数量多少)而可能被欧几里得距离误判为不相似的情况。
  • 高效处理高维数据:在处理文本、图像特征等高维数据时,余弦相似度通常表现良好,因为它只关注方向,而不是绝对距离。
  • 计算效率相对较高:相较于某些复杂的距离度量,点积和模的计算相对直接。
  • 直观的几何解释:夹角越小,相似度越高,这符合人类的直观理解。

6.2 适用场景

  • 文本挖掘与自然语言处理 (NLP)
    • 文档相似度:比较两篇文档的主题相似性,如使用词袋模型 (Bag-of-Words) 或 TF-IDF 向量表示文档。
    • 词语相似度:在词嵌入 (Word Embeddings) 模型(如 Word2Vec, GloVe)中,衡量两个词语的语义相似性。
    • 信息检索:评估查询与文档的相关性。
  • 推荐系统
    • 用户-用户相似度:根据用户对物品的评分或行为向量,计算用户之间的相似性,从而推荐相似用户喜欢的物品。
    • 物品-物品相似度:计算物品之间的相似性,推荐用户喜欢过的物品的相似物品。
  • 图像识别
    • 比较图像特征向量(如 SIFT, HOG 特征)的相似性。
  • 基因表达数据分析
    • 评估基因表达模式的相似性。

七、局限性

尽管余弦相似度在许多场景下表现出色,但它也有其局限性:

  • 忽略了向量的绝对大小信息:虽然这是其优势,但在某些情况下也可能是缺点。如果向量的模(长度)本身也包含重要的信息(例如,购买金额、城市人口等),那么只关注方向可能会丢失这些重要信息。
    • 示例:在购物篮分析中,购买了少量高价值商品的顾客,与购买了大量低价值商品的顾客,其商品向量方向可能相似,但实际消费能力差异很大。
  • 对稀疏数据敏感:在处理非常稀疏的向量时(大多数维度值为零),非零维度上的少量重叠可能会导致较高的余弦相似度,即使整体交集很小。
  • 不适用于负值:当向量的元素包含负值时,余弦相似度的解释可能不那么直观。例如,一个维度上的负值可能表示某种“不喜欢”或“反向趋势”,此时余弦相似度可能不完全符合预期。
  • 无法处理缺失值:计算点积和模要求向量的所有维度都有值,缺失值需要进行处理(如填充零或平均值)。

八、代码示例 (Python)

使用 Python 实现余弦相似度非常简单,可以手动编写,也可以利用科学计算库 numpyscipy

8.1 手动实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import math

def cosine_similarity_manual(vec1, vec2):
"""
手动计算两个向量的余弦相似度
Args:
vec1 (list): 向量1
vec2 (list): 向量2
Returns:
float: 余弦相似度
"""
if len(vec1) != len(vec2):
raise ValueError("向量维度不一致")

dot_product = sum(v1 * v2 for v1, v2 in zip(vec1, vec2))

magnitude_vec1 = math.sqrt(sum(v1**2 for v1 in vec1))
magnitude_vec2 = math.sqrt(sum(v2**2 for v2 in vec2))

if magnitude_vec1 == 0 or magnitude_vec2 == 0:
return 0.0 # 避免除以零,通常认为零向量与任何向量都不相似

return dot_product / (magnitude_vec1 * magnitude_vec2)

# 示例
A = [1, 1, 0, 1, 0, 1]
B = [0, 1, 1, 0, 1, 1]

sim = cosine_similarity_manual(A, B)
print(f"手动计算的余弦相似度: {sim}") # 输出: 手动计算的余弦相似度: 0.5

8.2 使用 NumPy 和 SciPy

NumPy 提供了高效的向量和矩阵操作,而 SciPy 库则包含了距离计算函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import numpy as np
from scipy.spatial.distance import cosine # 注意:scipy.spatial.distance.cosine 返回的是余弦距离 (1 - 余弦相似度)

def cosine_similarity_numpy(vec1, vec2):
"""
使用 NumPy 计算两个向量的余弦相似度
Args:
vec1 (list/np.array): 向量1
vec2 (list/np.array): 向量2
Returns:
float: 余弦相似度
"""
vec1 = np.array(vec1)
vec2 = np.array(vec2)

# 避免零向量
if np.linalg.norm(vec1) == 0 or np.linalg.norm(vec2) == 0:
return 0.0

return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))

def cosine_similarity_scipy(vec1, vec2):
"""
使用 SciPy 计算两个向量的余弦相似度
Args:
vec1 (list/np.array): 向量1
vec2 (list/np.array): 向量2
Returns:
float: 余弦相似度
"""
# scipy.spatial.distance.cosine 返回的是余弦距离,即 1 - 余弦相似度
# 需要将其转换为余弦相似度
return 1 - cosine(vec1, vec2)

# 示例
A = [1, 1, 0, 1, 0, 1]
B = [0, 1, 1, 0, 1, 1]

sim_np = cosine_similarity_numpy(A, B)
print(f"NumPy 计算的余弦相似度: {sim_np}") # 输出: NumPy 计算的余弦相似度: 0.5

sim_scipy = cosine_similarity_scipy(A, B)
print(f"SciPy 计算的余弦相似度: {sim_scipy}") # 输出: SciPy 计算的余弦相似度: 0.5

# 针对零向量的测试
C = [0, 0, 0]
D = [1, 2, 3]
print(f"零向量与非零向量的余弦相似度 (手动): {cosine_similarity_manual(C, D)}") # 输出: 0.0
print(f"零向量与非零向量的余弦相似度 (NumPy): {cosine_similarity_numpy(C, D)}") # 输出: 0.0

九、总结

余弦相似度是一种强大且广泛使用的相似性度量,尤其适用于那些关注数据对象内在模式或趋势,而不太关注其绝对量级差异的场景。它通过计算向量之间的夹角余弦值来评估方向一致性,从而在文本分析、推荐系统、图像识别等高维数据应用中展现出卓越的性能。然而,在应用时也需注意其对向量长度信息的忽略以及对稀疏数据和负值的敏感性,结合具体业务场景选择最合适的相似度度量方法至关重要。