Aller au contenu principal

Module 4 — GAN : générateur contre discriminateur

Un GAN (Generative Adversarial Network) ne cherche pas à modéliser explicitement p(x)p(x). 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 GG transforme un vecteur de bruit zN(0,I)z \sim \mathcal{N}(0, I) en une image. Le discriminateur DD 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 :

minGmaxD Expdata ⁣[logD(x)]+Ezp(z) ⁣[log(1D(G(z)))]\min_G \max_D\ \mathbb{E}_{x \sim p_{\text{data}}}\!\left[\log D(x)\right] + \mathbb{E}_{z \sim p(z)}\!\left[\log(1 - D(G(z)))\right]

À l'équilibre théorique, GG produit exactement pdatap_{\text{data}} et DD 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 DD, puis GG — 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 DD empêche que le gradient de la perte du discriminateur ne remonte dans GG : sans lui, GG apprend à se saboter pour aider DD à 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 log(1D(G(z)))\log(1 - D(G(z))) par logD(G(z))-\log D(G(z)) pour éviter que GG ne reçoive plus aucun gradient quand DD 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émentRecommandation DCGAN
Sous-échantillonnageconvolutions avec stride=2, jamais de MaxPool
Sur-échantillonnageconvolutions transposées avec stride=2
NormalisationBatchNorm dans GG et DD, sauf sortie de GG et entrée de DD
Activation cachéeReLU dans GG, LeakyReLU (pente 0.2) dans DD
Activation de sortietanh pour GG, sortie linéaire pour DD (les logits pour la BCE)
OptimiseurAdam, 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 ln41.39\ln 4 \approx 1.39 pour DD et ln20.69\ln 2 \approx 0.69 pour GG en formulation non saturante) : signe d'un équilibre relatif, l'entraînement avance.
  • La perte de DD s'effondre vers zéro et celle de GG explose : le discriminateur gagne trop vite, plus aucun gradient utile pour GG. C'est l'échec le plus fréquent au début.
  • La perte de DD 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).
La seule vraie évaluation d'un GAN reste visuelle

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 zz 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 DD, 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 DD. 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 DD.
  • L'entraînement alterne une mise à jour de DD (avec .detach() sur les faux) et une mise à jour de GG (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 zz à 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.