在数据科学和机器学习的日常工作中,我们经常需要衡量两个对象之间的相似度。比如,在推荐系统中判断两个用户的兴趣是否相近,或者在自然语言处理(NLP)中比较两篇文章的主题是否一致。这时,单纯的距离度量往往不够准确,我们需要一种能忽略“大小”而关注“方向”的指标——这就是余弦相似度。
在今天的文章中,我们将深入探讨如何在 Python 中计算余弦相似度。我们不仅会回顾它的数学原理,还会通过丰富的代码示例,带你领略从基础实现到处理高维矩阵的各种场景。无论你是使用 NumPy 这种基础库,还是想探索 2026 年更高效的工具,这篇文章都将为你提供实用的参考。
什么是余弦相似度?
简单来说,余弦相似度衡量的是两个向量在空间中的夹角。我们可以把这两个向量想象成空间中的两条线段,如果它们指向完全相同的方向,余弦相似度就是 1;如果垂直,则是 0;如果方向完全相反,则是 -1。
这种特性的优势在于:它对向量的长度(模长)不敏感。这意味着,即使两个向量的数值差异很大(比如一个文本比另一个文本长得多),只要它们包含关键词的比例相似,余弦相似度就会认为它们是相似的。这正是为什么它在文本分析(如 TF-IDF 向量空间)中占据统治地位的原因。
数学上,两个非零向量 A 和 B 的余弦相似度定义为它们点积与模长乘积的比值:
$$\text{Cosine Similarity} = \frac{A \cdot B}{\
\
}$$
准备工作
在开始写代码之前,请确保你的环境中安装了 numpy。我们将主要依赖它来进行高效的向量化运算。你可以通过以下命令安装:
pip install numpy
现在,让我们从最基础的场景开始动手实践。
示例 1:基础计算 —— 两个一维向量
首先,我们来看看如何计算两个简单的一维向量之间的相似度。这是理解整个计算流程的基石。
在这个场景中,我们将手动实现点积和模长的计算,以帮助你理解背后的数学逻辑。
import numpy as np
from numpy.linalg import norm
# 定义两个向量
# 向量 A
A = np.array([2, 1, 2, 3, 2, 9])
# 向量 B
B = np.array([3, 4, 2, 4, 5, 5])
# 计算点积
# 对应位置元素相乘再相加
dot_product = np.dot(A, B)
# 计算模长
# norm 函数默认计算 L2 范数(欧几里得距离)
norm_A = norm(A)
norm_B = norm(B)
cosine = dot_product / (norm_A * norm_B)
print(f"向量 A 和 B 的余弦相似度为: {cosine}")
代码解析:
- INLINECODE4ffc593f: 这是 NumPy 中计算点积的核心函数。在一维数组中,它执行的是 $A1 \times B1 + A2 \times B_2 + \dots$ 的操作。
-
norm(A): 这个函数计算向量的长度。你可以把它想象成从原点到向量终点的直线距离。 - 结果解读: 输出结果
0.818...表示这两个向量非常相似,它们之间的夹角很小。
示例 2:一对多计算 —— 一个向量对比一组向量
在实际应用中,我们经常需要用一个向量去对比一组向量。比如,你有一个用户的兴趣向量(A),你想在数据库中找到与该用户最相似的其他用户(矩阵 B 中的每一行)。
我们可以利用 NumPy 的广播机制来优雅地解决这个问题,而不需要编写笨拙的循环。
import numpy as np
from numpy.linalg import norm
# 定义一组向量 (3x3 矩阵)
# 每一行代表一个独立的向量
A = np.array([
[2, 1, 2],
[3, 2, 9],
[-1, 2, -3]
])
# 定义一个单独的查询向量
B = np.array([3, 4, 2])
# 计算点积
# 这里会发生广播:矩阵 A (3,3) 与 向量 B (3,) 进行运算
# 等同于计算 A 中每一行与 B 的点积
dot_products = np.dot(A, B)
# 计算模长
# axis=1 表示沿着行的方向计算 L2 范数,得到一个 (3,) 的数组
norm_A = norm(A, axis=1)
# B 是向量,计算其标量模长
norm_B = norm(B)
# 除法操作同样利用了广播机制
cosine_similarities = dot_products / (norm_A * norm_B)
print("A 中每一行与 B 的相似度:", cosine_similarities)
深入理解:
- 广播机制: 注意这里 INLINECODE78aa5eea 返回的是一个数组,而 INLINECODE81934d1c 是一个标量。在除法运算时,NumPy 会自动将
norm_B扩展以匹配数组的大小,这使得我们可以一次性计算多个相似度值,效率极高。 - 负值的意义: 在这个例子中,你可能注意到结果包含负值(例如
-0.049)。这意味着对应的向量与 B 的方向夹角大于 90 度,即它们在某些特征上是“相反”的。在文本分析中,这可能意味着语义的排斥。
示例 3:逐行计算 —— 两个矩阵之间的相似度
接下来,我们处理更复杂的情况:两个包含多个样本的矩阵,我们要计算它们对应行(即第 0 行对应第 0 行,第 1 行对应第 1 行…)之间的相似度。
这在批量处理数据时非常常见,比如比较昨天和今天两组用户的行为日志变化。
import numpy as np
from numpy.linalg import norm
# 定义矩阵 A 和 B,行数必须相同
A = np.array([
[1, 2, 2],
[3, 2, 2],
[-2, 1, -3]
])
B = np.array([
[4, 2, 4],
[2, -2, 5],
[3, 4, -4]
])
# 步骤 1:逐元素相乘
# A * B 对应位置元素相乘
element_wise_mult = A * B
# 步骤 2:按行求和得到点积
# axis=1 表示压缩行,将每一行的乘积相加得到一个标量
dot_products = np.sum(element_wise_mult, axis=1)
# 步骤 3:计算各自的模长
# 同样按行计算
norm_A = norm(A, axis=1)
norm_B = norm(B, axis=1)
# 步骤 4:计算最终相似度
cosine_similarities = dot_products / (norm_A * norm_B)
print("逐行对应的余弦相似度:", cosine_similarities)
进阶技巧:处理稀疏矩阵与 Scikit-Learn
虽然 NumPy 非常强大,但在处理大规模文本数据(如成千上万个特征的 TF-IDF 矩阵)时,为了节省内存,我们通常使用稀疏矩阵(Scipy CSR 或 CSC 格式)。直接使用 NumPy 的点积操作可能会让内存爆炸,或者计算效率低下。
在实际的工程项目中,我们更倾向于使用 INLINECODE387616a0 库中的 INLINECODE2145af90 函数。它不仅代码更简洁,而且针对稀疏矩阵进行了深度优化。
# 这是一个更符合生产环境的示例思路
from sklearn.metrics.pairwise import cosine_similarity
import numpy as np
# 模拟数据
# 比如这是两篇文档的 TF-IDF 向量
vector_a = np.array([[1, 0, 1, 0]])
vector_b = np.array([[0, 1, 1, 1]])
# 使用 Scikit-Learn 计算
# 注意输入必须是 2D 数组
sim = cosine_similarity(vector_a, vector_b)
print(f"使用 Scikit-Learn 计算的相似度: {sim[0][0]}")
实用见解: Scikit-learn 的实现非常健壮,它内部处理了数值稳定性的问题(比如处理全零向量),并且支持 GPU 加速(通过其他后端),因此在你构建实际的推荐系统或搜索引擎时,请优先考虑使用它而不是手写公式。
2026 年技术视野:AI 原生与高性能计算
进入 2026 年,我们的开发方式正在经历一场深刻的变革。随着 Agentic AI 和 Vibe Coding(氛围编程) 的兴起,我们不再只是单纯地编写代码,而是与 AI 结对编程。在计算余弦相似度时,我们也需要考虑这一新的背景。
#### 现代开发范式:Vibe Coding 与 AI 辅助
在现代开发环境中(如使用 Cursor、Windsurf 或 GitHub Copilot),我们经常将复杂的数学运算逻辑交给 AI 生成,而人类专家则专注于架构设计和业务逻辑。
最佳实践: 当我们让 AI 生成余弦相似度代码时,我们会明确要求它处理“除以零”的边界情况,或者要求它生成基于 INLINECODE7c282f67 的向量化版本而非 INLINECODEbc2c1805 循环版本。这种协同工作方式让我们能快速构建原型,并在 AI 生成的代码基础上进行性能调优。
#### 面向海量数据的向量检索
在 2026 年,简单的内存计算已经无法满足海量向量(十亿级)的检索需求。当我们需要在一个拥有数百万条记录的数据库中寻找 Top-K 相似向量时,单纯计算全量余弦相似度是极其低效的。
我们应当转向 向量数据库(如 Milvus, Pinecone, 或 Elasticsearch 的 KNN 搜索)。这些工具内部使用 HNSW(Hierarchical Navigable Small World) 算法或 近似最近邻(ANN) 搜索,能在毫秒级完成检索,且精度损失极小。
Python 集成示例:
假设我们使用 faiss(Facebook AI Similarity Search)库进行高效检索,这是处理大规模向量的工业标准。
import faiss
import numpy as np
# 模拟一个大规模数据库:100,000 个向量,维度为 128
dimension = 128
nb_vectors = 100000
# 生成随机数据库向量
# 注意:Faiss 要求 L2 归一化才能直接用内积模拟余弦相似度
# 或者我们可以直接归一化向量,然后使用内积
database_vectors = np.random.random((nb_vectors, dimension)).astype(‘float32‘)
# 关键步骤:归一化向量
# 归一化后,A dot B 等价于 Cosine Similarity
faiss.normalize_L2(database_vectors)
# 创建索引 - 使用 Inner Product (内积) 来计算归一化后向量的余弦相似度
index = faiss.IndexFlatIP(dimension)
index.add(database_vectors)
# 查询向量(假设我们要找最相似的 5 个)
query_vector = np.random.random((1, dimension)).astype(‘float32‘)
faiss.normalize_L2(query_vector)
# 搜索 k 个最相似的向量
k = 5
distances, indices = index.search(query_vector, k)
print(f"最相似的向量索引: {indices}")
print(f"对应的余弦相似度分数: {distances}")
生产级代码的边界情况与容灾
在我们最近的一个推荐系统重构项目中,我们遇到了一个棘手的问题:由于数据清洗流程的 Bug,导致部分用户特征向量变成了“全零向量”。如果不加处理,计算模长时除以零会导致整个服务崩溃。
生产级解决方案:
def safe_cosine_similarity(A, B, epsilon=1e-9):
"""
计算余弦相似度,包含除零保护和数值稳定性处理。
Args:
A, B: 输入向量
epsilon: 一个极小值,用于防止除以零
"""
dot_product = np.dot(A, B)
norm_A = np.linalg.norm(A)
norm_B = np.linalg.norm(B)
# 检查模长是否接近零
if norm_A < epsilon or norm_B < epsilon:
return 0.0 # 或者根据业务逻辑返回特定值
return dot_product / (norm_A * norm_B)
常见陷阱与注意事项
在计算余弦相似度时,有几个常见的“坑”是你一定要避免的:
- 除以零错误: 如果你的数据中存在全零向量(例如一个空的文档,或者一个没有任何交互的用户),它的模长
norm将是 0。直接代入公式会导致程序崩溃。
* 解决方案: 在计算前,务必检查分母是否为 0,或者在数据预处理阶段过滤掉全零向量。Scikit-learn 等成熟库通常已经内置了这种保护机制。
- 数据标准化: 虽然余弦相似度对长度不敏感,但它假设数据的各个维度已经处于同一量级。如果你的某个特征数值范围是 0-1,另一个是 0-1000,大数值的特征可能会主导余弦值。
* 最佳实践: 在计算前,通常建议先对特征进行标准化或归一化处理。
- 维度灾难: 在极高维的空间中(比如数万维的文本向量),计算效率会成为瓶颈。
* 优化建议: 考虑使用随机投影 或 降维技术(如 PCA 或 SVD)先降低向量维度,然后再计算余弦相似度,这往往能在保持精度的同时大幅提升速度。
性能优化策略
如果你需要处理百万级别的向量对,单纯的 Python 循环甚至是 NumPy 的某些操作都可能太慢。
- 向量化操作: 永远不要使用 Python 的 INLINECODE0ca9108a 循环去遍历数组计算点积,如我们在上面示例中展示的,利用 INLINECODE5189f00b 或
np.sum(axis=1)是利用 CPU 指令集加速的关键。 - 利用矩阵乘法: 如果你需要计算矩阵 A 中所有向量与矩阵 B 中所有向量的相似度(NxM 结果),直接使用
A @ B.T计算点积矩阵,然后再除以模长矩阵,这是最线代范的做法,速度最快。 - 使用近似算法: 对于超大规模数据,可以探索“局部敏感哈希”算法。它不直接计算相似度,而是将相似的向量以高概率哈希到同一个桶中,从而在亚线性时间内找到近似最近邻。
总结
在这篇文章中,我们全面探讨了如何在 Python 中计算余弦相似度。我们涵盖了从基础的 NumPy 公式实现,到处理多维矩阵的各种技巧,甚至提到了生产环境中的最佳实践和 2026 年的技术趋势。
关键要点回顾:
- 余弦相似度关注的是方向而非大小,非常适合文本和推荐系统。
- 使用 INLINECODE8762dcb7 的线性代数模块(INLINECODEb7ca8750,
norm)可以高效实现计算。 - 在处理大规模或稀疏数据时,优先考虑 INLINECODE4f53f07e 或 INLINECODE52c50868 等高度优化的库,并注意处理全零向量带来的除零风险。
- 面对 2026 年的复杂应用场景,拥抱 AI 辅助编程和向量数据库检索是提升开发效率和系统性能的关键。
希望这些示例和解释能帮助你在实际项目中更好地应用这一强大的算法。现在,你已经掌握了工具,不妨在你的数据集上试一试,看看能发现哪些隐藏的相似关系吧!