VAE : Guide Complet — Autoencodeur Variationnel
Résumé
Le VAE (Variational Autoencoder, ou autoencodeur variationnel en français) est l’un des modèles génératifs les plus élégants et les plus influents du machine learning moderne. Introduit en 2013 par Diederik Kingma et Max Welling, le VAE combine les idées de l’apprentissage profond avec celles de l’inférence variationnelle bayésienne pour apprendre une représentation latente probabiliste des données.
Contrairement à un autoencodeur classique qui produit une représentation déterministe, le VAE modélise chaque entrée comme une distribution de probabilité dans l’espace latent. Cette approche permet non seulement de compresser et reconstruire des données, mais aussi de générer de nouvelles données réalistes en échantillonnant depuis l’espace latent. Le VAE est devenu un pilier fondamental de la génération d’images, de la découverte de médicaments, de la compression d’images et de nombreux autres domaines de l’intelligence artificielle.
Dans ce guide complet, nous explorerons les fondements mathématiques du VAE, son intuition profonde, une implémentation pratique en PyTorch sur le jeu de données MNIST, ainsi que ses avantages, limites et cas d’usage concrets.
Principe Mathématique du VAE
Le modèle génératif sous-jacent
Le VAE repose sur une idée fondamentale : les données observées $x$ sont générées à partir de variables latentes non observées $z$. Le modèle suppose que :
$$p(x) = \int p(x|z) \cdot p(z) \, dz$$
où la prior sur les variables latentes est généralement une distribution normale standard :
$$p(z) = \mathcal{N}(0, I)$$
Cela signifie que chaque point de donnée $x$ est le résultat d’un processus à deux étapes : d’abord, on tire $z$ depuis une Gaussienne centrée réduite, puis on génère $x$ selon la distribution conditionnelle $p(x|z)$ paramétrée par un réseau de neurones (le décodeur).
Le problème de l’inférence
Pour apprendre ce modèle, nous devrions calculer la vraisemblance marginale $p(x)$ et la distribution postérieure $p(z|x)$. Malheureusement, l’intégrale $\int p(x|z) \cdot p(z) \, dz$ est intracable — elle ne peut pas être calculée analytiquement ni approximée efficacement par des méthodes classiques, surtout en haute dimension.
La solution du VAE est d’introduire une distribution d’approximation $q(z|x)$, paramétrée par un réseau de neurones (l’encodeur), qui approxime la vraie postérieure $p(z|x)$ :
$$q(z|x) \approx p(z|x)$$
Concrètement, l’encodeur produit les paramètres d’une distribution Gaussienne :
$$q(z|x) = \mathcal{N}(\mu(x), \sigma^2(x) \cdot I)$$
où $\mu(x)$ et $\sigma^2(x)$ sont les sorties de l’encodeur pour l’entrée $x$. Plutôt que de produire un point unique dans l’espace latent, l’encodeur produit une distribution — une petite région Gaussienne centrée sur $\mu(x)$ avec une variance $\sigma^2(x)$.
L’ELBO — Evidence Lower BOund
Au lieu de maximiser directement la vraisemblance $\log p(x)$ (intractable), le VAE maximise une borne inférieure appelée ELBO (Evidence Lower BOund) :
$$\mathcal{L}(x) = \mathbb{E}{q(z|x)}[\log p(x|z)] – D(q(z|x) \parallel p(z))$$
Cette borne se décompose en deux termes essentiels :
- Terme de reconstruction $\mathbb{E}_{q}[\log p(x|z)]$ : il mesure la qualité de la reconstruction. Le décodeur essaie de reconstruire $x$ à partir de $z$ le plus fidèlement possible. En pratique, pour des données continues, ce terme correspond souvent à une erreur quadratique moyenne (MSE) entre l’entrée originale et la reconstruction.
- Terme de régularisation KL $D_{KL}(q(z|x) \parallel p(z))$ : la divergence de Kullback-Leibler mesure à quel point la distribution approximante $q(z|x)$ s’écarte de la prior $p(z) = \mathcal{N}(0, I)$. Ce terme régularise l’espace latent en forçant chaque distribution encodée à rester proche de la Gaussienne standard.
KL Divergence analytique
Lorsque $q(z|x)$ et $p(z)$ sont toutes deux Gaussiennes, la divergence KL admet une forme fermée élégante :
$$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)$$
où $d$ est la dimension de l’espace latent. Cette formule est remarquable car elle est entièrement différentiable et peut être calculée efficacement sans échantillonnage.
Le Reparameterization Trick
Le défi fondamental du VAE est le suivant : comment calculer le gradient de l’espérance $\mathbb{E}_{q(z|x)}[\log p(x|z)]$ par rapport aux paramètres de $q$ ? Échantillonner directement depuis $q(z|x) = \mathcal{N}(\mu, \sigma^2)$ et appliquer la rétropropagation ne fonctionne pas — l’opération d’échantillonnage n’est pas différentiable.
La solution géniale de Kingma et Welling est le reparameterization trick (technique de reparamétrisation) :
$$z = \mu(x) + \sigma(x) \cdot \varepsilon \quad \text{où} \quad \varepsilon \sim \mathcal{N}(0, I)$$
Au lieu d’échantillonner directement depuis $\mathcal{N}(\mu, \sigma^2)$, on échantillonne $\varepsilon$ depuis $\mathcal{N}(0, I)$ (qui ne dépend pas des paramètres du modèle), puis on le transforme de manière déterministe par $\mu + \sigma \cdot \varepsilon$. Le gradient peut maintenant circuler à travers $\mu$ et $\sigma$ via la rétropropagation classique.
C’est cette astuce mathématique qui rend l’entraînement du VAE possible en pratique. Sans elle, le gradient ne pourrait pas traverser l’opération stochastique d’échantillonnage, et l’apprentissage serait impossible.
Intuition : Pourquoi le VAE Change Tout
Pour comprendre véritablement le VAE, il faut le comparer à un autoencodeur classique.
Un autoencodeur classique (AE) apprend une fonction déterministe : pour chaque image d’entrée, il produit un point fixe dans l’espace latent. L’encodeur mappe une image de chat à un vecteur $z_{chat}$, et une image de chien à un vecteur $z_{chien}$. Si vous échantillonnez un point entre $z_{chat}$ et $z_{chien}$, le décodeur produira probablement du bruit incompréhensible, car il n’a jamais vu ce point pendant l’entraînement. L’espace latent de l’AE contient des « trous » — des régions non couvertes par les données d’entraînement.
Le VAE, en revanche, ne mappe pas chaque image à un point fixe. Il la mappe à une petite région — une distribution Gaussienne. L’image du chat devient non pas un point, mais un petit nuage de probabilité autour de $\mu_{chat}$. L’image du chien devient un autre nuage autour de $\mu_{chien}$.
Cette différence subtile mais profonde a des conséquences énormes :
- Continuité : si vous échantillonnez dans l’espace latent du VAE, même entre deux points d’entraînement, vous obtiendrez toujours une image cohérente. C’est parce que les distributions Gaussiennes se chevauchent, créant un espace latent continu et lisse.
- Génération : puisque l’espace latent est régularisé vers $\mathcal{N}(0, I)$, il suffit de tirer un vecteur aléatoire $z \sim \mathcal{N}(0, I)$ et de le passer au décodeur pour obtenir une nouvelle donnée réaliste.
- Interpolation : on peut interpoler de manière fluide entre deux concepts. Par exemple, interpoler entre le latent d’un chiffre « 3 » et celui d’un chiffre « 7 » produira des formes intermédiaires qui ressemblent à des chiffres valides.
C’est la différence fondamentale entre mémoriser des photos (ce que fait un autoencodeur classique) et apprendre le concept de ce à quoi ressemble une image valide (ce que fait le VAE). Le VAE apprend la structure profonde des données, pas simplement leur apparence superficielle.
Implémentation Python avec PyTorch
Voici une implémentation complète d’un VAE sur le jeu de données MNIST (chiffres manuscrits), illustrant tous les concepts théoriques décrits plus haut.
Architecture du modèle VAE
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 # Poids du terme KL (VAE standard : BETA=1.0)
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
# --- Données MNIST ---
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)
# --- Modèle VAE ---
class VAE(nn.Module):
"""
Autoencodeur Variationnel (VAE) sur MNIST.
"""
def __init__(self, input_dim=784, hidden_dim=HIDDEN_DIM, latent_dim=LATENT_DIM):
super().__init__()
# Encodeur
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)
# Décodeur
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
# --- Fonction de perte VAE ---
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
# --- Entraînement ---
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"Époque {epoch} [{batch_idx * len(data)}/{n_samples} ({pct:.0f}%)]")
avg_loss = train_loss / len(train_loader.dataset)
print(f"Époque {epoch} terminée — Perte moyenne : {avg_loss:.4f}")
return avg_loss
# --- Génération ---
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} images générées sauvegardées dans {filename}")
# --- Boucle principale ---
if __name__ == "__main__":
model = VAE().to(DEVICE)
optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
print(f"=== Entraînement du VAE sur 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("Entraînement terminé !")
Analyse du code
Plusieurs aspects de cette implémentation méritent une attention particulière :
Le reparameterization trick (méthode reparameterize) est le cœur du VAE. Sans lui, le gradient ne peut pas circuler depuis la perte de reconstruction jusqu’aux paramètres de l’encodeur. Notez que torch.randn_like(std) crée le bruit $\varepsilon$ de la même forme que $\sigma$, ce qui permet une multiplication élément par élément.
La fonction de perte combine deux objectifs en tension : le décodeur veut reconstruire fidèlement (terme BCE), tandis que le terme KL veut que toutes les distributions latentes se rapprochent de $\mathcal{N}(0, I)$. Le paramètre $\beta$ contrôle cet équilibre — un $\beta$ trop élevé écrase l’espace latent (tous les points convergent vers l’origine), tandis qu’un $\beta$ trop faible rend l’espace latent discontinu.
La génération est remarquablement simple : on tire $z \sim \mathcal{N}(0, I)$ et on passe au décodeur. C’est cette simplicité qui fait la beauté du VAE.
Hyperparamètres Clés du VAE
Le choix des hyperparamètres est crucial pour obtenir un VAE performant. Voici les quatre principaux :
1. latent_dim — Dimension de l’espace latent
La dimension de l’espace latent contrôle la capacité de compression et la richesse de la représentation. Avec MNIST (images 28×28 pixels), une dimension de 20 fonctionne bien car les chiffres manuscrits ont une structure relativement simple. Pour des images plus complexes (visages, scènes naturelles), il faut typiquement entre 128 et 512 dimensions.
- Trop petit : le goulot d’étranglement est trop étroit, l’information est perdue, les reconstructions sont floues.
- Trop grand : le VAE peut « tricher » en ignorant le terme KL et se comporter comme un autoencodeur classique, perdant ainsi ses capacités de génération.
2. beta — Poids du terme KL
Le paramètre $\beta$ dans le β-VAE contrôle le compromis reconstruction/régularisation :
- β = 1.0 : VAE standard, équilibre classique entre les deux termes.
- β > 1.0 : force une régularisation plus forte, ce qui peut produire des représentations latentes désentremêlées (disentangled) où chaque dimension contrôle un facteur de variation indépendant (par exemple, une dimension pour l’orientation, une autre pour la taille).
- β < 1.0 : privilégie la reconstruction au détriment de la régularisation. Peut donner des reconstructions plus nettes mais un espace latent moins bien structuré.
3. hidden_dim — Dimension des couches cachées
La taille des couches cachées de l’encodeur et du décodeur détermine la capacité du réseau à apprendre des transformations complexes. Pour MNIST, 400 neurones suffisent. Pour CIFAR-10 ou ImageNet, on utilise typiquement des architectures convolutionnelles avec des dimensions bien plus élevées.
4. learning_rate — Taux d’apprentissage
Le VAE est généralement entraîné avec Adam. Un taux de $10^{-3}$ est un bon point de départ. Un taux trop élevé peut causer une instabilité (surtout à cause du terme KL qui peut exploser), tandis qu’un taux trop faible ralentit l’apprentissage inutilement.
| Hyperparamètre | Valeur par défaut | Plage typique | Impact principal |
|---|---|---|---|
latent_dim |
20 | 2 – 512 | Capacité de représentation |
beta |
1.0 | 0.1 – 10.0 | Régularisation de l’espace latent |
hidden_dim |
400 | 128 – 2048 | Capacité du réseau |
learning_rate |
1e-3 | 1e-5 – 5e-3 | Vitesse et stabilité d’apprentissage |
Avantages et Limites du VAE
Avantages
- Génération de données natives : Contrairement aux autoencodeurs classiques, le VAE peut générer de nouvelles données cohérentes en échantillonnant depuis l’espace latent. C’est un modèle génératif à part entière.
- Espace latent continu et structuré : Le terme KL force l’espace latent à être dense et continu. L’interpolation entre deux points produit toujours des échantillons valides et progressifs.
- Inférence directe : L’encodeur permet d’obtenir directement la représentation latente d’une nouvelle donnée sans optimisation supplémentaire.
- Fondement théorique solide : Le VAE maximise une borne inférieure rigoureuse sur la vraisemblance marginale. On peut analyser mathématiquement ce qui se passe.
- Interprétabilité potentielle : Avec un β-VAE, on peut obtenir des représentations désentremêlées où chaque dimension latente correspond à un facteur de variation sémantique identifiable.
Limites
- Reconstructions floues : C’est la critique la plus fréquente. En minimisant le KL et en utilisant une perte de reconstruction type MSE ou BCE, le VAE tend à produire des sorties « moyennées » qui manquent de netteté par rapport à la réalité. Les GAN surpassent généralement le VAE sur ce point.
- Posterior collapse : Dans certaines architectures (notamment avec des autoencodeurs variationnels utilisant des RNN), le décodeur puissant peut ignorer complètement le latent $z$ et apprendre à reconstruire directement depuis $x$. Le terme KL converge alors vers zéro et le VAE dégénère en un simple décodeur.
- Capacité de modélisation limitée : L’hypothèse que $q(z|x)$ est Gaussienne est restrictive. Les vraies distributions postérieures peuvent être multimodales ou avoir des formes complexes qu’une Gaussienne ne peut pas capturer.
- Qualité de génération inférieure aux GAN et modèles de diffusion : Pour la génération d’images haute résolution, les GAN et surtout les modèles de diffusion produisent des résultats visuellement supérieurs. Le VAE reste cependant un composant clé de nombreuses architectures hybrides.
4 Cas d’Usage Concrets du VAE
1. Génération de visages et d’images artistiques
Le VAE peut apprendre un espace latent de visages humains. En se déplaçant dans cet espace latent, on peut modifier l’expression faciale, l’angle de vue, l’éclairage ou même l’identité de manière progressive et contrôlée. Des modèles comme VQ-VAE (Vector Quantized VAE) ont été utilisés par DeepMind pour la génération d’images haute qualité et la compression d’images.
2. Découverte de médicaments et bio-informatique
En bio-informatique, le VAE est utilisé pour apprendre des représentations latentes de molécules. On encode des structures moléculaires connues, on explore l’espace latent pour trouver des régions correspondant à des propriétés souhaitées (efficacité, faible toxicité), puis on décode pour obtenir de nouvelles molécules candidates. Cette approche a été appliquée avec succès à la découverte de nouveaux antibiotiques et antiviraux.
3. Détection d’anomalies
Parce que le VAE apprend la distribution des données normales, il peut détecter des anomalies. Une donnée anormale aura un terme de reconstruction élevé (le VAE ne sait pas la reconstruire correctement). Cette approche est utilisée dans la surveillance industrielle, la détection de fraude bancaire et le diagnostic médical assisté.
4. Compression d’images et apprentissage de représentations
Le VAE forme un compresseur lossy naturel. La partie encodeur compresse l’image en un vecteur latent compact, et le décodeur la reconstruit. Des architectures comme le VQ-VAE-2 de DeepMind atteignent des taux de compression compétitifs avec les standards industriels. De plus, les représentations latentes apprises par le VAE sont excellentes comme features d’entrée pour des tâches en aval (classification, clustering, recherche d’images similaires).
Voir Aussi
- Maîtrisez le Calcul de la Somme des Diviseurs avec Python : Guide Complet et Astuces Efficaces
- Créer un Jeu de Glissement en Python : Guide Complet et Astuces pour les Développeurs

