DATASCI 447 Lecture 19: Functional and Fast VAEs

Kevin McAlister

March 24, 2026

Administrative Stuff

WHERE WE ARE

The VAE with perceptual loss produces diverse, structured outputs — different breeds, poses, colors, backgrounds.

But everything is blurry.

The standalone GAN produces sharp images but collapsed on our dataset. No encoder, no reconstruction, no structured latent space.

Today: combine them — then replace the bottleneck entirely.

THREE SIGNALS TO THE DECODER

Keep the full VAE. Add a PatchGAN discriminator that judges the decoder’s reconstructions.

The decoder now receives three gradient signals simultaneously:

  • L1: “match the input pixels”
  • LPIPS: “match the input features”
  • GAN: “look realistic”

The first two keep it honest. The third keeps it sharp.

THREE SIGNALS TO THE DECODER

THE PATCHGAN DISCRIMINATOR

GAN discriminators produced a single real/fake score per image. Need more information for reconstruction

The PatchGAN instead outputs a grid of scores — each one corresponding to an overlapping receptive field of the input.

  • Output: \((B, 1, 15, 15)\) — 225 overlapping local judgments per image.

Goal: For each patch in the image, try to maximize the realness score given by the adversarial discriminator.

THE PATCHGAN DISCRIMINATOR

THE PATCHGAN DISCRIMINATOR

Each position in the \(15 \times 15\) output sees a roughly \(34 \times 34\) patch of the \(128 \times 128\) input. Adjacent positions overlap heavily — offset by only 8 pixels.

  • Before final 1x1 convolution on the channels (weighted average of the channel values), each position in the 15x15 grid is associated with a vector of features (perceptual)

  • The weighted average links to the real/fake score by saying “Is this collection of features in this broad area realistic?”

This connects to the perceptual loss idea!

  • The discriminator doesn’t care about global composition — that’s the reconstruction loss’s job.

  • It cares about whether the local features at any given point could plausibly come from a real image.

  • Realistic fur texture? Realistic edge sharpness? Realistic eye detail? Wherever they appear?

PATCHGAN ARCHITECTURE

class PatchDiscriminator(nn.Module):
    def __init__(self):
        super().__init__()
        self.model = nn.Sequential(
            nn.Conv2d(3, 64, 4, stride=2, padding=1),    # 128→64, RF: 4
            nn.LeakyReLU(0.2),

            nn.Conv2d(64, 128, 4, stride=2, padding=1),  # 64→32,  RF: 10
            nn.GroupNorm(32, 128),
            nn.LeakyReLU(0.2),

            nn.Conv2d(128, 256, 4, stride=2, padding=1),  # 32→16, RF: 22
            nn.GroupNorm(32, 256),
            nn.LeakyReLU(0.2),

            nn.Conv2d(256, 1, 4, stride=1, padding=1),    # 16→15, RF: 34
        )
  • The first three layers downsample with stride=2, building up a \(34 \times 34\) receptive field. The final layer uses stride=1 — it doesn’t downsample further, just produces the real/fake score at each position.

  • LeakyReLU, not ReLU — the discriminator needs gradients for “obviously fake” regions.

THE GENERATOR LOSS

\[\mathcal{L}_G = \underbrace{\text{L1}(\mathbf{x}, \hat{\mathbf{x}})}_{\text{pixel}} + \underbrace{\lambda \cdot \text{LPIPS}(\mathbf{x}, \hat{\mathbf{x}})}_{\text{perceptual}} + \underbrace{\beta \cdot D_{KL}(Q \| P)}_{\text{regularization}} + \underbrace{w_{\text{gan}} \cdot \mathcal{L}_{\text{GAN}}}_{\text{sharpness}}\]

  • \(\lambda\) is set by gradient balancing (L1 vs LPIPS, same as Lecture 17).

\(w_{\text{gan}}\) is set by the same gradient balancing principle — now balancing combined reconstruction vs GAN:

\[w_{\text{gan}} = \frac{\|\nabla_{\mathbf{w}} \mathcal{L}_{\text{recon}}\|}{\|\nabla_{\mathbf{w}} \mathcal{L}_{\text{GAN}}\| + \epsilon}\]

  • Measured at decoder’s last layer.

DELAYED DISCRIMINATOR START

If we activate the discriminator from epoch 1, it overwhelms the VAE before the encoder and decoder have learned useful representations.

Train as a pure VAE (L1 + LPIPS + KL) for the first N epochs. Then activate the discriminator.

disc_factor = 1.0 if epoch >= 30 else 0.0
gan_term = disc_factor * w_gan * gan_loss

The VAE establishes good reconstructions first. The GAN sharpens on top of an already-working model.

  • Best outcome for me was a linear ramp of the gan term weight

WHY THE VAE-GAN DOESN’T COLLAPSE

The standalone GAN collapsed because the generator had no anchor — its only objective was fooling D. It found one good image and repeated it.

  • Training a GAN is quite difficult because of this lack of “correctness”

The VAE-GAN can’t collapse because of the reconstruction loss. Every training image demands its own reconstruction. Diversity is mandatory, not optional!

  • The GAN loss is a secondary signal on top of a primary reconstruction objective. Sharpness without sacrificing diversity.

THE TRAINING LOOP

for data, *labels in train_loader:
    # ── Discriminator step ──
    recon, mu, logvar = model(data)
    real_logits = D(data)
    fake_logits = D(recon.detach())    # detach: don't update VAE
    d_loss = discriminator_loss(real_logits, fake_logits)
    opt_D.zero_grad(); d_loss.backward(); opt_D.step()

    # ── Generator (VAE) step ──
    recon, mu, logvar = model(data)    # fresh forward pass
    fake_logits = D(recon)             # no detach: grads to VAE

    recon_loss = l1 + adaptive_weight * lpips
    gan_loss = generator_loss(fake_logits)
    w_gan = compute_adaptive_weight(recon_loss, gan_loss, last_layer)

    total = recon_loss + beta * kl + disc_factor * w_gan * gan_loss
    opt_G.zero_grad(); total.backward(); opt_G.step()

WHAT’S STILL WRONG

The images are sharper, but look closely:

  • Eyes not quite symmetric
  • Heads slightly dented or lopsided
  • Ears at wrong angles
  • Occasional merged features

Two problems:

  1. The flat vector bottleneck\(\mathbf{z} \in \mathbb{R}^{256}\) destroys all spatial information. The decoder must reconstruct “what goes where” from scratch.

  2. KL-reconstruction tension — training is slooooooooooooowwwwwww. In initial stages, the VAE pushes the KL divergence close to zero and the rest of the training procedure is spent moving away from the prior.

Can we fix both at once?

WHERE SPATIAL INFORMATION DIES

The current bottleneck:

\[\text{Encoder} \to (B, 512, 4, 4) \to \text{Flatten} \to \mathbb{R}^{8192} \to \text{fc\_mu} \to \mathbb{R}^{256}\]

The encoder produces a spatial feature map — position \((0,0)\) represents the top-left region, position \((3,3)\) represents the bottom-right.

Then we flatten and project — mixing every spatial position into one vector.

The decoder receives \(\mathbb{R}^{256}\) and must figure out: what goes in the top-left? What goes in the bottom-right? How do eyes relate to ears? All from a vector with no spatial organization.

WHERE SPATIAL INFORMATION DIES

KEEP IT SPATIAL

What if we skip the flatten entirely?

\[\text{Encoder} \to (B, D, 4, 4)\]

Each position in the \(4 \times 4\) grid corresponds to a specific region of the image. Top-left stuff stays in the top-left. The decoder starts with a rough spatial map and refines it.

The decoder’s job is now much easier: position \((0,0)\) should produce the top-left of the image. It receives features that already encode what the top-left looks like.

KEEP IT SPATIAL

BUT WE STILL HAVE THE KL PROBLEM

The spatial bottleneck helps with structure. But each of those \(D \times 4 \times 4\) continuous values still needs KL regularization toward \(\mathcal{N}(0, 1)\).

  • That’s \(D \times 16\) KL terms pulling each latent dimension toward zero — fighting the reconstruction loss at every step.

  • For D = 256, that’s 4096 KL terms! If each term contributes even a small amount to the loss, it adds up quickly and overwhelms the reconstruction loss

  • We’re left at an impasse - even weak spatial structure will kill our VAE!

THE BILLION DOLLAR IDEA: CLUSTERING

What if instead of pushing continuous latents toward a Gaussian, we snap each spatial position to the nearest prototype from a learned dictionary?

This is clustering. K-means finds \(K\) centroids that summarize the data. Our codebook learns \(K\) prototype feature vectors that summarize the encoder’s output space.

Clustering is dimensionality reduction — project a complicated, redundant pixel space to a small set of representative locations. We’ve been doing this all semester:

  • PCA reduces dimension through linear projection
  • VAEs reduce dimension through learned nonlinear projection + Gaussian regularization
  • VQ-VAE reduces dimension through learned nonlinear projection + discretization

THE BILLION DOLLAR IDEA: CLUSTERING

THE CODEBOOK

A codebook \(\mathbf{E} \in \mathbb{R}^{K \times D}\) — a matrix of \(K\) learned vectors, each of dimension \(D\).

Each row is a visual word — a prototype feature that captures a commonly occurring pattern in the encoder’s output.

  • One visual word might represent “fur texture”, another “grass background”, and so on.

Think of it like a palette. An artist doesn’t use infinitely many colors — they use a finite palette and paint from those. The codebook is a palette of visual features.

self.codebook = nn.Embedding(K, D)  # K entries, each D-dimensional
# Typical: K = 512 entries, D = 64 dimensions per entry

THE CODEBOOK

QUANTIZATION: HOW IT WORKS

The encoder outputs a continuous spatial feature map: \(4 \times 4 \times D\)

For each of the 16 spatial positions \((h, w)\):

  1. Take the continuous feature vector \(\mathbf{z}_e(h, w) \in \mathbb{R}^D\)
  2. Compute distance to every codebook entry: \(\|\mathbf{z}_e(h, w) - \mathbf{e}_j\|_2\) for \(j = 1, \ldots, K\)
  3. Replace with the nearest entry: \(\mathbf{z}_q(h, w) = \mathbf{e}_{k^*}\) where \(k^* = \arg\min_j \|\mathbf{z}_e(h,w) - \mathbf{e}_j\|_2\)

The output is a grid of 16 codebook indices — discrete tokens, not continuous values.

QUANTIZATION: HOW IT WORKS

QUANTIZATION: HOW IT WORKS

Once the visual vocabulary for an image has been created in the latent space, decode to an image!

  • Locations in the 4x4 grid are spatially meaningful!

  • Decoder is highly nonlinear and can figure out how grass background, gray fur, cat eyes, and cat ears should go together!

CODEBOOK SIZE IS EXPLICIT REGULARIZATION

With a continuous VAE, regularization (e.g. generalization) comes from the KL divergence — forcing the latent distribution toward \(\mathcal{N}(0, I)\).

With VQ-VAE, regularization comes from the codebook size \(K\).

  • Small \(K\) (e.g. 64): only 64 visual words. Heavy compression. The model must learn highly general prototypes.

Large \(K\) (e.g. 4096): enough visual words for fine distinctions. Less compression. Risk of memorizing rather than generalizing.

\(K\) directly controls the compression-reconstruction tradeoff — the same role \(\beta\) plays in the VAE, but as an architectural choice rather than a loss weight.

  • No gradients to balance. No annealing schedule. Just pick \(K\).

THE REPRESENTATIONAL CAPACITY IS ENORMOUS

How much variation can we really have with only 512 vectors, though?

  • Remember that the decoder figures out how to combine the actual vector with the positional context.

With \(K = 512\) codebook entries and a \(4 \times 4\) spatial grid (16 positions):

\[\text{Number of possible images} = K^{16} = 512^{16} \approx 10^{43}\]

Even with a small codebook (\(K = 64\)):

\[64^{16} \approx 10^{29}\]

Still billions of billions of billions of unique images. Discretization does not limit expressivity — it structures it.

NO KL. AT ALL.

The prior over codebook entries is uniform — each entry is equally likely.

\[D_{KL}(Q(\mathbf{z} | \mathbf{x}) \| P(\mathbf{z})) = \log K\]

This is a constant. It drops out of the optimization entirely.

No beta. No free bits. No warmup schedule. No annealing. No slow reconstruction tension.

The codebook provides structure through discretization rather than explicit prior tension

VQ-VAE