4、基于用户的协同过滤(UserCF):算法原理、用户-物品矩阵构建、寻找最近邻用户、预测评分与推荐、UserCF优缺点分析
好,咱们今天聊聊UserCF。这个算法可以说是推荐系统的“老前辈”了,我入行那会儿,很多电商网站的第一版推荐就是靠它撑起来的。说白了,它的核心思想就一句话:物以类聚,人以群分。
你想想看,如果你和隔壁老王都喜欢《三体》和《流浪地球》,那老王最近打五星的《球状闪电》,你是不是大概率也会喜欢?UserCF就是干这个的——找到和你口味最像的那群人,然后把他们喜欢的东西推荐给你。
4.1 算法原理:从“人以群分”说起
UserCF的全称是User-based Collaborative Filtering。它的基本假设是:如果两个用户过去对物品的偏好相似,那么他们对未来物品的偏好也会相似。
这个逻辑其实很朴素。我在做社交推荐项目时,发现用户的行为模式往往有很强的“圈子效应”。喜欢二次元的用户,大概率也会喜欢同人创作;喜欢硬核科幻的,大概率也会关注航天科技。UserCF就是把这个直觉数学化了。
算法流程大致分三步:
- 构建用户-物品矩阵:把用户对物品的评分或行为,整理成一个矩阵。
- 寻找最近邻用户:计算目标用户和其他用户的相似度,找到最像的K个邻居。
- 预测评分与推荐:根据邻居的评分,预测目标用户对未接触物品的评分,然后排序推荐。
嗯,这里要注意:UserCF不是万能的。它特别依赖用户的行为数据。如果用户是“冷启动”的,也就是刚注册啥都没干,那UserCF基本就抓瞎了。我曾经在一个新用户占比超过60%的平台上试过UserCF,效果惨不忍睹……后来还是靠内容推荐先撑了一阵子。
4.2 用户-物品矩阵构建:数据是地基
矩阵长什么样?很简单。行是用户,列是物品,交叉点就是评分或行为。
举个例子,假设我们有3个用户和4部电影:
| 用户\电影 | 《流浪地球》 | 《三体》 | 《星际穿越》 | 《阿凡达》 |
|---|---|---|---|---|
| 小明 | 5 | 4 | 0 | 3 |
| 小红 | 4 | 5 | 3 | 0 |
| 老王 | 0 | 2 | 4 | 5 |
这里的0表示用户没看过或者没评分。在实际工程中,这个矩阵会非常稀疏——用户可能只对1%的物品有过行为。我见过一个视频平台的数据,用户-物品矩阵的稀疏度高达99.8%。也就是说,100个物品里,用户平均只碰过0.2个。
构建矩阵时,有几点要注意:
- 评分归一化:不同用户的打分习惯不同。有人喜欢打高分,有人喜欢打低分。我习惯先做均值中心化,把每个用户的评分减去他的平均分,这样能消除用户自身的打分偏差。
- 隐式反馈处理:很多场景下没有评分,只有点击、购买、收藏等行为。这时候可以用0/1表示,或者用行为频次加权。我个人建议,对于隐式反馈,最好用“是否点击”作为正样本,而不是用点击次数——因为用户可能只是手滑点错了。
- 时效性加权:用户的口味会变。我做过一个实验,用最近3个月的行为数据做UserCF,比用全部历史数据的效果提升了15%。所以,构建矩阵时,可以给近期行为更高的权重。
4.3 寻找最近邻用户:相似度怎么算?
矩阵建好了,下一步就是找邻居。怎么衡量两个用户像不像?最常用的方法有三种:
4.3.1 余弦相似度
把每个用户的评分向量看作一个多维空间中的点,计算两个向量之间的夹角余弦值。值越接近1,说明方向越一致。
# 伪代码示例
def cosine_similarity(user_a, user_b):
# user_a, user_b 是评分向量
dot_product = sum(a * b for a, b in zip(user_a, user_b))
norm_a = sqrt(sum(a*a for a in user_a))
norm_b = sqrt(sum(b*b for b in user_b))
return dot_product / (norm_a * norm_b)
余弦相似度的好处是简单、计算快。但它有个问题:它不考虑用户评分的绝对值。比如小明所有评分都是5,小红所有评分都是3,但他们的评分趋势完全一致,余弦相似度会很高。这合理吗?其实不太合理。所以实际中我更常用皮尔逊相关系数。
4.3.2 皮尔逊相关系数
它相当于对评分做了均值中心化后的余弦相似度。说白了,就是先减去每个用户的平均分,再算余弦。
def pearson_similarity(user_a, user_b):
# 先计算均值
mean_a = sum(user_a) / len(user_a)
mean_b = sum(user_b) / len(user_b)
# 中心化
centered_a = [a - mean_a for a in user_a]
centered_b = [b - mean_b for b in user_b]
# 计算余弦
return cosine_similarity(centered_a, centered_b)
皮尔逊相关系数能更好地反映用户之间的“相对偏好”。我在做电影推荐时,发现用皮尔逊比用余弦的准确率高了大概8%。
4.3.3 Jaccard相似度
这个适用于只有0/1行为(比如是否点击)的场景。它只关心两个用户共同交互过的物品数量,不考虑评分高低。
def jaccard_similarity(set_a, set_b):
intersection = len(set_a & set_b)
union = len(set_a | set_b)
return intersection / union
Jaccard在隐式反馈场景下表现不错。我记得有个电商项目,用户只有点击行为,用Jaccard算相似度,推荐出来的商品点击率比用余弦高了12%。
4.4 预测评分与推荐:邻居们怎么说?
找到K个最近邻用户后,怎么预测目标用户对某个物品的评分?最常用的方法是加权平均:
def predict_rating(target_user, item, neighbors, similarities):
# neighbors: 邻居用户列表
# similarities: 对应的相似度
numerator = 0
denominator = 0
for neighbor, sim in zip(neighbors, similarities):
rating = get_rating(neighbor, item)
if rating > 0: # 只考虑有评分的邻居
numerator += sim * rating
denominator += sim
if denominator == 0:
return 0 # 没有邻居评分,返回默认值
return numerator / denominator
这里有个细节:如果邻居对物品没有评分,就跳过。所以K值的选择很关键。K太小,邻居不够,预测不稳定;K太大,引入了太多不相关的用户,预测精度下降。我一般会从K=20开始调,根据验证集的效果做网格搜索。
推荐的时候,对目标用户所有未评分的物品都做预测,然后按预测评分从高到低排序,取Top-N推荐给用户。
4.5 UserCF优缺点分析
聊了这么多,咱们来总结一下UserCF的优缺点。我做了个表格,方便你对比:
| 维度 | 优点 | 缺点 |
|---|---|---|
| 推荐解释性 | 强。可以告诉用户“和你口味相似的人还喜欢...” | —— |
| 冷启动 | 对物品冷启动友好。新物品只要有一个用户评分,就能被推荐 | 对用户冷启动不友好。新用户没有行为,找不到邻居 |
| 计算复杂度 | —— | 高。用户数远大于物品数时,相似度计算开销大 |
| 稀疏性 | —— | 用户-物品矩阵稀疏时,邻居质量差,推荐效果下降 |
| 实时性 | —— | 差。用户行为变化后,需要重新计算相似度矩阵 |
| 个性化程度 | 中等。依赖群体偏好,对长尾物品推荐能力有限 | —— |
我个人觉得,UserCF最大的价值在于它的社交可解释性。你想想看,当用户看到“和你一样喜欢《三体》的人还喜欢《球状闪电》”时,点击率往往比“因为你看过《三体》,所以推荐《球状闪电》”要高。为什么?因为用户觉得“有人和我一样”,这种认同感会增强信任。
但UserCF的缺点也很明显。当用户规模达到千万级时,计算用户之间的相似度矩阵,复杂度是O(N²),基本跑不动。我当年在某个社交平台,用户量从100万涨到500万,UserCF的计算时间从2小时变成了20小时……后来不得不切换到ItemCF。
另外,UserCF对热门物品有偏。因为热门物品被很多人评分过,两个用户如果都喜欢热门物品,他们的相似度会被拉高。但实际上,喜欢热门物品并不能说明他们口味相似——大家都喜欢《肖申克的救赎》,这能说明什么?所以,我建议在计算相似度时,对热门物品做降权处理,比如用TF-IDF的思想。
好了,关于UserCF的原理和工程实践,就聊到这儿。下一章咱们会讲ItemCF,到时候你会发现,它和UserCF就像一对镜像兄弟——思路相反,但各有千秋。