AI Knowledge Tree

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:

  1. Sample a latent z∼p(z)z \sim p(z) from a simple prior (typically N(0,I)\mathcal{N}(0, I)).
  2. Sample x∼pθ(x∣z)x \sim p_\theta(x \mid z) from a decoder distribution.

The data likelihood is pθ(x)=∫pθ(x∣z) p(z) dzp_\theta(x) = \int p_\theta(x \mid z)\, p(z)\, dz. The integral is intractable for any interesting decoder, so we cannot maximize log⁡pθ(x)\log p_\theta(x) directly.

The ELBO

Introduce an approximate posterior qϕ(z∣x)q_\phi(z \mid x) produced by an encoder. By the standard variational identity:

log⁡pθ(x)=Eqϕ(z∣x) ⁣[log⁡pθ(x∣z)]−DKL(qϕ(z∣x) ∥ p(z))+DKL(qϕ(z∣x) ∥ pθ(z∣x)).\log p_\theta(x) = \mathbb{E}_{q_\phi(z \mid x)}\!\left[\log p_\theta(x \mid z)\right] - D_{\text{KL}}(q_\phi(z \mid x)\,\|\,p(z)) + D_{\text{KL}}(q_\phi(z \mid x)\,\|\,p_\theta(z \mid x)).

The last KL is non-negative, so

log⁡pθ(x)≥Eqϕ(z∣x)[log⁡pθ(x∣z)]⏟reconstruction−DKL(qϕ(z∣x) ∥ p(z))⏟prior matching.\log p_\theta(x) \ge \underbrace{\mathbb{E}_{q_\phi(z \mid x)}[\log p_\theta(x \mid z)]}_{\text{reconstruction}} - \underbrace{D_{\text{KL}}(q_\phi(z \mid x)\,\|\,p(z))}_{\text{prior matching}}.

This is the evidence lower bound (ELBO). Maximize it (equivalently, minimize its negation) jointly over θ\theta and ϕ\phi.

The two terms have intuitive readings:

  • Reconstruction term: the decoder should make xx likely given zz sampled from the encoder. For a Gaussian decoder, this becomes −∥x−gθ(z)∥2-\lVert x - g_\theta(z)\rVert^2, 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: qϕ(z∣x)=N(μϕ(x), diag⁡(σϕ(x)2))q_\phi(z \mid x) = \mathcal{N}(\mu_\phi(x),\ \operatorname{diag}(\sigma_\phi(x)^2)), with μϕ\mu_\phi and log⁡σϕ\log \sigma_\phi produced by an encoder MLP.

With this choice and standard normal prior p(z)=N(0,I)p(z) = \mathcal{N}(0, I), the KL has a closed form:

DKL(qϕ ∥ p)=12∑j(μj2+σj2−1−log⁡σj2).D_{\text{KL}}(q_\phi \,\|\, p) = \frac{1}{2}\sum_j(\mu_j^2 + \sigma_j^2 - 1 - \log \sigma_j^2).

No Monte Carlo estimation needed for the KL.

To calibrate what this penalty costs, read it one latent dimension at a time, 12(μ2+σ2−1−log⁡σ2)\tfrac12(\mu^2 + \sigma^2 - 1 - \log\sigma^2):

  • μ=0, σ=1\mu = 0,\ \sigma = 1: the code already matches the prior, KL =0= 0. No penalty.
  • μ=1, σ=1\mu = 1,\ \sigma = 1: shifted one standard deviation off-center, KL =0.5= 0.5 nat. A modest pull back toward the origin.
  • μ=2, σ=1\mu = 2,\ \sigma = 1: two off-center, KL =2= 2 nats. The cost grows quadratically in the mean.
  • μ=0, σ=0.1\mu = 0,\ \sigma = 0.1: centered but the posterior has collapsed to a near delta, KL ≈1.81\approx 1.81 nats. The same σ=0.1\sigma = 0.1 at μ=3\mu = 3 costs ≈6.31\approx 6.31 nats.

Two ways to drift carry different prices, which is the distinction the loss enforces. A confident-but-centered code (μ=0, σ=0.1\mu = 0,\ \sigma = 0.1) and an uncertain-but-displaced code (μ=1, σ=1\mu = 1,\ \sigma = 1) are both off the prior, but the over-confident one costs 1.811.81 nats against 0.50.5: the KL punishes a posterior that pins zz down (small σ\sigma) 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 z∼qϕ(z∣x)z \sim q_\phi(z \mid x), which makes the loss non-differentiable in ϕ\phi (you can't backprop through a sample).

Reparameterization: write the sample as a deterministic function of ϕ\phi and a standard normal noise:

z=μϕ(x)+σϕ(x)⊙ϵ,ϵ∼N(0,I).z = \mu_\phi(x) + \sigma_\phi(x) \odot \epsilon, \qquad \epsilon \sim \mathcal{N}(0, I).

Now zz is a differentiable function of ϕ\phi (with ϵ\epsilon random but treated as input), and backprop flows through the sample. This is the key trick that made VAE training practical[2].

Training procedure

  1. Forward: encoder produces μ(x),σ(x)\mu(x), \sigma(x); sample zz via reparameterization; decoder produces x^\hat x.
  2. Loss: reconstruction + KL.
  3. Backward: gradient flows through both terms via reparameterization.

Inference (generation): sample z∼N(0,I)z \sim \mathcal{N}(0, I) 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 zz 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).

The VAE's KL term, KL(N(μ,σ²)‖N(0,1)) = ½(μ²+σ²−1−2lnσ), is zero only at μ=0, σ=1 and grows as the code drifts: the pressure that keeps the latent space close to the prior.
The VAE's KL term, KL(N(μ,σ²)‖N(0,1)) = ½(μ²+σ²−1−2lnσ), is zero only at μ=0, σ=1 and grows as the code drifts: the pressure that keeps the latent space close to the prior.

Check it on arrays

python · numpy
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 q=N(μ,σ2)q = \mathcal{N}(\mu, \sigma^2) and prior p=N(0,1)p = \mathcal{N}(0, 1), show that DKL(q ∥ p)=12(μ2+σ2−1−log⁡σ2)D_{\text{KL}}(q\,\|\,p) = \tfrac12(\mu^2 + \sigma^2 - 1 - \log \sigma^2).

Solution

Step 1. Write the KL as an expectation under qq of the log-density ratio:

DKL(q ∥ p)=Eq ⁣[log⁡q(z)−log⁡p(z)].D_{\text{KL}}(q\,\|\,p) = \mathbb{E}_q\!\left[\log q(z) - \log p(z)\right].

Step 2. Insert the two Gaussian log-densities. With log⁡q(z)=−12log⁡(2πσ2)−(z−μ)22σ2\log q(z) = -\tfrac12\log(2\pi\sigma^2) - \tfrac{(z-\mu)^2}{2\sigma^2} and log⁡p(z)=−12log⁡(2π)−z22\log p(z) = -\tfrac12\log(2\pi) - \tfrac{z^2}{2}, the constants −12log⁡2π-\tfrac12\log 2\pi cancel:

log⁡q(z)−log⁡p(z)=−12log⁡σ2−(z−μ)22σ2+z22.\log q(z) - \log p(z) = -\tfrac12\log\sigma^2 - \frac{(z-\mu)^2}{2\sigma^2} + \frac{z^2}{2}.

Step 3. Take the expectation under qq, using Eq[(z−μ)2]=σ2\mathbb{E}_q[(z-\mu)^2] = \sigma^2 and Eq[z2]=μ2+σ2\mathbb{E}_q[z^2] = \mu^2 + \sigma^2:

DKL=−12log⁡σ2−σ22σ2+μ2+σ22=−12log⁡σ2−12+μ2+σ22.D_{\text{KL}} = -\tfrac12\log\sigma^2 - \frac{\sigma^2}{2\sigma^2} + \frac{\mu^2 + \sigma^2}{2} = -\tfrac12\log\sigma^2 - \tfrac12 + \frac{\mu^2 + \sigma^2}{2}.

Step 4. Collect terms into 12(μ2+σ2−1−log⁡σ2)\tfrac12(\mu^2 + \sigma^2 - 1 - \log\sigma^2). At μ=0, σ=1\mu = 0,\ \sigma = 1 this is 00, 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 z=μ+σ ϵz = \mu + \sigma\, \epsilon with ϵ∼N(0,1)\epsilon \sim \mathcal{N}(0, 1). For a scalar loss ℓ(z)\ell(z), show that the gradient of Eq[ℓ(z)]\mathbb{E}_{q}[\ell(z)] with respect to μ\mu equals Eϵ[ℓ′(z)]\mathbb{E}_{\epsilon}[\ell'(z)], 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 μ\mu: Eq[ℓ(z)]=Eϵ∼N(0,1)[ℓ(μ+σϵ)]\mathbb{E}_q[\ell(z)] = \mathbb{E}_{\epsilon \sim \mathcal{N}(0,1)}[\ell(\mu + \sigma\epsilon)]. The randomness ϵ\epsilon carries none of the parameters.

Step 2. Differentiate under the expectation (legal because the integration measure no longer depends on μ\mu) and apply the chain rule with ∂z/∂μ=1\partial z / \partial \mu = 1:

∂∂μEϵ[ℓ(μ+σϵ)]=Eϵ ⁣[ℓ′(μ+σϵ)⋅1]=Eϵ[ℓ′(z)].\frac{\partial}{\partial \mu}\mathbb{E}_\epsilon[\ell(\mu + \sigma\epsilon)] = \mathbb{E}_\epsilon\!\left[\ell'(\mu + \sigma\epsilon)\cdot 1\right] = \mathbb{E}_\epsilon[\ell'(z)].

The gradient is the expected derivative of ℓ\ell at the sampled point, which a single Monte Carlo draw estimates with low variance.

Step 3. Without reparameterization the expectation is ∫ℓ(z) q(z;μ) dz\int \ell(z)\, q(z;\mu)\, dz and the parameter sits in the density. Differentiating gives the score-function (REINFORCE) form Eq[ℓ(z) ∂μlog⁡q(z;μ)]\mathbb{E}_q[\ell(z)\,\partial_\mu \log q(z;\mu)], which weights ℓ(z)\ell(z) 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 ℓ(z)=z2\ell(z) = z^2 and q=N(μ,σ2)q = \mathcal{N}(\mu, \sigma^2), Eq[z2]=μ2+σ2\mathbb{E}_q[z^2] = \mu^2 + \sigma^2, so ∂/∂μ=2μ\partial/\partial\mu = 2\mu. Estimate it by sampling z=μ+σϵz = \mu + \sigma\epsilon and averaging 2z2z (which is ℓ′(z)\ell'(z)), then check it matches 2μ2\mu.

Solution
python · numpy
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 Eϵ[2z]\mathbb{E}_\epsilon[2z] equals 2μ2\mu because the σϵ\sigma\epsilon 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. [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. [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.↩