Module 4 — GAN : générateur contre discriminateur
Un GAN (Generative Adversarial Network) ne cherche pas à modéliser explicitement . Il fait quelque chose de plus astucieux : il apprend à produire des échantillons qu'un juge entraîné à les repérer n'arrive plus à distinguer des vrais. Cette formulation change tout — la netteté des images, l'instabilité de l'entraînement, et le rapport tourmenté qu'ont les praticiens avec les courbes de pertes.
Un jeu à deux joueurs qui poussent en sens contraire
Deux réseaux s'entraînent en parallèle. Le générateur transforme un vecteur de bruit en une image. Le discriminateur reçoit une image (vraie ou générée) et prédit la probabilité qu'elle soit réelle. Leurs objectifs sont opposés.
Formellement, la formulation originale (Goodfellow, 2014) est un jeu minimax :
À l'équilibre théorique, produit exactement et ne peut plus faire mieux que 50 % sur chaque image. En pratique, l'équilibre n'est jamais atteint proprement, mais on peut s'en approcher suffisamment pour obtenir des échantillons convaincants.
Le générateur ne voit jamais une seule vraie image : tout son signal d'apprentissage vient du gradient rétro-propagé à travers le discriminateur. C'est cette absence de contact direct avec les données qui explique à la fois la netteté (aucune moyenne pixel à pixel) et l'instabilité (le signal est indirect).
L'alternance de l'entraînement
À chaque itération, on met à jour d'abord , puis — chacun avec sa propre perte, son propre optimiseur, ses propres pas.
import torch
from torch import nn, optim
opt_d = optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999))
opt_g = optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999))
bce = nn.BCEWithLogitsLoss()
for x_reel in chargeur_donnees:
# --- Etape D : distinguer vrai et faux
z = torch.randn(x_reel.size(0), dim_z, device=appareil)
x_faux = G(z).detach() # coupe le gradient vers G
d_reel = D(x_reel)
d_faux = D(x_faux)
perte_d = bce(d_reel, torch.ones_like(d_reel)) \
+ bce(d_faux, torch.zeros_like(d_faux))
opt_d.zero_grad(); perte_d.backward(); opt_d.step()
# --- Etape G : tromper D
z = torch.randn(x_reel.size(0), dim_z, device=appareil)
d_faux = D(G(z))
perte_g = bce(d_faux, torch.ones_like(d_faux)) # G veut D(faux) proche de 1
opt_g.zero_grad(); perte_g.backward(); opt_g.step()
Deux détails sont critiques. Le .detach() sur x_faux pendant la mise à jour de empêche que le gradient de la perte du discriminateur ne remonte dans : sans lui, apprend à se saboter pour aider à gagner. Ensuite, la perte du générateur utilise torch.ones_like (pas zeros_like) — c'est l'astuce dite non saturante de Goodfellow, qui remplace par pour éviter que ne reçoive plus aucun gradient quand le classe très mal.
DCGAN : la recette qui a marché la première
L'entraînement des GAN a mis plusieurs années à devenir routinier. Le tournant est venu du DCGAN (Radford, 2015), qui a fixé une liste de choix architecturaux étonnamment robustes sur les images naturelles.
| Élément | Recommandation DCGAN |
|---|---|
| Sous-échantillonnage | convolutions avec stride=2, jamais de MaxPool |
| Sur-échantillonnage | convolutions transposées avec stride=2 |
| Normalisation | BatchNorm dans et , sauf sortie de et entrée de |
| Activation cachée | ReLU dans , LeakyReLU (pente 0.2) dans |
| Activation de sortie | tanh pour , sortie linéaire pour (les logits pour la BCE) |
| Optimiseur | Adam, lr=2e-4, betas=(0.5, 0.999) |
Sur MNIST, un DCGAN de moins de 500 000 paramètres, entraîné pendant 20 à 30 époques, produit des chiffres nettement plus nets qu'un VAE de taille comparable — au prix d'une variance de qualité entre lancements bien plus grande.
Les pertes GAN ne racontent presque rien
C'est le point qui piège la plupart des débutants. Dans un entraînement classique (classification, régression), une perte qui descend signifie « le modèle progresse ». Rien de tel dans un GAN.
La perte du discriminateur mesure sa capacité à distinguer, la perte du générateur mesure la crédulité du discriminateur. Les deux se pilotent mutuellement et ne convergent pas vers un minimum monotone. Trois situations à savoir lire :
- Les deux pertes oscillent autour d'une valeur proche (souvent pour et pour en formulation non saturante) : signe d'un équilibre relatif, l'entraînement avance.
- La perte de s'effondre vers zéro et celle de explose : le discriminateur gagne trop vite, plus aucun gradient utile pour . C'est l'échec le plus fréquent au début.
- La perte de remonte fortement au bout de quelques milliers d'itérations : soit le générateur produit enfin des échantillons crédibles, soit un effondrement des modes commence (module 5).
Un journal de pertes ne suffit pas à savoir si un GAN progresse. Il faut échantillonner régulièrement (par exemple toutes les 500 itérations) une grille fixe de vecteurs toujours identiques, et regarder l'évolution des mêmes images générées au cours du temps. Cette grille de contrôle est le seul témoin fiable de la trajectoire. Le module 8 ajoutera des scores quantitatifs comme le FID, mais l'inspection visuelle reste indispensable.
Le mode collapse ordinaire, en un coup d'œil
Une pathologie surgit très tôt : le générateur découvre un petit sous-ensemble de sorties qui trompent particulièrement bien , et se met à ne produire presque plus que celles-là. Sur MNIST, on voit dix classes de chiffres au départ, puis trois, puis un seul « 1 » légèrement décliné. C'est l'effondrement des modes, sujet du module 5, et il apparaît avant même les problèmes de netteté.
Le générateur n'est pas puni pour la perte de diversité : sa seule cible est de tromper . Si trois « 1 » convaincants suffisent, il n'a aucune raison intrinsèque de continuer à générer des 8. Cette absence de terme de couverture dans la formulation minimax est la faille structurelle des GAN, et elle explique pourquoi tant d'astuces ont dû être inventées pour la contourner.
En résumé
- Un GAN oppose un générateur et un discriminateur dans un jeu minimax ; le générateur ne voit jamais de vraies données, tout son signal vient du gradient à travers .
- L'entraînement alterne une mise à jour de (avec
.detach()sur les faux) et une mise à jour de (perte non saturante-log D(G(z))). - Les recettes DCGAN — convolutions strides, BatchNorm ciblée, Adam avec beta1=0.5 — restent une base solide sur les images naturelles.
- Les courbes de pertes ne racontent presque rien : il faut échantillonner une grille fixe de vecteurs à intervalles réguliers et juger visuellement, en attendant les métriques du module 8.
Module suivant : les symptômes de l'instabilité et les techniques modernes pour la corriger — Wasserstein, pénalité de gradient, normalisation spectrale.