3. 相似度计算:余弦相似度、皮尔逊相关系数、Jaccard相似度、修正余弦相似度、相似度计算实战
相似度计算,说白了就是给两个用户或两个物品打分,看它们到底有多像。我在做推荐系统的头两年,踩过不少相似度计算的坑。嗯,今天咱们就把这块彻底聊透。
3.1 余弦相似度:最直观的"方向一致"
余弦相似度是我个人最常用的入门方法。它衡量的是两个向量在方向上的相似程度,跟向量的长度没关系。
公式很简单:
cosine_similarity(A, B) = (A · B) / (||A|| × ||B||)
举个例子。用户A对三部电影的评分是[4, 5, 3],用户B是[4, 4, 3]。你想想看,这两个人的口味方向基本一致,余弦值会接近1。
核心要点:余弦相似度只关心方向,不关心绝对数值。适合评分尺度不一致的场景。
我在项目中遇到过一个问题:两个用户都只看了两部电影,评分分别是[5, 5]和[1, 1]。余弦相似度算出来是1.0,完全相似。但实际呢?一个给高分,一个给低分,口味完全相反。这就是余弦相似度的局限性——它忽略了评分的绝对高低。
3.2 皮尔逊相关系数:去中心化的余弦
皮尔逊相关系数,其实就是把数据先减去各自的均值,再算余弦相似度。说白了,它关注的是"相对于平均水平的偏差"是否一致。
pearson(A, B) = Σ((A_i - μ_A) × (B_i - μ_B)) / (σ_A × σ_B)
刚才那个[5,5]和[1,1]的例子,用皮尔逊算出来会是0。为什么?因为两个用户的评分波动都是0,没有相关性。这才合理嘛。
我的建议:如果用户评分普遍偏高或偏低,用皮尔逊比余弦更靠谱。我习惯在用户-物品评分矩阵稀疏时优先考虑皮尔逊。
我曾经在一个电商项目中,发现余弦相似度推荐的结果总是偏向"老好人"用户(什么都给高分)。换成皮尔逊后,推荐质量明显提升。避坑指南:皮尔逊要求每个用户至少有2个共同评分项,否则分母为0。
3.3 Jaccard相似度:适合二值数据
Jaccard相似度,我一般用在"买了/没买"、"点击/未点击"这种场景。它不考虑评分高低,只看交集占并集的比例。
Jaccard(A, B) = |A ∩ B| / |A ∪ B|
用户A买了5件商品,用户B买了8件,共同买了3件。Jaccard就是3/(5+8-3)=0.3。
注意:Jaccard对数据稀疏非常敏感。如果两个用户都只买了1件商品,恰好相同,Jaccard=1.0。这其实是个假阳性。
我记得在做一个新闻推荐系统时,用Jaccard算用户相似度,结果发现大量用户相似度为0。后来一查,用户阅读的文章重叠率极低。这时候Jaccard就不太合适了。
3.4 修正余弦相似度:解决评分偏差
修正余弦相似度,是我在评分预测任务中的首选。它跟普通余弦的区别在于:减去了物品的平均分,而不是用户的平均分。
adjusted_cosine(A, B) = Σ((A_i - μ_i) × (B_i - μ_i)) / (√Σ(A_i - μ_i)² × √Σ(B_i - μ_i)²)
为什么要这么做?你想想看,有些电影天生评分就高(比如经典大片),有些电影评分普遍低(比如小众文艺片)。如果不减去物品均值,那些高分电影会主导相似度计算。
实战经验:在MovieLens数据集上,修正余弦相似度比普通余弦的RMSE低约5%-8%。我建议在做物品协同过滤时,优先尝试修正余弦。
3.5 相似度计算实战
好了,理论说完了,咱们直接上代码。这是我个人常用的一个相似度计算工具类:
import numpy as np
from scipy.stats import pearsonr
def cosine_similarity(vec1, vec2):
"""余弦相似度"""
dot = np.dot(vec1, vec2)
norm = np.linalg.norm(vec1) * np.linalg.norm(vec2)
return dot / norm if norm != 0 else 0
def pearson_similarity(vec1, vec2):
"""皮尔逊相关系数"""
mask = ~np.isnan(vec1) & ~np.isnan(vec2)
if np.sum(mask) < 2:
return 0
return pearsonr(vec1[mask], vec2[mask])[0]
def jaccard_similarity(set1, set2):
"""Jaccard相似度"""
intersection = len(set1 & set2)
union = len(set1 | set2)
return intersection / union if union != 0 else 0
def adjusted_cosine_similarity(ratings_matrix, item_i, item_j):
"""修正余弦相似度"""
common_users = ~np.isnan(ratings_matrix[:, item_i]) & ~np.isnan(ratings_matrix[:, item_j])
if np.sum(common_users) < 2:
return 0
item_means = np.nanmean(ratings_matrix, axis=0)
vec_i = ratings_matrix[common_users, item_i] - item_means[item_i]
vec_j = ratings_matrix[common_users, item_j] - item_means[item_j]
return cosine_similarity(vec_i, vec_j)
避坑指南:我曾经在线上环境忘记处理全零向量,导致cosine_similarity出现除零错误。记得加个norm != 0的判断。
实战中怎么选?我总结了一个小表格:
| 场景 | 推荐方法 | 原因 |
|---|---|---|
| 评分数据,用户尺度差异大 | 皮尔逊相关系数 | 去中心化,消除评分偏差 |
| 评分数据,物品尺度差异大 | 修正余弦相似度 | 消除物品评分偏差 |
| 二值数据(买/没买) | Jaccard相似度 | 简单高效,适合隐式反馈 |
| 评分尺度统一,数据密集 | 余弦相似度 | 计算快,效果不错 |
最后说一句,相似度计算没有银弹。我习惯在项目初期把几种方法都跑一遍,看哪个在验证集上表现最好。嗯,工程实践就是这样,理论指导方向,数据决定选择。