6 个用户 × 8 个物品,48 个格子里只有 24 个有评分。 按相似度加权预测,给用户 0 推的是台灯、耳机、充电宝;留一法命中 3/6,热门基线 0/6。
上一节把文本变成向量,这一节把「人和物品」变成矩阵。 推荐系统的核心问题是:矩阵里大部分格子是空的,怎么猜。
一、评分矩阵是个稀疏洞
Demo 造了 6 个用户对 8 个物品的评分表,1 到 5 分,0 表示没评过。 48 个格子里只有 24 个有值,50% 是空的。真实电商的稀疏度会到 99% 以上。 推荐系统要干的活就是:把空的那一半填出来,然后挑分数最高的几个推给人。 注意「0 表示没评过」这个约定——如果直接用 0 参与计算,会算成「这个人非常讨厌这个商品」。 所以代码里所有地方都要乘一个「有没有评过」的掩码。
二、相似度:只看共同打过分的东西
Demo 算的是用户之间的余弦相似度,但只取两边都打过分的格子。 这一步很关键:共同打分的数量太少时,相似度不可信。 结果:用户 1 和用户 5 都只和用户 0 有 1 个共同评分,余弦相似度却是 1.000; 用户 3 一个共同评分都没有,相似度 0.000。 「只有 1 个共同评分就给出 1.000 的相似度」——这正是协同过滤最脆弱的地方。 工程上通常要求共同评分数达到阈值(比如 5 个)才算数。 所以 Demo 那张表里我特意把「共同打分数」这一列打出来, 就是为了让「相似度 1.000」这句话有个限定条件。
三、加权预测与推荐
预测的算法是:把其他用户的评分按相似度加权平均。相似的权重高,不相似的权重低,没评过的不参与。 用户 0 没评过的四个物品,预测分是:耳机 4.00、台灯 4.49、充电宝 4.00、键盘 3.50。 取前三就是推荐列表:台灯、耳机、充电宝。 而「推最热门的三个」给出的是耳机、台灯、充电宝——三个重合。 这么小的数据,协同过滤和热门推荐给的答案几乎一样。 能看出差别的只有留一法那两行:3/6 对 0/6。
四、留一法:遮一个,看能不能猜回来
评估推荐系统的经典做法是留一法:把每个用户的一个已知评分遮住,用剩下的数据去预测, 看能不能把这件物品排进前三。 Demo 的结果:协同过滤命中 3/6,热门推荐 0/6。 热门推荐一个都没中,因为它推的就是「大家都买过的」,而这些早就在用户已评分的列表里了。 0/6 这个数很说明问题:热门推荐对老用户几乎没有信息量。 它的价值只在冷启动——完全不知道这个人是谁的时候,推热门至少不会太离谱。 所以真实系统里两者是叠着用的:新用户先推热门,攒够行为之后再切到个性化。
五、动手 Demo:用户相似度和热门基线
造一个 6×8 的部分填充评分矩阵; 算用户 0 与其他用户的余弦相似度(只取共同打分项),并打出共同打分的数量; 按相似度加权预测用户 0 没评过的物品,取前三;再和「推热门」对比;最后用留一法评估两种做法。
def cos(a, b):
m = (a > 0) & (b > 0) # 只看共同打过分
if not m.any():
return 0.0, 0
va, vb = a[m], b[m]
return float(va @ vb / (np.linalg.norm(va)
* np.linalg.norm(vb) + 1e-9)), int(m.sum())
num += sims[v] * (R[v] > 0) * R[v] # 掩码不能少
den += sims[v] * (R[v] > 0)
pred = np.where(den > 0, num / np.maximum(den, 1e-9), 0.0)
实跑输出:
--------------------------------------------------------------------
推荐系统:用户相似度和热门基线,谁更准
--------------------------------------------------------------------
用户-物品矩阵 6 用户 × 8 物品
已打分格子 24 / 48(50% 是空的)
和用户0 比 共同打分数 余弦相似度
-----------------------------------
用户1 1 1.000
用户2 3 0.941
用户3 0 0.000
用户4 3 0.887
用户5 1 1.000
用户0 没买过的 协同过滤预测分 多少人买过
-------------------------------------------
耳机 4.00 3
台灯 4.49 3
充电宝 4.00 3
键盘 3.50 3
协同过滤推荐 台灯、耳机、充电宝
热门推荐 耳机、台灯、充电宝
两种推荐重合 3 个
留一法命中(协同过滤) 3 / 6
留一法命中(热门) 0 / 6
两种推荐的 3 个物品其实完全一样——这么小的数据,协同过滤体现不出差异。
真正拉开差距的是最后两行:留一法 3/6 对 0/6。
而且协同过滤能说清【为什么推】:和你口味相近的人买了它。
热门推荐只能说「买的人多」——对老用户几乎没有信息量。
稀疏度那行:24 / 48,也就是一半是空的。 真实电商里这个比例是 99% 以上,所以推荐算法本质是在极端稀疏下猜。 相似度表里最该看的是中间那列:用户 1 和用户 5 只有 1 个共同评分,却给出 1.000 的相似度。 共同评分太少,这个数就没有意义。 预测分:台灯 4.49 最高,耳机和充电宝都是 4.00,键盘 3.50。 台灯高是因为几个相似用户都打了 4 到 5 分。 留一法 3/6 对 0/6。 热门推荐全错,因为它推的东西用户早就买过了——这是基线方法的典型失效方式。
30 秒实验
- 把共同评分数小于 2 的用户相似度强制置 0,看推荐列表变不变。
- 把矩阵补到 20 个用户,看热门基线的留一法命中会不会从 0 起来。
- 想一想:为什么新用户阶段反而该用热门推荐?
常见误区
- 「相似度 1.000 就是很像」——可能只共同评过 1 个物品。
- 「空值当 0 分算」——那等于说「他没评就是讨厌」。
- 「热门推荐没用」——冷启动阶段它是最稳的保底。
自测三问(点开对答案)
1. 算相似度时为什么要只看共同打分的项? —— 没评过的格子不是 0 分,参与计算会把相似度算歪。 2. 热门推荐的留一法命中为什么是 0? —— 它推的就是大家都买过的,这些早已在用户的已知列表里。 3. 协同过滤的推荐理由比热门好在哪里? —— 能说「和你口味相近的人买了它」,热门只能说「买的人多」。推荐讲完,下面进工业场景——异常检测是产线上最刚需的一类模型。
- ☐ (本节完,下一节 4.2.4 工业制造:质量控制、异常检测)