16

GANs: the forger and the detective

How does a generator learn by competing against a critic?

11 min

GANs: the forger and the detective

With voice. You can pause and jump to any step.

Lesson complete

Subtitles

A forger paints fake bills and a detective tries to spot them. Every time the detective catches one, the forger learns to paint better.

Voice

No voice: the lesson advances on its own with subtitles.

The voice for this lesson is being recorded: for now it advances on its own with subtitles.

0:00 / 0:00

What you will achieve

  • Explain what the generator does (turns a randomly drawn number into a fake data point) and what the discriminator does (estimates how likely a data point is to be real).
  • Describe adversarial training as a turn-based game and recognize its ideal end: the discriminator answers 0.5 because it can no longer tell.
  • Recognize mode collapse and instability in a real training run, and explain why diffusion took the lead in images.

Key ideas

  1. A GAN is two competing networks: the generator turns a randomly drawn number into a fake data point; the discriminator estimates how likely a data point is to be real.
  2. The generator never sees the real data: it learns only through the discriminator’s answer, moving its fakes toward where the discriminator says “more real.”
  3. The ideal end is a tie: the discriminator answers 0.5 to everything because it can no longer tell. The lesson’s GAN gets close, though not all the way.
  4. Mode collapse (producing only part of the variety) and instability are its typical problems: with one setting changed, four of twenty matches collapsed.
  5. Diffusion took the lead in images (Dhariwal and Nichol, 2021) because it trains stably and covers the variety better; GANs are still faster at generating.

What many people believe

Common belief A GAN’s generator learns by copying the real data it is shown.

What is true The generator never sees a real data point. It only receives random numbers and the discriminator’s answer, and adjusts its weights so that answer becomes “real.” Only the discriminator sees the real data.

Check yourself

Four questions. No grade, no account: they just show whether the idea stuck.

ConceptDuring GAN training, what does the generator receive?
ConceptAt the ideal end of a GAN, what does the discriminator answer to any data point?
CalculationIn the match that collapsed, the left mountain holds two fakes for every real data point. If the detective can only go by where a number lands, what is the best it can answer there?
ApplicationA factory wants to use a GAN to generate photos of rare defects to train its inspection system. The generated photos look very realistic. What should be checked before using them?

Under the hoodFor those who want the math and the code. You do not need it to follow the course.

The game in one formula

The generator GG takes noise z∼pzz \sim p_z (in the lesson, the standard bell) and produces G(z)G(z). The discriminator DD returns D(x)∈(0,1)D(x) \in (0, 1), the probability it assigns to xx coming from the data. Goodfellow et al. (2014) set up a two-player minimax game:

min⁡Gmax⁡D  V(D,G)=Ex∼pdata[log⁡D(x)]+Ez∼pz[log⁡(1−D(G(z)))]\min_G \max_D \; V(D, G) = \mathbb{E}_{x \sim p_{\text{data}}}\big[\log D(x)\big] + \mathbb{E}_{z \sim p_z}\big[\log\big(1 - D(G(z))\big)\big]

DD raises VV by saying “1” to real data and “0” to fakes (it is the cross-entropy of a binary classifier, with the sign flipped); GG lowers it by pushing D(G(z))D(G(z)) toward 1. What one gains, the other loses: it is a zero-sum game.

The best possible detective, and the tie

With GG fixed, the optimal discriminator is

D∗(x)=pdata(x)pdata(x)+pg(x)D^*(x) = \frac{p_{\text{data}}(x)}{p_{\text{data}}(x) + p_g(x)}

where pgp_g is the distribution of the fakes. If a region holds two fakes for every real point, the best answer there is 1/31/3. That is what the collapsing match measures: on the left mountain (all the fakes and half the real data) the detective answers between 0.32 and 0.35; on the right, where there are no fakes, 1.00. The paper proves the game’s global optimum is at pg=pdatap_g = p_{\text{data}}, with D=12D = \tfrac12 everywhere. There the detective’s loss equals 2ln⁡2≈1.3862\ln 2 \approx 1.386 and the generator’s (non-saturating version, below) ln⁡2≈0.693\ln 2 \approx 0.693; over the last two hundred steps of the lesson’s healthy match they average 1.384 and 0.702.

The non-saturating trick and the kk steps

Early on, DD rejects fakes with high confidence and log⁡(1−D(G(z)))\log(1 - D(G(z))) saturates: it gives almost no gradient. So the same paper proposes that GG maximize log⁡D(G(z))\log D(G(z)) instead, which has the same fixed point and stronger gradients early on. The lesson uses it. The gradient that corrects GG flows through DD (backpropagation through both networks, lesson 8): for each fake, ∂log⁡D/∂x\partial \log D / \partial x says which way to move it. The arrows in the scene are that derivative, computed from the detective’s real curve.

Algorithm 1 alternates kk steps of DD with one of GG; the authors used k=1k = 1. In the lesson, kk is the only hyperparameter that changes: with k=1k = 1, all 20 seeds cover both mountains; with k=5k = 5, 15 cover them, 4 collapse (one mountain gets under 5% of the fakes), and one ends up halfway (48% and 6%). The paper warns about the opposite imbalance, training GG too much without updating DD, which it calls “the Helvetica scenario”: GG sends too many values of zz to the same xx. Either way, the balance between the players matters.

The lesson’s GAN

  • Data: a 50/50 mix of N(−1.5, 0.42)\mathcal N(-1.5,\ 0.4^2) and N(1.5, 0.42)\mathcal N(1.5,\ 0.4^2). The scene’s 800 real points are quantiles of each bell, and the generator’s 800 draws are quantiles of N(0,1)\mathcal N(0, 1), so every histogram is reproducible.
  • Networks: GG and DD are identical, 1→10 tanh⁡→10 tanh⁡→11 \to 10\ \tanh \to 10\ \tanh \to 1 (141 parameters each). Adam with rate 5×10−45 \times 10^{-4} and β1=0.5\beta_1 = 0.5, batches of 256, 2,000 generator steps. The weights were exported quantized to 16 bits and the browser runs the networks (forward pass only); the scripts train_gan.py and train_detective.py are in the lesson folder.
  • The detective alone: to present it separately, the healthy match’s initial detective was trained for 1,000 steps against the frozen initial forger (everything near 0). It answers 0.95 to 1.6 and 0.05 to 0.07.
  • “Near a mountain”: within 0.8 of its center. Real data: 47.75% in each. Fakes at the end of the healthy match: 46.4% and 48.1%.
  • What did not come out perfect: the fake mountains are narrower (the tallest packs 337 of the 800 fakes into one 0.25-wide stretch; the real one, 98), and a bridge of fakes remains in the valley. The bridge is unavoidable with this design: GG is a continuous function of a single number, so the zz values near 0 land between the two mountains. There D(0)≈0.30D(0) \approx 0.30. The detective, small and trained at the same pace, does not detect the narrowness.

In code

import torch, torch.nn as nn
torch.manual_seed(0)

def net():   # 1 → 10 → 10 → 1, with tanh: 141 parameters
    return nn.Sequential(nn.Linear(1, 10), nn.Tanh(), nn.Linear(10, 10), nn.Tanh(), nn.Linear(10, 1))

def real(n):   # 50/50 mix of N(−1.5, 0.4²) and N(+1.5, 0.4²)
    sign = torch.randint(0, 2, (n, 1)) * 2 - 1
    return 1.5 * sign + 0.4 * torch.randn(n, 1)

G, D = net(), net()               # forger and detective
opt_G = torch.optim.Adam(G.parameters(), lr=5e-4, betas=(0.5, 0.999))
opt_D = torch.optim.Adam(D.parameters(), lr=5e-4, betas=(0.5, 0.999))
bce = nn.BCEWithLogitsLoss()      # applies the sigmoid and computes −log
one, zero = torch.ones(256, 1), torch.zeros(256, 1)

for step in range(2000):
    # detective’s turn: it should say 1 to real data and 0 to fakes
    fakes = G(torch.randn(256, 1)).detach()
    loss_D = bce(D(real(256)), one) + bce(D(fakes), zero)
    opt_D.zero_grad(); loss_D.backward(); opt_D.step()
    # forger’s turn: make the detective say “real” to its fakes
    loss_G = bce(D(G(torch.randn(256, 1))), one)
    opt_G.zero_grad(); loss_G.backward(); opt_G.step()

with torch.no_grad():
    x = G(torch.randn(10000, 1))
    left = ((x + 1.5).abs() < 0.8).float().mean().item()
    right = ((x - 1.5).abs() < 0.8).float().mean().item()
    print(f'fakes near each mountain: {left:.2f} and {right:.2f}')
    dr, df = torch.sigmoid(D(real(10000))).mean().item(), torch.sigmoid(D(x)).mean().item()
    print(f'detective, on average: real {dr:.2f}, fakes {df:.2f}')
# fakes near each mountain: 0.47 and 0.48
# detective, on average: real 0.50, fakes 0.50

(PyTorch 2.11 on CPU; another version may give slightly different figures.)

What comes next

GANs dominated high-quality image generation until Dhariwal and Nichol (2021) showed that diffusion models beat them. They themselves point out that GANs capture less diversity and are hard to train, “collapsing without carefully selected hyperparameters and regularizers,” while diffusion offers distribution coverage, a stationary training objective (it does not depend on a changing rival), and easy scalability; its drawback is slower generation, in many noise-removal steps. That is lesson 17.

Decision lens

What this idea means if your job is to decide, not to code.

  • The human eye is no longer a reliable detector: in Nightingale and Farid’s study (2022), 315 people were right 48.2% of the time when separating real faces from faces made with StyleGAN2, and with training and feedback they reached only 59.0%. Verifying authenticity takes provenance and tools, not intuition.
  • If a vendor generates synthetic data (for training or testing), ask how they measured variety: mode collapse can erase exactly the rare cases that matter most, without the samples “looking bad.”
  • Training a GAN is fragile: ask for results across several runs and seeds, not a single demo. In the lesson, changing one setting made four of twenty matches collapse.
  • Choose by use: diffusion gives more quality and variety but generates in many steps; a GAN generates in a single pass, useful when speed rules (real time, small devices).

Terms in this lesson

Full transcript
CommentsThis lesson’s public conversation: questions, facts to check, ideas.

Comments live in GitHub Discussions, in the course’s public repository; writing one requires a GitHub account. They load from giscus.app when you open this section. The course reviewers read them, and if a change is accepted, it is applied in both languages. Open the thread on GitHub

Sources