March 26, 2026
How does the VQ-VAE differ from the traditional VAE?
If VAEs are fancy nonlinear PCA, what would be the VQ-VAE’s equivalent?
The encoder compresses an image into a spatial feature map \((B, D, H', W')\).
Each spatial position is snapped to the nearest entry in a learned codebook.
The decoder reconstructs the image from the quantized features.
No KL divergence!
The encoder outputs a \(K \times K \times D\) tensor of the image. Each of the \(K \times K\) positions are spatial codes.
For a single spatial position:
The encoder outputs a continuous vector \(\mathbf{z}_e \in \mathbb{R}^D\).
We compute \(\|\mathbf{z}_e - \mathbf{e}_j\|_2\) for every codebook entry \(j = 1, \ldots, K\).
Replace with the winner: \(\mathbf{z}_q = \mathbf{e}_{k^*}\) where \(k^* = \arg\min_j \|\mathbf{z}_e - \mathbf{e}_j\|_2\)
How did standard VAEs regularize the latent space to prevent mass collapse and yield more generalizable representations that could support generation?
How does the VQ-VAE differ?
Size of the codebook is the regularizer!
If all images can only be constructed from 16 of the 256 available “visual words”, then there isn’t enough capacity in the model to make the latent space specific to the images!
Overfitting is controlled by size, not the nasty integral that I wasted 5 years of my life on (or so I thought until stable diffusion came along!)
The argmin is not differentiable. Gradients from the decoder can’t flow through the discrete selection!
Solution: Use the Straight-Through Estimator.
In the forward pass, the decoder sees \(\mathbf{z}_q\) (the codebook entry).
In the backward pass, the decoder sees \(\mathbf{z}_e\) (the encoder output). Gradients bypass the quantization step entirely.
What do we need to happen between the codebook and the encoder outputs to see this work?
Naive idea: just minimize \(\|\mathbf{z}_e - \mathbf{e}_{k^*}\|_2^2\) and let both the encoder and codebook update.
Why this fails: the encoder and codebook would chase each other. The encoder moves \(\mathbf{z}_e\) left, so the codebook moves \(\mathbf{e}_k\) left. Now \(\mathbf{z}_e\) moves further left. They drift together in an arbitrary direction, never converging.
The fix: alternate who moves.
Like two people walking toward each other — each takes a step, then the other takes a step. They converge to the middle instead of drifting off together.
One of the big advantages of the VQ approach is interpretability of the latent space
The codebook has \(K\) entries, each a \(D\)-dimensional vector.
The SNAP operation ensures that all images are using some combination (usually 16, 64, or 256 entries depending on richness of the training set) of the same visual vocabulary
Say that \(K = 512\). That’s not that many words! (I can say 512 unique words right now!)
The issue is that each codebook entry is a vector of length \(D\)
In weird latent visual space…
These are abstract — we can’t directly visualize a 64-dimensional vector.
Basically, the same problem as intepreting features from a CNN!
But we can ask: which images use each entry, and where?
The codebook entries are shared visual primitives. A golden retriever and a German shepherd might use the same “grass background” entry in their top-left positions, but different “fur texture” entries in their center positions.
We’re designing latent vectors that are intended to capture specific attributes of the input images.
But, we’re also asking the latent space to learn about:
Species
Breed
Color
Why waste capacity learning when we already know these values for each image!
We can reconstruct sharply with good spatial structure. But every generation is random — we can’t say “generate a golden retriever” or “make this dog black.”
We have metadata for every training image: breed, primary color, secondary color.
We want the decoder to use this information so it knows what to produce. The codebook indices handle the structure — where things go, what pose, what background. The conditioning handles the identity — what kind of animal, what color.
Each categorical attribute gets a learned embedding — a lookup table that maps a discrete category to a dense vector:
self.breed_embed = nn.Embedding(num_breeds, 16) # 120 breeds → R¹⁶
self.pcolor_embed = nn.Embedding(num_pcolors, 8) # 9 colors → R⁸
self.scolor_embed = nn.Embedding(num_scolors, 8) # 9 colors → R⁸Concatenated into a single conditioning vector: \(\mathbf{c} = [\mathbf{c}_{\text{breed}};\; \mathbf{c}_{\text{pcolor}};\; \mathbf{c}_{\text{scolor}}] \in \mathbb{R}^{32}\)
An embedding is just a matrix \(\mathbf{W} \in \mathbb{R}^{K \times D}\) where row \(k\) is the learned vector for class \(k\).
# nn.Embedding(120, 16) creates a 120×16 matrix
# Input: integer index 42 (golden retriever)
# Output: row 42 — a learned 16-dim vectorThe vectors are initialized randomly and trained end-to-end. The network learns whatever representation of “golden retriever” or “black” makes reconstruction easiest.
Why not a one-hot vector?
The encoder needs to know the class so it can organize the codebook indices efficiently.
Spatial broadcast: reshape \(\mathbf{c}\) to a spatial tensor and concatenate as extra input channels.
c_spatial = c.view(B, 32, 1, 1).expand(B, 32, H, W) # (B, 32, 128, 128)
x_cond = torch.cat([x, c_spatial], dim=1) # (B, 35, 128, 128)Without encoder conditioning: the codebook wastes entries encoding “is this a golden retriever or a husky.”
With encoder conditioning: the codebook is free to encode pose, texture, layout — the stuff that varies within a breed.
The decoder needs to know the class so it can interpret the codebook indices correctly.
Broadcast \(\mathbf{c}\) to the bottleneck spatial size and concatenate to the quantized features:
c_small = c.view(B, 32, 1, 1).expand(B, 32, H_prime, W_prime)
z_q_cond = torch.cat([z_q, c_small], dim=1)
reconstruction = self.decoder(z_q_cond)The codebook \(\mathbf{E} \in \mathbb{R}^{K \times D}\) is shared across all breeds and colors.
\(\mathbf{c}\) flows around the codebook, not through it:
\[\mathbf{x}, \mathbf{c} \;\to\; \text{Encoder} \;\to\; \mathbf{z}_e \;\to\; \text{Quantize} \;\to\; \mathbf{z}_q \;\to\; [\mathbf{z}_q \,;\, \mathbf{c}] \;\to\; \text{Decoder} \;\to\; \hat{\mathbf{x}}\]
\[\mathcal{L} = \text{L1} + \lambda \cdot \text{LPIPS} + w_{\text{gan}} \cdot \mathcal{L}_{\text{GAN}} + \|\text{sg}[\mathbf{z}_e] - \mathbf{e}\|^2 + \beta_c\|\mathbf{z}_e - \text{sg}[\mathbf{e}]\|^2\]
Exactly the same loss as the unconditional VQ-VAE. No new terms!
Conditioning flows through the architecture. The loss handles the rest.
Because \(\mathbf{c}\) and the codebook indices encode different things, we can swap parts of \(\mathbf{c}\) while keeping the indices fixed.
Codebook indices: pose, spatial structure, texture layout, background composition
\(\mathbf{c}\): breed, primary color, secondary color — identity
As we’ve seen before, optimizers are lazy
Any guesses about what can happen when training a conditioned VQ model?
If the bottleneck is given too much capacity (e.g. \(K\) is too high), it will learn that the most efficient strategy is to completely ignore the conditions and learn everything through the bottleneck!
Like with the GAN loss, we’ll need to put gutter-guards on the optimizer to prevent this.
The problem: the encoder hides the labels (e.g. “Cat” or “Golden Retriever”) inside the 16 x 16 latents for an image so the decoder doesn’t have to read our condition vector
The solution: Hire a strict proctor to prevent cheating - a tiny latent classifier network
Look at the encoder latents before quantization. Use those to try to predict the condition vector (e.g. “Cat” or “Dog”, “Black”, “Brown” or “White”)
If the classifier can use the encoder outputs to predict the conditions, CHEATING
If not, we’re all good
We want to train a classifier that will fail the cheating test.
The classifier is independent of the VQ-VAE. How do we give that info back to the encoder?
Have a separate optimizer for the proctor network. In each batch step the classifier.
In the forward pass, let the latents flow as normal and let the classifier make its guesses
In the backward pass, reverse the gradients (multiply by -1) for the classifier. We want to penalize the classifier when it gets the conditions right. Since the encoder gradient flows through the jointly trained classifier, the encoder combines its good gradients with the bad gradients!
In equilibrium, the classifier should be unable to predict the conditions from the latents and send a gradient signal of 0.
4D chess, baby!
Reward the snitch network for reporting cheaters and punish the cheater.
The encoder is punished for leaking information and learns to not do that!
In theory, this leads to better conditioning. In theory…
This now admits a very powerful property - editing
Train the Conditional VQ-VAE
Take an image and its conditions and encode it to the latent space (e.g. the 16 x 16 matrix of codebook values)
Concatenate new conditioning values to the existing codebook entries
Decode
Take a photo of a golden retriever. Make it black.
Step 1: Encode with original labels \(\to\) get codebook indices
Take a photo of a golden retriever. Make it black.
Step 2: Decode with edited condition — only color changes
c_edited = get_condition(breed=GOLDEN_RETRIEVER,
pcolor=BLACK, scolor=BLACK)
edited = model.decode(z_q, c_edited)Same codebook indices. Same pose. Same background. Different fur color.
Same codebook indices, different breed:
c_original = get_condition(GOLDEN_RETRIEVER, GOLDEN, WHITE)
c_edited = get_condition(SIBERIAN_HUSKY, GRAY, WHITE)
original_image = model.decode(z_q, c_original)
edited_image = model.decode(z_q, c_edited)Same spatial layout. The animal’s pose, the background, the lighting — all preserved. But the breed shifts.
Take stock of what you have:
This is the image generation backbone of DALL-E 1 (Ramesh et al., 2021) — the model that first demonstrated text-to-image generation.
DALL-E replaced our breed/color embeddings with CLIP text embeddings — continuous vectors from a text encoder trained on image-caption pairs. Everything else is the same architecture you’ve built.
Not that great yet.
There’s something that we’re still missing:
Conditioning on features can’t completely remove the identity information from the latent space.
We’re still missing the ability to learn important structure in the latent space - the order in which the codebook entries appear!
Sample a random index at each of the 16 spatial positions. Decode.
We’ve designed the VQ-VAE to preserve spatial information.
But we then ignore the spatial information when we’re trying to do new things with images.
With a large enough sample size (millions of images), this all comes out in the wash.
We don’t have the computational resources to process that!
“Dog ear” + “grass background” + “sky” in the right spatial positions = a dog photo.
The same entries in random positions = garbage.
The codebook indices form a sequence (raster scan: top-left to bottom-right):
\[s_1, s_2, s_3, \ldots, s_{16}\]
Each index depends on the previous ones:
\[P(s_1, s_2, \ldots, s_{16}) \neq \prod_i P(s_i)\]
Sampling independently ignores all dependencies.
Factor the joint distribution into a chain of conditionals:
\[P(s_1, s_2, \ldots, s_N) = \prod_i P(s_i \,|\, s_1, \ldots, s_{i-1})\]
Predict each token given all previous tokens.
This is a sequence modeling problem.
Train on the encoded training set:
Add \(\mathbf{c}\) as a prefix or bias to the sequence model:
\[P(s_1, \ldots, s_N \,|\, \mathbf{c}) = \prod_i P(s_i \,|\, s_1, \ldots, s_{i-1}, \mathbf{c})\]
“Given that this is a golden retriever with golden primary color, what codebook indices should I produce?”
The autoregressive model learns which codebook entries are used for golden retrievers vs huskies, and in what spatial arrangements.
Predicting the next token given all previous tokens. Does this sound like a familiar task? Maybe a tool that most of you probably have open on your laptop right now?
This is exactly what transformers do.
| VQ-VAE + Autoregressive | Language Model |
|---|---|
| VQ-VAE encoder | BPE tokenizer |
| Codebook indices | Word tokens |
| Autoregressive model | GPT-style transformer |
| Condition \(\mathbf{c}\) | Prompt |
We don’t have that tool yet. It’s next in the course — RNNs, attention, and transformers.
But let me show you what it looks like when it works.
In a continuous VAE, the latent vector \(\mathbf{z}\) is continuous. Even when you keep \(\mathbf{z}\) “fixed” and swap \(\mathbf{c}\), the decoder can pick up on subtle continuous signals in \(\mathbf{z}\) that leak identity information.
In the VQ-VAE, the codebook indices are discrete tokens. They don’t drift, blend, or leak. A codebook entry either means “pointy ear” or it doesn’t.
When you swap the color embedding, the codebook entries remain exactly the same — integer indices, unchanged.
Discretization enforces a hard separation between what the codebook encodes (structure) and what \(\mathbf{c}\) encodes (identity).
We’ve just shown that with a conditional VQ-VAE, anyone with this notebook can:
For dogs, this is fun.
For people, this is dangerous.
The same architecture applied to faces produces deepfakes.
Every technique you’ve learned — conditioning, editing, latent manipulation — is dual-use. The capability for creative applications is inseparable from the capability for harm.
The editing workflow is identical:
# Face version of what we just did
z_q, indices = model.encode(face_photo, identity=PERSON_A)
deepfake = model.decode(z_q, identity=PERSON_B)Same pose, same lighting, same background. Different person.
This is the first real ethical conundrum of the semester. It won’t be the last.
The same technology that enables creative expression can also be misused for deception.
Every technique you’ve learned — conditioning, editing, latent manipulation — is dual-use. The capability for creative applications is inseparable from the capability for harm.
| Model | Reconstruct | Generate | Edit | Control |
|---|---|---|---|---|
| VAE | Blurry | Blurry | Hard | None |
| VAE + LPIPS | Sharper | Sharper | Hard | None |
| VAE-GAN | Sharp | Sharp | Hard | None |
| VQ-VAE-GAN | Sharp | ❌ Needs AR | Clean | Conditioning |
| VQ-VAE-GAN + AR | Sharp | Sharp | Clean | Full |
The bottom row is DALL-E 1. You understand every piece.
VQ-VAE path: discrete tokens → autoregressive transformer → DALL-E
Alternative: keep continuous spatial latents. Instead of autoregressive prediction, use iterative denoising — start from noise, gradually refine.
Fun fact: the VQGAN paper (Esser, Rombach, & Ommer, 2021) and the Stable Diffusion paper (Rombach et al., 2022) share authors. The same lab produced both the discrete-token path and the continuous-diffusion path.
Next: sequences, RNNs, attention, and the transformer architecture that completes the generation pipeline.