🏠 总目录📚 本教程 04 · 协同过滤
📑 本页目录(点开跳转)

04 · 协同过滤:最简单的算法

25 分钟 | ⭐ 核心 | 🔨 有代码 | ✏️ 能手算


🎯 一句话

协同过滤 = 「让用户互相帮忙」。它有两个方向:「跟你像的人喜欢啥」(UserCF)和 「你喜欢的东西像啥」(ItemCF)。


🧭 两个方向,一张图讲清

UserCF「和你相似的人还喜欢…」小明科幻A科幻B科幻C小刚科幻A科幻B两人口味像→ 把 科幻C 推给小刚ItemCF「看了这个的人还看了…」盗梦空间星际穿越被同一批人喜欢过 → 这两部片就「像」小刚看了 盗梦空间→ 推 星际穿越两边都只用「谁和谁有过交互」,完全不看内容本身
同一份点击记录,两种读法:UserCF 先找「像你的人」,ItemCF 先找「像它的物品」。⭐ 工业界更常用 ItemCF —— 物品比用户稳定得多,相似度可以离线算好。

⚠️ 注意 ItemCF 的「像」不是内容像,是「被同一批人喜欢」。 所以 ItemCF 可能发现「买婴儿尿布的人也买啤酒」这种内容上八竿子打不着的关联——这正是协同过滤的魔力


✏️ 手算一遍 UserCF(5 分钟,值得做)

数据:

电影A 电影B 电影C 电影D
小明 1 1 1 0
小刚 1 1 0 ?
小红 0 0 1 1

(1 = 喜欢,0 = 没交互)

目标:该给小刚推 C 还是 D?

第 1 步:算小刚和每个人的相似度(用 Jaccard:交集 ÷ 并集)

小刚 ∩ 小明 = {A, B}      小刚 ∪ 小明 = {A, B, C}
sim(小刚, 小明) = 2/3 = 0.67

小刚 ∩ 小红 = {}          小刚 ∪ 小红 = {A, B, C, D}
sim(小刚, 小红) = 0/4 = 0

第 2 步:给候选打分(= Σ 相似用户的相似度 × 他对该物品的评分)

C 的得分 = 0.67 × 1(小明喜欢C) + 0 × 1(小红喜欢C) = 0.67
D 的得分 = 0.67 × 0(小明没看D) + 0 × 1(小红喜欢D) = 0

结论:推 C。

你刚刚完整地跑了一遍 UserCF。就这么回事。


📐 公式(配人话翻译)

UserCF

$$\hat{r}_{u,i} = \frac{\sum_{v \in N(u)} \text{sim}(u,v) \cdot r_{v,i}}{\sum_{v \in N(u)} |\text{sim}(u,v)|}$$

💡 人话

「跟我像的那批人(N(u)),他们给物品 i 打了多少分,按『像的程度』加权平均一下,就是我的预测分。」 分母是归一化,防止相似用户多的人分数虚高。

ItemCF

$$\hat{r}_{u,i} = \frac{\sum_{j \in I(u)} \text{sim}(i,j) \cdot r_{u,j}}{\sum_{j \in I(u)} |\text{sim}(i,j)|}$$

💡 人话

「我看过的东西(I(u)),谁跟物品 i 像,就按像的程度,把我给它的分加权过去。」

两个公式长得一样,只是「找相似」的对象换了:一个找人,一个找物。


🔬 相似度怎么算(三选一,够用了)

方法 公式 什么时候用
余弦相似度 $\frac{A \cdot B}{\|A\|\|B\|}$ 默认选它。有评分值时用
Jaccard $\frac{\|A \cap B\|}{\|A \cup B\|}$ 只有 0/1 隐式反馈时用
皮尔逊相关 余弦,但先减去均值 显式评分,且用户打分习惯差异大时(有人只打 4-5 分)
import numpy as np

def cosine(a, b):
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-8)

def jaccard(a, b):
    a, b = set(np.where(a > 0)[0]), set(np.where(b > 0)[0])
    return len(a & b) / (len(a | b) + 1e-8)

def pearson(a, b):
    mask = (a > 0) & (b > 0)          # 只在都评过分的物品上算
    if mask.sum() < 2: return 0
    return np.corrcoef(a[mask], b[mask])[0, 1]

⚔️ UserCF vs ItemCF:怎么选(⭐ 面试常问)

UserCF ItemCF
适合 用户少、物品多且变化快 物品相对稳定,用户多
典型场景 新闻资讯(内容时效强) 电商、视频(商品/影片长期存在)
推荐特点 社会化、发现惊喜、跟随热点 个性化强、可解释、稳定
可解释性 弱(「和你像的人」是谁?) 强(「因为你看了X」)✅
计算量 用户矩阵 O(U²),用户一多就爆 物品矩阵 O(I²),可离线算好缓存 ✅
实时性 差(用户相似度难实时更新) 好(物品相似度隔天更新即可)✅

🏆 工业界的答案:ItemCF 用得多得多

三个原因: 1. 物品相似度可以离线算好,存成表,线上只需查表 + 加权 → 快 2. 可解释:「因为你看了《流浪地球》」,用户接受度高 3. 物品数量通常比用户少,且新用户一有行为就能立刻推荐(UserCF 要等他跟别人产生足够重叠)

亚马逊 2003 年的论文 Item-to-Item Collaborative Filtering 就是靠这个撑起了整个推荐业务,至今仍是很多系统的召回通路之一。


🔨 动手:完整的 ItemCF(带工业级改进)

import numpy as np
from collections import defaultdict

class ItemCF:
    def __init__(self, alpha=0.5, k=20):
        """
        alpha: 热门惩罚强度。0=不惩罚, 0.5=常用值, 1=强惩罚
        k:     每个物品只保留最相似的 k 个(截断,省内存 + 去噪)
        """
        self.alpha, self.k = alpha, k

    def fit(self, user_items):
        """user_items: {user_id: [item_id, ...]}"""
        # 1) 统计共现:多少人同时喜欢 i 和 j
        cooccur = defaultdict(lambda: defaultdict(float))
        popularity = defaultdict(int)

        for user, items in user_items.items():
            # 🔑 改进1: 活跃用户降权。一个买了1000本书的人,
            #    他的任意两本书之间的"关联"其实很弱
            weight = 1.0 / np.log1p(len(items))
            for i in items:
                popularity[i] += 1
                for j in items:
                    if i != j:
                        cooccur[i][j] += weight

        # 2) 共现 → 相似度(余弦形式 + 热门惩罚)
        self.sim = {}
        for i, related in cooccur.items():
            scores = {}
            for j, c in related.items():
                # 🔑 改进2: 除以流行度,惩罚"人人都喜欢"的热门物品
                scores[j] = c / ((popularity[i] ** self.alpha) *
                                 (popularity[j] ** (1 - self.alpha)))
            # 🔑 改进3: 只留 top-k,砍掉长尾噪声
            self.sim[i] = dict(sorted(scores.items(), key=lambda x: -x[1])[:self.k])
        return self

    def recommend(self, history, n=10):
        scores = defaultdict(float)
        seen = set(history)
        for i in history:
            for j, s in self.sim.get(i, {}).items():
                if j not in seen:
                    scores[j] += s
        return sorted(scores.items(), key=lambda x: -x[1])[:n]


# ---- 试一下 ----
data = {
    "u1": ["科幻A", "科幻B", "科幻C", "热门X"],
    "u2": ["科幻A", "科幻B", "热门X"],
    "u3": ["动画P", "动画Q", "热门X"],
    "u4": ["动画P", "动画Q", "动画R", "热门X"],
    "u5": ["科幻A", "科幻C", "热门X"],
}
model = ItemCF(alpha=0.5, k=10).fit(data)
print("看过 科幻A 的人,接下来推:")
for item, score in model.recommend(["科幻A"], n=3):
    print(f"  {item}: {score:.3f}")

🔑 三个改进为什么重要(这是新手和熟手的差距)

改进 1:活跃用户降权(IUF)

一个用户买了 1000 本书,你不能说这 1000 本书两两相似。他的每一条行为都应该更轻。

$$w_u = \frac{1}{\log(1 + |I(u)|)}$$

改进 2:热门物品惩罚

不加这个,「热门X」会和所有东西都相似(因为人人都看),推荐结果永远是几个爆款。

$$\text{sim}(i,j) = \frac{|U(i) \cap U(j)|}{|U(i)|^{\alpha} \cdot |U(j)|^{1-\alpha}}$$

🎚️ 这是你上线后最常调的旋钮之一。

🎁 进阶预告:工业界还有一个比 ItemCF 更鲁棒的 i2i 算法 —— 阿里的 Swing(利用「用户对的重合度」过滤巧合共现)。等你学完主线,在 第 18 节·专题二 有完整讲解和实现。

改进 3:Top-K 截断

1 亿物品的两两相似度是 10^16 个数,存不下。只留每个物品最相似的 20–100 个,既省空间又去噪。


🕳️ 协同过滤的致命缺陷(这是后面所有算法出现的原因)

缺陷 具体表现 后面怎么解决
冷启动 新用户没行为、新物品没人看 → 完全推不了 内容特征、第 13 节
稀疏性 99.9% 的格子是空的,共现统计极不可靠 矩阵分解(第 5 节)
无法用副信息 用户年龄、物品类目、时间…全都用不上 特征化模型(第 7、8 节)
不可泛化 没共现过就是 0,学不到「潜在」关系 Embedding(第 5 节)
计算爆炸 物品一多,两两相似度算不动 向量检索 ANN(第 10 节)

🔑 关键洞察:ItemCF 的相似度是「数出来的」。 下一节的矩阵分解,相似度是「学出来的」——这是整个推荐算法史上最重要的一次跃迁。


🔗 这一章连到哪里

去哪为什么
数学原理 04CF 是「惰性学习」的典型:训练只是存数据,预测时才算
数学原理 05为什么高维上「最近邻不再更近」—— CF 稀疏时失效的几何原因
数学原理 06相似度函数的一般形式;RBF 核就是高斯加权的 kNN

✅ 检查点

  1. 手算题:用户 X 喜欢 {A,B},用户 Y 喜欢 {A,B,C},用户 Z 喜欢 {C,D}。UserCF 该给 X 推什么?
  2. 为什么工业界更爱用 ItemCF?(说出 3 个理由)
  3. 热门惩罚的 α 调大,会发生什么?
  4. 协同过滤最致命的缺陷是什么?
👀 答案 1. 推 C。sim(X,Y)=2/3,sim(X,Z)=0。C 的分 = 2/3 × 1 = 0.67,D 的分 = 0。 2. ① 相似度可离线算好缓存,线上快;② 可解释「因为你看了X」;③ 物品比用户稳定,新用户一有行为就能推。 3. 热门物品得分被压得更低 → 推荐更小众、多样性↑,但整体准确率/点击率可能↓。经典的准确性-多样性权衡。 4. 冷启动 + 无法利用副信息(年龄、类目等)。

🛑 可以停在这里

走神救援

协同过滤两个方向:UserCF(像你的人喜欢啥)、ItemCF(你喜欢的像啥)。工业界爱 ItemCF:可离线算、可解释、更稳。三个必备改进:活跃用户降权、热门惩罚(α)、top-k 截断。致命缺陷是冷启动 + 用不了副信息 → 所以有了下一节的矩阵分解。三个改进的分量比算法本身重:活跃用户降权(防一个刷子污染所有相似度)、热门惩罚 α(不加的话热门和谁都像)、top-k 截断(长尾噪声比信号多)。⭐⭐ 致命缺陷要理解成「后面所有算法的出发点」:CF 只会数共现,没见过的组合它一无所知,也用不了物品的内容和用户的画像。下一节的矩阵分解把「数出来的相似度」变成「学出来的向量」,从此有了泛化能力 —— 这是整个推荐算法史上最重要的一次跃迁。

下一节 👉 05-矩阵分解.md

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