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相似度 简单高效,适合隐式反馈
评分尺度统一,数据密集 余弦相似度 计算快,效果不错

最后说一句,相似度计算没有银弹。我习惯在项目初期把几种方法都跑一遍,看哪个在验证集上表现最好。嗯,工程实践就是这样,理论指导方向,数据决定选择。