Introduzione: Due Reti in Competizione
Le Generative Adversarial Networks (GAN), introdotte da Ian Goodfellow nel 2014, rappresentano uno dei paradigmi più innovativi del deep learning. L'idea e elegante nella sua semplicità: due reti neurali competono tra loro in un gioco a somma zero. Il Generatore crea dati sintetici cercando di ingannare il Discriminatore, che a sua volta impara a distinguere dati reali da quelli generati. Questa competizione spinge entrambe le reti a migliorare continuamente.
Le GAN hanno rivoluzionato la generazione di immagini: volti fotorealistici che non esistono, trasferimento di stile artistico, super-risoluzione, data augmentation e molto altro. In questo articolo esploreremo la teoria, le sfide del training e le varianti più importanti, con implementazione pratica in PyTorch.
Cosa Imparerai
- Architettura GAN: Generatore e Discriminatore
- La loss function adversariale: il gioco min-max
- Training loop: alternanza tra le due reti
- Mode collapse e training instability: le sfide principali
- Varianti: Conditional GAN, DCGAN, Wasserstein GAN
- Applicazioni: generazione volti, style transfer, data augmentation
- Implementazione DCGAN completa in PyTorch
Architettura: Generatore e Discriminatore
Il Generatore
Il Generatore G prende in input un vettore di rumore casuale z (tipicamente campionato da una distribuzione gaussiana o uniforme) e lo trasforma in un dato sintetico (es. un'immagine). L'obiettivo del generatore e produrre output cosi realistici da ingannare il discriminatore.
Il Discriminatore
Il Discriminatore D e un classificatore binario: riceve un dato (reale o generato) e produce una probabilità che sia reale. L'obiettivo del discriminatore e distinguere correttamente i dati reali da quelli generati.
La loss adversariale formalizza questo gioco: il discriminatore massimizza la probabilità di classificare correttamente, mentre il generatore minimizza la probabilità che il discriminatore rilevi i suoi output come falsi.
import torch
import torch.nn as nn
class Generator(nn.Module):
"""Generatore: rumore casuale -> immagine 64x64"""
def __init__(self, latent_dim=100, channels=3):
super().__init__()
self.net = nn.Sequential(
# latent_dim -> 512 x 4 x 4
nn.ConvTranspose2d(latent_dim, 512, 4, 1, 0, bias=False),
nn.BatchNorm2d(512),
nn.ReLU(True),
# 512 x 4 x 4 -> 256 x 8 x 8
nn.ConvTranspose2d(512, 256, 4, 2, 1, bias=False),
nn.BatchNorm2d(256),
nn.ReLU(True),
# 256 x 8 x 8 -> 128 x 16 x 16
nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
nn.BatchNorm2d(128),
nn.ReLU(True),
# 128 x 16 x 16 -> 64 x 32 x 32
nn.ConvTranspose2d(128, 64, 4, 2, 1, bias=False),
nn.BatchNorm2d(64),
nn.ReLU(True),
# 64 x 32 x 32 -> channels x 64 x 64
nn.ConvTranspose2d(64, channels, 4, 2, 1, bias=False),
nn.Tanh() # Output in [-1, 1]
)
def forward(self, z):
return self.net(z.view(z.size(0), -1, 1, 1))
class Discriminator(nn.Module):
"""Discriminatore: immagine 64x64 -> reale/falso"""
def __init__(self, channels=3):
super().__init__()
self.net = nn.Sequential(
nn.Conv2d(channels, 64, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(64, 128, 4, 2, 1, bias=False),
nn.BatchNorm2d(128),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(128, 256, 4, 2, 1, bias=False),
nn.BatchNorm2d(256),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(256, 512, 4, 2, 1, bias=False),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2, inplace=True),
nn.Conv2d(512, 1, 4, 1, 0, bias=False),
nn.Sigmoid()
)
def forward(self, img):
return self.net(img).view(-1, 1)
Training Loop: Il Gioco Adversariale
Il training delle GAN alterna l'aggiornamento del discriminatore e del generatore. Ad ogni iterazione: (1) il discriminatore viene addestrato su un batch di dati reali e un batch di dati generati; (2) il generatore viene addestrato cercando di ingannare il discriminatore aggiornato.
import torch.optim as optim
# Setup
latent_dim = 100
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
G = Generator(latent_dim).to(device)
D = Discriminator().to(device)
criterion = nn.BCELoss()
optimizer_G = optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizer_D = optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999))
def train_step(real_images):
batch_size = real_images.size(0)
real_labels = torch.ones(batch_size, 1).to(device)
fake_labels = torch.zeros(batch_size, 1).to(device)
# --- Addestra Discriminatore ---
optimizer_D.zero_grad()
# Su dati reali
real_output = D(real_images)
d_loss_real = criterion(real_output, real_labels)
# Su dati generati
z = torch.randn(batch_size, latent_dim).to(device)
fake_images = G(z).detach() # detach: non calcolare gradiente per G
fake_output = D(fake_images)
d_loss_fake = criterion(fake_output, fake_labels)
d_loss = d_loss_real + d_loss_fake
d_loss.backward()
optimizer_D.step()
# --- Addestra Generatore ---
optimizer_G.zero_grad()
z = torch.randn(batch_size, latent_dim).to(device)
fake_images = G(z)
fake_output = D(fake_images)
g_loss = criterion(fake_output, real_labels) # Vuole ingannare D
g_loss.backward()
optimizer_G.step()
return d_loss.item(), g_loss.item()
Sfide del Training: Mode Collapse e Instabilita
Mode Collapse
Il mode collapse e il problema più comune delle GAN: il generatore trova pochi output che ingannano il discriminatore e continua a produrre solo quelli, ignorando la diversità dei dati reali. Invece di generare volti diversi, potrebbe produrre sempre lo stesso volto con piccole variazioni.
Training Instability
Le GAN sono notoriamente difficili da addestrare. Se il discriminatore diventa troppo forte, il gradiente per il generatore diventa quasi zero (vanishing gradient). Se il generatore domina, il discriminatore non fornisce feedback utile. Trovare il bilanciamento e una sfida continua.
Wasserstein GAN: Stabilizzare il Training
La Wasserstein GAN (WGAN) risolve molti problemi di instabilita sostituendo la Binary Cross-Entropy con la distanza di Wasserstein (Earth Mover's Distance). Questo fornisce un gradiente significativo anche quando le distribuzioni reale e generata sono molto diverse, eliminando il vanishing gradient. WGAN usa weight clipping o gradient penalty (WGAN-GP) per garantire la condizione di Lipschitz.
Varianti Importanti
Conditional GAN (cGAN)
Le Conditional GAN aggiungono informazione condizionale (es. classe, testo) sia al generatore che al discriminatore. Questo permette di controllare la generazione: "genera un'immagine di un gatto" oppure "genera il numero 7".
CycleGAN
CycleGAN permette la traduzione tra domini senza dati accoppiati. Può trasformare foto in dipinti in stile Monet, convertire cavalli in zebre o trasformare paesaggi estivi in invernali, senza mai aver visto le stesse scene in entrambi i domini.
StyleGAN
StyleGAN (NVIDIA) ha raggiunto la generazione di volti fotorealistici ad alta risoluzione. Introduce il concetto di "stile" a diversi livelli della rete: gli stili grossolani controllano la struttura del volto, quelli fini i dettagli come texture e colore dei capelli.
Applicazioni nel Mondo Reale
- Generazione di volti: volti realistici per gaming, cinema, avatar (ThisPersonDoesNotExist.com)
- Data augmentation: generare dati di training sintetici per classi sotto-rappresentate in medicina, manifattura
- Super-risoluzione: aumentare la risoluzione di immagini (SRGAN) per fotografia, satelliti, medicina
- Image-to-image translation: sketch-to-photo, day-to-night, segmentation-to-image (Pix2Pix)
- Drug discovery: generare strutture molecolari con proprietà desiderate
Prossimi Passi nella Serie
- Nel prossimo articolo esploreremo i Diffusion Models, che hanno superato le GAN in qualità di generazione
- Vedremo il processo di diffusione: aggiungere e rimuovere rumore per generare immagini
- Analizzeremo DALL-E, Stable Diffusion e ControlNet







