CH 09 / SOURCE EXAMPLE

rq_vae.py

ch09 生成式推荐模块/code/ch03_semantic_id/rq_vae.py

这个示例讲什么

│ ├── rq_vae.py § 9.3.1 RQ-VAE:残差量化 + STE 直通估计

对应正文第 9 章 §9.3.1「RQ-VAE」与 §9.3.2「RQ-VAE 的工程实践问题」。 实现内容: * Encoder f_theta: x -> z_0; * 逐层残差量化(RQ): s^l = argmin_{k in C^l} ||z_{l-1} - e_k||^2, z_l = z_{l-1} - e_{s^l}, 于是 z_0 ~= sum_{l=1}^{L} e_{s^l}; * Decoder g_phi: x_hat = g_phi(sum_l e_{s^l}); * 正文的整体训练目标 L = L_recon + lambda * sum_l ||z_{l-1} - e_{s^l}||^2, 工程上把该量化项按 VQ-VAE 惯例拆成 codebook loss 与 commitment loss: ||sg[z_

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

源码结构与阅读入口

  1. make_items · 第 29 行合成带簇结构的 item embedding(模拟内容大模型输出)。
  2. ResidualQuantizer · 第 36 行L 层残差量化器, 每层码本大小 K。
  3. RQVAE · 第 72 行
  4. perplexity · 第 89 行返回 (Perplexity, 码本使用率)。
  5. main · 第 97 行

完整源码 · 166 行

1"""对应正文第 9 章 §9.3.1「RQ-VAE」与 §9.3.2「RQ-VAE 的工程实践问题」。23实现内容:4  * Encoder f_theta: x -> z_0;5  * 逐层残差量化(RQ): s^l = argmin_{k in C^l} ||z_{l-1} - e_k||^2,6    z_l = z_{l-1} - e_{s^l}, 于是 z_0 ~= sum_{l=1}^{L} e_{s^l};7  * Decoder g_phi: x_hat = g_phi(sum_l e_{s^l});8  * 正文的整体训练目标 L = L_recon + lambda * sum_l ||z_{l-1} - e_{s^l}||^2,9    工程上把该量化项按 VQ-VAE 惯例拆成 codebook loss 与 commitment loss:10      ||sg[z_{l-1}] - e_{s^l}||^2 + beta * ||z_{l-1} - sg[e_{s^l}]||^2;11  * STE 梯度近似: z_STE = z_e + sg(z_q - z_e), 前向等于 z_q, 反向 dz_STE/dz_e = 1;12  * 码本坍缩诊断: p_k = n_k / sum_j n_j, Perplexity = exp(-sum_k p_k log p_k),13    均匀使用时 Perplexity = K, 远小于 K 则说明出现 Codebook Collapse。1415本脚本在合成的「带簇结构」item embedding 上训练一个小规模 RQ-VAE, 打印16每层码本的使用率 / Perplexity / 逐层累计重构误差数值表。17"""1819from __future__ import annotations2021import random2223import numpy as np24import torch25import torch.nn as nn26import torch.nn.functional as F272829def make_items(n: int, d: int, n_cluster: int, gen: torch.Generator) -> torch.Tensor:30    """合成带簇结构的 item embedding(模拟内容大模型输出)。"""31    centers = torch.randn(n_cluster, d, generator=gen) * 2.032    idx = torch.randint(0, n_cluster, (n,), generator=gen)33    return centers[idx] + 0.35 * torch.randn(n, d, generator=gen)343536class ResidualQuantizer(nn.Module):37    """L 层残差量化器, 每层码本大小 K。"""3839    def __init__(self, n_layer: int, codebook_size: int, dim: int, beta: float = 0.25):40        super().__init__()41        self.n_layer, self.K, self.beta = n_layer, codebook_size, beta42        self.codebooks = nn.ParameterList(43            [nn.Parameter(torch.randn(codebook_size, dim) * 0.1) for _ in range(n_layer)]44        )4546    def forward(self, z0: torch.Tensor):47        residual = z048        quantized = torch.zeros_like(z0)49        codes, cb_loss, cm_loss = [], z0.new_zeros(()), z0.new_zeros(())50        layer_err = []51        for layer in range(self.n_layer):52            e = self.codebooks[layer]                       # (K, d)53            # s^l = argmin_k ||z_{l-1} - e_k||^254            dist = (residual.pow(2).sum(1, keepdim=True)55                    - 2 * residual @ e.t()56                    + e.pow(2).sum(1).unsqueeze(0))57            s = dist.argmin(1)58            codes.append(s)59            e_s = e[s]60            # 正文量化项的两个工程拆分: codebook loss + commitment loss61            cb_loss = cb_loss + F.mse_loss(e_s, residual.detach())62            cm_loss = cm_loss + F.mse_loss(residual, e_s.detach())63            quantized = quantized + e_s64            residual = residual - e_s65            layer_err.append(residual.detach().pow(2).sum(1).mean().item())66        # STE: z_q 前向用量化值, 反向把梯度直通给 encoder 输出67        z_ste = z0 + (quantized - z0).detach()68        quant_loss = cb_loss + self.beta * cm_loss69        return z_ste, torch.stack(codes, 1), quant_loss, layer_err707172class RQVAE(nn.Module):73    def __init__(self, in_dim: int, lat_dim: int, n_layer: int, K: int):74        super().__init__()75        self.encoder = nn.Sequential(76            nn.Linear(in_dim, 64), nn.GELU(), nn.Linear(64, lat_dim))77        self.decoder = nn.Sequential(78            nn.Linear(lat_dim, 64), nn.GELU(), nn.Linear(64, in_dim))79        self.rq = ResidualQuantizer(n_layer, K, lat_dim)8081    def forward(self, x: torch.Tensor, lam: float = 1.0):82        z0 = self.encoder(x)83        z_ste, codes, quant_loss, layer_err = self.rq(z0)84        x_hat = self.decoder(z_ste)85        recon = F.mse_loss(x_hat, x)86        return recon + lam * quant_loss, recon, codes, layer_err878889def perplexity(codes_layer: torch.Tensor, K: int) -> tuple[float, float]:90    """返回 (Perplexity, 码本使用率)。"""91    n_k = torch.bincount(codes_layer, minlength=K).double()92    p = n_k / n_k.sum()93    nz = p[p > 0]94    return float(torch.exp(-(nz * nz.log()).sum())), float((n_k > 0).float().mean())959697def main() -> None:98    random.seed(42)99    np.random.seed(42)100    torch.manual_seed(42)101    gen = torch.Generator().manual_seed(42)102103    N, D, LAT, L, K = 3000, 48, 16, 3, 64104    x = make_items(N, D, n_cluster=40, gen=gen)105    model = RQVAE(D, LAT, L, K)106    opt = torch.optim.Adam(model.parameters(), lr=3e-3)107108    print("=" * 72)109    print(f"RQ-VAE 训练: N={N} items, dim={D}, latent={LAT}, L={L} 层, K={K}/层")110    print(f"语义 ID 空间 = K^L = {K ** L:,}")111    print("-" * 72)112    print(f"{'step':>6}{'total':>11}{'recon':>11}{'层1 PPL':>10}{'层2 PPL':>10}{'层3 PPL':>10}")113    hist = []114    for step in range(301):115        perm = torch.randperm(N, generator=gen)[:512]116        loss, recon, codes, layer_err = model(x[perm])117        opt.zero_grad()118        loss.backward()119        opt.step()120        if step % 60 == 0:121            ppls = [perplexity(codes[:, l], K)[0] for l in range(L)]122            print(f"{step:>6}{loss.item():>11.4f}{recon.item():>11.4f}"123                  + "".join(f"{p:>10.1f}" for p in ppls))124            hist.append((step, recon.item(), ppls))125126    with torch.no_grad():127        loss, recon, codes, layer_err = model(x)128        z0 = model.encoder(x)129130    print()131    print("=" * 72)132    print("表 1  各层码本使用情况(Perplexity 上限 = K = %d)" % K)133    print("-" * 72)134    print(f"{'层 l':>5}{'被用码字数':>12}{'使用率':>10}{'Perplexity':>13}{'PPL/K':>9}")135    for l in range(L):136        ppl, usage = perplexity(codes[:, l], K)137        print(f"{l + 1:>5}{int(usage * K):>12}{usage:>10.1%}{ppl:>13.2f}{ppl / K:>9.1%}")138139    print()140    print("表 2  逐层残差量化的累计逼近效果 z_0 ~= sum_l e_{s^l}")141    print("-" * 72)142    base = z0.pow(2).sum(1).mean().item()143    print(f"{'量化层数':>10}{'残差能量 E||z_l||^2':>22}{'相对初始':>12}")144    print(f"{0:>10}{base:>22.4f}{1.0:>12.1%}")145    for l, err in enumerate(layer_err, 1):146        print(f"{l:>10}{err:>22.4f}{err / base:>12.1%}")147148    print()149    print("表 3  最终重构质量")150    print("-" * 72)151    with torch.no_grad():152        x_hat = model.decoder(model.rq(z0)[0])153        mse = F.mse_loss(x_hat, x).item()154        var = x.var().item()155    print(f"输入方差            = {var:.4f}")156    print(f"重构 MSE L_recon    = {mse:.4f}")157    print(f"相对重构误差 MSE/Var = {mse / var:.2%}")158    uniq = len({tuple(r.tolist()) for r in codes})159    print(f"唯一语义 ID 数       = {uniq} / {N} items (冲突率 {1 - uniq / N:.2%})")160    print("-" * 72)161    print("注: 本实现严格按正文 L = L_recon + lambda*sum_l||z_{l-1}-e_{s^l}||^2,")162    print("    并用 STE(z_e + sg(z_q - z_e)) 把 Decoder 梯度直通到 Encoder。")163164165if __name__ == "__main__":166    main()

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

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