尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

基于用户的协同过滤算法:原理、实现与推荐系统落地实践

基于用户的协同过滤算法:原理、实现与推荐系统落地实践 做推荐系统这几年我接触的第一套算法不是各种深度模型而是这玩意儿——基于用户的协同过滤userCF。当时看的教材例子也很简单三个用户五部电影算一张相似度矩阵然后给目标用户推荐他没看过的那一部。道理谁都懂但真把它搬到线上数据集上跑起来才会发现里面有大量细节值得琢磨。今天这篇就把 userCF 从原理到代码、从离线评估到实际落地中的坑完整地梳理一遍。不管你是刚入门推荐系统的学生还是要在业务里做第一版推荐的工程师这篇文章都应该能帮你在动手之前把思路理清楚。1. userCF 的核心思路与解决场景1.1 用户协同到底在做什么userCF 的出发点用一句话说就是和你兴趣相似的人喜欢的东西你大概率也会喜欢。这个逻辑跟你平时找人问“最近有什么好看的”一模一样。你问的不是全网评分最高的电影而是那些口味跟你接近的朋友推荐的片子因为他们见过你踩过的雷也知道你真正吃哪一套。在系统里实现这件事需要三步。第一步根据用户历史行为点击、收藏、评分、购买构造出每个用户的兴趣画像第二步用某种相似度指标算出用户两两之间的兴趣接近程度找到目标用户最像的那一批“邻居”第三步把这些邻居喜欢过、但目标用户还没有接触过的物品汇总起来按邻居的相似度和邻居对物品的评分做加权排序输出 TopN 推荐结果。理解这个流程之后你会发现 userCF 本质上是在做“人以群分”。它不关心物品具体是什么不解析文本、不提取图片特征只依赖用户行为矩阵。这也是它最大的优势——在冷启动到一定阶段、行为数据已经积累起来的场景里它不需要额外内容特征就能工作实现成本极低。1.2 它能解决什么解决不了什么userCF 最适合的场景是用户规模相对可控、且用户兴趣相对稳定的社区型产品。比如早期的豆瓣读书、知乎的关注推荐、一些垂直社区的帖子推荐这类产品用户之间有明显的“圈子”属性相似用户的行为往往有很强的参考价值。只要能把相似用户找出来推荐结果的说服力就很强。但它也有天然的短板。当用户量涨到千万级别计算两两相似度的成本是 O(n²) 级别离线任务能扛线上实时推荐却很难做到毫秒级响应。另一个典型问题是冷启动新用户没有任何行为记录系统根本找不到他的邻居推荐就无从谈起新物品同理它只被极少数人看过几乎没有机会进入候选集合。另外如果用户兴趣随时间漂移而相似度矩阵更新不及时推荐结果也会滞后。所以你在评估要不要选用 userCF 时先想清楚三件事用户行为数据够不够稠密、用户量级是否可控、产品是不是有明显的“圈子”结构。三个条件满足大部分再用它作为基线模型起步是比较稳妥的路径。2. 相似度计算userCF 的关键引擎2.1 余弦相似度与它的直觉相似度计算是整个算法的命根子邻居选错了后面推荐得再精细也没用。最常用的度量是余弦相似度。假设我们把每个用户的评分记录看成一个向量向量的每个维度是一个物品维度上的值就是用户对物品的评分或交互次数。两个用户向量的夹角越小说明他们在兴趣空间上越接近。余弦相似度的公式是similarity(u, v) (u · v) / (||u|| × ||v||)也就是两个向量点积除以各自模长的乘积。这个指标有个好处它对用户的评分尺度不敏感。A 用户习惯打分偏高平均 4 分B 用户手比较严平均 3 分但两个人对同一批商品的相对喜好是一致的——余弦相似度依然能捕捉到这种一致性不会被整体偏移干扰。不过在实际数据里用户行为矩阵极其稀疏一个用户可能只跟几百个物品产生过交互物品总量却是百万级。这时候向量里绝大多数维度是 0余弦相似度的效果会被稀释。所以工程上我们通常只取两个用户共同评分过的物品子集进行计算这既减少了计算量也避免了大量零维度对相似度数值的干扰。2.2 皮尔逊相关系数与去中心化如果评分数据不是“交互次数”而是真实的 1 到 5 分评分我更推荐用皮尔逊相关系数。它的思路是先把每个用户的评分减去自己的平均分再做余弦相似度。相当于把每个用户的评分先“去均值化”再比较他们的波动形态。皮尔逊相关系数的一个实际意义在于它能把“评分风格”和“兴趣形态”解耦。用户 A 平均打 4.2 分用户 B 平均打 3.1 分如果直接算余弦A 和 B 之间天然有差值容易被误判为不相似。但皮尔逊只看两个用户在共同物品上的评分是否同高同低。A 给《肖申克的救赎》打 5 分、给某烂片打 2 分B 给前者打 4 分、给后者打 1 分两者形态一致相似度就会很高。计算时要注意一个小坑如果两个用户只有一两个共同交互物品皮尔逊系数很容易被偶然性放大。比如只在一个物品上评分相同分母是 0计算结果会出现 NaN。实操中我一般会设置一个最小共同交互阈值比如至少要有 5 个共同评分的物品才参与相似度计算否则直接置为 0。这个阈值虽然朴素但非常有效。2.3 不同相似度指标的选型建议指标适用场景优点注意点余弦相似度隐式反馈点击/收藏计算快无需评分受稀疏性影响大皮尔逊相关系数显式评分1-5分去中心化抗评分风格差异共同物品少时容易失真Jaccard 相似度只看是否交互简单稳定忽略交互强度信息修正余弦相似度物品评分尺度差异大减掉用户均分再做余弦需要维护每用户均分选指标没有绝对标准关键看你的数据形态。如果只有隐式反馈我通常直接用余弦相似度如果是打分数据优先皮尔逊如果行为矩阵太稀疏可以把 Jaccard 作为辅助过滤条件先筛掉一批共同交互为 0 的用户对再在剩余用户对上计算精细相似度能省下不少计算资源。3. 从邻居集合到推荐列表完整流程拆解3.1 邻居选择K 值到底取多大相似度算完之后要对每个目标用户选出 K 个最相似的邻居。这里的 K 是 userCF 最重要的超参数直接影响推荐效果。K 太小邻居集合里都是“铁粉级”相似用户推荐结果过于集中多样性差K 太大混进来大量弱相似用户他们的行为会稀释强相似用户的信号推荐精度下降。业界比较常见的经验区间是 K 取 20 到 100 之间。具体取值需要跑离线评估来定我自己的习惯是先用折半搜索从 10 开始依次试 20、40、80观察 Precision10 和 Recall 的变化曲线。当 K 增加到某个值之后精度不再明显提升甚至下降说明已经越过拐点取拐点前一个值即可。值得注意的一点是K 的选择要跟物品流行度分布结合来看。如果产品是典型的头部效应市场少数热门物品占了大部分交互量K 值要适当调大一点让非热门物品有更多机会通过不同邻居进入候选集如果长尾分布不明显K 值偏小反而能让推荐更“懂”用户。3.2 打分预测与最终排序选出邻居集合之后需要对候选物品算一个预测分数。最基础的加权平均方法长这样pred(u, i) sum(sim(u, v) * rating(v, i)) / sum(sim(u, v))公式的含义是对目标用户 u 的所有邻居 v如果他们喜欢过物品 i就用 v 的相似度作为权重对 v 给 i 的评分做加权平均。权重越大该邻居的喜好对最终排序的影响越大。这比简单取“多少邻居喜欢过”更有区分度因为它能保留交互强度层面的信息。如果用皮尔逊相关系数做相似度预测公式通常还会做一个均值偏移修正。因为邻居的评分风格不同原始分直接加权会有偏差。修正后的公式是把邻居评分减去该邻居的均分加权求和之后再加回目标用户的均分pred(u, i) mean(u) sum(sim(u, v) * (rating(v, i) - mean(v))) / sum(sim(u, v))这个修正版本在实际显式评分数据上的表现会好不少尤其是用户群体评分标准差异大的时候。另外排序阶段还需要处理一个小问题候选物品里有大量“只有一个微弱相似邻居看过”的冷门物品它们加权分数可能很高但置信度很低。我一般会在排序时加一个次数惩罚只有至少被 N 个邻居交互过的物品才进入最终 TopN。N 取 2 到 3 比较合适既能保留长尾多样性又不会推荐出可靠性不足的结果。3.3 隐式反馈场景的适配很多现实产品拿不到评分只有点击、播放、购买这类隐式反馈。这种数据下“评分矩阵”变成“交互矩阵”0/1 值很难区分用户的喜欢程度。我的处理方式是把交互行为做次数归一化比如用户在一个视频下的完播次数超过 3 次就记为 1否则记 0.5购买行为权重可以高于浏览行为。这些权重设置需要结合业务经验不断调但核心思路是把隐式反馈量化为“兴趣强度”避免只用 0/1 导致相似度计算丢失太多信息。排序阶段更简单直接用相似度加权求和算出候选物品的得分即可不需要套用均值偏移公式因为 0/1 矩阵没有评分风格的概念。4. 手写一个 userCF从数据到推荐结果4.1 数据准备这部分我用一个公开的 MovieLens 风格数据集做演示数据格式是经典的三列user_id、item_id、rating。为了让大家清楚地看到每一步我只保留一个很小的示例片段但代码结构是通用的换成百万级数据也能跑。import pandas as pd import numpy as np data pd.DataFrame({ user_id: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], item_id: [101, 102, 103, 101, 102, 104, 102, 103, 104, 101, 103, 105], rating: [5, 3, 4, 4, 5, 2, 3, 4, 5, 4, 3, 4] })先把评分表转成用户-物品矩阵行为用户列为物品缺失值先填 0。注意这里填 0 只是为了方便相似度计算并不代表用户给了 0 分。matrix data.pivot_table( indexuser_id, columnsitem_id, valuesrating ).fillna(0)矩阵长这样行是用户列是电影值是对应的评分。构建这一步是整个流程里最容易出问题的地方千万注意 pivot_table 的聚合函数。如果原始数据里同一个用户对同一个物品有多条记录比如不同时间看了多次默认取均值会比直接报错更合理这也是为什么要先做好数据去重和聚合。4.2 相似度计算与推荐函数用 sklearn 的 cosine_similarity 直接算用户相似度矩阵。这里有个细节cosine_similarity 默认按行计算传入的是用户-物品矩阵得到的每一行就是该用户与其他用户的相似度。from sklearn.metrics.pairwise import cosine_similarity user_sim cosine_similarity(matrix) user_sim pd.DataFrame( user_sim, indexmatrix.index, columnsmatrix.index )接下来实现推荐函数。函数做的事情是取目标用户相似度最高的 K 个邻居遍历他们交互过的物品过滤掉目标用户已经交互过的用相似度加权求和得到候选分数最后取 TopN。def recommend(target, user_sim, matrix, k3, top_n3): # 目标用户已经交互的物品 interacted set(matrix.loc[target][matrix.loc[target] 0].index) # 取相似度最高的 k 个邻居 sim_scores user_sim[target].drop(target).sort_values(ascendingFalse) top_k sim_scores.head(k).index # 候选物品打分 score {} for neighbor in top_k: sim user_sim.loc[target, neighbor] for item in matrix.columns: if matrix.loc[neighbor, item] 0 and item not in interacted: score[item] score.get(item, 0) sim * matrix.loc[neighbor, item] ranked sorted(score.items(), keylambda x: x[1], reverseTrue) return [item for item, _ in ranked[:top_n]]这段代码把前面讲到的理论全部落地了。运行方式很简单print(recommend(1, user_sim, matrix, k2, top_n3))给用户 1 做推荐时函数会先选出和他最像的两位邻居再把这两位邻居看过但用户 1 没看过的物品捞出来按相似度和评分的乘积排序。整个逻辑很直观适合作为你本地实验的起点。4.3 调参实验记录我在自己数据上做过一组小实验把 K 从 2 调到 8观察推荐结果的差异。K2 的时候候选物品太少推荐结果基本被两个高相似用户的行为主导K6 的时候候选集合变大但开始出现相似度只有 0.2 左右的“弱邻居”推荐列表里混入了噪音。最后在 K4 附近效果最稳定。这类实验建议每次只动一个参数。很多新手一上来就同时调 K、调相似度指标、改权重最后结果变好了也说不清是哪个改动起了作用。先固定相似度指标单独扫 K 值找到最优 K 之后再换相似度指标做对比这样每一步的收益都能归因清楚。5. 实际落地中的常见问题与排查记录5.1 冷启动问题怎么破userCF 最头疼的就是两个冷启动新用户没行为、新物品没曝光。针对新用户工程上常用“热门兜底”策略先推荐全站热门物品等用户产生少量行为之后再切到 userCF。更细一点的做法是做一个行为引导页让新用户先选几个感兴趣的分类或物品用这几个初始行为快速定位相似用户。新物品的冷启动更麻烦因为它可能完全不在任何用户的交互记录里。我的建议是把推荐系统拆成两层userCF 负责在存量候选物品里做个性化排序另配一个单独的“新品召回”通道把新物品通过补充策略比如内容相似、人工运营混入候选集再参与统一排序。这样既能保留 userCF 的个性化能力又不至于让新品永远没有曝光机会。5.2 稀疏性和可扩展性用户-物品矩阵的稀疏率经常高达 99% 以上。稠密矩阵的相似度计算在千万用户级别完全不可行。实操中有几种有效的处理手段只计算有共同交互的用户对用倒排索引先过滤再在少量候选对上算相似度离线预计算相似度矩阵周期性更新比如每天一次在线阶段只查表取邻居对用户做分桶比如按活跃度分桶高频用户之间算精细相似度低频用户用热门兜底用随机采样降低计算量在超大规模场景下先采样一部分用户计算近似相似度牺牲少量精度换取可接受的运行时间。这些手段不是互斥的可以叠加使用。我经历过一个单机 Python 脚本完全跑不动的阶段后来加上倒排索引和分桶把计算时间从几十小时压缩到两小时内核心思路就是减少无效计算。5.3 热门物品偏差与推荐多样性userCF 有个比较隐蔽的陷阱它天然偏向热门物品。原因是热门物品出现在很多用户的交互记录里更容易成为候选加权得分也容易被堆高。结果就是推荐结果泛化用户觉得“系统推的都是我知道的东西”个性化体验很差。缓解方式大概有几种我按照推荐指数排序对相似度做降权两个用户如果共同交互的全是热门物品他们相似并不可贵可以乘一个小系数对物品流行度做惩罚候选物品得分除以物品热度的某个次方抑制头部物品引入多样性约束从候选列表里选取 TopN 时用类似最大边际相关MMR的方法保证推荐结果覆盖不同品类。这三种方法我都在实际项目里试过效果最立竿见影的是第二种。实现起来就是在排序打分阶段给热门物品乘一个惩罚系数比如得分除以 log(1 物品交互次数)。数据量大的时候这个改动能让推荐结果的覆盖率显著上升用户反馈也会更正向。6. userCF 与 itemCF 的选择以及后续演进6.1 两个算法行为上的差异很多人一开始搞不清 userCF 和 itemCF其实两者的逻辑很好区分userCF 是“找相似的人”itemCF 是“找相似的物品”。userCF 先找邻居用户再聚合物品itemCF 先找用户历史物品的相似物品再排序。从结果表现来看userCF 更容易推荐出跨品类的东西因为相似用户可能在完全不同的品类上有偏好itemCF 则倾向于推荐同品类的东西因为相似物品通常属于同一类别。这就是为什么新闻资讯、视频社区这类需要“探索兴趣边界”的产品更适合 userCF而电商、图书这类用户目的明确的场景更适合 itemCF。维度userCFitemCF核心逻辑人找人再找物物找物再推荐推荐结果跨品类、偏惊喜同品类、偏稳妥计算复杂度用户量平方级物品量平方级冷启动侧重新用户难新物品难典型场景社区、资讯、视频电商、图书、音乐6.2 从 userCF 出发可以往哪里走userCF 虽然是经典算法但它的思想渗透在很多更复杂的模型里。理解了它之后你可以顺着两条线继续深入。一条是矩阵分解方向把用户-物品矩阵分解成用户隐向量和物品隐向量用向量的内积代替相似度加权解决了稀疏性问题也提升了泛化能力另一条是图算法方向把用户和物品看成二分图的节点用随机游走等方法计算节点相关性本质上是 userCF 和 itemCF 的统一框架。在实际项目里我见过不少团队拿 userCF 当基线上线之后用它的推荐结果做对照实验验证后续复杂模型是否有真实提升。这种用法其实非常合理因为 userCF 实现简单、逻辑可解释是评估新模型性价比的最好标尺。最后再分享一个我个人的习惯每次写完推荐算法我都会随机抽几个用户人工看一遍他们的邻居集合和推荐结果把“看起来不合理的邻居”挑出来反推原因——是不是数据没做清洗、是不是相似度指标选错了、是不是 K 值不合适。这套人工抽检的流程虽然朴素但比任何离线指标都更能帮我发现算法在真实业务里的问题。毕竟推荐系统最终服务的是人数据指标和真实体验得对得上模型才算是真正落地。
返回列表