Machine Learning ML-14.02
Variational autoencoders
Make the latent space smooth enough to sample from: probabilistic codes, the KL term, the reparameterization trick, the ELBO and latent interpolation.
The big idea
A plain autoencoder compresses well, but its latent space is a scatter of islands. Pick a random point between them and the decoder draws a smudge, because nothing in training ever asked that point to mean anything. To generate new data you want the opposite: a latent space you can sample from blindly, where every point decodes to something plausible and nearby points decode to similar things.
A variational autoencoderAn autoencoder whose encoder outputs a probability distribution over codes, trained so that sampled codes reconstruct the input and the code distribution stays close to a simple prior, usually a standard normal. (VAE) gets there with two changes. The encoder no longer outputs one point; it outputs a small cloud, a mean and a spread, and the decoder must reconstruct the input from a random point in that cloud. And a penalty pulls every cloud toward the same standard normal distribution centred on zero. Think of a library where each book must be shelved somewhere inside one room, and each shelf label is a little fuzzy. Fuzzy labels force similar books onto neighbouring shelves (a wrong guess still lands on something similar), and the single room means there are no empty wings. Then you can walk to any shelf and find a sensible book.
How it works
Step 1: A probabilistic encoder
For an input , the encoder outputs two vectors: a mean and a standard deviation , one pair per latent number. Together they define a Gaussian : "this input's code is probably around here". During training the decoder receives a sample from that Gaussian, not itself.
Step 2: The loss: reconstruction plus KL
Training balances two terms for each input:
The first term is the reconstruction loss from Autoencoders , now averaged over random codes from the cloud. For pixels in [0, 1] it is usually binary cross-entropy. The second term is the KL divergenceKullback-Leibler divergence: a measure of how much one probability distribution differs from another; zero only when they are identical. between the encoder's Gaussian and the standard normal prior. For one latent number it has a closed form:
Worked example: , gives . With , the code is in the right place but too certain: .
The two terms pull in opposite directions. Reconstruction wants each input's cloud to be tiny and far from the others, so codes are unambiguous. KL wants every cloud to be wide and centred on zero. The compromise packs the codes of all the data together around the origin, with overlapping clouds, so the space between training examples is filled with codes the decoder was actually trained on.
Step 3: The reparameterization trick
There is a technical snag. The loss involves sampling , and you cannot backpropagate through "draw a random number". The fix is to move the randomness outside the network:
Now is an ordinary differentiable function of and , with as a fixed input for this step, so gradients flow from the decoder back into the encoder. Kingma and Welling's 2013 paper, Auto-Encoding Variational Bayes, made this reparameterization of the bound the centre of the method; Rezende, Mohamed and Wierstra published a closely related approach, stochastic backpropagation, in 2014. In practice the encoder outputs rather than , so the spread is always positive.
Step 4: The ELBO
Why this particular loss? A VAE is a probabilistic model: draw from the prior, then draw from the decoder's distribution . Training it properly would maximize for the data, but that requires integrating over every possible , which is intractable. The negative of the loss above is a lower bound on , the evidence lower boundEvidence lower bound: a quantity that is always at most log p(x) and can be estimated and optimized; maximizing it trains both the encoder and the decoder of a VAE. (ELBO):
The gap between the two sides is how far the encoder's is from the true posterior over codes. So maximizing the ELBO does two useful things at once: it improves the model of the data, and it makes the encoder a better approximation of which codes could have produced each input. You do not need the derivation to use a VAE, but it explains the name: "variational" refers to fitting an approximate distribution by optimization.
Step 5: Sampling and interpolation
After training, throw the encoder away to generate: draw and decode it. Because training pulled all the codes toward that same normal, random draws land where the decoder has seen data. Walking a straight line between two codes decodes into a gradual change from one input to the other, since the space between them was trained too.
Try it
The same architecture as the autoencoder in the previous lesson (64, 64, 2, 64, 64 units), trained offline in numpy on the scikit-learn digits with the VAE loss; the page uses the real decoder's weights.
- Compare the scatter with the autoencoder's. The codes now fill a round blob centred on zero, about one unit wide in each direction, matching the prior's circles; the plain autoencoder's codes were spread over a much larger, ragged area.
- Press "Sample z from N(0, I)" several times. Almost every sample lands among training codes and decodes to a recognizable digit, which is exactly what random codes in the plain autoencoder's box failed to do.
- Interpolate from 4 to 7, then do the same in the previous lesson's autoencoder. Here the strip changes from 4 to 7 directly; the plain autoencoder passes through something like a 1 on the way. Then try 1 to 0: with only two latent numbers, the 1s overlap the 7s, so the start already looks like a 7. A 2-D latent space is great for plotting and too small for 10 digit classes.
Going deeper
Code: the KL formula and why reparameterize
import numpy as np
rng = np.random.default_rng(0)
# 1. The KL term for one latent number: q = N(mu, sigma^2) against the prior p = N(0, 1)
def kl(mu, sigma): # closed form used in every VAE
return 0.5 * (mu**2 + sigma**2 - 1) - np.log(sigma)
def log_normal(z, mu, sigma):
return -0.5 * np.log(2 * np.pi * sigma**2) - (z - mu) ** 2 / (2 * sigma**2)
for mu, sigma in [(0.0, 1.0), (1.0, 1.0), (2.0, 0.5), (0.0, 0.1)]:
z = rng.normal(mu, sigma, 200_000) # Monte Carlo estimate of E_q[log q - log p]
mc = np.mean(log_normal(z, mu, sigma) - log_normal(z, 0, 1))
print(f"mu={mu:3.1f} sigma={sigma:3.1f} KL formula {kl(mu, sigma):6.3f} sampled {mc:6.3f}")
# 2. Why the reparameterization trick: gradient of E[f(z)] with respect to mu,
# for f(z) = (z - 3)^2 and z ~ N(mu, sigma^2). The true answer is 2 * (mu - 3).
mu, sigma, n = 1.0, 0.8, 100
f = lambda z: (z - 3) ** 2
reparam, score = [], []
for trial in range(2000):
eps = rng.normal(size=n)
z = mu + sigma * eps # z is a function of mu: gradients pass through
reparam.append(np.mean(2 * (z - 3))) # d f(mu + sigma * eps) / d mu
score.append(np.mean(f(z) * eps / sigma)) # score-function (REINFORCE) estimator
print(f"\ntrue gradient {2 * (mu - 3):.2f}")
for name, g in [("reparameterized", reparam), ("score function ", score)]:
print(f"{name}: mean {np.mean(g):6.2f} spread (std) {np.std(g):5.2f}")Output
mu=0.0 sigma=1.0 KL formula 0.000 sampled 0.000
mu=1.0 sigma=1.0 KL formula 0.500 sampled 0.500
mu=2.0 sigma=0.5 KL formula 2.318 sampled 2.322
mu=0.0 sigma=0.1 KL formula 1.808 sampled 1.809
true gradient -4.00
reparameterized: mean -4.00 spread (std) 0.16
score function : mean -4.00 spread (std) 0.96 Two checks of the pieces above, in plain numpy.
- The closed-form KL is right. For four Gaussians, the formula and a Monte Carlo average of over 200,000 samples agree: 0.000, 0.500, 2.318 against 2.322, and 1.808 against 1.809. The last case, and , shows that being too certain is penalized even when the mean is perfect.
- Reparameterized gradients are far less noisy. For with , the true gradient of with respect to is -4. Both estimators average -4.00 over 2,000 trials of 100 samples, but the reparameterized one has a spread of 0.16 and the score-function estimator (the same REINFORCE idea used for policy gradients) 0.96, six times larger. Lower-variance gradients are why VAEs train with ordinary backpropagation.
Tuning the balance
Weighting the KL term by a factor trades the two goals off explicitly. Larger gives a smoother, more regular latent space and blurrier reconstructions; smaller gives sharper reconstructions and more holes. If the decoder is very powerful, it can learn to ignore entirely and the KL term drives every code to the prior: the codes carry no information. Watching the KL term during training catches this.
Two properties of the standard VAE matter in practice. The decoder is trained with a per-pixel loss on a single sample, so where several outputs are plausible it hedges toward their average, which tends to look soft. And the latent prior is simple by design. Later lessons in this module trade those properties for sharper samples, and latent diffusion systems use an autoencoder, sometimes with a slight VAE-style KL penalty, only for compression, leaving generation to a diffusion model.
Trade-offs
| Option | Wins when | Loses when |
|---|---|---|
| Plain autoencoder | Sharpest reconstructions for compression and features | Latent space has holes; cannot sample new data |
| VAE | Smooth latent space; sampling and interpolation; a likelihood bound | Softer, averaged outputs; must balance KL against reconstruction |
| VAE with large beta | Very regular, sometimes more interpretable latent axes | Blurrier reconstructions; risk of codes carrying little information |
Common mistakes
- Sampling from the latent space of a plain autoencoder and expecting realistic outputs.
- Forgetting the reparameterization trick and sampling inside the graph, so the encoder gets no gradient.
- Predicting directly with no positivity constraint; predict instead.
- Reporting only the reconstruction loss. Track the KL term too: near zero means the latent codes are being ignored.
- Using the encoder's random sample when you want a deterministic code for a downstream task; use .
Where you will see this
VAEs are used where a smooth, sampleable latent space matters more than the sharpest samples: generating and interpolating molecules or designs, anomaly detection with a likelihood-style score, and learning compact representations of images, audio or sensor data. Their most visible role today is as the compression stage of latent diffusion image generators: an autoencoder (in one of the two published variants, trained with a slight VAE-style KL penalty) maps images to a smaller latent grid, and the diffusion model works there, as the diffusion lesson in this module shows.
Check yourself
-
What does a VAE's encoder output for one input?
Show answer
B. A mean and a spread that define a Gaussian over codes The encoder outputs mu and sigma. Training decodes a random code drawn from that Gaussian, which forces nearby codes to decode to similar inputs.
-
Compute the KL term for one latent number with mu = 2 and sigma = 1.
Show answer
C. 2 0.5 x (4 + 1 - 1) - log 1 = 2. The penalty grows with the square of the distance of the mean from zero.
-
Why does the reparameterization z = mu + sigma x eps matter?
Show answer
B. It moves the randomness into an outside input eps, so z is a differentiable function of mu and sigma and gradients reach the encoder You cannot backpropagate through drawing a random number, but you can through mu + sigma x eps with eps held fixed. The code shows the resulting gradients are much less noisy than the score-function alternative.
-
During training, the KL term of a VAE drops to almost exactly zero and stays there. What is the likely problem?
Show answer
B. The decoder ignores the code; every input's distribution has collapsed to the prior, so the codes carry no information Zero KL means q(z | x) equals the prior for every x, so z says nothing about x. Weakening the decoder or reducing beta helps.
-
Why can you generate new digits by decoding z drawn from N(0, I) in a VAE but not in a plain autoencoder?
Show answer
B. The KL term pulled all training codes toward N(0, I), so random draws land where the decoder was trained A plain autoencoder's codes can sit anywhere, with untrained gaps between them. The VAE's prior defines where codes live.
Recap
- A VAE's encoder outputs a Gaussian (mu, sigma) per input, and the decoder reconstructs from a random sample of it.
- The loss is reconstruction plus the KL divergence to a standard normal prior; together they fill the latent space without holes.
- The reparameterization z = mu + sigma x eps makes sampling differentiable, giving low-variance gradients.
- The negative loss is the ELBO, a lower bound on log p(x); maximizing it fits both the model and the approximate posterior.
- Sampling z from N(0, I) and decoding generates new data; straight lines in latent space interpolate smoothly.