Generative Adversarial Nets
Explained as a training loop with two optimisers, because that's what it is — plus the four ways it falls over, which the paper only hints at.
Download gan_from_scratch.py — everything below runs from it. NumPy, plus a tiny CPU PyTorch model for the training step. Twelve seconds on a laptop.
I'm an AI Engineer. I've shipped models, done the MLOps, and I still couldn't read this paper. Not because of the maths — there's one formula and one proof, and both fit on a page. Because nobody told me the trick: the loss function is a second model. Once you see that, the paper is a training loop you've already written, with one extra optimiser. The interesting part is what goes wrong.
The loss function is a model
You know this loop:
for x, y in data:
loss = criterion(model(x), y) # criterion is fixed: MSE, cross-entropy
loss.backward(); opt.step()
The criterion is a constant judge. It never learns. A GAN swaps it for a judge that is learning too — against you:
fake = G(z) # forger's attempt
loss_D = -(log(D(real)) + log(1 - D(fake.detach()))).mean() # detective's turn
loss_D.backward(); opt_D.step()
loss_G = -log(D(G(z))).mean() # forger's turn
loss_G.backward(); opt_G.step() # and repeat
That's the whole paper's beating heart. Two networks, two losses, taking turns. The paper's own analogy is counterfeiters versus police; mine is a red team and a blue team, each retraining on the other's latest output. The clever bit is that the forger never sees the real data. All it ever sees is the detective's verdict — the detective is its loss function, and that loss gets sharper every round. Nobody writes down p(x). No likelihood, no loss curve that means "better": just samples and backprop. That is why it worked, and why the logs are so hard to read.
| Term | Means | Programmer's analogy |
|---|---|---|
| G, generator | Noise in, sample out | A procedural generator: seed → level |
| D, discriminator | Sample in, P(real) out | A spam filter, retrained every round |
| z | Random noise fed to G | The RNG seed. Different seed, different sample |
| pg | The distribution of G(z) over all z | Everything your generator can produce, as a histogram |
| .detach() | A tensor with its history cut off | A read-only view. Gradients stop here |
The one formula
Equation 1. Read it as a scoreboard the detective wants high and the forger wants low:
| Step | Shape | What it does |
|---|---|---|
z | (n, noise_dim) | n random seeds. Gaussian noise; nothing learned |
G(z) | (n, 1) | n fakes. Here a point on a line; in the paper, an image |
D(x) | (n, 1) | Probability of "real", between 0 and 1 |
log D(x) | mean over n | Detective's score on real data. 0 if it's sure, very negative if wrong |
log(1 − D(G(z))) | mean over n | Detective's score on fakes. 0 if it spots every one |
maxD | — | The detective's turn: push both terms up |
minG | — | The forger's turn: push the second term down. It can't touch the first |
In code that is d_loss and g_loss_saturating, two lines each,
in the code section below. D maximises the whole thing, so its loss is the negative; G
only appears in the second term. Then the one practical trick the paper adds in the
paragraph after equation 1: don't train G to minimise log(1 − D(G(z))), train it to
maximise log D(G(z)). Same fixed point, "much stronger gradients early in learning".
That's g_loss, the non-saturating loss. It is a recommendation, not part of
the theory — the proofs are about equation 1. Everyone implements the recommendation.
The next two sections show why.
The best detective is a ratio, and −log 4 is the finish line
Section 4 proves two things, and both are arithmetic you can check on a grid with no networks. Proposition 1: for a fixed forger, the best possible detective is D*(x) = pdata(x) / (pdata(x) + pg(x)). Theorem 1: with that detective the value of the game is −log 4 + 2·JSD(pdata ‖ pg), so its global minimum is −log 4 = −1.386, reached exactly when the forger's distribution equals the data's. My data is three Gaussian bumps at −6, −2 and +2 — the paper's own Figure 1 is a 1-D toy like this. Three candidate forgers, scored in closed form:
-log 4 = -1.386 <- the value of the game if the forger wins
forger D*(-6) D*(-2) V(D*,G) -log4 + 2*JSD
only the bump at -2 1.000 0.250 -0.750 -0.750
all three, twice as wide 0.667 0.667 -1.203 -1.203
p_data exactly 0.500 0.500 -1.386 -1.386
Where the forger never goes (x = −6, first row) the detective is certain: 1.000. Where the forger matches the data exactly, the detective is a coin flip everywhere, 0.500, and the value bottoms out at −log 4. The last two columns agree to three decimals: that's Theorem 1, checked numerically.
Two losses, and why the trick exists
D is a sigmoid on a logit. Differentiate each forger loss with respect to that logit and each collapses to a single term:
saturating log(1 - D) -> d/dlogit = -D -> 0 as D -> 0
non-saturating -log D -> d/dlogit = -(1 - D) -> 1 as D -> 0
D(G(z)) |grad| saturating |grad| non-sat
0.500 0.500 0.500
0.100 0.100 0.900
0.010 0.010 0.990
0.001 0.001 0.999
Early in training the forger is terrible and the detective is sure of it: D(G(z)) ≈ 0. The loss as written then has a gradient of ≈ 0 — exactly when the forger most needs one. The trick flips it: the worse you're doing, the bigger the push. Measured on the real networks, after the detective gets a 1000-round head start against a frozen forger:
D(G(z)) after the head start: 0.0071
|gradG| with saturating (eq. 1) : 0.0213
|gradG| with non-saturating (the trick) : 2.2216
Same networks, same noise, same detective. A hundredth of the signal. "Saturating" means the sigmoid saturating — the same disease as an unscaled softmax in the attention paper. Numerical hygiene, again.
The game, played properly
One D step, one G step, 4000 rounds, Adam. The script prints the detective's accuracy on real and on fake data, the paper's V, the norm of the forger's gradient, the spread of 1000 fakes, and how many land within 1.0 of each bump:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
500 0.83 / 0.29 -1.363 0.064 3.42 108 / 177 / 308
1000 0.29 / 0.67 -1.380 0.081 3.10 192 / 95 / 342
2000 0.64 / 0.58 -1.369 0.116 3.34 210 / 165 / 358
3000 0.61 / 0.41 -1.377 0.492 3.64 224 / 258 / 346
4000 0.94 / 0.09 -1.384 0.215 3.78 238 / 272 / 330
final fakes: mean -2.01 std 3.78 off every bump: 160 of 1000
Three things to read. The last column: fakes on all three bumps, not perfectly even, still evening out. V: parked at −1.38, which is −log 4 to two decimals — the forger has effectively won. And the accuracy pair: 0.94 / 0.09 looks lopsided until you add them. Real plus fake ≈ 1.03 is chance — a detector that says "real" to everything scores exactly 1.00 on that sum. There is nothing left to detect. Note the detective's learning rate is a quarter of the forger's (5e-4 against 2e-3); at equal rates, on this data, the middle bump starves. The game is a balancing act, and the rest of this page is what happens when the balance goes.
The failure modes have names
The theory assumes the detective is optimal at every step and the forger has unlimited capacity. Neither is ever true, and section 6 admits it in a sentence: G must not be trained too much without updating D, "to avoid the Helvetica scenario in which G collapses too many values of z to the same value of x". Everyone who trains one of these hits that and its cousins. Four of them, each one edit away from the healthy run.
Mode collapse: the forger out-runs the detective
Give the forger ten times the learning rate, lr_g=2e-2. Every 100 rounds:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
100 1.00 / 0.00 -1.373 0.013 0.27 0 / 0 / 9
300 0.60 / 1.00 -1.276 0.038 0.31 0 / 992 / 0
400 0.00 / 1.00 -1.222 0.795 4.68 116 / 1 / 0
600 0.53 / 0.65 -1.372 0.022 5.74 58 / 52 / 571
800 0.33 / 1.00 -1.275 0.009 0.24 648 / 0 / 0
1000 0.54 / 0.46 -1.397 0.042 3.32 276 / 59 / 397
Fake std 0.31: a thousand samples in one heap. At round 300 the heap is on −2, at 600 it's on +2, at 800 it's on −6. The forger finds the one place the detective currently rates "real", moves everything there, the detective catches up, and it hops. It never learns to be in three places at once, because one place is always the cheapest next move. That's the Helvetica scenario, with counts. What did not collapse in my runs: five G steps per D step at the normal rate, and a 1-dimensional z. In 1-D a staircase from one noise value to three bumps is easy to learn; those two bite in 2-D and on images.
The detective wins: the forger's gradient vanishes
Now the opposite. Momentum SGD (what the paper says it used), five D steps per G step (Algorithm 1's k; the paper used 1), and G's loss exactly as equation 1 writes it:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
200 1.00 / 1.00 -0.017 0.044 0.14 0 / 0 / 0
400 0.99 / 1.00 -0.018 0.073 0.20 0 / 0 / 0
final fakes: mean +0.18 std 0.20 off every bump: 1000 of 1000
The detective is perfect. V is −0.02 instead of −1.39. And the forger has not moved: 400 rounds in, its samples still sit at 0 with a spread of 0.2, where the random init put them. A perfect detective is a useless teacher. Same run, one change — the non-saturating loss:
100 0.67 / 1.00 -0.729 0.671 1.01 0 / 0 / 604
400 0.67 / 1.00 -0.818 1.334 0.27 0 / 0 / 1000
The forger moves in the first 100 rounds — straight onto one bump, because a strong detective under SGD is still a bad game. But it moves. That's the entire content of the paper's "in practice" paragraph, in two tables.
The forgotten detach
In the detective's turn the fakes come out of G. If you don't .detach() them,
D's loss backpropagates through D and on into G. The norm of G's gradient right
after a D step:
detach=True |gradG| after D's step: 0.0000
detach=False |gradG| after D's step: 0.0606
cosine(leaked gradient, G's own gradient): -1.000
Cosine −1: the leaked gradient points exactly against the forger's own. Of course it does —
D's loss wants fakes more detectable. Whether it is ever applied depends on where
your zero_grad() sits. Zero G right before its own backward and the leak is
wiped; you only paid for a wasted backward pass, which is why this bug is often silent.
This script zeroes both networks at the top of the round, so it is applied:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
1000 0.33 / 1.00 -1.081 0.106 1.85 198 / 339 / 0
3000 0.33 / 1.00 -1.128 0.066 2.21 430 / 430 / 0
A whole bump gone, and the detective at 1.00 on fakes for the entire run. Compare the last column with the healthy table.
Forgetting to alternate: not a game any more
The tempting shortcut: add the two losses, one optimiser over both networks, one
backward(). It compiles, it runs, and it is not adversarial. D's own term
pushes D(G(z)) down; G's term, now flowing into D's weights too, pushes it up; they cancel
where D(G(z)) = 0.5. And G's gradient through the combined loss is zero at exactly that
point. Nobody pushes anybody:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
600 1.00 / 0.78 -0.694 0.000 0.00 0 / 0 / 0
1000 1.00 / 0.77 -0.693 0.001 0.00 0 / 0 / 0
final fakes: mean +0.03 std 0.00 off every bump: 1000 of 1000
Fake std 0.00 for a thousand rounds: the forger never left its starting point. V is −0.693 = −log 2, which is log 1 + log ½ — the detective says 1 to every real sample and ½ to every fake, and has no reason to say anything else. A game needs two players who each want something the other doesn't. One loss has one wish.
நான் ஒரு AI Engineer. Models ship செய்திருக்கிறேன், MLOps செய்திருக்கிறேன் — இருந்தும் இந்தக் கட்டுரையைப் படிக்க முடியவில்லை. கணிதம் பிரச்சனை இல்லை — ஒரே ஒரு formula, ஒரே ஒரு proof, இரண்டும் ஒரு பக்கத்தில் அடங்கும். பிரச்சனை என்னவென்றால் இந்த ஒரு விஷயத்தை யாரும் சொல்லவில்லை: loss function-ஏ ஒரு இரண்டாவது model. அது புரிந்த பிறகு, இந்தக் கட்டுரை நீங்கள் ஏற்கெனவே எழுதியிருக்கும் training loop-தான், ஒரு கூடுதல் optimiser-உடன். சுவாரஸ்யமான பகுதி — என்னென்ன தவறாகப் போகிறது என்பதுதான்.
Loss function-ஏ ஒரு model
இந்த loop உங்களுக்குத் தெரியும்:
for x, y in data:
loss = criterion(model(x), y) # criterion நிலையானது: MSE, cross-entropy
loss.backward(); opt.step()
Criterion ஒரு மாறாத நடுவர். அது ஒருபோதும் கற்றுக்கொள்வதில்லை. GAN அதை மாற்றி, கற்றுக்கொண்டே இருக்கும் ஒரு நடுவரை வைக்கிறது — உங்களுக்கு எதிராக:
fake = G(z) # forger-ன் முயற்சி
loss_D = -(log(D(real)) + log(1 - D(fake.detach()))).mean() # detective-ன் முறை
loss_D.backward(); opt_D.step()
loss_G = -log(D(G(z))).mean() # forger-ன் முறை
loss_G.backward(); opt_G.step() # திரும்பச் செய்
இதுதான் கட்டுரையின் இதயத் துடிப்பு. இரண்டு networks, இரண்டு losses, மாறி மாறி ஒரு முறை. கட்டுரையின் உவமை கள்ளநோட்டு அடிப்பவர்கள் எதிராக போலீஸ்; என்னுடையது ஒரு red team-ம் ஒரு blue team-ம், ஒவ்வொருவரும் மற்றவரின் சமீபத்திய output-ஐ வைத்து மீண்டும் train ஆகிறார்கள். சூட்சுமம் என்னவென்றால்: forger உண்மையான data-வை ஒருபோதும் பார்ப்பதே இல்லை. அது பார்ப்பதெல்லாம் detective-ன் தீர்ப்பு மட்டுமே — detective-ஏ அதன் loss function, அந்த loss ஒவ்வொரு சுற்றிலும் கூர்மையாகிறது. யாரும் p(x)-ஐ எழுதுவதில்லை. Likelihood இல்லை, "இப்போது நன்றாக இருக்கிறது" என்று பொருள்படும் loss curve இல்லை: samples-ம் backprop-ம் மட்டுமே. அதனால்தான் இது வேலை செய்தது, அதனால்தான் logs படிக்க இவ்வளவு கடினம்.
| சொல் | அர்த்தம் | Programmer-ன் உவமை |
|---|---|---|
| G, generator | Noise உள்ளே, sample வெளியே | ஒரு procedural generator: seed → level |
| D, discriminator | Sample உள்ளே, P(real) வெளியே | ஒரு spam filter, ஒவ்வொரு சுற்றிலும் மீண்டும் train ஆனது |
| z | G-க்குக் கொடுக்கப்படும் random noise | RNG seed. வேறு seed, வேறு sample |
| pg | எல்லா z-க்கும் G(z)-ன் distribution | உங்கள் generator உருவாக்கக்கூடிய அனைத்தும், histogram-ஆக |
| .detach() | வரலாறு துண்டிக்கப்பட்ட ஒரு tensor | ஒரு read-only view. Gradients இங்கே நின்றுவிடும் |
அந்த ஒரே formula
Equation 1. இதை ஒரு scoreboard-ஆகப் படியுங்கள் — detective அதை உயர்த்த விரும்புகிறது, forger தாழ்த்த விரும்புகிறது:
| படி | Shape | என்ன செய்கிறது |
|---|---|---|
z | (n, noise_dim) | n random seeds. Gaussian noise; எதுவும் கற்கப்படவில்லை |
G(z) | (n, 1) | n போலிகள். இங்கே ஒரு கோட்டின் மேல் ஒரு புள்ளி; கட்டுரையில் ஒரு image |
D(x) | (n, 1) | "உண்மை" என்பதற்கான probability, 0-க்கும் 1-க்கும் இடையே |
log D(x) | n-ன் மேல் mean | உண்மையான data-வில் detective-ன் மதிப்பெண். உறுதியாக இருந்தால் 0, தவறானால் மிகவும் எதிர்மறை |
log(1 − D(G(z))) | n-ன் மேல் mean | போலிகளில் detective-ன் மதிப்பெண். ஒவ்வொன்றையும் கண்டுபிடித்தால் 0 |
maxD | — | Detective-ன் முறை: இரண்டு terms-ஐயும் மேலே தள்ளு |
minG | — | Forger-ன் முறை: இரண்டாவது term-ஐ மட்டும் கீழே தள்ளு. முதல் term-ஐத் தொட முடியாது |
Code-ல் இது d_loss-ம் g_loss_saturating-ம், தலா
இரண்டு வரிகள், கீழே code பகுதியில். D முழு விஷயத்தையும் maximise செய்கிறது,
எனவே அதன் loss எதிர்மறை; G இரண்டாவது term-ல் மட்டுமே வருகிறது. பிறகு
equation 1-க்குப் பின் வரும் paragraph-ல் கட்டுரை சேர்க்கும் ஒரே நடைமுறை
தந்திரம்: G-ஐ log(1 − D(G(z)))-ஐ minimise செய்யப் பயிற்றுவிக்காதீர்கள்,
log D(G(z))-ஐ maximise செய்யப் பயிற்றுவியுங்கள். அதே fixed point, ஆனால்
"much stronger gradients early in learning". அதுதான் g_loss,
non-saturating loss. இது ஒரு பரிந்துரை, theory-ன் பகுதி அல்ல — proofs
எல்லாம் equation 1-ஐப் பற்றியவை. ஆனால் எல்லோரும் பரிந்துரையைத்தான்
implement செய்கிறார்கள். ஏன் என்பதை அடுத்த இரண்டு பகுதிகள் காட்டுகின்றன.
Loss function-ஏ oru rendaavadhu model. Adhu ungalukku ethiraa kathukkuthu. Equation 1-la D score-ai methala thallum, G kelaya thallum — aana practice-la G-kku non-saturating loss dhaan velai seiyum.
சிறந்த detective ஒரு ratio, −log 4 தான் இறுதிக் கோடு
Section 4 இரண்டு விஷயங்களை நிரூபிக்கிறது, இரண்டுமே networks இல்லாமல் ஒரு grid-ல் நீங்களே சரிபார்க்கக்கூடிய கணக்கு. Proposition 1: ஒரு நிலையான forger-க்கு, சாத்தியமான சிறந்த detective D*(x) = pdata(x) / (pdata(x) + pg(x)). Theorem 1: அந்த detective-உடன் game-ன் மதிப்பு −log 4 + 2·JSD(pdata ‖ pg), எனவே அதன் global minimum −log 4 = −1.386, forger-ன் distribution data-வுடன் சமமாகும் போது சரியாக அடையப்படுகிறது. என் data என்பது −6, −2, +2-ல் மூன்று Gaussian குன்றுகள் — கட்டுரையின் Figure 1-ம் இப்படி ஒரு 1-D பொம்மைதான். மூன்று forger வேட்பாளர்கள், closed form-ல் மதிப்பிடப்பட்டவை:
-log 4 = -1.386 <- forger ஜெயித்தால் game-ன் மதிப்பு
forger D*(-6) D*(-2) V(D*,G) -log4 + 2*JSD
only the bump at -2 1.000 0.250 -0.750 -0.750
all three, twice as wide 0.667 0.667 -1.203 -1.203
p_data exactly 0.500 0.500 -1.386 -1.386
Forger ஒருபோதும் போகாத இடத்தில் (x = −6, முதல் வரிசை) detective நிச்சயமாக இருக்கிறது: 1.000. Forger data-வுடன் சரியாகப் பொருந்தும் இடத்தில், detective எல்லா இடத்திலும் ஒரு நாணயச் சுழற்சி, 0.500, மதிப்பு −log 4-ல் தரையிறங்குகிறது. கடைசி இரண்டு columns மூன்று தசம இடங்கள் வரை ஒத்துப் போகின்றன: அதுதான் Theorem 1, எண்களால் சரிபார்க்கப்பட்டது.
இரண்டு losses, அந்தத் தந்திரம் ஏன் இருக்கிறது
D என்பது ஒரு logit-ன் மேல் ஒரு sigmoid. ஒவ்வொரு forger loss-ஐயும் அந்த logit-ஐப் பொறுத்து differentiate செய்யுங்கள், ஒவ்வொன்றும் ஒரே ஒரு term-ஆகச் சுருங்குகிறது:
saturating log(1 - D) -> d/dlogit = -D -> 0 as D -> 0
non-saturating -log D -> d/dlogit = -(1 - D) -> 1 as D -> 0
D(G(z)) |grad| saturating |grad| non-sat
0.500 0.500 0.500
0.100 0.100 0.900
0.010 0.010 0.990
0.001 0.001 0.999
Training-ன் ஆரம்பத்தில் forger மோசமாக இருக்கிறது, detective அதை உறுதியாக அறிகிறது: D(G(z)) ≈ 0. அப்போது எழுதப்பட்ட loss-ன் gradient ≈ 0 — சரியாக forger-க்கு அது மிகவும் தேவைப்படும் தருணத்தில். தந்திரம் அதைத் தலைகீழாக்குகிறது: நீங்கள் எவ்வளவு மோசமாக இருக்கிறீர்களோ, அவ்வளவு பெரிய தள்ளுதல். உண்மையான networks-ல் அளக்கப்பட்டது — உறைந்த forger-க்கு எதிராக detective-க்கு 1000 சுற்று முன்னோட்டம் கொடுத்த பிறகு:
D(G(z)) after the head start: 0.0071
|gradG| with saturating (eq. 1) : 0.0213
|gradG| with non-saturating (the trick) : 2.2216
அதே networks, அதே noise, அதே detective. Signal-ல் நூறில் ஒரு பங்கு. "Saturating" என்றால் sigmoid saturate ஆவது — attention கட்டுரையில் scale செய்யாத softmax-ன் அதே நோய். மீண்டும், numerical சுத்தம்.
Aarambathula D romba confident-a irukkum, adhanaala equation 1-oda loss-ku gradient kidaiyaadhu. Non-saturating loss adhai thalakeezhaa maathi, mosamaa irukkum podhu periya push kodukkuthu — 0.0213 vs 2.2216.
விளையாட்டு, சரியாக விளையாடப்பட்டது
ஒரு D step, ஒரு G step, 4000 சுற்றுகள், Adam. Script அச்சிடுவது: உண்மையான மற்றும் போலி data-வில் detective-ன் accuracy, கட்டுரையின் V, forger-ன் gradient-ன் norm, 1000 போலிகளின் பரவல், மற்றும் ஒவ்வொரு குன்றுக்கும் 1.0 தூரத்திற்குள் எத்தனை விழுகின்றன:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
500 0.83 / 0.29 -1.363 0.064 3.42 108 / 177 / 308
1000 0.29 / 0.67 -1.380 0.081 3.10 192 / 95 / 342
2000 0.64 / 0.58 -1.369 0.116 3.34 210 / 165 / 358
3000 0.61 / 0.41 -1.377 0.492 3.64 224 / 258 / 346
4000 0.94 / 0.09 -1.384 0.215 3.78 238 / 272 / 330
final fakes: mean -2.01 std 3.78 off every bump: 160 of 1000
மூன்று விஷயங்களைப் படியுங்கள். கடைசி column: மூன்று குன்றுகளிலும் போலிகள், சரியாகச் சமமாக இல்லை, ஆனாலும் சமன் ஆகிக்கொண்டிருக்கிறது. V: −1.38-ல் நிறுத்தப்பட்டிருக்கிறது, அது இரண்டு தசம இடங்களுக்கு −log 4 — forger நடைமுறையில் ஜெயித்துவிட்டது. Accuracy ஜோடி: 0.94 / 0.09 ஒருபக்கமாகத் தெரிகிறது, ஆனால் கூட்டிப் பாருங்கள். Real + fake ≈ 1.03 என்பது தற்செயல் — எல்லாவற்றுக்கும் "real" என்று சொல்லும் detector அந்தக் கூட்டலில் சரியாக 1.00 பெறும். கண்டுபிடிக்க எதுவும் மிச்சமில்லை. Detective-ன் learning rate forger-னுடையதில் கால் பங்கு (5e-4 எதிராக 2e-3); சம rate-களில், இந்த data-வில், நடு குன்று பட்டினி கிடக்கிறது. இந்த விளையாட்டு ஒரு சமன்பாட்டுக் கலை, இந்தப் பக்கத்தின் மீதி எல்லாம் அந்தச் சமநிலை போனால் என்ன ஆகிறது என்பதுதான்.
தோல்வி முறைகளுக்குப் பெயர்கள் உண்டு
Theory ஒவ்வொரு step-லும் detective உகந்ததாக இருக்கும் என்றும், forger-க்கு வரம்பற்ற capacity இருக்கும் என்றும் கருதுகிறது. இரண்டுமே ஒருபோதும் உண்மையல்ல, section 6 அதை ஒரே வாக்கியத்தில் ஒப்புக்கொள்கிறது: D-ஐ update செய்யாமல் G-ஐ அதிகமாகப் பயிற்றுவிக்கக் கூடாது, "to avoid the Helvetica scenario in which G collapses too many values of z to the same value of x". இதைப் பயிற்றுவிக்கும் எல்லோரும் அதையும் அதன் உறவினர்களையும் சந்திக்கிறார்கள். நான்கு பேர், ஒவ்வொருவரும் ஆரோக்கியமான run-ல் இருந்து ஒரே ஒரு edit தூரம்.
Mode collapse: forger detective-ஐ முந்திவிடுகிறது
Forger-க்குப் பத்து மடங்கு learning rate கொடுங்கள், lr_g=2e-2. ஒவ்வொரு 100 சுற்றுகளிலும்:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
100 1.00 / 0.00 -1.373 0.013 0.27 0 / 0 / 9
300 0.60 / 1.00 -1.276 0.038 0.31 0 / 992 / 0
400 0.00 / 1.00 -1.222 0.795 4.68 116 / 1 / 0
600 0.53 / 0.65 -1.372 0.022 5.74 58 / 52 / 571
800 0.33 / 1.00 -1.275 0.009 0.24 648 / 0 / 0
1000 0.54 / 0.46 -1.397 0.042 3.32 276 / 59 / 397
Fake std 0.31: ஆயிரம் samples ஒரே குவியலில். 300-வது சுற்றில் குவியல் −2-ல், 600-ல் +2-ல், 800-ல் −6-ல். Detective தற்போது "real" என்று மதிப்பிடும் ஒரே இடத்தை forger கண்டுபிடித்து, எல்லாவற்றையும் அங்கே நகர்த்துகிறது, detective பிடித்துவிடுகிறது, அது தாவுகிறது. மூன்று இடங்களில் ஒரே நேரத்தில் இருக்க அது ஒருபோதும் கற்றுக்கொள்வதில்லை, ஏனென்றால் ஒரே இடம்தான் எப்போதும் மலிவான அடுத்த நகர்வு. அதுதான் Helvetica scenario, எண்களுடன். என் runs-ல் collapse ஆகாதவை: சாதாரண rate-ல் ஒரு D step-க்கு ஐந்து G steps, மற்றும் 1-dimensional z. 1-D-ல் ஒரு noise மதிப்பிலிருந்து மூன்று குன்றுகளுக்கு ஒரு படிக்கட்டு கற்றுக்கொள்வது எளிது; அந்த இரண்டும் 2-D-லும் images-லும் கடிக்கும்.
Mode collapse-nu solradhu: forger ellaa samples-aiyum oru kunru-la kuvichudum, appuram adhu thaavum — 300-la -2, 600-la +2, 800-la -6. fake std 0.31 dhaan adhoda kaiyoppam.
Detective ஜெயிக்கிறது: forger-ன் gradient மறைகிறது
இப்போது நேர்மாறு. Momentum SGD (கட்டுரை தான் பயன்படுத்தியதாகச் சொல்வது), ஒரு G step-க்கு ஐந்து D steps (Algorithm 1-ன் k; கட்டுரை 1-ஐப் பயன்படுத்தியது), மற்றும் equation 1 எழுதுவது போலவே G-ன் loss:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
200 1.00 / 1.00 -0.017 0.044 0.14 0 / 0 / 0
400 0.99 / 1.00 -0.018 0.073 0.20 0 / 0 / 0
final fakes: mean +0.18 std 0.20 off every bump: 1000 of 1000
Detective குறையற்றது. V −1.39-க்குப் பதிலாக −0.02. Forger நகரவே இல்லை: 400 சுற்றுகள் ஆகியும், அதன் samples இன்னும் 0-ல் 0.2 பரவலுடன் உட்கார்ந்திருக்கின்றன, random init வைத்த இடத்திலேயே. குறையற்ற detective ஒரு பயனற்ற ஆசிரியர். அதே run, ஒரே ஒரு மாற்றம் — non-saturating loss:
100 0.67 / 1.00 -0.729 0.671 1.01 0 / 0 / 604
400 0.67 / 1.00 -0.818 1.334 0.27 0 / 0 / 1000
Forger முதல் 100 சுற்றுகளிலேயே நகர்கிறது — நேராக ஒரே ஒரு குன்றின் மேல், ஏனென்றால் SGD-ன் கீழ் ஒரு வலிமையான detective இன்னும் ஒரு மோசமான விளையாட்டுதான். ஆனால் அது நகர்கிறது. கட்டுரையின் "in practice" paragraph-ன் முழு உள்ளடக்கமும் இதுதான், இரண்டு tables-ல்.
மறந்துபோன detach
Detective-ன் முறையில் போலிகள் G-லிருந்து வருகின்றன. அவற்றை நீங்கள்
.detach() செய்யவில்லை என்றால், D-ன் loss D வழியாகவும்
தொடர்ந்து G-க்குள்ளும் backpropagate ஆகும். ஒரு D step-க்குப் பிறகு
உடனே G-ன் gradient-ன் norm:
detach=True |gradG| after D's step: 0.0000
detach=False |gradG| after D's step: 0.0606
cosine(leaked gradient, G's own gradient): -1.000
Cosine −1: கசிந்த gradient forger-னுடைய சொந்த gradient-க்கு நேர் எதிராகச்
சுட்டுகிறது. அப்படித்தான் இருக்கும் — D-ன் loss போலிகள் இன்னும்
அதிகமாகக் கண்டுபிடிக்கக்கூடியதாக இருக்க விரும்புகிறது. அது உண்மையில்
apply ஆகுமா என்பது உங்கள் zero_grad() எங்கே இருக்கிறது என்பதைப்
பொறுத்தது. G-ஐ அதன் சொந்த backward-க்கு முன்பு zero செய்தால் கசிவு
அழிக்கப்படும்; நீங்கள் ஒரு வீணான backward pass-க்கு மட்டுமே விலை
கொடுத்தீர்கள் — அதனால்தான் இந்த bug பெரும்பாலும் அமைதியானது. இந்த script
சுற்றின் தொடக்கத்தில் இரண்டு networks-ஐயும் zero செய்கிறது, எனவே இது
apply ஆகிறது:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
1000 0.33 / 1.00 -1.081 0.106 1.85 198 / 339 / 0
3000 0.33 / 1.00 -1.128 0.066 2.21 430 / 430 / 0
ஒரு முழுக் குன்றே காணாமல் போய்விட்டது, run முழுவதும் detective போலிகளில் 1.00. கடைசி column-ஐ ஆரோக்கியமான table-உடன் ஒப்பிடுங்கள்.
மாறி மாறிச் செய்ய மறப்பது: இது இனி ஒரு விளையாட்டே இல்லை
கவர்ச்சிகரமான குறுக்குவழி: இரண்டு losses-ஐயும் கூட்டு, இரண்டு networks-ன்
மேலும் ஒரே optimiser, ஒரே backward(). அது compile ஆகிறது,
ஓடுகிறது, ஆனால் அது adversarial அல்ல. D-ன் சொந்த term D(G(z))-ஐக் கீழே
தள்ளுகிறது; G-ன் term, இப்போது D-ன் weights-க்குள்ளும் பாய்ந்து, அதை மேலே
தள்ளுகிறது; D(G(z)) = 0.5-ல் அவை ரத்தாகின்றன. கூட்டு loss வழியாக G-ன்
gradient சரியாக அதே புள்ளியில் பூஜ்யம். யாரும் யாரையும் தள்ளுவதில்லை:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
600 1.00 / 0.78 -0.694 0.000 0.00 0 / 0 / 0
1000 1.00 / 0.77 -0.693 0.001 0.00 0 / 0 / 0
final fakes: mean +0.03 std 0.00 off every bump: 1000 of 1000
ஆயிரம் சுற்றுகளுக்கு fake std 0.00: forger அதன் தொடக்கப் புள்ளியை விட்டு நகரவே இல்லை. V என்பது −0.693 = −log 2, அது log 1 + log ½ — detective ஒவ்வொரு உண்மையான sample-க்கும் 1 என்றும் ஒவ்வொரு போலிக்கும் ½ என்றும் சொல்கிறது, வேறு எதுவும் சொல்ல அதற்குக் காரணமே இல்லை. ஒரு விளையாட்டுக்கு இரண்டு வீரர்கள் வேண்டும், ஒவ்வொருவரும் மற்றவர் விரும்பாத ஒன்றை விரும்ப வேண்டும். ஒரே loss-க்கு ஒரே ஆசைதான்.
मैं एक AI Engineer हूँ। मैंने models ship किए हैं, MLOps किया है — फिर भी यह paper मुझसे पढ़ा नहीं जाता था। गणित की वजह से नहीं — एक ही formula है और एक ही proof, दोनों एक पन्ने में आ जाते हैं। वजह यह थी कि किसी ने वह एक बात नहीं बताई: loss function ख़ुद एक दूसरा model है। यह दिख जाने के बाद, यह paper वही training loop है जो आप हज़ार बार लिख चुके हैं, बस एक अतिरिक्त optimiser के साथ। दिलचस्प हिस्सा यह है कि क्या-क्या ग़लत होता है।
Loss function ख़ुद एक model है
यह loop आप जानते हैं:
for x, y in data:
loss = criterion(model(x), y) # criterion तय है: MSE, cross-entropy
loss.backward(); opt.step()
Criterion एक स्थिर जज है। वह कभी सीखता नहीं। GAN उसे बदलकर एक ऐसा जज रख देता है जो ख़ुद भी सीख रहा है — आपके ख़िलाफ़:
fake = G(z) # forger की कोशिश
loss_D = -(log(D(real)) + log(1 - D(fake.detach()))).mean() # detective की बारी
loss_D.backward(); opt_D.step()
loss_G = -log(D(G(z))).mean() # forger की बारी
loss_G.backward(); opt_G.step() # और दोहराओ
यही पूरे paper की धड़कन है। दो networks, दो losses, बारी-बारी से। Paper की अपनी उपमा है जाली नोट बनाने वाले बनाम पुलिस; मेरी उपमा है एक red team और एक blue team, हर कोई दूसरे के ताज़ा output पर फिर से train होता हुआ। चतुराई की बात यह है कि forger असली data कभी देखता ही नहीं। वह सिर्फ़ detective का फ़ैसला देखता है — detective ही उसका loss function है, और वह loss हर round में और तेज़ होता जाता है। कोई p(x) लिखता नहीं। न likelihood, न कोई loss curve जिसका मतलब हो "अब बेहतर है": सिर्फ़ samples और backprop। इसीलिए यह काम कर गया, और इसीलिए इसके logs पढ़ना इतना मुश्किल है।
| शब्द | मतलब | Programmer की उपमा |
|---|---|---|
| G, generator | Noise अंदर, sample बाहर | एक procedural generator: seed → level |
| D, discriminator | Sample अंदर, P(real) बाहर | एक spam filter, हर round में दोबारा train किया हुआ |
| z | G को दिया गया random noise | RNG seed. अलग seed, अलग sample |
| pg | सभी z पर G(z) का distribution | आपका generator जो कुछ बना सकता है, histogram की तरह |
| .detach() | वह tensor जिसका इतिहास काट दिया गया | एक read-only view. Gradients यहीं रुक जाते हैं |
वह एक formula
Equation 1. इसे एक scoreboard की तरह पढ़िए जिसे detective ऊँचा चाहता है और forger नीचा:
| चरण | Shape | यह क्या करता है |
|---|---|---|
z | (n, noise_dim) | n random seeds. Gaussian noise; कुछ सीखा नहीं जाता |
G(z) | (n, 1) | n नक़ली नमूने। यहाँ एक रेखा पर एक बिंदु; paper में एक image |
D(x) | (n, 1) | "असली" होने की probability, 0 और 1 के बीच |
log D(x) | n पर mean | असली data पर detective का स्कोर। यक़ीन हो तो 0, ग़लत हो तो बहुत ऋणात्मक |
log(1 − D(G(z))) | n पर mean | नक़ली पर detective का स्कोर। हर एक पकड़ ले तो 0 |
maxD | — | Detective की बारी: दोनों terms ऊपर धकेलो |
minG | — | Forger की बारी: दूसरा term नीचे धकेलो। पहले को वह छू नहीं सकता |
Code में यह d_loss और g_loss_saturating हैं, दो-दो
लाइनें, नीचे code वाले हिस्से में। D पूरी चीज़ को maximise करता है, इसलिए
उसका loss ऋणात्मक है; G सिर्फ़ दूसरे term में आता है। फिर equation 1 के बाद
वाले paragraph में paper एक व्यावहारिक तरकीब जोड़ता है: G को log(1 − D(G(z)))
minimise करने के लिए train मत करो, log D(G(z)) maximise करने के लिए करो। वही
fixed point, मगर "much stronger gradients early in learning"। वही है
g_loss, non-saturating loss. यह एक सिफ़ारिश है, theory का हिस्सा
नहीं — proofs equation 1 के बारे में हैं। लेकिन implement हर कोई सिफ़ारिश ही
करता है। अगले दो हिस्से बताते हैं क्यों।
सबसे अच्छा detective एक ratio है, और −log 4 फ़िनिश लाइन है
Section 4 दो बातें साबित करता है, और दोनों ऐसा गणित हैं जिसे आप बिना किसी network के एक grid पर जाँच सकते हैं। Proposition 1: किसी तय forger के लिए, सबसे अच्छा संभव detective D*(x) = pdata(x) / (pdata(x) + pg(x)) है। Theorem 1: उस detective के साथ खेल का मान −log 4 + 2·JSD(pdata ‖ pg) है, इसलिए उसका global minimum −log 4 = −1.386 है, जो ठीक तभी मिलता है जब forger का distribution data के बराबर हो जाए। मेरा data है −6, −2 और +2 पर तीन Gaussian टीले — paper का अपना Figure 1 भी ऐसा ही एक 1-D खिलौना है। तीन उम्मीदवार forgers, closed form में जाँचे हुए:
-log 4 = -1.386 <- forger जीत जाए तो खेल का मान
forger D*(-6) D*(-2) V(D*,G) -log4 + 2*JSD
only the bump at -2 1.000 0.250 -0.750 -0.750
all three, twice as wide 0.667 0.667 -1.203 -1.203
p_data exactly 0.500 0.500 -1.386 -1.386
जहाँ forger कभी जाता ही नहीं (x = −6, पहली पंक्ति) वहाँ detective को पूरा यक़ीन है: 1.000। जहाँ forger data से बिल्कुल मेल खाता है, वहाँ detective हर जगह सिक्का उछालने जैसा है, 0.500, और मान −log 4 पर जाकर बैठ जाता है। आख़िरी दो columns तीन दशमलव तक एक जैसे हैं: यही Theorem 1 है, संख्याओं से जाँचा हुआ।
दो losses, और वह तरकीब है क्यों
D एक logit पर लगा sigmoid है। हर forger loss को उसी logit के सापेक्ष differentiate कीजिए, हर एक सिमटकर एक ही term रह जाता है:
saturating log(1 - D) -> d/dlogit = -D -> 0 as D -> 0
non-saturating -log D -> d/dlogit = -(1 - D) -> 1 as D -> 0
D(G(z)) |grad| saturating |grad| non-sat
0.500 0.500 0.500
0.100 0.100 0.900
0.010 0.010 0.990
0.001 0.001 0.999
Training की शुरुआत में forger बेकार होता है और detective को यह पक्का पता होता है: D(G(z)) ≈ 0। तब जैसा लिखा है वैसे loss का gradient ≈ 0 होता है — ठीक उसी वक़्त जब forger को उसकी सबसे ज़्यादा ज़रूरत है। तरकीब इसे उलट देती है: आप जितना बुरा कर रहे हैं, धक्का उतना बड़ा। असली networks पर नापा गया, जमे हुए forger के ख़िलाफ़ detective को 1000 round की बढ़त देने के बाद:
D(G(z)) after the head start: 0.0071
|gradG| with saturating (eq. 1) : 0.0213
|gradG| with non-saturating (the trick) : 2.2216
वही networks, वही noise, वही detective। संकेत का सौवाँ हिस्सा। "Saturating" का मतलब है sigmoid का saturate होना — वही बीमारी जो attention paper में बिना scale किए softmax की थी। फिर वही numerical सफ़ाई।
खेल, ठीक से खेला गया
एक D step, एक G step, 4000 rounds, Adam. Script छापता है: असली और नक़ली data पर detective की accuracy, paper का V, forger के gradient का norm, 1000 नक़ली नमूनों का फैलाव, और हर टीले के 1.0 के भीतर कितने गिरते हैं:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
500 0.83 / 0.29 -1.363 0.064 3.42 108 / 177 / 308
1000 0.29 / 0.67 -1.380 0.081 3.10 192 / 95 / 342
2000 0.64 / 0.58 -1.369 0.116 3.34 210 / 165 / 358
3000 0.61 / 0.41 -1.377 0.492 3.64 224 / 258 / 346
4000 0.94 / 0.09 -1.384 0.215 3.78 238 / 272 / 330
final fakes: mean -2.01 std 3.78 off every bump: 160 of 1000
तीन चीज़ें पढ़िए। आख़िरी column: नक़ली नमूने तीनों टीलों पर, पूरी तरह बराबर नहीं, फिर भी बराबर होते हुए। V: −1.38 पर टिका हुआ, जो दो दशमलव तक −log 4 है — forger असल में जीत चुका है। और accuracy की जोड़ी: 0.94 / 0.09 तब तक असंतुलित लगती है जब तक आप उन्हें जोड़ न लें। Real और fake का जोड़ ≈ 1.03 यानी संयोग — जो detector हर चीज़ को "real" कहे उसका यह जोड़ ठीक 1.00 आता है। पकड़ने को कुछ बचा ही नहीं। ध्यान दीजिए detective की learning rate forger की एक-चौथाई है (5e-4 बनाम 2e-3); बराबर rates पर, इस data पर, बीच वाला टीला भूखा रह जाता है। यह खेल एक संतुलन है, और इस पन्ने का बाक़ी हिस्सा यही है कि संतुलन बिगड़ने पर क्या होता है।
असफलताओं के नाम हैं
Theory मानती है कि detective हर step पर optimal है और forger की capacity असीमित है। दोनों कभी सच नहीं होतीं, और section 6 इसे एक वाक्य में मान लेता है: D को update किए बिना G को ज़्यादा train नहीं करना चाहिए, "to avoid the Helvetica scenario in which G collapses too many values of z to the same value of x"। जो भी इन्हें train करता है वह इससे और इसके रिश्तेदारों से टकराता है। चार हैं, और हर एक स्वस्थ run से बस एक edit की दूरी पर।
Mode collapse: forger detective से आगे निकल जाता है
Forger को दस गुना learning rate दीजिए, lr_g=2e-2। हर 100 rounds पर:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
100 1.00 / 0.00 -1.373 0.013 0.27 0 / 0 / 9
300 0.60 / 1.00 -1.276 0.038 0.31 0 / 992 / 0
400 0.00 / 1.00 -1.222 0.795 4.68 116 / 1 / 0
600 0.53 / 0.65 -1.372 0.022 5.74 58 / 52 / 571
800 0.33 / 1.00 -1.275 0.009 0.24 648 / 0 / 0
1000 0.54 / 0.46 -1.397 0.042 3.32 276 / 59 / 397
Fake std 0.31: हज़ार samples एक ही ढेर में। Round 300 पर ढेर −2 पर है, 600 पर +2 पर, 800 पर −6 पर। Forger वह एक जगह ढूँढ लेता है जिसे detective फ़िलहाल "real" मानता है, सब कुछ वहाँ ले जाता है, detective पकड़ लेता है, और वह कूद जाता है। तीन जगहों पर एक साथ होना वह कभी नहीं सीखता, क्योंकि एक जगह ही हमेशा सबसे सस्ती अगली चाल होती है। यही Helvetica scenario है, गिनती के साथ। मेरे runs में जो collapse नहीं हुआ: सामान्य rate पर एक D step के लिए पाँच G steps, और 1-dimensional z। 1-D में एक noise मान से तीन टीलों तक की सीढ़ी सीखना आसान है; वे दोनों 2-D में और images पर काटते हैं।
Detective जीत जाता है: forger का gradient ग़ायब हो जाता है
अब उल्टा। Momentum SGD (जो paper कहता है उसने इस्तेमाल किया), एक G step के लिए पाँच D steps (Algorithm 1 का k; paper ने 1 रखा था), और G का loss ठीक वैसा जैसा equation 1 लिखता है:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
200 1.00 / 1.00 -0.017 0.044 0.14 0 / 0 / 0
400 0.99 / 1.00 -0.018 0.073 0.20 0 / 0 / 0
final fakes: mean +0.18 std 0.20 off every bump: 1000 of 1000
Detective एकदम सही है। V −1.39 की जगह −0.02 है। और forger हिला ही नहीं: 400 round बाद भी उसके samples 0 पर, 0.2 के फैलाव के साथ बैठे हैं, जहाँ random init ने उन्हें रखा था। एक परफ़ेक्ट detective बेकार शिक्षक है। वही run, एक बदलाव — non-saturating loss:
100 0.67 / 1.00 -0.729 0.671 1.01 0 / 0 / 604
400 0.67 / 1.00 -0.818 1.334 0.27 0 / 0 / 1000
Forger पहले 100 rounds में ही हिलता है — सीधे एक ही टीले पर, क्योंकि SGD के नीचे एक मज़बूत detective अब भी एक बुरा खेल है। लेकिन वह हिलता तो है। Paper के "in practice" paragraph की पूरी बात यही है, दो tables में।
भूला हुआ detach
Detective की बारी में नक़ली नमूने G से आते हैं। अगर आपने उन्हें
.detach() नहीं किया, तो D का loss D से होकर आगे G में भी
backpropagate होगा। D के एक step के ठीक बाद G के gradient का norm:
detach=True |gradG| after D's step: 0.0000
detach=False |gradG| after D's step: 0.0606
cosine(leaked gradient, G's own gradient): -1.000
Cosine −1: रिसा हुआ gradient बिल्कुल forger के अपने gradient के उल्टा इशारा
करता है। करेगा भी क्यों नहीं — D का loss तो चाहता है कि नक़ली नमूने और
ज़्यादा पकड़ में आएँ। यह असल में लागू होता है या नहीं, यह इस पर
निर्भर है कि आपका zero_grad() कहाँ है। G को उसके अपने backward
से ठीक पहले zero कीजिए और रिसाव मिट जाता है; आपने सिर्फ़ एक बेकार backward
pass की क़ीमत चुकाई — इसीलिए यह bug अक्सर चुपचाप रहता है। यह script round के
शुरू में दोनों networks को zero करती है, इसलिए यहाँ वह लागू होता है:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
1000 0.33 / 1.00 -1.081 0.106 1.85 198 / 339 / 0
3000 0.33 / 1.00 -1.128 0.066 2.21 430 / 430 / 0
एक पूरा टीला ग़ायब, और पूरे run भर detective नक़ली पर 1.00। आख़िरी column की तुलना स्वस्थ table से कीजिए।
बारी-बारी करना भूलना: अब यह खेल ही नहीं रहा
लुभावना शॉर्टकट: दोनों losses जोड़ दो, दोनों networks पर एक optimiser, एक
backward()। यह compile होता है, चलता है, और adversarial नहीं
होता। D का अपना term D(G(z)) को नीचे धकेलता है; G का term, जो अब D के weights
में भी बह रहा है, उसे ऊपर धकेलता है; D(G(z)) = 0.5 पर दोनों कट जाते हैं। और
संयुक्त loss से होकर G का gradient ठीक उसी बिंदु पर शून्य है। कोई किसी को
नहीं धकेलता:
step D acc real/fake V |gradG| fake std near -6 / -2 / +2
600 1.00 / 0.78 -0.694 0.000 0.00 0 / 0 / 0
1000 1.00 / 0.77 -0.693 0.001 0.00 0 / 0 / 0
final fakes: mean +0.03 std 0.00 off every bump: 1000 of 1000
हज़ार rounds तक fake std 0.00: forger अपनी शुरुआती जगह से हिला ही नहीं। V है −0.693 = −log 2, जो log 1 + log ½ है — detective हर असली sample को 1 कहता है और हर नक़ली को ½, और उसके पास कुछ और कहने की कोई वजह नहीं। खेल के लिए दो खिलाड़ी चाहिए जिनमें हर एक वह चाहता हो जो दूसरा नहीं चाहता। एक loss की एक ही चाहत होती है।
The code
NumPy for the mechanism: the optimal detective and the value of the game, straight from
section 4. A tiny PyTorch model, CPU only, for the one part that needs gradients — the
training loop. Every function under 20 lines. log is torch.log
with a clamp, so log(0) can't happen.
Download the full script — 130 lines of mechanism plus seven runnable demos. Then:
python3 gan_from_scratch.py
def d_loss(d_real, d_fake):
"""Detective: maximise log D(x) + log(1 - D(G(z))) -> minimise minus that."""
return -(log(d_real) + log(1 - d_fake)).mean()
def g_loss_saturating(d_fake):
"""Forger, as WRITTEN in equation 1: minimise log(1 - D(G(z))).
When D is confident (D(G(z)) ~ 0) this has almost no gradient. Demo 2."""
return log(1 - d_fake).mean()
def g_loss(d_fake):
"""Forger, as the paper RECOMMENDS (section 3, the paragraph after eq. 1):
maximise log D(G(z)) instead. Same fixed point, far bigger gradient early."""
return -log(d_fake).mean()
def train_step(G, D, opt_G, opt_D, rng, batch, noise_dim,
k_d=1, k_g=1, g_loss_fn=g_loss, detach=True):
# Clear both gradient buffers once, at the top of the round.
opt_D.zero_grad()
opt_G.zero_grad()
# (1) THE DETECTIVE'S TURN, k_d times. Real should score 1, fake 0.
# fake.detach() cuts the graph: D's loss must not flow back into G.
for _ in range(k_d):
real = torch.from_numpy(sample_real(batch, rng))
fake = G(torch.randn(batch, noise_dim))
if detach:
fake = fake.detach()
loss_d = d_loss(D(real), D(fake))
loss_d.backward()
opt_D.step()
# (2) THE FORGER'S TURN, k_g times. Fresh noise. Push D(G(z)) towards 1.
# D's weights get a gradient too, but only opt_G.step() is called,
# so D stays put. That is what "taking turns" means in code.
for _ in range(k_g):
loss_g = g_loss_fn(D(G(torch.randn(batch, noise_dim))))
loss_g.backward()
opt_G.step()
return -loss_d.item(), grad_norm(G)
Now break it
This is the part that actually makes it stick. Reading won't do it. You debug for a living —
reverse-engineering from broken behaviour is your native mode, and with a GAN it's the only
mode: there is no loss curve to trust. Each row is one keyword argument to
train() in the healthy run, or one deleted line.
| Break this | Watch for |
|---|---|
Delete fake = fake.detach() in train_step (or pass detach=False) | |gradG| after D's step is 0.0606 instead of 0.0000, cosine −1.000 against G's own gradient. Trained: the +2 bump empties — 430 / 430 / 0 — and D scores 1.00 on fakes all run. |
Pass g_loss_fn=g_loss_saturating with a detective that can win: optim="sgd", k_d=5 | D 1.00 / 1.00, V −0.017, |gradG| 0.044, fake std 0.14, nothing near any bump for 400 rounds. The forger is frozen at its init. Same with g_loss: it moves in 100 rounds. |
Change lr_g=2e-3 to lr_g=2e-2 | Fake std drops to 0.31 and one column reads 992, the others 0. Then it hops: −2 at round 300, +2 at 600, −6 at 800. Mode collapse. |
Change lr_d=5e-4 to lr_d=5e-3 (detective ×10) | In my runs the middle bump starves — a count under 100 while the outer two hold around 400 each — and V drifts up from −1.38 towards −1.2. D pulls ahead; G chases the two bumps it can reach. |
Replace the two turns with not_a_game_step (combined=True): one loss, one optimiser | Fake std 0.00 and 0 / 0 / 0 for 1000 rounds. V flat at −0.693 = −log 2: D says 1 to real, ½ to fake, and the two gradients on G cancel. Nothing is adversarial. |
On the maths anxiety
Honestly: the maths in this paper is one expectation (an average over samples — a
.mean()), one log (so a product of probabilities becomes a sum), and one ratio.
Proposition 1 is "maximise a·log y + b·log(1 − y) over y", a one-line calculus exercise
with the answer a/(a+b). Theorem 1 is that ratio substituted back in and the result
recognised as a known distance between distributions, Jensen–Shannon. Same league as the
attention paper.
You don't need to derive the gradients — loss.backward() did — and you don't
need §4.2's convergence proof, which assumes a convex space of generators that a neural net
violates on line one; the paper says as much right after it. What you do need, and what
the maths won't give you, is the engineering: which loss, which optimiser, whose learning
rate is bigger, where the detach goes, and how to read a log where the loss going to zero
means you lost. That is the interview question, and it's this page.
Where to go next
This page gave you one angle: the GAN as two optimisers taking turns, and the four ways the turns go wrong. Deliberately narrow — no images, no convolutions, none of the fixes.
-
Generative Adversarial Nets
§3 and Algorithm 1 are
train_step. Proposition 1 and Theorem 1 are demo 1. Figure 1 is this page's toy, drawn. Then §6, which admits in advance everything demo 4 showed you. -
From GAN to WGAN
The full derivation of Theorem 1 with KL and Jensen–Shannon written out, then the thing I only named: why JS is a bad distance when the two distributions don't overlap, and how the Wasserstein distance (WGAN) answers it.
-
NIPS 2016 Tutorial: Generative Adversarial Networks
The taxonomy of generative models I skipped, the failure modes with the theory of why, and a tips-and-tricks section. Skim the tips first; read the rest when you want the map of the field.
-
ganhacks: how to train a GAN
A numbered list of tricks from a DCGAN author: normalise inputs, separate real and fake batches, label smoothing, which loss patterns mean trouble. The runbook for a log that looks like demo 4. Spectral normalisation and the rest of the stabilisation literature came after; this is the pre-flight list.