news 2026/9/9 4:30:44

Memory-Based协同过滤全解析:从评分矩阵到相似度计算与工程落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Memory-Based协同过滤全解析:从评分矩阵到相似度计算与工程落地

1. 为什么在深度学习时代,还要啃"内存式协同过滤"

先说个身边场景:你在旅游App刷完杭州西湖的攻略,底下给你推了西溪湿地、乌镇。表面看是系统"很懂你",实际上它可能根本不知道西湖有多美,它只是记住了——之前和你行为很相似的那批人,看完西湖之后还点了什么。这个"记忆并查找相似关系"的过程,就是基于内存的协同过滤,英文叫Memory-Based Collaborative Filtering,简称Memory-Based CF。

这篇是这个系列笔记的第二篇,我打算把Memory-Based CF的来龙去脉一次讲透。它适合什么人看?正在系统学推荐系统、但被各种深度学习模型绕晕的初学者;以及需要快速给业务拉一个可解释基线方案的工程同学。读完之后,你能搞清楚User-Based和Item-Based两条路线的完整计算流程、相似度怎么选、预测公式为什么长那样、真实项目里会踩哪些坑。

1.1 记忆就是一切:Memory-Based CF 到底是什么

Memory-Based CF从名字上拆开看,"Memory"指的是算法运行时直接使用用户的历史行为数据——也就是用户和物品之间的交互记录,比如评分、点击、收藏、购买。它不像矩阵分解或者深度学习那样先把数据压成一套隐向量,而是把这些历史记录当成"记忆"直接参与计算,用户来了就靠这份记忆找相似用户、找相似物品。

它包含两个经典分支:User-Based Collaborative Filtering,基于用户的协同过滤,核心是"和你口味相似的人喜欢了什么,就给你推荐什么";Item-Based Collaborative Filtering,基于物品的协同过滤,核心是"你喜欢的这个物品,和哪个物品长得像,就推荐那个"。

很多人第一次学到这里会有一个疑惑:既然深度学习都这么强了,为什么还要先啃这种"土办法"?我的答案是,协同过滤是整个推荐系统里少有的、用一句话就能解释清楚的推荐逻辑,它给后面所有复杂模型提供了"效果底线"和"可解释性参照"。你把Memory-Based CF吃透,再去看矩阵分解、神经协同过滤,会发现它们的核心还是在做同一件事:描述用户和物品之间的相似关系。

1.2 "现用现算"和"训练模型"的本质差异

Memory-Based这个词,是相对于Model-Based(基于模型)的协同过滤而言的。Model-Based方法会预先训练一个模型,把用户和物品映射到低维向量空间,预测时靠模型参数算出一个分数;而Memory-Based方法没有显式的训练过程,它是在收到推荐请求后,直接在当前这份历史数据上找相似关系。

用生活里的类比来讲:Memory-Based像一个从业十几年的老店员,你进店说想看悬疑小说,他凭脑子里记住的"像你这样的客人最后都买了东野圭吾"来给你推荐;Model-Based则像一个统计学家,他先收集几千个顾客的购买记录,归纳出"喜欢东野圭吾的人往往也喜欢紫金陈",然后用这个统计规律来推荐。前者依赖的是活生生的记忆,后者依赖的是归纳出来的规律。

这种"现用现算"的特性带来两个直接结果:一是解释性特别好,你能明确说出"因为用户A和目标用户口味相似,并且A给这个物品打了高分,所以推荐给你";二是实现简单,不需要训练超参,甚至不需要GPU。代价是当数据量特别大时,每一次在线计算相似度的开销都很大,所以工程上通常会做一些预计算和缓存,这个我放到第7章详细讲。

1.3 依然值得学的三个理由

第一,可解释性。"因为你喜欢的物品和另一个物品相似度很高,所以推荐它",这句话产品经理和用户都听得懂。在很多场景,尤其电商、酒旅、内容社区,可解释性直接和转化率挂钩,用户看到一个莫名其妙的推荐是会流失的。

第二,它是完美的Baseline。我做推荐项目时有个习惯:任何新模型上线前,必须先跟一套Memory-Based CF对比。如果深度学习模型连一个相似度加权都打不过,那说明要么特征没做好,要么数据量根本不够支撑复杂模型。

第三,小数据场景下它是真正的生产力。很多垂直领域的推荐系统,比如某个只有几千用户的旅游攻略站,用户交互矩阵非常稀疏,复杂的图神经网络根本学不出来,但一个朴素的Item-Based CF反而能给出稳定且合理的结果。这也就是为什么"协同过滤算法旅游推荐系统"这样的需求一直没消失——它简单,但它在很多真实场景里就是够用。

2. 一切从评分矩阵开始:读懂用户-物品交互表

无论是User-Based还是Item-Based,所有Memory-Based CF的起点都是同一张表:用户-物品交互矩阵。搞清楚这张表长什么样,后面所有计算都只是围绕它转。

2.1 用一张景点评分表理解数据形态

假设我们做一个旅游推荐系统,数据里有5个用户(A、B、C、D、E),5个景点(西湖、灵隐寺、乌镇、西溪湿地、千岛湖),用户给景点打分,分值1到5,0表示没去过、没评分。那么评分矩阵就是这样:

用户西湖灵隐寺乌镇西溪湿地千岛湖
A42334
B55143
C5?143
D24421
E12512

行是用户,列是物品,交叉点就是交互记录。我们假设用户C去过西湖、乌镇、西溪湿地、千岛湖,但没去过灵隐寺,现在要预测的,就是C会给灵隐寺打几分。

这张表就是"记忆"的全部载体。User-Based走路是按行看:找到和C这一行最像的其他行,让它们来填C的空缺;Item-Based走路是按列看:找到灵隐寺这一列和其他列的关系,用C已经打过分的数据来推测空缺值。本质上都是矩阵操作,只是看矩阵的方向不同。

2.2 稀疏性:Memory-Based 方法最大的隐形敌人

真实场景的评分矩阵不会像我上面这张表这么清爽。一个电商平台可能有1亿用户、1000万商品,但一个用户一辈子都不会和超过几百个商品发生交互,矩阵里99.9%以上的位置都是空白。

这就叫稀疏性,它是Memory-Based CF最核心的敌人。因为相似度计算依赖"两个用户共同评分过的物品",如果A只看过西湖,B只看过千岛湖,两人没有任何交集,那他们之间的相似度就只能算0,没法建立联系。更麻烦的是,即使有交集,如果交集只有1-2个物品,算出来的相似度也非常不可靠,可能纯属偶然。

所以我一直强调,学习阶段用小矩阵理解公式没问题,但脑子里要绷着一根弦:这些公式在稀疏矩阵上跑出来是什么样,和教科书案例完全是两码事。后面我会单独用一章讲这些坑。

2.3 显式反馈与隐式反馈的不同处理方式

评分矩阵里的值不一定是分数,它还分显式反馈和隐式反馈。

显式反馈,就是用户明确表达喜好的行为,典型的是1-5星评分、点赞、点踩。它的优点是信号明确,5分就是喜欢,1分就是不喜欢;缺点是数据往往很稀疏,因为用户主动评分的行为成本太高了。

隐式反馈,是用户无意识留下的行为痕迹,比如点击了商品详情页、收藏了攻略、播放了30秒视频、在酒店详情页停留了2分钟。它的优点是数据量大,几乎每个用户每天都会产生;缺点是没有明确的"负样本",没点击不代表不喜欢,可能是根本没看到。

Memory-Based CF处理这两类数据的方式不太一样。显式评分直接用皮尔逊相关系数这类方法效果最好;而隐式反馈通常会被转成0/1的二值化表示,点击过记1,没点击记0,然后相似度的计算往往就会换成接下来会讲到的Jaccard相似度。这在我实践里是个非常重要的区分点:拿二值化的点击数据去算皮尔逊相似度,虽然也能出结果,但对"评分尺度"的理解会变形,效果通常不如直接算Jaccard或余弦。

3. User-Based CF:找到"品味相近的人",再让他们帮忙打分

User-Based CF是最早被提出、也最符合直觉的协同过滤思路。它做的事情可以总结成一句话:如果你和一群用户在历史行为上很像,那这群用户喜欢但你还没看过的东西,大概率你也会喜欢。

3.1 完整工作流程四步走

为了不把概念搞混,我把完整流程拆成四个步骤:

  1. 构建评分矩阵:把用户对物品的评分整理成行是用户、列是物品的矩阵。
  2. 计算用户相似度:两两计算用户之间的相似度,得到一张用户相似度矩阵。
  3. 选出近邻用户:对目标用户来说,挑出相似度最高的一批用户作为"邻居",数量通常用K控制,比如Top-10或Top-20。
  4. 加权预测评分:用邻居对目标物品的评分,结合相似度作为权重,预测目标用户对没接触过的物品的打分,最后按预测分数排序推荐。

听起来不复杂,但第2步和第4步的细节,很多人初学时会理解错。我展开讲。

3.2 用户相似度到底怎么算

计算两个用户之间的相似度,常见的有两种思路。

余弦相似度:把用户对所有物品的评分看成一个向量,计算两个向量之间的夹角余弦值。公式是:

cos(u, v) = Σ(r_ui × r_vi) / (√Σ(r_ui²) × √Σ(r_vi²))

它的直觉是:两个向量方向越一致,相似度越高。

皮尔逊相关系数:它和余弦的区别在于,计算前先减掉用户自己的评分均值,公式是:

sim(u, v) = Σ(r_ui - r̄_u)(r_vi - r̄_v) / √(Σ(r_ui - r̄_u)² × Σ(r_vi - r̄_v)²)

这里r̄_u是用户u的全部已评分物品的均值。为什么要减均值?因为不同用户的打分尺度完全不同,有的人习惯给高分、一片赞美,有的人很严格、能打3分就算不错。减掉均值之后,保留的是"相对自己习惯的偏离程度",这样两个用户之间才有可比性。

实践里,我见到过不少项目直接用余弦相似度做User-Based CF,尤其在隐式反馈场景下。但在显式评分场景,皮尔逊通常更稳,因为它把用户打分尺度这个系统误差去掉了。第5章我再详细对比。

3.3 评分预测:别漏掉用户自己的打分尺度

找到邻居之后,预测目标用户u对物品i的评分,最常用的公式是:

pred(u, i) = r̄_u + Σ_{v∈N} sim(u, v) × (r_vi - r̄_v) / Σ_{v∈N} |sim(u, v)|

这个公式值得仔细拆。它做了三件事:

第一,对邻居的评分减掉邻居自己的均值r_vi - r̄_v,把邻居的"个人尺度"去掉。第二,用相似度作为权重,邻居越像你,贡献越大。第三,预测值是在目标用户自己的均值r̄_u上偏移,把目标用户的"个人尺度"加回来。

很多人第一次写User-Based CF会犯的错就是:简单地用相似度加权邻居的原始评分,比如直接算∑ sim * r_vi / ∑ sim。在小规模玩具数据上,这样可能也能跑出结果,但在真实评分数据上问题很大——如果目标用户习惯给3分,而邻居习惯给5分,加权出来的分就虚高了。所以说,保留这个均值偏移不是锦上添花,而是必要操作。

3.4 一个小例子,手算给你看

用第2章那张景点评分表,我们来预测用户C对灵隐寺的评分。

先算用户B和C的相似度。B和C共同评分过西湖、乌镇、西溪湿地、千岛湖这四个景点,数值分别是:

  • B:(5, 1, 4, 3)
  • C:(5, 1, 4, 3)

两人的均值都是3.25,各自中心化后向量为(0.75, -2.25, 0.75, -0.25),完全一致。所以不管用余弦还是皮尔逊,B和C的相似度都等于1,是完美正相关。

再看用户D和C。D在共同景点上的评分是(2, 4, 2, 1),中心化后为(-0.25, 1.75, -0.25, -1.25),和C的向量方向正好相反,算出来的皮尔逊相似度约为-0.66,是负相关。

如果K=2,并且我们允许把D也选作邻居,那预测公式就会变成:

pred(C, 灵隐寺) = 3.25 + [1×(5-3.25) + (-0.66)×(4-2.25)] / (1 + 0.66) ≈ 3.61

但如果我们的候选邻居只保留正向相似用户,那邻居就只剩B,预测结果为:

pred(C, 灵隐寺) = 3.25 + 1×(5-3.25) / 1 = 5

你看,负相关邻居的引入会直接拉低预测分。所以很多实现里会刻意过滤sim > 0的邻居,或者把K设大一点,让单个负相关邻居影响减小。这算是我在代码里踩过的一个实际教训,后面第6章我再结合代码说。

4. Item-Based CF:把矩阵转个方向,从物品找"邻居"

Item-Based CF在电商领域用得比User-Based更广,你可能听过那句经典的话:"买了这个商品的用户还买了什么"。它就是Item-Based CF的典型应用。它和User-Based用的数学工具几乎一模一样,只是把计算的视角从"行"换到了"列"。

4.1 从"用户邻居"到"物品邻居"的视角切换

User-Based CF关注的是"谁和我像",Item-Based CF关注的是"什么物品和我喜欢的物品像"。它的核心逻辑是:如果历史上大量用户都给物品i和物品j打了相近的分数,那这两个物品在用户眼中就是"相似物品";当目标用户喜欢物品i时,就可以把物品j推荐给他。

这里有个很容易绕进去的坎:为什么说物品相似度计算结果不依赖具体某个用户?因为物品i和物品j的相似度,是通过"所有用户对它们两个的评分"来算的,而不是通过目标用户一个人的偏好。比如灵隐寺和西溪湿地,只要大量用户对两者的评分趋同,它们就算在目标用户还没有任何评分的情况下,也是一个相似对。

实现上,User-Based矩阵操作可以理解为:先对矩阵做转置,让列变成行——物品变成"用户"、用户变成"物品"——然后套用和User-Based完全一样的相似度计算和预测流程。理解这一点之后,很多代码实现会变得非常清爽。

4.2 物品相似度的计算细节

Item-Based的相似度矩阵在离线阶段就能算好,这是它工程上比User-Based更讨喜的重要原因。我们还是用上面的评分表,展示灵隐寺和其他景点的相似度。

灵隐寺这一列在所有用户上的评分是:A=2、B=5、C=0、D=4、E=2。计算"灵隐寺"和"西湖"的普通余弦相似度,只保留两个物品都有评分的用户(A、B、D、E),得到约0.91;灵隐寺和西溪湿地的普通余弦相似度约0.93;和千岛湖约0.85;和乌镇约0.73。

如果我按相似度排序,灵隐寺最像的景点是西溪湿地,然后是西湖。这个结果在直觉上也很合理——去杭州的人通常会同时安排这几个景点,它们的评分模式自然就趋同。

4.3 修正余弦相似度为什么在Item-Based里更常用

我在实际项目中算Item-Based相似度,几乎不用普通余弦,而是用修正余弦(Adjusted Cosine)。公式长这样:

sim(i, j) = Σ_u (r_ui - r̄_u)(r_uj - r̄_u) / √(Σ_u (r_ui - r̄_u)² × Σ_u (r_uj - r̄_u)²)

注意,这里减的还是用户u的评分均值,也就是用每个用户自己的评分习惯把分数先校准一遍,再算物品之间的相似度。

为什么普通余弦在Item-Based里不够好?因为不同用户的评分标准差异太大了。比如用户A习惯严打分,西湖只给4分、灵隐寺只给2分;用户B是个"夸夸怪",给所有景点都打5分。如果直接用原始分数算余弦,B的"高分会放大一切",会让那些实际口味相距甚远的物品看起来也相似。减去用户均值后,我们看的是"这个用户比平时更喜欢哪个景点",而不是"这个用户打了多少分"。

我做这一行的一个经验是:如果你发现相似度矩阵里绝大多数值都偏高,比如都超过0.8,那大概率就是没有做中心化/均值偏移。这时候别急着调算法,先把相似度函数换掉,效果往往立竿见影。

4.4 Item-Based的预测公式

Item-Based CF的预测公式比User-Based简单一些,常见两种。一种是纯加权:

pred(u, i) = Σ_{j∈N} sim(i, j) × r_uj / Σ_{j∈N} |sim(i, j)|

其中N是目标用户u已经评分过、且和目标物品i相似度最高的K个物品集合。它的逻辑是:拿用户对相似物品的打分,按相似度加权,推测他对新物品的打分。

另一种也会加用户均值偏移,公式变成:

pred(u, i) = r̄_u + Σ_j sim(i, j) × (r_uj - r̄_u) / Σ_j |sim(i, j)|

用我们上面的矩阵,预测用户C对灵隐寺的评分。C已评分的物品有西湖5、乌镇1、西溪湿地4、千岛湖3。我们取与灵隐寺最相似的两个已评分物品:灵隐寺-西溪湿地相似度0.93,灵隐寺-西湖相似度0.91。代入纯加权公式:

pred(C, 灵隐寺) = (0.93×4 + 0.91×5) / (0.93 + 0.91) ≈ 4.49

约等于4.5分。这个结果挺好——C和B口味高度一致,B给灵隐寺打了5分;而C对西溪湿地和西湖的评分很高,Item-Based同样推断出他会喜欢灵隐寺。两种思路殊途同归。

5. 相似度公式大乱斗:余弦、皮尔逊、修正余弦、Jaccard

可能你已经发现了,Memory-Based CF最核心的计算都在"相似度"上。这一章我不再堆叠公式,而是把几个相似度指标放到一起对比,说清楚各自的脾气和适用场景,帮你少走弯路。

5.1 四种相似度的公式与直觉

余弦相似度,本质上衡量的是两个向量的方向一致性。它适合那种"关注点是否一致"的场景,特别是0/1二值数据,比如用户有没有点击过某类物品。缺点是对评分尺度敏感,一个打2分的用户和一个打5分的用户,即便偏好完全相同,计算出的余弦也可能偏低。

皮尔逊相关系数,等价于把两个向量都减去各自的均值后再算余弦,所以它相当于"中心化后的余弦"。它的优点是消除了用户个人评分尺度的影响,非常适合显式评分数据;缺点是如果共同评分样本太少,非常容易算出1或-1这种极端值,反而失真。

修正余弦相似度,它和皮尔逊的核心区别在于减的对象。皮尔逊减的是"当前用户的所有评分均值",修正余弦在计算物品相似度时,对每个用户也减去"该用户的所有评分均值",但整个运算是在所有共同评分的用户上铺开的。所以在Item-Based场景、尤其评分数据上,它基本是首选。

Jaccard相似度,公式是交集数量除以并集数量,只关心两个用户/物品"共同出现过的元素",完全忽略具体分数。它的优势是计算简单、抗数值噪声,适合点击、收藏这类隐式反馈;缺点是没法区分"很喜欢"和"一般般"的区别。

5.2 一张表说清楚怎么选

相似度指标适用数据类型是否消除评分尺度偏差主要使用场景
余弦相似度数值型、0/1型隐式反馈、二值点击、特征向量
皮尔逊相关系数显式评分User-Based CF
修正余弦相似度显式评分Item-Based CF
Jaccard相似度0/1型不涉及隐式反馈、行为交集判断

这张表大家可以收藏。我每次给新项目做方案时,基本就是按着这个思路选的:先确认手里是显式评分还是隐式行为,再决定用哪个相似度,然后去算相似度矩阵。

5.3 共同评分数量:被忽略的信任因子

这里要单独提醒一个很隐蔽的坑:相似度公式本身不会告诉你"这个相似度可信不可信"。两个用户只有一个共同评分,且恰好都打了5分,皮尔逊算出来是1;两个用户有200个共同评分,算出来是0.9。从数值上看,前者比后者还高,但显然200个共同评分算出的0.9更可靠。

我常用的补救方法是给相似度加一个"显著性加权":

sim_final(u, v) = sim_raw(u, v) × min(1, common_count / α)

其中α是个阈值,通常取25到50之间。共同评分数量超过α的,权重是1,完全保留;不足α的,按比例打折。这个技巧在数据量小的冷启动阶段特别有效,能显著减少偶然相似带来的误推。

6. 用Python从零实现一套最小可用CF

理论讲再多,不如对着代码跑一遍。这一章我用Python手写一个最小可用的Memory-Based CF,不依赖任何推荐系统框架,只用到Pandas和NumPy。

6.1 数据准备:构造一个能跑通的小数据集

我把第2章的评分表直接写成DataFrame,方便复现:

import pandas as pd import numpy as np R = pd.DataFrame({ '西湖': [4, 5, 5, 2, 1], '灵隐寺': [2, 5, 0, 4, 2], '乌镇': [3, 1, 1, 4, 5], '西溪湿地': [3, 4, 4, 2, 1], '千岛湖': [4, 3, 3, 1, 2] }, index=['A', 'B', 'C', 'D', 'E']) print(R)

输出就是前面那张评分矩阵。0表示用户没有评分。

6.2 User-Based实现与预测过程

先实现皮尔逊相似度,只考虑两个用户共同评分过的物品:

def user_pearson(u, v): common = [item for item in R.columns if R.loc[u, item] > 0 and R.loc[v, item] > 0] if len(common) < 2: return 0.0 ru = R.loc[u, common] rv = R.loc[v, common] ru_centered = ru - ru.mean() rv_centered = rv - rv.mean() denom = np.sqrt((ru_centered ** 2).sum() * (rv_centered ** 2).sum()) if denom == 0: return 0.0 return (ru_centered * rv_centered).sum() / denom

再写预测函数。有一个关键点:邻居只保留相似度大于0的用户,同时用均值偏移做校准:

def predict_user_based(u, item, k=2): ru_bar = R.loc[u, R.loc[u] > 0].mean() candidates = [] for v in R.index: if v == u: continue if R.loc[v, item] == 0: continue sim = user_pearson(u, v) if sim > 0: rv_bar = R.loc[v, R.loc[v] > 0].mean() candidates.append((v, sim, R.loc[v, item], rv_bar)) candidates.sort(key=lambda x: x[1], reverse=True) top = candidates[:k] if not top: return ru_bar num = sum(sim * (rating - rv_bar) for _, sim, rating, rv_bar in top) den = sum(abs(sim) for _, sim, _, _ in top) return ru_bar + num / den print(predict_user_based('C', '灵隐寺', k=2))

按前面手算的逻辑,B和C的相似度为1,E和C的相似度是负值、会被过滤掉,所以预测值输出约为5.0。这跟手工计算一致。

6.3 Item-Based实现与预测过程

Item-Based的代码和User-Based有点像,但计算对象变成了物品列。这里我用余弦相似度,代码更直观,实际工程中你把它换成修正余弦即可:

def item_cosine(i, j): common = [u for u in R.index if R.loc[u, i] > 0 and R.loc[u, j] > 0] if len(common) < 2: return 0.0 ri = R.loc[common, i] rj = R.loc[common, j] denom = np.sqrt((ri ** 2).sum() * (rj ** 2).sum()) if denom == 0: return 0.0 return (ri * rj).sum() / denom def predict_item_based(u, item, k=2): rated_items = [j for j in R.columns if j != item and R.loc[u, j] > 0] candidates = [] for j in rated_items: sim = item_cosine(item, j) if sim > 0: candidates.append((j, sim, R.loc[u, j])) candidates.sort(key=lambda x: x[1], reverse=True) top = candidates[:k] if not top: return 0.0 num = sum(sim * rating for _, sim, rating in top) den = sum(abs(sim) for _, sim, _ in top) return num / den print(predict_item_based('C', '灵隐寺', k=2))

前面手算过,与灵隐寺最相似的两个已评分物品是西溪湿地和西湖,预测输出约为4.49。

6.4 两种方法预测同一个评分的差异分析

同一个问题,User-Based给出5.0,Item-Based给出4.49,出现了差异,这是为什么?

User-Based的预测路径是"找相似用户"。因为B和C的评分几乎完全一致,B给灵隐寺打了5分,所以结果接近5。Item-Based的路径是"找相似物品"。C给西溪湿地和西湖打了高分,而这两个物品和灵隐寺相似,所以加权出大约4.5。路径不同,结果自然不同。

这个差异在真实项目里其实是好事。如果你在线上同时跑两套模型,用户刚点过和某个物品相似的另一个物品时,Item-Based会更灵敏;当存在一个口味极其相似的用户群体时,User-Based会更灵敏。很多团队会把两者结果做加权融合,或者用其中一方做召回、另一方做排序特征,效果往往比只用一套好。

7. 工程落地时会踩的坑:稀疏、冷启动、热门偏差

把代码跑通只是万里长征第一步。到了真实业务数据上,Memory-Based CF会遇到几个很顽固的问题,我在项目里一个个踩过,拿出来说透。

7.1 无共同评分,相似度直接失灵

真实评分矩阵非常稀疏,两个用户之间可能一个共同评分都没有。这时候User-Based的相似度矩阵就像个漏勺——大量用户之间的相似度是0,根本形成不了"信任链"。

这事的本质原因是:评分是用户主动产生的,成本太高,绝大多数用户只对极小一部分物品留下过痕迹。

应对思路我常用两种:

  • 把评分矩阵从显式评分扩展到隐式行为数据。比如用户在景点详情页的停留时长、点击次数,这些数据密度高很多,能补上相似度空洞。
  • 用"基于内容"的相似度做补充。比如两个景点都属于"人文历史类",即使没有共同评分,也可以给一个初始相似度,等后续交互数据积累后再逐渐让位于协同过滤。

7.2 热门物品绑架了相似度

假设西湖是这个旅游推荐系统里最热门的景点,80%的用户都评过分。那么计算任意两个用户相似度时,西湖几乎总是落在"共同评分"里,它的评分贡献会盖过其他小众景点。结果是:两个口味完全不同的人,仅仅因为都给西湖打了分,可能就会算出很高的相似度。

这是热门偏差。解决办法是给物品加权,降低热门物品的影响力。常用的一个技巧叫IUF(Inverse User Frequency),类比TF-IDF的思路:

w_i = 1 / log(1 + n_i)

其中n_i是给物品i评过分的用户数。把评分r_ui乘以这个权重后再去算相似度,热门物品因为权重小,就不会再主导相似度计算了。

还有个更简单粗暴的办法:在计算相似度时,如果共同评分里包含热门物品,可以直接给它设一个很小的全局权重。我在很多小项目里都是这么干的,效果稳定,又不用引入太多参数。

7.3 冷启动不止新用户,还有新物品

冷启动分两种。新用户没有历史评分,Memory-Based CF找不到他的邻居,也无法通过他已评物品做Item-Based推荐;新物品没有用户评分,它不会出现在任何人的相似物品集合里,自然也就无法被推荐出去。

处理新用户,我通常先给一个基于热度的兜底推荐,比如"热门景点榜",等用户产生几条交互后,再用协同过滤;处理新物品,则要靠内容特征来"搭桥",比如新上架一个景点,先用它的地理位置、类型标签、价格区间去找内容上相似的旧物品,把相似度先算出来,这样等有了评分后模型已经能推了。

7.4 离线预计算相似度矩阵才是工程常态

"Memory-Based"这个名字很容易让人误以为:每次来了推荐请求,都要现场把所有用户/物品相似度算一遍。如果真这么做,线上延迟会爆炸。

工程上的标准做法是:把"相似度计算"这个重活放到离线,定时用Spark或者普通Python批处理任务算好用户相似度矩阵或物品相似度矩阵,存储到Redis、内存KV存储或者向量数据库里。线上推荐时,只需要拿到目标用户/目标物品,去查它的Top-K邻居,然后做一次轻量加权计算。整个过程毫秒级就能完成。

订单相关项目里,我会再设置一个增量更新任务,比如每15分钟或每小时把新增的交互记录并入,重新计算受影响的那部分相似度。全量重算的成本太高,没必要每次跑。把这个流程想清楚,Memory-Based CF就具备了支撑日活百万级业务的能力。

8. User-Based还是Item-Based:一张决策清单

学完两种算法,实践中总会有一个绕不开的问题:该用哪个?

8.1 关键维度对比

维度User-Based CFItem-Based CF
核心视角找相似用户找相似物品
计算规模用户数越多,矩阵越大物品数越多,矩阵越大
可解释性"和你有相同口味的人还喜欢…""因为你喜欢X,所以推荐相似的Y"
实时性对用户新行为敏感,能快速跟进口味变化对物品上新更敏感,用户行为变化反映稍慢
冷启动压力新用户问题突出新物品问题突出
长尾推荐相对容易发现小众新物品容易集中推荐热门相似物品
适用场景社交、资讯、用户群体特征明显的领域电商、内容社区、酒旅、景点

8.2 我选择时的经验原则

我不喜欢记死规则,但下面几条原则是经过几个项目验证过的,可以帮你快速做决策:

  • 看用户数和物品数的相对规模。用户数远大于物品数,推荐用Item-Based,因为物品相似度矩阵小、计算快、且物品实体相对稳定。旅游推荐系统里,景点数量往往远小于用户量,所以Item-Based天然合适。反过来,如果物品数量庞大且更新极快,比如新闻信息流,User-Based可能更灵活,因为它能通过用户的邻居快速把新物品"传"给目标用户。
  • 看重可解释性的业务,优先Item-Based。"因为你喜欢乌镇,所以推荐西塘"比"因为和你类似的游客喜欢西塘"更容易让用户接受。
  • 小数据起步阶段,两套都跑,取并集做召回。再多说一句,很多时候你不必在User-Based和Item-Based之间二选一,把两者的推荐结果合并作为召回池,再让排序模型去打分,是工业界非常常见的做法。

8.3 笔记之外的结束语

最后分享一点个人习惯。我不管后续要用多复杂的模型,接手一个新推荐项目的第一天,一定先写一套Item-Based CF跑出基线,同时配一套User-Based CF做对比。不是因为它们能直接达到最好的效果,而是因为它们是整个推荐系统领域最透明的镜子——如果连这面镜子都跑不出合理结果,说明数据质量或者相似度选择一定有问题,这时候去调深度学习模型纯粹是浪费时间。

另外有个小技巧想送给你:实际业务里,相似度矩阵结果一定要定期人工抽检。我遇到过很多次,模型离线指标涨了,但线上推荐的结果莫名其妙,查到最后都是相似度矩阵里出现了"西湖和千岛湖相似度接近1"这种反直觉数据。数据清洗、评分归一化、去热门这些基础工作,比选一个高级模型更影响最终体验。先把Memory-Based CF这层地基打牢,再往上盖模型,你会少走很多弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 4:28:06

基于SpringBoot+Vue的学校防疫物资管理系统解析

最近在帮几个准备做毕业设计的同学看项目&#xff0c;发现好几个人不约而同选了“学校物资管理系统”这个方向。这类管理系统在课设、毕设里确实非常常见&#xff0c;但大多数同学找到的参考项目要么只有后端没有前端&#xff0c;要么前端直接用非前后端分离的模板渲染&#xf…

作者头像 李华
网站建设 2026/9/9 4:27:36

Flutter跨端开发实战:从零搭建OpenHarmony计数器应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 4:27:25

五大降AI率工具实测:从原理到实操,让论文更像人写的

先说个背景&#xff0c;2025届的毕业生现在基本都懂一个词叫“AIGC疑似率”。不少学校已经把论文里的“AI检测率”写进了学位论文质量审核标准&#xff0c;要求控制在30%甚至20%以内。我见过很多学生拿着自己辛辛苦苦写的初稿&#xff0c;一查AI率直接飙到70%以上&#xff0c;整…

作者头像 李华
网站建设 2026/9/9 4:26:53

技嘉猎鹰白金电源怎么选?功率计算与装机实战指南

每次看到装机清单里只剩电源没定&#xff0c;很多人的第一反应就是“额定功率买大点总没错”。这个思路不能说全错&#xff0c;但放在预算有限、机箱里还要走线、显卡又是瞬时功耗大户的今天&#xff0c;实在不算最优解。这两天我专门把技嘉猎鹰白金电源的选型逻辑从头捋了一遍…

作者头像 李华
网站建设 2026/9/9 4:26:34

加密锁驱动与授权机制详解:从硬件原理到运维排查

简介&#xff1a;面向已购买广联达软件并安装590或592驱动的用户&#xff0c;这款深思S4 2.4写锁与续期工具包&#xff0c;专门用于在较长周期内解决软件授权期限和加密锁管理问题&#xff0c;尤其适合需要搭配广材助手完成工程造价编制、投标报价、算量对量等工作的工程预算与…

作者头像 李华
网站建设 2026/9/9 4:25:49

光伏微逆安全核芯:数字隔离器如何防高压击穿与共模干扰

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华