ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

协同过滤实战:UserCF与ItemCF原理、调优及工程落地

协同过滤实战:UserCF与ItemCF原理、调优及工程落地

1. 从零拆解协同过滤:为什么基于邻域的方法至今仍是推荐系统的基石

推荐系统这个领域,每年都有新模型刷榜,从矩阵分解到深度神经网络再到各种注意力机制,论文里的指标一个比一个漂亮。但如果你真正在生产环境里搭过推荐服务,就会发现一个很有意思的现象:大量中小型产品、冷启动阶段的新业务、甚至一些日活千万级产品的召回通道,跑的还是基于邻域的协同过滤。UserCF 和 ItemCF 这两个听起来有点"古老"的算法,为什么没有被淘汰?答案其实很朴素——它们足够简单、足够可解释、足够好维护,而且在很多场景下效果并不差。

这篇文章要聊的就是这个"老而弥坚"的东西。我会把 UserCF 和 ItemCF 从直觉到公式、从相似度计算到工程落地、从参数调优到踩坑排查,完整地过一遍。适合谁看?如果你刚接触推荐系统,想找一个能真正跑起来、能理解透的入门算法,这篇就是给你写的;如果你已经做过一段时间推荐,但一直停留在"调包"层面,没搞清楚相似度矩阵到底怎么算、邻居数 K 怎么选、评分偏差怎么处理,那这篇也能帮你把地基夯实。

我个人的判断是:协同过滤不是"过时的技术",而是"被低估的基线"。任何复杂的深度模型,如果连 ItemCF 的基线都打不过,那这个模型大概率是有问题的。所以把基于邻域的方法吃透,不只是学一个算法,更是建立一套评估推荐效果的参照系。

2. 核心思想与方案选型:UserCF 和 ItemCF 到底在算什么

2.1 一句话讲清协同过滤的底层逻辑

协同过滤的核心假设特别简单:相似的人喜欢相似的东西,相似的东西被人以相似的方式喜欢。这句话拆开就是两个方向——从人的角度出发是 UserCF,从物的角度出发是 ItemCF。

打个生活化的比方。你想找一部电影看,有两种办法:第一种是问和你口味差不多的朋友最近看了什么好片(UserCF);第二种是你先看了一部喜欢的电影,然后找和它相似的其他电影(ItemCF)。两种办法都能给你推荐,但适用的场景完全不同。

这里有个关键点很多人一开始会混淆:UserCF 和 ItemCF 不是"哪个更好"的关系,而是"哪个更适合当前场景"的关系。选错了方向,效果可能差出一大截。

2.2 UserCF 与 ItemCF 的选型对照

我把两者的核心差异整理成一张表,方便你快速判断该用哪个:

维度UserCFItemCF
核心逻辑找相似用户,推荐他们喜欢的找相似物品,推荐相似物品
相似度矩阵规模用户数 × 用户数物品数 × 物品数
适用场景用户少、物品多、时效性强用户多、物品少、长尾稳定
可解释性较弱("和你相似的人喜欢")较强("因为你看了A")
实时更新用户兴趣变化快,需频繁更新物品相似度相对稳定
典型代表新闻、社交内容推荐电商、视频、图书推荐

选型的经验法则:用户数量远小于物品数量时优先 UserCF,反之优先 ItemCF。比如一个新闻 App,用户可能几百万,但新闻每天新增几十万条,物品的相似度矩阵根本来不及算,这时候 UserCF 更合适。而一个电商平台,商品 SKU 相对稳定,用户却在不断增长,ItemCF 的相似度矩阵可以离线算好、定期更新,明显更划算。

2.3 为什么不用矩阵分解替代

有人会问,既然有 SVD、ALS 这些矩阵分解方法,为什么还要用基于邻域的?我的实测体会是三点:

第一,可解释性。ItemCF 能直接告诉你"因为你看过《A》,所以推荐《B》",这个理由用户能懂、运营能懂、产品经理也能懂。矩阵分解出来的隐向量,你很难解释第 37 维代表什么。

第二,增量更新成本低。新来一个用户行为,ItemCF 只需要更新对应的共现计数,不需要重新训练整个模型。矩阵分解往往要全量重训或者复杂的增量算法。

第三,冷启动友好度。新物品只要有少量交互,就能通过共现关系进入推荐池。矩阵分解对完全没有交互的物品基本无能为力。

当然,基于邻域的方法也有明显短板:稀疏场景下相似度计算不准、热门物品会主导推荐结果、无法捕捉深层非线性关系。这些短板正是后续各种改进和深度模型要解决的问题。但在很多业务里,这些短板的影响并没有想象中那么大。

3. 相似度计算:整个算法的发动机

3.1 三种主流相似度度量及其适用性

相似度计算是协同过滤的心脏,选错了度量方式,后面再怎么调参都是白费。常用的有三种:

余弦相似度(Cosine):把用户对物品的评分看成向量,计算两个向量夹角的余弦值。公式是 sim = (A·B) / (|A|·|B|)。它的特点是只关注方向不关注长度,适合评分尺度不一致的场景。

皮尔逊相关系数(Pearson):先减去各自的均值再算余弦,相当于做了中心化。它能消除不同用户评分习惯的偏差——有人习惯打高分,有人习惯打低分,Pearson 能把这个偏差抹平。

修正余弦相似度(Adjusted Cosine):这是 ItemCF 里最常用的。它减去的是物品的平均得分,而不是用户的。为什么?因为在 ItemCF 里,我们要衡量两个物品的相似性,但不同物品的评分基准不一样——爆款物品平均分 4.5,冷门物品平均分 3.0,直接算余弦会把这种基准差异当成相似性。

我用一个具体例子说明修正余弦的必要性。假设物品 A 被用户打分为 [5, 5, 5],物品 B 被打分为 [3, 3, 3]。如果直接算余弦,两者方向完全一致,相似度为 1。但实际上 A 是"人人都爱"的爆款,B 是"人人都觉得一般"的普通品,它们并不相似。修正余弦减去各自均值后,A 变成 [0.5, 0.5, 0.5] 附近,B 变成 [-0.5, -0.5, -0.5] 附近,相似度直接变成 -1,正确反映了它们的关系。

3.2 相似度计算的工程实现要点

理论公式好写,工程实现有几个坑必须注意。

第一个坑是零向量问题。如果某个用户对所有物品评分都是同一个值,中心化之后向量全为 0,分母为 0 会导致除零错误。处理办法是在计算前过滤掉方差为 0 的用户,或者加一个极小的平滑项。

第二个坑是稀疏矩阵的存储。真实场景下用户-物品矩阵稀疏度通常在 99% 以上,用稠密矩阵存储会直接爆内存。实践中用 scipy 的 sparse 矩阵或者自己维护倒排索引。

第三个坑是相似度矩阵的规模。ItemCF 要算物品两两相似度,如果有 10 万个物品,理论上要算 100 亿对。实际做法是:只对共现次数超过阈值的物品对计算相似度,其余直接置零。这个阈值一般设在 1 到 5 之间,具体看数据密度。

下面是一段修正余弦相似度的核心实现思路,用 Python 伪代码展示:

import numpy as np from scipy.sparse import csr_matrix def adjusted_cosine(item_user_matrix): # item_user_matrix: 物品-用户评分矩阵,行是物品,列是用户 # 计算每个物品的平均得分 item_mean = np.array(item_user_matrix.mean(axis=1)).flatten() # 中心化:每个评分减去该物品的均值 # 注意只对非零元素操作,保持稀疏性 centered = item_user_matrix.copy().astype(float) rows, cols = centered.nonzero() centered[rows, cols] -= item_mean[rows] # 计算模长 norms = np.sqrt((centered.multiply(centered)).sum(axis=1)) norms[norms == 0] = 1e-9 # 防止除零 # 归一化后点积即为修正余弦相似度 normalized = centered.multiply(1.0 / norms) sim_matrix = normalized @ normalized.T return sim_matrix

这段代码的关键在于保持稀疏性——只对非零元素做中心化,不要傻乎乎地把整个矩阵稠密化再操作。我见过有同学直接matrix - mean然后内存直接炸掉,就是因为忽略了这一点。

3.3 相似度归一化:一个容易被忽略的细节

算完相似度之后,还有一个步骤经常被跳过但影响很大:归一化。具体做法是把每个物品的相似度向量除以该向量的最大值,让相似度落在 [0, 1] 区间。

为什么要做这一步?因为不同物品的相似度分布范围不一样。有的物品和很多物品都高度相似,相似度最大值可能是 0.9;有的物品比较独特,最大值只有 0.3。如果不归一化,前者在加权求和时会天然占据更大权重,导致推荐结果被少数"社交广泛"的物品主导。

归一化之后,每个物品的相似度都被拉到同一个尺度,加权求和时更公平。这个技巧在 ItemCF 里几乎是标配,但在很多教程里被一笔带过,实际做的时候千万别漏。

4. 完整实操流程:从原始数据到推荐结果

4.1 数据准备与预处理

假设我们拿到一份用户行为日志,字段是 user_id、item_id、rating、timestamp。第一步不是急着算相似度,而是做几件脏活累活。

去重:同一个用户对同一个物品可能有多条记录(比如多次点击、多次购买),需要按时间取最新一条,或者做聚合。我一般按 (user_id, item_id) 分组取 timestamp 最大的那条。

过滤低频:交互次数少于 5 次的用户、被交互少于 5 次的物品,直接过滤掉。这些数据对相似度计算贡献极小,反而增加噪声和计算量。这个阈值不是拍脑袋定的,可以用分位数看分布——通常过滤掉后 10% 的长尾。

时间衰减:用户三个月前喜欢的物品和昨天喜欢的物品,权重不应该一样。常见做法是给评分乘一个衰减因子,比如 exp(-λ * Δt),λ 取 0.01 到 0.05 之间。这个参数需要根据业务节奏调,快消品衰减快,耐用品衰减慢。

构建矩阵:把清洗后的数据转成用户-物品评分矩阵。如果只有隐式反馈(点击、浏览),没有显式评分,就把交互次数或者是否交互作为值,通常用 0/1 或者 log(1+count) 表示。

4.2 UserCF 的完整计算流程

UserCF 的流程可以拆成五步:

  1. 构建用户-物品矩阵:行是用户,列是物品,值是评分。
  2. 计算用户相似度:用 Pearson 或余弦,得到用户相似度矩阵。
  3. 选取 Top-K 邻居:对每个用户,找出最相似的 K 个用户。K 一般取 20 到 100。
  4. 生成推荐:对目标用户未交互过的物品,用邻居的评分加权求和预测得分。
  5. 排序取 Top-N:按预测得分排序,取前 N 个推荐。

预测公式是:pred(u, i) = Σ(v∈N(u)) sim(u,v) * r(v,i) / Σ|sim(u,v)|。其中 N(u) 是 u 的邻居集合,r(v,i) 是邻居 v 对物品 i 的评分。

这里有个细节:加权求和时要不要减去用户均值?如果用的是 Pearson 相似度,最好减,因为 Pearson 本身已经中心化了,预测时保持一致。如果用的是余弦,可以不减。这个一致性很重要,混用会导致预测偏差。

4.3 ItemCF 的完整计算流程

ItemCF 的流程和 UserCF 对称,但有几个关键差异:

  1. 构建物品-用户矩阵:行是物品,列是用户。
  2. 计算物品相似度:用修正余弦,这是和 UserCF 最大的区别。
  3. 选取 Top-K 相似物品:对每个物品,找出最相似的 K 个。K 一般取 10 到 50,比 UserCF 小。
  4. 生成推荐:对用户交互过的物品,找出它们的相似物品,加权求和。
  5. 排序取 Top-N。

预测公式是:pred(u, i) = Σ(j∈S(i,K)∩I(u)) sim(i,j) * r(u,j)。其中 S(i,K) 是和 i 最相似的 K 个物品,I(u) 是用户 u 交互过的物品集合。

注意这里没有除以相似度之和,因为 ItemCF 的推荐理由通常是"因为你喜欢 A,所以推荐相似的 B",加权求和更符合直觉。如果除以归一化因子,反而会削弱强相似物品的贡献。

4.4 一个可复现的参数配置参考

我把实践中比较稳的一套参数列出来,你可以作为起点:

参数UserCF 推荐值ItemCF 推荐值说明
相似度度量Pearson修正余弦按场景可换
邻居数 K4020需网格搜索
相似度阈值0.10.05过滤弱相似
最小交互数55过滤长尾
推荐列表长度 N2020按业务定
时间衰减 λ0.020.02快消品调大

这套参数不是万能的,但能保证你在大多数数据集上跑出一个"不丢人"的基线。调参的顺序建议是:先调 K,再调相似度阈值,最后调时间衰减。

5. 常见问题与排查技巧实录

5.1 推荐结果全是热门物品怎么办

这是新手最常遇到的问题。原因通常是热门物品的共现次数天然就多,相似度计算时被放大。解决办法有三个:

第一,对热门物品做惩罚。在相似度公式里除以物品流行度的对数,比如 sim / log(1 + popularity)。这个惩罚力度要适中,太强会伤害准确性。

第二,在推荐结果里做多样性重排。取 Top-100 候选,然后用 MMR(最大边际相关)算法重排,保证推荐列表里既有热门也有长尾。

第三,降低热门物品的评分权重。如果用的是隐式反馈,把热门物品的 0/1 值改成 IDF 权重,让冷门物品的交互更有信息量。

我实测下来,第一种和第二种组合使用效果最好。单纯靠惩罚流行度,容易把真正的好物品也压下去。

5.2 相似度矩阵太大算不动怎么办

物品数超过 10 万时,全量相似度矩阵会非常吃资源。工程上的处理策略:

  • 分块计算:把物品分成若干块,块内算相似度,块间只算共现次数高的对。
  • 倒排索引加速:对每个用户,只计算他交互过的物品之间的相似度,其余跳过。因为两个物品如果没有共同用户,相似度必然是 0。
  • 近似算法:用 MinHash 或 SimHash 做局部敏感哈希,把相似度计算降到亚线性。
  • 定期全量 + 增量更新:相似度矩阵每周全量重算一次,每天做增量更新。

这里有个经验:ItemCF 的相似度矩阵其实不需要特别精确。因为最终推荐是 Top-K 加权,少量相似度的误差对排序结果影响很小。所以用近似算法完全可接受,不必追求精确计算。

5.3 新用户和新物品怎么处理

新用户没有历史行为,UserCF 和 ItemCF 都无能为力。这时候需要降级策略:

  • 新用户:推热门榜单、编辑精选、或者基于注册信息的规则推荐。
  • 新物品:给一个初始曝光池,用探索-利用策略(如 Thompson Sampling)快速收集反馈。
  • 混合方案:把协同过滤结果和内容推荐结果融合,新用户以内容推荐为主。

我踩过的一个坑是:新物品刚上线时,因为共现次数少,相似度算出来全是 0,导致永远得不到曝光。解决办法是给新物品一个"探索配额",强制在推荐列表里留 1 到 2 个位置给新物品,用实际反馈来积累共现数据。

5.4 常见问题速查表

问题现象可能原因排查方向解决思路
推荐全是热门流行度偏差看推荐物品的流行度分布加流行度惩罚 + 多样性重排
相似度全为0数据太稀疏统计共现次数分布降低最小交互阈值
预测分数异常高除零或未归一化检查分母和相似度范围加平滑项 + 归一化
推荐结果不更新缓存未失效检查缓存更新策略设置合理的 TTL
离线指标好线上差数据泄漏检查训练测试划分按时间划分而非随机划分
冷启动效果差无历史行为统计新用户占比降级到热门或内容推荐

5.5 几个只有踩过才知道的坑

坑一:离线评估的数据泄漏。很多人做离线评估时随机划分训练测试集,这是错的。因为推荐系统有时间属性,用未来的数据预测过去,指标会虚高。正确做法是按时间划分,用前 80% 的数据训练,后 20% 测试。

坑二:相似度矩阵的对称性。理论上 sim(A,B) = sim(B,A),但工程实现时如果用了近似算法或者分块计算,可能不对称。使用前最好做一次对称化处理,取两者的平均或最大值。

坑三:评分预测和排序是两回事。协同过滤优化的是评分预测的 RMSE,但业务关心的是排序质量(NDCG、Hit Rate)。这两个目标不完全一致。实践中应该直接用排序指标来调参,而不是盯着 RMSE。

坑四:K 值不是越大越好。K 太大,会引入不相似的邻居,噪声增加;K 太小,覆盖不够,推荐结果单一。我一般从 20 开始,以 10 为步长往上试,找到指标拐点。

6. 效果评估与调优:怎么知道你的协同过滤跑得好不好

6.1 离线评估指标的选择

评估推荐系统不能只看一个指标。我通常同时看四个:

准确率类:Precision@K、Recall@K、NDCG@K。NDCG 最重要,因为它考虑了排序位置,越靠前的推荐权重越大。

覆盖率:推荐系统能覆盖多少比例的物品。覆盖率太低说明推荐结果集中,长尾物品得不到曝光。

多样性:推荐列表内部物品的差异程度。多样性太低,用户会觉得"怎么老是推这些"。

新颖性:推荐物品的平均流行度。新颖性高说明推荐了冷门但可能相关的物品。

这四个指标往往互相矛盾——准确率高的模型通常覆盖率低。所以调优的本质是在多个指标之间找平衡,而不是单点最优。

6.2 线上评估的注意事项

离线指标好不代表线上效果好,这是推荐系统的铁律。线上评估要注意:

  • A/B 测试的样本量:太小容易得出错误结论,一般需要至少一周的数据。
  • 核心指标的选择:点击率、转化率、停留时长、次日留存,不同业务关注点不同。
  • 长期效果 vs 短期效果:有些策略短期点击率高,但长期会伤害用户信任。需要看留存指标。

我的经验是:离线指标提升 5% 以内,线上大概率看不出来。所以离线调优时,要追求显著提升,而不是纠结小数点后的差异。

6.3 一个实用的调优流程

我总结的调优流程是这样的:

  1. 先跑通基线,记录所有指标。
  2. 单独调 K 值,画指标曲线,找拐点。
  3. 单独调相似度阈值,观察覆盖率和准确率的权衡。
  4. 加入时间衰减,看指标变化。
  5. 尝试不同的相似度度量,对比效果。
  6. 做消融实验,确认每个改进的贡献。

每一步只改一个变量,这样才能知道是哪个改动带来的效果。我见过有人一次性改五个参数,结果指标涨了也不知道为什么涨,跌了也不知道为什么跌,这种调优是无效的。

7. 从协同过滤到混合推荐:下一步可以怎么走

基于邻域的协同过滤是一个很好的起点,但它不是终点。当你把 UserCF 和 ItemCF 都跑通、调优之后,下一步可以考虑几个方向。

方向一:融合 UserCF 和 ItemCF。两者各有优势,加权融合往往比单用好。融合权重可以用验证集学习,也可以简单取 0.5。我实测下来,融合后 NDCG 通常能提升 3% 到 8%。

方向二:加入内容特征。纯协同过滤在冷启动和稀疏场景下表现差,加入物品的类别、标签、文本描述等内容特征,能显著改善。这就是经典的混合推荐思路。

方向三:用矩阵分解做召回。协同过滤作为召回通道,矩阵分解作为另一路召回,最后用排序模型融合。这是工业界最常见的架构。

方向四:图神经网络。把用户-物品交互建成二部图,用 GNN 做消息传递,本质上是对协同过滤的深度化扩展。这个方向近几年的论文很多,但工程落地的复杂度也高。

我个人在实际操作中的体会是:不要急着上深度模型,先把协同过滤的基线做扎实。很多团队一上来就搞双塔、搞 DIN,结果连 ItemCF 的基线都没跑过,最后发现是数据处理出了问题。协同过滤的价值不只是它本身,更是它提供了一个简单、可解释、可调试的参照系。当你的复杂模型打不过这个参照系时,问题往往不在模型,而在数据或评估环节。

最后再分享一个小技巧:把 ItemCF 的相似物品列表导出来人工看一遍。你会发现很多有意思的现象——有些相似度很高的物品对,业务上完全不相关,这通常意味着数据里有噪声或者共现是偶然的。这种人工检查是发现数据问题最快的方式,比看任何指标都直观。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进