CH 09 / SOURCE EXAMPLE
item_id_vs_semantic_id.py
ch09 生成式推荐模块/code/ch01_overview/item_id_vs_semantic_id.py
这个示例讲什么
│ └── item_id_vs_semantic_id.py Item ID vs 语义 ID:词表规模 / 输出层参数 / NTP 数据形态
对应正文第 9 章 §9.1「生成式推荐概述」: 为什么要先做语义离散化编码(Semantic Tokenization)。 正文要点: * 生成式推荐把推荐任务转化为序列生成任务: 给定用户历史行为序列、上下文与画像, 由模型直接生成用户未来最可能感兴趣的内容标识; * 但推荐系统的 Item 规模常达百万甚至亿级, 直接把 Item ID 当生成目标会导致词表 规模过大、训练与推理成本极高; * 因此主流做法是先把 Item 映射为一个或多个语义 ID: Item_A -> [s_1, s_2, s_3, s_4] 训练阶段把用户行为序列转成语义 ID 序列 [s_1, s_2, s_3] -> [s_4], 用 NTP 目标训练; * 推理阶段自回归生成 [s_1, s_2] -> [s_3] -> [s_4], 生成完整语义
上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。
源码结构与阅读入口
analytic_table· 第 36 行解析对比: 词表规模 N vs. L*K, 输出层参数 = 词表 × d, softmax FLOPs 同量级。ItemIdHead· 第 48 行[MOCK] 直接以 Item ID 为生成目标的输出头(教学规模: 只取 20 万物品)。SemanticIdHead· 第 59 行以语义 Token 为生成目标的输出头: 词表只有 L*K。bench· 第 70 行demo_sequence· 第 79 行构造 Item -> 语义 ID 的映射并展示 NTP 样本与自回归生成过程。main· 第 87 行
1r"""对应正文第 9 章 §9.1「生成式推荐概述」: 为什么要先做语义离散化编码(Semantic Tokenization)。23正文要点:4 * 生成式推荐把推荐任务转化为序列生成任务: 给定用户历史行为序列、上下文与画像,5 由模型直接生成用户未来最可能感兴趣的内容标识;6 * 但推荐系统的 Item 规模常达百万甚至亿级, 直接把 Item ID 当生成目标会导致词表7 规模过大、训练与推理成本极高;8 * 因此主流做法是先把 Item 映射为一个或多个语义 ID:9 Item_A -> [s_1, s_2, s_3, s_4]10 训练阶段把用户行为序列转成语义 ID 序列 [s_1, s_2, s_3] -> [s_4], 用 NTP 目标训练;11 * 推理阶段自回归生成 [s_1, s_2] -> [s_3] -> [s_4], 生成完整语义 ID 后再查12 「语义 ID -> Item ID」映射表完成检索;13 * 正文的 4 层 × 512 码字例子: 组合空间 512^4 ≈ 6.87e10, 足以覆盖极大规模物品集。1415本实现:16 1. 按正文公式逐项算出「Item ID 词表」与「语义 ID 词表」两种方案的词表规模、17 输出层参数量、softmax 浮点开销与组合空间, 打印对照表(纯解析计算, 无需训练);18 2. 用一个极小的真实模型实测两种输出头的参数量/前向耗时, 验证解析结论;19 3. 演示「行为序列 -> 语义 ID 序列 -> NTP 训练样本 -> 自回归生成」的完整数据形态,20 打印可读的 Token 流。21"""2223from __future__ import annotations2425import random26import time2728import torch29import torch.nn as nn3031D_MODEL = 64 # 演示用隐层维度(正文无具体取值)32L_LAYER = 4 # 正文例子: 4 层 codebook33K_CODE = 512 # 正文例子: 每层 512 个码字343536def analytic_table():37 """解析对比: 词表规模 N vs. L*K, 输出层参数 = 词表 × d, softmax FLOPs 同量级。"""38 rows = []39 for n_item in (1_000_000, 10_000_000, 100_000_000, 1_000_000_000):40 v_item = n_item # 每个 Item 一个 Token41 v_sid = L_LAYER * K_CODE # 所有层码字共享/拼接后的词表42 rows.append((n_item, v_item, v_sid,43 v_item * D_MODEL, v_sid * D_MODEL,44 v_item / v_sid))45 return rows464748class ItemIdHead(nn.Module):49 """[MOCK] 直接以 Item ID 为生成目标的输出头(教学规模: 只取 20 万物品)。"""5051 def __init__(self, n_item):52 super().__init__()53 self.proj = nn.Linear(D_MODEL, n_item, bias=False)5455 def forward(self, h):56 return self.proj(h)575859class SemanticIdHead(nn.Module):60 """以语义 Token 为生成目标的输出头: 词表只有 L*K。"""6162 def __init__(self):63 super().__init__()64 self.proj = nn.Linear(D_MODEL, L_LAYER * K_CODE, bias=False)6566 def forward(self, h):67 return self.proj(h)686970def bench(head, h, n_rep=20):71 with torch.no_grad():72 head(h) # warmup73 t0 = time.perf_counter()74 for _ in range(n_rep):75 head(h).log_softmax(-1)76 return (time.perf_counter() - t0) / n_rep * 1e3777879def demo_sequence(gen, n_item=40, hist=3):80 """构造 Item -> 语义 ID 的映射并展示 NTP 样本与自回归生成过程。"""81 sid = torch.stack([torch.randint(0, 8, (n_item,), generator=gen)82 for _ in range(L_LAYER)], 1) # 演示用小码本 K=883 seq = [int(torch.randint(0, n_item, (1,), generator=gen)) for _ in range(hist + 1)]84 return sid, seq858687def main() -> None:88 random.seed(42)89 torch.manual_seed(42)90 gen = torch.Generator().manual_seed(42)9192 print("=" * 96)93 print("表 1 Item ID 词表 vs. 语义 ID 词表(正文例子: L=4 层, 每层 K=512 码字, "94 f"d={D_MODEL})")95 print("-" * 96)96 print(f"{'物品规模 N':>14}{'Item ID 词表':>14}{'语义 Token 词表':>16}"97 f"{'输出层参数(Item ID)':>22}{'输出层参数(SID)':>18}{'缩减倍数':>10}")98 for n, vi, vs, pi, ps, ratio in analytic_table():99 print(f"{n:>14,}{vi:>14,}{vs:>16,}{pi / 1e6:>19.1f}M"100 f"{ps / 1e6:>16.3f}M{ratio:>10.0f}x")101 print("-" * 96)102 print(f"语义 ID 的可表示组合空间 = K^L = {K_CODE}^{L_LAYER} = "103 f"{K_CODE ** L_LAYER:.6g} ≈ {K_CODE ** L_LAYER / 1e10:.2f}e10"104 f"(正文: 512^4 ≈ 6.87e10), 远大于亿级物品集")105 print("代价: 生成一个物品需要 L 次自回归前向, 而 Item ID 方案只需 1 次 —— "106 "这是本章后续\n §9.7 讨论 MTP/DLM 等并行解码的根本原因。")107108 print()109 print("表 2 实测输出头开销(受内存限制, Item ID 方案只取 20 万物品)")110 print("-" * 96)111 n_small = 200_000112 h = torch.randn(32, D_MODEL)113 head_i, head_s = ItemIdHead(n_small), SemanticIdHead()114 pi = sum(p.numel() for p in head_i.parameters())115 ps = sum(p.numel() for p in head_s.parameters())116 ti, ts = bench(head_i, h), bench(head_s, h)117 print(f"{'输出头':<30}{'词表':>12}{'参数量':>14}{'单次前向+softmax(ms)':>24}")118 print(f"{'Item ID 直接生成':<30}{n_small:>12,}{pi:>14,}{ti:>24.3f}")119 print(f"{'语义 Token 生成':<30}{L_LAYER * K_CODE:>12,}{ps:>14,}{ts:>24.3f}")120 print("-" * 96)121 print(f"参数量比 = {pi / ps:.1f}x, 实测耗时比 = {ti / ts:.1f}x"122 f"(仅 20 万物品即已如此, 亿级物品下 Item ID 方案的输出层根本无法放入显存)")123124 print()125 print("表 3 数据形态: 行为序列 -> 语义 ID 序列 -> NTP 样本(演示用 L=4, K=8)")126 print("-" * 96)127 sid, seq = demo_sequence(gen)128 for i, it in enumerate(seq):129 code = [int(sid[it, l]) for l in range(L_LAYER)]130 tag = "历史" if i < len(seq) - 1 else "目标"131 print(f" {tag} Item_{it:<4} -> [" + ", ".join(f"s{l + 1}={c}"132 for l, c in enumerate(code))133 + "]")134 flat = []135 for it in seq[:-1]:136 flat += [f"({l + 1},{int(sid[it, l])})" for l in range(L_LAYER)]137 tgt = [f"({l + 1},{int(sid[seq[-1], l])})" for l in range(L_LAYER)]138 print()139 print(" NTP 输入(flatten 后的语义 Token 序列):")140 print(" " + " ".join(flat))141 print(" NTP 预测目标:")142 print(" " + " ".join(tgt))143 print()144 print(" 自回归生成过程(正文 [s1,s2] -> [s3] -> [s4]):")145 ctx = list(flat)146 for step, t in enumerate(tgt):147 print(f" 第 {step + 1} 次前向: 输入 ...{' '.join(ctx[-3:])} => 生成 {t}")148 ctx.append(t)149 print(f" 得到完整语义 ID {''.join(tgt)} 后, 查映射表恢复 Item_{seq[-1]}")150 print("-" * 96)151 print("这正是正文概述的闭环: 语义离散化 -> NTP 训练 -> 自回归生成 -> SID 查表检索。")152153154if __name__ == "__main__":155 main()如何用于学习或编写新代码
- 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
- 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
- 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
- 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。