rumblr Work in progressWIP

● The AI Primer · Lesson 35 · Generating images, audio and video

GANs

a forger against a detective

You'll be able to explain A forger against a detective: adversarial training, and why it is unstable

Members · open during launch 54 min16 figures and diagrams
Guide is what to use and when. How it works builds it from scratch. Math & code adds the formulas and the Python.

The lesson in one minute

What you'll be able to explain

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

Reading it: start at the far left. Noise goes into the generator and comes out as a fake point. Real points come in from the data. Both kinds of point go through the same detective, which gives each one a probability. The two dotted arrows are the learning signals, and they pull in opposite directions: the detective adjusts itself to score real points high and fakes low, and the generator adjusts itself to make the detective score its points high. Notice that the generator never sees a real point. Everything it learns about the data arrives through the detective's verdicts.

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

Reading it: follow one lap from the top. A fresh batch of real points and noise comes in, the forger turns the noise into fakes, and then the two players move in turn: first the detective takes one step of gradient ascent on V (it wants V higher), then the forger takes one step of gradient descent (it wants V lower), judged by the detective as it is after its step. The loop repeats thousands of times. At the bottom, only the generator is kept. The detective was scaffolding: its whole job was to teach.

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

0.0 0.1 0.2 0.3 0.4 density Where each distribution puts its points real data p_data forger p_g −4 −2 0 2 4 position x 0.0 0.2 0.4 0.6 0.8 1.0 D*(x) The best detective: the share of points at x that are real best verdict D* D* once the forger matches the data

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

Reading it: the top panel shows two distributions on a line: the real data (blue) has two narrow bumps at −2 and +2, and the forger (red) spreads one wide bump over the middle. The bottom panel is the best verdict at every position. Over the real bumps it rises to about 0.8, because most points found there are real (the forger's wide bump still reaches them). In the middle and at the far edges it falls towards 0, because only fakes live there. The grey dashed line at 0.5 is what becomes once the forger matches the data: the detective has nothing left to go on. The shape of the bottom curve is exactly the information the forger needs, which way to move its mass.

Figure 4 · Drawn from the lesson's code

−3 −2 −1 0 1 2 3 −3 −2 −1 0 1 2 3 The detective's verdict after 2,500 steps real forger 0.0 0.2 0.4 0.6 0.8 1.0 D(x): probability real

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

Reading it: this is the real ring after 2,500 rounds of training. The background colour is the trained detective's verdict at every spot: blue for "real", red for "fake", white for 0.5. Black dots are real points, green dots are the forger's. The green points sit on all eight clouds, and the background there is pale, close to white: at the eight centres the verdict is between 0.46 and 0.58. That is the equilibrium made visible, the detective reduced to guessing where the forgery is good. Away from the ring, where neither real nor fake points ever appear, the formula says nothing, and the detective's colouring there is arbitrary (deep red in the empty middle).

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

−6 −4 −2 0 2 detective's score a for a fake (left: sure it is fake) −2 0 2 4 6 forger's loss The original loss goes flat when the detective is sure D = 0.01 original: log(1 - D) non-saturating: -log D 0 100 200 300 400 500 600 700 800 detective's head start (training steps) 1 0 − 1 1 0 0 1 0 1 size of the forger's gradient Measured on the ring: a head start starves one loss D=0.484 D=0.268 D=0.029 D=0.006 original loss non-saturating loss

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

Reading it: on the left, the x-axis is the detective's score for a fake: far left means "certainly fake". The red curve, the original loss, goes flat as you move left, and a flat curve has no slope, so no gradient. The blue curve, the non-saturating loss, keeps a steady downhill slope all the way. On the right is a measurement from our ring: the detective trains alone against a frozen, untrained forger for more and more steps, and we measure how big a gradient each loss would hand the forger (log scale). At the start both are about 0.5 to 0.6. After 800 detective steps its verdict on fakes is 0.003, the original loss's gradient has shrunk to 0.09, and the non-saturating one has grown to 15: over a hundred times larger.

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

Reading it: the boxes form a loop because the game has no downhill direction that both players share. Each arrow is a sensible move for the player making it, but each player only reacts to the other's current position, so the forger arrives just after the detective has changed its mind. The loop is a rotation around the equilibrium, not a descent into it.

Figure 6 · Drawn from the lesson's code

−2 −1 0 1 2 forger's theta (where the fake sits) −2 −1 0 1 2 detective's psi (its slope) The Dirac GAN never settles start (1, 0) equilibrium simultaneous steps: spiral out alternating steps: endless orbit

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

Reading it: the horizontal axis is the forger's θ, the vertical axis the detective's ψ, and the black cross at (0, 0) is the equilibrium. Both paths start at the dot, (1, 0), and take 300 steps of size 0.2. The red path, where both players step at once, spirals outwards: after 300 steps it is 2.6 away from the equilibrium, where it started 1 away. The blue path, where the forger waits to see the detective's new ψ before it moves, stays on a closed loop about 1 away forever. Neither ever arrives. Plain gradient steps do not solve this game even in two dimensions.

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

Reading it: each box is a phase of training, and the arrows go round in a circle. The forger does not have to cover every cloud to fool the detective today; it only has to put its points where the detective currently says "real". The detective then learns those spots are over-supplied with fakes, the verdict there drops, and the forger's gradient pushes the whole pile somewhere else. It is the chase from part 1, now played out across the clouds of the data. The alternation between odd and even clouds is what our run actually does, pictured next.

Figure 8 · Drawn from the lesson's code

equal speeds (0.001) step 1,750 X.X...X. step 2,000 .X.X.X.X step 2,250 X.X.X... step 2,500 .X.X.X.. detective 3x faster (0.003) step 1,750 XXXXXXXX step 2,000 XXXXXXXX step 2,250 XXXXXXXX step 2,500 XXXXXXXX detective 10x faster (0.01) step 1,750 XXX.XXX. step 2,000 XXX.XXX. step 2,250 XXX.XXX. step 2,500 XXX.XXX. The forger's points (green) against the eight real clouds (grey)

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

Reading it: each panel is the page, grey circles mark the eight real clouds, and green dots are 1,000 points from the forger at that step (always made from the same noise, so the panels are comparable). The panel titles spell the coverage with X for a covered cloud, counting anticlockwise from the cloud on the right. Top row, equal learning rates: the green points hop from clouds 1, 3 and 7 at step 1,750 to 2, 4, 6 and 8 at step 2,000, back to 1, 3 and 5, then to 2, 4 and 6, never holding more than half the ring. Middle row, a detective that learns three times faster than the forger: all eight clouds, every snapshot. Bottom row, a detective ten times faster: six clouds, and the same six at every snapshot. Two clouds stay empty for good.

Figure 9 · Drawn from the lesson's code

0 500 1000 1500 2000 2500 training step 0 1 2 3 4 5 6 7 8 clouds covered (of 8) How much of the ring the forger covers equal speeds (0.001) detective 3x faster (0.003) detective 10x faster (0.01)

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

Reading it: the x-axis is the training step, the y-axis the number of clouds covered out of eight. The blue line (detective three times faster) climbs to 8 by step 1,000 and stays there. The red line (equal speeds) zigzags between 1 and 4, the hopping from the top row above. The amber line (detective ten times faster) jumps to 6 by step 500 and never moves again. The same code and the same data give three very different outcomes, and the only thing that changed is how fast the detective learns.

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

Reading it: read it as a chain of causes. A fast detective turns every region the forger isn't already covering into a wall of confident "fake" verdicts. To reach an empty cloud the forger would have to move points through such a wall. But a gradient only sees the next small step, and every small step into the wall makes the verdict worse, so the gradient pushes the points back onto the clouds they already occupy. The forger spends its effort sharpening those, and the missing ones are never discovered.

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

Reading it: failures are on the left and the fixes that target them on the right. Players moving out of step are fixed by choosing their speeds. A detective whose verdict jumps from 0 to 1 across a narrow cliff is fixed by penalising or capping its steepness. A yardstick that can't tell "near miss" from "far miss" is replaced by the Wasserstein distance, which in practice needs one of the steepness fixes to compute, hence the last arrow.

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

0 50 100 150 200 250 300 step 0.0 0.5 1.0 1.5 2.0 2.5 distance from equilibrium A gradient penalty damps the chase simultaneous steps alternating steps simultaneous + R1 penalty (gamma = 1)

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

Reading it: the x-axis is the step, the y-axis the distance of (θ, ψ) from the equilibrium (0, 0). The red line (plain simultaneous steps) climbs in steps, one per lap: the outward spiral. The blue line (alternating steps) hovers near 1 forever: the endless orbit. The green line adds the R1 penalty with γ = 1 to the simultaneous steps and dives: 0.08 after 50 steps, essentially 0 by 100. The friction turns the rotation into a spiral inwards. That is the Mescheder, Geiger and Nowozin result in two numbers, and it is why StyleGAN and many later GANs train with an R1 penalty. The Wasserstein GAN with gradient penalty (WGAN-GP) uses the same idea with a different target: it keeps the slope near 1 at points between real and fake.

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

−4 −3 −2 −1 0 1 2 3 4 where the forger's pile sits (the real pile is at 0) 0.0 0.5 1.0 1.5 2.0 2.5 3.0 3.5 4.0 distance between the piles Only one yardstick says which way to move log 2 = 0.693 Jensen-Shannon divergence Wasserstein distance

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

Reading it: the x-axis is where the forger's pile sits; the real pile is at 0. The red line (Jensen-Shannon) is flat at 0.693 everywhere except the single point θ = 0, where it drops to 0: a flat line has no slope, so nothing tells the forger which way to move. The blue V (Wasserstein) slopes down towards 0 from both sides: wherever the forger is, the slope points at the data. On images, where real photos and early fakes barely overlap, this difference is the difference between learning and not.

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

Reading it: push an arrow through the layer and back through its transpose, over and over. Each round trip multiplies the arrow's most-stretched component by more than any other, so the arrow swings round to point along that direction, and the stretch is read off at the end. Spectral normalization keeps one arrow per layer and runs a single round trip per training step: the weights change slowly, so the arrow stays nearly right, and the cost is a couple of extra matrix-vector products.

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

Reading it: this is an autoencoder (see 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

Goodfellow et al., Generative Adversarial Nets (2014)

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 ↗
Metz, Poole, Pfau and Sohl-Dickstein, Unrolled Generative Adversarial Networks (2016)

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 ↗
Heusel et al., GANs Trained by a Two Time-Scale Update Rule Converge to a Local Nash Equilibrium (2017)

Showed that separate learning rates for the two players give provable convergence, and introduced the FID score for judging generated images.

The paper ↗
Arjovsky, Chintala and Bottou, Wasserstein GAN (2017)

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 ↗
Gulrajani et al., Improved Training of Wasserstein GANs (2017)

Enforced the Wasserstein critic's slope limit with a gradient penalty instead of weight clipping.

The paper ↗
Mescheder, Geiger and Nowozin, Which Training Methods for GANs do actually Converge? (2018)

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 ↗
Miyato et al., Spectral Normalization for Generative Adversarial Networks (2018)

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.