March 5, 2026
We want to create a coherent space that we can sample from to produce images of dogs with high probability.
The problem: pixel space is enormous. A 64×64×3 image lives in \(\mathbb R^{64 \times 64 \times 3}\).
Almost all of that space is garbage — random static, melted faces, impossible textures. The set of “images that look like dogs” is a tiny, thin manifold winding through this vast space.
Project high-dimensional images down to a low-dimensional code vector in a simple space that uniquely maps back to the image.
Do it so that:
Nearby codes → similar images
We can sample new codes to generate new images
Minimize reconstruction error after passing through a low-dimensional bottleneck.
We project to a latent vector (not a tensor) with orthogonal dimensions:
We know how to define distributions over vectors (like \(\mathcal N(0, \mathbf I)\))
We want each dimension to capture an independent factor of variation
Sampling from a structured tensor is much harder — what does a “random 512×4×4 feature map” even mean?
With infinite capacity and no constraints, the encoder maps each image to an isolated point — a Dirac delta.
The latent space becomes a scattered collection of dots with vast voids.
The decoder has never seen the voids. Sample there → garbage.
An even worse failure mode: with enough capacity, the model learns to skip the bottleneck entirely.
The decoder learns to reconstruct without z — essentially an identity function that routes information around the bottleneck.
The latent space becomes meaningless. The encoder outputs the same z for every input.
Instead of learning a point mapping x → z, assume each image was generated as a draw from a posterior distribution over z.
Learn \(Q(\mathbf{z} | \mathbf{x})\) instead of a single z. Give it probabilistic structure.
This ensures the latent space is:
Smooth — a continuum of dogs with no holes
Compact — everything concentrated in the same region
A priori orthogonal — independent dimensions via the prior N(0, I)
To compute the posterior \(P(\mathbf{z} | \mathbf{x}, \theta)\), we need a prior \(P(\mathbf{z})\).
A natural choice is \(P(\mathbf{z}) = \mathcal{N}(\mathbf{z} | \mathbf{0}, \mathbf{I})\), a standard Gaussian.
Induces compactness properties
Easy to sample from
This is going to be the standard for any generation methods we show over this semester!
The encoder \(f(\mathbf{x}) \to \mathbf{z}\) and decoder \(f(\mathbf{z}) \to \mathbf{x}\) are both nonlinear neural networks.
We don’t know the functional form of \(P(\mathbf{z} | \mathbf{x}, \theta)\).
Too many unknowns, not enough constraints:
z is a function of x, but we never observe z directly
The neural network parameters need to be learned too
Everything depends on everything else
Since z is a proper probability distribution, marginalize it out:
\[P(\mathbf{x} | \theta) = \int P(\mathbf{x} | \mathbf{z}, \theta) \, P(\mathbf{z}) \, d\mathbf{z}\]
Choose \(\theta\) that maximizes this value
NOTATION
\[P(\mathbf{x} | \theta) = \int P(\mathbf{x} | \mathbf{z}, \theta) \, P(\mathbf{z}) \, d\mathbf{z}\]
Neural network decoder → highly nonlinear
z is high-dimensional → no grid method
Naive Monte Carlo → astronomical variance
\(\log \int \neq \int \log\)
Bayes’ rule is circular — the denominator is the same integral
We left off here last time. Today: the way out.
Tempting thought. We wrote the posterior \(P(\mathbf{z} | \mathbf{x}, \theta) \propto P(\mathbf{x} | \mathbf{z}, \theta) P(\mathbf{z})\). Conjugacy gave us a nice Gaussian formula.
Can we just compute it?
Our goal is \(P(\mathbf x | \theta)\)
Rearranging Bayes’ theorem:
\[P(\mathbf{z} | \mathbf{x}, \theta) = \frac{P(\mathbf{x} | \mathbf{z}, \theta) \; P(\mathbf{z})}{P(\mathbf{x} | \theta)}\]
\[P(\mathbf{x} | \theta) = \frac{P(\mathbf{x} | \mathbf{z}, \theta) \; P(\mathbf{z})}{P(\mathbf{z} | \mathbf x , \theta)}\]
If I knew \(P(\mathbf{z} | \mathbf{x}, \theta)\), then I could compute \(P(\mathbf{x} | \theta)\).
We can’t compute the true posterior \(P(\mathbf{z} | \mathbf{x}, \theta)\).
So let’s approximate it.
Introduce \(Q_\phi(\mathbf{z} | \mathbf{x})\) — a learned approximation parameterized by ϕ (its own neural network).
Q takes an image \(\mathbf X\) as input and outputs the parameters of a posterior distribution for that image.
We choose Q to be Gaussian:
\[Q_\phi(\mathbf{z} | \mathbf{x}) = \mathcal{N}(\mathbf{z} \;|\; \boldsymbol{\mu}_\phi(\mathbf{x}),\; \text{diag}(\boldsymbol{\sigma}^2_\phi(\mathbf{x})))\]
Full support. Convenient.
This is the encoder. The neural network that maps images to posterior parameters. Two heads that map \(\mathbf X\) to \(\boldsymbol{\mu}(\mathbf X)\) and \(\boldsymbol{\sigma}^2(\mathbf X)\).
Defensible as \(N \to \infty\) due to Bayesian CLT - Bernstein-von Mises theorem.
Goal: Find the best normal approximation to the true posterior!
Start from the log marginal likelihood — the thing we want to maximize but can’t compute:
\[\log P(\mathbf{x} | \theta) = \log P(\mathbf{x} | \theta) \cdot \underbrace{\int Q_\phi(\mathbf{z} | \mathbf{x}, \theta) \; d\mathbf{z}}_{= 1}\]
\(\log P(\mathbf x | \theta)\) doesn’t have anything to do with \(\mathbf z\), so bring the log inside the integral:
\[= \int Q_\phi(\mathbf{z} | \mathbf{x}, \theta) \; \log P(\mathbf{x} | \theta) \; d\mathbf{z}\]
Law of the Unconcious Statistician: doesn’t matter what the transformation is, still an expectation:
\[= \mathbb{E}_{Q}\left[\log P(\mathbf{x} | \theta)\right]\]
Now use Bayes’ rule: \(P(\mathbf{x} | \theta) = \frac{P(\mathbf{x} | \mathbf{z}, \theta) \; P(\mathbf{z})}{P(\mathbf{z} | \mathbf{x}, \theta)}\)
\[= \mathbb{E}_{Q}\left[\log \frac{P(\mathbf{x} | \mathbf{z}, \theta) \; P(\mathbf{z})}{P(\mathbf{z} | \mathbf{x}, \theta)}\right]\]
Multiply and divide by \(Q(\mathbf{z} | \mathbf{x}, \theta)\) inside the log:
\[\log P(\mathbf{x} | \theta) = \mathbb{E}_{Q}\left[\log \frac{P(\mathbf{x} | \mathbf{z}, \theta) \; P(\mathbf{z}) \; Q(\mathbf z | \mathbf x, \theta)}{P(\mathbf{z} | \mathbf{x}, \theta) \; Q(\mathbf z | \mathbf x, \theta)}\right]\]
Deconstruct into a particular form to move the true posterior out:
\[\log P(\mathbf{x} | \theta) = \mathbb{E}_{Q}\left[\log \frac{P(\mathbf{x} | \mathbf{z}, \theta) \; P(\mathbf{z})}{Q(\mathbf{z} | \mathbf{x}, \theta)}\right] + \mathbb{E}_{Q}\left[\log \frac{Q(\mathbf{z} | \mathbf{x}, \theta)}{P(\mathbf{z} | \mathbf{x}, \theta)}\right]\]
And deconstruct one more time using rules of logs and linearity of expectations:
\[\log P(\mathbf{x} | \theta) = \mathbb{E}_{Q}\left[\log P(\mathbf{x} | \mathbf{z}, \theta)\right] + \mathbb{E}_{Q}\left[\log \frac{P(\mathbf{z})}{Q(\mathbf{z} | \mathbf{x}, \theta)}\right] + \mathbb{E}_{Q}\left[\log \frac{Q(\mathbf{z} | \mathbf{x}, \theta)}{P(\mathbf{z} | \mathbf{x}, \theta)}\right]\]
\[ \mathbb{E}_{Q}\left[\log \frac{P(\mathbf{z})}{Q(\mathbf{z} | \mathbf{x}, \theta)}\right] \]
Recognize this thing? Maybe it would be more familiar in the integral form?
An expected log density ratio…
\[ E_{Q}\left[ \log \frac{P(\mathbf z)}{Q(\mathbf{z} | \mathbf{x}, \theta)}\right] = \int Q(\mathbf{z} | \mathbf{x}, \theta) \; \log \frac{P(\mathbf z)}{Q(\mathbf{z} | \mathbf{x}, \theta)} \; d\mathbf{z} \]
\[ E_{Q}\left[ \log \frac{P(\mathbf z)}{Q(\mathbf{z} | \mathbf{x}, \theta)}\right] \]
This is the expected log ratio of the prior to the approximate posterior
Also known as the KL divergence between the prior and the approximate posterior:
\[ D_{KL}(Q(\mathbf{z} | \mathbf{x}, \theta) \| P(\mathbf{z})) \]
Taking an expectation w.r.t. the approximate posterior density, what is the expected distance between the two distributions?
0 means that the approximate posterior = prior
Always positive. Higher values correspond to greater differences!
For \(Q(\mathbf{z}|\mathbf{x}) = \mathcal{N}_K(\boldsymbol{\mu}, \text{diag}(\boldsymbol{\sigma}^2))\) and \(P(\mathbf{z}) = \mathcal{N}_K(\mathbf{0}, I)\):
\[D_{KL} = -\frac{1}{2} \sum_{j=1}^{K} \left(1 + \log \sigma_j^2 - \mu_j^2 - \sigma_j^2\right)\]
No approximation required. Just \(\boldsymbol \mu \in \mathbb R^K\) and \(\boldsymbol \sigma^2 \in \mathbb R^K\) from the encoder, plugged into a formula.
\[\log P(\mathbf{x} | \theta) = \mathbb{E}_{Q}\left[\log P(\mathbf{x} | \mathbf{z}, \theta)\right] + \mathbb{E}_{Q}\left[\log \frac{P(\mathbf{z})}{Q(\mathbf{z} | \mathbf{x}, \theta)}\right] + \mathbb{E}_{Q}\left[\log \frac{Q(\mathbf{z} | \mathbf{x}, \theta)}{P(\mathbf{z} | \mathbf{x}, \theta)}\right]\]
\[\log P(\mathbf{x}) = \underbrace{\mathbb{E}_{Q}[\log P(\mathbf{x} | \mathbf{z}, \theta)] - D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}))}_{\text{ELBO: } \mathcal{L}(\theta, \theta; \mathbf{x})} + \underbrace{D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}|\mathbf{x}))}_{\geq 0, \text{ unknowable}}\]
Term 1 — Reconstruction: \(\mathbb{E}_{Q}[\log P(\mathbf{x} | \mathbf{z}, \theta)]\)
“How well does the decoder reconstruct x from z’s sampled according to Q?”
How close is our prediction to the truth?
The expected log-likelihood of an image in our dataset given a latent space, encoder, and decoder.
The autoencoder loss! Maximize to create best reconstruction of training data.
\[\log P(\mathbf{x} | \theta) = \mathbb{E}_{Q}\left[\log P(\mathbf{x} | \mathbf{z}, \theta)\right] + \mathbb{E}_{Q}\left[\log \frac{P(\mathbf{z})}{Q(\mathbf{z} | \mathbf{x}, \theta)}\right] + \mathbb{E}_{Q}\left[\log \frac{Q(\mathbf{z} | \mathbf{x}, \theta)}{P(\mathbf{z} | \mathbf{x}, \theta)}\right]\]
\[\log P(\mathbf{x}) = \underbrace{\mathbb{E}_{Q}[\log P(\mathbf{x} | \mathbf{z}, \theta)] - D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}))}_{\text{ELBO: } \mathcal{L}(\theta, \theta; \mathbf{x})} + \underbrace{D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}|\mathbf{x}))}_{\geq 0, \text{ unknowable}}\]
Term 2 — KL Regularizer: \(-D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}))\)
“How close is the approximate posterior to the prior?” This is the term that enforces the shared geometry.
How far is the approximation from our prior?
Regularization in the latent space
Always greater than zero.
Minimize by making latent posteriors for each image exactly N(0,I)
\[\log P(\mathbf{x} | \theta) = \mathbb{E}_{Q}\left[\log P(\mathbf{x} | \mathbf{z}, \theta)\right] + \mathbb{E}_{Q}\left[\log \frac{P(\mathbf{z})}{Q(\mathbf{z} | \mathbf{x}, \theta)}\right] + \mathbb{E}_{Q}\left[\log \frac{Q(\mathbf{z} | \mathbf{x}, \theta)}{P(\mathbf{z} | \mathbf{x}, \theta)}\right]\]
\[\log P(\mathbf{x}) = \underbrace{\mathbb{E}_{Q}[\log P(\mathbf{x} | \mathbf{z}, \theta)] - D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}))}_{\text{ELBO: } \mathcal{L}(\theta, \theta; \mathbf{x})} + \underbrace{D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}|\mathbf{x}))}_{\geq 0, \text{ unknowable}}\]
Term 3 — Approximation Gap: \(D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}|\mathbf{x}))\)
“How good is our approximation?”
Always \(\ge\) 0 (Gibbs inequality).
Can’t compute it — it requires the true posterior. Drop it.
\[\log P(\mathbf{x}) = \underbrace{\mathbb{E}_{Q}[\log P(\mathbf{x} | \mathbf{z})] - D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}))}_{\text{ELBO: } \mathcal{L}(\theta, \theta; \mathbf{x})} + \underbrace{D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z}|\mathbf{x}))}_{\geq 0, \text{ unknowable}}\]
Since the gap term \(\geq\) 0:
\[\text{ELBO} = \mathbb{E}_{Q}[\log P(\mathbf{x} | \mathbf{z})] - D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z})) \;\leq\; \log P(\mathbf{x})\]
The Evidence Lower BOund (ELBO) is a lower bound on the log marginal likelihood.
Maximizing it simultaneously:
Pushes up \(\log P(\mathbf x | \theta)\) — our actual generative objective
Tightens the bound — makes Q closer to the true posterior
\[\text{ELBO} = \mathbb{E}_{Q}[\log P(\mathbf{x} | \mathbf{z})] - D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| P(\mathbf{z})) \;\leq\; \log P(\mathbf{x})\]
L4 callback: the marginal likelihood was our “golden goose” — the quantity that automatically trades off fit and complexity, the thing that replaces cross-validation.
The ELBO is our computable approximation to it.
\[\text{ELBO} = \underbrace{\mathbb{E}_{Q}[\log P(\mathbf{x} | \mathbf{z}, \theta)]}_{\text{Reconstruction}} - \underbrace{D_{KL}(Q(\mathbf{z}|\mathbf{x}, \theta) \| P(\mathbf{z}))}_{\text{Regularization}}\]
Look at this through the lens of every model we’ve built:
Reconstruction term → the encoder-decoder must still compress and reconstruct well. This is the autoencoder job. Push \(Q(\mathbf z | \mathbf x, \theta)\) to be informative about \(\mathbf x\).
KL term → the posteriors must stay close to \(\mathcal N(0, \mathbf I)\) - minimize the distance between the image posterior and the prior. Prevent mass collapse. Maintain shared geometry. Push \(Q(\mathbf z | \mathbf x , \theta)\) toward the prior.
These two forces are in tension. Same reconstruction-vs-regularization tradeoff as every model we’ve seen — just expressed in latent space.
The penalty \(D_{KL}(Q(\mathbf{z}|\mathbf{x}) \| \mathcal{N}(0, I))\) does three things simultaneously:
Pulls posterior means toward the origin → compactness (no codes flying off to infinity)
Prevents posterior variances from collapsing to zero → smoothness (no Dirac deltas)
Encourages independence between dimensions → orthogonality (decorrelated latent factors)
All three properties we wanted from the prior — enforced automatically by one penalty term.
Classical variational inference (pre-deep learning):
For each observation xᵢ, learn separate posterior parameters \((\mu_i, \sigma^2_i)\). N observations \(\to\) N separate optimization problems.
This was the standard approach for decades. No one did this…
Amortized inference:
Learn a single function \(Q_\theta(\mathbf{z} | \mathbf{x})\) — a neural network that takes any x and outputs \((\mu_\theta(\mathbf x), \sigma^2_\theta(\mathbf x))\).
One network replaces N optimizations.
Amortization seems obviously better. And it is.
One neural network handles all observations — no per-observation optimization
Generalizes to new x at test time — just run the encoder
Scales to millions of observations without growing the parameter count
So why wasn’t this done from the start?
It required autodifferentiation frameworks like PyTorch to be practical. Classical VI was developed when computing gradients through a neural encoder was not feasible.
Amortized variational inference is a relatively recent idea — Kingma & Welling, 2014 — even though in hindsight it’s the most natural approach. Once you have an autodifferentiator, of course you parameterize the posterior with a neural network.
Put it all together:
Encoder network \(Q_\theta\): takes \(\mathbf x\), outputs \((\mu_\theta(\mathbf x), \sigma^2_\theta(\mathbf x))\). This is \(Q(\mathbf{z} | \mathbf{x}, \theta)\).
Decoder network \(f_\theta\): takes \(\mathbf z \in \mathbb{R}^K\), outputs reconstructed image. This is \(P(\mathbf{x} | \mathbf{z}, \theta)\).
Train both jointly by maximizing the ELBO (really minimizing the negative ELBO) with gradient descent.
The encoder does inference — maps images to posteriors.
The decoder does generation — maps codes to images.
One loss function. One backprop. End to end.
That’s the Variational Autoencoder.
The training strategy:
Use CNN to project images from pixel space to latent posterior - \(\mathcal N(\boldsymbol \mu_\theta(\mathbf x), \text{diag}(\boldsymbol{\sigma}^2_\theta(\mathbf x)))\)
Latent posterior represents the plausible locations of \(\mathbf x\) in \(\mathbf z \in \mathbb R^K\)
Decode the latent posterior back to pixel space
Minimize the negative ELBO. Under MSE reconstruction loss (the average sum of squared pixel misses in an image):
\[ \begin{aligned} -\text{ELBO} = \; & \frac{1}{N} \sum_{i=1}^N \|\mathbf{X}_i - \hat{\mathbf{X}}_i \|^2_F \\ + \; & \frac{1}{N} \sum_{i=1}^N D_{KL}\!\left(\mathcal{N}\!\left(\boldsymbol{\mu}_\theta(\mathbf{x}_i),\, \text{diag}(\boldsymbol{\sigma}^2_\theta(\mathbf{x}_i))\right) \,\Big\|\, \mathcal{N}(\mathbf{0}, \mathbf{I})\right) \end{aligned} \]
New idea: How do we deal with a distribution in the middle of an encoder/decoder?
A distribution is a pair of values and densities.
Not just a single number
How do we differentiate with respect to a distribution?
We don’t!
Instead, each iteration of the autodifferentiator we sample a single value:
\[ \mathbf{z}_i \sim \mathcal{N}\left(\boldsymbol{\mu}_\theta(\mathbf{x}_i),\, \text{diag}(\boldsymbol{\sigma}^2_\theta(\mathbf{x}_i))\right) \]
Sampling forces the encoder and decoder to hedge their bets.
Every forward pass, z lands at a slightly different location in the posterior.
The decoder must learn to reconstruct from all of them — not just the posterior mean.
This is what fills in the latent space.
We need to backpropagate through the encoder. But the sampling operation \(z \sim \mathcal N(\mu, \sigma^2 I)\) is stochastic.
Gradients can’t flow through a random draw.
The loss depends on z.
z was sampled randomly.
How do we compute \(\frac{\partial \mathcal{L}}{\partial \boldsymbol{\mu}}\) and \(\frac{\partial \mathcal{L}}{\partial \boldsymbol{\sigma}^2}\)?
Instead of sampling \(z \sim \mathcal{N}(\mu, \sigma^2 I)\) directly, rewrite it:
\[\mathbf{z} = \boldsymbol{\mu} + \boldsymbol{\sigma} \odot \boldsymbol{\epsilon}, \qquad \boldsymbol{\epsilon} \sim \mathcal{N}(\mathbf{0}, I)\]
The randomness is moved into \(\epsilon\), which doesn’t depend on any parameters.
\(z\) is now a deterministic, differentiable function of \(\mu\) and \(\sigma\) (given \(\epsilon\)). Gradients flow cleanly.
Key design decisions for our dog VAE:
Replace MaxPool with Conv stride 2 — learnable downsampling gives the encoder more control over what information is preserved vs. discarded
Symmetry between encoder and decoder — ConvTranspose stride 2 mirrors Conv stride 2. Balanced reconstruction problem.
No skip connections — U-Net skip connections let information bypass the bottleneck. That’s cheating for generation. If the decoder gets pixel details from skip connections, it ignores z → posterior collapse.
We’ve used BatchNorm everywhere this semester. It stabilized training, enabled deeper networks. Why stop?
The problem: BatchNorm normalizes across the batch. Each sample’s normalization depends on what other images happen to be in the same minibatch.
The encoder needs per-image posterior parameters — \(\mu(x)\) and \(\sigma^2(x)\) that reflect this specific image. BatchNorm contaminates those outputs with information from other images.
Worse: at generation time there is no input image. We’re decoding random \(z\)’s. The batch statistics are meaningless — they correspond to nothing the decoder is generating.
The fix: GroupNorm. Instead of normalizing across the batch, GroupNorm normalizes across groups of channels within a single image.
Each image’s normalization depends only on itself. No cross-contamination between samples. Identical behavior at training and test time — no running averages, no train/eval mode mismatch.
This works because feature maps channels can be arranged in any order.
Organize them so that each group contains channels that are highly correlated
Rule of thumb: use GroupNorm in generative models.
The encoder produces two vectors, not one:
\(\boldsymbol \mu\) = the “best guess” location in latent space for this image
\(\log \boldsymbol{\sigma}^2\) = how uncertain the encoder is about that location
We output \(\log \boldsymbol{\sigma}^2\) instead of \(\boldsymbol{\sigma}^2\) for numerical stability — it can be any real number, and we exponentiate when needed. \(\boldsymbol{\sigma}^2\) must be positive; \(\log \boldsymbol{\sigma}^2\) is unconstrained.
def vae_loss(recon, target, mu, log_var, beta=1.0):
recon_loss = nn.functional.mse_loss(recon, target, reduction='sum') / target.shape[0]
kl_loss = torch.sum(-0.5 * torch.mean(1 + log_var - mu.pow(2) - log_var.exp())) / target.shape[0]
return recon_loss + beta * kl_loss, recon_loss, kl_lossThe entire ELBO in four lines. Reconstruction (MSE) + \(\beta\) × KL penalty.
def forward(self, x):
# Encode
mu, log_var = self.encode(x)
# Sample (reparameterized)
z = self.reparameterize(mu, log_var)
# Decode
recon = self.decode(z)
return recon, mu, log_varEncode → sample → decode. One forward pass. Backprop updates both encoder and decoder jointly.
VAEs are notoriously picky at the beginning of training.
Now, I’ll outline some training tricks that help VAEs settle into decent generation machines.
If we turn on the full KL penalty from the start, the model never learns to reconstruct — the penalty dominates and pushes everything to N(0, I) immediately. The decoder gets no useful signal.
KL annealing: start with \(\beta\) = 0 (pure autoencoder), then linearly increase \(\beta\) to 1 over the first 20ish epochs.
This lets the model first learn good reconstructions, then gradually impose latent space structure. The encoder and decoder establish useful features before the KL term asks them to organize.
Even with annealing, some latent dimensions can collapse — their KL goes to zero.
The encoder has learned to ignore them entirely!
The decoder reconstructs without those dimensions. They carry no signal — just noise that matches the prior.
Free bits (Kingma et al., 2016): set a minimum KL threshold λ per dimension. If a dimension’s KL is below λ, don’t penalize it.
\[\mathcal{L}_{KL} = \sum_{j=1}^{d} \max\left(\lambda, \; D_{KL}(q_\phi(z_j | \mathbf{x}) \| p(z_j))\right)\]
Each dimension gets a budget of λ nats of information for free. The encoder isn’t penalized for using a dimension up to that budget.
Think of it as a minimum occupancy requirement — each latent dimension must carry at least λ nats of information before the regularizer kicks in.
Without free bits: lazy encoder kills unused dimensions.
With free bits: every dimension has incentive to carry signal.
VAEs don’t converge to a fixed point — they keep improving the longer you train them. Reconstruction gets sharper, latent space gets more organized.
This means: don’t let the learning rate decay to zero. Standard schedulers (ReduceLROnPlateau, cosine annealing to 0) may kill training prematurely.
Options:
Cosine annealing with warm restarts — periodic LR bumps that push the model out of local optima
Cosine annealing to a nonzero floor (eta_min = 1e-5)
Simple constant LR (sometimes the best option for VAEs)
Cosine annealing with warm restarts: the LR drops smoothly, then jumps back up. Each restart lets the model escape the current basin and explore new regions.
scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(
optimizer, T_0=50, T_mult=2, eta_min=1e-5
)This often beats standard scheduling for VAEs because the loss landscape is non-convex and multi-modal — the model benefits from periodic exploration.
Training reconstructions will be a little blurry. This is expected.
The Gaussian decoder averages over posterior samples, and MSE rewards the mean prediction. The mean of several sharp images is a soft, blurry compromise.
But validation set reconstructions should still be recognizable dogs. The model generalizes — unlike the deterministic autoencoder, which memorized training images and produced garbage on everything else.
The payoff moment.
Sample \(\mathbf z \sim \mathcal N(\mathbf 0, \mathbf I)\). Decode.
Get… actual dogs. Blurry dogs, but dogs. Coherent structure, correct anatomy (mostly), recognizable breeds.
During training: sample z ~ N(μ, σ²I) to fill in the latent space. The stochastic noise is essential for learning.
During evaluation: skip the sampling, use μ directly.
This removes stochastic noise and gives the decoder the posterior’s best estimate. Helps a little with blurriness. But the fundamental issue remains — MSE treats pixels independently.
MSE treats each pixel independently:
\[\text{MSE} = \frac{1}{N} \sum_i (x_i - \hat{x}_i)^2\]
Pixel (32, 17) doesn’t know about pixel (32, 18). But we know neighboring pixels are highly correlated — edges, textures, and shapes span many pixels.
When the decoder is uncertain, the MSE-optimal output is the pixel-wise mean of all plausible images.
The mean of sharp images is blurry — like averaging 10 photos of a face and getting a smooth blob.
Instead of comparing pixels, compare features.
Take a pretrained VGG network (same transfer learning backbone from L13). Pass both the real image and reconstruction through VGG. Compare the intermediate feature maps:
\[\mathcal{L}_{\text{perceptual}} = \sum_{\ell} \left\| \phi_\ell(\mathbf{x}) - \phi_\ell(\hat{\mathbf{x}}) \right\|^2\]
where \(\phi_\ell\) is the feature map at layer ℓ of VGG.
VGG features capture spatial relationships between pixels. Early layers detect edges and textures. Later layers detect object parts and compositions.
Two images can have low perceptual loss even if they differ pixel-by-pixel — as long as they have the same visual structure.
class VGGPerceptualLoss(nn.Module):
def __init__(self):
super().__init__()
vgg = torchvision.models.vgg16(pretrained=True).features
self.blocks = nn.ModuleList([
vgg[:4], # relu1_2
vgg[4:9], # relu2_2
vgg[9:16], # relu3_3
])
for p in self.parameters():
p.requires_grad = FalseThis frees the decoder from averaging over pixels. It can produce sharp, coherent outputs.
We sample z ~ N(0, I) because that’s our prior.
But the actual distribution of encoded z-values — the aggregate posterior — isn’t exactly N(0, I). The KL penalty pulls toward it, but doesn’t force an exact match.
Some regions of N(0, I) have high prior density but low aggregate posterior density. Sampling there produces weaker generations.
The model oversamples the “easy” modes and undersamples the diversity.
After training the first VAE, train a second, smaller VAE on the encoded latent vectors {μ₁, …, μₙ}.
The second VAE learns the actual distribution of latent codes.
Sampling pipeline:
Sample \(\mathbf{z}_2 \sim \mathcal{N}(\mathbf{0}, I)\) from the second VAE’s prior
Decode through the second VAE → get \(\mathbf{z}_1\)
Decode through the original VAE → get an image
The first VAE learned a good latent space and a good decoder. The only problem was the mismatch between where we sample and where the data lives.
The second VAE corrects exactly that mismatch without touching the original VAE.
Key detail: train the two stages sequentially, not jointly. The first stage establishes good representations. The second stage fixes the sampling distribution. Joint training degrades both (Dai & Wipf, 2019).
The second VAE’s job is easier — the distribution of μ-vectors is already much closer to Gaussian than raw pixel data. The residual mismatch at the second level is negligible.
Generated images should now cover a wider variety of dogs — different breeds, poses, colorings.
The samey-ness is reduced because we’re sampling from a distribution that matches the actual latent structure.
Two directions:
Conditional VAEs — control what you generate. “Give me a golden retriever.” Condition the encoder and decoder on class labels or other attributes.
VQ-VAE — replace the continuous Gaussian bottleneck with a discrete codebook. Eliminates blurriness entirely. But creates a new problem: how do you sample from a discrete codebook?
That question will lead us from generation to sequences.