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.

Leave a Comment

Your email address will not be published. Required fields are marked *