🏠 总目录📚 本教程 01 · 10分钟跑通
📑 本页目录(点开跳转)

01 · 10 分钟跑通一个推荐系统

10 分钟 | ⭐ 核心 | 🔨 有代码,必须动手


🎯 一句话

推荐的核心逻辑只有一句:「你喜欢 A,而 B 和 A 很像,所以推 B 给你」。这一节你就把它写出来。


😤 先说清楚:不需要准备任何东西

没装 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 步,看图)

第 1 步:把「电影」变成一串数字
────────────────────────────────
          小明 小红 小刚 小美 阿强 阿丽 老王 小张
星际穿越 [ 5    0    4    0    5    3    5    0  ]  ← 这就是「星际穿越」这部电影
千与千寻 [ 4    5    0    5    0    5    3    4  ]  ← 这就是「千与千寻」

  💡 一部电影 = 「所有人对它的评分」组成的一串数字。
     两部电影被同一批人喜欢 → 这两串数字长得像 → 这两部电影像。


第 2 步:算「像不像」
────────────────────────────────
  余弦相似度 = 两串数字的夹角

  完全一样方向 → 1.0(超级像)
  完全没关系   → 0.0(毫无关系)


第 3 步:加权打分
────────────────────────────────
  小刚给「星际穿越」打了 4 分
  「星际穿越」和「千与千寻」相似度 0.6
                            ↓
  「千与千寻」得分 += 4 × 0.6 = 2.4

  把小刚看过的每一部都这样算一遍、加起来 → 总分最高的就是推荐结果

💡 人话翻译「你给高分的电影,它们像谁,我就推谁。你给的分越高、越像,推荐分就越高。」


🔍 一个你可能没注意的坑(很重要)

看小红和小美的结果——她们明明都是「文艺+动画」口味,系统却给她们推了星际穿越流浪地球

为什么?

因为小明既喜欢科幻也喜欢动画,他一个人就把这两个圈子「连」起来了。而且科幻片被打分的人多,向量「更长」,天然得分高。

这个现象叫 热门偏置(Popularity Bias)

越热门的物品越容易被推荐,越容易被推荐就越热门 —— 一个恶性循环。

这是推荐系统最经典的问题之一,工业界花了大量精力解决它(第 11 节讲怎么治)。

你在第 10 分钟就撞见了一个真实的工业难题。👏


🔨 玩一下(可选,但很有用)

改代码,看结果怎么变。这比读十页文字有用:

  1. 给自己加一行:在 ratings 里加 "你": [...],填自己的口味,看推什么
  2. 改成推 5 个recommend(user, top_n=5)
  3. 试试治热门偏置:把打分那行改成除以物品被评分次数 python count = sum(1 for u in users if ratings[u][j] > 0) score = score / (count ** 0.5) # 惩罚热门物品 再跑一遍,小红的推荐变了吗?

🔗 这一章连到哪里

去哪为什么
ML基础 01同样的「10 分钟先跑通」思路,那边是监督学习版
数学原理 04ItemCF 本质是 kNN —— 那一章讲它为什么在高维会失效
上线之后 02你算出的相似度好看,不代表线上有效

✅ 检查点

不用写答案,心里过一遍。答不上来就往上翻:

  1. 「星际穿越」这部电影,在代码里被表示成了什么?
  2. 为什么用「余弦相似度」而不是直接比大小?
  3. 为什么看过的电影要跳过?
  4. 热门偏置是什么意思?

👀 答案
  1. 被表示成一个「谁看过它」的向量——矩阵里它那一列(或那一行)。⭐ 注意:代码完全不知道它是科幻片、不知道诺兰导演、甚至不知道它是电影,它只知道「哪些用户和它有过交互」。这就是协同过滤的全部信息来源。
  2. 因为要比的是「方向」不是「大小」。直接比大小的话,看过 500 部电影的重度用户会和所有人都「很相似」,仅仅因为他的向量长。余弦相似度先做了归一化,问的是「口味重合的比例」而不是「重合的绝对数量」
  3. 因为推荐已经看过的东西没有价值——用户不会再看一遍,这个坑位就浪费了。⚠️ 而且它会让离线指标虚高:模型很容易学会「推他看过的」,分数很好看,线上毫无效果
  4. 热门物品因为交互多,会和几乎所有东西都「相似」,于是被推给所有人 → 它更热 → 更容易被推。⭐ 富者愈富,长尾内容永无出头之日。这是协同过滤的结构性缺陷,不是 bug——第 11 章的重排和多样性约束就是为了对抗它。

🛑 可以停在这里

你今天已经完成了最难的一步:开始了。

这个 40 行的程序,剩下 17 节全是在回答同一个问题的升级版:

「真实世界有 3 亿用户、1 亿个物品、要在 200 毫秒内出结果,这 40 行怎么变成能扛住的系统?」


走神救援(隔天回来看这个就够)

我写了个 ItemCF:把电影表示成「所有人对它的评分向量」→ 算两两余弦相似度 → 用户喜欢的电影像谁就推谁。发现了热门偏置问题。这一章真正的价值不是那段代码,是它暴露的两个问题热门偏置(热门物品和谁都像,于是被推给所有人 → 富者愈富)和没有验证(你不知道推得好不好)。这两个问题分别通向第 11 节的重排和第 12 节的评估。另外注意:这里用余弦相似度而不是直接比大小,是因为要比的是口味方向不是活跃程度 —— 不归一化的话,看过 500 部电影的人会和所有人都「相似」。

下一节 👉 02-推荐系统到底是什么.md

打卡记录保存在你的浏览器里,首页能看到总进度