Generative Adversarial Networks
A generator needs feedback about whether its examples resemble the training data. A GAN trains a second model to distinguish generated examples from real ones, giving the generator a changing source of feedback.
10 sections · 10 questions · 3 coding challenges
The arrow keys move between sections too.
Could Another Model Tell the Generator What Looks Wrong?
BOTIE
Goodfellow and colleagues proposed generative adversarial networks in 2014. They trained a generator alongside a discriminator that distinguishes generated samples from examples in the dataset. The discriminator supplies a changing learning signal instead of relying only on a fixed reconstruction target.
The generator maps a random input to a proposed example. The discriminator examines examples from both sources. As the discriminator learns distinctions, the generator is trained to produce examples that defeat them. This interaction can teach rich structure, but the two learning processes can become unbalanced, and a generator may produce only a narrow selection of the possible outputs.
Sources for this history
Who Can Tell a Generator What It Is Getting Wrong?
Suppose we want a model to create handwritten digits. A classifier could tell us which digit an existing image resembles, but that does not tell a generator exactly which pixels to produce. We need feedback on the examples it creates.
A generative adversarial network, or GAN, learns that feedback with a second model. The generator creates candidate examples. The discriminator learns to distinguish those candidates from examples in the training data. Each model gives the other a changing problem to solve.
Give the generator a source of variation
The generator reads a random input, often a vector sampled from a simple distribution. Its parameters transform that input into an example. Different random inputs allow different outputs; the random vector is not a label saying which exact training example to copy.
Input
- Random code
Generator
- Learned transformation
Output
- Candidate example
Start with a distribution we can inspect directly
The numerical GAN generates single numbers rather than images. It learns to shift a Gaussian distribution toward the training distribution while a sigmoid discriminator learns to tell them apart.
This lets us inspect real updates and their losses before discussing larger image generators or training failures such as mode collapse.
Explore the full example
Set training steps to zero to inspect the starting game. Increase the steps and follow the generator’s shift and the discriminator’s two scores. Then compare learning rates. Either loss can rise as the opponent changes.
Calculating the example…
Part 1. Give the Two Models Different Objectives
1. Create examples from random inputs
Suppose we want a model to draw handwritten digits. A classifier can say which digit an existing picture most resembles, but that does not tell a generator which pixels to produce, and a brand new picture has no labeled answer to be compared against. The generator needs feedback on the examples it creates, and a GAN learns that feedback with a second model rather than writing a rule for it by hand.
The generator transforms a random latent input into a candidate observation. A model for images could output a grid of pixel values. The random input provides variation, while the generator’s parameters determine how that variation becomes an output.
Our example removes image complexity. The input is a scalar Gaussian noise draw, and the generator adds a trainable shift. It can move the distribution left or right, but cannot change its shape or represent several distinct modes.
The live experiment makes the task that small on purpose. The real collection is 128 numbers centred near 1.5. The generator starts with a shift of −1.5, so its 128 outputs are centred near −1.5, three units to the left of where they should be. Both collections are built from the same noise draws, so they have the same spread, and the generator would reproduce the real collection exactly if its shift reached 1.5. Nothing tells it that number. It has to find it from feedback.
2. Learn to distinguish sources
The discriminator takes an observation and returns a score interpreted as the probability that it came from the real collection. Its training examples mix real observations with outputs of the current generator.
The basic GAN uses source labels, real or generated, which the training procedure knows automatically. Class labels such as digit identity are optional additional information in a conditional GAN. A conditional generator and discriminator both receive the condition.
The discriminator in the live experiment is as small as the generator. It multiplies the number it is shown by one weight, adds one bias, and passes the result through a sigmoid, which turns any number into a score between zero and one. A positive weight means that larger numbers look more real to it. It starts with a weight of 0.5 and a bias of zero, which is already a rough guess in the right direction. With those values it gives the real collection a mean score of 0.667 and the generated collection a mean score of 0.329.
Now the two objectives can be stated, and they are not the same objective. The discriminator is trained to be right about sources. That means raising its score on real examples and lowering its score on generated ones. The generator is trained to be mistaken for real. That means raising the discriminator’s score on generated examples. The second row of the table is where the two collide.
| Quantity | At the start | The discriminator wants it | The generator wants it |
|---|---|---|---|
| Mean score on real examples | 0.667 | higher | cannot change it |
| Mean score on generated examples | 0.329 | lower | higher |
The starting scores of the live experiment, with a discriminator weight of 0.5, a bias of zero and a generator shift of −1.5.
This is how a second model can stand in for the rule nobody could write. The discriminator’s score is a calculation, so the generator can ask how that score would change if its output changed a little, and the answer is a direction to move in. Here the weight is positive, so a larger number earns a higher score, and the generator learns to raise its shift. That is toward the real collection, because a discriminator that is doing its job has a weight that points from the generated numbers toward the real ones.
Keep in mind
The generator is never shown a real example. Everything it learns about the real collection reaches it through the discriminator’s score, so its feedback is only as good as the discriminator giving it. Part 3 returns to what happens when that opponent is too weak, or changes too quickly.
Part 2. Calculate the Adversarial Feedback
3. Define the two losses separately
Part 1 said what each model wants. Training needs that written as a number to make smaller, one number for each model. Both are built from the same ingredient, the negative logarithm of a probability. It is close to zero when the probability is close to one, and it grows without limit as the probability falls toward zero. So it charges a model for giving a low probability to the answer it should have given.
Let G map noise z to a generated observation and D return a real-source probability. The discriminator should give real examples high probabilities and generated examples low probabilities. This SDK uses the average of the real and generated binary cross-entropies.
L_D = −½ (E_real[log D(x)]
+ E_noise[log(1 − D(G(z)))])
L_G = −E_noise[log D(G(z))]Read the first line one term at a time. The term log D(x) is charged on real examples, and it costs least when the real score is near one. The term log(1 − D(G(z))) is charged on generated examples, and it costs least when the generated score is near zero. The second line charges the generator on those same generated examples for the opposite outcome, which is a generated score that is not near one. E means an average, over the real examples in one case and over the noise draws in the other.
The generator uses the non-saturating objective shown in the second line. It encourages a higher real score for generated examples and provides a stronger learning signal when the discriminator initially rejects them confidently. It differs from minimizing the generator term in the original minimax expression.
›Why the generator does not minimize log(1 − D(G(z)))
The original minimax expression has the generator minimize the very term the discriminator is charged on, log(1 − D(G(z))), pulled the other way. Both that term and the non-saturating one fall as the generated score rises, so both point the generator in the same direction. They differ in how hard they push. Write s for the discriminator’s logit, so that D is the sigmoid of s, and take the slope of each with respect to s.
d/ds log(1 − sigmoid(s)) = −sigmoid(s) = −D
d/ds (−log sigmoid(s)) = −(1 − sigmoid(s)) = −(1 − D)The first slope is the generated score itself, and the second is one minus it. Early in training the discriminator rejects generated examples, so D is small. A small D leaves the minimax slope close to zero, which is the saturation the name refers to, while the non-saturating slope stays close to one in size. At the draw traced in the next step D is about 0.321, so the two slopes are 0.321 and 0.679 in size, and the gap widens as D falls.
The factor of one half sets the discriminator gradient scale in this implementation. It does not combine the generator and discriminator into a single loss that both minimize.
4. Trace a single generated number
For an illustrative zero noise draw, use the initial generator shift and discriminator parameters. Sigmoid converts the discriminator’s weighted sum into its real score.
G(z) = z + m
D(x) = sigmoid(w x + b)
z = 0, m = −1.5, w = 0.5, b = 0
Generated value = 0 + (−1.5) = −1.5
Discriminator logit = (0.5 × (−1.5)) + 0 = −0.75
D(G(z)) = 1 / (1 + exp(0.75))
Generator loss for this draw = −log D(G(z))Carrying the arithmetic through gives the two numbers the trace ends on.
D(G(z)) = 1 / (1 + exp(0.75)) ≈ 0.321
Generator loss = −log(0.321) ≈ 1.137A score of 0.321 says the discriminator thinks this value is probably generated, and it is right. The loss of 1.137 is what the generator is charged for that.
The feedback is the slope of that loss with respect to the shift. Raising the shift by a small amount raises the generated value by the same amount. The weight of 0.5 passes half of that on to the logit, and the loss falls as the logit rises. With a learning rate of 0.1, one descent step on this single draw works out as follows.
d(loss)/d(logit) = −(1 − D(G(z))) = −(1 − 0.321) ≈ −0.679
d(logit)/d(m) = w = 0.5
d(loss)/d(m) ≈ (−0.679) × 0.5 ≈ −0.340
New shift = −1.5 − (0.1 × (−0.340)) ≈ −1.466The slope is negative, so the loss falls as the shift rises, and the step moves the shift up from −1.5 to about −1.466. That is the adversarial feedback in the title of this part. No rule said the generated numbers should be larger. The discriminator’s weight said it.
The discriminator’s loss needs a real example as well as a generated one. The real collection is the noise with 1.5 added, so the real partner of this zero draw is 1.5.
Real logit = (0.5 × 1.5) + 0 = 0.75
D(x) = 1 / (1 + exp(−0.75)) ≈ 0.679
L_D = −½ (log(0.679) + log(1 − 0.321))
≈ −½ ((−0.387) + (−0.387))
≈ 0.387The two terms are equal because the real value and the generated value sit the same distance either side of zero, so this discriminator is exactly as sure of one as of the other. Its loss is smaller than the generator’s because it is the model that is currently right.
The live experiment averages over a fixed batch of Gaussian draws, not just this zero draw. During the discriminator update, generated values are treated as fixed examples. During the generator update, the gradient passes through the fixed discriminator to the generator shift.
| Quantity | The zero draw alone | Mean over the 128 draws |
|---|---|---|
| Score on the real value | 0.679 | 0.667 |
| Score on the generated value | 0.321 | 0.329 |
| Discriminator loss | 0.387 | 0.418 |
| Generator loss | 1.137 | 1.173 |
The starting game, before any update. The right-hand column is what the live example reports at step zero.
One full alternation at a learning rate of 0.1 then moves the discriminator’s weight from 0.5 to 0.538 and the generator’s shift from −1.5 to −1.463. The shift moved a little further than the −1.466 worked above, and the order of the alternation is the reason. The discriminator is updated first, so the generator stepped against a weight of 0.538 and not 0.5.
Check yourself
Questions on Parts 1 and 2
Question 1 of 5
At the start of the live experiment the discriminator gives generated examples a mean score of 0.329. What does each model want that number to do?
Part 3. Train a Small Game and Inspect Its Limits
5. Run the actual alternating updates
The training distribution is the same fixed Gaussian noise batch shifted toward the positive side. Reusing the noise keeps before-and-after comparisons from changing merely because a fresh batch was sampled.
import numpy as np
from oop_ml.numpy.modern import AdversarialGaussian
noise = np.random.default_rng(3).normal(size=128)
real = noise + 1.5
history = AdversarialGaussian(generator_mean=-1.5).train(
real, noise, steps=30, learning_rate=0.1,
)
print(history[-1])Each recorded state reports both losses and both mean discriminator scores after a complete alternation. A discriminator score near one half can indicate confusion, but by itself cannot prove that a rich data distribution has been matched.
| Step | Generator shift | Mean D(real) | Mean D(fake) | D loss | G loss |
|---|---|---|---|---|---|
| 0 | −1.500 | 0.667 | 0.329 | 0.418 | 1.173 |
| 15 | −0.755 | 0.747 | 0.355 | 0.407 | 1.199 |
| 30 | 0.081 | 0.744 | 0.483 | 0.552 | 0.861 |
Thirty alternations at a learning rate of 0.1, which are the live example’s default settings. The target shift is 1.5.
Read the shift column first. It rises at every one of the thirty steps, from −1.5 to 0.081, so the generator is being pushed the right way throughout. It is also far from finished. The real collection sits at 1.5, and after thirty steps the shift is still 1.42 short of it.
Now read the two loss columns, and notice that neither tells that story. The generator loss rose during each of the first seven steps, and at step 15 it is 1.199, higher than the 1.173 it started at, although the shift had improved at every step. It rose because the discriminator was improving faster than the generator was. The discriminator’s weight went from 0.5 to 0.878 over those fifteen steps, so it had become more confident about the same generated values. From step 8 to step 30 it is the discriminator loss that rises, because the generated values are moving in among the real ones and are harder to reject. Each loss measures one model against its opponent as that opponent currently stands, and the opponent keeps changing.
The last row also carries a small warning about reading the scores. At step 30 the mean score on generated values is 0.483, close to one half, while the shift is still 1.42 away from the target.
A longer run shows something a single falling loss never would. The table below gives the state after 150 alternations at the smallest, the default and the largest learning rate the live example offers.
| Learning rate | Generator shift | Mean D(real) | Mean D(fake) | D loss | G loss |
|---|---|---|---|---|---|
| 0.01 | −0.776 | 0.746 | 0.352 | 0.405 | 1.207 |
| 0.1 | 2.006 | 0.446 | 0.443 | 0.697 | 0.813 |
| 0.3 | 1.491 | 0.500 | 0.500 | 0.693 | 0.693 |
The state after 150 alternations, the most the live example allows. The target shift is 1.5.
At 0.01 the steps are too small to get far. At 0.1 the shift has gone past the target. It reached 1.5 at step 74, kept going, and peaked at 2.012 at step 141. It can overshoot because the generator does not know where the real collection is. It follows the discriminator’s weight, and that weight was still positive when the shift passed 1.5. Once the generated values sit to the right of the real ones, larger numbers begin to look less real, the weight falls through zero, and the feedback turns around. By step 150 the weight is −0.022 and the shift has started back.
At 0.3 the same excursion happened sooner. The shift peaked at 2.007 at step 46 and had come back to 1.491 by step 150. That last row is what a settled game looks like here. Both mean scores are 0.500 and both losses are 0.693, which is not an accident.
−log(0.5) = log 2 ≈ 0.693A discriminator that answers one half to everything is charged log 2 on every example, real or generated, and the generator is charged the same. So 0.693 is the sign that the discriminator has nothing left to go on. It is not zero, and neither loss was ever heading for zero.
Keep in mind
These are measurements on this page’s one batch of 128 draws. They do not say that 0.3 is a better learning rate than 0.1 in general. They say that this game overshoots its answer and comes back before it settles, and that 150 steps at the larger rate get through more of that.
6. Look for missing variety and changing opponents
A mode is a region where the data concentrates. A collection of handwritten digits has one for each digit, and a generator that draws only convincing sevens has settled on a single one of them.
Mode collapse occurs when a generator produces only a narrow subset of the data’s variety. Several attractive examples can hide that failure. A discriminator that is too weak can also give unhelpful feedback, while a rapidly improving one can make the generator’s task difficult.
A collapse is easy to miss because of how samples are usually judged, which is one at a time. Each of those sevens looks real, and nothing in a judgment of one picture counts how many kinds of picture were produced. Variety is a property of the whole generated collection, and it has to be checked on the collection.
Both warnings about the opponent appeared in step 5 in small form. The rising generator loss of the first seven steps was a discriminator improving faster than the generator. The overshoot past 1.5 was a discriminator whose advice was out of date, since its weight still pointed right after the generated values had arrived.
Our scalar family cannot demonstrate image realism or mode coverage. It does expose the two objectives and actual alternating gradients. Evaluating a full GAN requires checking the generated distribution, variety, and task-specific quality beyond its training losses.
The reason it cannot is worth spelling out. The generator adds one number to every draw, so its outputs always form a single cluster with the spread of the noise. If the real collection were two separate clusters, no shift would cover both. The discriminator is limited in a matching way. One weight and one bias can only say that larger numbers look more real, or that they look less real, so it has no way to describe real values lying on both sides of the generated ones. The last practice problem runs that case and reports what both models end up saying.
The original paper introduces the two-model game and discusses the non-saturating generator objective. Generative Adversarial Nets.
Continue with Diffusion Models.
Check yourself
Questions on Part 3
Question 1 of 5
The same fixed Gaussian noise batch is reused so that a before-and-after comparison cannot change merely because a fresh batch was sampled.
Try it with Botie
Build with Botie
Write and run Python