The lesson in one minute
What you'll be able to explain
- A GAN trains a generator (forger) to turn noise into samples and a discriminator (detective) to tell real samples from fakes, against each other. The forger learns only through the detective's gradient.
- The game: min over G of max over D of E[log D(x)] + E[log(1 − D(G(z)))]. The best detective says p_data / (p_data + p_g); at the equilibrium the forger matches the data and the detective says 1/2 everywhere.
- The non-saturating loss (maximise log D(G(z))) keeps the forger's gradient strong when the detective is confident, exactly when the original loss goes quiet.
- Unstable because each player's target moves: plain gradient steps circle or spiral around the equilibrium, the forger hops between a few modes (mode collapse), and a detective that wins too fast freezes it.
- Fixes: balanced learning rates, gradient penalties (R1, WGAN-GP), spectral normalization, and the Wasserstein distance, which still points the way when real and fake don't overlap.
- Today: diffusion has overtaken GANs for generating images, but adversarial losses remain inside image and audio decoders and fast distilled samplers.
Level 1
The practitioner's guide
In one sentence
A generative adversarial network (GAN) trains a generator to turn random noise into samples by pitting it against a discriminator that learns to tell its samples from real ones, which yields sharp output in a single pass at the price of a training game that is hard to keep stable.
When you need it
Two tells. You need a generator that runs in one pass, because it sits in a real-time loop (a speech vocoder, an interactive tool, a game) and a many-step diffusion sampler is too slow. Or you have a decoder that rebuilds images or audio and its output is blurry: a reconstruction loss rewards averages, and an adversarial loss is the standard cure. You don't need to train a GAN from scratch to generate new images or video today: since 2021 diffusion and flow models are the default there, because they train stably and cover the data, which is exactly what this lesson shows GANs struggling to do. The number that shows the naive approach failing: on this lesson's toy data (eight small clouds on a ring), a GAN trained with the same learning rate for both players covers 3 of the 8 clouds after 2,500 rounds, and only 16% of its samples land on any cloud. Change nothing but the discriminator's learning rate to three times the generator's and it covers all 8 with 81% on target.
Your options
From the least commitment to the most:
| Option | What it does | What it guarantees | What it costs | Where it lives |
|---|---|---|---|---|
| A diffusion or flow model instead | Generates by removing noise over many passes | Stable training, full coverage; matches BigGAN-deep's image quality with 25 passes and better coverage | Tens of network passes per sample | A hosted API or a diffusion library |
| A pretrained GAN generator | One pass from noise to sample, with an editable latent space | Real-time generation in its domain (faces, one class of object) | You are limited to domains someone trained; less variety than diffusion | Released checkpoints (StyleGAN family) |
| Train a GAN with the standard stabilisers | Non-saturating loss, a faster discriminator, an R1 penalty or spectral normalization | A one-pass generator for your own narrow domain | Two networks, coverage monitoring, learning-rate tuning; a run can still collapse | Your training loop |
| Wasserstein critic (WGAN-GP) | Replaces the verdict with a distance that still points home when real and fake don't overlap | A gradient wherever the generator is, and a loss that tracks quality | Several critic steps per generator step, plus a gradient penalty | Your training loop |
| Adversarial loss inside a decoder | A discriminator judges reconstructions against originals | Sharp detail where a reconstruction loss alone gives blur | One more network to train and keep stable | The training of VAEs, tokenizers and vocoders |
| Adversarial distillation of a diffusion model | A student learns to match a diffusion teacher in 1 to 4 steps, with a discriminator keeping it sharp | Real-time sampling from a foundation model | A teacher, a distillation run, some loss of variety | The fast sampling path of a diffusion system |
How to choose
Start from the latency you need and the variety you can't lose.
- A new image, audio or video generator with no hard latency limit: use diffusion or flow matching, and put adversarial losses only in the decoder and in a distilled fast path.
- One-pass generation is non-negotiable and the domain is narrow: a GAN generator, trained with every stabiliser in the table, or a distilled diffusion model if a teacher exists for your domain.
- A blurry decoder: add a discriminator that compares reconstructions with originals, and expect the same instabilities as any GAN.
- A latent space to edit (age a face, change a pose): a style-based generator, whose latent space was designed for disentangled control.
- Whatever you pick, measure coverage, not just per-sample quality. Mode collapse produces beautiful samples that all look the same, and a metric that scores one sample at a time cannot see it.
What it costs
Sampling is the GAN's strength: one generator pass per sample. HiFi-GAN generates 22.05 kHz speech 167.9 times faster than real time on one V100 GPU, and its small version runs 13.4 times faster than real time on a CPU; a diffusion model of the 2021 generation needed 25 network passes per image to match BigGAN-deep. Training is where the cost lies: two networks, one loss surface that moves every time the other player steps, and a set of dials whose settings decide the outcome. On the ring, 2,500 rounds with the discriminator learning at 0.003 against the generator's 0.001 covers all eight clouds; at 0.001 it covers three and at 0.01 it covers six. Each stabiliser has a price. An R1 gradient penalty needs the gradient of the discriminator's own gradient, an extra backward pass on every discriminator step; spectral normalization costs a couple of matrix-vector products per layer per step; a Wasserstein critic is trained for several steps per generator step. Quality and variety trade against each other on every dial: the ten-times-faster discriminator gives 86% on-target samples against the balanced run's 81%, and leaves two clouds empty for good. BigGAN exposed the same trade as a knob, its truncation trick, which trades sample variety for fidelity.
What breaks
- Mode collapse. The generator covers a few kinds of data and hops between them as the discriminator catches up: on the ring, 3 clouds of 8, alternating between odd and even ones every 250 steps. Track coverage (FID or a per-class count) and rebalance the learning rates.
- A silent gradient. With the original generator loss, a confident discriminator hands the generator almost nothing: after an 800-step head start its verdict on fakes is 0.003 and the gradient 0.09, against 15 with the non-saturating loss. Use the non-saturating loss; every modern GAN does.
- Oscillation. Plain gradient steps rotate around the equilibrium rather than descending into it: in the two-number Dirac GAN, simultaneous steps drift from 1 away to 2.6 away in 300 steps. Losses that swing without trending are the symptom; an R1 penalty (0.08 away after 50 steps) is the fix.
- A discriminator that wins too fast. Every region the generator has not reached becomes a wall of confident "fake", and the generator polishes what it has: six clouds from step 500 to the end, at ten times the generator's rate. A faster discriminator helps up to a point, then hurts.
- No overlap, no direction. When real and generated data don't overlap, the original objective is flat (Jensen-Shannon stuck at 0.693 whether the generator is 1 or 10 away). The Wasserstein critic's distance still slopes towards the data.
- A seed that lies. The same settings can find all eight clouds with one seed and three with another. Judge a recipe over several seeds.
In the wild
StyleGAN (Karras, Laine and Aila) is the reference one-pass image generator: a style-based generator that disentangles high-level attributes from stochastic detail, trained on the FFHQ face dataset the paper introduced, and trained with the R1 penalty. BigGAN scaled class-conditional GANs to ImageNet with spectral normalization, reaching an Inception Score of 166.5 and an FID of 7.4 at 128 × 128. FID itself, the standard score for generated images, came from the two time-scale paper (Heusel et al.), along with the proof that separate learning rates converge. Dhariwal and Nichol's Diffusion Models Beat GANs marked the handover on image quality. Adversarial losses now live inside other systems: the autoencoders of latent diffusion and VQGAN's tokenizer are trained with a discriminator, HiFi-GAN and the neural audio codecs use adversaries to keep waveforms clean, and Adversarial Diffusion Distillation turns a diffusion model into a one-to-four-step sampler by pairing score distillation with an adversarial loss. Every paper is linked at the end of the lesson.
Go deeper
Level 2 builds both players in NumPy, derives the best possible discriminator and the game's equilibrium, shows why the original generator loss goes silent, then reproduces each failure on the ring (the Dirac GAN's spiral, mode collapse, the over-strong discriminator) and runs each fix. If you only needed to choose, you are done.
Level 2
How it works, from scratch
A forger wants to print banknotes that pass as real. A detective wants to catch every fake. At first the forger is hopeless: smudged ink, the wrong colour, and the detective spots every note. But every time the detective rejects a note, the forger learns what gave it away and fixes that. Every time the forger improves, the detective has to look more closely. Neither is ever told what a real banknote should look like. They only push against each other, and both keep getting better.
The contest ends when the forger's notes are so good that the detective can do no better than guess: "real" half the time, "fake" the other half. At that point the forger has learned to make banknotes, and nobody ever wrote down a rule for what a banknote is.
That is a generative adversarial network, a GAN. The forger is a neural network called the generator; the detective is a second network called the discriminator. Train them against each other on photos of faces and the generator learns to produce new faces that never existed.
This lesson builds both players from scratch in NumPy, trains them on a toy
picture you can see at a glance, and then shows the three ways the contest
goes wrong: the players chase each other in circles, the forger settles for
copying a few examples (mode collapse), and the detective wins so
completely that the forger stops learning. Each failure gets a fix you can
run. The same goal, making new samples, is reached differently by
primer.ml.generative.autoencoders (squeeze and rebuild) and
primer.ml.generative.diffusion (remove noise a little at a time).
Chapter 1
The two players
Everyday picture The forger is a recipe that turns a few dice rolls into a banknote: different rolls, a different note. The detective is a machine you feed a note into, and out comes a single number, how sure it is that the note is real.
Tiny worked example Our "banknotes" are points on a flat page. The real data is a ring of eight little clouds: eight centres evenly spaced on a circle of radius 2, each real point scattered a tiny amount (a spread of 0.05) around one centre, chosen at random. This toy picture comes from the research literature on GAN failures because you can see whether a forger has found all eight clouds.
- The generator G takes two random numbers z (the dice rolls, drawn from a bell curve) and returns one point on the page. It is a small neural network: 2 numbers in, two layers of 32 tanh units, 2 numbers out.
- The discriminator D takes a point and returns a probability that it is real. It is another small network: 2 numbers in, 64 tanh units, one score out, then squashed into the range 0 to 1.
The squashing is the sigmoid, the same one primer.ml.neural_net uses to
turn a score into a probability:
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| a point on the page, real or forged | ||
| the detective's raw score for (also called a logit): any number, large and positive for "surely real" | ||
| the first coordinate of | 2 | |
Euler's number, about 2.718 (see primer.notation) |
||
| the sigmoid: squashes any score into a probability between 0 and 1 | ||
| the detective's verdict: the probability that is real | 0.731 |
In words: "the detective computes a score for the point, and the sigmoid turns the score into a probability that the point is real."
With the numbers: take a detective with a single neuron whose score is . The point , one of the eight real centres, scores , and : probably real. The point in the middle of the ring scores and gets : probably fake.
Level 3: in Python
import math
def sigma(a):
return 1 / (1 + math.exp(-a))
# a one-neuron detective: score a(x) = x_1 - 1
def a(x):
return x[0] - 1
round(sigma(a((2, 0))), 3) # → 0.731
round(sigma(a((0, 0))), 3) # → 0.269
Figure 1 · Diagram
flowchart LR Z["noise z<br/>2 random numbers"] --> G["Generator G<br/>the forger"] G --> F["fake point G(z)"] R["real point x<br/>from the ring"] --> D["Discriminator D<br/>the detective"] F --> D D --> P["D(point)<br/>probability it is real"] P -. "learns to call real real<br/>and fake fake" .-> D P -. "learns to make D say real<br/>about its fakes" .-> G
Why it matters that last point is the whole trick. Nobody writes down
what a face, a voice or a banknote is. The detective discovers what separates
real from fake, and its gradient (the direction that would make it less
sure a fake is fake; see primer.ml.neural_net for gradients) tells the
forger how to improve.
In code: ring_of_gaussians draws real points around ring_modes, MLP is both players (its MLP.backward also returns the gradient with respect to the input, the channel through which the forger learns), with shapes GENERATOR_SIZES and DISCRIMINATOR_SIZES, and sigmoid turns the detective's score into a probability.
Chapter 2
The game: one number both players fight over
Everyday picture Picture a scoreboard. The detective earns points for confident, correct calls and loses points, heavily, for confident mistakes. The detective wants the score as high as possible. The forger wants it as low as possible. One number, two players pulling it in opposite directions: that is a minimax game.
Tiny worked example The detective looks at two real notes and two
fakes. The score uses the logarithm (log): log 1 = 0, and the log of a
small number is a large negative number, so a confident mistake costs far
more than a hesitant one (see primer.notation for logs from scratch).
| Note | Real? | Verdict D | What is scored | Value |
|---|---|---|---|---|
| 1 | real | 0.9 | log D = log 0.9 | −0.105 |
| 2 | real | 0.8 | log D = log 0.8 | −0.223 |
| 3 | fake | 0.2 | log (1 − D) = log 0.8 | −0.223 |
| 4 | fake | 0.4 | log (1 − D) = log 0.6 | −0.511 |
The average over the real notes is −0.164, over the fakes −0.367, and the total is −0.53. A perfect detective (1 on every real note, 0 on every fake) would score log 1 + log 1 = 0, the ceiling. A detective reduced to a coin flip (0.5 on everything) scores log 0.5 + log 0.5 = −1.386.
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| the value: the scoreboard number | −0.53 | |
| "the detective chooses its weights to make what follows as large as possible" | ||
| "the forger chooses its weights to make that best-case value as small as possible" | ||
expectation: the average over many draws (see primer.notation, probability notation) |
an average over 2 notes | |
| " drawn from the real data" | notes 1 and 2 | |
| " drawn from the noise the forger starts from" | the dice rolls behind notes 3 and 4 | |
| the verdict on a real point | 0.9, 0.8 | |
| a fake point made from noise | notes 3 and 4 | |
| the verdict on a fake | 0.2, 0.4 | |
| natural logarithm: 0 at 1, very negative near 0 |
In words: "average the log of the verdicts on real points, add the average log of one minus the verdicts on fakes; the detective pushes this number up, the forger pushes it down."
With the numbers: (log 0.9 + log 0.8) / 2 + (log 0.8 + log 0.6) / 2 = −0.164 + (−0.367) = −0.53.
Level 3: in Python
import math
# D(x) on two real notes, D(G(z)) on two fakes
d_real, d_fake = [0.9, 0.8], [0.2, 0.4]
# E over real x of log D(x): an average
real_term = sum(math.log(d) for d in d_real) / len(d_real)
round(real_term, 3) # → -0.164
# E over noise z of log(1 - D(G(z)))
fake_term = sum(math.log(1 - d) for d in d_fake) / len(d_fake)
round(fake_term, 3) # → -0.367
round(real_term + fake_term, 2) # → -0.53
# a coin-flip detective, D = 1/2 on everything
round(math.log(0.5) + math.log(0.5), 3) # → -1.386
If this looks familiar, it should: −V is exactly the binary
cross-entropy of a classifier that labels real points 1 and fakes 0 (see
primer.ml.losses). The detective is an ordinary classifier. What is new
is that its second class, the fakes, keeps changing underneath it.
Training alternates. Nobody can solve "min over G of max over D" directly, so each round takes one small step for each player:
Figure 2 · Diagram
flowchart TB
S["sample a batch of real points<br/>and a batch of noise"] --> F["forger makes fakes G(z)"]
F --> DS["detective step:<br/>climb V, so D(real) rises<br/>and D(fake) falls"]
DS --> GS["forger step:<br/>change G so the new detective<br/>scores its fakes higher"]
GS --> CHK{"trained enough?"}
CHK -- no --> S
CHK -- yes --> OUT["keep G;<br/>throw D away"]
Why it matters each player is trained with an ordinary optimizer on an ordinary loss, but the loss surface moves every time the other player steps. That is the root of everything that goes wrong later in this lesson.
In code: value_function computes V from a batch of verdicts, and train_gan runs the loop above on the ring, one detective step then one forger step, each with Adam (primer.ml.optimizers.Adam).
Chapter 3
The best possible detective, and where the game ends
Everyday picture Suppose that, at one spot on the page, real points turn up three times as often as fakes. However clever the detective is, it cannot tell two identical-looking points apart, so the best it can do there is to say "75% real". It should match the local mix, no more and no less.
Tiny worked example At a point where the real data's density (how thickly its points cover that spot) is 0.3 and the forger's is 0.1, the best verdict is 0.3 / (0.3 + 0.1) = 0.75. Try its neighbours: the detective's expected score at that spot is 0.3 · log D + 0.1 · log(1 − D), which is −0.2274 at D = 0.7, −0.2249 at D = 0.75 and −0.2279 at D = 0.8. The middle one is the highest.
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| the best possible verdict at , for a forger that is held fixed | 0.75 | |
| how densely the real data covers the spot | 0.3 | |
| how densely the forger's fakes cover the spot | 0.1 |
In words: "the best verdict at each point is the share of the points found there that are real."
With the numbers: 0.3 / (0.3 + 0.1) = 0.75. If the forger matched the data exactly, everywhere, and every verdict would be 0.3 / (0.3 + 0.3) = 0.5.
Level 3: in Python
import math
p_data, p_g = 0.3, 0.1
round(p_data / (p_data + p_g), 2) # → 0.75
# the detective's expected score at this spot, for a few verdicts D
def score(D):
return p_data * math.log(D) + p_g * math.log(1 - D)
[round(score(D), 4) for D in (0.7, 0.75, 0.8)] # → [-0.2274, -0.2249, -0.2279]
# a forger that matches the data exactly
round(0.3 / (0.3 + 0.3), 2) # → 0.5
Where does the formula come from? At each point the detective is choosing one number D to maximise . The slope of that with respect to D is , and setting the slope to zero gives . This is the central result of the original GAN paper, and it tells you where the game ends. At the equilibrium the forger's distribution equals the data's, and the best detective says 1/2 everywhere, scoring V = −log 4 ≈ −1.386, the coin-flip score from the table above. Plug back into V and what remains is −log 4 plus twice the Jensen-Shannon divergence between the real and forged distributions, a measure of how different two piles of probability are that is zero only when they are identical. So a forger facing a perfect detective is really minimising that divergence. Keep that in mind: it returns as a problem in the fixes section.
Figure 3 · Drawn from the lesson's code
Top: the real data has two bumps and the forger one wide bump; bottom: the best detective's verdict rises to about 0.8 on the real bumps, drops towards 0 where only fakes live, and would be flat at one half if the forger matched
Figure 4 · Drawn from the lesson's code
After training with balanced learning rates, the forger's green points sit on all eight clouds, and the detective's verdict on the ring is close to one half
Why it matters the detective never needs to model what real data looks like; it only estimates a ratio between two densities. That is why GANs could learn sharp images long before anyone could write down a probability for an image.
In code: optimal_discriminator is the formula, fit_discriminator_table trains the most flexible detective possible (one free score per point) against a fixed forger and arrives at the same verdicts without being told the formula, and detective_verdicts evaluates a trained detective anywhere on the page.
Chapter 4
Why the forger uses a different loss
Everyday picture Early in training, the forger is terrible and the detective is sure every fake is fake. Now picture a game of "hotter, colder" where the helper stops talking once you are far enough away: the further off you are, the quieter the hints. That is the original forger loss. The fix is a helper who shouts louder the further off you are.
Tiny worked example The detective looks at a fake and is 99% sure it is fake: D(G(z)) = 0.01. How hard does each loss push the forger?
- The original loss, log(1 − D(G(z))), which the forger minimises: its slope with respect to the detective's score is −D = −0.01. Almost nothing.
- The non-saturating loss, −log D(G(z)), which the forger also minimises: its slope is −(1 − D) = −0.99. Ninety-nine times more.
When the detective is undecided, D = 0.5, both slopes are −0.5. The two losses agree when it doesn't matter and differ exactly when it does.
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| the detective's score for a fake, before the sigmoid | ||
| the verdict D on that fake | 0.01 | |
the derivative with respect to : how much the loss changes when the score is nudged up a tiny amount (see primer.notation) |
||
| the original, "saturating" forger loss | ||
| the non-saturating forger loss |
In words: "the original loss changes by only D when the detective's score moves, so it goes quiet when D is near 0; the non-saturating loss changes by 1 − D, so it is loudest exactly when the forger is doing worst."
With the numbers: at D = 0.01, the slopes are −0.01 and −0.99.
Level 3: in Python
import math
def sigma(a):
return 1 / (1 + math.exp(-a))
# a score that makes the detective 99% sure the fake is fake
a = math.log(0.01 / 0.99)
round(sigma(a), 2) # → 0.01
# measure each slope by nudging the score a tiny bit either way
h = 1e-6
def slope(loss):
return (loss(a + h) - loss(a - h)) / (2 * h)
def saturating(a):
return math.log(1 - sigma(a))
def non_saturating(a):
return -math.log(sigma(a))
round(slope(saturating), 3) # → -0.01
round(slope(non_saturating), 3) # → -0.99
Both losses want the same thing, a higher verdict on the fakes, and they share the same equilibrium. Only the strength of the push differs. The original GAN paper already recommended the switch, and essentially every GAN since uses the non-saturating loss or a Wasserstein-style loss (below).
Figure 5 · Drawn from the lesson's code
Left: as the detective grows sure a fake is fake, the original loss flattens out while the non-saturating loss keeps a steady slope; right: giving the detective an 800-step head start shrinks the original loss's gradient to 0.09 while the non-saturating one grows to 15
Why it matters a forger that gets no gradient does not learn, and the detective's lead only widens. Early in training the detective always has the easy job, so this failure is the default, not an edge case.
In code: generator_logit_gradient returns both slopes, and head_start_experiment gives the detective a head start and measures the forger's gradient under each loss.
Chapter 5
Why training is unstable, part 1: the players chase each other in circles
Everyday picture Two people share an office thermostat. One turns the heat up whenever it feels cold; the other opens a window whenever it feels hot. Each reacts to where the room was, so the temperature overshoots one way, then the other, and never settles. A GAN's players do the same: the forger moves to where the detective currently says "real", and by the time it arrives, the detective has moved.
Tiny worked example The smallest GAN there is, the Dirac GAN, has one number per player. The real data is a single point at x = 0. The forger has one number θ and always outputs x = θ. The detective has one number ψ and scores a point as ψ · x, so D(x) = σ(ψx). Start with the forger at θ = 1 and a detective with ψ = 0 (it cannot tell anything apart), and let both take steps of size 1 at the same time:
| Step | σ(ψθ) | θ (forger) | ψ (detective) | Distance from (0, 0) |
|---|---|---|---|---|
| 0 | 1 | 0 | 1 | |
| 1 | σ(0) = 0.5 | 1 | −0.5 | 1.118 |
| 2 | σ(−0.5) = 0.3775 | 0.811 | −0.878 | 1.195 |
After step 1 the detective has learned "bigger x means fake" (ψ turned negative), and after step 2 the forger has moved towards the data at 0. Both moves were sensible, yet the pair got further from the equilibrium, the point (0, 0) where the forger sits on the data and the detective is indifferent.
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| the forger's only number: where it puts its fake | 1 | |
| the detective's only number: the slope of its score | 0 | |
| the detective's score for the real data at : always 0, so | 0 | |
| the verdict on the fake | 0.5 | |
| the slope the detective climbs: | ||
| the slope the forger descends: | 0 | |
| the step size (learning rate) | 1 | |
| "is replaced by" |
In words: "the detective tilts its slope against wherever the fake sits; the forger slides along whichever way the slope says is more real; each reacts to the other's old position."
With the numbers: step 1: σ(0) = 0.5, so ψ becomes 0 − 1 · 0.5 · 1 = −0.5 and θ stays 1 + 1 · 0.5 · 0 = 1. Step 2: σ(−0.5) = 0.3775, so θ becomes 1 + 0.3775 · (−0.5) = 0.811 and ψ becomes −0.5 − 0.3775 · 1 = −0.878.
Level 3: in Python
import math
def sigma(a):
return 1 / (1 + math.exp(-a))
theta, psi, lr = 1.0, 0.0, 1.0
s = sigma(psi * theta)
s # → 0.5
# both move at once, each using the other's old position
theta, psi = theta + lr * s * psi, psi - lr * s * theta
(theta, psi) # → (1.0, -0.5)
s = sigma(psi * theta)
round(s, 4) # → 0.3775
theta, psi = theta + lr * s * psi, psi - lr * s * theta
(round(theta, 3), round(psi, 3)) # → (0.811, -0.878)
# distance from the equilibrium (0, 0): 1, then 1.118, then 1.195
round(math.hypot(theta, psi), 3) # → 1.195
Figure 7 · Diagram
flowchart LR A["fake sits to the right<br/>of the data"] --> B["detective tilts:<br/>right means fake"] B --> C["forger slides left,<br/>overshoots past the data"] C --> D["detective tilts back:<br/>left means fake"] D --> E["forger slides right,<br/>overshoots again"] E --> A
Figure 6 · Drawn from the lesson's code
In the plane of the forger's theta against the detective's psi, simultaneous steps spiral outward from the start at (1, 0), and alternating steps circle the equilibrium without ever reaching it
Why it matters a real GAN has millions of numbers per player, and this rotation happens in many directions at once. It shows up as losses that swing up and down without trending, and samples that keep changing character instead of steadily improving.
In code: dirac_gan runs this two-number game with simultaneous or alternating steps.
Chapter 6
Why training is unstable, part 2: mode collapse
Everyday picture The forger discovers that one particular note, say the twenty, always fools the detective. So it prints nothing but twenties. The detective eventually learns that twenties are suspicious, so the forger switches to printing only fifties, and so on. Each forgery is good, but the forger never produces the full variety of real money. Each distinct kind of real example is a mode, and settling on a few of them is mode collapse.
Tiny worked example To measure variety on the ring, generate 1,000 points and count, for each of the eight clouds, how many land within 0.15 of its centre (3 spreads). A cloud is covered if it gets at least 20 of the 1,000 (2%; a perfect forger gives each about 125). A point that lands near any centre counts as high quality. After 2,500 rounds with equal learning rates for both players, the counts for the eight clouds are (0, 68, 0, 20, 0, 51, 0, 17). Clouds 2, 4 and 6 are covered, cloud 8's 17 falls short, so 3 of 8 modes are covered, and only 156 of the 1,000 points are high quality: the rest are strung between clouds.
Figure 10 · Diagram
stateDiagram-v2 direction LR EvenClouds: forger piles onto clouds 1, 3, 5, 7 CatchEven: detective learns those clouds are suspicious OddClouds: forger jumps to clouds 2, 4, 6, 8 CatchOdd: detective learns these are suspicious EvenClouds --> CatchEven CatchEven --> OddClouds OddClouds --> CatchOdd CatchOdd --> EvenClouds
Figure 8 · Drawn from the lesson's code
Three rows of four snapshots between steps 1,750 and 2,500: with equal learning rates the green points hop between alternate clouds; with a detective three times faster they cover all eight; with a detective ten times faster they sit on six and never reach the other two
Figure 9 · Drawn from the lesson's code
Modes covered over training: equal learning rates wander between one and four; a three-times-faster detective reaches all eight by step 1,000 and stays; a ten-times-faster detective locks onto six at step 500
These runs use one fixed seed. With other seeds the equal-speed run sometimes finds all eight clouds late on, and the ten-times run sometimes escapes after a while; the pattern, not the exact counts, is the lesson.
Why it matters mode collapse is the classic GAN failure on real data. A face generator that has collapsed produces beautiful faces that all look like the same few people. Quality metrics that look at one sample at a time can't see it; you have to measure coverage, as we did here.
In code: mode_coverage counts covered clouds and high-quality points, and train_gan records it every 250 steps.
Chapter 7
Why training is unstable, part 3: a detective that wins too fast
Everyday picture A detective who learns much faster than the forger soon rejects everything outside the few spots the forger has already mastered, with total confidence. Every small experiment the forger tries in a new direction comes back "more fake", so it retreats to what already works and stops exploring.
Tiny worked example The bottom row above: with the detective's learning rate ten times the forger's, after 2,500 steps the counts per cloud are (199, 181, 117, 0, 132, 160, 72, 0). The six covered clouds are served well: 86% of points are high quality, more than the balanced run's 81%. But clouds 4 and 8 get nothing, from step 500 until the end. Better-looking samples, less variety: a trade-off you will meet again with real image generators.
Figure 11 · Diagram
flowchart LR A["detective learns<br/>much faster"] --> B["gaps between the forger's clouds<br/>become confident fake zones"] B --> C["every small step towards them<br/>lowers the verdict"] C --> D["forger polishes the clouds<br/>it already has"] D --> E["empty clouds stay empty"]
Why it matters "just train the detective harder" sounds like it should give the forger a better teacher. Past a point it gives it a worse one. The detective needs to be good enough to point the way, not so good that it only says no.
In code: train_gan takes both learning rates as arguments; this run gives the detective ten times the forger's.
Chapter 8
Fixes that make the game trainable
Everyday picture A good sparring partner matters more than a strong one. Every stabiliser below is a way of keeping the detective useful: fast enough to teach, smooth enough that its verdicts point somewhere.
Figure 14 · Diagram
flowchart LR P1["players out of step"] --> F1["careful learning rates<br/>two time-scales"] P2["detective builds<br/>steep cliffs"] --> F2["gradient penalty"] P2 --> F3["spectral normalization"] P3["piles don't overlap,<br/>so the verdict gives no direction"] --> F4["Wasserstein distance"] F4 --> F2
Careful learning rates
Everyday picture Pace the two sparring partners so that neither runs away with the match.
Tiny worked example The three rows of the ring figure differ only in the detective's learning rate, with the forger's fixed at 0.001:
| Detective's rate | Clouds covered at step 2,500 | High-quality points |
|---|---|---|
| 0.001 (equal) | 3, and hopping | 16% |
| 0.003 (three times) | 8, steady | 81% |
| 0.01 (ten times) | 6, stuck | 86% |
A modestly faster detective keeps its verdicts close to the best-detective formula for the forger it is facing now, so the forger follows a gradient that points at the data rather than at yesterday's detective. Heusel and colleagues called this the two time-scale update rule and proved it reaches an equilibrium under reasonable conditions. The right ratio depends on the problem; the lesson is that it is a dial worth turning.
In code: train_gan with a detective learning rate of 0.003 is the balanced run; the other two rows are the same call with 0.001 and 0.01.
Gradient penalty: fine the detective for steep cliffs
Everyday picture Charge the detective a fee for how sharply its score changes right on top of the real data. A detective with gentle slopes still ranks real above fake, but it can no longer swing wildly, and the swinging is what fed the spiral in part 1.
Tiny worked example In the Dirac GAN the detective's score is ψ · x, whose slope with respect to x is just ψ. The R1 gradient penalty adds (γ / 2) · ψ² to what the detective minimises. With γ = 1 and ψ = −0.5 the fee is 0.125, and its gradient, γψ = −0.5, pulls ψ back towards 0 on every step, like friction. With step size 0.2 that friction shrinks ψ by a factor 1 − 0.2 · 1 = 0.8 per step, on top of the game's own push.
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the Dirac example |
|---|---|---|
| the penalty added to the detective's loss | 0.125 | |
| how heavily steepness is fined | 1 | |
| the detective's score at | ||
| the gradient of the score with respect to the input point: which way, and how steeply, the score rises as you move | ||
the squared length of that gradient (see primer.notation, the length of a vector) |
0.25 | |
| averaged over real points only | the single real point |
In words: "measure how steeply the detective's score changes at the real points, square it, average it, and charge the detective γ/2 times that."
With the numbers: (1 / 2) · (−0.5)² = 0.125.
Level 3: in Python
gamma, psi = 1.0, -0.5
# slope of the score psi * x with respect to x, at the real point
slope = psi
gamma / 2 * slope ** 2 # → 0.125
# the penalty's pull on psi: its derivative, gamma * psi
gamma * psi # → -0.5
# one step of size 0.2 against that pull shrinks psi by a factor 0.8
round(psi - 0.2 * gamma * psi, 2) # → -0.4
Figure 12 · Drawn from the lesson's code
Distance from the equilibrium over 300 steps: simultaneous steps grow from 1 to 2.6, alternating steps hold near 1, and with the R1 penalty the distance falls to 0.08 within 50 steps and to zero after
In code: dirac_gan takes the penalty weight γ as an argument and adds the R1 penalty's pull, −γψ, to the detective's step.
Wasserstein distance: a yardstick that knows near from far
Everyday picture Picture the real data and the fakes as two piles of sand. The Wasserstein distance, also called the earth mover's distance, is the least work needed to reshape one pile into the other: the amount of sand moved times how far it travels. The Jensen-Shannon divergence from the best-detective section only asks how much the piles overlap.
Tiny worked example Put all the real sand at x = 0 and all the fake sand at x = θ. If θ = 1, the piles don't overlap, and the Jensen-Shannon divergence is log 2 = 0.693. If θ = 5, they still don't overlap, and it is still 0.693. It cannot tell a near miss from a far one, so it gives the forger no direction. The Wasserstein distance is 1 for θ = 1 and 5 for θ = 5: it shrinks as the forger approaches, so it always points home.
For two equally sized samples on a line, the cheapest way to move the sand is to pair the smallest with the smallest, the next with the next, and so on:
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
| the Wasserstein (earth mover's) distance between two piles on a line | 3 | |
| how many grains (samples) each pile has | 3 | |
| the -th smallest value in the first pile (the brackets mean "after sorting") | ||
| the -th smallest value in the second pile | sorts to | |
| the absolute value: the gap, ignoring direction |
In words: "sort both piles, pair them up in order, and average how far each grain has to travel."
With the numbers: pairs (0, 3), (1, 4), (2, 5), gaps 3, 3, 3, average 3.
Level 3: in Python
a = [0, 1, 2]
b = [5, 3, 4]
# pair the smallest with the smallest, and so on
pairs = list(zip(sorted(a), sorted(b)))
pairs # → [(0, 3), (1, 4), (2, 5)]
sum(abs(x - y) for x, y in pairs) / len(pairs) # → 3.0
Figure 13 · Drawn from the lesson's code
As the forger's pile slides from minus 4 to plus 4, the Jensen-Shannon divergence is flat at log 2 everywhere except exactly 0, while the Wasserstein distance is a V shape pointing at 0
The Wasserstein GAN replaces the detective with a critic that outputs an unbounded score instead of a probability, and trains it so that the gap between its average score on real and on fake points estimates this distance. The catch: the estimate is only valid if the critic's slope is at most 1 everywhere. The original WGAN enforced that crudely by clipping every weight; WGAN-GP does it with a gradient penalty; spectral normalization, next, does it by capping each layer.
In code: wasserstein_1d is the sort-and-pair formula, and js_divergence computes the Jensen-Shannon divergence between two histograms, flat at log 2 whenever they don't overlap.
Spectral normalization: a speed limit on every layer
Everyday picture A layer of a network is a matrix, and a matrix stretches some directions more than others. If no layer can stretch anything by more than 1, the whole detective can't change its score faster than the input changes: its cliffs have a speed limit.
Tiny worked example The matrix W = [[3, 0], [0, 1]] stretches the horizontal direction by 3 and the vertical by 1. Its largest stretch is 3. Divide W by 3 and no direction is stretched by more than 1.
Level 3: the formula and its symbols
Symbols
| Symbol | Meaning here | In the example |
|---|---|---|
one layer's weight matrix (see primer.notation for matrices) |
[[3, 0], [0, 1]] | |
| an input direction: an arrow of length 1 | ||
| the length of | 1 | |
| the arrow after passing through the layer | ||
| "the largest value over every arrow of length 1" | ||
| the spectral norm: the most W lengthens any arrow | 3 | |
| the normalised layer used in the detective | [[1, 0], [0, 1/3]] |
In words: "find the direction the layer stretches most, and divide the whole layer by that stretch."
With the numbers: becomes , length 3; stays length 1; every other direction lands in between. So , and stretches by at most 1.
Level 3: in Python
import math
W = [[3, 0], [0, 1]]
def stretch(v):
Wv = [sum(w * x for w, x in zip(row, v)) for row in W]
return math.hypot(*Wv) / math.hypot(*v)
# try unit arrows all the way round the circle, one per degree
angles = [2 * math.pi * k / 360 for k in range(360)]
round(max(stretch((math.cos(t), math.sin(t))) for t in angles), 3) # → 3.0
# divide W by its largest stretch
W = [[w / 3 for w in row] for row in W]
round(max(stretch((math.cos(t), math.sin(t))) for t in angles), 3) # → 1.0
Trying every direction is fine for a 2 × 2 matrix but not for a layer with a thousand inputs. Power iteration finds the most-stretched direction cheaply:
Figure 15 · Diagram
flowchart LR
V["start: any arrow v"] --> U["u = W v,<br/>rescaled to length 1"]
U --> B["v = W transpose u,<br/>rescaled to length 1"]
B --> Q{"repeat"}
Q -- again --> U
Q -- done --> S["largest stretch = length of W v"]
Why it matters spectral normalization (Miyato and colleagues, 2018) is a one-line change to the detective that made GAN training far less sensitive to learning rates and architecture, and it became a default in large image GANs such as BigGAN.
In code: largest_stretch is power iteration, and spectrally_normalize divides a matrix by it.
Chapter 9
Where GANs stand today
Everyday picture The forger-and-detective idea started as the whole machine. Today it is more often a picky critic inside another machine, brought in for the one thing it does best: making outputs look sharp and real.
Tiny worked example A GAN makes an image in one pass through the
generator. A diffusion model typically runs its network tens of times per
image, removing a little noise each time (see
primer.ml.generative.diffusion). For a while, that speed plus very sharp
samples (StyleGAN's faces, from 2018, are still striking) made GANs the best
image generators. Then diffusion models overtook them on image quality and,
above all, on coverage: they don't mode-collapse and they train stably,
which is everything this lesson has been fighting. Since around 2021 most
new image and video generators are diffusion or flow models.
GANs didn't disappear. Their loss did the moving:
Figure 16 · Diagram
flowchart LR X["real image or audio"] --> E["encoder"] E --> C["compact code"] C --> DEC["decoder"] DEC --> Y["reconstruction"] Y --> L1["reconstruction loss<br/>pixel or perceptual"] X --> L1 Y --> DISC["detective:<br/>real or reconstructed?"] X --> DISC L1 --> T["total loss for<br/>encoder and decoder"] DISC --> T
primer.ml.generative.autoencoders) with a detective bolted on. The top
path squeezes the input into a compact code and rebuilds it. A
reconstruction loss alone rewards averages, and the average of many
plausible textures is a blur. The detective on the lower path looks at the
reconstruction and the original and asks "which is real?", which punishes
blur and rewards crisp detail. The decoder is trained on both losses
together. This design is how the image autoencoders inside latent diffusion
models are trained, how VQGAN's image tokenizer is trained, and how neural
audio decoders such as HiFi-GAN produce clean waveforms. GAN-style losses
are also used to distil a slow many-step diffusion model into a fast one-
or few-step generator.Why it matters when you meet a modern image, audio or video system, expect a GAN loss somewhere in its decoder or its fast sampling path, even when the headline method is diffusion. Everything in this lesson, the non-saturating loss, gradient penalties, spectral normalization and careful learning rates, is still how those critics are kept stable.
Test yourself
8 questions
Answer each one out loud or on paper before you open it. If you can explain it, you know it.
Question 1Explain a GAN to a non-engineer in 30 seconds.Think it through, then reveal
Two programs play a game. One makes fake pictures; the other looks at real and fake pictures and guesses which is which. Every time the guesser catches a fake, the faker learns what gave it away and improves. After enough rounds the fakes are good enough that the guesser is reduced to guessing, and the faker has learned to make realistic pictures without anyone ever describing what a picture should look like.
Question 2Why does the generator never need to see real data?Think it through, then reveal
It learns only from the discriminator's gradient with respect to its own outputs: which way to move each fake so the discriminator finds it more real. The discriminator has seen real data, so its verdicts carry that information to the generator.
Question 3What does the optimal discriminator compute, and what does it say at equilibrium?Think it through, then reveal
D*(x) = p_data(x) / (p_data(x) + p_g(x)): the share of points found at x that are real. When the generator matches the data, p_g = p_data and D* is 1/2 everywhere, the value is −log 4, and the discriminator can do no better than a coin flip.
Question 4Why do almost all GANs use the non-saturating generator loss?Think it through, then reveal
The original loss log(1 − D(G(z))) has a slope of −D with respect to the discriminator's score, which is near zero when the discriminator confidently rejects fakes, as it does early in training. The non-saturating loss −log D(G(z)) has slope −(1 − D), largest exactly then. Both have the same equilibrium.
Question 5What is mode collapse, and how would you detect it?Think it through, then reveal
The generator covers only some of the distinct kinds of data (modes), often hopping between them as the discriminator catches up. Per-sample quality can look excellent, so you detect it by measuring coverage: how many modes or classes receive samples, or a distribution-level metric such as FID on real data.
Question 6Why doesn't plain gradient descent find the GAN equilibrium?Think it through, then reveal
The game has no shared downhill direction. Near the equilibrium the two players' updates form a rotation, so simultaneous steps spiral outward and alternating steps orbit forever, as the two-number Dirac GAN shows. Damping the discriminator with a gradient penalty turns the spiral inward.
Question 7Why is the Wasserstein distance a better training signal than Jensen-Shannon divergence?Think it through, then reveal
When the real and generated distributions don't overlap, Jensen-Shannon is stuck at log 2 however far apart they are, so it gives no direction. Wasserstein measures how far the mass must move, so it shrinks steadily as the generator approaches the data. Estimating it needs a critic whose slope is capped, which is what weight clipping, gradient penalties and spectral normalization provide.
Question 8If diffusion models won, why learn GANs?Think it through, then reveal
Adversarial losses are still how many image and audio decoders get sharp output, and how some diffusion models are distilled into one-step generators. And the instabilities here, moving targets and collapsing variety, show up wherever two models are trained against each other.
Primary sources
The papers behind this lesson
Introduced the generator-discriminator game, derived the optimal discriminator and the equilibrium where the generator matches the data, and suggested the non-saturating loss.
Read the annotated companion →The paper ↗Used the ring of eight Gaussians to show a generator hopping between modes, and reduced it by letting the generator look ahead at several discriminator steps.
The paper ↗Showed that separate learning rates for the two players give provable convergence, and introduced the FID score for judging generated images.
The paper ↗Replaced the Jensen-Shannon objective with the earth mover's distance, which still gives a direction when real and generated data don't overlap.
Read the annotated companion →The paper ↗Enforced the Wasserstein critic's slope limit with a gradient penalty instead of weight clipping.
The paper ↗Introduced the two-number Dirac GAN to show why plain GAN training circles instead of converging, and the R1 penalty that makes it converge.
The paper ↗Capped every discriminator layer's largest stretch at 1 using one step of power iteration per training step.
The paper ↗Researcher's shelf
Further reading
- Goodfellow, NIPS 2016 Tutorial: Generative Adversarial Networks: https://arxiv.org/abs/1701.00160
- Radford, Metz and Chintala, Unsupervised Representation Learning with Deep Convolutional GANs (DCGAN, 2015): https://arxiv.org/abs/1511.06434
- Karras, Laine and Aila, A Style-Based Generator Architecture for Generative Adversarial Networks (StyleGAN, 2018): https://arxiv.org/abs/1812.04948
- Dhariwal and Nichol, Diffusion Models Beat GANs on Image Synthesis (2021): https://arxiv.org/abs/2105.05233
- Esser, Rombach and Ommer, Taming Transformers for High-Resolution Image Synthesis (VQGAN, 2020): https://arxiv.org/abs/2012.09841
- Rombach et al., High-Resolution Image Synthesis with Latent Diffusion Models (2021): https://arxiv.org/abs/2112.10752
- Kong, Kim and Bae, HiFi-GAN: Generative Adversarial Networks for Efficient and High Fidelity Speech Synthesis (2020): https://arxiv.org/abs/2010.05646
- Sauer et al., Adversarial Diffusion Distillation (2023): https://arxiv.org/abs/2311.17042
- PyTorch, DCGAN Tutorial: https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html
About this lesson. This is the illustrated edition of a lesson from the open-source AI Primer. Its text, figures and numbers are generated from the Primer's source at commit 048aeaa, so the two always agree: the explanation, the code that builds it and the tests that prove it.