Variational autoencoders
Derive the VAE's evidence lower bound and train it with the reparameterization trick.
The plain autoencoder learns a useful bottleneck but its latent space is unstructured: sampling random codes gives garbage. The variational autoencoder (VAE) adds a probabilistic structure: each input maps to a distribution over latents, and a prior keeps that distribution well-behaved. Sampling becomes principled, the model has a likelihood, and the math (the ELBO) generalizes to a huge family of latent-variable models[1].
The generative story
A VAE assumes data comes from a two-step process:
- Sample a latent from a simple prior (typically ).
- Sample from a decoder distribution.
The data likelihood is . The integral is intractable for any interesting decoder, so we cannot maximize directly.
The ELBO
Introduce an approximate posterior produced by an encoder. By the standard variational identity:
The last KL is non-negative, so
This is the evidence lower bound (ELBO). Maximize it (equivalently, minimize its negation) jointly over and .
The two terms have intuitive readings:
- Reconstruction term: the decoder should make likely given sampled from the encoder. For a Gaussian decoder, this becomes , the same as the plain autoencoder.
- KL term: the encoder distribution should stay close to the prior. This prevents the encoder from collapsing to delta functions (which would maximize reconstruction but make the latent space useless).
Gaussian encoder
The standard choice: , with and produced by an encoder MLP.
With this choice and standard normal prior , the KL has a closed form:
No Monte Carlo estimation needed for the KL.
To calibrate what this penalty costs, read it one latent dimension at a time, :
- : the code already matches the prior, KL . No penalty.
- : shifted one standard deviation off-center, KL nat. A modest pull back toward the origin.
- : two off-center, KL nats. The cost grows quadratically in the mean.
- : centered but the posterior has collapsed to a near delta, KL nats. The same at costs nats.
Two ways to drift carry different prices, which is the distinction the loss enforces. A confident-but-centered code () and an uncertain-but-displaced code () are both off the prior, but the over-confident one costs nats against : the KL punishes a posterior that pins down (small ) harder than one that merely sits a step away, which is what keeps the latent space smooth enough to interpolate through.
The reparameterization trick
The reconstruction term requires sampling , which makes the loss non-differentiable in (you can't backprop through a sample).
Reparameterization: write the sample as a deterministic function of and a standard normal noise:
Now is a differentiable function of (with random but treated as input), and backprop flows through the sample. This is the key trick that made VAE training practical[2].
Training procedure
- Forward: encoder produces ; sample via reparameterization; decoder produces .
- Loss: reconstruction + KL.
- Backward: gradient flows through both terms via reparameterization.
Inference (generation): sample from the prior, decode. Because the KL term forced the encoder's posterior distributions to stay close to the prior, the prior is the "right" distribution to sample from.
Posterior collapse
A common failure: the decoder is so expressive that it can fit the data without using at all. The encoder then becomes useless: it outputs the prior regardless of input, the KL term is zero, and the latent encodes nothing.
Symptoms: KL term goes to zero, latent dimensions look like random noise, reconstructions ignore the encoder.
Mitigations:
- KL annealing: scale the KL term up over training (start with 0, end with 1).
- Free bits: only penalize KL above a threshold per dimension.
- Architectural choices: less expressive decoder, autoregressive prior.
What VAEs are good at and not
Good at:
- Smooth, interpretable latent spaces (interpolation works).
- Likelihood estimation (the ELBO is a true bound on log-likelihood).
- Conditional generation when class labels or text are fed to the decoder.
- Building block for many later methods (latent diffusion uses a VAE).
Not as good as:
- Sample quality vs GANs (VAEs produce blurry samples because Gaussian decoders smooth over uncertainty).
- Sample quality vs diffusion (diffusion now dominates both).
VAEs hold up best as a representation-learning backbone and as a component in larger pipelines (e.g., the autoencoder in Stable Diffusion is a VAE).
Check it on arrays
import numpy as np
rng = np.random.default_rng(0)
# 1D toy: data from two well-separated modes
n = 1000
x = np.concatenate([rng.normal(-2, 0.3, n // 2), rng.normal(2, 0.3, n // 2)])
# Simple VAE: encoder produces mu, log_sigma; decoder produces x_hat
d_z, d_h = 2, 16
def init(in_d, out_d): return rng.normal(scale=0.3 / np.sqrt(in_d), size=(in_d, out_d))
W_enc1 = init(1, d_h); b_enc1 = np.zeros(d_h)
W_mu = init(d_h, d_z); b_mu = np.zeros(d_z)
W_logs = init(d_h, d_z); b_logs = np.zeros(d_z)
W_dec1 = init(d_z, d_h); b_dec1 = np.zeros(d_h)
W_dec2 = init(d_h, 1); b_dec2 = np.zeros(1)
def encode(x):
h = np.maximum(0, x[:, None] @ W_enc1 + b_enc1)
return h @ W_mu + b_mu, h @ W_logs + b_logs
def decode(z):
h = np.maximum(0, z @ W_dec1 + b_dec1)
return (h @ W_dec2 + b_dec2).ravel()
def vae_step(x, lr=0.01, kl_weight=0.5):
mu, log_sigma = encode(x)
sigma = np.exp(log_sigma)
eps = rng.normal(size=mu.shape)
z = mu + sigma * eps
xhat = decode(z)
recon = np.mean((xhat - x)**2)
kl = 0.5 * np.mean(np.sum(mu**2 + sigma**2 - 1 - 2 * log_sigma, axis=1))
return recon, kl, mu, sigma, xhat
# Train (single-step, no full backprop) just to check the forward and loss
recon, kl, mu, sigma, xhat = vae_step(x)
print(f"initial ELBO terms: recon = {recon:.3f}, KL = {kl:.3f}")
print(f"latent means range: [{mu.min():.2f}, {mu.max():.2f}]")
# Sampling from the prior and decoding gives synthetic data
z_sample = rng.normal(size=(20, d_z))
x_sample = decode(z_sample)
print(f"sampled from prior, decoded: {x_sample.round(2)[:10]}")The ELBO splits cleanly into reconstruction and KL; prior samples decode to outputs in the data range (training would push them toward the modes).
Exercises
Exercise 1. Derive the closed-form KL term for one latent dimension. With and prior , show that .
Solution
Step 1. Write the KL as an expectation under of the log-density ratio:
Step 2. Insert the two Gaussian log-densities. With and , the constants cancel:
Step 3. Take the expectation under , using and :
Step 4. Collect terms into . At this is , and it is positive elsewhere, matching the per-dimension penalty the lesson read off the scale.
Exercise 2. The reparameterization trick rewrites a sample as with . For a scalar loss , show that the gradient of with respect to equals , so the gradient passes through the sample. Contrast this with differentiating the same expectation when the sample is not reparameterized.
Solution
Step 1. Reparameterized, the expectation is over the fixed noise distribution, not over : . The randomness carries none of the parameters.
Step 2. Differentiate under the expectation (legal because the integration measure no longer depends on ) and apply the chain rule with :
The gradient is the expected derivative of at the sampled point, which a single Monte Carlo draw estimates with low variance.
Step 3. Without reparameterization the expectation is and the parameter sits in the density. Differentiating gives the score-function (REINFORCE) form , which weights itself rather than its derivative and typically has much higher variance. Reparameterization moves the parameter out of the measure so backprop can flow through the sample.
Exercise 3 (code). Verify the reparameterized gradient estimator against the analytic gradient. For and , , so . Estimate it by sampling and averaging (which is ), then check it matches .
Solution
import numpy as np
rng = np.random.default_rng(0)
mu, sigma = 1.5, 0.7
N = 2_000_000
eps = rng.normal(size=N)
z = mu + sigma * eps # reparameterized samples
grad_est = np.mean(2 * z) # E[l'(z)] with l(z) = z^2, l'(z) = 2z
grad_true = 2 * mu # d/dmu of (mu^2 + sigma^2)
print(f"reparam gradient estimate: {grad_est:.4f} analytic: {grad_true:.4f}")
assert np.isclose(grad_est, grad_true, atol=2e-3)The averaged derivative equals because the term has mean zero, confirming Exercise 2: the gradient flows through the sample as the expected derivative of the loss.
The next lesson covers VAE variants that improve disentanglement and discrete-latent control.
References
- [1]Kingma, D. P., & Welling, M. (2014). "Auto-encoding variational bayes". ICLR. arXiv:1312.6114. The original VAE: the ELBO and the amortized variational posterior.↩
- [2]Goodfellow, I., Bengio, Y., & Courville, A. (2016). Deep Learning, §20.10.3 (Variational Autoencoders). MIT Press. The reparameterization trick as a path to low-variance gradients.↩