老陈搬进阳光小区,第一天就在“社区美食群”里潜水。他发现自己和一位叫小林的群友口味出奇一致:都爱吃川菜、都嫌小区门口的奶茶太甜、都常去同一家卤味店。老陈心想:既然小林说后街新开的火锅店值得一试,那多半差不了。这种“口味像的人说好,我就去试试”的生活直觉,就是用户协同过滤的灵感来源。推荐系统把这套直觉搬进算法——先找出和你口味最像的一小群人,再让这群人替你投票、替你筛选、替你打分。你不需要懂商品百科,只需要找到“懂你的朋友”就够了。这篇文章会带你拆解这套逻辑的每一步:怎么量化相似、怎么选朋友、怎么聚合推荐,以及它在真实产品里如何落地。
掌握 UserCF,你就拿到了推荐系统的第一把钥匙——理解“人以群分”如何驱动个性化分发,能在电商、内容、社交等场景快速落地第一版推荐。
目录
可视化演示
动态演示用户协同过滤的核心机制,通过全局/局部/动态/静态四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
用户协同过滤:用户CF 用户CF核心思想 用户相似度计算方法 核心机制可视化 用户协同过滤整体架构与关键组件
核心思想:人以群分的口碑密码
老陈搬进阳光小区,第一天就在“社区美食群”里潜水。他发现自己和一位叫小林的群友口味出奇一致:都爱吃川菜、都嫌小区门口的奶茶太甜、都常去同一家卤味店。老陈心想:既然小林说后街新开的火锅店值得一试,那多半差不了。这种“口味像的人说好,我就去试试”的生活直觉,正是用户协同过滤(UserCF)的灵感来源。
推荐系统把这套直觉搬进算法:把每个用户积累的评分、点赞、购买记录看作一份“口味档案”。要预测老陈会不会喜欢某件商品,系统不去死磕商品本身的属性,而是先找出和老陈档案最像的那批人,再让这批人“替你投票”。算法只信三个字——人以群分。
在数学上,所有用户的口味档案被组织成一张“用户-物品”评分矩阵。每一行是一个用户对所有物品的态度,每一列是一件物品被所有用户的评价情况。UserCF 的执行分三步:从矩阵读出每位用户的行为向量;两两计算用户的相似度;选出相似邻居后按邻居评分加权预测并排序。
回到小区美食群:每个群友一行,每家店一列,谁夸过哪家店就在格子里记一笔。UserCF 就是在这个“口碑登记表”上做文章——先找到笔迹最像你的人,再让你代他下判断。
- 矩阵维度评分表共有 $m$ 行和 $n$ 列,行对应 $m$ 个用户,列对应 $n$ 件物品。
- 行向量含义第 $u$ 行整体刻画了用户 $u$ 的口味画像,即他对各件物品的喜好。
- 列向量含义第 $i$ 列反映了物品 $i$ 被社区里不同用户评价的情况。
- 元素含义$r_{ui}$ 具体表示用户 $u$ 对物品 $i$ 的评分,可以是显式打分,也可以是点击、收藏等隐式信号转换成的分值。
相似度计算:如何量化“口味相投”
老陈想挑出最像自己的群友。他翻开小林的朋友圈:20 家店里 19 家和自己同款好评。再看另一位群友小周:只有 3 家重叠,而且那 3 家老陈都觉得一般。老陈立刻明白:小林才是“知音”。但如果只数共同点赞数,天天刷屏的人会占便宜——所以更公平的办法是看“交集占并集的比例”,或者更精细地比较两人的口味向量方向。
工程中最常用的是余弦相似度。把用户 $u$ 的评分记录看作一个向量 $\vec{r}_u$,没评过分的维度填 0。两个用户口味越接近,两个向量在“口味空间”里的夹角就越小,余弦值就越接近 1。
在实践中还有一个重要约束:如果两位用户共同评分过的物品太少,算出的相似度只是“惊鸿一瞥”,并不可靠。就好比老陈和小周只同时吃过一家店,仅凭一次重叠就说“咱俩口味像”,未免草率。所以在相似度计算前,通常要设置“共同评分数量下限”,过滤掉那些交情太浅的用户对。
- 分子求和把两位用户在每件物品上的评分相乘再相加。两人都喜欢或都不喜欢的物品贡献正分,分歧大的物品贡献低分甚至负分。
- 分母模长$\sqrt{\sum_{i} r_{ui}^{2}}$ 是用户 $u$ 评分向量的“长度”,作用是消除评分数量与打分尺度的影响。
- 对称归一分母再乘上用户 $v$ 的模长,让相似度只取决于方向夹角,与两人各自的评分习惯无关。
- 整体比值两个向量夹角的余弦值,值域为 $[-1,1]$,越接近 $1$ 越投缘,接近 $0$ 则基本无关。
邻居选择:只信最像的几位饭友
老陈没有打算问遍群里 300 人——一半潜水,一半口味完全不同。他只挑最像自己的前 4 位“核心饭友”,参考他们的意见就够了。如果只信 1 个人,万一那人在某家店上口味突变,推荐就翻车;如果拉 100 个人进来,泛泛之交的噪声又会淹没知音的声音。这个“取前 K 个最像的人”的过程,就是邻居选择。
K 近邻法是 UserCF 最常用的邻居选择策略:把其他用户按相似度降序排列,取前 $K$ 个构成邻居集合 $N_K(u)$。$K$ 是超参数,小型系统通常取 20 到 60,数据量越大,$K$ 也相应增大。排序后切片即可,不需要遍历所有集合组合。
除了 K 近邻,也可以设定相似度阈值:低于 0.3 的一律不认作邻居。两种方式可以结合——先按阈值过滤,再取前 K 个,防止热门用户大面积出现在所有人的邻居列表里。老陈的“核心饭友团”就是这样一个经过筛选的小圈子,固定人数,宁缺毋滥。
- 邻居集合$N_K(u)$ 表示目标用户 $u$ 选出的 $K$ 个邻居组成的集合。
- 排序下标$v^{(i)}$ 表示在所有候选用户中,与 $u$ 相似度排名第 $i$ 位的用户。
- 单调递减链相似度从第一名到第 $K$ 名依次不增,确保留下的都是“最像”的人。
评分预测:温柔地加权,聪明地修正
饭友团里,小林给后街火锅打了 5 分,另一位饭友小吴打了 4 分。老陈不会直接平均成 4.5 分——因为他知道小吴是个“老好人”,去过的店基本都打 5 分,4 分在他那里已经算差评了。所以老陈心里要先打个折扣:看一个人“相对自己平时水平”的评分,而不是绝对分数。
这正是评分预测里的“中心化”思想。在加权平均前,先把每个邻居的评分减去他自己的平均分,得到“相对偏好”;预测时再把这个偏移加回目标用户自己的平均分上,还原成“老陈尺子”下的分数。
回到老陈的例子:小吴虽然给了 4 分,但他平时都打 5 分,这个 4 分实际上是“偏负”的;小林平时打分偏严,5 分是“超预期好评”。中心化之后,小林的 5 分权重被放大,小吴的 4 分被修正,最后老陈得到 4.2 分——非常接近他心里的判断。
- 基准分$\bar{r}_u$ 是目标用户 $u$ 自己的平均分,作为预测的起点。老陈平时给分偏严,那预测值也会相应偏低。
- 相对偏差$r_{vi} - \bar{r}_v$ 表示邻居 $v$ 对物品 $i$ 的评分相对他自己平均水平的偏移,正数代表“超常好评”。
- 加权求和把每个邻居的偏移乘上相似度再累加,越像的邻居话语权越大。
- 归一还原除以相似度绝对值之和,最后加回 $\bar{r}_u$,得到最终预测分。
TopN 推荐:输出“大家都说好”的前三名
评分预测算完后,老陈并不需要 200 家店的精确分数,他只想看到“前 3 名”。TopN 推荐就是干这件事的:把候选物品按照预测分数排序,砍掉老陈已经去过或买过的,返回得分最高的 N 件。如果评分信号是隐式的(点击、收藏),连预测公式都可以简化成“邻居们对它的加权热度”。
在实际系统中,TopN 的效率很关键。系统先收集邻居们购买过的所有物品作为候选集,排除目标用户已有的物品,然后对每个候选物品算一个加权得分——邻居的打分越高、相似度越高,这个物品的得分就越靠前。
这个过程好比饭友团开了一场“口碑听证会”:每件候选商品陈述自己被多少人推荐、推荐人的靠谱程度,最后按总分排名。老陈看到的推荐页,就是这份“大家说好”的前三名榜单。
- 候选物品$i$ 遍历邻居们评价过而目标用户 $u$ 未接触过的物品。
- 邻居评分$r_{vi}$ 表示邻居 $v$ 对物品 $i$ 的打分,隐式场景下可以换成“是否点击”的 0/1 值。
- 加权累积每个邻居的评分都乘以 $\mathrm{sim}(u,v)$ 再求和,相似度越高的饭友贡献越大。
- 排序截断对所有候选物品按得分降序排列,取前 $N$ 个作为最终推荐结果。
稀疏性问题:潜水党太多怎么办
小区群里 300 个人,真正认真晒过美食清单的不到 10%。老陈想找知音,可和小林共同评价过的店只有 3 家,而系统里大部分用户之间共同评分为 0,相似度算出来全是“白卷”。群里的“潜水党”越多,找饭搭子越难——这就是推荐系统里人尽皆知的稀疏性问题。
评分矩阵里,绝大多数格子是空的,因为用户不可能评价所有物品。衡量稀疏程度的指标叫稀疏率:非零评分数占总格子数的比例越接近 1,数据就越稀疏。与此相伴的是冷启动困境:新用户没有历史行为,系统无法判断他与谁相似;新物品没有评分,也不会出现在任何人的候选集里。
缓解稀疏性的常用策略有四种:一是用浏览、点击、收藏等隐式信号补充评分,把“潜水”也变成数据;二是要求共同评分数量达到阈值再计算相似度,避免“一面之交”的虚假相似;三是引入物品内容特征做冷启动兜底;四是用矩阵分解把高维稀疏矩阵压缩成低维稠密向量。老陈的经验是:与其在全是潜水的群里硬找知己,不如先把大家逛店的行为都记下来,再谈相似度。
\mathrm{sparsity} = 1 - \frac{|R_{\mathrm{non}\mbox{-}\mathrm{zero}}|}{m \times n}- 非零总数$|R_{\mathrm{non}\mbox{-}\mathrm{zero}}|$ 是矩阵中实际有评分的元素个数,也就是用户真正留下的行为记录。
|R_{\mathrm{non}\mbox{-}\mathrm{zero}}| - 全部组合$m \times n$ 是所有用户与所有物品可能组成的评分格子总数。
- 空置比例用 $1$ 减去“已被填写比例”,得到空白比例。这个值越接近 1,矩阵越稀疏。
1 - \frac{|R_{\mathrm{non}\mbox{-}\mathrm{zero}}|}{m \times n}
应用与优化:从口碑群到规模化系统
小区美食群越做越大,老陈发现了新问题:新店开业的信息至少要等饭友去尝过、晒过,才会出现在推荐里;而且群友增加到几千人后,两两比较口味变得异常吃力。这正是 UserCF 落地时面临的两个核心工程挑战:实时性差、计算量随用户数平方增长。
在工业界,UserCF 通常采用“离线计算 + 在线服务”的分层架构。离线阶段,定时(每天或每小时)计算用户间的相似度矩阵,并只保留每个用户相似度最高的 TopK 邻居;在线阶段,当用户请求推荐时,直接查表取出邻居,再对候选物品做加权排序。相似度矩阵本身的存储复杂度是用户数的平方级,因此必须采用稀疏存储——绝大多数用户对的相似度为零,根本不需要占用空间。
除了架构上的“离线算邻居、在线做推荐”,UserCF 还有两个优化方向:一是周期性地用增量数据刷新相似度,防止用户兴趣漂移后推荐跟不上;二是只在活跃用户和热门物品的“有效子集”上计算相似度,砍掉大量垃圾数据。老陈的“口碑群公告”每周更新一次,管理员离线整理群友新晒的店,在线回答“我该去哪吃”——这和工业界的 UserCF 系统是同一个节奏。
- 用户总数$|\mathcal{U}|$ 表示系统中的用户数量。
- 两两配对任意两个用户之间都要计算一次相似度,所以共有 $|\mathcal{U}|^2$ 个可能的关系对。
- 数量级表示$\mathcal{O}$ 表示渐进复杂度,用户从 10 万涨到 100 万,存储压力约增大 100 倍。
AI 应用场景
内容社区:找到你的“兴趣同温层”
阶段:离线召回与相似用户挖掘
解决的问题:新用户进入小红书、豆瓣这类社区后,不知道关注谁、看什么。传统做法靠编辑人工运营推荐位,成本高且无法规模化。UserCF 用行为记录自动寻找“兴趣同温层”,把相似用户赞过、收藏过的内容推给新人。
具体案例:小红书给美妆新手用户推送“和你相似的人也在看”的笔记流,把相似用户的收藏夹作为候选池,再按权重排序。相比单纯的新热榜,点击率常有明显提升。
在线阅读:用相似读者的完读率做预测
阶段:评分预测与排序
解决的问题:阅读平台没有显式的“打分”按钮,用户对一本书的评价只能从阅读时长、完读率、书架加入行为中推断。UserCF 把隐式行为转换成伪评分,再预测用户对其他书籍的偏好分,解决“书海捞书”的难题。
具体案例:起点读书对同一本书的读者群构建相似度网络,当某位读者读完一本书后,系统参考相似读者的高完读书单做加权推荐,显著提升完读率。
电商私域:宝妈群体里的口碑接力
阶段:TopN 推荐生成
解决的问题:母婴、宠物等低频非标品类,单个用户购买行为非常稀疏,直接用物品相似度(ItemCF)很难找到关联。UserCF 在买家群体中寻找“相似妈妈”,按群体的购买口碑推荐,更贴近“小区宝妈群互相种草”的真实决策方式。
具体案例:母婴社区电商在拼团场景中,把相似宝宝月龄、相似城市、相似消费水平的妈妈组成虚拟“口碑圈”,推荐圈内妈妈买过的纸尿裤、辅食组合。
Python 演示
# -*- coding: utf-8 -*-"""UserCF 演示: 找相似用户, 预测评分并生成 TopN 推荐."""from math import sqrt # 评分数据: 用户 -> 物品 -> 评分(1~5 分)ratings = { 'A': {'薯片': 5, '可乐': 4, '泡面': 3, '咖啡': 1}, 'B': {'薯片': 4, '可乐': 5, '泡面': 4, '咖啡': 2}, 'C': {'薯片': 5, '可乐': 5, '咖啡': 1, '巧克力': 4}, 'D': {'薯片': 1, '可乐': 2, '泡面': 5, '咖啡': 5}, 'E': {'可乐': 2, '泡面': 4, '咖啡': 5},} def mean_score(user_rates): """计算一个用户的平均分.""" return sum(user_rates.values()) / len(user_rates) def cosine_sim(a, b): """余弦相似度: 用共同评分的物品计算夹角余弦.""" common = set(a) & set(b) if not common: return 0.0 dot = sum(a[item] * b[item] for item in common) na = sqrt(sum(x * x for x in a.values())) nb = sqrt(sum(x * x for x in b.values())) if na == 0 or nb == 0: return 0.0 return dot / (na * nb) def user_cf(ratings, target, k=2, top_n=3): """UserCF 主流程: 算相似度, 选邻居, 预测评分, 输出 TopN.""" target_rates = ratings[target] sims = [] for user, rates in ratings.items(): if user == target: continue sims.append((cosine_sim(target_rates, rates), user)) sims.sort(reverse=True) neighbors = sims[:k] mean_t = mean_score(target_rates) cand = set() for _, u in neighbors: cand |= set(ratings[u]) - set(target_rates) preds = [] for item in cand: num = 0.0 den = 0.0 for sim, u in neighbors: rates_u = ratings[u] if item in rates_u: num += sim * (rates_u[item] - mean_score(rates_u)) den += abs(sim) if den > 0: preds.append((mean_t + num / den, item)) preds.sort(reverse=True) return [item for _, item in preds[:top_n]] if __name__ == '__main__': print('A 的推荐结果:', user_cf(ratings, 'A'))
sims.append((cosine_sim(target_rates, rates), user))把“相似度 + 用户名”打包成元组,方便统一排序sims.sort(reverse=True)相似度从高到低排,最像的饭友排在列表最前面neighbors = sims[:k]只留下前 K 位核心饭友,这就是邻居选择cand |= set(ratings[u]) - set(target_rates)候选集 = 饭友买过而目标用户没买过的物品集合num += sim * (rates_u[item] - mean_score(rates_u))中心化后的加权分子,把邻居的评分习惯差异先减掉preds.append((mean_t + num / den, item))加上目标用户自己的平均分,还原成“老陈尺子”下的预测分
常见误区
误区两个用户共同评分很多、余弦相似度接近 1,那推荐就一定准,相似度是可以当准确率指标直接用的。
正解相似度高只说明两人的历史记录“长得像”,不等于未来判断也一致。如果共同评分集中在热门物品上,相似度里塞满了大众口味,几乎没有个性化信息;如果共同评分只有三五个,相似度又充满噪声。正确做法是同时检查共同评分数量、用离线指标(准确率、召回率)验证,而不是只看相似度数值。就像老陈和小林都爱吃川菜,但小林还爱喝酒、老陈滴酒不沾,单看川菜维度的相似度会高估整体契合度。
为何常见:相似度是 0 到 1 之间的“好看的数字”,“越大越好”的直觉容易让人忽略它只是估计值,而不是推荐质量的直接度量。
误区反正都叫协同过滤,核心就是“找相似”,用哪个维度都一样,跑出来看哪个指标好用哪个。
正解UserCF 以“人”为纽带,推荐结果体现群体偏好,适合社区氛围浓厚、内容更新快的场景(新闻、论坛、书影音);ItemCF 以“物”为纽带,适合物品丰富、用户行为稳定的购物场景(淘宝的“看了又看”)。用错范式,推荐结果会显得“跑题”。老陈关心“谁和我口味像”,适合 UserCF;如果他只是想知道“这东西还配什么一起买”,ItemCF 更合适。
为何常见:两个名字都带“协同过滤”,如果不深究“用户维度 vs 物品维度”的差异,很容易混淆适用边界。
误区邻居都是口味相似的人,那预测分就是把他们的打分求个平均值,中心化不过是多余的数学绕弯。
正解不同用户的打分习惯差异很大:有人只给 4 到 5 分,有人 1 到 5 分通吃。直接平均会让“手松”的邻居抬高整体预测。正确做法是先减各自均值、加权、再加回目标用户自己的均值。这样借鉴的是“相对偏好”,而不是被个人尺度污染的绝对分数。就像老陈知道小吴的 4 分其实等于别人的 2 分一样。
为何常见:平均是最符合直觉的汇总方式,中心化“多了一步”,容易被初学者当作可有可无的细节省略。
误区UserCF 用的是别的用户的数据,新用户一进来就应该能获得推荐,冷启动是 ItemCF 才有的问题。
正解新用户在系统里一条行为都没有时,系统无法判断“他和谁相似”,这正是用户侧冷启动。常规解法是用注册信息(性别、城市、兴趣标签)做粗略画像,或先推热门榜单,等积累了几条行为后再启动 UserCF。老陈刚进群时,群里也没人知道他喜欢什么,只能先让他选几个感兴趣的标签。
为何常见:只想到“推荐用的是其他用户的数据”这半条推理链,却忽略了 UserCF 的输入恰恰是目标用户自己的历史行为。
面试题
Q1. UserCF 的第一步是什么?
- A.计算物品特征向量
- B.构建用户兴趣画像← 正确答案
- C.训练深度神经网络
- D.随机生成推荐列表
答案:B。UserCF 的一切推断都建立在用户历史行为之上。没有兴趣画像就无法计算相似度,更谈不上选邻居和预测,所以第一步是构建用户兴趣画像。
Q2. Jaccard 相似度的核心思想是什么?
- A.交集大小占并集比例← 正确答案
- B.评分数值的加权平均
- C.关注用户活跃度高低
- D.统计点击次数多少
答案:A。Jaccard 相似度等于两个集合交集大小除以并集大小,只关心“两人共同评价过的物品占总共评价过的比例”,不关心评分高低。
Q3. TopN 推荐中的 N 指的是什么?
- A.返回前 N 个物品的条数← 正确答案
- B.最近 N 天的评分记录
- C.需要采样的用户数量
- D.数据分析师的数量
答案:A。TopN 推荐把候选物品按得分降序排列,取前 N 个作为最终推荐列表返回给用户,N 是推荐条数,与时间、用户数无关。
Q4. 余弦相似度对“有人全打 5 分、有人全打 1 分”的打分尺度不敏感,原因是?
- A.余弦只看向量方向不看模长← 正确答案
- B.余弦会自动修正评分习惯
- C.余弦能消除全部主观偏差
- D.评分尺度被归一化成零一
答案:A。余弦相似度只关心向量夹角,模长被分母约掉了,所以绝对分数的高低不影响结果。这也是为什么工程上要先用中心化修正再算相似度。
Q5. K 近邻中 K 值取得过大,主要会带来什么问题?
- A.引入大量低相关用户致噪声← 正确答案
- B.使相似度计算自动变精确
- C.让评分矩阵变得更稠密
- D.让计算结果必然完全正确
答案:A。K 太大导致“泛泛之交”也被拉进邻居集合,他们的低相似度意见会稀释核心饭友的声音,导致推荐结果趋向平庸、噪声变大。
Q6. 评分预测前做“中心化”的根本目的是什么?
- A.消除用户打分习惯的差异← 正确答案
- B.让所有分数都变成正数
- C.方便对向量进行排序操作
- D.提高评分矩阵的稀疏程度
答案:A。中心化通过减去用户自身均值,把评分从“绝对分数”变成“相对偏好”,消除了不同用户打分松紧不一造成的偏差。
Q7. 以下哪些场景更适合使用 UserCF?
- A.社交社区里寻找同好用户← 正确答案
- B.音频书籍口味相似推荐← 正确答案
- C.图书影音的口碑种草推荐← 正确答案
- D.导航软件的实时路况预测
答案:ABC。UserCF 以“人”为媒介,适合有社区氛围、兴趣群体明显的场景。导航路况是时空传感器数据,与用户口味画像无关。
Q8. 评分矩阵非常稀疏时,可能带来哪些后果?
- A.用户间相似度可靠性降低← 正确答案
- B.共同评分太少导致失真← 正确答案
- C.新用户冷启动更加严重← 正确答案
- D.相似度矩阵必然为空矩阵
答案:ABC。稀疏意味着共同评分的物品很少,相似度要么算不出、要么噪声大,新用户与新品也没有历史可用于关联。但“必然为空”的说法过于绝对,只要有一点非零数据矩阵就不为空。
Q9. 用户规模从 10 万增长到 100 万,相似度矩阵的存储量最坏情况下如何变化?
- A.增长到原来的一百倍← 正确答案
- B.增长到原来的十倍
- C.和用户规模增长相同
- D.存储开销完全没变化
答案:A。相似度矩阵规模为 O(|U|²),用户数放大 10 倍,矩阵元素数量放大 100 倍。所以必须用稀疏存储或只保留 TopK 邻居来控制开销。
Q10. 以下哪些是缓解 UserCF 冷启动问题的常用策略?
- A.用物品内容特征加权弥补← 正确答案
- B.给新用户推荐热门物品榜← 正确答案
- C.引导新用户完善兴趣标签← 正确答案
- D.主动丢弃所有未登录行为
答案:ABC。冷启动的本质是缺少行为数据,内容特征、热门榜、兴趣标签都能在无历史时提供兜底信号。丢弃未登录行为反而浪费了可用的信号,不可取。