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.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
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
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.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
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
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
# 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.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
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 |