Réseau antagoniste génératif (GAN) PyTorch

Réseau antagoniste génératif (GAN)est l'une des architectures de modèles les plus créatives en apprentissage profond. En faisant s'affronter deux réseaux de neurones et apprendre l'un de l'autre, il peut finalement générer des données très réalistes. Les GAN sont largement utilisés dans des scénarios tels que la génération d'images, le transfert de style et l'augmentation de données.


1. Principes fondamentaux des GAN

L'idée centrale des GAN vient du « jeu à somme nulle » en théorie des jeux. Il se compose de deux réseaux concurrents :

  • Générateur: Apprend à générer de fausses données, dans le but de rendre le discriminateur incapable de distinguer les données générées des données réelles
  • Discriminateur: Apprend à distinguer les données réelles des données générées, dans le but de rendre les jugements aussi précis que possible

Les deux sont en concurrence pendant l'entraînement et s'améliorent continuellement, atteignant finalement un état d'équilibre de Nash.

1.1 Fonction objectif des GAN

L'objectif d'entraînement des GAN peut être exprimé comme le jeu minimax suivant :

\[ \min_G \max_D \mathbb{E}_{x \sim p_{data}(x)}[\log D(x)] + \mathbb{E}_{z \sim p_z(z)}[\log(1 - D(G(z)))] \]

où :

  • \(G\) désigne le réseau générateur
  • \(D\) désigne le réseau discriminateur
  • \(x\) désigne les données réelles
  • \(z\) désigne le vecteur de bruit aléatoire (suivant généralement une loi normale standard)
  • \(G(z)\) désigne les données factices générées par le générateur à partir du bruit

1.2 Comprendre le processus d'entraînement

L'entraînement d'un GAN se divise en deux étapes :

Étape 1 : entraîner le discriminateur

Geler le générateur et améliorer la capacité de discrimination du discriminateur :

\[ \max_D \mathbb{E}_{x \sim p_{data}}[\log D(x)] + \mathbb{E}_{z \sim p_z}[\log(1 - D(G(z)))] \]

Étape 2 : entraîner le générateur

Geler le discriminateur et améliorer la capacité du générateur à tromper :

\[ \min_G \mathbb{E}_{z \sim p_z}[\log(1 - D(G(z)))] \]

En pratique, le discriminateur est généralement entraîné pendant k étapes d'abord, puis le générateur pendant 1 étape, afin de maintenir l'équilibre.


2. Implémentation de base d'un GAN

Ci-dessous se trouve une implémentation minimale de GAN — utilisée pour générer des points de données en deux dimensions.

2.1 Définir le Générateur et le Discriminateur

Exemple

import torch
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt

# Fixer la graine aléatoire
torch.manual_seed(42)

# ── Réseau générateur ──────────────────────────────────────
class Generator(nn.Module):
    """
Générateur : générer des données à partir de bruit aléatoire
Entrée : vecteur de bruit (batch_size, noise_dim)
Sortie : données générées (batch_size, data_dim)
    """

    def __init__(self, noise_dim, data_dim, hidden_dim=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(noise_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, data_dim),
            # La sortie n'est pas activée ; le GAN apprendra la distribution appropriée
        )

    def forward(self, x):
        return self.net(x)


# ── Réseau discriminateur ──────────────────────────────────────
class Discriminator(nn.Module):
    """
Discriminator : distinguer les données réelles des données générées
Entrée : points de données (batch_size, data_dim)
Sortie : probabilité de données réelles (batch_size, 1)
    """

    def __init__(self, data_dim, hidden_dim=64):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(data_dim, hidden_dim),
            nn.LeakyReLU(0.2),  # LeakyReLU empêche la disparition des gradients
            nn.Linear(hidden_dim, hidden_dim),
            nn.LeakyReLU(0.2),
            nn.Linear(hidden_dim, 1),
            nn.Sigmoid()  # Probabilité de sortie
        )

    def forward(self, x):
        return self.net(x)


# Hyperparamètres
NOISE_DIM = 16
DATA_DIM = 2
HIDDEN_DIM = 64
BATCH_SIZE = 128

# Créer les réseaux
generator = Generator(NOISE_DIM, DATA_DIM, HIDDEN_DIM)
discriminator = Discriminator(DATA_DIM, HIDDEN_DIM)

print(f"Nombre de paramètres du générateur : {sum(p.numel() for p in generator.parameters()):,}")
print(f"Nombre de paramètres du discriminateur : {sum(p.numel() for p in discriminator.parameters()):,}")

2.2 Boucle d'entraînement

Exemple

# ── Optimiseur ──────────────────────────────────────
lr = 0.001
g_optimizer = optim.Adam(generator.parameters(), lr=lr)
d_optimizer = optim.Adam(discriminator.parameters(), lr=lr)

# Fonction de perte : entropie croisée binaire
criterion = nn.BCELoss()

# ── Données d'entraînement : distribution en anneau ──────────────────────────
def generate_real_data(batch_size):
    """Génère des données réelles avec une distribution en anneau"""
    angles = torch.rand(batch_size) * 2 * torch.pi
    radius = 1.0 + torch.randn(batch_size) * 0.1  # Le rayon vaut environ 1
    x = radius * torch.cos(angles)
    y = radius * torch.sin(angles)
    return torch.stack([x, y], dim=1)


# ── Boucle d'entraînement ──────────────────────────────────────
NUM_EPOCHS = 1000
d_losses = []
g_losses = []

for epoch in range(NUM_EPOCHS):
    # 1. Entraîner le discriminateur
    # Générer des données factices
    noise = torch.randn(BATCH_SIZE, NOISE_DIM)
    fake_data = generator(noise).detach()  # détacher pour éviter de calculer les gradients du générateur

    # Générer des données réelles
    real_data = generate_real_data(BATCH_SIZE)

    # Perte du discriminateur
    real_pred = discriminator(real_data)
    fake_pred = discriminator(fake_data)
    d_loss = criterion(real_pred, torch.ones_like(real_pred)) + \
             criterion(fake_pred, torch.zeros_like(fake_pred))

    # Mettre à jour le discriminateur
    d_optimizer.zero_grad()
    d_loss.backward()
    d_optimizer.step()

    # 2. Entraîner le générateur
    # Générer un nouveau lot de données factices
    noise = torch.randn(BATCH_SIZE, NOISE_DIM)
    fake_data = generator(noise)

    # Perte du générateur : faire croire au discriminateur que les données générées sont réelles
    fake_pred = discriminator(fake_data)
    g_loss = criterion(fake_pred, torch.ones_like(fake_pred))

    # Mettre à jour le générateur
    g_optimizer.zero_grad()
    g_loss.backward()
    g_optimizer.step()

    # Enregistrer les pertes
    d_losses.append(d_loss.item())
    g_losses.append(g_loss.item())

    if (epoch + 1) % 100 == 0:
        print(f"Époque {epoch+1:4d} | Perte D: {d_loss:.4f} | Perte G: {g_loss:.4f}")

print("Entraînement terminé !")

2.3 Visualiser les résultats générés

Exemple

# Générer des données et visualiser
def visualize_results(generator, num_samples=1000):
    noise = torch.randn(num_samples, NOISE_DIM)
    generated_data = generator(noise).detach().numpy()

    plt.figure(figsize=(6, 6))
    plt.scatter(generated_data[:, 0], generated_data[:, 1],
                alpha=0.5, s=10, c='blue', label='Généré')
    plt.xlim(-2, 2)
    plt.ylim(-2, 2)
    plt.xlabel('x')
    plt.ylabel('y')
    plt.title('Données générées par GAN')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.show()


# Voir les résultats de génération
visualize_results(generator)

3. DCGAN - GAN convolutif profond

DCGAN est une architecture classique qui introduit les réseaux de neurones convolutifs dans les GAN, améliorant considérablement la qualité de génération d'images.

3.1 Points clés de l'architecture DCGAN

  • Utiliser la convolution transposée pour le suréchantillonnage afin de générer des images
  • Utiliser la convolution avec pas pour le sous-échantillonnage afin de discriminer les images
  • Utiliser BatchNorm dans le générateur et le discriminateur (mais pas dans la couche de sortie ou la couche d'entrée)
  • Le générateur utilise ReLU, le discriminateur utilise LeakyReLU

3.2 Implémentation de DCGAN

Exemple

import torch
import torch.nn as nn

# ── Générateur DCGAN ─────────────────────────────────
class DCGenerator(nn.Module):
    """
Générateur DCGAN : suréchantillonnage par convolution transposée
    """

    def __init__(self, noise_dim=100, channels=3, features_g=64):
        super().__init__()
        self.noise_dim = noise_dim

        # Entrée : noise_dim x 1 x 1
        self.net = nn.Sequential(
            # Convolution transposée : (batch, features_g*8, 4, 4)
            nn.ConvTranspose2d(noise_dim, features_g * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(features_g * 8),
            nn.ReLU(True),

            # (batch, features_g*4, 8, 8)
            nn.ConvTranspose2d(features_g * 8, features_g * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(features_g * 4),
            nn.ReLU(True),

            # (batch, features_g*2, 16, 16)
            nn.ConvTranspose2d(features_g * 4, features_g * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(features_g * 2),
            nn.ReLU(True),

            # (batch, features_g, 32, 32)
            nn.ConvTranspose2d(features_g * 2, features_g, 4, 2, 1, bias=False),
            nn.BatchNorm2d(features_g),
            nn.ReLU(True),

            # Sortie : (batch, channels, 64, 64)
            nn.ConvTranspose2d(features_g, channels, 4, 2, 1, bias=False),
            nn.Tanh()  # Plage de sortie [-1, 1]
        )

    def forward(self, x):
        # x: (batch, noise_dim) -> (batch, noise_dim, 1, 1)
        x = x.view(x.size(0), x.size(1), 1, 1)
        return self.net(x)


# ── Discriminateur DCGAN ─────────────────────────────────
class DCDiscriminator(nn.Module):
    """
Discriminateur DCGAN : sous-échantillonner en utilisant convolution
    """

    def __init__(self, channels=3, features_d=64):
        super().__init__()

        # Entrée : (batch, channels, 64, 64)
        self.net = nn.Sequential(
            # (batch, features_d, 32, 32)
            nn.Conv2d(channels, features_d, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),

            # (batch, features_d*2, 16, 16)
            nn.Conv2d(features_d, features_d * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(features_d * 2),
            nn.LeakyReLU(0.2, inplace=True),

            # (batch, features_d*4, 8, 8)
            nn.Conv2d(features_d * 2, features_d * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(features_d * 4),
            nn.LeakyReLU(0.2, inplace=True),

            # (batch, features_d*8, 4, 4)
            nn.Conv2d(features_d * 4, features_d * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(features_d * 8),
            nn.LeakyReLU(0.2, inplace=True),

            # Sortie : (batch, 1, 1, 1)
            nn.Conv2d(features_d * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x).view(x.size(0), -1)


# Tester le réseau
noise_dim = 100
generator = DCGenerator(noise_dim=noise_dim, channels=3, features_g=64)
discriminator = DCDiscriminator(channels=3, features_d=64)

# Test de propagation avant
noise = torch.randn(2, noise_dim)
fake_images = generator(noise)
print(f"Forme de l'image générée : {fake_images.shape}")  # torch.Size([2, 3, 64, 64])

decision = discriminator(fake_images)
print(f"Forme du résultat de discrimination : {decision.shape}")     # torch.Size([2, 1])

3.3 Code complet d'entraînement DCGAN

Exemple

# ── Configuration d'entraînement ─────────────────────────────────────
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
print(f"Using device: {device}")

NOISE_DIM = 100
LEARNING_RATE = 0.0002
BETA1 = 0.5  # Paramètres Adam

# Créer les réseaux
generator = DCGenerator(noise_dim=NOISE_DIM).to(device)
discriminator = DCDiscriminator().to(device)

# Optimiseur
g_optimizer = optim.Adam(generator.parameters(), lr=LEARNING_RATE, betas=(BETA1, 0.999))
d_optimizer = optim.Adam(discriminator.parameters(), lr=LEARNING_RATE, betas=(BETA1, 0.999))

criterion = nn.BCELoss()

# ── Boucle d'entraînement ─────────────────────────────────────
fixed_noise = torch.randn(64, NOISE_DIM, device=device)  # Pour la visualisation

def train_dcgan(generator, discriminator, g_optimizer, d_optimizer, criterion,
                num_epochs, device, fixed_noise):
    G_losses = []
    D_losses = []

    for epoch in range(num_epochs):
        for batch_idx in range(100):  # On suppose que chaque époque comporte 100 lots
            # Entraîner le discriminateur
            discriminator.zero_grad()

            # Images réelles (supposées disponibles)
            # real_images = ...
            # Utiliser du bruit aléatoire pour simuler ici
            real_images = torch.randn(32, 3, 64, 64).to(device)

            batch_size = real_images.size(0)
            labels_real = torch.ones(batch_size, 1).to(device)
            labels_fake = torch.zeros(batch_size, 1).to(device)

            # Perte des images réelles
            output = discriminator(real_images)
            d_loss_real = criterion(output, labels_real)

            # Perte des images générées
            noise = torch.randn(batch_size, NOISE_DIM).to(device)
            fake_images = generator(noise)
            output = discriminator(fake_images.detach())
            d_loss_fake = criterion(output, labels_fake)

            # Perte totale
            d_loss = d_loss_real + d_loss_fake
            d_loss.backward()
            d_optimizer.step()

            # Entraîner le générateur
            generator.zero_grad()

            noise = torch.randn(batch_size, NOISE_DIM).to(device)
            fake_images = generator(noise)
            output = discriminator(fake_images)
            g_loss = criterion(output, labels_real)  # On espère que les images générées soient jugées comme réelles

            g_loss.backward()
            g_optimizer.step()

            # Enregistrer la perte
            if batch_idx % 50 == 0:
                G_losses.append(g_loss.item())
                D_losses.append(d_loss.item())
                print(f"[{epoch}/{num_epochs}][{batch_idx}/100] "
                      f"D_loss: {d_loss:.4f} | G_loss: {g_loss:.4f}")

    return G_losses, D_losses


# Démarrer l'entraînement
# G_losses, D_losses = train_dcgan(generator, discriminator, g_optimizer,
# d_optimizer, criterion, 5, device, fixed_noise)
print("L'architecture DCGAN a été définie, l'entraînement peut commencer !")

4. Conseils pour l'entraînement des GAN

4.1 Problèmes courants et solutions

Problème Cause Solution
Effondrement du mode Le générateur ne produit qu'une variété limitée d'échantillons Utilisez WGAN, ajoutez une discrimination de mini-lots, utilisez le lissage des étiquettes
Discriminateur trop fort Les gradients du générateur disparaissent, ce qui l'empêche d'apprendre Entraîner le générateur plusieurs fois, réduire le taux d'apprentissage du discriminateur, utiliser LeakyReLU
Entraînement instable La fonction objectif du GAN est non convexe et difficile à faire converger Utiliser la normalisation spectrale, la pénalité de gradient, le préchauffage du taux d'apprentissage
Faible qualité de génération Capacité du réseau insuffisante ou entraînement insuffisant Augmenter la profondeur du réseau, utiliser davantage de données d'entraînement, entraîner plus longtemps

4.2 Améliorations de la fonction de perte

Le GAN original utilise la divergence JS, qui présente des problèmes de disparition du gradient. WGAN utilise la distance de Wasserstein, qui est plus stable:

Exemple

# Fonction de perte WGAN (remplace BCE)
def wgan_d_loss(real_pred, fake_pred):
    """Perte du discriminateur : les échantillons réels obtiennent un score élevé, les échantillons générés obtiennent un score faible"""
    return -(torch.mean(real_pred) - torch.mean(fake_pred))

def wgan_g_loss(fake_pred):
    """Perte du générateur : faire en sorte que les échantillons générés obtiennent un score élevé"""
    return -torch.mean(fake_pred)

# Pénalité de gradient - WGAN-GP
def gradient_penalty(discriminator, real_images, fake_images, device):
    """Terme de pénalité de gradient WGAN-GP"""
    batch_size = real_images.size(0)
    alpha = torch.rand(batch_size, 1, 1, 1).to(device)

    # Interpoler entre les images réelles et générées
    interpolated = alpha * real_images + (1 - alpha) * fake_images
    interpolated.requires_grad = True

    # Calculer la sortie du discriminateur pour les images interpolées
    pred = discriminator(interpolated)

    # Calculer le gradient
    gradients = torch.autograd.grad(
        outputs=pred,
        inputs=interpolated,
        grad_outputs=torch.ones_like(pred),
        create_graph=True,
        retain_graph=True,
        only_inputs=True
    )[0]

    # Calculer la norme du gradient
    gradients = gradients.view(batch_size, -1)
    gradient_norm = gradients.norm(2, dim=1)
    penalty = ((gradient_norm - 1) ** 2).mean()

    return penalty

4.3 Normalisation spectrale

La normalisation spectrale peut stabiliser l'entraînement des GAN en contrôlant la constante de Lipschitz du discriminateur :

Exemple

import torch.nn.utils.spectral_norm as spectral_norm

# Discriminateur utilisant la normalisation spectrale
class SNDiscriminator(nn.Module):
    def __init__(self, channels=3, features_d=64):
        super().__init__()
        self.net = nn.Sequential(
            spectral_norm(nn.Conv2d(channels, features_d, 4, 2, 1)),
            nn.LeakyReLU(0.2, inplace=True),

            spectral_norm(nn.Conv2d(features_d, features_d * 2, 4, 2, 1)),
            nn.LeakyReLU(0.2, inplace=True),

            spectral_norm(nn.Conv2d(features_d * 2, features_d * 4, 4, 2, 1)),
            nn.LeakyReLU(0.2, inplace=True),

            spectral_norm(nn.Conv2d(features_d * 4, 1, 4, 1, 0)),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.net(x).view(x.size(0), -1)

5. GAN conditionnel (cGAN)

Le GAN conditionnel permet de spécifier des étiquettes de classe pour les données générées, ce qui permet une génération conditionnelle.

5.1 Architecture du cGAN

Exemple

import torch
import torch.nn as nn

class ConditionalGenerator(nn.Module):
    """Générateur conditionnel : reçoit à la fois le bruit et les étiquettes de classe"""
    def __init__(self, noise_dim, num_classes, embed_dim, img_channels, features_g=64):
        super().__init__()
        self.label_emb = nn.Embedding(num_classes, embed_dim)

        # Concaténer le bruit et les embeddings d'étiquettes
        self.net = nn.Sequential(
            nn.Linear(noise_dim + embed_dim, features_g * 8 * 4 * 4),
            nn.BatchNorm1d(features_g * 8 * 4 * 4),
            nn.ReLU(),
            # Ensuite, appliquer des convolutions transposées (similaires à DCGAN)
            # ...
        )

    def forward(self, noise, labels):
        # Intégrer les étiquettes de classe à la même dimension que le bruit
        label_embedding = self.label_emb(labels)
        # Concaténer le bruit et les embeddings d'étiquettes
        x = torch.cat([noise, label_embedding], dim=1)
        return self.net(x)


class ConditionalDiscriminator(nn.Module):
    """Discriminateur conditionnel : reçoit à la fois des images et des étiquettes de classe"""
    def __init__(self, img_channels, num_classes, embed_dim, features_d=64):
        super().__init__()
        self.label_emb = nn.Embedding(num_classes, embed_dim)

        # Concaténer les embeddings d'image et d'étiquette
        self.net = nn.Sequential(
            nn.Conv2d(img_channels + embed_dim, features_d, 4, 2, 1),
            nn.LeakyReLU(0.2),
            # ...
        )

    def forward(self, img, labels):
        # Redimensionner les plongements d'étiquettes à la même taille spatiale que les images
        label_embedding = self.label_emb(labels)
        # Ajuster la forme pour la concaténation
        label_embedding = label_embedding.unsqueeze(2).unsqueeze(3)
        label_embedding = label_embedding.expand(-1, -1, img.size(2), img.size(3))
        # Concaténer l'image et l'étiquette
        x = torch.cat([img, label_embedding], dim=1)
        return self.net(x)

6. Métriques d'évaluation des GAN

6.1 Métriques d'évaluation courantes

Métrique Description Avantages Inconvénients
Score Inception (IS) Utiliser Inception v3 pour évaluer la qualité et la diversité des images générées Simple à calculer, présente une certaine corrélation avec le jugement humain N'évalue pas le surapprentissage et ne peut pas détecter l'effondrement des modes
Distance de Fréchet Inception (FID) Calcule la distance entre les images réelles et générées dans l'espace des caractéristiques Plus sensible au bruit et à l'effondrement des modes Nécessite un grand nombre d'échantillons et est lent à calculer
Évaluation humaine Les humains jugent la qualité des images générées Reflète le plus fidèlement la perception humaine Subjective et chronophage

6.2 Implémentation du calcul de la FID

Exemple

import numpy as np
from scipy import linalg

def calculate_fid(real_activations, fake_activations):
    """
Calculer la distance Fréchet Inception
real_activations : vecteurs de caractéristiques des images réelles (N, dim)
fake_activations : vecteurs de caractéristiques des images générées (N, dim)
    """

    # Calculer la moyenne et la covariance
    mu1, sigma1 = real_activations.mean(axis=0), np.cov(real_activations, rowvar=False)
    mu2, sigma2 = fake_activations.mean(axis=0), np.cov(fake_activations, rowvar=False)

    # Calculer la FID
    diff = mu1 - mu2
    # Calculer la somme des matrices de covariance
    covmean, _ = linalg.sqrtm(sigma1.dot(sigma2), disp=False)

    # Éviter les nombres complexes
    if np.iscomplexobj(covmean):
        covmean = covmean.real

    fid = diff.dot(diff) + np.trace(sigma1 + sigma2 - 2 * covmean)
    return fid


# Exemple simplifié : utiliser des données aléatoires
np.random.seed(42)
real_acts = np.random.randn(1000, 2048)  # Dimension de sortie d'Inception v3
fake_acts = np.random.randn(1000, 2048)

fid_score = calculate_fid(real_acts, fake_acts)
print(f"Score FID : {fid_score:.2f}")
# Un FID plus faible est préférable ; généralement, en dessous de 50 indique une bonne qualité de génération

7. Variantes courantes des GAN

Depuis son développement, le GAN a produit de nombreuses variantes adaptées à différents scénarios d'application :

Modèle Nom complet Fonctionnalités Scénarios applicables
DCGAN GAN convolutif profond Utilise des réseaux convolutifs, génère des images de haute qualité Génération d'images
WGAN Wasserstein GAN Utilise la distance de Wasserstein, entraînement plus stable Entraînement stable
WGAN-GP WGAN avec pénalité de gradient La pénalité de gradient remplace l'écrêtage des poids Entraînement stable
CGAN GAN conditionnel Ajoute des informations conditionnelles, génération contrôlable Génération conditionnelle
CycleGAN GAN à cohérence de cycle Traduction image-à-image non supervisée Transfert de style
StyleGAN GAN basé sur le style Contrôle du style, génération de visages de haute qualité Génération de visages
BigGAN Big GAN Génération d'images à grande échelle et de haute qualité Images à haute résolution
ProGAN GAN à croissance progressive Augmentation progressive de la résolution Génération à haute résolution
Autres extensions