📑 本页目录(点开跳转)
01 · 10 分钟跑通一个推荐系统
⏱ 10 分钟 | ⭐ 核心 | 🔨 有代码,必须动手
🎯 一句话
推荐的核心逻辑只有一句:「你喜欢 A,而 B 和 A 很像,所以推 B 给你」。这一节你就把它写出来。
😤 先说清楚:不需要准备任何东西
- ❌ 不用装库(numpy 都不用)
- ❌ 不用下数据集
- ❌ 不用懂数学
- ✅ 只要一个能跑 Python 的地方
没装 Python? 打开 Google Colab,新建笔记本,粘代码,按 Shift+Enter。15 秒搞定。
🔨 动手:复制这段代码,跑它
# -*- coding: utf-8 -*-
"""10 分钟推荐系统 —— 不用装任何库,不用下载任何数据"""
# ============ 第 1 步:数据 ============
# 8 个用户,对 10 部电影的评分(1-5 分,0 表示没看过)
movies = ["流浪地球", "星际穿越", "泰坦尼克", "教父", "低俗小说",
"你的名字", "千与千寻", "复仇者联盟", "肖申克救赎", "疯狂动物城"]
ratings = {
"小明": [5, 5, 0, 0, 0, 4, 4, 5, 0, 3],
"小红": [0, 0, 5, 0, 0, 5, 5, 0, 4, 5],
"小刚": [5, 4, 0, 5, 5, 0, 0, 4, 5, 0],
"小美": [0, 0, 5, 0, 0, 4, 5, 0, 5, 5],
"阿强": [4, 5, 0, 4, 4, 0, 0, 5, 4, 0],
"阿丽": [0, 3, 4, 0, 0, 5, 5, 0, 4, 4],
"老王": [5, 5, 0, 5, 4, 0, 3, 5, 5, 0],
"小张": [3, 0, 5, 0, 0, 5, 4, 0, 0, 5],
}
# ============ 第 2 步:算物品之间有多像 ============
def similarity(a, b):
"""余弦相似度:两个物品的评分向量夹角越小 = 越像。返回 0~1"""
dot = sum(x * y for x, y in zip(a, b))
na = sum(x * x for x in a) ** 0.5
nb = sum(y * y for y in b) ** 0.5
return dot / (na * nb) if na and nb else 0.0
# 把「每个用户对电影 i 的评分」抽成一列,就是电影 i 的向量
users = list(ratings.keys())
item_vectors = [[ratings[u][i] for u in users] for i in range(len(movies))]
# 10x10 的相似度表
sim = [[similarity(item_vectors[i], item_vectors[j]) for j in range(len(movies))]
for i in range(len(movies))]
# ============ 第 3 步:给某个人推荐 ============
def recommend(user, top_n=3):
my_ratings = ratings[user]
scores = {}
for j in range(len(movies)):
if my_ratings[j] > 0: # 看过的不推
continue
# 打分 = Σ(我给看过的电影的分 × 那部电影和它的相似度)
score = sum(my_ratings[i] * sim[i][j] for i in range(len(movies)) if my_ratings[i] > 0)
scores[movies[j]] = score
return sorted(scores.items(), key=lambda x: -x[1])[:top_n]
# ============ 第 4 步:看结果 ============
for user in users:
seen = [movies[i] for i, r in enumerate(ratings[user]) if r >= 4]
print(f"\n【{user}】喜欢:{'、'.join(seen)}")
print(" 推荐给他:")
for name, score in recommend(user):
print(f" → {name} (推荐分 {score:.2f})")
🎉 你应该看到这个
【小刚】喜欢:流浪地球、星际穿越、教父、低俗小说、复仇者联盟、肖申克救赎
推荐给他:
→ 千与千寻 (推荐分 10.35)
→ 你的名字 (推荐分 6.47)
→ 疯狂动物城 (推荐分 5.96)
【小张】喜欢:泰坦尼克、你的名字、千与千寻、疯狂动物城
推荐给他:
→ 肖申克救赎 (推荐分 12.71)
→ 星际穿越 (推荐分 8.23)
→ 复仇者联盟 (推荐分 5.99)
恭喜。这就是一个真的推荐系统。
它叫 ItemCF(基于物品的协同过滤),亚马逊靠它做了十几年推荐,至今仍在无数产品里跑着。
🧠 它到底干了什么?(3 步,看图)
ItemCF 的三步
① 把每部电影写成评分向量
| 电影 / 用户 | 小明 | 小红 | 小刚 | 小美 | 阿强 | 阿丽 | 老王 | 小张 |
|---|---|---|---|---|---|---|---|---|
| 星际穿越 | 5 | 0 | 4 | 0 | 5 | 3 | 5 | 0 |
| 千与千寻 | 4 | 5 | 0 | 5 | 0 | 5 | 3 | 4 |
读法:一行就是一部电影的“被谁喜欢、喜欢多少”。被同一批人高分的两行会更像。
② 用余弦相似度比较两行:方向完全一样是 1.0,完全无关是 0.0;它关心口味是否同向,不是评分总和多大。
③ 把相似电影给用户的分数加权:小刚给《星际穿越》4分,而它和《千与千寻》的相似度是0.6,所以《千与千寻》获得 4 × 0.6 = 2.4 分。把小刚看过的每部电影都这样累计,最高分就是推荐结果。
💡 人话翻译:「你给高分的电影,它们像谁,我就推谁。你给的分越高、越像,推荐分就越高。」
🔍 一个你可能没注意的坑(很重要)
看小红和小美的结果——她们明明都是「文艺+动画」口味,系统却给她们推了星际穿越和流浪地球。
为什么?
因为小明既喜欢科幻也喜欢动画,他一个人就把这两个圈子「连」起来了。而且科幻片被打分的人多,向量「更长」,天然得分高。
这个现象叫 热门偏置(Popularity Bias):
越热门的物品越容易被推荐,越容易被推荐就越热门 —— 一个恶性循环。
这是推荐系统最经典的问题之一,工业界花了大量精力解决它(第 11 节讲怎么治)。
你在第 10 分钟就撞见了一个真实的工业难题。👏
🔨 玩一下(可选,但很有用)
改代码,看结果怎么变。这比读十页文字有用:
- 给自己加一行:在
ratings里加"你": [...],填自己的口味,看推什么 - 改成推 5 个:
recommend(user, top_n=5) - 试试治热门偏置:把打分那行改成除以物品被评分次数
python count = sum(1 for u in users if ratings[u][j] > 0) score = score / (count ** 0.5) # 惩罚热门物品再跑一遍,小红的推荐变了吗?
🔗 这一章连到哪里
✅ 检查点
不用写答案,心里过一遍。答不上来就往上翻:
- 「星际穿越」这部电影,在代码里被表示成了什么?
- 为什么用「余弦相似度」而不是直接比大小?
- 为什么看过的电影要跳过?
- 热门偏置是什么意思?
👀 答案
- 被表示成一个「谁看过它」的向量——矩阵里它那一列(或那一行)。⭐ 注意:代码完全不知道它是科幻片、不知道诺兰导演、甚至不知道它是电影,它只知道「哪些用户和它有过交互」。这就是协同过滤的全部信息来源。
- 因为要比的是「方向」不是「大小」。直接比大小的话,看过 500 部电影的重度用户会和所有人都「很相似」,仅仅因为他的向量长。余弦相似度先做了归一化,问的是「口味重合的比例」而不是「重合的绝对数量」。
- 因为推荐已经看过的东西没有价值——用户不会再看一遍,这个坑位就浪费了。⚠️ 而且它会让离线指标虚高:模型很容易学会「推他看过的」,分数很好看,线上毫无效果。
- 热门物品因为交互多,会和几乎所有东西都「相似」,于是被推给所有人 → 它更热 → 更容易被推。⭐ 富者愈富,长尾内容永无出头之日。这是协同过滤的结构性缺陷,不是 bug——第 11 章的重排和多样性约束就是为了对抗它。
🛑 可以停在这里
你今天已经完成了最难的一步:开始了。
这个 40 行的程序,剩下 17 节全是在回答同一个问题的升级版:
「真实世界有 3 亿用户、1 亿个物品、要在 200 毫秒内出结果,这 40 行怎么变成能扛住的系统?」
⚡ 走神救援(隔天回来看这个就够)
先记住这几件事
- ItemCF 根据物品之间的交互相似度,推荐与用户喜欢的物品相近的候选。
- 跑通后观察热门偏置,不要把所有人都看到的结果误当成个性化。
- 能输出推荐只是开始,还要建立验证来判断推荐是否有效。
下一节 👉 02-推荐系统到底是什么.md