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)")4. 实际应用示例 #
4.1 文本相似度计算 #
余弦相似度在文本相似度计算中非常常用,可以比较两段文本的相似程度。
# 导入必要的库
import numpy as np
from collections import Counter
# 定义函数,将文本转换为向量(基于词频)
def text_to_vector(text):
"""
将文本转换为向量(简单的词频向量)
参数:
text: 输入文本(字符串)
返回:
词频向量(numpy数组)
"""
# 将文本转换为小写并按空格分割成单词列表
words = text.lower().split()
# 统计每个词出现的次数
word_counts = Counter(words)
# 获取所有唯一的词
unique_words = sorted(word_counts.keys())
# 创建向量,每个位置对应一个词的词频
vector = np.array([word_counts[word] for word in unique_words])
# 返回向量和词汇表(用于后续对齐)
return vector, unique_words
# 定义函数,对齐两个文本的向量(使它们维度相同)
def align_vectors(vec1, words1, vec2, words2):
"""
对齐两个文本向量,使它们具有相同的维度
参数:
vec1: 第一个文本的向量
words1: 第一个文本的词汇表
vec2: 第二个文本的向量
words2: 第二个文本的词汇表
返回:
对齐后的两个向量
"""
# 获取所有唯一的词(两个文本的并集)
all_words = sorted(set(words1 + words2))
# 创建对齐后的向量
aligned_vec1 = np.array([words1.count(word) if word in words1 else 0 for word in all_words])
aligned_vec2 = np.array([words2.count(word) if word in words2 else 0 for word in all_words])
# 返回对齐后的向量
return aligned_vec1, aligned_vec2
# 定义函数,计算文本相似度
def text_similarity(text1, text2):
"""
计算两个文本的余弦相似度
参数:
text1: 第一个文本
text2: 第二个文本
返回:
余弦相似度值
"""
# 将文本转换为向量
vec1, words1 = text_to_vector(text1)
vec2, words2 = text_to_vector(text2)
# 对齐向量
aligned_vec1, aligned_vec2 = align_vectors(vec1, words1, vec2, words2)
# 计算余弦相似度
dot_product = np.dot(aligned_vec1, aligned_vec2)
norm1 = np.linalg.norm(aligned_vec1)
norm2 = np.linalg.norm(aligned_vec2)
# 避免除以0
if norm1 == 0 or norm2 == 0:
return 0.0
# 返回相似度
return dot_product / (norm1 * norm2)
# 主程序:测试文本相似度计算
if __name__ == "__main__":
# 定义三个文本示例
text1 = "apple banana apple fruit"
text2 = "banana fruit orange"
text3 = "car bike vehicle"
# 计算文本1和文本2的相似度
sim_12 = text_similarity(text1, text2)
print(f"文本1: '{text1}'")
print(f"文本2: '{text2}'")
print(f"相似度: {sim_12:.4f}")
print(f"说明: 文本1和文本2都包含'banana'和'fruit',所以相似度较高")
# 计算文本1和文本3的相似度
sim_13 = text_similarity(text1, text3)
print(f"\n文本1: '{text1}'")
print(f"文本3: '{text3}'")
print(f"相似度: {sim_13:.4f}")
print(f"说明: 文本1和文本3没有共同词汇,所以相似度较低")4.2 推荐系统示例 #
在推荐系统中,可以使用余弦相似度来找到兴趣相似的用户。
# 导入必要的库
import numpy as np
# 定义简单的推荐系统类
class SimpleRecommender:
def __init__(self):
# 存储用户画像(每个用户用一个向量表示其偏好)
self.user_profiles = {}
def add_user_rating(self, user_id, item_ratings):
"""
添加用户的评分数据
参数:
user_id: 用户ID
item_ratings: 用户对各个物品的评分列表(向量)
"""
# 将评分列表转换为numpy数组并存储
self.user_profiles[user_id] = np.array(item_ratings, dtype=float)
def cosine_similarity(self, vec1, vec2):
"""
计算两个向量的余弦相似度
参数:
vec1: 第一个向量
vec2: 第二个向量
返回:
余弦相似度值
"""
# 计算点积
dot_product = np.dot(vec1, vec2)
# 计算模长
norm1 = np.linalg.norm(vec1)
norm2 = np.linalg.norm(vec2)
# 避免除以0
if norm1 == 0 or norm2 == 0:
return 0.0
# 返回相似度
return dot_product / (norm1 * norm2)
def recommend_similar_users(self, target_user, top_k=3):
"""
推荐与目标用户兴趣相似的其他用户
参数:
target_user: 目标用户ID
top_k: 返回前k个最相似的用户
返回:
相似用户列表,每个元素是(相似度, 用户ID)的元组
"""
# 检查目标用户是否存在
if target_user not in self.user_profiles:
return []
# 获取目标用户的向量
target_vector = self.user_profiles[target_user]
# 存储所有相似度
similarities = []
# 遍历所有用户
for user_id, user_vector in self.user_profiles.items():
# 跳过目标用户自己
if user_id != target_user:
# 计算相似度
sim = self.cosine_similarity(target_vector, user_vector)
# 添加到列表
similarities.append((sim, user_id))
# 按相似度降序排序
similarities.sort(reverse=True)
# 返回前k个最相似的用户
return similarities[:top_k]
# 主程序:测试推荐系统
if __name__ == "__main__":
# 创建推荐系统实例
recommender = SimpleRecommender()
# 添加用户评分数据
# 假设有5个物品(比如:电影、书籍等),每个用户对这5个物品的评分
recommender.add_user_rating('user1', [5, 3, 0, 1, 4]) # 用户1的评分
recommender.add_user_rating('user2', [4, 2, 1, 2, 5]) # 用户2的评分
recommender.add_user_rating('user3', [0, 1, 5, 4, 2]) # 用户3的评分
# 为用户1推荐相似用户
similar_users = recommender.recommend_similar_users('user1', top_k=2)
# 打印结果
print("为用户1推荐的相似用户:")
for i, (sim, user_id) in enumerate(similar_users, 1):
print(f" 第 {i} 名: 用户 {user_id}, 相似度 {sim:.4f}")4.3 图像特征相似度 #
在图像检索中,可以使用余弦相似度来比较图像的特征向量。
# 导入必要的库
import numpy as np
# 定义函数,在图像数据库中查找相似图像
def find_similar_images(query_features, image_database, top_k=5):
"""
在图像数据库中查找与查询图像最相似的图像
参数:
query_features: 查询图像的特征向量
image_database: 图像数据库(字典,key是图像ID,value是特征向量)
top_k: 返回前k个最相似的图像
返回:
相似图像列表,每个元素是(相似度, 图像ID)的元组
"""
# 存储所有相似度
similarities = []
# 遍历数据库中的每张图像
for img_id, features in image_database.items():
# 计算余弦相似度
dot_product = np.dot(query_features, features)
norm_query = np.linalg.norm(query_features)
norm_features = np.linalg.norm(features)
# 避免除以0
if norm_query == 0 or norm_features == 0:
sim = 0.0
else:
sim = dot_product / (norm_query * norm_features)
# 添加到列表
similarities.append((sim, img_id))
# 按相似度降序排序
similarities.sort(reverse=True)
# 返回前k个最相似的图像
return similarities[:top_k]
# 主程序:测试图像相似度查找
if __name__ == "__main__":
# 模拟图像数据库(实际应用中,这些特征向量来自深度学习模型)
# 假设每张图像用128维向量表示其特征
image_database = {
'img1': np.random.rand(128),
'img2': np.random.rand(128),
'img3': np.random.rand(128),
'img4': np.random.rand(128),
'img5': np.random.rand(128),
}
# 创建查询图像的特征向量(与img1相似)
query_features = image_database['img1'] * 1.5 # 方向相同,大小不同
# 查找相似图像
similar_images = find_similar_images(query_features, image_database, top_k=3)
# 打印结果
print("查询图像的特征向量(与img1方向相同)")
print(f"\n找到最相似的 3 张图像:")
for i, (sim, img_id) in enumerate(similar_images, 1):
print(f" 第 {i} 名: {img_id}, 相似度 {sim:.4f}")5. 与欧氏距离的对比 #
5.1 核心区别 #
欧氏距离关注的是向量之间的"直线距离",受向量大小影响。 余弦相似度关注的是向量之间的"角度",不受向量大小影响。
5.2 对比示例 #
让我们通过具体例子来看两者的区别:
# 导入必要的库
import numpy as np
# 定义函数,计算欧氏距离
def euclidean_distance(a, b):
"""
计算两个向量的欧氏距离
参数:
a: 第一个向量
b: 第二个向量
返回:
欧氏距离值
"""
# 计算两个向量的差
diff = a - b
# 计算差的平方和
squared_diff = np.sum(diff ** 2)
# 返回平方根,即欧氏距离
return np.sqrt(squared_diff)
# 定义函数,计算余弦相似度
def cosine_similarity(a, b):
"""
计算两个向量的余弦相似度
参数:
a: 第一个向量
b: 第二个向量
返回:
余弦相似度值
"""
# 计算点积
dot_product = np.dot(a, b)
# 计算模长
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
# 避免除以0
if norm_a == 0 or norm_b == 0:
return 0.0
# 返回相似度
return dot_product / (norm_a * norm_b)
# 主程序:对比欧氏距离和余弦相似度
if __name__ == "__main__":
# 示例1:方向相同但大小不同的向量
a = np.array([1, 1])
b = np.array([2, 2]) # a的2倍,方向相同
c = np.array([100, 100]) # a的100倍,方向相同
print("示例1:方向相同但大小不同的向量")
print(f"向量A: {a}")
print(f"向量B: {b} (A的2倍)")
print(f"向量C: {c} (A的100倍)")
print(f"\nA和B的余弦相似度: {cosine_similarity(a, b):.4f} (接近1.0,因为方向相同)")
print(f"A和B的欧氏距离: {euclidean_distance(a, b):.4f}")
print(f"A和C的余弦相似度: {cosine_similarity(a, c):.4f} (接近1.0,因为方向相同)")
print(f"A和C的欧氏距离: {euclidean_distance(a, c):.4f} (很大,因为大小差异大)")
print("\n结论:余弦相似度不受大小影响,欧氏距离受大小影响")
# 示例2:大小相同但方向不同的向量
d = np.array([3, 4]) # 模长为5
e = np.array([4, 3]) # 模长也为5,但方向不同
print(f"\n示例2:大小相同但方向不同的向量")
print(f"向量D: {d} (模长: {np.linalg.norm(d):.2f})")
print(f"向量E: {e} (模长: {np.linalg.norm(e):.2f})")
print(f"D和E的余弦相似度: {cosine_similarity(d, e):.4f} (小于1.0,因为方向不同)")
print(f"D和E的欧氏距离: {euclidean_distance(d, e):.4f}")
print("\n结论:两者都能区分方向不同的向量")5.3 如何选择? #
使用余弦相似度的情况:
- 关注向量的方向而非大小
- 文本相似度(文档长度不同)
- 推荐系统(用户行为强度不同)
- 稀疏数据(很多值为0)
使用欧氏距离的情况:
- 需要考虑向量的大小
- 物理距离(如坐标点之间的距离)
- 数值差异很重要的情况
6.常见问题和注意事项 #
6.1 零向量处理 #
零向量(所有元素都是0)的模长为0,会导致除以0的错误。需要特殊处理。
# 导入必要的库
import numpy as np
# 定义函数,安全地计算余弦相似度(处理零向量)
def safe_cosine_similarity(a, b):
"""
安全地计算余弦相似度,处理零向量等特殊情况
参数:
a: 第一个向量
b: 第二个向量
返回:
余弦相似度值,如果出现零向量则返回0.0
"""
# 计算模长
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
# 如果任一向量为零向量,返回0.0
if norm_a == 0 or norm_b == 0:
return 0.0
# 计算点积
dot_product = np.dot(a, b)
# 计算并返回余弦相似度
return dot_product / (norm_a * norm_b)
# 主程序:测试零向量处理
if __name__ == "__main__":
# 正常向量
vec1 = np.array([1, 2, 3])
vec2 = np.array([4, 5, 6])
print(f"正常向量相似度: {safe_cosine_similarity(vec1, vec2):.4f}")
# 零向量
vec3 = np.array([0, 0, 0])
vec4 = np.array([1, 2, 3])
print(f"零向量相似度: {safe_cosine_similarity(vec3, vec4):.4f} (应该为0.0)")6.2 向量维度必须相同 #
计算余弦相似度时,两个向量的维度必须相同,否则无法计算点积。
# 导入必要的库
import numpy as np
# 定义函数,检查并计算余弦相似度
def cosine_similarity_with_check(a, b):
"""
计算余弦相似度,并检查向量维度
参数:
a: 第一个向量
b: 第二个向量
返回:
余弦相似度值
抛出:
ValueError: 如果向量维度不匹配
"""
# 转换为numpy数组
a = np.array(a)
b = np.array(b)
# 检查维度是否相同
if a.shape != b.shape:
raise ValueError(f"向量维度不匹配: {a.shape} vs {b.shape}")
# 计算相似度
norm_a = np.linalg.norm(a)
norm_b = np.linalg.norm(b)
if norm_a == 0 or norm_b == 0:
return 0.0
return np.dot(a, b) / (norm_a * norm_b)
# 主程序:测试维度检查
if __name__ == "__main__":
# 维度相同的向量
vec1 = np.array([1, 2, 3])
vec2 = np.array([4, 5, 6])
print(f"维度相同,相似度: {cosine_similarity_with_check(vec1, vec2):.4f}")
# 维度不同的向量(会报错)
vec3 = np.array([1, 2])
vec4 = np.array([3, 4, 5])
try:
result = cosine_similarity_with_check(vec3, vec4)
except ValueError as e:
print(f"捕获错误: {e}")6.3 归一化的影响 #
对向量进行归一化(使模长为1)后,余弦相似度的计算可以简化为点积。
# 导入必要的库
import numpy as np
# 定义函数,归一化向量
def normalize_vector(v):
"""
将向量归一化为单位向量(模长为1)
参数:
v: 输入向量
返回:
归一化后的向量
"""
# 计算模长
norm = np.linalg.norm(v)
# 如果模长为0,返回原向量(避免除以0)
if norm == 0:
return v
# 返回归一化后的向量
return v / norm
# 定义函数,使用归一化向量计算余弦相似度
def cosine_similarity_normalized(a, b):
"""
使用归一化向量计算余弦相似度(更高效)
参数:
a: 第一个向量
b: 第二个向量
返回:
余弦相似度值
"""
# 归一化向量
a_norm = normalize_vector(a)
b_norm = normalize_vector(b)
# 归一化后,余弦相似度就是点积
return np.dot(a_norm, b_norm)
# 主程序:对比两种方法
if __name__ == "__main__":
# 定义两个向量
vec1 = np.array([1, 2, 3])
vec2 = np.array([4, 5, 6])
# 方法1:标准方法
dot_product = np.dot(vec1, vec2)
norm1 = np.linalg.norm(vec1)
norm2 = np.linalg.norm(vec2)
similarity1 = dot_product / (norm1 * norm2)
# 方法2:归一化后计算
similarity2 = cosine_similarity_normalized(vec1, vec2)
# 打印结果
print(f"方法1(标准方法): {similarity1:.6f}")
print(f"方法2(归一化方法): {similarity2:.6f}")
print(f"两种方法结果相同: {np.isclose(similarity1, similarity2)}")
print("\n说明:归一化后,余弦相似度 = 点积,计算更简单")7. 总结 #
7.1 核心要点 #
- 余弦相似度关注方向:不受向量大小影响,只关注两个向量的夹角
- 取值范围:[-1, 1],1表示最相似,-1表示最不相似
- 计算公式:点积除以两个向量模长的乘积
- 适用场景:文本相似度、推荐系统、图像检索等
7.2 优势 #
- 不受向量大小影响:适合比较不同长度的文档或不同强度的用户行为
- 对稀疏数据友好:适合处理很多值为0的稀疏向量
- 计算效率高:使用numpy可以高效计算
- 直观易懂:结果范围明确,易于理解和解释
7.3 适用场景 #
适合使用余弦相似度:
- 文本相似度计算
- 推荐系统(用户-物品矩阵)
- 图像特征比较
- 任何需要衡量方向相似度的任务
不适合使用余弦相似度:
- 需要考虑向量大小的任务(如物理距离)
- 向量包含负值且方向不重要的情况
- 需要精确数值差异的场景