VAE: Variational Autoencoder

VAE : Guide Complet — Autoencodeur Variationnel

VAE: Complete Guide — Variational Autoencoder

Summary

The VAE (Variational Autoencoder) is one of the most elegant and influential generative models in modern machine learning. Introduced in 2013 by Diederik Kingma and Max Welling, the VAE combines ideas from deep learning with those of Bayesian variational inference to learn a probabilistic latent representation of data.

Unlike a classical autoencoder that produces a deterministic representation, the VAE models each input as a probability distribution in the latent space. This approach not only allows compressing and reconstructing data, but also generating new realistic data by sampling from the latent space. The VAE has become a fundamental pillar of image generation, drug discovery, image compression, and many other areas of artificial intelligence.

In this complete guide, we will explore the mathematical foundations of the VAE, its deep intuition, a practical implementation in PyTorch on the MNIST dataset, as well as its advantages, limitations, and concrete use cases.

Mathematical Principle of the VAE

The Underlying Generative Model

The VAE is based on a fundamental idea: observed data $x$ is generated from unobserved latent variables $z$. The model assumes that:

$$p(x) = \int p(x|z) \cdot p(z) \, dz$$

where the prior on the latent variables is typically a standard normal distribution:

$$p(z) = \mathcal{N}(0, I)$$

This means that each data point $x$ is the result of a two-step process: first, we draw $z$ from a centered-reduced Gaussian, then we generate $x$ according to the conditional distribution $p(x|z)$ parameterized by a neural network (the decoder).

The Inference Problem

To learn this model, we would need to compute the marginal likelihood $p(x)$ and the posterior distribution $p(z|x)$. Unfortunately, the integral $\int p(x|z) \cdot p(z) \, dz$ is intractable — it cannot be computed analytically nor efficiently approximated by classical methods, especially in high dimensions.

The VAE’s solution is to introduce an approximation distribution $q(z|x)$, parameterized by a neural network (the encoder), which approximates the true posterior $p(z|x)$:

$$q(z|x) \approx p(z|x)$$

Concretely, the encoder produces the parameters of a Gaussian distribution:

$$q(z|x) = \mathcal{N}(\mu(x), \sigma^2(x) \cdot I)$$

where $\mu(x)$ and $\sigma^2(x)$ are the encoder’s outputs for input $x$. Rather than producing a single point in the latent space, the encoder produces a distribution — a small Gaussian region centered on $\mu(x)$ with variance $\sigma^2(x)$.

The ELBO — Evidence Lower Bound

Instead of directly maximizing the log-likelihood $\log p(x)$ (intractable), the VAE maximizes a lower bound called the ELBO (Evidence Lower Bound):

$$\mathcal{L}(x) = \mathbb{E}{q(z|x)}[\log p(x|z)] – D(q(z|x) \parallel p(z))$$

This bound decomposes into two essential terms:

  1. Reconstruction term $\mathbb{E}_{q}[\log p(x|z)]$: it measures the quality of the reconstruction. The decoder tries to reconstruct $x$ from $z$ as faithfully as possible. In practice, for continuous data, this term often corresponds to a mean squared error (MSE) between the original input and the reconstruction.
  2. KL regularization term $D_{KL}(q(z|x) \parallel p(z))$: the Kullback-Leibler divergence measures how much the approximating distribution $q(z|x)$ deviates from the prior $p(z) = \mathcal{N}(0, I)$. This term regularizes the latent space by forcing each encoded distribution to stay close to the standard Gaussian.

Analytical KL Divergence

When $q(z|x)$ and $p(z)$ are both Gaussian, the KL divergence admits an elegant closed-form expression:

$$D_{KL}(q(z|x) \parallel p(z)) = \frac{1}{2} \sum_{j=1}^{d} \left( \sigma_j^2 + \mu_j^2 – \log(\sigma_j^2) – 1 \right)$$

where $d$ is the dimension of the latent space. This formula is remarkable because it is entirely differentiable and can be computed efficiently without sampling.

The Reparameterization Trick

The fundamental challenge of the VAE is the following: how to compute the gradient of the expectation $\mathbb{E}_{q(z|x)}[\log p(x|z)]$ with respect to the parameters of $q$? Sampling directly from $q(z|x) = \mathcal{N}(\mu, \sigma^2)$ and applying backpropagation does not work — the sampling operation is not differentiable.

Kingma and Welling’s brilliant solution is the reparameterization trick:

$$z = \mu(x) + \sigma(x) \cdot \varepsilon \quad \text{where} \quad \varepsilon \sim \mathcal{N}(0, I)$$

Instead of sampling directly from $\mathcal{N}(\mu, \sigma^2)$, we sample $\varepsilon$ from $\mathcal{N}(0, I)$ (which does not depend on the model parameters), then transform it deterministically via $\mu + \sigma \cdot \varepsilon$. The gradient can now flow through $\mu$ and $\sigma$ via classical backpropagation.

This mathematical trick is what makes training the VAE possible in practice. Without it, the gradient could not pass through the stochastic sampling operation, and learning would be impossible.

Intuition: Why the VAE Changes Everything

To truly understand the VAE, you need to compare it to a classical autoencoder.

A classical autoencoder (AE) learns a deterministic function: for each input image, it produces a fixed point in the latent space. The encoder maps a cat image to a vector $z_{cat}$, and a dog image to a vector $z_{dog}$. If you sample a point between $z_{cat}$ and $z_{dog}$, the decoder will probably produce incomprehensible noise, because it never saw that point during training. The AE’s latent space contains “holes” — regions not covered by the training data.

The VAE, on the other hand, does not map each image to a fixed point. It maps it to a small region — a Gaussian distribution. The cat image becomes not a point, but a small probability cloud around $\mu_{cat}$. The dog image becomes another cloud around $\mu_{dog}$.

This subtle but profound difference has enormous consequences:

  • Continuity: if you sample in the VAE’s latent space, even between two training points, you will always get a coherent image. This is because the Gaussian distributions overlap, creating a continuous and smooth latent space.
  • Generation: since the latent space is regularized toward $\mathcal{N}(0, I)$, you just need to draw a random vector $z \sim \mathcal{N}(0, I)$ and pass it to the decoder to get a new realistic data point.
  • Interpolation: you can smoothly interpolate between two concepts. For example, interpolating between the latent of a “3” digit and a “7” digit will produce intermediate shapes that look like valid digits.

This is the fundamental difference between memorizing photos (what a classical autoencoder does) and learning the concept of what a valid image looks like (what the VAE does). The VAE learns the deep structure of the data, not just its superficial appearance.

Python Implementation with PyTorch

Here is a complete implementation of a VAE on the MNIST dataset (handwritten digits), illustrating all the theoretical concepts described above.

VAE Model Architecture

import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torchvision.utils import save_image
import os

# --- Configuration ---
BATCH_SIZE = 128
LEARNING_RATE = 1e-3
EPOCHS = 20
LATENT_DIM = 20
HIDDEN_DIM = 400
BETA = 1.0  # Weight of the KL term (standard VAE: BETA=1.0)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# --- MNIST Data ---
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = datasets.MNIST("./data", train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=True)

# --- VAE Model ---
class VAE(nn.Module):
    """
    Variational Autoencoder (VAE) on MNIST.
    """

    def __init__(self, input_dim=784, hidden_dim=HIDDEN_DIM, latent_dim=LATENT_DIM):
        super().__init__()
        # Encoder
        self.fc_encode_1 = nn.Linear(input_dim, hidden_dim)
        self.fc_mu = nn.Linear(hidden_dim, latent_dim)
        self.fc_log_var = nn.Linear(hidden_dim, latent_dim)
        # Decoder
        self.fc_decode_1 = nn.Linear(latent_dim, hidden_dim)
        self.fc_decode_2 = nn.Linear(hidden_dim, input_dim)

    def encode(self, x):
        h = F.relu(self.fc_encode_1(x))
        mu = self.fc_mu(h)
        log_var = self.fc_log_var(h)
        return mu, log_var

    def reparameterize(self, mu, log_var):
        std = torch.exp(0.5 * log_var)
        epsilon = torch.randn_like(std)
        return mu + std * epsilon

    def decode(self, z):
        h = F.relu(self.fc_decode_1(z))
        x_recon = torch.sigmoid(self.fc_decode_2(h))
        return x_recon

    def forward(self, x):
        mu, log_var = self.encode(x)
        z = self.reparameterize(mu, log_var)
        x_recon = self.decode(z)
        return x_recon, mu, log_var

# --- VAE Loss Function ---
def vae_loss(x_recon, x, mu, log_var, beta=BETA):
    recon_loss = F.binary_cross_entropy(x_recon, x, reduction="sum")
    kl_loss = -0.5 * torch.sum(1 + log_var - mu.pow(2) - log_var.exp())
    return recon_loss + beta * kl_loss, recon_loss, kl_loss

# --- Training ---
def train(model, train_loader, optimizer, epoch):
    model.train()
    train_loss = 0
    for batch_idx, (data, _) in enumerate(train_loader):
        data = data.view(-1, 784).to(DEVICE)
        optimizer.zero_grad()
        x_recon, mu, log_var = model(data)
        loss, recon_loss, kl_loss = vae_loss(x_recon, data, mu, log_var)
        loss.backward()
        optimizer.step()
        train_loss += loss.item()
        if batch_idx % 100 == 0:
            n_samples = len(train_loader.dataset)
            pct = 100. * batch_idx / len(train_loader)
            print(f"Epoch {epoch} [{batch_idx * len(data)}/{n_samples} ({pct:.0f}%)]")

    avg_loss = train_loss / len(train_loader.dataset)
    print(f"Epoch {epoch} complete — Average loss: {avg_loss:.4f}")
    return avg_loss

# --- Generation ---
def generate_samples(model, num_samples=64, filename="vae_samples.png"):
    model.eval()
    with torch.no_grad():
        z = torch.randn(num_samples, LATENT_DIM).to(DEVICE)
        samples = model.decode(z)
        samples = samples.view(-1, 1, 28, 28)
        save_image(samples, filename, nrow=8)
    print(f"{num_samples} generated images saved to {filename}")

# --- Main Loop ---
if __name__ == "__main__":
    model = VAE().to(DEVICE)
    optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
    print(f"=== Training VAE on MNIST ===")
    for epoch in range(1, EPOCHS + 1):
        train(model, train_loader, optimizer, epoch)
        generate_samples(model, filename=f"vae_epoch_{epoch:03d}.png")
    print("Training complete!")

Code Analysis

Several aspects of this implementation deserve particular attention:

The reparameterization trick (the reparameterize method) is the heart of the VAE. Without it, the gradient cannot flow from the reconstruction loss to the encoder parameters. Note that torch.randn_like(std) creates the noise $\varepsilon$ with the same shape as $\sigma$, enabling element-wise multiplication.

The loss function combines two objectives in tension: the decoder wants to reconstruct faithfully (BCE term), while the KL term wants all latent distributions to stay close to $\mathcal{N}(0, I)$. The $\beta$ parameter controls this balance — a $\beta$ that is too high crushes the latent space (all points converge toward the origin), while a $\beta$ that is too low makes the latent space discontinuous.

Generation is remarkably simple: draw $z \sim \mathcal{N}(0, I)$ and pass it to the decoder. It is this simplicity that makes the beauty of the VAE.

Key Hyperparameters of the VAE

Choosing hyperparameters is crucial for obtaining a performant VAE. Here are the four main ones:

1. latent_dim — Dimension of the Latent Space

The dimension of the latent space controls the compression capacity and the richness of the representation. With MNIST (28×28 pixel images), a dimension of 20 works well because handwritten digits have a relatively simple structure. For more complex images (faces, natural scenes), you typically need between 128 and 512 dimensions.

  • Too small: the bottleneck is too narrow, information is lost, reconstructions are blurry.
  • Too large: the VAE can “cheat” by ignoring the KL term and behave like a classical autoencoder, thus losing its generation capabilities.

2. beta — Weight of the KL Term

The $\beta$ parameter in the β-VAE controls the reconstruction/regularization tradeoff:

  • β = 1.0: standard VAE, classic balance between the two terms.
  • β > 1.0: forces stronger regularization, which can produce disentangled latent representations where each dimension controls an independent factor of variation (e.g., one dimension for orientation, another for size).
  • β < 1.0: prioritizes reconstruction at the expense of regularization. Can give sharper reconstructions but a less well-structured latent space.

3. hidden_dim — Dimension of Hidden Layers

The size of the encoder and decoder hidden layers determines the capacity of the network to learn complex transformations. For MNIST, 400 neurons are sufficient. For CIFAR-10 or ImageNet, convolutional architectures with much higher dimensions are typically used.

4. learning_rate — Learning Rate

The VAE is typically trained with Adam. A rate of $10^{-3}$ is a good starting point. A rate that is too high can cause instability (especially because the KL term can explode), while a rate that is too low slows learning unnecessarily.

Hyperparameter Default Value Typical Range Main Impact
latent_dim 20 2 – 512 Representation capacity
beta 1.0 0.1 – 10.0 Latent space regularization
hidden_dim 400 128 – 2048 Network capacity
learning_rate 1e-3 1e-5 – 5e-3 Learning speed and stability

Advantages and Limitations of the VAE

Advantages

  1. Native data generation: Unlike classical autoencoders, the VAE can generate new coherent data by sampling from the latent space. It is a generative model in its own right.
  2. Continuous and structured latent space: The KL term forces the latent space to be dense and continuous. Interpolation between two points always produces valid and progressive samples.
  3. Direct inference: The encoder allows obtaining the latent representation of a new data point directly, without additional optimization.
  4. Solid theoretical foundation: The VAE maximizes a rigorous lower bound on the marginal likelihood. What happens can be analyzed mathematically.
  5. Potential interpretability: With a β-VAE, you can obtain disentangled representations where each latent dimension corresponds to an identifiable semantic factor of variation.

Limitations

  1. Blurry reconstructions: This is the most frequent criticism. By minimizing the KL and using an MSE or BCE type reconstruction loss, the VAE tends to produce “averaged” outputs that lack sharpness compared to reality. GANs generally surpass the VAE on this point.
  2. Posterior collapse: In certain architectures (notably variational autoencoders using RNNs), the powerful decoder can completely ignore the latent $z$ and learn to reconstruct directly from $x$. The KL term then converges to zero and the VAE degenerates into a simple decoder.
  3. Limited modeling capacity: The assumption that $q(z|x)$ is Gaussian is restrictive. True posterior distributions can be multimodal or have complex shapes that a Gaussian cannot capture.
  4. Generation quality inferior to GANs and diffusion models: For high-resolution image generation, GANs and especially diffusion models produce visually superior results. The VAE remains however a key component of many hybrid architectures.

4 Concrete Use Cases of the VAE

1. Face Generation and Artistic Images

The VAE can learn a latent space of human faces. By moving through this latent space, you can modify facial expression, viewing angle, lighting, or even identity in a progressive and controlled way. Models like VQ-VAE (Vector Quantized VAE) have been used by DeepMind for high-quality image generation and image compression.

2. Drug Discovery and Bioinformatics

In bioinformatics, the VAE is used to learn latent representations of molecules. Known molecular structures are encoded, the latent space is explored to find regions corresponding to desired properties (efficacy, low toxicity), then decoded to obtain new candidate molecules. This approach has been successfully applied to the discovery of new antibiotics and antivirals.

3. Anomaly Detection

Because the VAE learns the distribution of normal data, it can detect anomalies. An anomalous data point will have a high reconstruction term (the VAE doesn’t know how to reconstruct it correctly). This approach is used in industrial monitoring, bank fraud detection, and assisted medical diagnosis.

4. Image Compression and Representation Learning

The VAE forms a natural lossy compressor. The encoder part compresses the image into a compact latent vector, and the decoder reconstructs it. Architectures like DeepMind’s VQ-VAE-2 achieve compression rates competitive with industry standards. Furthermore, the latent representations learned by the VAE are excellent as input features for downstream tasks (classification, clustering, similar image search).

See Also


About Salah YAHIAOUI

Salah YAHIAOUI publie sur The Pingouin des tutoriels Python et des décryptages sur l’intelligence artificielle. Les cas pratiques récents précisent leurs données, leurs tests et leurs limites. À propos, méthode éditoriale et contact.

View all posts by Salah YAHIAOUI →