Item-CF¶
Item-CF 根据用户喜欢过的电影,推荐相似电影。
它出现得很早,也很实用。User-CF 要找相似用户,但用户兴趣会变,行为也更稀疏。电影之间的关系通常更稳定一些。如果很多喜欢《黑客帝国》的人也喜欢另一部电影,那另一部电影就可以成为候选。
先别急着把它想成公式。Item-CF 的直觉很像逛视频网站时看到的“看过这部的人也看了”。它不关心你是谁,也不关心电影简介写了什么。它只看行为:哪些电影经常被同一批用户喜欢。
flowchart LR
U[目标用户] --> L[他喜欢过的电影]
L --> S[找这些电影的相似电影]
S --> F[过滤用户已经看过的电影]
F --> R[按分数排序得到推荐列表]
在 MovieLens 上,可以先用评分构建电影和电影的相似度矩阵。第一版不用复杂,把评分大于等于 4.0 当作喜欢,然后对电影列向量算余弦相似度。
它为什么先推荐电影,而不是先找人¶
User-CF 的想法是找相似用户。听起来很自然,但实际会遇到几个麻烦。
第一,用户很多,而且每个用户看过的电影很少。两个用户之间可能根本没有多少共同电影,算出来的相似度很不稳定。
第二,用户兴趣会变。一个人大学时喜欢动作片,几年后可能更喜欢纪录片。你把他的所有历史混在一起算相似用户,结果可能很乱。
第三,线上系统里用户变化比电影变化更快。每天都有新用户、新行为,但电影库变化相对慢一些。电影之间的相似关系可以提前算好,推荐时查表就行。
Item-CF 的好处就在这里:它把主要计算放到“电影和电影之间的关系”上。
MovieLens 上的一条样本怎么进入 Item-CF¶
假设有几条评分:
| userId | movieId | rating |
|---|---|---|
| 1 | A | 5.0 |
| 1 | B | 4.5 |
| 2 | A | 4.0 |
| 2 | C | 4.5 |
| 3 | B | 5.0 |
| 3 | C | 4.0 |
如果把 4.0 以上都当成喜欢,就能得到一张用户-电影表:
| 用户 | A | B | C |
|---|---|---|---|
| 1 | 1 | 1 | 0 |
| 2 | 1 | 0 | 1 |
| 3 | 0 | 1 | 1 |
现在看电影 A 和 B:用户 1 同时喜欢它们,所以它们有关系。电影 A 和 C:用户 2 同时喜欢它们,也有关系。电影 B 和 C:用户 3 同时喜欢它们,也有关系。真实数据里用户和电影更多,相似度就从这些共同喜欢的模式里长出来。
相似度到底怎么算¶
第一版用余弦相似度就够了。把每部电影看成一个很长的向量,向量长度等于用户数。某个用户喜欢这部电影,对应位置就是 1;不喜欢或没记录,就是 0。
两部电影的余弦相似度大,意思是喜欢它们的用户重合比较多。
similarity(A, B) = dot(A, B) / (norm(A) * norm(B))
这个公式不用死记。你可以把它理解成:共同喜欢的人越多,相似度越高;但如果一部电影本来就人人都喜欢,分母会把它的影响压一压,避免热门电影把所有东西都连起来。
推荐分数怎么来¶
对一个目标用户,先找出他喜欢过的电影。每部喜欢过的电影都会带出一批相似电影。一个候选电影的最终分数,可以把这些相似度加起来。
flowchart TB
A[用户喜欢 A] --> D[候选 D<br/>sim A,D = 0.7]
B[用户喜欢 B] --> D2[候选 D<br/>sim B,D = 0.4]
D --> Score[候选 D 总分 = 1.1]
D2 --> Score
如果你还保留原始评分,也可以加权:用户给 5 分的电影,比给 4 分的电影权重大一点。但第一版先别加太多东西,先把最朴素的版本写清楚。
手算一个很小的推荐例子¶
假设目标用户喜欢过两部电影:
| 用户喜欢过 | 用户评分 |
|---|---|
| A: The Matrix | 5.0 |
| B: Inception | 4.5 |
离线已经算好了电影相似度:
| 原电影 | 相似电影 | 相似度 |
|---|---|---|
| The Matrix | Blade Runner | 0.82 |
| The Matrix | John Wick | 0.63 |
| Inception | Interstellar | 0.79 |
| Inception | Blade Runner | 0.40 |
现在给候选电影加分:
| 候选电影 | 来自哪部已喜欢电影 | 分数 |
|---|---|---|
| Blade Runner | The Matrix, Inception | 0.82 + 0.40 = 1.22 |
| Interstellar | Inception | 0.79 |
| John Wick | The Matrix | 0.63 |
所以推荐顺序是:
- Blade Runner
- Interstellar
- John Wick
这个例子里,Blade Runner 排第一,不是因为它和某一部电影最像,而是因为它同时被两部用户喜欢过的电影支持。Item-CF 很多时候就是这样工作的:多个弱证据叠在一起,最后变成一个强推荐。
如果目标用户已经看过 Blade Runner,就要把它过滤掉。推荐系统不是考试,不能把答案里已经出现过的东西再交一次。
第一版代码建议这样写:
- 读取
ratings.csv。 - 构建稀疏的用户-电影矩阵。
- 为每部电影计算最相似的电影。
- 对某个用户,把他喜欢过的电影拿出来找相似电影。
- 过滤掉他已经评分过的电影。
Item-CF 适合第一个写,因为推荐结果容易解释。推荐了哪部电影,通常能追溯到它是被用户喜欢过的哪部电影带出来的。
常见坑¶
最常见的坑是把“没评分”当成“不喜欢”。MovieLens 里用户没给某部电影评分,可能只是没看过,不代表讨厌它。所以第一版做隐式反馈时,通常只把高评分当成正样本,没出现的地方先当未知。
第二个坑是热门电影过强。特别热门的电影会和很多电影都有共同用户,导致推荐列表被大热门占满。后面可以试试惩罚热门电影,或者只保留每部电影最相似的前 K 个邻居。
第三个坑是没有做已看过滤。如果把用户已经评分过的电影又推荐给他,指标可能看起来还行,但真实推荐体验很差。
运行方式¶
从仓库根目录运行:
./01-traditional-statistics/item-cf/run.sh --sample-ratings 2000000
需要更大样本或全量数据时:
./01-traditional-statistics/item-cf/run.sh --sample-ratings 5000000
./01-traditional-statistics/item-cf/run.sh --sample-ratings none
运行后会在本目录生成 report.md 和 report.zh.md。
读完应该能回答¶
- Item-CF 为什么比 User-CF 更适合提前计算?
- 为什么 MovieLens 里的“没评分”不能直接当成“不喜欢”?
- 余弦相似度在这里到底比较的是什么?
- 为什么要过滤用户已经看过的电影?