Generative Adversarial Networks (GAN)

A Generative Adversarial Network (GAN) trains two neural networks against each other: a generator that turns random noise into samples, and a discriminator that tries to tell those samples apart from real data. Introduced by Goodfellow et al. in 2014, GANs were the dominant approach to high-fidelity image synthesis until diffusion models overtook them around 2021–2022. They remain relevant wherever single-step, low-latency generation matters.

This page covers the minimax objective, the training loop, why training is unstable, the stabilizing tricks that matter in practice, a runnable PyTorch example, and the major variants: DCGAN, StyleGAN, and CycleGAN.


1. How a GAN Works

GAN Training Loop: Generator vs Discriminator Noise zz ~ N(0, I) Generator GG(z; θg) Fake x̂G(z) Real xx ~ pdata Discriminator DD(x; θd) ∈ (0,1) LossBCE ∇θd: D learns real vs fake ∇θg: gradient flows back through D into G D and G alternate updates; G never sees real data directly, only D's gradient signal.

The generator G maps a latent vector z (usually 64–512 dimensions of Gaussian noise) to a sample in data space, such as a 64×64 RGB image. The discriminator D is a binary classifier that outputs the probability that its input came from the real dataset. Neither network is given an explicit density; the generator learns an implicit distribution pg purely from the feedback D provides.

Training alternates: one (or k) gradient steps on D using a mini-batch of real and fake samples, then one step on G where the gradient is backpropagated through a frozen D into G's weights. G improves only insofar as D's decision surface provides a useful slope.

2. The Minimax Objective

Generated Distribution Converging to the Data Distribution Early Mid-training Equilibrium solid green = pdata dashed yellow = pg (generator) blue = D(x); flattens to 0.5 when pg = pdata

The original value function is a two-player zero-sum game:

minG maxD V(D,G) = Ex~pdata[log D(x)] + Ez~pz[log(1 − D(G(z)))]

For a fixed G, the optimal discriminator is D*(x) = pdata(x) / (pdata(x) + pg(x)). Substituting it back shows that G is minimizing 2·JSD(pdata || pg) − log 4, the Jensen-Shannon divergence. The global optimum is pg = pdata, where D outputs 0.5 everywhere.

In practice G uses the non-saturating loss maxG E[log D(G(z))] instead of minimizing log(1 − D(G(z))). Both have the same fixed point, but the non-saturating form gives strong gradients early in training when D easily rejects fakes.

Other objectives worth knowing: WGAN replaces JSD with the Earth-Mover (Wasserstein-1) distance, using a 1-Lipschitz critic (weight clipping or a gradient penalty, WGAN-GP); LSGAN uses least-squares loss; hinge loss is common in large conditional GANs such as BigGAN.

3. Why Training Is Unstable

Common GAN Failure Modes Mode collapseG maps many z to a few outputsfix: minibatch-std, unrolled D Vanishing gradientsD too strong, log(1−D(G(z))) saturatesfix: non-saturating loss, WGAN Oscillation / divergencemin-max never settlesfix: TTUR, spectral norm, R1 Monitor sample grids and FID, not just losses. GAN losses do not decrease monotonically and a "good" loss curve can hide a collapsed generator.

Gradient descent on a min-max game is not guaranteed to converge; simultaneous updates can circle around the equilibrium. The three classic symptoms:

Stabilizers that hold up in practice: spectral normalization on D, R1 gradient penalty on real data, two time-scale update rule (TTUR: higher LR for D), Adam with β1 = 0 or 0.5, exponential moving average (EMA) of G's weights for evaluation, and differentiable data augmentation (DiffAugment, ADA) when data is limited.

4. Minimal PyTorch GAN (MNIST)

import torch, torch.nn as nn
from torchvision import datasets, transforms
from torch.utils.data import DataLoader

device = "cuda" if torch.cuda.is_available() else "cpu"
Z = 64
tfm = transforms.Compose([transforms.ToTensor(), transforms.Normalize([0.5], [0.5])])
loader = DataLoader(datasets.MNIST("data", train=True, download=True, transform=tfm),
                    batch_size=128, shuffle=True, drop_last=True)

G = nn.Sequential(
    nn.Linear(Z, 256), nn.LeakyReLU(0.2),
    nn.Linear(256, 512), nn.LeakyReLU(0.2),
    nn.Linear(512, 784), nn.Tanh()).to(device)
D = nn.Sequential(
    nn.Linear(784, 512), nn.LeakyReLU(0.2),
    nn.Linear(512, 256), nn.LeakyReLU(0.2),
    nn.Linear(256, 1)).to(device)          # logits; BCEWithLogits is numerically stable

bce = nn.BCEWithLogitsLoss()
opt_g = torch.optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999))
opt_d = torch.optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999))

for epoch in range(20):
    for real, _ in loader:
        real = real.view(real.size(0), -1).to(device)
        b = real.size(0)
        ones, zeros = torch.ones(b, 1, device=device), torch.zeros(b, 1, device=device)

        # 1) Discriminator step: real -> 1, fake -> 0
        fake = G(torch.randn(b, Z, device=device))
        loss_d = bce(D(real), ones) + bce(D(fake.detach()), zeros)
        opt_d.zero_grad(); loss_d.backward(); opt_d.step()

        # 2) Generator step (non-saturating): make D call fakes real
        loss_g = bce(D(fake), ones)
        opt_g.zero_grad(); loss_g.backward(); opt_g.step()
    print(f"epoch {epoch}  D {loss_d.item():.3f}  G {loss_g.item():.3f}")

with torch.no_grad():
    samples = G(torch.randn(16, Z, device=device)).view(-1, 1, 28, 28)

Note fake.detach() in the D step so G's graph is not updated twice, and that the G step reuses the same fake tensor with gradients still attached.

5. Major Variants

VariantKey ideaTypical use
DCGANAll-convolutional G and D with BatchNorm and strided convsBaseline image GAN, teaching
Conditional GANFeed class label or other conditioning to G and DClass-controlled generation
WGAN / WGAN-GPWasserstein loss with Lipschitz criticMore stable gradients
Pix2PixPaired image-to-image with U-Net G and PatchGAN DPaired translation tasks
CycleGANUnpaired translation via cycle-consistencyStyle / domain transfer
StyleGANMapping network + style modulation per layerPhotorealistic faces, latent editing
BigGANLarge-batch class-conditional GAN, truncation trickImageNet-scale synthesis

6. GANs vs Other Generative Models

Strengths: one forward pass per sample (fast inference), sharp outputs, a compact and often well-structured latent space that supports interpolation and editing.

Weaknesses: no likelihood to evaluate, unstable and hyperparameter-sensitive training, poorer mode coverage than likelihood-based models, harder to scale to open-ended text-to-image tasks.

Compared with VAEs, GANs give sharper samples but no encoder or ELBO. Compared with diffusion models, GANs are much faster at sampling but cover the distribution less completely; modern distillation methods for diffusion often reintroduce an adversarial loss to get few-step sampling. Evaluation typically uses FID (Fréchet Inception Distance) and precision/recall metrics rather than the training losses.

7. Practical Pitfalls

Generative ModelsSectionoverview of generative model families DCGANgansconvolutional GAN architecture guidelines StyleGANgansstyle-based generator, latent editing CycleGANgansunpaired image-to-image translation Diffusion ModelsSectionthe successor paradigm for image synthesis Variational Autoencoders (VAE)autoencoders Convolutional Neural Networks (CNN)Section