生成式推荐(Generative Recommendation, GR)是近年来快速发展的一个推荐系统范式,其核心思路是:不再像传统方法那样从固定候选集做检索和排序,而是直接"生成"用户下一个可能交互的物品。
GR with Semantic IDs:为什么是它?
在所有 GR 方案中,基于语义 ID(Semantic ID, SID)的生成式推荐是当前最主流的范式,其核心动机在于弥合两种信号的鸿沟:
🔵 语义信息(Semantic Knowledge)
来自大型语言模型(LLM/VLM)对物品文本、图片等模态特征的理解,携带了开放世界的知识,对新品、长尾品友好。
🟣 协同信号(Collaborative Signal)
来自用户与物品的交互历史,两件物品如果常常出现在相似用户的购买序列中,它们就有较强的协同相似性。
SID 的思路是:用量化方法(如 RQ-VAE)把 LLM 生成的连续 embedding 压缩成一组离散 token 序列——即 SID。这样,两件语义相似的物品会共享 SID 前缀,序列推荐模型在预测 SID 时,既能学到语义先验,也能从历史序列中捕捉协同信号。
当前痛点
- 复现成本极高,需要深度技术积累 + 精细超参调优
- GR 管道本身涉及多个互相影响的因素(如训练策略、架构选型),debug 困难
- 设计选择的经验教训散落各处,从业者缺乏系统性认知,浪费大量资源
GRID 的贡献
- 1发布 GRID 开源框架:模块化实现了 GR with SID 管道中的所有核心组件(Semantic ID Tokenizer、Sequential Recommender 等),是第一个可复现现有文献结果的开源资源。
- 2系统性消融实验:使用 GRID 对 GR with SID 各组件做了全面实验,揭示了多个被现有文献忽视但对性能影响显著的设计选择。
GRID 将 GR with SID 分成两个清晰解耦的阶段:先 Tokenization(分词),再 Generation(生成),各阶段提供即插即用的组件,支持快速替换和实验。
形式化问题定义: 设用户集合为 $\mathcal{U}$,物品集合为 $\mathcal{I}$,每个物品 $i \in \mathcal{I}$ 有语义特征 $\mathbf{f}_i$(文本/图像等)。用户 $u$ 的交互序列为 $\mathcal{S}_u = [i^1_u, i^2_u, \cdots, i^{L_u}_u]$。GR 的目标是:给定序列 $\mathcal{S}_u$,生成用户下一步最可能交互的物品 $i^{L_u+1}_u$。
2.1 Tokenization 阶段:语义 ID 生成
Tokenization 阶段把物品的连续 embedding 转换为离散 SID 序列,分两步进行:
-
1Modality Encoder:连续 Embedding 提取
用预训练的模态编码器 $E(\cdot): \mathbf{f} \rightarrow \mathbb{R}^d$,将物品的文本/图像等特征转换为 $d$ 维 dense embedding $\mathbf{h}_i$。GRID 支持直接插入 HuggingFace 上的任意模型(如 Flan-T5-XL、CLIP 等)。 -
2Hierarchical Quantization Tokenizer:映射为 SID
用分层聚类 Tokenizer 把 $\mathbf{h}_i$ 映射为离散 SID 序列。GRID 支持三种算法:RK-Means、R-VQ、RQ-VAE。
- $\mathbf{h}_i \in \mathbb{R}^d$:物品 $i$ 的 dense embedding(由 LLM/VLM 编码器输出)
- $\text{SID}^l_i \in \{0, 1, \ldots, W\}$:第 $l$ 层量化得到的离散 token ID
- $W$:每层 codebook 的大小(即每层最多有多少个不同的 token)
- $L$:SID 的层数(Residual 深度)
三种 Tokenizer 算法对比
🔵 RK-Means(Residual Mini-Batch K-Means)
来自 One-Rec 论文。逐层做 K-Means 聚类:第 1 层对原始 $\mathbf{h}_i$ 聚类,每个物品的 SID 第 1 位是其所属簇的 ID;随后用残差向量继续对下一层聚类。无需额外神经网络,速度快。
🟣 R-VQ(Residual Vector Quantization)
每层学习一个 codebook(码本),物品 embedding 匹配最近的 codebook entry 作为该层 SID,余下残差进入下一层。与 RK-Means 类似,但 codebook 通过梯度更新学习。
🟢 RQ-VAE(Residual Quantization Variational Autoencoder)
文献中最常用的方案(TIGER 采用)。同时训练一个 Autoencoder(编码器+解码器)和分层量化模块。优点是重建质量好,缺点是需要同时优化 AE 损失 + 量化损失,训练更复杂、更容易坍塌(Codebook Collapse)。
假设 item "运动耳机 X" 的 Flan-T5-XL embedding 经 RK-Means 量化(L=3, W=256),得到 SID = [42, 7, 191]。
- 第 0 位(42):粗粒度类别,表示该物品属于第 42 个大类簇(比如"电子产品")
- 第 1 位(7):中粒度,该簇内的子类(比如"音频设备")
- 第 2 位(191):细粒度,具体款型(比如"运动蓝牙耳机")
→ 类似物品的 SID 会共享前缀(e.g., 同类音频设备都以 42 开头),这正是 GR 模型能从 SID 中学到语义先验的原因。
2.2 Generation 阶段:下一个 Item 生成
为每个物品生成 SID 后,对每个用户序列,GR 框架用序列模型来自回归地预测用户下一步最可能交互的物品的 SID。
GRID 支持以下生成模型配置:
- 架构:Encoder-Decoder(T5 风格)或 Decoder-only(GPT 风格)
- 训练目标:Next-token prediction(自回归预测 SID 的每一位)
- 数据增强:Sliding Window(滑动窗口),将用户长序列切分为所有连续子序列
- 推理:Beam Search(支持 KV-cache 加速),可选 Constrained(限制输出在合法 SID 集合内)或 Unconstrained(自由生成)
- 用户 Token:可选地在序列头部加入用户 Token(TIGER 设计)
- SID 去重:向 SID 末尾追加消歧位解决不同物品 SID 碰撞问题
用户历史购买序列:[运动鞋, 篮球, 护腕],对应 SID 为 [[12,3,1], [12,3,8], [12,1,5]]。
Encoder 输入: "[SID₁=12,3,1] [SID₂=12,3,8] [SID₃=12,1,5]"(拼接成 token 序列)
Decoder 输出(自回归预测): "12" → "12, 2" → "12, 2, 7"(逐位预测下一个物品的 SID)
→ 这类似于语言模型预测下一个 token,但预测的是 SID token 序列,天然支持 Beam Search 生成多个候选。
2.3 完整 Pipeline
- 1数据准备:整理用户交互序列 + 物品文本信息
- 2LLM Embedding:用 Flan-T5 等 LLM 的最后一层隐状态(mean pooling)生成物品 embedding
- 3SID 训练:在 embedding 上训练 RK-Means / R-VQ / RQ-VAE,学习分层 codebook;推理得到每个物品的 SID
- 4GR 模型训练:用物品 SID 序列训练 Transformer 模型(next-token prediction + sliding window augmentation)
- 5推理:给定用户历史 SID 序列,Beam Search 生成 Top-K 候选物品 SID,查表还原为物品 ID
3.1 Tokenizer 算法对比(Table 1)
文献中 RQ-VAE 是事实上的默认 Tokenizer(TIGER 论文起),但其训练复杂、容易坍塌。GRID 实验显示:
| Tokenizer | Beauty R@5 | R@10 | N@5 | N@10 | Toys R@5 | R@10 | N@5 | N@10 | Sports R@5 | R@10 | N@5 | N@10 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| RK-Means | 0.0422 | 0.0639 | 0.0277 | 0.0347 | 0.0376 | 0.0577 | 0.0243 | 0.0308 | 0.0236 | 0.0353 | 0.0153 | 0.0191 |
| R-VQ | 0.0422 | 0.0638 | 0.0282 | 0.0351 | 0.0327 | 0.0493 | 0.0209 | 0.0262 | 0.0234 | 0.0352 | 0.0151 | 0.0189 |
| RQ-VAE | 0.0404 | 0.0593 | 0.0268 | 0.0329 | 0.0342 | 0.0514 | 0.0224 | 0.0280 | 0.0205 | 0.0312 | 0.0132 | 0.0166 |
3.2 LLM 编码器规模影响(Table 2)
使用 Flan-T5 的三个规格(Large 780M → XL 3B → XXL 11B)提取 embedding:
| LM | Beauty R@5 | R@10 | N@5 | N@10 | Toys R@5 | R@10 | N@5 | N@10 | Sports R@5 | R@10 | N@5 | N@10 |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Large (780M) | 0.0429 | 0.0639 | 0.0285 | 0.0353 | 0.0373 | 0.0565 | 0.0237 | 0.0300 | 0.0224 | 0.0347 | 0.0145 | 0.0185 |
| XL (3B) | 0.0422 | 0.0639 | 0.0277 | 0.0347 | 0.0376 | 0.0577 | 0.0243 | 0.0308 | 0.0236 | 0.0353 | 0.0153 | 0.0191 |
| XXL (11B) | 0.0429 | 0.0646 | 0.0282 | 0.0352 | 0.0381 | 0.0586 | 0.0245 | 0.0311 | 0.0239 | 0.0363 | 0.0154 | 0.0194 |
Flan-T5-XXL 比 Large 拥有 14 倍参数,能更细致地理解物品描述(如区分"防水运动耳机"和"降噪无线耳机")。然而 SID 量化把这个 2048 维 embedding 压缩为 [L=3, W=256] 的三个离散数字,细粒度语义可能在压缩过程中大量丢失。
→ 这是整个 GR with SID 方向的一个系统性瓶颈:如何让 SID 更好地保留 LLM 的细粒度语义,仍是开放问题。
3.3 Codebook 维度消融(Table 3,Beauty 数据集)
在 RK-Means 上系统地改变层数 $L$ 和每层宽度 $W$:
| L × W | Recall@5 | Recall@10 | NDCG@5 | NDCG@10 | 备注 |
|---|---|---|---|---|---|
| 3 × 128 | 0.0412 | 0.0617 | 0.0273 | 0.0339 | 宽度偏小 |
| 3 × 256 | 0.0422 | 0.0639 | 0.0277 | 0.0347 | ✅ 最优(默认) |
| 3 × 512 | 0.0415 | 0.0631 | 0.0273 | 0.0342 | 宽度过大反而略降 |
| 2 × 256 | 0.0403 | 0.0618 | 0.0264 | 0.0333 | 层数不足 |
| 4 × 256 | 0.0405 | 0.0609 | 0.0265 | 0.0331 | 层数过多反而下降 |
| 5 × 256 | 0.0396 | 0.0596 | 0.0257 | 0.0321 | 层数太多明显下降 |
固定 SID Tokenizer 为 RK-Means (L=3, W=256, Flan-T5-XL),对生成模型的 5 个设计选择进行系统消融。
4.1 用户 Token 数量(Table 4,Beauty 数据集)
TIGER 原文在每个用户序列头部加入用户 Token(通过随机 hash 映射到固定词表),以期实现个性化。GRID 系统地测试了不同词表大小:
| User Token 词表 | Recall@5 | Recall@10 | NDCG@5 | NDCG@10 |
|---|---|---|---|---|
| 0(不使用) | 0.0408 | 0.0618 | 0.0270 | 0.0330 |
| 2,000 | 0.0396 | 0.0597 | 0.0264 | 0.0328 |
| 4,000 | 0.0401 | 0.0612 | 0.0264 | 0.0332 |
| 6,000 | 0.0401 | 0.0611 | 0.0264 | 0.0331 |
| 8,000 | 0.0405 | 0.0610 | 0.0269 | 0.0335 |
4.2 Encoder-Decoder vs. Decoder-only(Table 5)
大多数 GR with SID 论文默认使用 Encoder-Decoder(如 T5),但 Decoder-only(如 GPT)架构在 LLM 领域更为主流:
| 架构 | Beauty R@5 | R@10 | N@5 | N@10 | Toys R@5 | Sports R@5 |
|---|---|---|---|---|---|---|
| Enc-Dec | 0.0396 | 0.0597 | 0.0264 | 0.0328 | 0.0357 | 0.0192 |
| Dec-only | 0.0300 | 0.0438 | 0.0206 | 0.0251 | 0.0286 | 0.0152 |
用户历史序列 [运动耳机, 蓝牙音箱, 耳机充电盒, 护耳套],预测下一个物品。
Enc-Dec: Encoder 用双向注意力一次看完全部 4 个历史 item,捕捉"这个用户喜欢音频类配件"的全局语义;Decoder 基于这个全局表示自回归生成下一个 SID。
Dec-only: 每个位置只能看到它之前的 token,"耳机充电盒"位置看不到后来的"护耳套",全局历史建模能力较弱。
→ 这与 NLP 中 Decoder-only 统治地位相悖,说明推荐任务的特殊性:历史序列是已知完整输入,双向建模比因果建模更合适。
4.3 数据增强:滑动窗口(Table 6)
| 增强方式 | Beauty R@5 | R@10 | N@5 | N@10 | Toys R@5 | Sports R@5 |
|---|---|---|---|---|---|---|
| 滑动窗口 | 0.0396 | 0.0597 | 0.0264 | 0.0328 | 0.0357 | 0.0192 |
| 无增强 | 0.0279 | 0.0447 | 0.0171 | 0.0226 | 0.0277 | 0.0174 |
一个用户有 5 条历史交互 $[A, B, C, D, E]$,不增强时只有 1 条训练样本:输入 [A,B,C,D] → 预测 E。
滑动窗口增强后,变成 4 条训练样本:
- 输入 [A] → 预测 B
- 输入 [A,B] → 预测 C
- 输入 [A,B,C] → 预测 D
- 输入 [A,B,C,D] → 预测 E
→ 数据量 4 倍增加,模型见到了用户在不同历史长度下的偏好,泛化能力显著提升。
4.4 SID 去重策略(Table 7)
量化不可避免会产生碰撞——不同物品可能被映射到相同的 SID,导致 Beam Search 生成 SID 后无法唯一确定物品。
TIGER 去重(With De-dup)
在 SID 末尾追加一个消歧位(基于全局 SID 分布决定),将碰撞的物品区分开。代价:序列长度+1,解码复杂度上升,且需要全局 SID 分布知识(大规模场景不实用)。
随机选择(No De-dup)
SID 碰撞时随机选择其中一个物品。更简单,无需全局知识。
4.5 Constrained vs. Unconstrained Beam Search(Table 8)
| 策略 | Beauty R@5 | R@10 | N@5 | N@10 | Toys R@5 | Sports R@5 | 效率 |
|---|---|---|---|---|---|---|---|
| Constrained | 0.0396 | 0.0597 | 0.0264 | 0.0328 | 0.0357 | 0.0192 | 低 |
| Free-form (Unconstrained) | 0.0405 | 0.0609 | 0.0268 | 0.0334 | 0.0356 | 0.0198 | 高 |
Unconstrained(Free-form):每步自由生成,生成完再查表,不存在就丢弃 Constrained:每步生成前先过滤,只允许能对应真实 item 的 token 参与竞争 _check_valid_prefix 实现(代码里是暴力广播,不是 Trie) self.codebooks 矩阵 shape: (N_items, num_hierarchies),常驻显存(~12MB) 每步对所有候选前缀做广播对比: [N_items, 1, h] == [1, candidates, h] → [N_items, candidates, h] .all(dim=2).any(dim=0) → [candidates] True = 至少有一个 item 匹配 代码注释自注:TODO 应改用排序+前缀树,当前为临时方案 → Constrained 推理开销大的根本原因在于此
GRID 的系统实验揭示了多个被现有文献忽视的重要发现:
RQ-VAE 不是最佳 Tokenizer
文献中几乎默认使用 RQ-VAE,但更简单的 RK-Means 表现更好,且训练迭代数只有前者的 1/5。
更大 LLM 不能显著提升推荐
参数量增大 14 倍(780M → 11B),推荐 Recall 几乎无变化,说明现有 SID 量化流程是知识瓶颈。
User Token 设计未能个性化
TIGER 风格的用户 Token 反而略微降低性能,去掉 User Token 效果最好。
更多 SID 层数不等于更好性能
L=3 是最优,L=5 时性能明显下降。序列可学习性与语义信息量的 trade-off 至关重要。
Encoder-Decoder 显著优于 Decoder-only
双向 Encoder 对用户历史的全局建模是 GR 推荐任务成功的关键,与 LLM 领域趋势相悖。
数据增强(滑动窗口)至关重要
移除滑动窗口增强后 Recall 下降约 29%,是影响性能最大的单一因素之一。
Free-form Beam Search 效率更高
Unconstrained 与 Constrained 性能持平,但计算成本低得多,工程实践推荐使用 Free-form。
SID 去重策略影响有限
TIGER 去重略好,但工程代价不值。随机选择对大规模场景更实用。
TIGER(NeurIPS 2023)
首个将 Transformer + RQ-VAE SID 用于序列推荐的工作,引入用户 Token 和 Constrained Beam Search,是 GR with SID 领域的奠基之作。
One-Rec(2025)
提出 Residual K-Means(RK-Means)作为更简单高效的 SID Tokenizer,并结合 RL 做偏好对齐。GRID 实验证明其 Tokenizer 优于 RQ-VAE。
RQ-VAE 原文(CVPR 2022)
最初用于图像生成的自回归建模(如 RQ-Transformer),被 TIGER 引入推荐系统。
P5(RecSys 2022)
将推荐视为语言处理任务的统一预训练框架,GRID 使用其预处理的 Amazon Beauty/Sports/Toys 数据集。
论文:Generative Recommendation with Semantic IDs: A Practitioner's Handbook
链接:https://arxiv.org/abs/2507.22224
代码:https://github.com/snap-research/GRID
收录:CIKM 2025(The 34th ACM International Conference on Information and Knowledge Management)
"我们发现,许多被假设为至关重要的组件——且往往计算或工程成本高昂——实际上可以用更高效的替代方案取代而不损失性能。相反,一些通常被忽视的设计选择(如 Encoder-Decoder 架构和数据增强)被证明是至关重要的。"
— GRID 论文结论
GRID 以 PyTorch Lightning + Hydra 为框架基础,实现了完整的 GR with SID 管道。本节从项目结构、核心模块到关键逻辑,用结构图的方式逐层解析——不贴原始代码,只看骨架与设计。
7.1 项目结构
configs/(Hydra 声明式配置)+ src/(核心实现)+ 入口脚本,各层完全解耦,切换实验无需修改代码。
GRID/ ├── src/train.py ─── ① 训练入口(Hydra main) ├── src/inference.py ─── ② 推理入口(Hydra main) │ ├── src/modules/ 顶层 LightningModule │ ├── clustering/ │ │ ├── residual_quantization.py ─── ③ ResidualQuantization(RQ 总控) │ │ └── vector_quantization.py ─── ④ VQ 单层(RQ-VAE 用) │ └── semantic_embedding_inference_module.py ─── ⑤ LLM Embedding 推理 │ ├── src/models/modules/ │ ├── clustering/ │ │ └── mini_batch_kmeans.py ─── ⑥ MiniBatchKMeans(RK-Means 单层) │ └── semantic_id/ │ └── tiger_generation_model.py ─── ⑦ TIGER Enc-Dec 生成推荐模型 │ ├── src/components/ 可插拔组件(函数级) │ ├── distance_functions.py 距离度量(欧氏等) │ ├── clustering_initializers.py K-Means++ 初始化 │ ├── quantization_strategies.py ─── ⑧ 量化策略(STE / Gumbel / Rotation) │ ├── loss_functions.py WeightedSquaredError 等 │ └── eval_metrics.py Recall / NDCG │ ├── src/data/loading/ │ ├── components/ │ │ ├── pre_processing.py ─── ⑨ 预处理函数链 │ │ ├── collate_functions.py 滑动窗口增强在此 │ │ └── label_function.py NextKTokenMasking │ └── datamodules/ │ └── sequence_datamodule.py LightningDataModule 封装 │ ├── src/utils/ │ └── launcher_utils.py ─── ⑩ PipelineModules 上下文管理器 │ └── configs/experiment/ Hydra 声明式配置(无代码) ├── sem_embeds_inference_flat.yaml Step1: LLM Embedding ├── rkmeans_train_flat.yaml Step2: RK-Means 训练 ├── rkmeans_inference_flat.yaml Step2: 生成 SID ├── rqvae_train_flat.yaml Step2: RQ-VAE 训练 ├── tiger_train_flat.yaml Step3: GR 模型训练 └── tiger_inference_flat.yaml Step4: 推荐推理
7.2 整体 Pipeline 数据流
╔══ Step 1 LLM Embedding 生成 ══════════════════════════════════╗ ║ ║ ║ items/ (TFRecord) ║ ║ │ TFRecordIterator → 预处理函数链(4步) ║ ║ ▼ ║ ║ SemanticEmbeddingInferenceModule ║ ║ │ frozen HuggingFace LLM → mean pooling ║ ║ ▼ ║ ║ merged_predictions.pt → { item_id : embedding[d] } ║ ╚════════════════════════════════════════════════════════════════╝ ▼ ╔══ Step 2 SID Tokenizer 训练(以 RK-Means 为例)══════════════╗ ║ ║ ║ ResidualQuantization ← n_layers=3 train_layer_wise=True ║ ║ ║ ║ Layer 0 ──► L2-Norm(embedding) → MiniBatchKMeans ║ ║ assign SID₀ → residual₁ = emb - c₀ ║ ║ Layer 1 ──► L2-Norm(residual₁) → MiniBatchKMeans ║ ║ assign SID₁ → residual₂ = r₁ - c₁ ║ ║ Layer 2 ──► L2-Norm(residual₂) → MiniBatchKMeans ║ ║ assign SID₂ → residual₃(最终误差) ║ ║ ║ ║ 输出:checkpoint + { item_id : [SID₀, SID₁, SID₂] } ║ ╚════════════════════════════════════════════════════════════════╝ ▼ ╔══ Step 3 GR 模型训练(TIGER Enc-Dec)══════════════════════════╗ ║ ║ ║ 用户历史序列 [i₁, i₂, …, iₙ] → 滑动窗口增强 ║ ║ → 展开为所有前缀子序列(数据量 ~N 倍) ║ ║ ║ ║ 输入 SID Token 序列(已 Offset) ║ ║ [SID₀, SID₁, SID₂, SEP, SID₀', SID₁', SID₂', SEP, …] ║ ║ ▼ ║ ║ T5 Encoder(双向注意力)→ encoder_output ║ ║ ▼ ║ ║ T5 Decoder(因果)+ bos_token 起始 ║ ║ │ for h in [0,1,2]: ║ ║ │ logits[h] = decoder_mlp[h](output[:, h]) ║ ║ │ loss[h] = CrossEntropy(logits[h], SID_target[h])║ ║ │ total_loss = Σ loss[h] ║ ║ ║ ║ 输出:GR 模型 checkpoint ║ ╚════════════════════════════════════════════════════════════════╝ ▼ ╔══ Step 4 推荐推理(Beam Search)══════════════════════════════╗ ║ ║ ║ Encoder 只算一次 → encoder_output(可复用) ║ ║ ║ ║ for h = 0 → num_hierarchies-1: ║ ║ Decoder(+KV Cache)→ logits[h] (batch×top_k, W) ║ ║ beam_search_one_step: ║ ║ 概率累乘 → topK 胜出 beam ║ ║ reorder_cache(winning_idx) ←── KV Cache 同步 ║ ║ ║ ║ generated_ids: (batch, top_k, num_hierarchies) ║ ║ → 查 SID→item_id 映射表 → 推荐列表 ║ ╚════════════════════════════════════════════════════════════════╝
7.3 & 7.4 SID Tokenizer:前向流程与三种量化方案
文件:residual_quantization.py(总控)+ mini_batch_kmeans.py / vector_quantization.py(单层)
ResidualQuantization 是统一的多层量化外壳,通过 quantization_layer 插槽切换内层算法。三种 SID Tokenizer(RK-Means / R-VQ / RQ-VAE)共享相同的残差叠加逻辑,只有单层的 Loss 计算和梯度路径不同。
输入:item embedding h ∈ ℝᵈ (来自冻结 LLM 的语义向量) 可选:encoder(h) → z_e (RQ-VAE 先压缩到隐空间,RK-Means/R-VQ 直接用 h) residual = h (或 z_e) cluster_ids = [] quantized_sum = 0 for l = 0, 1, …, L-1: │ ├─ [若 normalize_residuals=True] │ residual = L2_norm(residual) │ 防止残差模长随层数衰减(RK-Means 默认开启) │ ├─ q, SID_l = quantize_layer[l](residual) │ ↑ 单层量化:找最近 codebook 向量 q,记录 ID(具体见下) │ ├─ cluster_ids.append(SID_l) ├─ quantized_sum += q └─ residual = residual - q ← 关键:下一层的输入 = 当前层未被捕捉的部分 输出: cluster_ids (batch, L) ← 每层 SID,拼成 item 的 Semantic ID quantized_sum (batch, d) ← 各层量化向量之和(重建向量) loss ← 累加各层 loss(见下方各方案)
h(原始 embedding,2048维) │ Layer 0 L2-Norm(h) → argmin dist → centroid c₀[42] SID₀=42 residual₁ = h_norm - c₀[42] ← 粗粒度类别信息留在这里 │ Layer 1 L2-Norm(r₁) → argmin dist → centroid c₁[7] SID₁=7 residual₂ = r₁_norm - c₁[7] ← 中粒度差异信息 │ Layer 2 L2-Norm(r₂) → argmin dist → centroid c₂[191] SID₂=191 最终 SID = [42, 7, 191] 重建 = c₀[42] + c₁[7] + c₂[191]
三种量化方案:单层 quantize_layer 的 Loss 与训练方式
Centroid 初始化(训练开始前的 "预热" 阶段) centroids 初始值:zeros(n_clusters, d) ← 全零占位,尚未有意义 is_initialized = False Phase 1 Buffer 积累(前 init_buffer_size=1000 个样本) ┌─────────────────────────────────────────────────────────┐ │ 每个 mini-batch 进来 → 追加到 init_buffer │ │ 同时返回 dummy loss = MSE(centroids, 0) │ │ → SGD 把全零 centroids 继续拉向 0,保持占位 │ │ → 这些 batch 的 SID 分配是假的,不影响后续训练 │ └─────────────────────────────────────────────────────────┘ Phase 2 一次性 K-Means++ 初始化(buffer 满后触发,仅 rank-0 执行) ┌─────────────────────────────────────────────────────────┐ │ KMeansPlusPlusInitInitializer.forward(init_buffer) │ │ │ │ Step 0:随机选 1 个点作为 centroid[0] │ │ Step k:for k = 1 … K-1: │ │ min_dist[i] = min‖x_i - centroid[0..k-1]‖² │ │ p[i] = min_dist[i] / Σ min_dist │ │ centroid[k] = sample(buffer, p) ← 距现有中心越远越可能被选 │ │ │ │ 效果:初始 centroid 分散覆盖数据空间,避免随机初始化的 │ │ 多个中心扎堆、死区中心等问题 │ └─────────────────────────────────────────────────────────┘ Phase 3 "热身" step(is_initial_step=True) loss = MSE(centroids_zero, init_centroids) → SGD 把 centroids 从全零一步移动到 K-Means++ 结果 → 完成后 is_initialized=True,进入正常 mini-batch 更新 正常训练阶段(单层前向) assignments = argmin‖residual - centroid[k]‖² ← argmin 不可微 q = centroid[assignments] Loss(WeightedSquaredError) batch_mean[k] = mean(residual 中分配到 cluster k 的样本) weight[k] = batch_count[k] / global_count[k] ← 自适应权重 L_kmeans = Σₖ weight[k] · ‖centroid[k] - batch_mean[k]‖² 等价于:centroid ← centroid × (1 - w) + batch_mean × w (Sculley 2010 在线 K-Means,weight 随全局计数自动衰减) 反向 / 训练方式 只更新 centroid(即 Embedding Table 参数),无 encoder 参数 梯度通过 WeightedSquaredError 直接作用于 centroids ✓ train_layer_wise=True:逐层训练,等分 total_steps,第 l 段只训练第 l 层 ✗ encoder / decoder:均为 Identity(直接用 LLM embedding,不压缩)
整体结构:同样是多层残差(与 RK-Means 一样),每层量化该层的残差,不是原始 h residual₀ = h for l = 0..L-1: q_l = codebook_l[argmin‖residual_l - codebook_l[k]‖²] loss_l = BetaQuantizationLoss(residual_l, q_l) ← 与残差比,不是与 h 比! residual_{l+1} = residual_l - q_l BetaQuantizationLoss(每层的 loss) loss_function(batch=residual_l, embeddings=q_l): L_vq = MSE(residual_l.detach(), q_l) ← codebook 向该层残差靠拢 L_commit = MSE(residual_l, q_l.detach()) ← 残差向 codebook 靠拢 loss_l = L_vq + β · L_commit ← β=0.25 注:h 是冻结的 LLM embedding,L_commit 的梯度虽然流向 residual_l, 但由于没有 encoder,最终无可训练参数可更新,L_commit 名存实亡 与 RK-Means 的本质区别 RK-Means:WeightedSquaredError(centroid, batch_mean, weight) → centroid 做加权平均移动(在线 K-Means 等价于 SGD lr=0.5) R-VQ: BetaQuantizationLoss(residual_l, q_l) → codebook 用标准 optimizer(AdamW)更新,无在线权重衰减机制 → 理论上等价,但优化器行为不同 配置关键字段(rvq_train_flat.yaml) compute_reconstruction_loss_embeddings: false ← STE 梯度穿透功能未激活 optimizer: AdamW (lr=0.001) ← codebook 参数走标准反向传播 train_layer_wise: True ← 逐层训练(与 RK-Means 相同) ✗ encoder / decoder:均默认 Identity(无压缩)
整体结构 h ──► encoder MLP(只跑一次) ──► z_e = r₀ │ ┌─────────────── 残差量化(L 层,在 z_e 空间内进行)───┐ │ Layer 0: q₀ = argmin‖r₀ - codebook₀‖² │ │ r₁ = r₀ - q₀ │ │ Layer 1: q₁ = argmin‖r₁ - codebook₁‖² │ │ r₂ = r₁ - q₁ ... │ └──────────────────────────────────────────────────────┘ │ z_q = q₀ + q₁ + … + q_{L-1} ← 各层 codebook 向量之和 │ decoder MLP(只跑一次) ──► ĥ(重建回 h 的维度) ※ encoder 和 decoder 各只跑一次,残差 r₀→r₁→…→r_L 全程在 z_e 空间传递 Loss(三项之和) L_recon = ‖h - ĥ‖² ↑ 重建 Loss,h 是原始 LLM embedding,ĥ 是 decoder(z_q) 输出 L_commit = Σₗ ‖r_l - q_l.detach()‖² ↑ 每层:该层残差 r_l 向 codebook 靠拢(r_l 有梯度,q_l 冻结) ↑ r_l 在 z_e 空间,不是 h 空间;encoder 只跑一次,不是每层跑 L_vq = Σₗ ‖r_l.detach() - q_l‖² ↑ 每层:codebook 向该层残差 r_l 靠拢(q_l 有梯度,r_l 冻结) Total = λ_recon · L_recon + λ_commit · L_commit + λ_vq · L_vq 量化策略(STE / Gumbel / Rotation,RQ-VAE 专属) 作用:让 L_recon 的梯度能通过量化步骤(argmin 不可微)流回 encoder ┌── STE:x_out = r_l + (q_l - r_l).detach() 前向=q_l,反向梯度直通 r_l→encoder ├── Gumbel-Softmax:软分配 w·codebook,完全可微,梯度自然流向 r_l→encoder └── Rotation Trick:旋转矩阵 R·r_l = q_l,梯度方差最小 反向传播路径 L_recon → ĥ → decoder → z_q → [STE 等] → r₀=z_e → encoder → h(的 encoder 参数) L_commit → r_l → encoder(通过多层残差链往回传) L_vq → q_l(codebook 参数本身)
| 维度 | RK-Means | R-VQ | RQ-VAE |
|---|---|---|---|
| 单层量化 | MiniBatchKMeans(argmin) | VectorQuantization(argmin + 量化策略) | VectorQuantization(argmin + 量化策略) |
| encoder | Identity(直接用 h) | 无(直接用冻结 h) | MLP(h → z_e,降维) |
| decoder | 无 | 无 | MLP(z_q → ĥ,重建) |
| Loss | WeightedSquaredError(centroid 移动) | BetaQuantizationLoss(r_l, q_l) = MSE(r_l.detach(), q_l) + β·MSE(r_l, q_l.detach()),逐层对残差做 | L_recon + L_commit + L_vq(三项均有效) |
| 梯度到 codebook | 直接(SGD lr=0.5 on WeightedSquaredError) | autograd(AdamW,通过 L_vq) | autograd(通过 L_vq) |
| 梯度到 encoder | 无(encoder=Identity) | 无(h 冻结;STE 的 compute_recon_emb=false) | STE(compute_recon_emb=true)+ L_recon |
| 训练策略 | 逐层(train_layer_wise=True) | 逐层(train_layer_wise=True) | 各层同时(train_layer_wise=False) |
| 论文效果 | 最优(简单有效) | 次优 | 略差(重建目标与推荐目标不完全对齐) |
7.5 SemanticIDEncoderDecoder:TIGER 生成模型
文件:src/models/modules/semantic_id/tiger_generation_model.py
SemanticIDEncoderDecoder (TransformerBaseModule) │ ├── 关键属性 │ ├── item_sid_embedding_table_encoder ← 单张 Embedding Table │ │ 大小 = num_hierarchies × codebook_size(e.g. 3×256=768) │ │ 不同层级 SID 通过 offset 区分: │ │ SID₀=42 → 查[42] SID₁=7 → 查[7+256=263] SID₂=191 → 查[703] │ │ │ ├── sep_token ← 可学习分隔 token(每 item 后插入) │ ├── user_embedding ← 可选 User Token(消融证明去掉更好) │ ├── encoder (T5EncoderModel, 4层) ← 双向注意力 │ └── decoder (T5Stack, 4层, is_decoder=True) │ ├── bos_token ← 可学习起始 token │ └── decoder_mlp[0..L-1] ← 每层 hierarchy 独立线性分类头 │ ├── encoder_forward_pass(history_sids) │ │ │ ├── Step 1 _add_repeating_offset_to_rows ← SID offset 变换 │ ├── Step 2 item_sid_embedding_table_encoder ← 查 Embedding Table │ ├── Step 3 _inject_sep_token_between_sids ← 插入 SEP token │ ├── Step 4 [可选] 拼接 user_id embedding 在序列头部 │ └── Step 5 T5 Encoder(双向 Self-Attn) ← encoder_output │ ├── model_step (训练) │ │ │ ├── encoder_forward_pass → encoder_output │ ├── decoder_forward_pass (teacher forcing,use_cache=False) │ │ 输入: [bos, SID₀_target, SID₁_target, SID₂_target] │ │ 输出: decoder_output[:, :-1](移除末位,共 L 个时间步) │ │ │ └── for h in 0..L-1: │ logits = decoder_mlp[h](decoder_output[:, h]) │ loss += CrossEntropy(logits, fut_ids[:, h]) │ └── generate (推理 Beam Search) └── 见 7.6
用户历史:[item_A, item_B, item_C] ↓ 映射 SID(每 item 3 token)+ 插入 SEP Encoder 输入序列: ┌──────┬──────┬──────┬─────┬──────┬──────┬──────┬─────┬──────┬──────┬──────┬─────┐ │ SID₀ │ SID₁ │ SID₂ │ SEP │ SID₀ │ SID₁ │ SID₂ │ SEP │ SID₀ │ SID₁ │ SID₂ │ SEP │ │ A层0 │ A层1│ A层2│ │ B层0 │ B层1│ B层2│ │ C层0 │ C层1│ C层2│ │ └──────┴──────┴──────┴─────┴──────┴──────┴──────┴─────┴──────┴──────┴──────┴─────┘ 所有 token 均已做 offset,通过同一张 Embedding Table 查询
── 训练时完整数据流 ────────────────────────────────────────────────── 【输入数据】 history_sids : [42, 7,191, 88, 3, 44, 15,91, 6] ← 历史 3 个 item 已展开的 SID fut_ids : [33, 12, 76 ] ← 目标 item_D 的 SID(3 层) └─ SID₀=33 ─┘ └─ SID₁=12 ─┘ └─ SID₂=76 ─┘ ── encoder_forward_pass(history_sids) ─────────────────────── history_sids ──► Embedding + SEP 插入 ──► T5 Encoder(双向 Self-Attn) │ encoder_output shape (batch, enc_len, 128) │ ┌────────────────────────────┘ cross-attention(每个 Decoder 层) ▼ ── decoder_forward_pass(teacher forcing) ──────────────────── Decoder 输入(已知的 ground truth 前缀): [bos_token, SID₀_D=33, SID₁_D=12] ↑ Teacher Forcing:每步喂真实标签,而非上一步预测值 Decoder 内部每层: Self-Attn → 看已生成的 token(因果掩码) Cross-Attn → attend 到 encoder_output(参考用户历史) decoder_output[:, :-1] 共 L=3 个时间步 时间步 0 ──► mlp[0] ──► logits ──► 预测 SID₀_D = 33 (vs fut_ids[:, 0]) 时间步 1 ──► mlp[1] ──► logits ──► 预测 SID₁_D = 12 (vs fut_ids[:, 1]) 时间步 2 ──► mlp[2] ──► logits ──► 预测 SID₂_D = 76 (vs fut_ids[:, 2]) Total Loss = CE(mlp[0](out[:,0]), fut_ids[:,0]) + CE(mlp[1](out[:,1]), fut_ids[:,1]) + CE(mlp[2](out[:,2]), fut_ids[:,2])
fut_ids 来自 NextKTokenMasking:将输入序列最后 num_hierarchies 个 token(即目标 item 的完整 SID)挖出,作为 Decoder 的预测目标。每个 hierarchy 有独立分类头 decoder_mlp[h],因为不同层捕获的语义粒度完全不同(第 0 层粗粒度类别,第 2 层细粒度残差),共用分类头会相互干扰。
7.6 Beam Search 推理实现
Encoder 前向(只算一次) encoder_output ←──────────────────────────────── 固定不变,复用给每个 beam │ ▼ hierarchy = 0 (生成第一个 SID digit) ├── Decoder([bos_token]) → logits shape (batch, W) ├── softmax + Top-K → beam_ids = (batch, K, 1) │ ↑ 同时重置 KV Cache(第 0 步无合法 cache) │ hierarchy = 1 (生成第二个 SID digit) ├── beam 扩展:(batch, K) → (batch×K,) ← repeat_interleave ├── encoder_output 复制 K 份 ├── Decoder(generated_ids[:, :1], kv_cache) → logits (batch×K, W) ├── 概率累乘 → batch 内共 K×W 个候选 ├── Top-K 选出胜者 → winning_beam_idx ├── kv_cache.reorder_cache(winning_beam_idx) ← 关键!保持 Cache 与 beam 同步 └── beam_ids = (batch, K, 2) │ hierarchy = 2 (生成第三个 SID digit,同上) │ ▼ 输出 generated_ids: (batch, top_k, 3) → 查 SID→item_id 映射表 → 最终推荐列表 Top-K
winning_beam_idx 重新排列,确保后续 Decoder step 的 Q·K 计算基于正确的历史上下文should_check_prefix=False 时跳过前缀合法性检查(_check_valid_prefix),推理更快,论文实验证明性能持平encoder_output(cross-attention),大幅降低计算量7.7 数据处理管线
Step A 预处理函数链(在 DataModule 的 per-row 阶段串行执行) 原始 TFRecord row │ ├─ filter_features_to_consider 只保留 config 指定的字段 ├─ convert_to_dense_numpy_array sparse tensor → dense numpy ├─ convert_fields_to_tensors numpy → torch.Tensor(按 dtype 映射) └─ map_sparse_id_to_semantic_id ← 关键:item_id 序列 → SID 序列 item_id = [A, B, C] id_map shape (L=3, N_items) ──────────────────────────────────── id_map.t()[item_id] → (3, 3) ← 每 item 3 个 SID digit .view(-1) → [SID₀ₐ, SID₁ₐ, SID₂ₐ, SID₀ᵦ, SID₁ᵦ, SID₂ᵦ, …] ──────────────────────────────────── 展开为交织形式:同一 item 的 L 个 token 连续排列 ────────────────────────────────────────────────── Step B 滑动窗口增强(在 collate_fn 阶段,per-batch) 函数:collate_with_sid_causal_duplicate 配置:max_batch_size = 512 单条序列 [i₁, i₂, i₃, i₄, i₅] ↓ 生成所有"前缀→下一个 item"样本 ┌──────────────────┬──────────┐ │ 输入(历史) │ 标签 │ ├──────────────────┼──────────┤ │ [i₁] │ i₂ │ │ [i₁, i₂] │ i₃ │ │ [i₁, i₂, i₃] │ i₄ │ │ [i₁, i₂, i₃,i₄]│ i₅ │ └──────────────────┴──────────┘ 数据量 ≈ 4× 增加 → Recall@5 提升约 29%(Table 6) ────────────────────────────────────────────────── Step C NextKTokenMasking(label 提取) 输入序列(SID 展开后): [SID₀ᵢ₁, SID₁ᵢ₁, SID₂ᵢ₁, …, SID₀ᵢₙ, SID₁ᵢₙ, SID₂ᵢₙ] └──────── 最后 L=3 个 token ──────┘ ↑ 挖出作为 label,原位置填 masking_token=-1
7.8 Hydra 配置:实验切换设计
GRID 所有组件均通过 Hydra YAML 的 _target_ 字段声明,无需修改 Python 代码即可切换 Tokenizer、优化器、评估指标等。
model:
_target_: src.modules.clustering.residual_quantization.ResidualQuantization
n_layers: 3
normalize_residuals: true
train_layer_wise: true
quantization_layer: ← 切换此处选择 Tokenizer
┌─ RK-Means(论文最优)
│ _target_: ...mini_batch_kmeans.MiniBatchKMeans
│ n_clusters: 256
│
├─ R-VQ(向量量化)
│ _target_: ...vector_quantization.VectorQuantization
│ quantization_strategy:
│ _target_: ...STEQuantization ← 或 Gumbel / Rotation
│
└─ RQ-VAE(编解码器路径)
_target_: ...VectorQuantization ← 同上,但激活 encoder/decoder
另需在 ResidualQuantization 中配置:
encoder: MLP(d_in → d_latent)
decoder: MLP(d_latent → d_in)
reconstruction_loss_function: MSELoss
7.9 三种量化策略对比
| 策略 | 梯度传播原理 | 使用场景 | 特点 |
|---|---|---|---|
| STEQuantization | 前向取量化值 q,反向梯度直通 z_ex_q = z_e + (q − z_e).detach() |
RQ-VAE encoder 训练 | 实现最简单,梯度近似误差较大 |
| GumbelSoftmaxQuantization | 软分配 w = Gumbel_softmax(−dist/τ)量化 embedding = w @ codebook(可微) |
RQ-VAE(可微分路径) | 温度 τ 控制软硬程度,训练更平稳 |
| RotationTrickQuantization | 等距旋转变换 batch → q,梯度通过旋转矩阵流回 encoder |
RQ-VAE(最新方案) | 梯度方差小,优于 STE,实现复杂 |