对应原课:CMU 11-667 Fall 2025 · Lecture 17 · Thu 30-Oct-2025 · LLMs for Recommendation Systems(客座:Neil Shah) 原课 slides:Recommenders 阅读材料:Kang & McAuley (2018), Self-Attentive Sequential Recommendation(SASRec);BehnamGhader et al. (2024), LLM2Vec: Large Language Models Are Secretly Powerful Text Encoders;Metzler et al. (2021), Rethinking Search: Making Domain Experts out of Dilettantes;Qin et al. (2023), Large Language Models are Effective Text Rankers with Pairwise Ranking Prompting 配套代码:code/lecture17/(1 个模块 + 4 个演示脚本) 本讲的写法:这是一次客座讲座,原课的四篇阅读材料对应 LLM 进入推荐系统的四个位置:
- 把推荐写成语言建模;
- 把商品写成 token;
- 把 LLM 当编码器;
- 把 LLM 当重排序器。
我们在一个”真实偏好已知”的合成集市上把四件事各做一遍。所有数字都来自配套代码的实际输出。
目录
- 0. 本讲导读
- 1. 推荐系统与语言模型
- 2. 序列推荐:预测下一件商品
- 3. 商品怎样变成 token:语义 ID 与生成式检索
- 4. 把解码器 LLM 变成文本编码器
- 5. 用 LLM 重排序:逐点、成对、列表
- 6. LLM 在推荐系统里的其他角色与挑战
- 7. 小结、常见误区、练习
0. 本讲导读
0.1 这一讲要回答的问题
推荐系统大概是机器学习在工业界最赚钱的应用:短视频、电商、新闻、广告、社交网络的信息流,背后都是推荐。它和语言模型看起来是两个领域,其实有很深的共同点:
- 用户按时间排好的点击或购买序列,就像一个句子;
- 商品就像词;
- “推荐下一件商品”就是”预测下一个 token”。
这一讲沿着阅读材料,看 LLM(以及 LLM 的技术)怎样进入推荐系统的四个位置:
- 建模:用 Transformer 建模行为序列(SASRec);
- 表示:商品怎样写成 token(原子 ID vs 语义 ID),推荐变成”生成下一件商品的 ID”(生成式检索);
- 编码:把解码器 LLM 改造成文本编码器(LLM2Vec),给没有行为数据的新商品一个向量;
- 排序:用 LLM 给候选重新排序(逐点、成对、列表)。
0.2 学完这一讲你应该能
- 说出推荐系统的基本结构(召回 → 排序 → 重排)和常用的评价指标;
- 解释 SASRec 和语言模型的对应关系,以及损失函数(全 softmax vs 负采样)的影响;
- 比较原子 ID 和语义 ID,解释生成式检索为什么在冷启动上有困难、前缀匹配怎样缓解;
- 说出把解码器变成编码器的步骤(池化、双向注意力、MNTP、对比学习),并理解”相似”由训练目标定义;
- 比较逐点、成对、列表三种重排序的效果、代价和位置偏差。
0.3 本讲的实验平台:花园集市
- 商品:144 件,是 43 个花园名词和若干形态的组合。
- 水果:seeds、sapling、basket、jam;
- 蔬菜:seeds、basket、soup;
- 花:seeds、bulb、bouquet;
- 动物:feed、toy、house。
- 每件商品有一段描述,例如
ripe plum jam。
- 用户类型:6 种,都是隐藏的:果园主(orchard)、厨师(kitchen)、花匠(florist)、农场主(farm)、宠物主人(pets)、送礼的人(gifts)。每种对品类和形态有不同的偏好;同一品类里的名词按 Zipf 定律有冷热。
- 购买过程:每个用户买 5~20 件。
- 以概率 ρ = 0.4 买”上一件的配套”:同一个名词、另一种形态,例如买了 plum seeds 接着买 plum sapling。
- 否则按自己类型的偏好重新挑一件。
因为生成过程完全已知,看过任何一段购买历史之后,“下一件商品”的真实分布都能精确算出来:先对 6 种类型做贝叶斯后验,再按后验混合。这给出了任何推荐模型都不可能超过的上限,就像第 1 讲的真实困惑度下界。
1. 推荐系统与语言模型
1.1 推荐系统在做什么
推荐系统的输入是用户的历史行为(点击、购买、观看、点赞,通常是”隐式反馈”——只知道用户做了什么,不知道他不喜欢什么),输出是”这个用户接下来最可能感兴趣的东西”的排序。工业系统一般分几个阶段:
- 召回(retrieval / candidate generation):从上亿件商品里快速找出几百到几千个候选。常用双塔模型:用户向量和商品向量的内积 + 近似最近邻搜索(第 10 讲 §4、第 11 讲)。
- 排序(ranking):用更复杂的模型(能看到用户和商品的交叉特征)给候选打分。
- 重排(re-ranking):考虑多样性、新鲜度、业务规则,对最终的列表做调整。
越往后的阶段处理的候选越少、模型越贵。LLM 的计算量很大,最自然的位置是在后面的阶段,或者在离线生成特征。
1.2 三类经典方法
- 协同过滤(collaborative filtering):
- 思路:“和你买过同样东西的人,还买了什么”。
- 矩阵分解把用户-商品矩阵分解成用户向量和商品向量的乘积。商品向量完全从行为里学出来,和商品是什么无关。
- 基于内容(content-based):用商品的描述、类别、图片等内容特征,推荐和用户喜欢过的东西”相似”的商品。不需要别人的行为数据,所以能处理新商品(冷启动)。
- 序列推荐(sequential recommendation):考虑行为的顺序。买了相机接下来可能买存储卡,而不是又一台相机。马尔可夫链、RNN(GRU4Rec)、Transformer(SASRec、BERT4Rec)都属于这一类。
1.3 和语言建模的对应
| 语言模型 | 序列推荐 |
|---|---|
| 词表(几万个 token) | 商品库(几万到几十亿件商品) |
| 一篇文档 | 一个用户的行为序列 |
| 预测下一个 token | 预测下一件商品 |
| 词嵌入(word2vec) | 商品嵌入(item2vec:把会话当句子训练 word2vec) |
| 上下文推断”这是什么文档” | 历史推断”这是什么用户” |
这个对应不只是比喻。
- SASRec(Kang & McAuley 2018)几乎就是第 2 讲的 GPT 式模型,只是 token 换成了商品 ID。
- Meta 的生成式推荐模型 HSTU(Zhai et al. 2024, Actions Speak Louder than Words)把行为序列当作”语言”,训练到上万亿参数,并观察到了和 LLM 类似的规模定律(第 12 讲)。
1.4 怎样评价
留一法(leave-one-out):每个用户的最后一个行为作为答案,用之前的历史去预测。常用指标(答案在推荐列表里的名次记为 r):
- HR@10(hit rate):答案在前 10 名里的比例。
- NDCG@10:答案在第 r 名时得 分(第 1 名得 1,第 3 名得 0.5),r > 10 得 0;只有一个答案时这就是 NDCG。
- MRR: 的平均。
一个常见的陷阱是抽样评测:只把答案和 100 个随机负例排序,而不是和全部商品排序。Krichene & Rendle (2020) 证明,这样算出的指标会改变不同方法之间的相对名次(练习 3)。本讲的实验都在全部 144 件商品上排序。
2. 序列推荐:预测下一件商品
2.1 SASRec
SASRec 的结构就是第 2 讲的因果 Transformer:
- 输入是商品 ID 的序列,每个位置用因果自注意力看之前的所有位置,输出这个位置的隐藏状态。
- 隐藏状态和所有商品的嵌入做内积,就是”下一件商品”的打分。输入嵌入和输出嵌入共享(第 1 讲 §4.4 的权重共享)。
原文的训练用负采样:每个位置一个正例(真实的下一件)和 1 个随机负例,二元交叉熵。语言模型的做法则是全 softmax:在全部商品上做 softmax,交叉熵。
2.2 实验:离上限有多远
代码 01 §1 先看贝叶斯后验怎样随购买更新。一个真实类型是 pets 的测试用户:
| 买的商品 | 后验最大的两种类型 |
|---|---|
| hungry hen feed | farm 0.55,pets 0.41 |
| hungry duck toy | pets 0.86,farm 0.08 |
| little duck house | pets 0.87,farm 0.08 |
| hungry hen house | pets 0.94,farm 0.05 |
| … | … |
| hungry cat toy | pets 1.00 |
第一件”鸡饲料”农场主和宠物主人都会买;买了”鸭子玩具”之后,几乎可以确定是宠物主人。
代码 01 §2:训练 6000 个用户,测试 2000 个用户(留一法)。
| 方法 | HR@10 | NDCG@10 | MRR |
|---|---|---|---|
| 流行度(不看历史) | 0.289 | 0.150 | 0.131 |
| 一阶马尔可夫(只看最后一件) | 0.654 | 0.430 | 0.370 |
| SASRec(d = 64、2 层) | 0.710 | 0.452 | 0.383 |
| 贝叶斯最优(真实分布) | 0.733 | 0.480 | 0.412 |
- 即使知道真实分布,HR@10 也只有约 0.73(期望值 0.729)。下一件商品本来就有很大的随机性——这是推荐的”困惑度下界”。只看指标的绝对值,没法判断一个模型好不好;真实数据集没有这把标尺,所以要和强基线比。
- 马尔可夫抓住了”买配套”。SASRec 还利用了更早的历史来推断用户类型,离上限只差一点。
2.3 实验:损失函数比结构更重要
代码 01 §3(NDCG@10):
| 训练用户数 | 马尔可夫 | SASRec + 1 个负例 BCE | SASRec + 全 softmax |
|---|---|---|---|
| 500 | 0.404 | 0.290 | 0.382 |
| 2000 | 0.426 | 0.371 | 0.434 |
| 6000 | 0.430 | 0.420 | 0.452 |
- 全 softmax 在每个数据量上都更好,数据越少差距越大:1 个随机负例给的信号太弱,大多数随机负例很容易区分(“一个爱宠物的人不会买番茄汤”),模型学不到细微的差别。Klenitskiy & Vasilev (2023) 发现,把 SASRec 的损失换成全 softmax 交叉熵后,它追上甚至超过了 BERT4Rec。很多”新模型更好”的结论,其实来自损失函数的差别。
- 商品有上亿件时全 softmax 太贵,于是有了各种折中:采样 softmax(用多个负例并做概率校正)、批内负例、困难负例挖掘。
- 只有 500 个用户时,简单的马尔可夫胜过两种 SASRec:数据少时,“只看最后一件”这个强假设是优点(偏差-方差权衡,前置讲 P2)。
2.4 实验:历史越长,越需要看整段历史
代码 01 §4(NDCG@10):
| 历史长度 | 人数 | 马尔可夫 | SASRec | 贝叶斯 |
|---|---|---|---|---|
| 4~7 件 | 515 | 0.405 | 0.410 | 0.437 |
| 8~12 件 | 669 | 0.425 | 0.456 | 0.490 |
| 13~19 件 | 816 | 0.448 | 0.475 | 0.498 |
历史越长,类型越确定,上限越高。马尔可夫只看最后一件,用不上这些信息;SASRec 和马尔可夫的差距随历史变长而拉大。
2.5 实验:SASRec 在隐藏状态里推断用户类型
代码 01 §5:
- 探针:在另外 3000 个用户上,用 SASRec 最后一个位置的隐藏状态训练一个线性探针(第 14 讲 §3),预测用户的类型。
- 结果:探针的准确率 94.4%,贝叶斯后验的 argmax(能做到的最好)97.3%,瞎猜 16.7%。
SASRec 从没被告知”用户有类型”,却在隐藏状态里形成了接近最优的类型推断:要预测下一件商品,就得先弄清楚”这是什么样的用户”。这和语言模型在上下文里推断”这是什么样的文档”(第 9 讲 §2:上下文学习是隐含的贝叶斯推断)是同一回事。
3. 商品怎样变成 token:语义 ID 与生成式检索
3.1 原子 ID 的问题
SASRec 给每件商品一个独立的 token:原子 ID。它有三个问题:
- 嵌入表巨大:10 亿件商品 × 64 维 = 640 亿个参数,比很多 LLM 还大,而且绝大多数商品的嵌入只被训练过几次(长尾,和第 11 讲的冷门知识一样)。
- 没有共享:
plum jam和pear jam的 ID 之间没有任何关系,一件商品的数据帮不了另一件。 - 冷启动:新上架的商品从没出现在训练数据里,它的嵌入是随机的。
3.2 语义 ID 与生成式检索
“模型即索引”(Metzler et al. 2021, Rethinking Search):
- 传统检索:建倒排索引或向量索引,模型只负责给文档打分。
- 设想:一个模型直接记住所有文档,给定查询就生成相关文档的 ID。
- 实现:DSI(Tay et al. 2022, Transformer Memory as a Differentiable Search Index)实现了这个设想。它发现文档 ID 怎么写很重要:随机的整数 ID 不如有语义结构的 ID——先对文档的嵌入做层次聚类,ID 就是聚类树上的路径。
TIGER(Rajput et al. 2023)把同样的想法用在推荐上:
- 语义 ID:用 RQ-VAE(残差量化)把商品的内容嵌入量化成几个离散的码 (c1, c2, c3)。
- 第 1 层码表示粗的类别,后面的码是对残差的细化。
- 内容相似的商品共享前缀。
- 推荐:变成”给定历史里各件商品的语义 ID 序列,生成下一件商品的语义 ID”,用束搜索、并限制在有效 ID 的前缀树(trie)上解码,避免生成不存在的商品。
3.3 实验:三种写法
代码 02 §1:
| 方案 | 每件商品 | 词表大小 | 例子 |
|---|---|---|---|
| 原子 ID | 1 个 token | 144 | sweet apple jam → i3 |
| 商品名的词 | 2 个 token(名词、形态) | 53 | sweet apple jam → apple jam |
| 残差 k-means 码 | 3 个 token | 29 | sweet apple jam → a8 b2 c0 |
残差 k-means 是 RQ-VAE 的”不训练编码器”版本。内容向量是品类、形态、名词、形容词的加权 one-hot。
- 第 1 层的 13 个簇几乎正好对应(品类, 形态):a8 = fruit-jam,a12 = animal-feed……
- 第 2 层对残差聚类,却把名词随意地分组(例如 b0 = bean、cabbage、duck、kiwi、leek、lettuce……):内容向量里不同名词的 one-hot 互相正交,没有”哪些名词相似”的信息。
- 第 3 位是前缀相同时的消歧编号,最多到 7。
真实系统用文本编码器(TIGER 用 Sentence-T5)得到内容向量,名词之间的语义相似会让后几层的码也有意义——语义 ID 的质量取决于内容向量的质量(§4)。
3.4 实验:冷启动
代码 02 §2:
- 新商品:随机挑 24 件商品作为”刚上架的新商品”。训练用的 3000 个用户来自一个还没有它们的集市。
- 测试用户:1000 个,来自完整的集市。
- 217 人的答案是新商品;
- 83% 的人的历史里有新商品——新商品不只是”要推荐的东西”,也出现在输入里。
- 上限:贝叶斯最优对老商品答案的 NDCG@10 是 0.478,对新商品答案的 HR@10 是 0.797。
三个模型的结构相同,打分 = 商品的全部 token 在历史之后的 log 概率之和:
| 方案 | 老商品答案 NDCG@10 | 新商品答案 HR@10 |
|---|---|---|
| 原子 ID | 0.401 | 0.037 |
| 商品名的词 | 0.397 | 0.000 |
| 残差 k-means 码 | 0.388 | 0.046 |
- 老商品上三种方案差不多:原子 ID 并不吃亏。README 还记录了一个原型实验:只有 500 个训练用户时,原子 ID 反而最好。“语义 ID 更省数据”在这里没有得到支持。
- 新商品上全都很差,包括语义 ID。
plum jam从没出现过,模型学到的恰恰是”plum 后面不会跟 jam”——商品名的词这一方案对新商品的概率几乎是 0。生成式的模型把训练里见过的组合当作全部可能的商品,它记住了目录。
3.5 实验:前缀匹配
TIGER 处理冷启动的办法:模型生成前缀,前缀相同的新商品也放进候选。代码 02 §3 用一个等价的打分:
- 老商品仍用完整的 log 概率;
- 新商品用”前缀的 log 概率 − log(同前缀的商品数)“,相当于假设最后一个 token 在同前缀的商品里均匀分布。
| 方案 | 新商品答案 HR@10 | 老商品答案 NDCG@10 | 全部答案 NDCG@10 |
|---|---|---|---|
| 原子 ID | 0.037 | 0.401(原 0.401) | 0.319(原 0.319) |
| 商品名的词 | 0.479 | 0.380(原 0.397) | 0.348(原 0.311) |
| 残差 k-means 码 | 0.240 | 0.375(原 0.388) | 0.311(原 0.307) |
- 原子 ID 没有前缀可用,新商品永远推荐不出来。
- 商品名的词让新商品和老商品共享
plum:模型知道用户接下来很可能买 plum 的什么东西,新的plum jam就有了位置。残差码的第 2 层是随意分组的,前缀提供的信息少得多。 - 前缀匹配要付代价:新商品的分数是”猜”出来的,会挤掉一些老商品(老商品的 NDCG 略降)。但新商品答案的收益更大,全部答案的 NDCG 上升了。新商品越多,这笔交易越划算。
4. 把解码器 LLM 变成文本编码器
4.1 为什么需要文本编码器
§3 的冷启动还有另一条路:按内容推荐。把商品描述编码成向量,推荐和用户买过的东西相似的商品——新商品只要有描述,就有向量。这需要一个好的文本编码器。过去用 BERT 式的双向编码器(Sentence-BERT、E5);而现在最强的语言模型都是解码器(GPT 式)。能不能直接拿来用?
解码器当编码器有两个天生的问题:
- 因果注意力:第 i 个 token 的表示只看到前面的 token。句子开头的词”不知道”后面说了什么,所以单个位置的表示都是”不完整的”。
- 池化:取最后一个 token 的表示(只有它看过整句),还是取所有位置的平均(每个位置都只看过一部分)?
LLM2Vec(BehnamGhader et al. 2024)用三步把解码器变成编码器:
- 打开双向注意力:把因果掩码换成全可见的掩码,参数不变。
- 掩码下一个 token 预测(MNTP):随机遮住一些 token,用前一个位置的输出预测被遮住的 token。这结合了 BERT 的掩码语言建模和 GPT 的”位置 i−1 预测 i”,让模型在很少的训练里适应双向注意力。
- 无监督对比学习(SimCSE,Gao et al. 2021):同一个句子经过两次不同的随机 dropout 得到两个表示,作为正例拉近;和批里其他句子推远。
此外常用的还有:用有标注的检索数据做有监督对比学习(E5-Mistral 等);“回声嵌入”(把句子重复两遍,取第二遍的表示,这样每个 token 都”看过”整句)。
4.2 实验:一步步把解码器变成编码器
代码 03:
- 预训练:在集市的文本上预训练一个小解码器(d = 64、2 层,训练 4 遍)。
- 商品目录:全部 144 件商品的描述,包括 24 件新商品;
- 3000 个用户的购物日记,例如
I bought pretty poppy bulb . then I bought sweet apple sapling . …。 - 新商品只出现在目录里,从没出现在日记里。
- 评价:商品向量的余弦相似度和”真实亲和度”的 Spearman 相关。真实亲和度是同一个用户按偏好挑时买两件商品的 PMI:花匠既买 tulip bulb 也买 rose bouquet,它们的亲和度高。只看名词不同的商品对(名词相同的”配套”太容易),以及名词和形态都不同的商品对(词袋在这里只剩”品类”一个线索)。
| 商品向量 | 名词不同 | 名词、形态都不同 |
|---|---|---|
| 词袋(one-hot) | 0.510 | 0.345 |
| 解码器,最后一个 token | 0.495 | 0.343 |
| 解码器,平均池化 | 0.516 | 0.416 |
| 直接打开双向注意力(不训练) | 0.440 | 0.330 |
| + MNTP 适应(双向) | 0.552 | 0.463 |
| + 无监督 SimCSE | 0.453 | 0.316 |
| + 有监督对比(共同购买的商品对) | 0.692 | 0.672 |
- 平均池化好于最后一个 token:因果模型里最后一个 token 的表示主要关于”描述的最后一个词”(形态)。
- 直接打开双向注意力反而变差:模型从没在双向的条件下训练过,突然看到”未来”的 token,表示就乱了。这正是 LLM2Vec 观察到的现象。
- MNTP 只用 2 遍训练就让模型适应了双向注意力,表示比原来的解码器更好。
- 无监督 SimCSE 在这里变差了——和 LLM2Vec 原文在语义相似度(STS)任务上的结果相反。它的目标是”把同一段文本的两个噪声版本拉近、和所有其他文本推远”,
plum jam和pear jam也被推远了。它学到的相似是”是不是同一段文字”,不是”会不会被一起买”。 - 用共同购买的商品对做有监督的对比学习(推荐版本的对比学习,例如 UniSRec、Recformer 的思路)直接优化了推荐需要的相似,提升最大。
“相似”是由训练目标定义的。 同一个编码器,用不同的对比目标,学出的是不同意义的相似。
4.3 实验:按内容给新商品做推荐
代码 03 §3:用户向量 = 历史里各件商品向量的平均,按余弦相似度给全部商品排序。
| 商品向量 | 新商品答案 HR@10 | 全部答案 NDCG@10 |
|---|---|---|
| 词袋 | 0.410 | 0.201 |
| 解码器,最后一个 token | 0.258 | 0.111 |
| 解码器,平均池化 | 0.359 | 0.134 |
| 直接打开双向注意力 | 0.336 | 0.126 |
| + MNTP | 0.387 | 0.124 |
| + 无监督 SimCSE | 0.419 | 0.166 |
| + 有监督对比(共同购买) | 0.488 | 0.163 |
| 对照:语义 ID + 前缀匹配(§3.5) | 0.479 | — |
- 按内容推荐对新商品很公平:新商品和老商品的向量用同样的方法算出来,不需要购买记录。
- 词袋在这里出奇地强:
- “配套”就是同名词的另一种形态,词袋的名词重合正好捕捉了它。
- 解码器的几种表示在亲和度上更好,在这个任务上却不如词袋。表示的”好”要看下游任务。
- 有监督对比学习的向量对新商品推荐得最好,和语义 ID + 前缀匹配相当(217 个新商品答案,差距在误差之内)。新商品的描述和老商品共享词,于是继承了”哪些东西会被一起买”的知识。
- 按内容推荐对全部答案很弱(NDCG 远低于 SASRec 的 0.452):它不知道顺序,也不知道”配套”的方向。实际系统把两者结合:序列模型负责老商品,内容向量负责新商品,或者把内容向量作为序列模型的输入特征。
5. 用 LLM 重排序:逐点、成对、列表
5.1 三种问法
第二阶段的排序模型可以是一个 LLM:把用户的历史(或搜索的查询)和候选写成文本,让 LLM 判断。Qin et al. (2023) 总结了三种”问法”:
| 问法 | 提示的样子(原理) | 调用次数 | 问题 |
|---|---|---|---|
| 逐点(pointwise) | “这件商品和用户相关吗?“取”是”的概率;或者用”根据这件商品生成查询 / 历史”的似然(查询似然,UPR,Sachan et al. 2022) | n | 不同候选的分数不一定可比(校准问题);回答”是 / 否”的格式要求模型输出概率 |
| 成对(pairwise,PRP) | “A 和 B 哪个更相关?” | 全部比较 O(n²);排序算法 O(n log n);滑动窗口取前 k 名 O(kn) | 模型可能偏向某个位置;比较结果可能不传递(A > B、B > C、C > A) |
| 列表(listwise,RankGPT,Sun et al. 2023) | 一次给出全部候选,让模型输出排好的列表 | 1(候选太多时用滑动窗口) | 长上下文;对候选的初始顺序敏感;可能漏掉或编造候选 |
PRP 的发现是:在中等规模的开源 LLM 上,成对的问法明显好于逐点和列表,因为比较两个比给出绝对分数容易;每一对正反各问一次,可以抵消位置偏差。
5.2 实验设置
代码 04:
- 第一阶段:一个弱的召回——和用户最后买的那件商品内容最相似的 20 件。它便宜,也不看整段历史。
- 在 600 个测试用户上,答案落在 20 个候选里的比例是 68.2%,这是任何重排序都无法超过的上限。
- 第一阶段自己的顺序 NDCG@10 只有 0.148;按真实分布重排这 20 个候选能达到 0.439。
- 重排序器:三个模型都从预训练的 SASRec(6000 个用户)出发微调 5 遍,像拿一个预训练的 LLM 来做重排序。训练数据是另外 6000 个用户。
- 三种问法:都只在最后一个位置预测一个”答案 token”。
逐点 <bos> 历史 <sep> 候选 → <yes> / <no>
成对 <bos> 历史 <sep> A <sep2> B → <A> / <B>
列表 <bos> 历史 <sep> 候选1 … 候选20 <sep2> → 说出最可能的那件商品
5.3 实验:效果与代价
| 排序方法 | NDCG@10 | 调用次数(每个用户) |
|---|---|---|
| 第一阶段的顺序 | 0.148 | — |
| 逐点(yes 的 logit − no 的 logit) | 0.281 | 20 |
| 成对:全部比较,正反各一次 | 0.279 | 380 |
| 成对:全部比较,只问一个方向 | 0.267 | 190 |
| 成对:滑动窗口 10 趟(只要前 10) | 0.274 | 290 |
| 列表:一次给出全部候选 | 0.344 | 1 |
| 贝叶斯最优重排 | 0.439 | — |
- 三种重排序器都把第一阶段的 0.148 提高到了 0.27~0.34。
- 在这个设置里,列表最好,而且只要 1 次调用(但输入长度是逐点的好几倍)。
- 成对正反各问一次比只问一个方向好,但调用次数多得多。滑动窗口只要前 10 名,省了约四分之一的调用,效果接近。
这和 PRP 在 LLM 上”成对最好”的结论不同,原因可能有两个:我们的列表模型是专门微调过的,而 PRP 讨论的是零样本提示;我们的候选只有 20 个,一个上下文放得下。
5.4 实验:位置偏差
代码 04 §3:
- 成对:把同一对 (A, B) 正反各问一次,两次结论一致的比例是 81.9%;平均”选第一个”的概率是 0.535(无偏差应为 0.5)。模型略偏向排在前面的那个。
- 列表:
| 列表模型 | 候选顺序 | NDCG@10 |
|---|---|---|
| 按第一阶段的顺序训练 | 第一阶段顺序 | 0.344 |
| 按第一阶段的顺序训练 | 倒过来 | 0.325 |
| 按第一阶段的顺序训练 | 随机打乱 | 0.334 |
| 用随机顺序训练 | 第一阶段顺序 | 0.343 |
| 用随机顺序训练 | 倒过来 | 0.344 |
按第一阶段顺序训练的列表模型学到了一点”排在前面的更可能是答案”的捷径,候选顺序一倒过来就掉了约 2 个点;用随机顺序训练就对顺序不敏感。
LLM 的列表重排序同样对初始顺序敏感。Tang et al. (2024) 的”排列自洽性”对多个随机顺序的结果取平均;PRP 正反各问一次也是同样的思路。位置偏差是从训练数据里学来的捷径(第 7 讲、第 9 讲反复出现的主题)。
5.5 实验:不微调,直接用预训练模型的概率
代码 04 §4:不做任何微调,直接用预训练 SASRec 给每个候选的”下一件商品”概率排序,NDCG@10 = 0.432,几乎达到上限 0.439,比三种微调过的重排序器都好。
- 为什么:预训练的目标(预测下一件商品)本来就是排序需要的东西。把模型改造成”回答 yes/no、A/B”的新格式,要用有限的数据重新学会格式,反而丢掉了一部分预训练的知识。把学习率降低到 3e-4 也没有改善(README)。
- 和真实 LLM 的对应:用似然打分的零样本方法常常是很强的基线(例如 UPR 用”由文档生成查询”的似然给文档排序)。微调的价值在于数据充足、或者排序标准和预训练目标差别很大的时候(例如复杂的业务规则、多目标的权衡)。
6. LLM 在推荐系统里的其他角色与挑战
6.1 其他角色
- 直接让 LLM 推荐:把用户历史写成文本,让 LLM 推荐。零样本的效果通常不如专门训练的推荐模型(它不知道商品库里有什么、哪些商品流行),但在没有行为数据的冷启动场景、或者需要解释推荐理由时有用。
- 生成特征:
- 用 LLM 离线生成商品的摘要、标签,以及用户的兴趣画像,作为传统推荐模型的输入特征。
- 这把 LLM 的高成本放到了离线,线上仍然用便宜的模型。
- 对话式推荐:用户用自然语言描述需求(“给喜欢园艺的朋友买个礼物,100 块以内”),LLM 负责理解需求和多轮澄清,推荐模型负责检索。
- 用户模拟器:用 LLM 扮演用户来评估推荐系统或生成训练数据。第 8 讲讨论过用模拟用户评估的陷阱:模拟用户是否像真实用户?
6.2 挑战
- 延迟和成本:推荐系统每天要处理数十亿次请求,每次只有几十毫秒。LLM 只能用在少量候选上,或者离线使用,或者蒸馏成小模型(第 19 讲的高效推理)。
- 编造商品:生成式的推荐可能生成不存在的商品。解决办法是约束解码(只允许商品库里的 ID 或标题)。
- 流行度偏差和位置偏差:LLM 偏向它在预训练语料里见得多的东西(热门商品、知名品牌);重排序有位置偏差(§5.4)。
- 反馈循环:推荐系统决定了用户能看到什么,用户的行为又成了下一轮的训练数据。偏差会自我强化(第 15 讲的偏见放大)。
- 隐私:行为序列高度敏感;第 16 讲的记忆与成员推断同样适用于推荐模型。
7. 小结、常见误区、练习
7.1 小结
- 推荐与语言建模是同一个问题:
- 行为序列就像句子,商品就像 token;SASRec 是商品上的因果 Transformer。
- 在花园集市上它离贝叶斯最优(HR@10 0.733)只差一点(0.710),还在隐藏状态里学会了推断隐藏的用户类型(探针准确率 94%)。
- 全 softmax 交叉熵在各个数据量上都好于 1 个负例的 BCE。
- 商品的写法:
- 原子 ID 没有共享,新商品是陌生的 token;语义 ID 让商品共享 token。
- 生成式的模型会学到”没见过的组合不存在”,要靠前缀匹配才能推荐新商品(新商品 HR@10 从 0.000 到 0.479)。
- 语义 ID 的价值取决于内容向量的质量。
- 解码器变编码器:
- 平均池化优于最后一个 token;直接打开双向注意力会变差,MNTP 适应后变好。
- “相似”由训练目标定义:无监督 SimCSE 在推荐的亲和度上变差,用共同购买对的有监督对比学习提升最大(0.552 → 0.692)。
- 按内容推荐能自然地处理新商品。
- 重排序:
- 逐点、成对、列表各有代价:调用次数、校准、位置偏差。
- 位置偏差是学来的捷径,正反各问一次或用随机顺序训练能消除它。
- 预训练目标和任务一致时,零样本的似然打分可能比微调成新格式更好。
7.2 常见误区
- “推荐指标 HR@10 只有 0.7,模型还很差”:下一件商品本来就有随机性,上限可能就在附近(§2.2)。
- “新的结构更好”:先检查损失函数、负采样、评测方式是否相同(§2.3、§1.4)。
- “用 100 个随机负例评测和全量排序结论一样”:抽样评测会改变方法之间的名次(练习 3)。
- “语义 ID 自动解决冷启动”:生成式的模型会学到”没见过的组合不存在”,需要前缀匹配等额外的机制(§3.4)。
- “解码器的最后一个 token 就是句子向量”:平均池化通常更好,双向注意力需要适应训练(§4.2)。
- “对比学习总能让文本向量更好”:取决于正例的定义是否符合下游任务(§4.2)。
- “微调过的重排序器一定好于零样本”:格式转换可能丢掉预训练的知识(§5.5)。
7.3 练习
练习 1(NDCG):四个测试用户的答案分别排在第 1、3、12、2 名。算 HR@10、NDCG@10、MRR。
参考答案
- HR@10 = 3/4 = 0.75(第 12 名不算)。
- NDCG@10 = [1 + 1/log₂4 + 0 + 1/log₂3] / 4 = [1 + 0.5 + 0 + 0.631] / 4 = 0.533。
- MRR = [1 + 1/3 + 1/12 + 1/2] / 4 = 1.917 / 4 = 0.479。
NDCG 对名次的惩罚比 MRR 温和:第 2 名在 NDCG 里得 0.63,在 MRR 里只得 0.5。
练习 2(类型后验):只有两种用户类型,先验各 0.5。某件商品 x 在”重新挑”时被类型 A 买的概率是 0.02,被类型 B 买的概率是 0.005。
(a) 一个用户的第一件商品是 x,他是 A 类型的后验概率是多少? (b) 接着他买了 y(不是 x 的配套),两种类型买 y 的概率分别是 0.01 和 0.03(ρ = 0.4),后验是多少?
参考答案
(a) P(A | x) = 0.5 × 0.02 / (0.5 × 0.02 + 0.5 × 0.005) = 0.01 / 0.0125 = 0.8。
(b) y 不是 x 的配套,所以”买配套”那一项是 0,每种类型的似然是 (1 − ρ) g_k(y):
- A:0.6 × 0.01 = 0.006;
- B:0.6 × 0.03 = 0.018。
P(A | x, y) = 0.8 × 0.006 / (0.8 × 0.006 + 0.2 × 0.018) = 0.0048 / (0.0048 + 0.0036) = 0.571。
每一件商品都按”不同类型买它的概率之比”更新后验,这就是 type_posterior 做的事。§2.2 的表里,“鸭子玩具”让 pets 的后验从 0.41 跳到 0.86,因为农场主几乎不买玩具。
练习 3(抽样评测的陷阱):全部 144 件商品里,某个测试用户的答案排第 12 名(前面有 11 件)。
(a) 如果只把答案和 100 个从其余 143 件里随机抽的负例排序,答案前面期望有几件?这次”命中前 10”吗? (b) 另一个答案排第 30 名呢? (c) 为什么这会改变不同方法之间的比较?
参考答案
(a) 排在前面的 11 件里,期望被抽中 11 × 100/143 ≈ 7.7 件,答案期望排第 8.7 名,大多数情况下算命中。全量排序时它不在前 10。
(b) 29 × 100/143 ≈ 20.3 件,期望排第 21 名左右,不命中。
(c) 抽样评测把”全量排序里的名次”非线性地压缩了:
- 排在 11~15 名的答案大多变成了”命中”;
- 排在前 10 的答案名次也被往前挪,NDCG 会变高。
两个方法如果一个擅长”把答案排进前 5”、另一个擅长”把答案排进前 20”,抽样评测可能颠倒它们的名次。Krichene & Rendle (2020) 证明抽样指标和全量指标在名次上可能不一致,建议使用全量排序或做校正。
练习 4(负采样的信号):SASRec 的 BCE 损失每个位置用 1 个随机负例。
(a) 对一个”宠物主人”用户,144 件商品里大约 24 件是动物用品。随机抽的负例有多大概率是动物用品(真正难区分的负例)? (b) 全 softmax 的梯度和 1 个负例的 BCE 有什么不同? (c) 为什么数据越少,差距越大?
参考答案
(a) 约 24/144 ≈ 17%。83% 的时候负例是”宠物主人根本不会买”的东西,模型很快就能把它们的分数压下去,之后这些负例几乎不提供梯度。
(b) 全 softmax 的梯度把每个商品的分数按它当前的预测概率往下推。模型越”误以为”会买的商品被推得越多,所以难的负例(其他动物用品)自动得到最多的关注——这相当于自动的”困难负例挖掘”。1 个随机负例的 BCE 只推一个随机商品。
(c) 数据少时,总的梯度步数少。1 个随机负例的 BCE 浪费了大部分步数在容易的负例上,在 500 个用户时 NDCG 差了 0.09(0.290 vs 0.382);数据多时,即使信号稀疏,最终也能学到(6000 个用户时只差 0.03)。
练习 5(嵌入表的大小):一个平台有 10 亿件商品,嵌入维度 64。
(a) 原子 ID 的嵌入表有多少参数?用 fp32 存要多大? (b) 用 3 层、每层 256 个码的语义 ID,码的嵌入表有多少参数?3 层码能区分多少种组合? (c) 不够的话怎么办?
参考答案
(a) 10⁹ × 64 = 640 亿参数,fp32 下 256 GB。这比很多 LLM 都大,而且需要专门的分布式嵌入存储。
(b) 3 × 256 × 64 ≈ 4.9 万参数。3 层能区分 256³ ≈ 1678 万种组合,比 10 亿少得多。
(c) 有两个办法:
- 加层数或码本大小:4 层就是 43 亿种组合。
- 加消歧位:像 TIGER 和本讲的 c3,区分前缀相同的商品。
前缀相同的商品越多,消歧位越大,生成最后一个 token 也越难(它没有语义可言)。实际系统常用”语义 ID + 少量原子 ID 嵌入”的混合方案。
练习 6(约束解码):用语义 ID 做生成式推荐时,模型可能生成一个不存在的 (c1, c2, c3) 组合。
(a) 怎样避免? (b) 本讲 §3.4 的”商品名的词”方案里,新商品的概率几乎是 0。约束解码能解决这个问题吗?
参考答案
(a) 把商品库里所有有效的 ID 组织成前缀树(trie)。解码每一步时,只允许选择”当前前缀在树上的子节点”(把其他 token 的概率设成 0 再归一化),束搜索就只会产生有效的商品。LLM 生成商品标题时也一样,约束在商品库的标题集合上。
(b) 不能直接解决。约束解码保证生成的是存在的商品,但新商品在模型的分布里本来就概率很低(“plum 后面不会跟 jam”),约束之后仍然排在后面。要把新商品放进 trie,并且像 TIGER 那样按前缀给它们分配候选位置(§3.5 的前缀匹配),或者用内容向量单独召回(§4.3)。
练习 7(重排序的调用次数):有 100 个候选,要前 10 名。
(a) 逐点要多少次调用? (b) 成对全部比较(正反各一次)要多少次? (c) 成对滑动窗口(每趟从后往前比较相邻两个,正反各一次)要多少次? (d) 列表滑动窗口(窗口 20、步长 10,从后往前一趟)要多少次?
参考答案
(a) 100 次。
(b) 100 × 99 = 9900 次。
(c) 第 p 趟(p = 0..9)比较 99 − p 对,每对 2 次:2 × (99 + 98 + … + 90) = 2 × 945 = 1890 次。(代码 04 里 20 个候选、10 趟是 2 × 145 = 290 次。)
(d) 窗口从 [80, 100) 开始,每次往前移 10,到 [0, 20) 为止,共 (100 − 20)/10 + 1 = 9 次调用。每次调用的输入有 20 个候选,长得多。一趟只能保证最好的 10 个被”带”到前面(RankGPT 的做法),要更多的前 k 名需要多趟。
这些数字说明了为什么 LLM 重排序通常只用在最后几十个候选上。
练习 8(抵消位置偏差):假设成对比较时,模型给出 P(选 A) = σ(s_A − s_B + β),β > 0 是对第一个位置的偏好。
(a) 只问 (A, B) 一次,按 P > 0.5 判断,什么时候会出错? (b) 正反各问一次,取 [P(A 先) + (1 − P(B 先))] / 2,偏差还在吗?
参考答案
(a) 当 0 < s_B − s_A < β 时:B 真的更好,但偏好让 σ(s_A − s_B + β) > 0.5,模型选了 A。两者越接近,越容易被位置偏差左右。
(b) 第一次 P₁ = σ(s_A − s_B + β);第二次问 (B, A),P₂ = σ(s_B − s_A + β),A 胜的概率是 1 − P₂ = σ(s_A − s_B − β)。
取平均 [σ(d + β) + σ(d − β)] / 2(d = s_A − s_B)。它关于 d 是奇对称的(d = 0 时恰好是 0.5),所以按 > 0.5 判断时,A 胜当且仅当 d > 0——位置偏差被完全抵消了,只是平均后的概率比 σ(d) 更”平”(不那么自信)。§5.4 的模型平均选第一个的概率是 0.535,正反各一次的 NDCG 比只问一个方向高(0.279 vs 0.267)。
练习 9(思考:一个没有行为数据的新平台):一个新的手工艺品集市刚上线:有 5 万件商品的标题、描述和图片,但几乎没有用户行为数据。只讨论原理,设计它的推荐系统,并说明随着数据积累应该怎样演变。
参考答案(开放)
第一阶段(几乎没有行为数据):
- 内容向量:用现成的文本 / 多模态编码器(或 LLM2Vec 式改造的 LLM)给商品编码,做”看过这件的人可能喜欢相似的”推荐(§4)。
- 冷启动的用户:用对话或少量问题了解兴趣,让 LLM 把回答转成查询。
- LLM 零样本重排:用 LLM 对少量候选按用户描述的兴趣排序(§5),注意位置偏差和成本。
- 离线生成特征:用 LLM 给商品生成标签和摘要,作为特征。
第二阶段(有了一些行为数据):
- 用共同购买 / 共同点击的商品对做有监督的对比学习,微调内容编码器(§4.2 的最后一行),让”相似”变成”会被一起买”。
- 开始训练序列模型(SASRec 式,全 softmax 交叉熵)。数据少时先和马尔可夫这类简单基线比较(§2.3 里 500 个用户时马尔可夫更好)。
第三阶段(数据充足):
- 序列模型负责老商品,内容向量 / 语义 ID + 前缀匹配负责新商品。
- 两阶段架构:便宜的召回 + 较贵的排序。
- 持续做全量排序的离线评测和线上 A/B 测试,监控流行度偏差和反馈循环。
贯穿始终:评测要用留一法 + 全量排序;对新商品和老商品分别报告指标(§3.4);注意隐私(第 16 讲)。
7.4 延伸阅读
- 原课阅读:
- Kang & McAuley (2018), Self-Attentive Sequential Recommendation
- BehnamGhader et al. (2024), LLM2Vec
- Metzler et al. (2021), Rethinking Search
- Qin et al. (2023), Large Language Models are Effective Text Rankers with Pairwise Ranking Prompting
- 序列推荐:
- Hidasi et al. (2016), GRU4Rec
- Sun et al. (2019), BERT4Rec
- Klenitskiy & Vasilev (2023), Turning Dross Into Gold Loss: is BERT4Rec really better than SASRec?
- Zhai et al. (2024), Actions Speak Louder than Words: Trillion-Parameter Sequential Transducers for Generative Recommendations(HSTU)
- Krichene & Rendle (2020), On Sampled Metrics for Item Recommendation
- 语义 ID 与生成式检索:
- Tay et al. (2022), Transformer Memory as a Differentiable Search Index(DSI)
- Rajput et al. (2023), Recommender Systems with Generative Retrieval(TIGER)
- Singh et al. (2024), Better Generalization with Semantic IDs
- 文本编码器:
- Gao et al. (2021), SimCSE
- Wang et al. (2024), Improving Text Embeddings with Large Language Models(E5-Mistral)
- Springer et al. (2024), Repetition Improves Language Model Embeddings(回声嵌入)
- Hou et al. (2022), Towards Universal Sequence Representation Learning for Recommender Systems(UniSRec)
- LLM 排序:
- Sachan et al. (2022), Improving Passage Retrieval with Zero-Shot Question Generation(UPR)
- Sun et al. (2023), Is ChatGPT Good at Search? Investigating Large Language Models as Re-Ranking Agents(RankGPT)
- Tang et al. (2024), Found in the Middle: Permutation Self-Consistency Improves Listwise Ranking in Large Language Models
- 本仓库的相关内容:
- 第 2 讲(因果 Transformer)
- 第 5 讲(排序指标)
- 第 10 讲、第 11 讲(检索与稠密向量)
- 第 14 讲(探针)
- 第 21 讲(长上下文与”中间丢失”)
7.5 下一讲预告
这一讲的重排序器是用”用户真的买了什么”来训练的——有明确的答案。可是很多时候我们想让模型”更有用、更无害”,这些目标没法写成一个标准答案,只能由人来比较两个回答哪个更好。第 18 讲:基于人类反馈的强化学习(RLHF)。我们会在花园世界里定义一个真实奖励已知的”偏好”,然后:
- 训练奖励模型(Bradley–Terry,和本讲的成对比较是同一个模型);
- 用 REINFORCE + KL 惩罚优化策略,观察奖励被”钻空子”;
- 推导 DPO 怎样把强化学习变成一个分类问题。