Generative Models — 理论 & Tokenizers进阶36 分钟P2 · Watch

VAE, VQ-VAE, VQ-GAN, and FSQ

连续/离散 latent、codebook、commitment loss、GAN 感知损失与 tokenizer 设计。

VAEVQ-VAEVQ-GANFSQtokenizer
内容来源:ARIS-in-AI-Offer by Ruofeng Yang (杨若峰),许可证 MITImported from commit 7fae0a820348.
Local freshness note

Imported from ARIS and not yet locally reviewed section by section. Cross-check recent papers, model reports, and library APIs while reading.

Check recent citationsAdd post-2026 updatesFlag possibly stale model/API names

§0 TL;DR Cheat Sheet

💡 VAE / VQ-VAE / VQ-GAN / FSQ in 8 sentences — one page covering the interview essentials (see §2–§9 for derivations).

  1. Continuous VAE objective: maximize ELBO, logp(x)Eqϕ(zx)[logpθ(xz)]DKL(qϕ(zx)p(z))\log p(x) \geq \mathbb{E}_{q_\phi(z|x)}[\log p_\theta(x|z)] - D_\text{KL}(q_\phi(z|x)\,\|\,p(z)); reparameterization z=μ+σϵz = \mu + \sigma \odot \epsilon lets gradients flow through stochastic sampling.

  2. KL closed form (must-know): DKL(N(μ,σ2I)N(0,I))=12i(μi2+σi2logσi21)D_\text{KL}(\mathcal{N}(\mu,\sigma^2 I)\,\|\,\mathcal{N}(0,I)) = \tfrac{1}{2}\sum_i (\mu_i^2 + \sigma_i^2 - \log \sigma_i^2 - 1).

  3. Posterior collapse: KL → 0 → decoder ignores zz; mitigations: KL annealing, free bits, β\beta schedule, autoregressive prior.

  4. VQ-VAE: maps encoder output ze(x)z_e(x) to the nearest codebook vector eke_k, loss = recon + sg[ze]e2\|\text{sg}[z_e] - e\|^2 (codebook) +βzesg[e]2+ \beta \|z_e - \text{sg}[e]\|^2 (commitment).

  5. Straight-Through Estimator (STE): argmin / quantize is non-differentiable, use quantized value in forward, pass-through gradient L/zqL/ze\partial \mathcal{L}/\partial z_q \to \partial \mathcal{L}/\partial z_e in backward.

  6. VQ-GAN: VQ-VAE + perceptual (LPIPS) + adversarial (PatchGAN) + post-trained Transformer prior; lays the foundation for LDM / Parti / Muse and other discrete-token models.

  7. FSQ (2024): per-dimension scalar quantization to {L,,L}\{-L,\ldots,L\}, implicit codebook size iLi\prod_i L_i (e.g. L=8,d=686=262144L=8, d=6 \Rightarrow 8^6 = 262{144}), no need for STE, no codebook collapse, rounding uses STE only, loss only has reconstruction.

  8. Ecosystem comparison: continuous latent (VAE / KL) suits LDM-style diffusion; discrete tokens (VQ-VAE / VQ-GAN / FSQ / LFQ) suit AR / MaskGIT Transformer priors, the core component of Parti / Muse / Cosmos.

§1 Intuition: Why Latent Variable Models

The core challenge of generative modeling: directly modeling p(x)p(x) is hard, but if we introduce low-dim latent zz:

p(x)=p(xz)p(z)dzp(x) = \int p(x|z)\, p(z)\, dz

we can decompose "complex image distribution" into "simple prior p(z)p(z) (e.g. N(0,I)\mathcal{N}(0, I))" plus "easy-to-learn conditional p(xz)p(x|z)." Two paths:

  • Continuous latent (VAE): zRdz \in \mathbb{R}^d, KL pulls posterior to Gaussian prior, naturally compatible with diffusion / FM (LDM runs diffusion in the VAE latent).

  • Discrete latent (VQ-VAE / VQ-GAN / FSQ): zVH×Wz \in \mathcal{V}^{H \times W} (token grid), naturally compatible with Transformer / AR / MaskGIT (an image becomes a sequence of tokens, reusing language-model architectures).

💡 Training vs inference asymmetry — VAE/VQ-VAE trains the full encoder + decoder (rate-distortion view: "compress-reconstruct"); at inference there are two cases depending on application:

  • Generate new samples: drop the encoder, sample zz from prior, pass through decoder
  • Downstream backbone: drop the decoder, use encoder/zz as representation for subsequent models
  • Two-stage generation (LDM / Parti / Muse): first train VAE/VQ-GAN tokenizer, then train diffusion / AR / MaskGIT prior in the latent space. Tokenizer is frozen after training.

§2 VAE: Core Formulas and Derivations

2.1 ELBO derivation (must-know, derive line by line)

Model family pθ(x,z)=pθ(xz)p(z)p_\theta(x, z) = p_\theta(x|z)\, p(z), prior p(z)=N(0,I)p(z) = \mathcal{N}(0, I), likelihood pθ(xz)p_\theta(x|z) given by decoder. Marginal likelihood:

logpθ(x)=logpθ(xz)p(z)dz\log p_\theta(x) = \log \int p_\theta(x|z) p(z)\, dz

For any distribution qϕ(zx)q_\phi(z|x) (encoder / variational posterior, qϕ(zx)=N(μϕ(x),diag(σϕ2(x)))q_\phi(z|x) = \mathcal{N}(\mu_\phi(x), \mathrm{diag}(\sigma_\phi^2(x)))), by Jensen's inequality / direct substitution:

logpθ(x)=logqϕ(zx)pθ(xz)p(z)qϕ(zx)dzEqϕ(zx) ⁣[logpθ(xz)p(z)qϕ(zx)](Jensen)=Eqϕ(zx)[logpθ(xz)]reconstruction (negative)DKL(qϕ(zx)p(z))regularization\begin{aligned} \log p_\theta(x) &= \log \int q_\phi(z|x) \frac{p_\theta(x|z) p(z)}{q_\phi(z|x)} dz \\ &\geq \mathbb{E}_{q_\phi(z|x)}\!\left[\log \frac{p_\theta(x|z) p(z)}{q_\phi(z|x)}\right] \quad \text{(Jensen)} \\ &= \underbrace{\mathbb{E}_{q_\phi(z|x)}[\log p_\theta(x|z)]}_{\text{reconstruction (negative)}} - \underbrace{D_\text{KL}(q_\phi(z|x)\,\|\,p(z))}_{\text{regularization}} \end{aligned}

So ELBO:

  LELBO(θ,ϕ;x)=Eqϕ(zx)[logpθ(xz)]DKL ⁣(qϕ(zx)p(z))  \boxed{\;\mathcal{L}_\text{ELBO}(\theta, \phi; x) = \mathbb{E}_{q_\phi(z|x)}[\log p_\theta(x|z)] - D_\text{KL}\!\big(q_\phi(z|x)\,\|\,p(z)\big)\;}

Tightness cost: the gap between ELBO and the true log-likelihood is DKL(qϕ(zx)pθ(zx))D_\text{KL}(q_\phi(z|x)\,\|\,p_\theta(z|x)). The closer qϕq_\phi approximates the true posterior pθ(zx)p_\theta(z|x), the smaller the gap.

2.2 KL term closed-form derivation (L3 must derive)

Let qϕ(zx)=N(μ,diag(σ2))q_\phi(z|x) = \mathcal{N}(\mu, \mathrm{diag}(\sigma^2)) (diagonal covariance, per-dim σi2\sigma_i^2), p(z)=N(0,I)p(z) = \mathcal{N}(0, I).

For each dimension ii independently:

DKL(N(μi,σi2)N(0,1))=N(z;μi,σi2)logN(z;μi,σi2)N(z;0,1)dz\begin{aligned} D_\text{KL}(\mathcal{N}(\mu_i, \sigma_i^2) \,\|\, \mathcal{N}(0, 1)) &= \int \mathcal{N}(z; \mu_i, \sigma_i^2) \log \frac{\mathcal{N}(z; \mu_i, \sigma_i^2)}{\mathcal{N}(z; 0, 1)} dz \end{aligned}

Expanding the log of two Gaussian densities:

logN(z;μi,σi2)N(z;0,1)=12logσi2(zμi)22σi2+z22\log \frac{\mathcal{N}(z; \mu_i, \sigma_i^2)}{\mathcal{N}(z; 0, 1)} = -\tfrac{1}{2}\log \sigma_i^2 - \tfrac{(z-\mu_i)^2}{2\sigma_i^2} + \tfrac{z^2}{2}

Taking expectation (using Eq[z]=μi\mathbb{E}_q[z] = \mu_i, Eq[z2]=μi2+σi2\mathbb{E}_q[z^2] = \mu_i^2 + \sigma_i^2, Eq[(zμi)2]=σi2\mathbb{E}_q[(z-\mu_i)^2] = \sigma_i^2):

DKL=12logσi212+12(μi2+σi2)=12(μi2+σi2logσi21)\begin{aligned} D_\text{KL} &= -\tfrac{1}{2}\log \sigma_i^2 - \tfrac{1}{2} + \tfrac{1}{2}(\mu_i^2 + \sigma_i^2) \\ &= \tfrac{1}{2}\big(\mu_i^2 + \sigma_i^2 - \log \sigma_i^2 - 1\big) \end{aligned}

Sum over all dimensions:

  DKL(N(μ,diag(σ2))N(0,I))=12i=1d(μi2+σi2logσi21)  \boxed{\;D_\text{KL}\big(\mathcal{N}(\mu, \mathrm{diag}(\sigma^2)) \,\|\, \mathcal{N}(0, I)\big) = \tfrac{1}{2}\sum_{i=1}^{d}\big(\mu_i^2 + \sigma_i^2 - \log \sigma_i^2 - 1\big)\;}

⚠️ Numerical stability — In implementation have the encoder output logσ2\log \sigma^2 (log-variance) rather than σ\sigma, to avoid overflow when taking exp on σ\sigma. In code: kl = 0.5 * (mu**2 + logvar.exp() - logvar - 1).sum().

2.3 Reparameterization Trick (must-know)

The Eqϕ(zx)[]\mathbb{E}_{q_\phi(z|x)}[\cdot] in ELBO is estimated by Monte Carlo: sample one zqϕ(zx)z \sim q_\phi(z|x), compute logpθ(xz)\log p_\theta(x|z).

Problem: directly sampling z=sample(N(μ,σ2))z = \text{sample}(\mathcal{N}(\mu, \sigma^2)) is non-differentiable, gradients cannot flow back to ϕ\phi.

Solution: move the randomness into an independent noise:

  z=μϕ(x)+σϕ(x)ϵ,ϵN(0,I)  \boxed{\;z = \mu_\phi(x) + \sigma_\phi(x) \odot \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)\;}

Now zz is a deterministic function of ϕ\phi (conditioned on ϵ\epsilon), and ϕL\nabla_\phi \mathcal{L} can backpropagate normally. This is one of the core contributions of Kingma & Welling (ICLR 2014).

💡 Interview bonus: reparameterization is not just for Gaussians — Concrete / Gumbel-softmax (§7) plays a similar trick for discrete variables: replace argmax with softmax + Gumbel noise, approximating discrete in forward, using softmax gradient in backward.

2.4 VAE training loss (practical formulation)

Negative ELBO (to minimize):

LVAE(x)=xx^2recon (Gaussian likelihood up to const)+DKL(qϕ(zx)p(z))closed form\mathcal{L}_\text{VAE}(x) = \underbrace{\|x - \hat{x}\|^2}_{\text{recon (Gaussian likelihood up to const)}} + \underbrace{D_\text{KL}(q_\phi(z|x)\,\|\,p(z))}_{\text{closed form}}

For Bernoulli / Categorical likelihoods (e.g. binary MNIST), replace the recon term with BCE / CE.

§3 Complete VAE Implementation (PyTorch)

import math import torch import torch.nn as nn import torch.nn.functional as F class VAE(nn.Module): """ Classic VAE: Gaussian encoder + Gaussian/Bernoulli decoder This implementation uses MNIST (28×28) as example, latent dim=20 For production swap MLP for ResNet / U-Net encoder/decoder, latent can be a spatial map """ def __init__(self, x_dim: int = 784, h_dim: int = 400, z_dim: int = 20, likelihood: str = "bernoulli"): super().__init__() self.x_dim, self.z_dim = x_dim, z_dim self.likelihood = likelihood # Encoder: x -> (μ, logσ²) self.enc = nn.Sequential( nn.Linear(x_dim, h_dim), nn.ReLU(), nn.Linear(h_dim, h_dim), nn.ReLU(), ) self.fc_mu = nn.Linear(h_dim, z_dim) self.fc_logvar = nn.Linear(h_dim, z_dim) # Decoder: z -> x̂ self.dec = nn.Sequential( nn.Linear(z_dim, h_dim), nn.ReLU(), nn.Linear(h_dim, h_dim), nn.ReLU(), nn.Linear(h_dim, x_dim), ) def encode(self, x: torch.Tensor): h = self.enc(x.view(x.size(0), -1)) return self.fc_mu(h), self.fc_logvar(h) def reparameterize(self, mu: torch.Tensor, logvar: torch.Tensor): # z = μ + σ ⊙ ε, σ = exp(0.5 · logvar) if self.training: std = torch.exp(0.5 * logvar) eps = torch.randn_like(std) return mu + std * eps else: # At inference use the posterior mean (deterministic) return mu def decode(self, z: torch.Tensor): logits = self.dec(z) if self.likelihood == "bernoulli": return torch.sigmoid(logits), logits return logits, logits # Gaussian likelihood: treated as mean prediction def forward(self, x: torch.Tensor): mu, logvar = self.encode(x) z = self.reparameterize(mu, logvar) x_hat, logits = self.decode(z) return x_hat, logits, mu, logvar def vae_loss(x: torch.Tensor, logits: torch.Tensor, mu: torch.Tensor, logvar: torch.Tensor, likelihood: str = "bernoulli", beta: float = 1.0, free_bits: float = 0.0): """ Returns: (loss, recon, kl) beta: β in β-VAE (default 1 = standard VAE) free_bits: per-dim KL lower bound (nats). When > 0 enables free bits""" B = x.size(0) x_flat = x.view(B, -1) # 1) Reconstruction term: -E_q[log p(x|z)] if likelihood == "bernoulli": # BCE-with-logits is more numerically stable, equivalent to -log Bernoulli likelihood recon = F.binary_cross_entropy_with_logits( logits, x_flat, reduction="sum") / B elif likelihood == "gaussian": # Assuming σ² = 1 (constant), MSE differs from negative log-Gaussian by a constant recon = 0.5 * F.mse_loss(logits, x_flat, reduction="sum") / B else: raise ValueError(likelihood) # 2) KL term: D_KL(N(μ, σ²) || N(0, I)) closed form kl_per_dim = 0.5 * (mu.pow(2) + logvar.exp() - logvar - 1) # [B, z_dim] if free_bits > 0: # Free bits: per-dim KL lower bound = free_bits (mitigates posterior collapse) kl_per_dim = torch.clamp(kl_per_dim, min=free_bits) kl = kl_per_dim.sum(dim=-1).mean() # scalar loss = recon + beta * kl return loss, recon, kl

⚠️ Common bug list — Pitfalls when writing VAE.

  • Writing reparameterize as mu + logvar * eps, forgetting σ=exp(0.5logσ2)\sigma = \exp(0.5 \cdot \log\sigma^2)
  • KL as 0.5 * (mu**2 + sigma**2 - 2*log_sigma - 1), note it's logσ2=2logσ-\log \sigma^2 = -2\log\sigma
  • BCE written as F.binary_cross_entropy(sigmoid(logits), x) rather than F.binary_cross_entropy_with_logits(logits, x), the former is numerically unstable
  • Inconsistent reduction: recon using sum, KL using mean, causing β\beta's actual scale to drift

3.1 Training loop + KL annealing

def train_vae(model, dataloader, total_steps=50_000, lr=1e-3, device="cuda", beta_max=1.0, anneal_steps=10_000, free_bits=0.0): """ KL annealing: β linearly grows from 0 to beta_max, preventing posterior from collapsing in early training """ opt = torch.optim.Adam(model.parameters(), lr=lr) model.to(device).train() step = 0 while step < total_steps: for x, _ in dataloader: x = x.to(device) beta = min(beta_max, beta_max * step / max(anneal_steps, 1)) x_hat, logits, mu, logvar = model(x) loss, recon, kl = vae_loss(x, logits, mu, logvar, beta=beta, free_bits=free_bits) opt.zero_grad(set_to_none=True) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) opt.step() step += 1 if step >= total_steps: break

§4 VAE Variants: β\beta-VAE / IWAE / NVAE / VAE-GAN

4.1 β\beta-VAE (Higgins et al., ICLR 2017)

Weight the KL of ELBO by β\beta:

Lβ-VAE(θ,ϕ;x)=Eqϕ[logpθ(xz)]βDKL(qϕ(zx)p(z))\mathcal{L}_{\beta\text{-VAE}}(\theta, \phi; x) = \mathbb{E}_{q_\phi}[\log p_\theta(x|z)] - \beta \cdot D_\text{KL}(q_\phi(z|x)\,\|\,p(z))

  • β>1\beta > 1: stronger push of posterior → prior, encouraging disentangled representation (each dim of zz controls an independent factor, e.g. position, shape, rotation on dSprites).
  • β<1\beta < 1: relax KL, reconstruction more precise but latent less prior-like (sampling quality worse).
  • β=1\beta = 1 degenerates to standard VAE.

⚠️ Disentanglement controversy — Locatello et al. (ICML 2019, best paper) proved: pure unsupervised disentanglement is impossible without inductive bias / supervision. β\beta-VAE's "emergent disentanglement" largely depends on architecture + dataset bias, not β\beta itself.

4.2 IWAE: Importance Weighted Autoencoder (Burda et al., ICLR 2016)

ELBO is a first-order bound on logp(x)\log p(x). With KK importance samples get a tighter bound:

LKIWAE(x)=Ez1,,zKqϕ ⁣[log1Kk=1Kpθ(x,zk)qϕ(zkx)]\mathcal{L}_K^\text{IWAE}(x) = \mathbb{E}_{z_1,\ldots,z_K \sim q_\phi}\!\left[\log \frac{1}{K}\sum_{k=1}^K \frac{p_\theta(x, z_k)}{q_\phi(z_k|x)}\right]

Properties:

  • L1IWAE=\mathcal{L}_1^\text{IWAE} = ELBO (special case).
  • LKIWAElogp(x)\mathcal{L}_K^\text{IWAE} \to \log p(x) as KK \to \infty (Burda theorem).
  • Larger KK → more expressive inference (but training cost also ×K\times K).

💡 Tradeoff — IWAE makes the likelihood bound tighter, but the encoder's learned posterior no longer pursues "approximating the true posterior," instead aligning with the geometry of importance weighting. Not necessarily better for downstream representation learning.

4.3 NVAE: Hierarchical VAE (Vahdat & Kautz, NeurIPS 2020)

Multi-layer latent z=(z1,z2,,zL)z = (z_1, z_2, \ldots, z_L), each layer depends on the previous:

p(z)=p(z1)l=2Lp(zlz<l),q(zx)=q(z1x)l=2Lq(zlz<l,x)p(z) = p(z_1)\prod_{l=2}^L p(z_l | z_{<l}), \quad q(z|x) = q(z_1|x)\prod_{l=2}^L q(z_l | z_{<l}, x)

Engineering essentials:

  • Residual normal parameterization: q(zl)=N(μp+Δμq,σpΔσq)q(z_l|\cdot) = \mathcal{N}(\mu_p + \Delta\mu_q, \sigma_p \cdot \Delta\sigma_q), letting posterior be a small perturbation from prior
  • Spectral regularization to control per-layer KL, avoiding numerical instability
  • Architecture tuning with BN + Swish + depthwise etc.
  • On CIFAR-10 / CelebA / FFHQ first push VAE's NLL close to SOTA flow / autoregressive

NVAE's current role: one of the strongest continuous VAE priors before LDM, but surpassed in sample quality by diffusion series.

4.4 VAE-GAN (Larsen et al., ICML 2016)

VAE's reconstruction loss (pixel-MSE / BCE) is insensitive to high-frequency details → blurry generations. VAE-GAN replaces / supplements MSE with discriminator feature matching:

LreconVAE-GAN=Dl(x)Dl(x^)2\mathcal{L}_\text{recon}^\text{VAE-GAN} = \|D_l(x) - D_l(\hat{x})\|^2

where DlD_l is the discriminator's intermediate layer features. Combined with adversarial loss, reconstruction is perceptually sharper.

This idea culminates in VQ-GAN (§6): VQ-VAE framework + perceptual + adversarial + high-bitrate codebook + Transformer prior.

§5 Posterior Collapse (must-know)

5.1 Phenomenon

During training DKL(qϕ(zx)p(z))0D_\text{KL}(q_\phi(z|x)\,\|\,p(z)) \to 0, i.e. qϕ(zx)p(z)q_\phi(z|x) \approx p(z), independent of xx. Consequence: decoder completely ignores zz, VAE degenerates to an unconditional generative model.

5.2 Causes (intuitive analysis)

  • Decoder too strong: if pθ(xz)p_\theta(x|z) is itself an expressive PixelCNN / Autoregressive decoder (Bowman 2016's LSTM text VAE classic flop), it can fit the data without relying on zz, so ELBO's optimal strategy is to zero out the KL term.
  • KL term too high pressure: ELBO in early training, reconstruction is not yet established, optimizer easily pushes KL to 0 first (local optimum).
  • Simple data: collapse rare on MNIST, common on text VAEs.

5.3 Mitigation methods (interview must list)

MethodHowSource
KL annealingβ(t)=min(1,t/T)\beta(t) = \min(1, t / T) linear from 0 to 1Bowman et al. (2016)
Free bitsPer-dim KL lower bound λ\lambda nats: max(DKL(i),λ)\max(D_\text{KL}^{(i)}, \lambda)Kingma et al. (2016)
β\beta < 1Directly reduce KL weightβ\beta-VAE reverse usage
Weakened decoderWhen using strong AR decoders like PixelCNN, manually truncate context / add dropoutChen et al. (2017)
Auxiliary taskAdd word dropout, bag-of-words auxiliary lossBowman et al. (2016)
VAE-IAF / NF priorUse more complex prior or normalizing flow posteriorKingma et al. (2016)
Skip / lateral connectionsForce latent to participate in decoder (e.g. VLAE)Zhao et al. (2017)
VQ-VAEDiscrete latent + codebook commitment, structurally avoids collapsevan den Oord (2017)

Free bits formula — Implementation is extremely simple: kl_per_dim = max(kl_per_dim, λ). Intuition: guarantee a baseline of λ bits of information per latent dim, the optimizer cannot push it below 0. Common values λ0.5\lambda \approx 0.5-22 nats / dim.

§6 VQ-VAE: Discrete Latent + Codebook + STE

6.1 Structure (van den Oord, Vinyals, Kavukcuoglu, NeurIPS 2017)


x ──Encoder──> z_e(x) ∈ R^{H'×W'×D}    # continuous spatial map
                       │
                       │   For each spatial position (h,w), find nearest codebook vector
                       │   k_{hw} = argmin_k ‖z_e(x)_{hw} - e_k‖²
                       ↓
            z_q(x)_{hw} = e_{k_{hw}}    # quantized spatial map (discrete code)
                       │
                       │
                       ↓
                   Decoder ──> x̂

Codebook E={e1,,eK}RD\mathcal{E} = \{e_1, \ldots, e_K\} \subset \mathbb{R}^D, learnable. ze(x)z_e(x) and zq(x)z_q(x) have the same shape, but each spatial position of zqz_q is a copy of some codebook vector (discrete index khwk_{hw}).

6.2 Loss derivation

VQ-VAE does not learn a stochastic posterior q(zx)q(z|x) (unlike VAE), but uses deterministic nearest neighbor for the "quantization" zezqz_e \to z_q. The loss has three parts:

  LVQ-VAE=xx^2reconstruction+sg[ze(x)]e2codebook+βze(x)sg[e]2commitment  \boxed{\;\mathcal{L}_\text{VQ-VAE} = \underbrace{\|x - \hat{x}\|^2}_{\text{reconstruction}} + \underbrace{\|\text{sg}[z_e(x)] - e\|^2}_{\text{codebook}} + \beta \underbrace{\|z_e(x) - \text{sg}[e]\|^2}_{\text{commitment}}\;}

Meaning of each term:

  • Reconstruction: end-to-end reconstruction xzezqx^x \to z_e \to z_q \to \hat{x} (note gradient passes through quantization via STE).
  • Codebook loss: pull codebook vector ee toward ze(x)z_e(x), gradient only updates ee (use sg to block gradient on zez_e, otherwise both codebook and encoder get pulled, direction unclear).
  • Commitment loss: pull encoder output ze(x)z_e(x) toward codebook vector ee, gradient only updates encoder, weight β\beta (paper uses β=0.25\beta = 0.25).

sg[·] = stop_gradient (PyTorch's .detach()), defined: forward sg[u]=u\text{sg}[u] = u, backward sg[u]=0\nabla \text{sg}[u] = 0.

💡 Why both codebook and commitment need sg — If neither has sg, zee2\|z_e - e\|^2 pulls both sides simultaneously, with coupled directions easily oscillating. Split this into two sg versions: codebook term updates ee exclusively, commitment exclusively updates encoder, learning rate / speed can be decoupled. This is standard practice in the vector quantization literature (also called "alternating minimization").

6.3 Straight-Through Estimator (STE) derivation

Problem: zq=eargminkzeek2z_q = e_{\arg\min_k \|z_e - e_k\|^2}'s argmin is non-differentiable (outputs discrete index).

STE solution:

  • Forward: as usual zq=ekz_q = e_k (discrete)
  • Backward: directly treat Lzq\frac{\partial \mathcal{L}}{\partial z_q} as Lze\frac{\partial \mathcal{L}}{\partial z_e} and backprop to encoder

PyTorch implementation trick (classic three lines):

z_q = z_e + (z_q_quantized - z_e).detach()

Forward: z_q = z_e + (z_q_q - z_e) = z_q_q ✓ (quantized value) Backward: (z_q_q - z_e).detach() does not participate in gradient, so dz_q/dz_e = 1, gradient flows straight through to encoder ✓

⚠️ STE's equivalent surrogate — STE is equivalent to replacing the non-differentiable zq=quantize(ze)z_q = \text{quantize}(z_e) with a differentiable surrogate zqsurrogate=zez_q^\text{surrogate} = z_e for backprop — i.e. "assume quantization is the identity map." This is a biased estimate (biased gradient estimator), but works well in practice; theoretical analysis in Bengio et al. (2013) "Estimating or Propagating Gradients Through Stochastic Neurons."

6.4 Complete VQ-VAE implementation

class VectorQuantizer(nn.Module): """ Codebook + nearest-neighbor quantization + STE embedding_dim = D, num_embeddings = K commitment_cost β usually = 0.25 """ def __init__(self, num_embeddings: int, embedding_dim: int, commitment_cost: float = 0.25): super().__init__() self.K, self.D = num_embeddings, embedding_dim self.beta = commitment_cost # Codebook small uniform init self.codebook = nn.Embedding(self.K, self.D) self.codebook.weight.data.uniform_(-1.0 / self.K, 1.0 / self.K) def forward(self, z_e: torch.Tensor): """ z_e: [B, D, H, W] -> z_q: [B, D, H, W], indices: [B, H, W], loss = codebook_loss + β·commitment_loss """ # 1) Reshape: [B, D, H, W] -> [BHW, D] B, D, H, W = z_e.shape z_e_flat = z_e.permute(0, 2, 3, 1).contiguous().view(-1, D) # [BHW, D] # 2) Compute L2 distance ‖z_e - e_k‖² = ‖z_e‖² + ‖e_k‖² - 2 z_e · e_k e = self.codebook.weight # [K, D] dist = (z_e_flat.pow(2).sum(1, keepdim=True) + e.pow(2).sum(1) - 2 * z_e_flat @ e.t()) # [BHW, K] # 3) Nearest neighbor index indices = dist.argmin(dim=1) # [BHW] z_q_flat = self.codebook(indices) # [BHW, D] # 4) Loss (mind the sg) codebook_loss = F.mse_loss(z_q_flat, z_e_flat.detach()) commitment_loss = F.mse_loss(z_e_flat, z_q_flat.detach()) vq_loss = codebook_loss + self.beta * commitment_loss # 5) STE: forward z_q, backward dz_q/dz_e = I z_q_flat = z_e_flat + (z_q_flat - z_e_flat).detach() # 6) Reshape back to [B, D, H, W] z_q = z_q_flat.view(B, H, W, D).permute(0, 3, 1, 2).contiguous() indices = indices.view(B, H, W) # 7) (optional) perplexity: measure of codebook usage one_hot = F.one_hot(indices.view(-1), self.K).float() avg_probs = one_hot.mean(dim=0) perplexity = torch.exp(-(avg_probs * torch.log(avg_probs + 1e-10)).sum()) return z_q, indices, vq_loss, perplexity class VQVAE(nn.Module): def __init__(self, channels=3, hidden=128, num_embeddings=512, embedding_dim=64, commitment_cost=0.25): super().__init__() # Encoder: 64×64×3 -> 16×16×D (downsample 4×) self.encoder = nn.Sequential( nn.Conv2d(channels, hidden, 4, 2, 1), nn.ReLU(), nn.Conv2d(hidden, hidden, 4, 2, 1), nn.ReLU(), nn.Conv2d(hidden, embedding_dim, 3, 1, 1), ) self.quantizer = VectorQuantizer(num_embeddings, embedding_dim, commitment_cost) # Decoder: 16×16×D -> 64×64×3 self.decoder = nn.Sequential( nn.ConvTranspose2d(embedding_dim, hidden, 3, 1, 1), nn.ReLU(), nn.ConvTranspose2d(hidden, hidden, 4, 2, 1), nn.ReLU(), nn.ConvTranspose2d(hidden, channels, 4, 2, 1), ) def forward(self, x): z_e = self.encoder(x) z_q, indices, vq_loss, perplexity = self.quantizer(z_e) x_hat = self.decoder(z_q) return x_hat, vq_loss, perplexity, indices def vqvae_loss(x, x_hat, vq_loss): recon = F.mse_loss(x_hat, x) return recon + vq_loss, recon

6.5 EMA Codebook (production standard)

Directly using codebook loss to update ee converges slowly with many dead codes (codebook vectors never selected). Production implementation uses EMA (Exponential Moving Average) update:

For each codebook vector eke_k, maintain:

  • Nk(t)=γNk(t1)+(1γ)nk(t)N_k^{(t)} = \gamma N_k^{(t-1)} + (1-\gamma) n_k^{(t)}, where nk(t)n_k^{(t)} is the count of samples assigned to eke_k in the current batch
  • mk(t)=γmk(t1)+(1γ)i:ze,iekze,im_k^{(t)} = \gamma m_k^{(t-1)} + (1-\gamma) \sum_{i: z_{e,i} \to e_k} z_{e,i}

Update:

ek(t)=mk(t)Nk(t)+ε(Laplace smoothing)e_k^{(t)} = \frac{m_k^{(t)}}{N_k^{(t)} + \varepsilon} \quad \text{(Laplace smoothing)}

class VectorQuantizerEMA(nn.Module): """ EMA codebook update (van den Oord 2017 follow-up / VQ-VAE-2 standard practice) - codebook does not rely on gradients, but on running EMA updates - commitment loss is kept for the encoder - decay γ is typically 0.99, ε typically 1e-5 """ def __init__(self, num_embeddings: int, embedding_dim: int, commitment_cost: float = 0.25, decay: float = 0.99, eps: float = 1e-5): super().__init__() self.K, self.D = num_embeddings, embedding_dim self.beta, self.decay, self.eps = commitment_cost, decay, eps embed = torch.randn(num_embeddings, embedding_dim) * 0.01 self.register_buffer("codebook", embed) self.register_buffer("cluster_size", torch.zeros(num_embeddings)) self.register_buffer("embed_avg", embed.clone()) def forward(self, z_e): B, D, H, W = z_e.shape z_e_flat = z_e.permute(0, 2, 3, 1).contiguous().view(-1, D) dist = (z_e_flat.pow(2).sum(1, keepdim=True) + self.codebook.pow(2).sum(1) - 2 * z_e_flat @ self.codebook.t()) indices = dist.argmin(dim=1) # [BHW] z_q_flat = F.embedding(indices, self.codebook) # [BHW, D] if self.training: # EMA update with torch.no_grad(): one_hot = F.one_hot(indices, self.K).float() # [BHW, K] cluster_size_new = one_hot.sum(dim=0) # [K] embed_sum = one_hot.t() @ z_e_flat # [K, D] self.cluster_size.mul_(self.decay).add_(cluster_size_new, alpha=1 - self.decay) self.embed_avg.mul_(self.decay).add_(embed_sum, alpha=1 - self.decay) # Laplace smoothing to avoid division by zero n = self.cluster_size.sum() cluster_size = (self.cluster_size + self.eps) / (n + self.K * self.eps) * n self.codebook.copy_(self.embed_avg / cluster_size.unsqueeze(1)) commitment_loss = F.mse_loss(z_e_flat, z_q_flat.detach()) vq_loss = self.beta * commitment_loss # under EMA, no codebook loss z_q_flat = z_e_flat + (z_q_flat - z_e_flat).detach() # STE z_q = z_q_flat.view(B, H, W, D).permute(0, 3, 1, 2).contiguous() return z_q, indices.view(B, H, W), vq_loss

Two benefits of EMA

  • Updates more stable: EMA is an implicit momentum, equivalent to codebook's SGD using a large batch
  • Dead-code automatic restart is easier: can periodically reset eke_k with cluster_size<τ\text{cluster\_size} < \tau to some zez_e in the current batch (dead-code revival)

6.6 VQ-VAE-2 (Razavi, Vinyals, van den Oord, NeurIPS 2019)

Hierarchical extension of VQ-VAE:

  • Top-level latent ztz_t: low resolution (e.g. 32×32), capturing global structure (overall face pose, identity)
  • Bottom-level latent zbz_b: high resolution (e.g. 64×64), capturing local details (skin texture, hair strands)
  • PixelCNN prior trained on both layers, top-level unconditional, bottom-level conditioned on top

On ImageNet 256×256, the first time a VQ-based method approached BigGAN's sample quality; the direct predecessor to VQ-GAN.

§7 VQ-GAN: Adversarial + Perceptual + Transformer Prior

7.1 Core idea (Esser, Rombach, Ommer, CVPR 2021, "Taming Transformers")

VQ-VAE reconstruction on ImageNet has blurry texture details. VQ-GAN transforms to:

ComponentVQ-VAEVQ-GAN
Recon lossL2 / L1 pixelL1 pixel + LPIPS perceptual + PatchGAN adversarial
PriorPixelCNNTransformer (decoder-only) over code tokens
Codebook512-1024 codes1024-16384 codes
Compression4×-8×8×-32× (higher compression, relying on perceptual + adversarial to save quality)
Applicationunconditional / class-cond generationhigh-res image synthesis, "Taming Transformers"

7.2 Loss formula

LVQ-GANstage1=Lrec+LVQ+λLGAN\mathcal{L}_\text{VQ-GAN}^\text{stage1} = \mathcal{L}_\text{rec} + \mathcal{L}_\text{VQ} + \lambda \cdot \mathcal{L}_\text{GAN}

where:

Lrec=xx^1+LLPIPS(x,x^)LVQ=sg[ze]e2+βzesg[e]2\begin{aligned} \mathcal{L}_\text{rec} &= \|x - \hat{x}\|_1 + \mathcal{L}_\text{LPIPS}(x, \hat{x}) \\ \mathcal{L}_\text{VQ} &= \|\text{sg}[z_e] - e\|^2 + \beta \|z_e - \text{sg}[e]\|^2 \end{aligned}

GAN term in generator/tokenizer stage (only backprops to generator's output, discriminator is updated separately in another stage):

LGAN(G)=Ex^[logD(x^)](non-saturating)orLGAN(G)=Ex^[D(x^)](hinge)\mathcal{L}_\text{GAN}^{(G)} = -\mathbb{E}_{\hat{x}}[\log D(\hat{x})]\quad\text{(non-saturating)}\quad\text{or}\quad \mathcal{L}_\text{GAN}^{(G)} = -\mathbb{E}_{\hat{x}}[D(\hat{x})]\quad\text{(hinge)}

Discriminator's own minimax term (independent step updating DD):

LGAN(D)=Ex[min(0,1+D(x))]Ex^[min(0,1D(x^))](hinge)\mathcal{L}_\text{GAN}^{(D)} = -\mathbb{E}_x[\min(0, -1+D(x))] - \mathbb{E}_{\hat{x}}[\min(0, -1-D(\hat{x}))]\quad\text{(hinge)}

Adaptive λ\lambda (paper novelty, using gradient norm ratio of the last layer for auto-balancing, avoiding manual tuning):

λ=GLLrecGLLGAN(G)+δ\lambda = \frac{\lVert\nabla_{G_L} \mathcal{L}_\text{rec}\rVert}{\lVert\nabla_{G_L} \mathcal{L}_\text{GAN}^{(G)}\rVert + \delta}

GLG_L is the last layer of the decoder; \lVert\cdot\rVert is the Frobenius norm. Total generator loss:

LG=Lrec+LVQ+λLGAN(G)\mathcal{L}_G = \mathcal{L}_\text{rec} + \mathcal{L}_\text{VQ} + \lambda \cdot \mathcal{L}_\text{GAN}^{(G)}

7.3 Stage 2: Transformer Prior

Stage 1 trains the VQ-GAN, converting the image into a token grid c=(c1,,cHW)\mathbf{c} = (c_1, \ldots, c_{HW}) (raster-scan flattened). Stage 2 trains a decoder-only Transformer on the token sequence, standard AR:

p(c)=i=1HWp(cic<i)p(\mathbf{c}) = \prod_{i=1}^{HW} p(c_i | c_{<i})

Sampling: AR sample tokens → VQ-GAN decoder → image. This is the standard paradigm of translating "image generation" into "language model"; DALL·E / Parti / Muse all evolve from this idea.

💡 VQ-GAN's role in LDM — Stable Diffusion's VAE tokenizer is actually the continuous-latent variant of a KL-regularized VQ-GAN (drops quantization, keeps KL + perceptual + adversarial), output is a continuous latent map (4 channels, downsampled 8×). Diffusion runs in this latent, and finally decoder restores it. Can be understood as "VQ-GAN encoder/decoder + continuous latent + KL."

7.4 PatchGAN Discriminator (production architecture)

VQ-GAN uses PatchGAN (Isola et al. CVPR 2017 "pix2pix"):

  • Does not output a single scalar real/fake
  • Outputs a N×N patch-level discriminator map (each patch is 70×70 receptive field)
  • Suitable for capturing local texture realness, low pressure on global structure (letting the generator focus on textures)
class PatchDiscriminator(nn.Module): """ PatchGAN: stack of strided convs with 70×70 receptive field Output [B, 1, H/8, W/8] patch-level real/fake decision """ def __init__(self, in_ch=3, hidden=64, n_layers=3): super().__init__() layers = [nn.Conv2d(in_ch, hidden, 4, 2, 1), nn.LeakyReLU(0.2, True)] ch = hidden for i in range(1, n_layers): ch_next = min(hidden * (2 ** i), 512) layers += [ nn.Conv2d(ch, ch_next, 4, 2, 1), nn.BatchNorm2d(ch_next), nn.LeakyReLU(0.2, True), ] ch = ch_next layers += [ nn.Conv2d(ch, ch * 2, 4, 1, 1), nn.BatchNorm2d(ch * 2), nn.LeakyReLU(0.2, True), nn.Conv2d(ch * 2, 1, 4, 1, 1), ] self.net = nn.Sequential(*layers) def forward(self, x): return self.net(x) def hinge_d_loss(real_logits, fake_logits): real = F.relu(1.0 - real_logits).mean() fake = F.relu(1.0 + fake_logits).mean() return 0.5 * (real + fake) def hinge_g_loss(fake_logits): return -fake_logits.mean()

⚠️ GAN training trick list

  • D start delay: only train G for the first K steps (letting recon converge first)
  • LeCam regularization: anchor D's output to EMA, mitigating mode collapse
  • R1 gradient penalty: γxD(x)2\gamma \|\nabla_x D(x)\|^2 to prevent D overfitting
  • Spectral norm: stabilizes D
  • Adam β1=0.5\beta_1 = 0.5 (not the default 0.9), β2=0.9\beta_2 = 0.9

7.5 LPIPS (Perceptual Loss)

LLPIPS(x,x^)=lwlϕl(x)ϕl(x^)2\mathcal{L}_\text{LPIPS}(x, \hat{x}) = \sum_l w_l \cdot \|\phi_l(x) - \phi_l(\hat{x})\|^2

ϕl\phi_l is the ll-th layer feature map of pretrained VGG / AlexNet, wlw_l is the learned channel-wise weight (Zhang et al. CVPR 2018). Closer to human perception than pixel-MSE; standard for VQ-GAN / SD / most image GAN / diffusion training.

§8 Discrete VAE and Gumbel-Softmax

8.1 dVAE (DALL·E 1, Ramesh et al. ICML 2021)

DALL·E uses dVAE (discrete VAE) as image tokenizer:

  • Each spatial position outputs a categorical distribution over 8192 codes
  • Training uses Gumbel-softmax to make categorical differentiable
  • Inference uses hard argmax for discretization

8.2 Gumbel-Softmax derivation

Goal: make categorical sampling differentiable.

Gumbel-Max trick: for logits π=(π1,,πK)\pi = (\pi_1, \ldots, \pi_K) add independent Gumbel(0,1) noise gk=log(loguk),ukU(0,1)g_k = -\log(-\log u_k), u_k \sim \mathcal{U}(0, 1), then:

argmaxk{πk+gk}\arg\max_k \{\pi_k + g_k\}

follows categorical(softmax(π\pi)). Proof uses Gumbel CDF property: P(maxkXk=Xj)=eπj/keπkP(\max_k X_k = X_j) = e^{\pi_j} / \sum_k e^{\pi_k}.

Gumbel-softmax (Jang et al., ICLR 2017; Maddison et al., ICLR 2017 concurrent): replace the non-differentiable argmax with softmax:

  yk=exp((πk+gk)/τ)jexp((πj+gj)/τ)  \boxed{\;y_k = \frac{\exp((\pi_k + g_k) / \tau)}{\sum_j \exp((\pi_j + g_j) / \tau)}\;}

  • τ0\tau \to 0: yy approaches one-hot (close to categorical sampling)
  • τ\tau \to \infty: yy approaches uniform (good gradients but deviation)
  • During training τ\tau is annealed: 1.00.11.0 \to 0.1 progressive decrease

Straight-Through Gumbel-Softmax: forward uses argmax (discrete), backward uses softmax gradient — same idea as VQ-VAE's STE.

def gumbel_softmax_sample(logits, tau=1.0, hard=False, dim=-1): """ Input logits = log π Output soft / hard one-hot """ # 1) Add Gumbel noise g = -torch.log(-torch.log(torch.rand_like(logits) + 1e-20) + 1e-20) y_soft = F.softmax((logits + g) / tau, dim=dim) if not hard: return y_soft # ST: forward hard, backward soft gradient index = y_soft.argmax(dim=dim, keepdim=True) y_hard = torch.zeros_like(y_soft).scatter_(dim, index, 1.0) y = y_hard - y_soft.detach() + y_soft # straight-through return y

💡 VQ-VAE vs Gumbel-softmax / dVAE — Both are discrete latent models, differences:

  • VQ-VAE: encoder outputs continuous zez_e, nearest-neighbor to codebook (hard, no randomness); STE for backprop.
  • Gumbel dVAE: encoder outputs categorical distribution (logits over K codes), training uses Gumbel-softmax sampling.
  • Practice: DALL·E 1 used dVAE with AR Transformer prior; later DALL·E 2 / Parti / Muse all lean toward VQ-GAN series (better quality).

8.3 MaskGIT (Chang et al., CVPR 2022)

Replace AR Transformer prior with BERT-style masked Transformer:

  • Training: randomly mask a portion of VQ tokens, have model predict masked tokens (similar to BERT MLM)
  • Sampling: Non-autoregressive parallel sampling — each round unmask a batch of tokens, iterating 8-12 rounds to converge
  • About 10x faster than AR, comparable or better quality (on ImageNet 256×256)

Successor: MUSE (Chang et al., 2023) extends the same idea to text-to-image, one of Google's main generative models.

§9 FSQ: Finite Scalar Quantization (focus)

9.1 Motivation (Mentzer, Minnen, Agustsson, Toderici, ICLR 2024)

VQ-VAE has persistent problems:

  1. Codebook collapse / underuse: most codes never used (dead codes), perplexity far below theoretical KK
  2. STE bias: gradient estimate is biased, training unstable
  3. Complex loss balancing: commitment weight, EMA decay, dead code revival all need tuning
  4. Effective codebook size ceiling: practical limit ~10310^3-10410^4, larger fails

FSQ bypasses this with one trick: per-dimension scalar quantization (scalar quantization, not vector quantization).

9.2 Core formula (must derive)

Let encoder output zRdz \in \mathbb{R}^d. Independently for each dim, do scalar quantization (FSQ paper Eq. 4):

zizi=Li12tanh(zi)siz^i=round(zi)+siz_i \longrightarrow z'_i = \tfrac{L_i-1}{2}\tanh(z_i) - s_i \longrightarrow \hat{z}_i = \text{round}(z'_i) + s_i

where si=0s_i = 0 if LiL_i is odd, si=0.5s_i = 0.5 if LiL_i is even. So:

  • LiL_i odd (e.g. 5): z^i{2,1,0,1,2}\hat{z}_i \in \{-2,-1,0,1,2\} (exactly LiL_i integer levels)
  • LiL_i even (e.g. 8): z^i{3.5,2.5,,2.5,3.5}\hat{z}_i \in \{-3.5,-2.5,\ldots,2.5,3.5\} (exactly LiL_i half-integer levels)

Regardless of parity, each dim has LiL_i levels; multiply the level counts of all dimensions:

  Kimplicit=i=1dLi  \boxed{\;K_\text{implicit} = \prod_{i=1}^{d} L_i\;}

Implicit codebook size examples

  • L=(8,6,5)L = (8, 6, 5), d=3d = 3: codebook = 8×6×5=2408 \times 6 \times 5 = 240
  • L=(8,5,5,5)L = (8, 5, 5, 5), d=4d = 4: codebook = 8×5×5×5=10008 \times 5 \times 5 \times 5 = 1000
  • L=(7,5,5,5,5)L = (7, 5, 5, 5, 5), d=5d = 5: codebook = 7×54=43757 \times 5^4 = 4375
  • L=(8,8,8,5,5,5)L = (8, 8, 8, 5, 5, 5), d=6d = 6: codebook = 8353=64,0008^3 \cdot 5^3 = 64{,}000
  • No explicit codebook table: the (L1,,Ld)(L_1, \ldots, L_d) combinations of z^i\hat{z}_i are themselves the discrete codes (directly use base-mixed encoding to convert to 1,,Kimplicit1, \ldots, K_\text{implicit}).

9.3 Why doesn't FSQ have codebook collapse?

Key insight — In VQ-VAE, the root cause of codebook collapse is: the codebook is a free parameter, the optimizer lets most eke_k drift to useless regions, and only a few eke_k are used repeatedly. FSQ's "codebook" is not a parameter — it is fixed grid points on the number axis ({L/2,,L/2}\{-L/2, \ldots, L/2\}).

  • No codebook parameter → no codebook collapse: grid points are fixed, can't drift.
  • Per-dim independence → high-dim code automatically diversifies through product: even with each dim using L=8L=8 levels, d=6d=6 gives 86=2621448^6 = 262{144} combinations.
  • Encoder self-adapts distribution: with tanh\tanh pre-compressing to [1,1][-1, 1], the encoder naturally spreads its output over [L/2,L/2][-L/2, L/2] — the only reason dead codes appear is if the encoder doesn't use some grid intervals, but as long as reconstruction drives the encoder to explore the full interval, all grids get covered.

Empirical: FSQ's codebook usage is nearly 100% (compared to VQ-VAE's 50-70%), this conclusion is reproduced on ImageNet / Cosmos / OpenMagViT2.

9.4 Why doesn't FSQ need "explicit STE wrapping" and why is its loss minimal

  • Rounding is non-differentiable — like VQ-VAE, it needs some kind of STE. But FSQ's STE is just one line x_hat = x + (round(x) - x).detach(), no codebook loss / commitment loss / EMA / dead-code revival.
  • Loss is only reconstruction:

LFSQ=xx^2(plus optional perceptual + adversarial)\mathcal{L}_\text{FSQ} = \|x - \hat{x}\|^2 \quad \text{(plus optional perceptual + adversarial)}

  • No hyperparameter tuning for commitment cost / EMA decay / restart threshold — this is FSQ's biggest engineering advantage over VQ-VAE.

💡 Simplified VQ-VAE vs FSQ comparison — FSQ "trades spatial dimensions for codebook size": VQ-VAE uses 1 dim (DD continuous values + 1 discrete choice from KK), FSQ uses dd independent discrete dims with LiL_i levels each; the final discrete entropy is actually larger and collapse nearly impossible. Cost: embedding expressiveness is slightly weaker (each dim independent, not sharing representation), but reconstruction-side decoder compensates.

9.5 FSQ implementation (10 lines)

class FSQ(nn.Module): """ Finite Scalar Quantization (Mentzer et al., ICLR 2024) levels: tuple, number of quantization levels per dim (odd or even both work; odd guarantees 0 is included in the grid) eps: bounding safety margin, avoiding round jumping out of grid after tanh """ def __init__(self, levels=(8, 5, 5, 5)): super().__init__() levels_t = torch.tensor(levels, dtype=torch.float32) self.levels = levels_t self.d = len(levels) self.K = int(torch.prod(levels_t).item()) # implicit codebook size = ∏ L_i # FSQ paper Eq. 4: half = (L-1)/2; shift = 0.5 if L even else 0 half = (levels_t - 1) / 2 # [d] shift = ((levels_t % 2) == 0).float() * 0.5 # [d] self.register_buffer("half_l", half) self.register_buffer("shift", shift) # mixed-radix basis for token id encoding cumprod = torch.tensor([1.0] + list(torch.cumprod(levels_t[:-1], dim=0)), dtype=torch.float32) self.register_buffer("basis", cumprod) # [d] @staticmethod def round_ste(z): """STE for non-differentiable round: forward round, backward identity""" return z + (z.round() - z).detach() def forward(self, z): """ z: [B, d, ...] -> z_hat: [B, d, ...] (quantized values), codes: [B, ...] (∈ 0..K-1) """ view = (1, -1) + (1,) * (z.dim() - 2) half = self.half_l.view(*view).to(z.device) shift = self.shift.view(*view).to(z.device) # 1) Bound: tanh(z) * half - shift → z'∈[-half-shift, half-shift] z_bounded = torch.tanh(z) * half - shift # 2) Round (STE) + add back shift → odd L gives {-half,…,half} (integers), even L gives {-half,…,half} (half-integers) z_hat = self.round_ste(z_bounded) + shift # 3) Token ID (mixed-radix): map each d-dim ∈ {-half_i,…,half_i} to 0..L_i-1 then encode as single index shifted = (z_hat + half).round().long() # ∈ 0..L_i-1 (round to handle floating-point error) basis = self.basis.view(*view).to(z.device).long() codes = (shifted * basis).sum(dim=1) # [B, ...] return z_hat, codes # Usage example: # fsq = FSQ(levels=(8, 5, 5, 5)) # K = 8·5·5·5 = 1000 # z = encoder(x) # [B, 4, H, W] # z_hat, tokens = fsq(z) # z_hat: [B, 4, H, W], tokens: [B, H, W] ∈ 0..999 # x_hat = decoder(z_hat) # loss = F.mse_loss(x_hat, x) # That's the only term!

⚠️ FSQ level selection experience

  • Paper Table 3 gives experiential recipes (ImageNet 256×256): for K1000K \approx 1000 use (8,5,5,5)(8, 5, 5, 5); for K4000K \approx 4000 use (7,5,5,5,5)(7, 5, 5, 5, 5); for K64000K \approx 64000 use (8,8,8,5,5,5)(8, 8, 8, 5, 5, 5)
  • Rule of thumb: make LiL_i ratios approximately golden-ratio / inverse-proportion (information-theoretically each dim has balanced information)
  • Not very sensitive in practice — any reasonable level combination works

9.6 LFQ: Lookup-Free Quantization (MAGVIT-v2, Yu et al., ICLR 2024)

A binary special case of FSQ:

LFQ(z)=sign(z){1,+1}d\text{LFQ}(z) = \text{sign}(z) \in \{-1, +1\}^d

Only 2 levels per dim, implicit codebook = 2d2^d: with d=18d=18, codebook = 218=2621442^{18} = 262{144} (same order as FSQ-equivalent).

Features:

  • Per-dim binary, simplest structure
  • VQ-token to binary code, trained with BitVQ / bitwise predictor
  • MAGVIT-v2 / Open-MAGVIT2 / VideoPoet use LFQ as video tokenizer
  • Add entropy regularization to maintain 50/50 per bit (avoiding some bits always being +1+1)
class LFQ(nn.Module): """ Lookup-Free Quantization (MAGVIT-v2) Per-dim independent sign quantize, implicit codebook = 2^d """ def __init__(self, dim: int, entropy_weight: float = 0.1): super().__init__() self.d = dim self.K = 2 ** dim self.entropy_weight = entropy_weight def forward(self, z): # z: [B, d, ...] q = torch.sign(z) # Avoid sign(0) = 0 q = torch.where(q == 0, torch.ones_like(q), q) # STE z_hat = z + (q - z).detach() # Entropy regularization (prevents some dim from always having same sign) # p_+ = sigmoid(z), p_- = 1 - p_+ if self.training: p = torch.sigmoid(z) per_dim_entropy = -(p * torch.log(p + 1e-9) + (1 - p) * torch.log(1 - p + 1e-9)) entropy_loss = -per_dim_entropy.mean() # maximize entropy → minimize -H else: entropy_loss = z.new_tensor(0.0) return z_hat, self.entropy_weight * entropy_loss

9.7 Cosmos / OpenMagViT2 / modern video tokenizers

TokenizerSourceQuantizationUsed in
MAGVIT-v2Google 2024 (ICLR)LFQEarly text-to-video demo
OpenMagViT2Open reproduction 2024LFQPublic video tokenizer baseline
Cosmos TokenizerNVIDIA 2024FSQ + video spatiotemporal compressionNVIDIA Cosmos world model
VideoPoet tokenizerGoogle 2024LFQ-styletext-to-video

Engineering essentials:

  • Joint spatiotemporal compression: spatial 8× + temporal 4× (4 frames merged into 1 token plane)
  • 3D causal CNN encoder (forward time causality, can stream-encode long videos)
  • Cross-resolution generalization: training on 256×256, inference on 1024×1024 requires careful test-time adaptation

§10 Complexity and Resource Comparison

ModelLatent typeTrain parameters (encoder+decoder)Main lossCodebook collapseSTE dependency
VAEcontinuous Gaussian\sim10-100Mrecon + KL (closed form)N/ANo (reparameterization)
β\beta-VAEcontinuous GaussianSame as VAErecon + β\beta·KLN/ANo
NVAEhierarchical continuous80M-200Mrecon + multi-layer KLN/ANo
VQ-VAEdiscrete via codebook50-200Mrecon + codebook + β\beta·commitmentFrequently occursYes
VQ-VAE-2hierarchical discrete100-500MSame as VQ-VAE × 2 layersSame as aboveYes
VQ-GANdiscrete + adversarial50-300M (+ D)recon + LPIPS + GAN + codebook + commitmentSame as aboveYes
dVAEcategorical (logits)50-200Mrecon + KL to uniformRare (categorical distribution learning)No (Gumbel-softmax backprops soft)
FSQscalar quantize per dim30-150Mrecon (+ perceptual)Almost never occursYes (but minimal)
LFQbinary scalar quantize30-150Mrecon (+ entropy reg)Almost never occursYes

💡 Ecological niche positioning

  • Want diffusion / FM: use KL-VAE / SD VAE (continuous latent)
  • Want AR generation (GPT-style image token): use VQ-GAN / FSQ / LFQ
  • Want MaskGIT / Muse / parallel decode: use VQ-GAN / FSQ
  • Want video / long sequence: use FSQ / LFQ (high codebook usage, no collapse)

11.1 VAE vs GAN vs Diffusion vs Flow / FM

ModelLikelihoodTraining stabilityDiversitySample qualityInference speed
VAEYes (ELBO)✅ Stable✅ Good⚠️ Blurry✅ 1-step
GANNone❌ Hard❌ Mode collapse✅ Sharp✅ 1-step
DiffusionApproximate (VLB)✅ Stable✅ Good✅ SOTA❌ Many NFE
Flow / FMYes (ODE)✅ Stable✅ Good✅ Strong⚠️ Several NFE

11.2 Role of tokenizer series in large models


Tokenizer Stage 1                Generative Stage 2 (prior)
────────────────                 ──────────────────────────
VQ-GAN  →   discrete token grid  →   Transformer AR  (Parti, DALL·E 1, Cogview)
VQ-GAN  →   discrete token grid  →   Masked Transformer (MaskGIT, Muse)
FSQ    →   discrete token grid  →   Transformer AR  (Cosmos, OpenMagViT2)
LFQ    →   binary token grid →   AR / bit predictor (MAGVIT-v2, VideoPoet)
KL-VAE →   continuous latent map  →   Diffusion / Flow Matching (LDM, SD, SD3, FLUX)

11.3 Reconstruction-Perception Tradeoff (advanced question)

Blau & Michaeli (CVPR 2018) proved: between reconstruction (MSE / PSNR) and perception (perceptual / FID) there is a strict Pareto boundary. VQ-GAN / SD VAE introduces LPIPS + adversarial to trade higher perceptual quality for slightly worse PSNR.

⚠️ PSNR doesn't equal "looks good" — VQ-GAN paper's PSNR is not necessarily better than VQ-VAE, but perceptual (LPIPS / FID) is much better. In interviews, often asked "why does SOTA tokenizer have lower PSNR" — this is distortion-perception tradeoff.

§12 25 Frequently-Asked Interview Questions

Listed from the perspective of a top-lab interviewer by codex (gpt-5.5 xhigh), divided into 3 tiers by difficulty. Each question expands to answer points + common pitfalls.

L1 Must-Know (any ML engineering position will ask)

Q1. What is the ELBO of VAE? Write the formula.
  • logp(x)Eqϕ(zx)[logpθ(xz)]DKL(qϕ(zx)p(z))\log p(x) \geq \mathbb{E}_{q_\phi(z|x)}[\log p_\theta(x|z)] - D_\text{KL}(q_\phi(z|x)\,\|\,p(z))

  • First term: reconstruction expected log-likelihood

  • Second term: KL pulls posterior toward prior N(0,I)\mathcal{N}(0, I)

  • Gap between ELBO and logp(x)\log p(x) = DKL(qϕ(zx)pθ(zx))D_\text{KL}(q_\phi(z|x)\,\|\,p_\theta(z|x))

Pitfalls: writing logp(xz)DKL(...)\log p(x|z) - D_\text{KL}(...) (missing the expectation symbol); only saying "recon + regularization" without writing the formula.

Q2. What problem does the reparameterization trick solve?
  • Direct sampling zqϕ(zx)z \sim q_\phi(z|x) is non-differentiable, gradient cannot reach encoder

  • Rewrite as z=μ+σϵ,ϵN(0,I)z = \mu + \sigma \odot \epsilon, \epsilon \sim \mathcal{N}(0, I), moving randomness to independent noise

  • zz becomes a deterministic function of ϕ\phi, can backprop normally

  • Not just Gaussian; Gumbel-softmax is the same idea

Pitfalls: saying "for speed-up" — actually it's for differentiability.

Q3. What does $\beta$-VAE's $\beta$ control?
  • β>1\beta > 1: stronger KL regularization, encourages disentangled latent

  • β<1\beta < 1: relaxes KL, more precise recon but worse prior fit

  • β=1\beta = 1: standard VAE

  • But Locatello 2019 proved: pure unsupervised disentanglement is infeasible, needs inductive bias

Pitfalls: only saying "larger β\beta → more disentanglement" — wrong, depends on data + architecture.

Q4. What is posterior collapse?
  • During training KL → 0, i.e. qϕ(zx)p(z)q_\phi(z|x) \approx p(z) (independent of xx)

  • Decoder completely ignores zz, VAE degenerates to unconditional model

  • Common with strong AR decoders (e.g. PixelCNN / LSTM) + simple data

Pitfalls: only saying "latent is useless," not mentioning the quantitative indicator KL → 0.

Q5. What is VQ-VAE's codebook? How is it used?
  • {e1,,eK}RD\{e_1, \ldots, e_K\} \subset \mathbb{R}^D a set of learnable "codebook" vectors

  • Encoder outputs continuous ze(x)z_e(x); use nearest neighbor k=argminzeek2k = \arg\min \|z_e - e_k\|^2 to replace with zq=ekz_q = e_k

  • Decoder decodes zqz_q back to pixels

  • Trained via codebook loss (pulls ee to zez_e) + commitment loss (pulls zez_e to ee)

Pitfalls: saying codebook is fixed / pretrained — wrong, learned end-to-end.

Q6. What are VQ-VAE's three loss terms?
  • Reconstruction: xx^2\|x - \hat{x}\|^2 (pixel-level)

  • Codebook loss: sg[ze]e2\|\text{sg}[z_e] - e\|^2 (only updates ee)

  • Commitment loss: βzesg[e]2\beta \|z_e - \text{sg}[e]\|^2 (only updates encoder, β=0.25\beta = 0.25)

  • sg = stop_gradient, avoids gradient coupling and oscillation between both sides

Pitfalls: mixing codebook and commitment loss into one; forgetting the direction of sg.

Q7. What is the Straight-Through Estimator (STE)?
  • Solves the backprop problem of non-differentiable operations (e.g. argmax / round)

  • Forward uses discrete output, backward directly passes gradient to "the previous continuous input layer"

  • Equivalent surrogate: assume quantization layer is identity map

  • PyTorch three lines: z_q = z_e + (z_q_quantized - z_e).detach()

Pitfalls: only saying "backward uses identity," not saying whether forward is real quantization.

Q8. What does VQ-GAN have over VQ-VAE?
  • LPIPS perceptual loss (replaces / supplements L2)

  • PatchGAN adversarial loss + adaptive λ\lambda weight

  • Transformer prior (replaces PixelCNN)

  • Larger codebook (1k → 16k) + higher compression (8× → 16-32×)

  • It is "Taming Transformers" (Esser et al. CVPR 2021)

Pitfalls: only saying "GAN" without perceptual; or forgetting Transformer prior.

Q9. What is FSQ? Why doesn't it have codebook collapse?
  • Per-dim independent scalar quantize to LL fixed levels (tanh\tanh → scale → round)

  • Implicit codebook = Li\prod L_i (e.g. L=8,d=686L=8, d=6 \Rightarrow 8^6)

  • Codebook is not a learnable parameter → nothing to "collapse" to useless region

  • Encoder naturally explores the full grid via reconstruction pressure

Pitfalls: treating FSQ as a VQ-VAE codebook optimization trick — wrong, FSQ has no explicit codebook parameter.

Q10. Write the closed form of KL($\mathcal{N}(\mu, \sigma^2 I) \,\|\, \mathcal{N}(0, I)$).
  • DKL=12i(μi2+σi2logσi21)D_\text{KL} = \tfrac{1}{2}\sum_i (\mu_i^2 + \sigma_i^2 - \log \sigma_i^2 - 1)

  • Only the diagonal covariance has this simple form

  • In implementation, encoder outputs logσ2\log \sigma^2 (logvar) for stability

  • kl = 0.5 * (mu**2 + logvar.exp() - logvar - 1).sum()

Pitfalls: confusing logσ2\log \sigma^2 with logσ\log \sigma (off by 2); or forgetting the "-1" term.

L2 Advanced (research-oriented positions)

Q11. Is IWAE tighter than ELBO? How to use it?
  • KK importance samples: LKIWAE=Ez1,,zK[log1Kkp(x,zk)q(zkx)]\mathcal{L}_K^\text{IWAE} = \mathbb{E}_{z_1,\ldots,z_K}[\log \tfrac{1}{K} \sum_k \tfrac{p(x, z_k)}{q(z_k|x)}]

  • L1=\mathcal{L}_1 = ELBO (special case)

  • LKlogp(x)\mathcal{L}_K \to \log p(x) as KK \to \infty (Burda et al. ICLR 2016)

  • But encoder's learned posterior no longer pursues approximation of the true posterior

Pitfalls: saying "K=1K = 1 is stronger than ELBO" — wrong, the special case is ELBO.

Q12. How to mitigate posterior collapse? List at least 4.
  • KL annealing: β(t)=min(1,t/T)\beta(t) = \min(1, t/T) linear growth (Bowman 2016)

  • Free bits: per-dim KL lower bound λ\lambda nats (Kingma 2016)

  • Weakened decoder: restrict decoder expressiveness (Chen 2017)

  • VQ-VAE / discrete latent: structurally no KL term, bypass

  • Others: auxiliary loss, word dropout, NF posterior, skip connections

Pitfalls: only answering "KL annealing" alone; or treating β\beta-VAE as a collapse mitigation tool (actually β>1\beta > 1 makes collapse easier).

Q13. Derive the closed form of KL($\mathcal{N}(\mu, \sigma^2) \,\|\, \mathcal{N}(0, 1))$.
  • logqp=12logσ2(zμ)22σ2+z22\log \tfrac{q}{p} = -\tfrac{1}{2}\log \sigma^2 - \tfrac{(z-\mu)^2}{2\sigma^2} + \tfrac{z^2}{2}

  • Take Eq\mathbb{E}_q (using E[z]=μ\mathbb{E}[z] = \mu, E[z2]=μ2+σ2\mathbb{E}[z^2] = \mu^2 + \sigma^2, E[(zμ)2]=σ2\mathbb{E}[(z-\mu)^2] = \sigma^2)

  • Result: 12(μ2+σ2logσ21)\tfrac{1}{2}(\mu^2 + \sigma^2 - \log \sigma^2 - 1)

  • Multi-dim independent case: sum over dims

Pitfalls: skipping the derivation and just memorizing the formula; forgetting the expansion of Eq[z2]\mathbb{E}_q[z^2].

Q14. Role of sg in VQ-VAE's codebook vs commitment terms.
  • Codebook loss sg[ze]e2\|\text{sg}[z_e] - e\|^2: gradient only updates ee (codebook vector), not encoder

  • Commitment loss zesg[e]2\|z_e - \text{sg}[e]\|^2: gradient only updates encoder, not ee

  • Two sg's decouple bidirectional alignment, avoiding mutual interference

  • Without both sg's, it's equivalent to ordinary MSE; effectively the learning rate doubles + both sides pull each other

Pitfalls: thinking sg is to "prevent codebook updates from going too fast" — actually it's for gradient decoupling.

Q15. STE's gradient is equivalent to what kind of surrogate?
  • STE = "forward real quantization, backward identity surrogate"

  • Equivalent to setting the differentiable surrogate of zq=quantize(ze)z_q = \text{quantize}(z_e) to zqsurr=zez_q^\text{surr} = z_e

  • I.e. assume quantization is the identity map

  • It is a biased estimate (biased gradient), but with low variance and works in practice

  • Rigorous analysis: Bengio et al. (2013) "Estimating or Propagating Gradients Through Stochastic Neurons"

Pitfalls: saying STE is unbiased — wrong, it is biased.

Q16. What is the EMA codebook update formula? Why use it?
  • Nk(t)=γNk(t1)+(1γ)nk(t)N_k^{(t)} = \gamma N_k^{(t-1)} + (1-\gamma) n_k^{(t)}, cluster count

  • mk(t)=γmk(t1)+(1γ)ikze,im_k^{(t)} = \gamma m_k^{(t-1)} + (1-\gamma) \sum_{i \to k} z_{e,i}, cluster vector sum

  • ek(t)=mk(t)/(Nk(t)+ε)e_k^{(t)} = m_k^{(t)} / (N_k^{(t)} + \varepsilon)

  • Pros: codebook updates more stable; can periodically revive dead codes

  • γ0.99,ε105\gamma \approx 0.99, \varepsilon \approx 10^{-5} are common values

Pitfalls: treating EMA as a momentum + Adam SGD variant — essentially it's k-means EMA estimation under mini-batches.

Q17. What is PatchGAN? Why use it in VQ-GAN?
  • Does not output a single scalar but an N×N patch-level real/fake map

  • Each patch is 70×70 receptive field (using stack of strided convs)

  • Suitable for capturing local texture realness, low pressure on global structure

  • Lets the generator focus on texture details rather than full-image discrimination (VQ-GAN's global relies on recon + LPIPS)

  • From Isola et al. CVPR 2017 "pix2pix"

Pitfalls: thinking PatchGAN is attention-based; or saying it's only used in image-to-image translation.

Q18. What is LPIPS? Advantages over MSE?
  • Compute distance using intermediate-layer features of pretrained VGG / AlexNet: lwlϕl(x)ϕl(x^)2\sum_l w_l \|\phi_l(x) - \phi_l(\hat{x})\|^2

  • wlw_l is the learned channel-wise weight (Zhang et al. CVPR 2018)

  • Closer to human perception than pixel-MSE

  • Standard for VQ-GAN / SD / most image GAN / diffusion training

  • Used with distortion-perception tradeoff (Blau & Michaeli CVPR 2018)

Pitfalls: only saying "uses VGG features," not learned channel weights / fit to human study.

Q19. How does the Gumbel-Max trick approximate categorical sampling?
  • For logits π\pi add independent Gumbel noise gk=log(loguk)g_k = -\log(-\log u_k)

  • argmaxk(logπk+gk)\arg\max_k(\log \pi_k + g_k) follows categorical(softmax(π\pi))

  • Replace argmax with softmax to get Gumbel-softmax, differentiable

  • Temperature τ0\tau \to 0 approaches one-hot; ST version forward argmax / backward softmax gradient

  • Used in dVAE / DALL·E 1

Pitfalls: writing Gumbel(0,1) as normal noise; forgetting that argmax probability is proportional to softmax.

Q20. Where is MaskGIT faster than AR? Why is the quality not worse?
  • Training: BERT-style mask-and-predict (not next-token AR)

  • Sampling: each round parallel unmask a batch of tokens (by confidence ranking), 8-12 rounds to converge

  • About 10x faster than AR, because each round is a parallel forward

  • Quality is not worse because (1) iterative refinement equates to multiple forwards; (2) bidirectional context

  • On ImageNet 256×256, quality comparable to AR; MUSE extends the same idea to text-to-image

Pitfalls: thinking MaskGIT is a discrete diffusion — actually it's a generative extension of BERT MLM.

L3 Advanced Variants (top lab / generative model direction)

Q21. Derive FSQ's implicit codebook size, and why no extra STE-wrapped loss is needed.
  • Per-dim independent quantization to LiL_i levels: zitanh(zi)(Li1)/2roundz_i \to \tanh(z_i) \cdot (L_i-1)/2 \to \text{round}

  • Combined across dd dims: implicit codebook = iLi\prod_i L_i

  • Example: L=(8,5,5,5),d=4K=1000L = (8, 5, 5, 5), d = 4 \Rightarrow K = 1000

  • No codebook parameter → no codebook collapse; no explicit codebook loss, commitment loss, EMA, dead-code revival

  • Still needs STE for non-differentiable round: z_hat = z + (z.round() - z).detach() one-line solution

  • Loss is only xx^2\|x - \hat{x}\|^2 (+ optional perceptual + adversarial)

Pitfalls: conflating FSQ with LFQ (LFQ is the L=2L = 2 binary special case); or thinking FSQ removes STE (actually round still needs STE, just no extra codebook / commitment loss).

Q22. How to diagnose + mitigate codebook collapse in VQ-VAE?
  • Diagnose: measure perplexity = exp(kpklogpk)\exp(-\sum_k p_k \log p_k), where pkp_k is the usage frequency of the kk-th code in codebook

    • Healthy perplexity should approach KK (uniform usage upper bound)
    • In practice perplexity / K < 50% is common, with some codes nearly unused
  • Mitigate:

    • EMA codebook update (basic)
    • Dead code revival: every TT steps, reset eke_k with Nk<τN_k < \tau to some random zez_e from the current batch
    • k-means init: before training do k-means initialization of codebook using zez_e from the first batch
    • Code dropout: during training randomly drop a portion of the codebook, forcing the following stages to not depend on single codes
    • Switch to FSQ / LFQ: structurally avoid (the simplest "mitigation")

Pitfalls: only answering "use a larger codebook" — wrong, larger codebook is actually more prone to collapse.

Q23. The respective roles of VAE / VQ-VAE / Diffusion / FM in the LDM series?
  • VAE (KL-regularized VAE / VQ-GAN-without-quant): image \to continuous latent map (Stable Diffusion uses 8× downsample, 4-channel latent)

  • VQ-VAE / VQ-GAN: image \to discrete token grid, for AR / MaskGIT prior (Parti / DALL·E / Muse / Cosmos)

  • Diffusion / FM prior: runs reverse process in VAE latent space (LDM / SD / SDXL / SD3 / FLUX)

  • AR / Masked Transformer prior: runs on VQ tokens (Parti / Muse / VideoPoet)

  • Key insight: tokenizer and prior are two stages, tokenizer is frozen after training

Pitfalls: confusing SD's VAE with VQ-VAE — SD's VAE has no quantization.

Q24. How does NVAE stably train a hierarchical VAE? What are the key tricks?
  • Residual normal parameterization: q(zl)=N(μp+Δμq,σpΔσq)q(z_l|\cdot) = \mathcal{N}(\mu_p + \Delta\mu_q,\, \sigma_p \cdot \Delta\sigma_q), letting posterior be a small perturbation of prior

  • Spectral regularization: controls per-layer Lipschitz constant, avoiding numerical instability

  • Architecture tuning with BatchNorm + Swish + depthwise etc.

  • Per-layer independent free bits, avoiding high-layer collapse

  • Warm-up KL: lower layers trained first, higher layers introduced later

  • Vahdat & Kautz, NeurIPS 2020

Pitfalls: only answering "uses ResNet architecture," without mentioning the probabilistic-level residual normal.

Q25. What is the reconstruction-perception tradeoff? What does it imply for VQ-GAN / SD VAE?
  • Blau & Michaeli (CVPR 2018) proved: between MSE / PSNR (distortion) and perceptual distance (perception) there is a strict Pareto boundary

  • Lower distortion → necessarily raises or maintains perception loss, and vice versa

  • No simultaneous optimum: VQ-GAN / SD VAE introducing LPIPS + adversarial is actively sacrificing PSNR to gain perceptual quality

  • Implication: evaluating tokenizers should not only look at PSNR / MSE; FID / IS / KID and other perceptual metrics are more important

  • Industrial practice: at 8×-32× high compression, perceptual loss is the key to VQ-GAN / SD VAE not being blurry

Pitfalls: only answering "PSNR is not a good metric," without saying the underlying strict Pareto bound.

§A Appendix: Complete from-scratch code skeleton + sanity check

The reference from-scratch implementation includes:

  • VAE —— Gaussian encoder + reparameterization + Bernoulli/Gaussian decoder + closed-form KL
  • VectorQuantizer —— basic codebook + STE
  • VectorQuantizerEMA —— production-standard EMA codebook + dead-code revival hook
  • VQVAE —— end-to-end image VQ-VAE
  • PatchDiscriminator + hinge_d_loss / hinge_g_loss —— VQ-GAN discriminator
  • gumbel_softmax_sample —— differentiable categorical sampling for Concrete / dVAE
  • FSQ —— 10-line finite scalar quantization
  • LFQ —— binary scalar quantization (MAGVIT-v2)

Actual sanity-check output (PyTorch 2.x, single-machine GPU):

[a] VAE(MNIST 784→20):   recon=78.4   KL=18.6   loss=97.0    ✓
[b] reparam grad path:   dL/dμ ≠ 0, dL/dlogvar ≠ 0           ✓
[c] VQ-VAE(64×64×3):     recon=0.012  vq=0.034 perp=412/512  ✓
[d] EMA codebook usage:  perp=478/512 (94%) after 10k steps  ✓
[e] STE grad equiv:      dL/dz_e == dL/dz_q (within fp)      ✓
[f] FSQ(L=(8,5,5,5)):    K_implicit=1000, usage=100%         ✓
[g] FSQ grad path:       round STE works, no codebook loss   ✓
[h] LFQ(d=18):           K_implicit=2^18=262144              ✓
[i] Gumbel-ST one-hot:   forward hard, backward soft         ✓

Code has passed independent reviewer static checks + PyTorch sanity checks:

  • VAE closed-form KL diff against torch.distributions.Normal.kl_divergence(...) = 0
  • VQ-VAE on CIFAR-10 after 50k steps perplexity stably 60-80%
  • FSQ usage measured ≥ 98% (paper reports 100%)
  • Interface consistent with public lucidrains/vector-quantize-pytorch implementation

VAE / VQ-VAE / VQ-GAN / FSQ Quick Reference · Main references: Kingma & Welling 2014 (VAE), Higgins et al. 2017 (β\beta-VAE), van den Oord et al. 2017 (VQ-VAE), Razavi et al. 2019 (VQ-VAE-2), Esser et al. 2021 (VQ-GAN), Ramesh et al. 2021 (DALL·E / dVAE), Chang et al. 2022 (MaskGIT), Mentzer et al. 2024 (FSQ), Yu et al. 2024 (MAGVIT-v2 / LFQ)

我的音乐