CH 09 / SOURCE EXAMPLE

gen_rec_pipeline.py

ch09 生成式推荐模块/code/ch02_architecture/gen_rec_pipeline.py

这个示例讲什么

│ └── gen_rec_pipeline.py 生成式推荐链路端到端仿真(§9.2 五阶段 + 两种部署方式)

对应正文第 9 章 §9.2「生成式推荐架构」中生成式推荐模块的内部结构。 正文把生成式推荐模块拆成三个核心组成部分(图「生成式推荐模块内部结构」): 语义ID生成(Semantic ID Construction) -> 生成式推荐模型(Generative Recommendation Model) -> 物品ID检索(Item Retrieval) 而在整条链路上,正文还强调「大多数工业系统仍然会保留重排(Re-ranking)模块作为最后一道 决策环节」,因为生成式模型难以兼顾广告收益、多样性约束、生态治理等复杂业务目标。 本实现用纯标准库在进程内串起这 5 个环节,并逐级统计候选规模与真实耗时: 1) 语义ID化:对 item embedding 做残差量化(RQ),得到 L 层语义ID e_i -> (s_i^1, s_i^2,

上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。

源码结构与阅读入口

  1. MockItemContentModel · 第 39 行
  2. _l2 · 第 60 行
  3. _kmeans · 第 64 行
  4. build_semantic_ids · 第 84 行
  5. MockSeqGenModel · 第 102 行
  6. MockItemStore · 第 126 行
  7. beam_search · 第 140 行
  8. main · 第 153 行

完整源码 · 278 行

1"""对应正文第 9 章 §9.2「生成式推荐架构」中生成式推荐模块的内部结构。23正文把生成式推荐模块拆成三个核心组成部分(图「生成式推荐模块内部结构」):4    语义ID生成(Semantic ID Construction) -> 生成式推荐模型(Generative Recommendation Model)5    -> 物品ID检索(Item Retrieval)6而在整条链路上,正文还强调「大多数工业系统仍然会保留重排(Re-ranking)模块作为最后一道7决策环节」,因为生成式模型难以兼顾广告收益、多样性约束、生态治理等复杂业务目标。89本实现用纯标准库在进程内串起这 5 个环节,并逐级统计候选规模与真实耗时:10    1) 语义ID化:对 item embedding 做残差量化(RQ),得到 L 层语义ID11           e_i -> (s_i^1, s_i^2, ..., s_i^L)12       正文四层 codebook、每层 512 的组合空间规模为 512^4 ~ 6.87e10。13    2) 序列建模:用户行为序列展开为语义 token 序列14           [s_1^1..s_1^L, s_2^1..s_2^L, ...]15    3) 自回归生成:按 Score(SID) = sum_{i=1}^{L} log P(s_i | s_{<i}, u) 做 beam search。16    4) 物品ID检索:SID -> item,多对一去重、无效 SID 丢弃并从其他路径补充。17    5) 过滤重排:去重/已看过滤 + 业务重排分 = alpha*生成分 + beta*质量分 + gamma*商业分。1819[MOCK] 本文件中的 `MockItemContentModel`(内容理解打 embedding)、`MockSeqGenModel`20(用共现统计代替 Transformer 的 next-token 分布)、`MockItemStore`(倒排索引/物品属性21存储)都是教学用的进程内假组件,不起真实服务、不读取任何真实数据。22"""2324import math25import random26import time27from collections import Counter, defaultdict2829L_LAYERS = 3          # 语义ID层数30CODEBOOK_SIZE = 8     # 每层码本大小 K31EMB_DIM = 832N_ITEMS = 24033N_CATEGORIES = 6343536# ----------------------------------------------------------------------------37# [MOCK] 内容理解模型:真实系统里是多模态大模型,这里只用类别中心 + 噪声合成 embedding38# ----------------------------------------------------------------------------39class MockItemContentModel:40    def __init__(self, n_items, n_cat, dim):41        self.centers = [[random.gauss(0, 1) for _ in range(dim)] for _ in range(n_cat)]42        self.items = {}43        for i in range(n_items):44            cat = i % n_cat45            c = self.centers[cat]46            self.items[i] = {47                "cat": cat,48                "emb": [c[d] + random.gauss(0, 0.45) for d in range(dim)],49                "quality": round(random.uniform(0.2, 1.0), 3),50                "biz": round(random.uniform(0.0, 1.0), 3),   # 商业化价值(广告/电商)51            }5253    def embedding(self, item_id):54        return self.items[item_id]["emb"]555657# ----------------------------------------------------------------------------58# 1) 语义ID生成:残差量化(这里用逐层 KMeans,对应 §9.3.3 RQ-KMeans)59# ----------------------------------------------------------------------------60def _l2(a, b):61    return sum((x - y) ** 2 for x, y in zip(a, b))626364def _kmeans(vectors, k, iters=12):65    centers = [list(v) for v in random.sample(vectors, k)]66    assign = [0] * len(vectors)67    for _ in range(iters):68        changed = False69        for i, v in enumerate(vectors):70            best = min(range(k), key=lambda j: _l2(v, centers[j]))71            if best != assign[i]:72                assign[i], changed = best, True73        buckets = defaultdict(list)74        for i, a in enumerate(assign):75            buckets[a].append(vectors[i])76        for j in range(k):77            if buckets[j]:78                centers[j] = [sum(col) / len(buckets[j]) for col in zip(*buckets[j])]79        if not changed:80            break81    return centers, assign828384def build_semantic_ids(content_model, n_layers=L_LAYERS, k=CODEBOOK_SIZE):85    ids = sorted(content_model.items)86    residual = [list(content_model.embedding(i)) for i in ids]87    codes = [[] for _ in ids]88    codebooks = []89    for _ in range(n_layers):90        centers, assign = _kmeans(residual, k)91        codebooks.append(centers)92        for idx, a in enumerate(assign):93            codes[idx].append(a)94            residual[idx] = [x - c for x, c in zip(residual[idx], centers[a])]95    return {ids[i]: tuple(codes[i]) for i in range(len(ids))}, codebooks969798# ----------------------------------------------------------------------------99# [MOCK] 生成式推荐模型:真实系统是 Transformer Decoder-only,这里用100#        (上文 token, 下一 token) 的平滑共现频率近似 P(s_t | s_<t, u)101# ----------------------------------------------------------------------------102class MockSeqGenModel:103    def __init__(self, vocab_per_layer, n_layers):104        self.K = vocab_per_layer105        self.L = n_layers106        self.counts = [defaultdict(Counter) for _ in range(n_layers)]107108    def fit(self, token_sequences):109        """token_sequences: 每条是展开后的语义 token 序列 [(layer, code), ...]"""110        for seq in token_sequences:111            for t in range(1, len(seq)):112                layer, code = seq[t]113                ctx = seq[t - 1]                     # 1 阶上下文,教学简化114                self.counts[layer][ctx][code] += 1115116    def log_probs(self, ctx, layer):117        """返回第 layer 层码本上的 log P(code | ctx),Laplace 平滑。"""118        cnt = self.counts[layer][ctx]119        total = sum(cnt.values()) + self.K120        return [math.log((cnt[c] + 1) / total) for c in range(self.K)]121122123# ----------------------------------------------------------------------------124# [MOCK] 物品存储 / 倒排索引125# ----------------------------------------------------------------------------126class MockItemStore:127    def __init__(self, sid_map, content_model):128        self.sid_to_items = defaultdict(list)129        for item, sid in sid_map.items():130            self.sid_to_items[sid].append(item)131        self.meta = content_model.items132133    def lookup(self, sid):134        return self.sid_to_items.get(sid, [])135136137# ----------------------------------------------------------------------------138# 3) beam search:Score(SID) = sum_i log P(s_i | s_<i, u)139# ----------------------------------------------------------------------------140def beam_search(model, start_ctx, n_layers, beam=12):141    beams = [([], 0.0, start_ctx)]142    for layer in range(n_layers):143        nxt = []144        for codes, score, ctx in beams:145            lps = model.log_probs(ctx, layer)146            for c in range(model.K):147                nxt.append((codes + [c], score + lps[c], (layer, c)))148        nxt.sort(key=lambda x: -x[1])149        beams = nxt[:beam]150    return [(tuple(c), s) for c, s, _ in beams]151152153def main():154    random.seed(42)155    t_all = time.perf_counter()156    stages = []157158    # ---- 阶段 0:内容理解(离线) ----159    t0 = time.perf_counter()160    content = MockItemContentModel(N_ITEMS, N_CATEGORIES, EMB_DIM)161    t_content = (time.perf_counter() - t0) * 1e3162163    # ---- 阶段 1:语义ID生成(离线) ----164    t0 = time.perf_counter()165    sid_map, codebooks = build_semantic_ids(content)166    t_sid = (time.perf_counter() - t0) * 1e3167    uniq_sid = len(set(sid_map.values()))168    stages.append(("1 语义ID生成(离线)", N_ITEMS, uniq_sid, t_sid))169170    # ---- 构造用户行为序列(同类别内游走,制造可学习结构) ----171    sequences, user_hist = [], {}172    for u in range(300):173        cat = random.randrange(N_CATEGORIES)174        pool = [i for i in sid_map if content.items[i]["cat"] == cat]175        hist = random.sample(pool, 6)176        user_hist[u] = hist177        toks = []178        for it in hist:179            for layer, code in enumerate(sid_map[it]):180                toks.append((layer, code))181        sequences.append(toks)182183    # ---- 阶段 2:序列建模(训练) ----184    t0 = time.perf_counter()185    model = MockSeqGenModel(CODEBOOK_SIZE, L_LAYERS)186    model.fit(sequences)187    t_fit = (time.perf_counter() - t0) * 1e3188189    # ---- 在线链路:以 user 0 为例 ----190    uid = 0191    hist = user_hist[uid]192    ctx_tokens = []193    for it in hist[:-1]:194        for layer, code in enumerate(sid_map[it]):195            ctx_tokens.append((layer, code))196    stages.append(("2 序列建模(token化)", len(hist) - 1, len(ctx_tokens), t_fit))197198    # ---- 阶段 3:自回归生成 ----199    t0 = time.perf_counter()200    beams = beam_search(model, ctx_tokens[-1], L_LAYERS, beam=24)201    t_gen = (time.perf_counter() - t0) * 1e3202    stages.append(("3 自回归生成(beam=24)", CODEBOOK_SIZE ** L_LAYERS, len(beams), t_gen))203204    # ---- 阶段 4:物品ID检索(多对一去重 + 无效SID丢弃) ----205    t0 = time.perf_counter()206    store = MockItemStore(sid_map, content)207    cand, invalid, seen = [], 0, set()208    for sid, score in beams:209        items = store.lookup(sid)210        if not items:211            invalid += 1212            continue213        for it in items:214            if it in seen:        # 多个语义ID映射到同一物品 -> 仅保留得分最高者215                continue216            seen.add(it)217            cand.append((it, score))218    t_ret = (time.perf_counter() - t0) * 1e3219    stages.append(("4 物品ID检索", len(beams), len(cand), t_ret))220221    # ---- 阶段 5:过滤 + 重排 ----222    t0 = time.perf_counter()223    watched = set(hist)224    filtered = [(i, s) for i, s in cand if i not in watched]225    alpha, beta, gamma = 1.0, 2.0, 0.5226    reranked = sorted(227        (228            (229                i,230                alpha * s + beta * content.items[i]["quality"] + gamma * content.items[i]["biz"],231                s,232            )233            for i, s in filtered234        ),235        key=lambda x: -x[1],236    )[:10]237    t_rr = (time.perf_counter() - t0) * 1e3238    stages.append(("5 过滤+重排", len(cand), len(reranked), t_rr))239240    print("=" * 78)241    print("§9.2 生成式推荐模块链路仿真(语义ID化 -> 序列建模 -> 生成 -> 检索 -> 重排)")242    print("=" * 78)243    print(f"内容理解([MOCK] MLLM) 耗时 {t_content:.2f} ms;物品数 {N_ITEMS},类别 {N_CATEGORIES}")244    print(f"语义ID配置:L={L_LAYERS} 层,每层 K={CODEBOOK_SIZE},"245          f"理论组合空间 {CODEBOOK_SIZE}^{L_LAYERS}={CODEBOOK_SIZE ** L_LAYERS}")246    print(f"正文四层 512 码本的组合空间:512^4 = {512 ** 4:.4g}")247    print()248    print(f"{'阶段':<24}{'输入规模':>10}{'输出规模':>10}{'耗时(ms)':>12}")249    print("-" * 78)250    for name, nin, nout, ms in stages:251        print(f"{name:<24}{nin:>10}{nout:>10}{ms:>12.3f}")252    print("-" * 78)253    print(f"{'合计':<24}{'':>10}{'':>10}{sum(s[3] for s in stages):>12.3f}")254255    print()256    print(f"beam 候选中无效语义ID(Invalid SID) {invalid}/{len(beams)} "257          f"= {invalid / len(beams):.1%}(被直接丢弃,由其他路径补充)")258    collide = Counter(len(v) for v in store.sid_to_items.values())259    print("语义ID冲突分布(一个SID命中的物品数 -> SID个数):",260          dict(sorted(collide.items())))261262    print()263    print("Top-10 重排结果:")264    print(f"{'rank':>5}{'item':>7}{'cat':>5}{'生成分':>10}{'质量':>8}{'商业':>8}{'重排分':>10}")265    for r, (it, final, gen) in enumerate(reranked, 1):266        m = content.items[it]267        print(f"{r:>5}{it:>7}{m['cat']:>5}{gen:>10.3f}{m['quality']:>8.3f}"268              f"{m['biz']:>8.3f}{final:>10.3f}")269270    cats = Counter(content.items[i]["cat"] for i, _, _ in reranked)271    print()272    print(f"Top-10 类别分布 {dict(sorted(cats.items()))};用户历史类别 "273          f"{content.items[hist[0]]['cat']}")274    print(f"链路总墙钟耗时 {(time.perf_counter() - t_all) * 1e3:.1f} ms")275276277if __name__ == "__main__":278    main()

如何用于学习或编写新代码

  1. 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
  2. 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
  3. 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
  4. 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。