Variational autoencoders (VAE): new images from a continuous latent space
A generative model with a latent variable, a VAE written from scratch with the reparameterization trick, an ordered latent space, generating digits from noise compared with an ordinary autoencoder, the effect of β and collapse, interpolation, and where VAEs are used.
Series AI basics Part 44 of 52
- Map of artificial intelligence: what is what and how it all fits together
- A brief history of artificial intelligence: from Turing to language models
- Data, model, training, inference: the basic terms on a single example
- How to read mathematical notation: symbols, indices, sums and quantifiers
- Sets and relations: the building blocks of definitions in AI
- Functions and mappings: why every AI model is a function
- Logic and proofs: the rules AI reasons by
- Graphs and trees: the structure that both search and neural networks stand on
- Linear algebra for machine learning: vectors, matrices and why AI mostly multiplies
- Derivatives, the gradient and the chain rule: how a model finds out which way to learn
- Gradient descent: how a model learns
- What probability is: from rolling dice to the Kolmogorov axioms
- Conditional probability and Bayes’ theorem: how to update your estimate when you learn something new
- Random variables and the cumulative distribution function: one function that tells you everything about a distribution
- Probability density and probability mass functions: why a continuous variable has zero probability at a point
- Expected value, variance and covariance: where a distribution lies, how wide it is and what goes with what
- Overview of probability distributions: Bernoulli, binomial, Poisson, uniform, normal and exponential
- Law of large numbers and central limit theorem: why averages behave so predictably
- Statistics and parameter estimation: maximum likelihood, MAP and confidence intervals
- Information theory: entropy, cross-entropy and KL divergence — where loss functions come from
- State-space search: BFS, DFS, UCS and A* with animations
- Games and adversaries: minimax, alpha-beta pruning and Monte Carlo tree search
- Constraint satisfaction: backtracking, constraint propagation and AC-3 on sudoku and a map of Czech regions
- Logic and knowledge systems: forward and backward chaining, resolution and expert systems
- Planning: STRIPS, the blocks world, heuristics from relaxation and planning in AI agents
- Local search and simulated annealing: a travelling salesman through the Czech regional capitals
- Genetic algorithms: population, crossover, mutation and selection on a backpack for a hike
- Evolution strategies and genetic programming: from the one-fifth rule to Kepler’s law
- Ant colony optimization: how a pheromone trail finds a short path
- Particle swarm optimisation: inertia, personal experience and the swarm’s experience
- Neuroevolution: evolving the weights and shape of neural networks, and the NEAT algorithm
- Fuzzy sets and membership functions: how to write down “somewhat warm”
- Fuzzy logic and probability: why “somewhat warm” does not mean “probably warm”
- Fuzzy logic: t-norms, implications and IF–THEN rules
- Fuzzy control: Mamdani and Sugeno from a fan to a pole on a cart
- Fuzzy clustering: the fuzzy c-means algorithm and points between clusters
- Neuro-fuzzy systems (ANFIS): fuzzy rules that learn from data
- Markov chains: the transition matrix, the stationary distribution and PageRank
- Hidden Markov models: the Viterbi algorithm and the dishonest casino
- Bayesian networks: why the grass is wet and what it means for the sprinkler
- The Kalman filter: how to estimate where a car is and how fast it goes from imprecise measurements
- Types of machine learning: supervised, unsupervised, semi-supervised and more
- Prompts, context and RAG: how to give a model the right information
- Variational autoencoders (VAE): new images from a continuous latent space
- Generative adversarial networks (GAN): the forger versus the detective
- Diffusion models: how pure noise becomes an image
- Markov decision processes: states, actions, rewards and the Bellman equations
- Q-learning and DQN: learning the value of actions, from a table to a neural network
- Policy gradient and PPO: learning the policy directly
- How an AI agent works: a language model in a loop with tools
- Programming with AI agents: the task, tests and review
- Evaluating and securing AI systems: how to measure and how to keep watch
Full series syllabus 52 of 83 topics published
- Why an ordinary autoencoder is not good at generating.
- How a VAE works: a distribution instead of a point, the KL penalty and the reparameterization trick.
- What β costs, why VAE images are blurry, and where VAEs are used today.
Useful to know first: Information theory: entropy, cross-entropy and KL divergence — where loss functions come from
The autoencoder from the part on representations can squeeze an image into a few numbers and put it back together from them. An obvious question: if the decoder can turn a code into a digit, couldn't we give it a code we made up and get a new digit that nobody ever drew? With an ordinary autoencoder this hardly works. The variational autoencoder is a modification that makes exactly this possible — and it is the first of the generative models we will look at in this section of the series.
It builds on the parts Autoencoders and Information theory (KL divergence).
1. From compression to generation
Definition — generative model
Let 𝒳 be the set of possible data (here 8 × 8 images with pixel brightnesses in [0, 1], i.e. 𝒳 = [0, 1]64), n ≥ 1, and let x1, …, xn ∈ 𝒳 be training data, which we assume to be independent samples from an unknown distribution pdata on 𝒳. Let Θ be a set of admissible parameters and, for every θ ∈ Θ, let pθ be a distribution on 𝒳. A generative model is a family of distributions (pθ)θ ∈ Θ together with a procedure for sampling from the distribution pθ (producing new x ~ pθ). Learning chooses θ so that pθ is as close as possible to pdata, typically as the maximum likelihood estimate
θ̂ = argmaxθ ∈ Θ ∑i=1n log pθ(xi).
A latent variable model additionally introduces a latent space 𝒵 = ℝk (k ≥ 1), a fixed density p(z) on it (the prior), and for every code z ∈ 𝒵 a distribution pθ(x | z) on 𝒳 (the decoder). The distribution of the data is then
pθ(x) = ∫𝒵 pθ(x | z) p(z) dz,
and sampling happens in two steps:
- draw z ~ p(z);
- draw x ~ pθ(x | z) — in practice (including here) the output of the decoder, i.e. the pixel probabilities, is often taken directly.
The new x thus look like the data but are not copies of it. From here on we drop the index θ and write p(x | z), p(x).
A classifier answers “what is this”. A generative model can answer “show me something that could be in the data”. The recipe: roll a few numbers with dice, hand them to a painter (the decoder), and the painter paints a picture from them.
p(x) = ∫ p(x | z) · p(z) dz
- p(z)
- distribution of the code (the prior) — we choose it ourselves and keep it simple, here N(0, I), because we will need to draw from it when generating
- p(x | z)
- the decoder — how probable the image x is given the code z; here a network that returns, for every pixel, the probability that it is lit
- ∫ … dz
- a sum over all possible codes — the image could have come from any code; but we cannot compute an integral over the whole latent space, which is why the VAE replaces it with an estimate (below)
The catch with an ordinary autoencoder: we do not know where to draw the code from. The encoder places the digits in the latent space wherever it pleases. In our network with a two-dimensional code, the codes of the test digits lie around the point (-3.9, -0.9) with standard deviations 4.3 and 6.0, and between the groups there are holes where the decoder has never seen anything.
2. The variational autoencoder
Definition — variational autoencoder (VAE)
Let 𝒳 = [0, 1]64 be the set of images, k ≥ 1 the dimension of the code, I the k × k identity matrix and β > 0. All logarithms in this part are natural (log = ln). A variational autoencoder (VAE; Diederik Kingma and Max Welling, 2013) consists of three parts:
- the encoder — a neural network that assigns to an image x ∈ 𝒳 vectors μ(x) ∈ ℝk and log σ²(x) ∈ ℝk (logarithm taken componentwise); these determine the distribution of the code q(z | x) = N(μ(x), diag σ²(x)), i.e. the coordinates z1, …, zk are independent and zj ~ N(μj(x), σj²(x)) (the normal distribution);
- the decoder — a neural network that assigns to a code z ∈ ℝk a distribution p(x | z) on 𝒳 (here via a vector of pixel probabilities y(z) ∈ (0, 1)64);
- the prior p(z) = N(0, I), the standard normal distribution on ℝk.
The parameters of both networks are learned by minimizing the average over the training images of the loss
L(x) = −Ez ~ q(z | x)[log p(x | z)] + β · DKL(q(z | x) ‖ N(0, I)),
where for densities q, p on ℝk, DKL(q ‖ p) = ∫ q(z) log q(z)/p(z) dz is the continuous analogue of the KL divergence from the part on information (with the natural logarithm, in nats). The first term of the loss is the reconstruction error (here the binary cross-entropy of the pixels, see below), the second is a penalty for how much the code distribution differs from the prior. For q = N(μ, diag σ²) it comes out as
DKL(q ‖ N(0, I)) = ½ ∑j=1k (μj² + σj² − log σj² − 1).
A VAE thus encodes an image into a distribution, not a point, and penalizes it for deviating from the distribution that will later be used for generating. With β = 1, −L(x) is a lower bound on log p(x) (the ELBO, theorem below).
The encoder may not say “this seven lives exactly here”, only “somewhere in this little cloud”. The decoder then has to be able to draw a seven from any point of the cloud. And the KL penalty pushes all the clouds towards the origin and does not let them get too small — so they overlap, fill the space, and no holes are left between them.
L(x) = −Ez ~ q(z | x)[ log p(x | z) ] + β · DKL( q(z | x) ‖ N(0, I) )
- −Ez ~ q[log p(x | z)]
- reconstruction error — how badly the decoder draws x from a code drawn from the cloud. The expectation over the whole cloud (not just its centre) forces the decoder to draw correctly in the whole neighbourhood of the code; the minus is there because we want the log-probability as large as possible and the loss as small as possible
- β
- weight of the penalty — sets the trade-off between accuracy and order in the space; β = 1 is exactly the ELBO, other values are tried in the section on β
- DKL(q(z | x) ‖ N(0, I))
- distance of the cloud from the distribution we will draw from — without it the encoder would shrink the clouds to points and scatter them far apart (reconstruction would be better), and we would be back to an ordinary autoencoder
DKL(q(z | x) ‖ N(0, I)) = ½ ∑j=1k ( μj² + σj² − log σj² − 1 )
- DKL
- the penalty from the previous formula, computed exactly for a normal distribution; the code coordinates are independent, hence the sum over j
- μj²
- shift of the cloud’s centre from the origin — it grows quadratically, so distant codes cost a lot and the encoder pulls them towards zero, where we will be drawing
- σj² − log σj²
- width of the cloud — the function s − log s has its minimum 1 exactly at s = 1; it penalizes a cloud that is too wide, but above all one that is too narrow (−log σ² → ∞ for σ → 0), so that the cloud does not turn into a point
Theorem — the ELBO is a lower bound on the log-likelihood
Let p(z) be a density on ℝk (the prior), for every z ∈ ℝk let p(x | z) be a distribution on 𝒳 (the decoder), let x ∈ 𝒳 be an image with p(x) = ∫ p(x | z) p(z) dz > 0, and let
p(z | x) = p(x | z) p(z)/p(x)
be the posterior density of the code (Bayes’ theorem). Let q(z | x) be any density on ℝk that is positive only where p(z | x) is positive, and for which the expectations Eq = Ez ~ q(z | x) below exist and are finite. Then
log p(x) = Eq[log p(x | z)] − DKL(q(z | x) ‖ p(z)) + DKL(q(z | x) ‖ p(z | x)),
and therefore
log p(x) ≥ Eq[log p(x | z)] − DKL(q(z | x) ‖ p(z)),
with equality if and only if q(· | x) = p(· | x).
Proof. log p(x) does not depend on z, so it equals its own expectation over q. By Bayes’ theorem, for every z with q(z | x) > 0
p(x) = p(x | z) p(z)/p(z | x) = p(x | z) · p(z)/q(z | x) · q(z | x)/p(z | x),
where we multiplied the fraction by q(z | x) in the numerator and the denominator. We take the logarithm and the expectation over z ~ q(z | x):
log p(x) = Eq[log p(x | z)] + Eq[log p(z)/q(z | x)] + Eq[log q(z | x)/p(z | x)].
By definition, the second term is −DKL(q(z | x) ‖ p(z)), the third is DKL(q(z | x) ‖ p(z | x)). The KL divergence is always non-negative and equal to zero exactly for identical distributions (Gibbs’ inequality from the part on information theory, which holds for densities as well), so dropping the third term gives both the inequality and the condition for equality. ∎
The right-hand side of the inequality is the ELBO (evidence lower bound), and with β = 1 it is exactly −L(x). By minimizing L we therefore raise a lower bound on log p(x) without having to compute the integral. The bound is the tighter, the closer q(z | x) is to the true posterior distribution p(z | x).
Definition — binary cross-entropy of pixels
Let x ∈ [0, 1]64 be an image (pixel brightnesses xj, j = 1, …, 64) and y ∈ (0, 1)64 the output of the decoder, where we understand yj as the probability that pixel j is lit. The binary cross-entropy of the image x with respect to the prediction y is
BCE(x, y) = −∑j=164 [ xj log yj + (1 − xj) log(1 − yj) ].
For a black-and-white image x ∈ {0, 1}64 and a decoder under which the pixels are lit independently, each with probability yj(z), exactly BCE(x, y(z)) = −log p(x | z). For grey brightnesses xj ∈ (0, 1) the same expression is used as the reconstruction error.
An example for one pixel: it is lit (x = 1) and the decoder says 0.9 → −ln 0.9 ≈ 0.105; it says 0.5 → ln 2 ≈ 0.693; it is not lit (x = 0) and the decoder says 0.9 → −ln 0.1 ≈ 2.303. A confident mistake costs the most. Because the output is a sigmoid, yj = 1/1 + e−aj, the derivative with respect to its input aj is simply ∂BCE/∂aj = yj − xj — just as in logistic regression.
Definition — reparameterization trick
Let μ(x) ∈ ℝk and σ(x) ∈ (0, ∞)k be the outputs of the encoder for an image x. Instead of drawing z ~ q(z | x) = N(μ(x), diag σ²(x)) directly, the reparameterization trick proceeds as follows:
- draw ε = (ε1, …, εk) with independent εj ~ N(0, 1) — noise whose distribution does not depend on the network’s parameters;
- for j = 1, …, k set zj = μj(x) + σj(x) · εj.
The z obtained this way has exactly the distribution q(z | x) (a linear transformation of a normal variable is normal), and at the same time it is a differentiable function of the parameters: ∂zj/∂μj = 1 and ∂zj/∂σj = εj. The gradient therefore flows through backpropagation all the way into the encoder, which is not possible with plain sampling z ~ N(μ, σ²).
We “factor the randomness out”: instead of “draw a point from the cloud” we say “draw generic noise and shift and stretch it as the encoder says”. Shifting and stretching are ordinary arithmetic, and that can be differentiated.
zj = μj(x) + σj(x) · εj, σj = exp(½ · log σj²), εj ~ N(0, 1)
- μj(x)
- centre of the cloud, an output of the encoder — ∂zj/∂μj = 1, the decoder’s gradient passes into the encoder unchanged
- σj(x)
- width of the cloud — the encoder does not return σ but log σ², which may be any real number; the exponential always turns it into a positive width, so the network does not have to respect any constraint. ∂zj/∂σj = εj
- εj ~ N(0, 1)
- all of the randomness — it does not depend on any parameter, so nothing has to be differentiated through it; when generating, we skip the encoder and draw z directly from N(0, I)
Example — one test digit (0) in the trained VAE with k = 2
encoder: μ = (0.285, 1.587), log σ² = (−2.695, −1.931) → σ = (0.260, 0.381)
j = 1: ½ (0.081 + 0.068 + 2.695 − 1) ≈ 0.922
j = 2: ½ (2.518 + 0.145 + 1.931 − 1) ≈ 1.797 → DKL ≈ 2.72 nats
reparameterization with ε = (0.5, −1.0): z = (0.285 + 0.260 · 0.5, 1.587 − 0.381 · 1.0) = (0.415, 1.206)
(colours as in the formulas for DKL and reparameterization above)
Most of the penalty here comes not from the distance to the origin but from the narrowness of the cloud: the term −log σ² contributes 2.695 and 1.931. The encoder would like the code to be as precise as possible; DKL keeps it wider. The average DKL over the test digits is 3.42 nats. The reconstruction error of this digit (from the centre μ) is 34.6 nats over 64 pixels.
def krok_vae(davka, beta=1.0):
for x in davka:
mu, logvar = koder(x) # k + k numbers
sigma = exp(0.5 * logvar)
eps = normal(0, 1, size=len(mu)) # randomness from outside
z = mu + sigma * eps # reparameterization trick
y = dekoder(z) # pixel probabilities
rek = -sum(x * log(y) + (1 - x) * log(1 - y))
kl = 0.5 * sum(mu**2 + exp(logvar) - logvar - 1)
zpetne_sireni(rek + beta * kl) # gradient flows z → mu, sigma → encoder
adam_krok() # average gradient of the batch
Our network: 64 pixels → 32 neurons (tanh) → μ and log σ² (k numbers each) → 32 neurons → 64 pixels (sigmoid), trained with Adam for 150 epochs on 600 synthetic digits. The same network, only without the noise ε and without the KL penalty, is an ordinary autoencoder — so the comparison is fair.
3. An ordered latent space
Both networks group identical digits together. The autoencoder, however, scatters them over a large area — both circles of the normal distribution are small compared with it. The VAE codes lie around the origin with standard deviations 1.14 and 1.04, that is, exactly where we will be drawing from.
What the whole space looks like is shown by a grid: regularly spaced points of the latent space, each passed through the decoder.
In the VAE the digits flow smoothly into one another and almost every cell is a readable digit. To be fair: in the plane the autoencoder’s decoder is also quite smooth, and its grid also contains mostly digits. The difference only shows in full with a code of more dimensions.
4. Generation: new digits from noise
We draw 200 codes and have them decoded: for the VAE from N(0, I), for the autoencoder (to give it a fair chance) from a normal distribution fitted to its actual codes. We measure quality by the distance (mean squared error) of a sample to the nearest training digit. Real test digits are on average 0.017 from the nearest training digit, random noise 0.159.
Definition — distance to the nearest training digit and the fitted distribution
Let x(1), …, x(600) ∈ [0, 1]64 be the training digits. The distance of an image y ∈ [0, 1]64 to the nearest training digit is
d(y) = mini ∈ {1, …, 600} 1/64 ∑j=164 (yj − x(i)j)²,
i.e. the mean squared error (MSE) to the most similar digit. For drawn images y(1), …, y(200), the quality of the generator is the average
d̄ = 1/200 ∑s=1200 d(y(s)).
A small d̄ means “the samples look like some real digits” — beware, a generator that merely copies the training data would also score zero.
Let c(1), …, c(N) ∈ ℝk be the codes that the autoencoder’s encoder assigns to N = 200 test digits. The fitted distribution is N(m, diag s²), where for j = 1, …, k
mj = 1/N ∑i=1N c(i)j, sj = √1/N ∑i=1N (c(i)j − mj)².
The autoencoder thus draws zj = mj + sj · εj with independent εj ~ N(0, 1), i.e. each coordinate from a normal distribution with the same mean and standard deviation as its actual codes have (for k = 2: m = (−3.9, −0.9), s = (4.3, 6.0)). The coordinates are drawn independently — the fitted distribution does not capture any dependence between them.
| code size k | AE reconstruction (MSE) | VAE reconstruction (MSE) | AE samples | VAE samples |
|---|---|---|---|---|
| 2 | 0.051 | 0.063 | 0.042 | 0.044 |
| 8 | 0.025 | 0.036 | 0.090 | 0.054 |
| 16 | 0.017 | 0.039 | 0.112 | 0.055 |
With a two-dimensional code both networks do about equally well (0.042 versus 0.044). With eight dimensions the autoencoder falls apart: its samples have a distance of 0.090, more than halfway to noise, while the VAE holds at 0.054. With sixteen it is 0.112 versus 0.055. In many dimensions the autoencoder’s codes lie on a thin, twisted surface, and a random point from the fitted normal distribution practically never lands on it.
The price: the VAE reconstructs worse. With k = 16 its reconstruction error is 0.039 versus 0.017 for the autoencoder, and its images are blurrier. The code is noisy, so the decoder learns to draw the “average” of what could be in the neighbourhood of the code. Blurriness is a well-known weakness of VAEs.
5. Reconstruction versus order: β
The weight β determines how strongly the KL penalty pushes the codes towards the normal distribution. Five VAEs with a code of k = 2 and different β:
| β | KL (nats per image) | reconstruction (MSE) | samples (distance) |
|---|---|---|---|
| 0.1 | 8.18 | 0.053 | 0.044 |
| 0.5 | 4.71 | 0.057 | 0.044 |
| 1 | 3.42 | 0.063 | 0.044 |
| 2 | 2.07 | 0.076 | 0.052 |
| 5 | 0.05 | 0.131 | 0.075 |
A small β leaves the codes looser (KL 8.2) and reconstruction is more accurate. A large β squeezes them towards zero. At β = 5 the KL drops to 0.05: the encoder has stopped storing anything in the code, and the decoder keeps drawing roughly the same thing, whatever code it gets. This is called posterior collapse.
Definition — the nat, and KL as the price of the code
The nat is the unit of information corresponding to the natural logarithm: an event with probability p ∈ (0, 1] carries −ln p nats (instead of −log2 p bits). Since ln p = ln 2 · log2 p,
1 nat = 1/ln 2 bits ≈ 1.443 bits.
Let x be a random image with the data distribution pdata (here a uniformly chosen test digit), let the code z given x have the encoder’s distribution q(z | x), and let p(z) = N(0, I) be the prior. Denote by
q(z) = Ex ~ pdata[q(z | x)]
the distribution of the codes of all images together, and by I(x; z) the mutual information of the image and the code (the continuous analogue of the definition from the part on information). Then
Ex ~ pdata[DKL(q(z | x) ‖ p(z))] = I(x; z) + DKL(q(z) ‖ p(z)) ≥ I(x; z),
so the average KL divergence over the data is an upper bound on the information the code carries about the image. It is the “price” the encoder pays for whatever it stores in the code about the image.
At β = 0.1 this is at most 8.18 nats ≈ 11.8 bits per image, at β = 5 only 0.05 nats ≈ 0.07 bits — less than a tenth of a bit, so the code says almost nothing about the image and the decoder has no choice but to keep drawing the same thing.
Conversely, β greater than 1 is used deliberately by the β-VAE (Irina Higgins et al., 2017): stronger pressure towards independent code coordinates means that individual dimensions often correspond to understandable properties (rotation, stroke thickness).
6. Smooth transitions
We take the codes of two test digits (networks with an eight-dimensional code, for each digit the image the VAE reconstructs best) and walk along the straight line between them in eight steps: z(t) = (1 − t) · za + t · zb for t = 0, 1/7, 2/7, …, 1, where za, zb are the centres μ of the two digits. We decode every intermediate point.
The transition is smooth for both networks: the intermediate steps are plausible shapes between the two digits. Along a straight line between two real codes we do not get far from the data, so here the ordinary autoencoder does not lag behind — its weakness is only drawing blindly. The same principle in large models makes it possible to smoothly change, say, a facial expression or the style of an image.
7. Where VAEs are used
- Compression for other generators. Stable Diffusion (latent diffusion, Robin Rombach et al., 2022) does not work with pixels but with the code of an autoencoder with a small KL penalty: it compresses a 512 × 512 × 3 image to 64 × 64 × 4, and diffusion runs in that smaller space. Diffusion will be the subject of one of the next parts.
- Discrete codes. VQ-VAE (Aäron van den Oord et al., 2017) picks entries from a learned codebook instead of a continuous code. An image thus becomes a sequence of “tokens” that a transformer can generate.
- Scientific data. Molecules, single-cell data, sound — wherever a smooth space is useful in which one can search and interpolate.
In image sharpness VAEs lose to GANs and to diffusion models. But they have stable training, a genuine probabilistic interpretation, and an encoder for free.
8. Summary
- A generative model learns the distribution of the data so that it can be sampled; with a latent variable it draws a code and decodes it.
- A VAE encodes into a distribution N(μ, σ²), and loss = reconstruction + β · KL to N(0, I). The trick z = μ + σε makes it possible to differentiate through the sampling.
- VAE codes lie around the origin and fill the space; autoencoder codes are scattered and full of holes.
- With an 8-dimensional code: autoencoder samples 0.090, VAE 0.054 (real digits 0.017, noise 0.159).
- The price is worse reconstruction and blurriness; too large a β leads to collapse (KL 0.05).
Want to get hands-on? Have an AI agent train a VAE with a two-dimensional code on the MNIST dataset and plot a grid of decoded images — you will see a whole “map” of handwritten digits.
Do you see it differently? Write to support@coden-app.com.