PyTorch GAN Basics
A Generative Adversarial Network (GAN) consists of two neural networks trained together in competition: a Generator that creates fake data and a Discriminator that tries to tell real data from fake. As they compete, the Generator improves until its output is indistinguishable from real data. GANs can generate photorealistic images, synthesize audio, and create novel designs from scratch.
The Adversarial Game
Discriminator: Input: real image OR fake image Task: output 1 (real) or 0 (fake) Goal: correctly classify both real and fake images Generator: Input: random noise vector z Task: produce a fake image Goal: fool the discriminator into outputting 1 (real) for its fakes Training dynamic: Generator improves → fakes look more real Discriminator improves → gets better at detecting fakes At equilibrium → fakes are indistinguishable from real images
GAN Architecture Diagram
Real images ──────────────────────┐
▼
Noise z → Generator → Fake image → Discriminator → Real/Fake score
│ │
└──── Adversarial loss ────────┘
↓ ↓
Generator loss Discriminator loss
(fool D) (classify correctly)
Building the Generator
The Generator takes a random noise vector (z) as input and transforms it into a fake image through a series of layers. For a simple MNIST GAN, the output is a 28×28 grayscale image represented as a 784-value vector.
import torch
import torch.nn as nn
class Generator(nn.Module):
def __init__(self, latent_dim=100, img_size=784):
super().__init__()
self.net = nn.Sequential(
nn.Linear(latent_dim, 256),
nn.LeakyReLU(0.2),
nn.BatchNorm1d(256),
nn.Linear(256, 512),
nn.LeakyReLU(0.2),
nn.BatchNorm1d(512),
nn.Linear(512, img_size),
nn.Tanh() # output range: -1 to 1 (matches normalized images)
)
def forward(self, z):
return self.net(z)Building the Discriminator
The Discriminator receives either a real image or a generated image and outputs a single probability score: how likely the input is real.
class Discriminator(nn.Module):
def __init__(self, img_size=784):
super().__init__()
self.net = nn.Sequential(
nn.Linear(img_size, 512),
nn.LeakyReLU(0.2),
nn.Dropout(0.3),
nn.Linear(512, 256),
nn.LeakyReLU(0.2),
nn.Dropout(0.3),
nn.Linear(256, 1),
nn.Sigmoid() # output: probability of being real
)
def forward(self, x):
return self.net(x)Loss Functions
Both networks use Binary Cross-Entropy loss, but they optimize opposite goals.
loss_fn = nn.BCELoss()
# Real labels = 1 (these are real images)
# Fake labels = 0 (these are generated images)
# Discriminator loss:
# maximize: correctly label real as 1, fake as 0
def discriminator_loss(D, real_imgs, fake_imgs):
real_labels = torch.ones(real_imgs.size(0), 1)
fake_labels = torch.zeros(fake_imgs.size(0), 1)
d_real = loss_fn(D(real_imgs), real_labels)
d_fake = loss_fn(D(fake_imgs.detach()), fake_labels)
return d_real + d_fake
# Generator loss:
# maximize: fool D into labeling fakes as 1
def generator_loss(D, fake_imgs):
real_labels = torch.ones(fake_imgs.size(0), 1)
return loss_fn(D(fake_imgs), real_labels)Training Loop
import torch.optim as optim
latent_dim = 100
G = Generator(latent_dim)
D = Discriminator()
opt_G = optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999))
opt_D = optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999))
for epoch in range(50):
for real_imgs, _ in train_loader:
batch_size = real_imgs.size(0)
real_imgs = real_imgs.view(batch_size, -1) # flatten to [B, 784]
# ── Train Discriminator ──
z = torch.randn(batch_size, latent_dim)
fake_imgs = G(z)
opt_D.zero_grad()
d_loss = discriminator_loss(D, real_imgs, fake_imgs)
d_loss.backward()
opt_D.step()
# ── Train Generator ──
z = torch.randn(batch_size, latent_dim)
fake_imgs = G(z)
opt_G.zero_grad()
g_loss = generator_loss(D, fake_imgs)
g_loss.backward()
opt_G.step()
print(f"Epoch {epoch+1} D Loss: {d_loss.item():.4f} G Loss: {g_loss.item():.4f}")Training Dynamics Explained
Early training: D loss → low (D easily tells real from obvious fakes) G loss → high (G produces garbage, D catches it) Mid training: G starts producing more convincing images D struggles more, loss rises G loss starts falling Equilibrium (ideal): D loss ≈ 0.693 (log(2) — random guessing for 50/50 real/fake) G loss ≈ 0.693 Generator fakes are visually realistic
Generating New Images
G.eval()
with torch.no_grad():
z = torch.randn(16, latent_dim)
fake_imgs = G(z)
fake_imgs = fake_imgs.view(16, 1, 28, 28) # reshape to image format
print(fake_imgs.shape) # torch.Size([16, 1, 28, 28])Common GAN Problems and Fixes
Mode collapse: Generator produces only a few types of images (e.g. always the same digit) Fix: use Wasserstein GAN (WGAN), add noise to discriminator inputs Training instability: Losses oscillate wildly Fix: use betas=(0.5, 0.999) in Adam, use LeakyReLU, lower learning rate Discriminator too strong too fast: Generator can't keep up, gradients vanish Fix: train G twice per D step, or use label smoothing (real labels = 0.9)
Summary
A GAN trains a Generator and Discriminator in competition. The Generator maps random noise to fake data. The Discriminator classifies real vs fake. The Generator's goal is to fool the Discriminator; the Discriminator's goal is to not be fooled. Train them in alternating steps — first update D, then update G. Use fake_imgs.detach() when training D so gradients don't flow back to G. Use Binary Cross-Entropy loss with real labels set to 1 and fake labels to 0. Monitor both losses — a healthy GAN shows both losses around 0.693 after convergence. GANs are the foundation of image synthesis, style transfer, data augmentation, and many generative AI applications.
