余弦相似度(Cosine Similarity)完全指南
向量相似度计算的基石,从数学推导到 NumPy 实现,一文讲透。
一、引言
在 RAG(检索增强生成)、推荐系统、语义搜索等场景中,我们经常需要回答一个问题:
给定一个用户查询和一堆知识库文档,如何找到与用户意图最相关的那些文档?
答案的核心就是向量相似度计算。大模型将文本转换为高维向量(Embedding),然后我们通过计算向量之间的相似度来度量语义相关性。
余弦相似度是其中最常用、最直观的方法之一。
二、基础概念
2.1 向量的点积(Dot Product)
点积是两个向量对应分量相乘再求和的结果:
PYTHON
a · b = Σ aᵢ × bᵢ点积衡量的是一种"方向上的对齐程度":两个向量方向越一致,点积越大。
PYTHON
# 点积的手动实现
dot_product = sum(a * b for a, b in zip(vec_a, vec_b))
# NumPy 实现
dot_product = np.dot(vec_a, vec_b)2.2 L2 范数(L2 Norm / Euclidean Norm)

L2 范数就是向量的长度(模长),也叫欧几里得范数。计算公式:
直观理解:从原点 (0,0,0) 到向量终点 (v₁, v₂, v₃) 的直线距离。
PYTHON
def l2_norm(vector):
"""手动计算 L2 范数"""
return math.sqrt(sum(v ** 2 for v in vector))
# NumPy
norm = np.linalg.norm(vector, ord=2) # ord=2 表示 L2 范数示例:向量 [3, 4, 0] 的 L2 范数 =
2.3 欧氏距离(Euclidean Distance)

欧氏距离就是两个向量终点之间的直线距离:
PYTHON
dist(a, b) = ‖a - b‖₂PYTHON
def euclidean_distance(a, b):
return np.linalg.norm(a - b, ord=2)
# 示例
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
# 输出: 5.196概念 | 公式 | 含义 |
|---|---|---|
L2 范数 | ‖v‖₂ | 向量到原点的距离 |
欧氏距离 | ‖a - b‖₂ | 两个向量终点之间的距离 |
点积 | a · b | 方向对齐程度的度量 |
2.4 L2 归一化(L2 Normalization)
将向量缩放到长度为 1(单位向量),只保留方向信息:
PYTHON
def l2_normalize(vector):
norm = np.linalg.norm(vector, ord=2)
return vector / norm if norm != 0 else vector归一化后 ‖v_normalized‖₂ ≈ 1.0。
2.5 余弦相似度(Cosine Similarity)⭐
余弦相似度衡量的是两个向量之间的夹角余弦值,只关心方向是否一致,不关心长度:
取值范围:[-1, 1]
值 | 含义 |
|---|---|
1 | 方向完全相同(夹角 0°) |
0 | 正交(夹角 90°,不相关) |
-1 | 方向完全相反(夹角 180°) |
为什么用余弦相似度而不是欧氏距离?
在 Embedding 空间中,向量的长度可能受文本长度影响,而
余弦相似度只关心方向——即语义的"指向"。两个相似语义的向量即使长度不同,夹角也会很小。
三、代码实现
3.1 纯 Python 手写版
不依赖任何第三方库,适合理解原理:
PYTHON
import math
def cosine_similarity(vec_a, vec_b):
if len(vec_a) != len(vec_b):
raise ValueError("两个向量的维度必须相同!")
# 1. 分子:点积
dot_product = sum(a * b for a, b in zip(vec_a, vec_b))
# 2. 分母:各自的模长
norm_a = math.sqrt(sum(a ** 2 for a in vec_a))
norm_b = math.sqrt(sum(b ** 2 for b in vec_b))
# 3. 防止分母为 0
if norm_a == 0 or norm_b == 0:
return 0.0
# 4. 余弦值
return dot_product / (norm_a * norm_b)
# 测试
user_query = [0.1, 0.8, 0.2] # "去草原怎么走?"
knowledge_1 = [0.12, 0.78, 0.18] # "前往呼伦贝尔草原的路线如下..."
knowledge_2 = [0.9, 0.1, -0.4] # "如何办理退款业务..."
print(f"与知识库1的相似度: {cosine_similarity(user_query, knowledge_1):.4f}")
print(f"与知识库2的相似度: {cosine_similarity(user_query, knowledge_2):.4f}")输出:
PLAINTEXT
与知识库1的相似度: 0.9994
与知识库2的相似度: 0.10943.2 NumPy 生产版
实际项目中推荐使用,简洁高效:
PYTHON
import numpy as np
def cosine_sim_numpy(v1, v2):
vec1 = np.array(v1)
vec2 = np.array(v2)
return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2))3.3 sklearn 一行版
PYTHON
from sklearn.metrics.pairwise import cosine_similarity
# 注意:sklearn 期望输入为二维矩阵
sim = cosine_similarity([[0.1, 0.8, 0.2]], [[0.12, 0.78, 0.18]])
print(sim[0][0]) # 0.99943.4 批量计算版(推荐用于 RAG)
一次性计算一个查询向量与多个文档向量的相似度:
PYTHON
def cosine_similarity_batch(X, y):
"""
X: [n_samples, dim] — 文档向量矩阵
y: [dim] — 查询向量
"""
X_norm = X / np.linalg.norm(X, axis=1, keepdims=True)
y_norm = y / np.linalg.norm(y)
return np.dot(X_norm, y_norm)
# 示例
matrix = np.array([[1, 2], [3, 4], [5, 6]])
query = np.array([1, 1])
sims = cosine_similarity_batch(matrix, query)
print(sims) # [0.949, 0.990, 0.996]四、核心函数详解
4.1 np.array()
将 Python 列表转换为 NumPy 数组,支持向量化运算。
PYTHON
v = np.array([1, 2, 3]) # shape: (3,)
m = np.array([[1, 2], [3, 4]]) # shape: (2, 2)NumPy 数组支持广播、切片、逐元素运算等,效率远高于纯 Python 循环。
4.2 np.dot(a, b)
向量点积(内积)。
PYTHON
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
np.dot(a, b) # 1*4 + 2*5 + 3*6 = 32对于二维矩阵,np.dot 执行矩阵乘法。
np.linalg.norm(x, ord=None, axis=None, keepdims=False)
计算向量或矩阵的范数。
参数 | 含义 | 常用值 |
|---|---|---|
| 范数类型 | 2(L2/欧氏,默认), 1(L1) |
| 计算哪个轴 | 1 按行, 0 按列 |
| 是否保持维度 | True 方便广播运算 |
PYTHON
v = np.array([3, 4, 0])
np.linalg.norm(v, ord=2) # 5.0(L2 范数)
# 矩阵按行归一化
matrix = np.array([[1, 2], [3, 4]])
np.linalg.norm(matrix, axis=1, keepdims=True)
# [[2.236], [5.0]] 每行的 L2 范数np.sum(a, axis=None)
对数组元素求和。
PYTHON
v = np.array([1, 2, 3])
np.sum(v) # 6(全部求和)
np.sum(v ** 2) # 14(先求平方再求和)np.sqrt(x)
逐元素开平方。
PYTHON
np.sqrt(25) # 5.0
np.sqrt(np.array([1, 4, 9])) # [1, 2, 3]五、三种实现对比
方式 | 行数 | 依赖 | 性能 | 适用场景 |
|---|---|---|---|---|
纯 Python | ~10 | 无 | 慢 | 教学/理解原理 |
NumPy | 1 | numpy | 极快 | 生产环境 |
sklearn | 1 | scikit-learn | 极快 | 快速原型 |
六、总结
余弦相似度 = 点积 ÷ (L2范数 × L2范数),取值范围 [-1, 1]
只关心方向,不关心长度,适合语义匹配
先对向量做 L2 归一化,再求点积,结果等价于余弦相似度
L2 范数是向量的欧氏长度,欧氏距离是两个向量终点的直线距离
纯 Python 适合理解原理,NumPy 适合生产,sklearn 适合快速验证

