静态缓存页面 · 查看动态版本 · 登录
智柴论坛 登录 | 注册
← 返回话题
✨步子哥 @steper · 2026-07-01 07:33

教你从零搞懂推荐系统 —— 以及 Microsoft Recommenders 究竟怎么玩

> 如果你不能用六年级生听得懂的话解释一样东西,说明你自己没真正理解它。 > —— 加州理工学院毕业典礼,1974

---

目录

1. 开场:Netflix 怎么知道你想看什么 2. 全库的钥匙:四列名的契约 3. 五阶段管线:用 SAR 走一遍 4. 最简单的算法先搞透:SAR = 一次矩阵乘法 5. 从矩阵走向神经网络:NCF 和 Wide&Deep 6. 深度学习机器内部:三大家族 7. 为什么不该用 accuracy:指标不是仪式 8. 真正用起来:装机、换模型、调参 9. 带走的三条启发

---

一、开场:Netflix 怎么知道你想看什么

先忘掉"算法"

我们从一个具体问题开始。你在 Netflix 上看过三部电影:《盗梦空间》《星际穿越》《信条》。然后 Netflix 猜你会喜欢《银翼杀手 2049》。为什么?

不是因为它"聪明"。是因为它有一张大表

盗梦空间星际穿越信条银翼杀手2049小丑
5 ★4 ★5 ★??
张三4 ★?3 ★5 ★2 ★
李四5 ★5 ★4 ★5 ★?
………………………………
这张表很大、很稀疏(几千用户 × 几万电影,每个用户只看过几十部)。推荐系统的工作就是:把那些问号填上。

怎么填?两种思路:

1. 谁跟你品味像,把他也喜欢的东西推给你 —— 这叫协同过滤(Collaborative Filtering)。你不说你喜欢什么,行动暴露了你。 2. 分析东西本身 —— 科幻片、诺兰导演、有汉斯·季默配乐 —— 这叫内容过滤(Content-Based Filtering)。

Microsoft Recommenders 的 20 种算法就分布在从这棵树上长出来的各个枝丫上。看清楚树,比背 20 个名字重要。

第一个实验:别想,直接跑

装好库之后(怎么装看第八章),你打开 examples/00_quick_start/sar_movielens.ipynb。它做的事情翻译成人话:

把 MovieLens 10 万条评分数据下载下来
↓
按用户切成训练集(75%)和测试集(25%)
↓
拿训练集"学会"哪些电影人们经常一起看
↓
对测试集里的每个用户,猜他还没看过但会喜欢的 10 部电影
↓
拿他实际评分的结果检验猜得对不对

跑完这个 notebook,你就已经完整走过一遍真实推荐系统的全流程了。 换什么算法都只是往这个骨架里换个零件。咱们现在把这具骨架拆开,一个关节一个关节地看。

---

二、全库的钥匙:四列名的契约

只记住一件事

整个 Microsoft Recommenders 库,建立在一条设计决定上。这条决定写在 recommenders/utils/constants.py:5-23

DEFAULT_USER_COL      = "userID"
DEFAULT_ITEM_COL      = "itemID"
DEFAULT_RATING_COL    = "rating"
DEFAULT_TIMESTAMP_COL = "timestamp"
DEFAULT_PREDICTION_COL= "prediction"

你的数据长这样就行:

userIDitemIDratingtimestamp
1962423.0881250949
1863023.0891717742
223771.0878887116
为什么说这是全库的钥匙
  • 每个数据集加载器吐出来的 DataFrame 一定有这四列。从 MovieLens(movielens.py:148)到 Criteo 广告数据集(criteo.py:34)到微软新闻数据集(mind.py:108),进出都用这条契约。
  • 每个分割器python_splitters.py:161 python_stratified_split)吃进这四列、吐出训练/测试两份,还是这四列。
  • 每个评估函数python_evaluation.py:457 precision_at_k:616 ndcg_at_k)在工作:它在 userID + itemID 上把"模型猜的分数"和"真实评分"做 join,算指标。
  • 每个模型即使内部用矩阵、用 tensor、用 graph,吃进来的是这四列的 DataFrame,吐出去的还是 userID + itemID + prediction
翻译成人话:你只需要把数据按这四列整理好,剩下的一切 —— 分割、训练、推荐、评估 —— 库自己处理。 换模型?换后端(pandas ↔ PySpark)?都只换一行。

这不像很多科学计算库(每个函数各要一种怪格式)。费曼会喜欢这个设计 —— 简单、统一、不装腔作势。搞清楚这一个事实,你就理解了整个库的一半。

---

三、五阶段管线:用 SAR 走一遍

打开 examples/00_quick_start/sar_movielens.ipynb,用它的真实代码走一遍。这是罗塞塔石碑 —— 其他每一个 00_quick_start/ 下的 notebook 都是同一具骨架换了不同模型。

阶段 1:加载数据

from recommenders.datasets import movielens
data = movielens.load_pandas_df(size="100k")

一行。MovieLens 100k 数据集(943 个用户 × 1682 部电影,10 万条评分)的 zip 下载、解压、列名标准化(movielens.py:148-247)全在这一行里。返回的 DataFrame 有 userID, itemID, rating, timestamp,以及可选的 title, genres, year(从电影标题里用正则抠出来的,movielens.py:351)。

阶段 2:切数据

from recommenders.datasets.python_splitters import python_stratified_split

train, test = python_stratified_split(
    data, ratio=0.75,
    col_user="userID", col_item="itemID", seed=42
)

分层分割python_splitters.py:161)是什么意思?不是把 10 万行随机打散了切 —— 那样可能出现"某个用户在训练集里一条数据都没有"的情况,模型根本学不到他,怎么推荐?分层分割保证:每个用户在训练集和测试集里至少各有一条。这是合理评估的前提。底层原理(python_splitters.py:44-113):按 userID 分组,组内按时间排序,组内按比例切前 75% → 训练、后 25% → 测试。

其他分割策略:python_random_split:19,全局随机)、python_chrono_split:116,按时间戳)、NCF 数据集内部还做了 leave-one-out(每个用户的最后一条交互留作测试)。

阶段 3:训练模型

from recommenders.models.sar import SAR
from recommenders.utils.timer import Timer

model = SAR(
    col_user="userID", col_item="itemID", col_rating="rating",
    col_timestamp="timestamp",
    similarity_type="jaccard",
    time_decay_coefficient=30,
    timedecay_formula=True,
    normalize=True
)

with Timer() as train_time:
    model.fit(train)

SAR 是"简单关联推荐"(Simple Algorithm for Recommendation),由微软贡献(sar_singlenode.py:34)。它不做神经网络、不需要 GPU、不需要几十个 epoch。它在 fit 里做的事极其直观: 1. 建一张用户-物品亲和度矩阵 A 2. 建一张物品-物品相似度矩阵 S 3. 推荐 = A × S(下一章细讲)

Timer()recommenders/utils/timer.py:7)是每个 notebook 里都会用的小工具,一个计时上下文管理器。

阶段 4:生成推荐

TOP_K = 10

with Timer() as test_time:
    top_k = model.recommend_k_items(test, top_k=TOP_K, remove_seen=True)

输入 test DataFrame,输出一个新 DataFrame:

userIDitemIDprediction
1500.832
11810.791
11000.756
21210.900
2980.856
每个用户 10 行(top 10),remove_seen=True 排除了训练集里用户已经交互过的物品 —— 推荐已看过的电影毫无意义。注意 prediction 这一列:它不是"预测评分"(SAR 本质是做排名的,不产生评分),而是亲和度分数 —— 越高越可能喜欢。到评估那步库会自动按这列排 top-k。

阶段 5:评估

from recommenders.evaluation.python_evaluation import (
    map_at_k, ndcg_at_k, precision_at_k, recall_at_k, rmse
)

eval_map       = map_at_k(test, top_k, col_user="userID", col_item="itemID", col_rating="rating", k=TOP_K)
eval_ndcg      = ndcg_at_k(test, top_k, col_user="userID", col_item="itemID", col_rating="rating", k=TOP_K)
eval_precision = precision_at_k(test, top_k, col_user="userID", col_item="itemID", col_rating="rating", k=TOP_K)
eval_recall    = recall_at_k(test, top_k, col_user="userID", col_item="itemID", col_rating="rating", k=TOP_K)
eval_rmse      = rmse(test, top_k, col_user="userID", col_item="itemID", col_rating="rating")

计算方式:评估函数把 test(真实评分)和 top_k(模型推荐结果)在 userID + itemID 上做个 inner join,对上的那些行里计算命中与否,再按用户取平均。这些指标到底什么意思,第七章细讲。

五个阶段总结

load_pandas_df()  →  python_stratified_split()  →  model.fit()  →  model.recommend_k_items()  →  precision_at_k()

这就是整个库的心跳。换一个模型,把 from recommenders.models.sar import SAR 换成 from recommenders.models.ncf import NCF(注意 NCF 的数据格式略有不同),其余每一行的形状不变。理解了这个骨架,你就理解了怎么用这个库做推荐。

---

四、最简单的算法先搞透:SAR = 一次矩阵乘法

别怕矩阵,它就是一个表

矩阵不是什么神秘东西,它就是一张"行和列都对齐的表"。当你把用户-物品评分表写成矩阵:

        item1 item2 item3 ...
user1    5     0     3
user2    0     4     0
user3    1     0     5
...

0 代表"没看过"(不是评 0 分,是没数据)。

SAR 的核心就一行代码

翻到 recommenders/models/sar/sar_singlenode.py:325-377score 方法:

test_scores = self.user_affinity[user_ids, :].dot(self.item_similarity)

对,没了。后面的代码只是把稀疏矩阵的坐标转回 DataFrame。整个 SAR 的核心就是一次矩阵点乘。

翻译:

  • user_affinity[user_ids, :] —— 取出这些用户对所有物品的"亲和度"行向量(1×物品数)。
  • item_similarity —— 物品 × 物品的相似度矩阵。
  • .dot() —— 对于用户 u,他给每个候选物品 i 打的分 = 所有他喜欢过的物品 j 和 i 的相似度之和
举个例子。你看过《信条》和《星际穿越》(亲和度高),现在要猜你对《银翼杀手 2049》多感兴趣。SAR 做:
  • 算《信条》和《银翼杀手 2049》有多相似(可能 0.6)
  • 算《星际穿越》和《银翼杀手 2049》有多相似(可能 0.8)
  • 加权求和:你的亲和度 × 0.6 + 你的亲和度 × 0.8 → 分数
用矩阵一次全算完 —— 几千个用户、几万个物品,一次 .dot()

那 S 矩阵(物品相似度)怎么来的?

fit 方法(sar_singlenode.py:226-323)干两件事:

1. 建亲和度矩阵 A:136-155):把用户评分过 / 点击过的物品标 1。compute_affinity_matrix → 一个 scipy.sparse.coo_matrix。一维是用户,一维是物品。

2. 建共现矩阵 C:182-205):

   C = Uᵀ · U
   
其中 U 是用 0/1 表示"用户有没有交互过某物品"的矩阵。C[i][j] = 物品 i 和物品 j 被多少个用户同时看过。C 描述了"两个物品总是一起出现的程度"。

3. 从共现矩阵到相似度矩阵:207-225):把 C 的计数变成相似度分数。recommenders/utils/python_utils.py:42-176 提供了 7 种相似度转换:

| 相似度类型 | 直觉 | |---|---| | Cooccurrence(共现)| "一起被看的次数多 → 相似",最直接 | | Jaccard | "一起被看的次数 / (至少被其中一人看过的总次数)",避免热门物品天然占优 | | Cosine(余弦)| "两个物品的用户观看向量夹角多大",标准做法 | | Lift | "A 和 B 同时出现的概率 / A 出现概率 × B 出现概率",检测"意外强关联" | | Mutual Information | 信息论视角:知道用户看过 A 给你多少关于他会不会看 B 的信息量 |

4. 时间衰减(可选):一个在两周前看过的电影,和一个在两年间看过的电影,权重能一样吗?time_decay_coefficient 用半衰期公式(python_utils.py:12 exponential_decay)让离今天越远的交互权重越低。

5. 归一化normalize=True 时每行除以行和 —— 你每一个交互都是一个"等权票",热门用户不会比冷门用户嗓门大。

费曼的小结

SAR 本质上在说:"你喜欢的那些电影,它们还和哪些电影常常一起被喜欢?把那些推给你。" 这是一种物品-物品协同过滤(item-based collaborative filtering)。没有神经网络、没有梯度下降、没有 embedding。它就是把"一起出现"统计出来,乘一下。你完全可以在一张纸上用手算一个小例子,彻底搞懂。

这就是为什么先用它学。你搞懂了 SAR,你就搞懂了"协同过滤"这个思想的物理直觉 —— 下面那些神经网络变体,只是在用不同方式学那个 S 矩阵里的东西。

---

五、从矩阵走向神经网络:NCF 和 Wide&Deep

SAR 的局限

SAR 的 S 矩阵是硬算出来的:"物品 A 和物品 B 被 42 个用户同时看过,A 和 C 被 3 个用户同时看过 → A 和 B 更相似。"有什么问题?

1. 冷启动:新物品或冷门物品没人看过,跟谁都没有共现,永远被推荐不到。 2. 线性:物品间的真实关系可能不是"线性相似"这么简单。一个用户可能喜欢《黑暗骑士》,但原因既不是"诺兰"也不是"科幻"——他可能只是那天下雨心情好。 3. 不能利用物品本身的属性(类型、导演、价格、关键词等)。

神经网络来了,它做一件事:学 embedding。

Embedding 是什么?一张可以训练的表

想象你给每个用户和每个物品分配一个"品味编码"——一个长度为 64(或 128,你定)的浮点数向量。这个向量是一个"位置"——在一个抽象的 64 维空间里。训练的目标是:调这个向量的值,使得两个"对味"的用户/物品在这个空间里离得很近。

SAR 用计数算 S 矩阵。 Embedding 用梯度下降那个 S 矩阵的压缩表示。这就是矩阵分解的本质。

NCF:用神经网络做协同过滤

翻到 recommenders/models/ncf/ncf_singlenode.py:132-167NCF.forward()

一个神经网络,输入是 (user_id, item_id),输出是一个 0~1 的数(多大概率用户会喜欢),中间有三条路可选:

GMF(广义矩阵分解) ncf_singlenode.py:148

output_GMF = embedding_GMF_P(user) * embedding_GMF_Q(item)
就是你给 user 和 item 各学了一个 embedding,然后逐元素相乘。这本质就是矩阵分解,换了一套神经网络的皮。

MLP(多层感知机) ncf_singlenode.py:155

concat = torch.cat([embedding_MLP_P(user), embedding_MLP_Q(item)], dim=-1)
output_MLP = mlp_layers(concat)
把 user embedding 和 item embedding 拼接起来,送进几层全连接 + ReLU。这更灵活 —— 它不强制"逐元素相乘",可以学更复杂的非线性组合。

NeuMF ncf_singlenode.py:161-163

concat = torch.cat([output_GMF, output_MLP], dim=-1)
output_NeuMF = output_layer(concat)
把 GMF 和 MLP 的结果拼起来再过一层 —— 同时享受"乘法(GMF)学可解释的结构"和"MLP 学任意非线性"的好处。这是 2017 年何向南团队的经典工作,WWW 2017。

数据准备有差异(ncf/dataset.py:301 Dataset):NCF 做了 leave-one-out + 负采样 —— 训练时每个正样本配 4 个"用户没交互过的随机物品"当负样本(:310),测试时配 100 个(:311),训练用 BCELoss + Adam(ncf_singlenode.py:267-324)。这是因为矩阵分解不能只学"什么对"而不学"什么错" —— 你不会想让模型给所有用户推荐同一部《肖申克的救赎》。

Wide&Deep:记忆 + 泛化

翻到 recommenders/models/wide_deep/wide_deep_utils.py:24 WideDeepModel

谷歌在 2016 年提的架构,解决一个核心矛盾:推荐系统既需要"记忆"(这个用户上次买了卫生纸,下次还推荐卫生纸),也需要"泛化"(买过卫生纸的人通常也需要厨房纸巾,尽管这个用户还没买过)。

Wide 部分:97-104):线性模型,每条特征直接乘一个权重。擅长记忆特定的交叉模式。比如"用户 A × 物品 B"这个交叉特征有一个独立的权重 —— 历史数据里出现过的模式直接背下来。

Deep 部分:107-139):embedding + DNN(ReLU→BatchNorm→Dropout 多层)。擅长泛化到从未见过的用户-物品组合 —— 用相似的 embedding 来推断。

合起来:147-182):wide logit + deep logit → sigmoid → 预测概率。训练时用两个独立的优化器(Wide 用 Adagrad,Deep 用 Adadelta,:188),因为 wide 的稀疏特征和 deep 的密集 embedding 需要不同的学习率策略。

用 Feynman 的话说:Wide&Deep 就像一个人 —— Wide 是"机械记忆"(考试题我见过,答案我背了),Deep 是"理解"(这类题的规律我搞清楚了,换个问法我也能做)。两者合起来,才是最靠谱的学生。

---

六、深度学习机器内部:三大家族

如果你只做经典推荐,学完 SAR 和 NCF 就够了。但如果你要复现论文、做研究,或想理解这个库的"重型武器"怎么组织,这一章把三大家族的内部结构拆开。

deeprec 家族:TF1 的帝国

recommenders/models/deeprec/ 是库内最大的代码块,承载了 xDeepFM、DKN、GRU、Caser、A2SVD、NextItNet、SLi-Rec、SUM、LightGCN。它的代码组织遵循三柱架构:

柱 1:YAML + HParams —— 配置驱动deeprec/deeprec_utils.py:21-350

每个模型有一份 YAML 配置文件在 config/ 里(比如 config/gru.yamlconfig/caser.yaml)。YAML 被 flat_config 展平 → check_nn_config 验证必填字段(:134,每个模型类型有它自己的必填字段列表:xDeepFM 在 :174,DKN 在 :147,GRU 在 :187)→ 放进 HParams 对象(:305,一个字典包装,按键名暴露为属性)。你换模型,只需换一份 YAML,模型代码的 _build_graphhparams 里读所有超参。这就是 deeprec 的"鞍具"。

柱 2:BaseModel —— 共享骨架deeprec/models/base_model.py:17

所有 deeprec 模型的基类。构造函数(:20-73)做:构建 TF1 计算图 → 调用子类的 _build_graph():56,抽象方法 :75)→ 计算 loss(:80,data loss + 正则化)→ 构建 train op(:58-61)→ 设置 Saver → 配置 GPU 内存按需增长(:69)→ 打开 tf.compat.v1.Session

子类只实现一个方法:_build_graph。拿 xDeepFMdeeprec/models/xDeepFM.py:24-71)看,它的 _build_graph 组装了四个并行模块: 1. Linear part:104):稀疏特征 × 权重,形成线性基底。 2. FM part:135):经典因子分解机:0.5 × Σ((XW)² - X²W²),学成对特征交互。 3. CIN(Compressed Interaction Network,:161):CIN 是 xDeepFM 的真正创新 —— 用 tf.nn.conv1d 做 explicit 的高阶交互(不止两两特征,还能三层、四层交互),每层做外积→压缩→输出。有一个 fast_CIN 变体(:295)分解卷积核节省内存。 4. DNN:453):普通多层感知机,加 BatchNorm,学 implicit 交互。

四部分的输出求和:46-69)成最终 logit。use_Linear_part/use_FM_part/use_CIN_part/use_DNN_part 四个开关全在 YAML 里控制 —— 你可以一键关掉 FM 只留 CIN+DNN。

柱 3:Iterator —— 数据喂入deeprec/io/iterator.py:9

BaseIterator ABC 定义了 parser_one_lineload_data_from_file_convert_datagen_feed_dict。具体实现:FFMTextIterator:44,读 FFM 格式文本行),DKNTextIteratordkn_iterator.py:13,知识图谱特征),SequentialIteratorsequential_iterator.py:15),NextItNetIteratornextitnet_iterator.py:15)。

所以整个 deeprec 的训练流程是:

YAML → HParams → 传给模型 → _build_graph → 用 BaseModel 的 session 跑 → Iterator 喂数据

学会了这个三柱架构,你就看懂了 deeprec 下面所有模型 —— 换模型只是换 YAML + 换 _build_graph + 换 Iterator,骨架不动。

:deeprec 里 LightGCN(models/graphrec/lightgcn.py:35)是个另类 —— 它用的是 PyTorch,不是 TF1。在 DataModel/ImplicitCF.py:21 里预计算归一化邻接矩阵 D⁻¹⸍²AD⁻¹⸍²,然后叠 n_layers 层图卷积。评估层用的还是 recommenders.evaluation.python_evaluation 那四个排名指标 —— 你看到了,不管你用的是什么模型,评估永远是同一套东西。

newsrec 家族:双编码器模式

recommenders/models/newsrec/ 实现了 NRMS、NAML、NPA、LSTUR 四种新闻推荐模型。它们共享同一个双编码器模式newsrec/models/base_model.py:18):

新闻编码器:把一篇新闻(标题词 ID 序列,可选正文/类别/子类别)变成一个稠密向量。词 ID → 共享的 Embedding 层(可以用预训练的 word2vec .npy 初始化,nrms.py:169-174)→ CNN 或 Self-Attention → 注意力池化(layers.py:10 AttLayer2,加性注意力)。

用户编码器:把一个用户的点击历史(一序列的新闻向量)变成一个用户向量。怎么变取决于模型 —— NRMS(nrms.py:14)用多头自注意力(layers.py:110 SelfAttention),NAML(naml.py:15)加上了 body/vert/subvert 多视角,NPA(npa.py:14)让注意力权重依赖用户 ID embedding(个性化注意力),LSTUR(lstur.py:18)用 GRU 建模长短期兴趣。

预测:用户向量 · 候选新闻向量 → sigmoid → 概率。训练时是 softmax 多选一(base_model.py:72-77:同时构建 model 用作训练、scorer 用作推理)。

数据流datasets/mind.py:108 read_clickhistory 解析微软新闻数据集(MIND)的 behaviors.tsvio/mind_iterator.py:14 MINDIterator 迭代。

SASRec 家族:用 Transformer 做序列推荐

recommenders/models/sasrec/model.py:390 SASREC。这是 Kang 和 McAuley ICDM 2018 的工作。

做什么:跟 SAR / NCF 不一样的是,它考虑你行为的顺序。不只是"你喜欢过哪些物品",而是"你前天看了 A,昨天看了 B,今天看了 C → 猜你明天会看什么"。

怎么做

  • Item embedding + positional embedding → Transformer encoder(model.py:506)。
  • MultiHeadAttention:14)with causal masking:79-84)—— 预测 t 时刻的物品时,只能看到 ≤ t 的时刻。和 GPT 是同一个思想:不能偷看未来。
  • 训练时拿 (pos_seq, neg_seq) 几组候选,用 BCE loss(:607)。
  • 评估时报告 NDCG@10 和 Hit@10(:784-881),跟 SAR 的那套评估没有任何区别。
一个变体 SSEPTssept.py:12)继承了 SASREC 但额外加了 user embedding —— 让推荐也考虑"谁在看",不仅是"看了什么顺序"。

三个家族一张图小结

家族位置框架核心理念代表模型
deeprecmodels/deeprec/TF1 静态图特征交互(FM/CIN/DNN) + 序列 + 图xDeepFM, DKN, GRU, Caser, LightGCN
newsrecmodels/newsrec/Keras/TF1双编码器(新闻→向量,用户→向量,点积)NRMS, NAML, NPA, LSTUR
sasrecmodels/sasrec/PyTorchTransformer encoder + causal maskingSASRec, SSEPT
加上第四章的 SAR(numpy 矩阵)、第五章的 NCF 和 Wide&Deep(PyTorch),你现在手上有了看懂整个动物园的钥匙。

---

七、为什么不该用 accuracy:指标不是仪式

花半分钟做一个思想实验

你要评估一个约会软件的推荐质量。训练集里用户 A 喜欢了 100 个 profile 中的 3 个。模型给这 100 个 profile 都打了分:它一个都不推荐(输出全是"不喜欢")。这个模型在分类准确率(accuracy)上是多少?

97%。100 个里猜对了 97 个。

这有意义吗? 一点没有。用户打开约会软件看到空列表,准确率 97%,用户走了。

推荐系统面临的就是这个根本问题:正负样本极度不平衡。 一个用户看过的电影相对所有没看过的,比例可能是 1:1000 甚至 1:10000。用 accuracy 衡量推荐质量是自欺 —— 它奖励说"不"的模型。

用对了的指标

实现在 recommenders/evaluation/python_evaluation.py(1751 行),Spark 版在 spark_evaluation.py。我们来搞懂最重要的四个:

Precision@k:457):

Precision@k = (推荐的 k 个物品里用户真正喜欢的个数) / k
你推荐了 10 部电影,用户标记为喜欢的有 3 部 → Precision@10 = 0.3。不管用户总共喜欢了多少部。这个指标问的是:"我推给你的东西里有用的比例是多少?"

Recall@k:510):

Recall@k = (推荐的 k 个物品里用户真正喜欢的个数) / (用户总共喜欢的个数)
用户总共喜欢了 20 部电影,你推荐的 10 部里覆盖了 3 部 → Recall@10 = 0.15。这个指标问的是:"我喜欢的东西,你找到了多少?"

NDCG@k:616):比 Precision/Recall 更进一步 —— 它不光算"推对了没",还管对的东西排在什么位置。排在第 1 位的"对"比排在第 10 位的"对"更有价值。用对数衰减来给排位加权。NDCG 是最常用的推荐系统排名指标 —— 比赛常用、论文常用、面试也问。

MAP:751):对所有用户的 Average Precision 取平均。Average Precision 问的是:"这个用户从上往下扫推荐列表时,每扫到一个好东西,到那时为止的 precision 是多少?"把它整个清单扫完,平均一下。

还有"超越准确度"的指标

推荐的任务不止是"推对":

  • 多样性diversity :1341):你推给用户的 10 部电影是不是全是漫威?如果全是同一类,"对了"也没用 —— 用户会觉得这个 App 很无聊。
  • 新颖性novelty :1439):你推荐的东西是不是用户自己本来就知道会看排行榜的?热门电影《泰坦尼克号》推对了也不算本事 —— 用户自己也能想到。
  • 意外性serendipity :1631):推的东西既相关又出乎意料 —— "我没想到你会推这个,但确实很对我胃口。" 这个最难量化但不能不关心。
  • 覆盖率catalog_coverage :1680):你是不是翻来覆去只推"安全牌"的头部电影,还是敢探索长尾?

怎么跑?

不需要先算 accuracy 再算 precision。库的评估函数直接做:

from recommenders.evaluation.python_evaluation import precision_at_k

eval_precision = precision_at_k(
    test,         # 真实评分 DataFrame (userID, itemID, rating)
    top_k,        # 模型预测 DataFrame (userID, itemID, prediction)
    col_user="userID", col_item="itemID", col_rating="rating",
    k=10
)

里面的逻辑(merge_ranking_true_pred :379get_top_k_items :866):把 test 和 top_k 在 (userID, itemID) 上做 inner join → 按 prediction 排每个用户的前 k 个 → 对上的那几行就是命中的 → 按公式算。

一个陷阱:如果你用评分类指标(RMSE/MAE)来评估一个"天生做排名"的模型(比如 SAR),你得先把评分二值化 —— 在 notebook 里(sar_movielens.ipynb cell 12)有这一步,把 ≥3 的评分标为正、<3 的标为负。模型不输出 "预测评分 4.2",它输出 "置信度 0.83"。别搞混。

---

八、真正用起来:装机、换模型、调参

装库(跳过废话,直接上命令)

项目推荐用 uv 管理环境:

# 1. 装 uv(如果你还没有)
curl -LsSf https://astral.sh/uv/install.sh | sh

# 2. 建虚拟环境
uv venv ~/.venvs/recommenders --python 3.11
source ~/.venvs/recommenders/bin/activate

# 3. 核心包(CPU 能跑所有非 GPU notebook)
uv pip install recommenders

# 4. 创建 Jupyter kernel
uv pip install ipykernel
python -m ipykernel install --user --name recommenders --display-name "Python (recommenders)"

# 5. Clone 仓库,在 VS Code 里打开 notebook
git clone https://github.com/recommenders-team/recommenders.git

extras:什么时候多装什么

定义在 setup.py:50-82

Extra装了有什么什么时候需要
[gpu]TensorFlow 2.8-2.15 + PyTorch 2.0+跑 NCF/SASRec/Wide&Deep 的 GPU 加速,训练速度差 10-50x
[spark]PySpark 3.3+跑 Spark ALS、用 Spark 分桶做大数据评估
[dev]black + pytest + 测试工具链你想往库上改代码、跑单元测试
[experimental]xlearn, vowpalwabbit, nni, pymanopt, lightfm, scikit-surprise用对应的"实验性"模型(还没完全测试好的)
Windows/macOS 上安装有额外步骤(编译工具链、C++ 依赖),参考仓库的 SETUP.md

换模型只换一行

你写好了一个五阶段管线脚本,用的 SAR。现在想试 NCF。你要改什么?两处:

1. 模型构建:

# 之前
from recommenders.models.sar import SAR
model = SAR(col_user="userID", col_item="itemID", col_rating="rating", ...)
model.fit(train)

# 之后
from recommenders.models.ncf import NCF
from recommenders.models.ncf.dataset import Dataset as NCFDataset

data = NCFDataset(train=train, test=test, ...)   # NCF 需要负采样
model = NCF(n_users=data.n_users, n_items=data.n_items, model_type="NeuMF", ...)
model.fit(data)

2. 评估不变。完全不变。 同样是那四行 precision_at_k / ndcg_at_k / recall_at_k / map_at_k,SAR 的 top_k 输出和 NCF 的 top_k 输出是一样的 DataFrame 结构。

这就是那条四列名契约的力量 —— 换模型不改管线。

调参

傻瓜式(网格搜索)

recommenders/tuning/parameter_sweep.py:9 generate_param_grid 只用了 56 行代码来完成这件事:给你一个参数字典 {"相似度类型": ["jaccard","cosine","lift"], "top_k": [5,10,20]},它用 itertools.product 展开成笛卡尔积(:51),得到 3×3=9 组参数,逐组跑,选指标最高的。

更聪明的(NNI)

recommenders/tuning/nni/ncf_training.py:30 展示了如何把 NCF 接到 Microsoft NNI(nni==1.5,一个超参搜索框架)上:NNI 的搜索算法(TPE 等)自动选一组 n_factors/lr/n_epochs → 调 ncf_training 跑训练 → 回报指标 → NNI 根据结果决定下一组参数。比你自己乱猜快得多。

选哪个模型?一个粗暴的决策表

你的场景先试什么
刚学推荐系统,想快速看到结果SAR。不用 GPU,一行 fit,效果合理。
有用户/物品的属性特征(年龄、类别、价格……)Wide&DeepxDeepFM。这些天生吃多特征。
用户行为有明显顺序(先看 A 再看 B 再买 C)SASRecGRU。序列模型。
做新闻/文章推荐,文本是主要内容NRMSDKN。newsrec 家族就是为这个生的。
数据大到 pandas 放不下Spark 路线:Spark ALSals_movielens.ipynb)或 LightGBM on Sparkmmlspark_lightgbm_criteo.ipynb)。
想拿一套成熟的 baseline 跟你的方法比跑一下 examples/06_benchmarks/movielens.ipynb —— 表里列了各算法在 MovieLens 100k 上的全部指标(README.md:140-149)。
记住:不对的算法选对了数据预处理,可能比对的数据预处理配错的算法要好。 别上来就纠结是 NCF 还是 SASRec。先把数据看明白、把评估选对、拿 SAR 跑个 baseline。SAR 的 0.11 MAP 是你的底线 —— 一个新算法比不过它,你就是 cargo cult 调参。

---

九、带走的三条启发

1. 契约 > 复杂

整个库 20 个算法、5 万行代码,最重要的一行在 constants.py:5-23 —— 那四个列名。一个简单的约定,让分割器、模型、评估器可以任意组合。这不是什么高深学问,这是工程上最值钱的习惯:用最小的接口耦合最复杂的系统。下次你自己设计系统时,用同样的思路。

2. 先搞懂最简单的,再往上堆

SAR 只需要一个 dot()。NCF 需要 embedding + MLP + BCELoss。xDeepFM 需要 TF1 静态图 + CIN + DNN + FM + Linear 四路并行。但它们在本质上做的是同一件事:从 user-item 交互里学出一个从用户到物品的映射。搞懂了 SAR,你就能用"这个算法在 S 矩阵这件事上有什么新想法"来理解每一个后续算法,而不是被它们的名字吓住。

3. 命名不等于理解

你能列出 20 种推荐算法、18 个子目录名,不等于你会用它们。检验标准:你能不能不看文档,拿一个新数据集,从 load 到 evaluate,用 SAR 跑完一遍?如果能,你已经学会了。下一步只需要在 from recommenders.models.sar import SAR 那一行换 import —— 概念是一样的,只是内部机制变了。如果跑不出来,回去看第四章和第三章,直到能为止。

---

就这么回事。Go build something.

---

#RecommenderSystems #MicrosoftRecommenders #SAR #NCF #WideAndDeep #DeepRec #FeynmanLearning #智柴系统实验室🎙️

暂无表态