GAN mini dan bedah anatomi pada dua chapter sebelumnya berakhir seimbang karena konfigurasinya pas: kapasitas D sebanding dengan G, learning rate sama, loss non-saturating. Geser keseimbangan itu saja — D dibuat lebih besar dan lebih sering dilatih, G lebih lambat, atau loss G dikembalikan ke rumus asli — dan pelatihan gagal dengan pola yang khas serta dapat dikenali. GAN memang terkenal sebagai arsitektur generatif yang paling rapuh untuk dilatih, karena objectifnya berupa permainan dua pihak, bukan satu fungsi loss yang diturunkan sendirian. Chapter ini bertindak sebagai kotak diagnosis: kita picu tiga kegagalan klasik secara nyata pada data tiga mode, ukur gejalanya dengan angka, lalu susun peta gejala → diagnosis → obat.
Tiga Gejala Klasik #
Kegagalan GAN hampir selalu muncul dalam salah satu dari tiga bentuk berikut:
- Mode collapse —
Gberhenti menjelajah dan mengarahkan seluruh (atau hampir seluruh) sampelnya ke satu mode data. Pada data tiga mode, 90% sampel menumpuk di satu titik mode dan dua mode lain ditinggalkan; pada data gambar, gejalanya “semua hasil terlihat serupa”. - Non-convergence / D menang mutlak —
lossDmerosot menuju 0 dan akurasiDmenuju 1:Dmemisahkan data asli dari tiruan dengan sempurna. Permainan berhenti danGberhenti belajar. - Gradien hilang saat D terlalu yakin — sinyal pembelajaran
Gharus mengalir menembusD, dan turunan sigmoid yang jenuh mendekati nol. Semakin yakinD, semakin tipis jalur gradien menujuG— tepat ketikaGpaling membutuhkannya.
Loss Saturating vs Non-Saturating #
Rumus asli GAN 2014 meminimalkan −log(1 − D(G(z))) pada G — loss saturating. Praktik modern (dan kedua chapter sebelumnya) memakai bentuk non-saturating: −log D(G(z)). Tujuan keduanya sama (menipu D), tetapi turunannya terhadap logit u sebelum sigmoid berbeda drastis: loss saturating menghasilkan gradien σ(u) = D, loss non-saturating menghasilkan 1 − D. Saat D sangat yakin sampel itu palsu (D → 0), gradien saturating ikut menuju nol sementara non-saturating menempel di sekitar 1. Tabel gradien pada keluaran skrip di bawah mengukur keduanya dengan autograd pada lima tingkat keyakinan — pada D(G(z)) = 0.001, gradien saturating tinggal 0,00101 sementara non-saturating 0,99899: rasio hampir 1:1000.
Demo: Tiga Konfigurasi pada Data Tiga Mode #
Berkas stabilitas_gan.py memakai data yang sama untuk semua percobaan: 4000 titik dari campuran tiga Gaussian di [-2.2, 1.6], [2.2, 1.6], dan [0.0, -1.8] (std 0,45 per mode; mean total [0.02, 0.49], std total [1.86, 1.65]). Arsitektur G dan jumlah iterasi juga identik; hanya keseimbangan yang berbeda. Konfigurasi sehat memakai D 129 parameter dengan ritme 1:1. Konfigurasi collapse membuat D dominan: 17.025 parameter, dua langkah D untuk setiap satu langkah G, lrG diperkecil menjadi 5e-4. Konfigurasi D menang memakai pengaturan collapse persis — satu-satunya perbedaan ada pada satu baris: G dilatih dengan loss saturating seperti rumus asli 2014:
"""Diagnosis training GAN: mode collapse, D menang mutlak, gradien hilang.
Tiga bagian pada data campuran tiga Gaussian 2D (seimbang):
1. Gradien loss saturating vs non-saturating terhadap logit D --
angka autograd nyata untuk tiap tingkat keyakinan D.
2. Tiga duel: data, arsitektur G, dan jumlah iterasi identik.
- sehat : D seimbang + loss non-saturating -> 3/3 mode tercakup.
- collapse : D dominan (besar + 2 langkah D per 1 langkah G) dan G
lambat -> seluruh sampel G menumpuk pada satu mode saja.
- menang : konfigurasi collapse persis, tetapi G memakai loss
saturating (rumus asli 2014) -> lossD menuju 0, gradien ke G
praktis hilang, sampel G tidak menyerupai data.
3. Ringkasan statistik ketiganya untuk diagnosis. Deterministik, CPU.
"""
import numpy as np
import torch
import torch.nn as nn
torch.manual_seed(0)
rng = np.random.default_rng(13)
gen_stat = torch.Generator().manual_seed(99) # noise khusus statistik
# ---------- 1. Data: campuran tiga Gaussian 2D ----------
MODE = np.array([[-2.2, 1.6], [2.2, 1.6], [0.0, -1.8]])
lbl = rng.integers(0, 3, size=4000)
X = MODE[lbl] + rng.normal(0, 0.45, size=(4000, 2))
Xt = torch.tensor(X, dtype=torch.float32)
print(f"data: 4000 titik, 3 mode {MODE.tolist()}, std 0.45 per mode")
print(f" mean total {np.round(X.mean(0).astype(np.float64), 2).tolist()},"
f" std total {np.round(X.std(0).astype(np.float64), 2).tolist()}")
print()
def fraksi_mode(x_gen, radius=1.0):
"""Fraksi sampel di sekitar tiap mode (radius 1.0)."""
d = np.sqrt(((x_gen[:, None, :] - MODE[None, :, :]) ** 2).sum(-1))
return [float((d[:, i] < radius).mean()) for i in range(3)]
# ---------- 2. Gradien saturating vs non-saturating ----------
def ukur_gradien(u_nilai):
"""Kembalikan (s, loss_sat, loss_non, |grad|_sat, |grad|_non)."""
hasil = []
for rumus in ("sat", "non"):
u = torch.tensor(float(u_nilai), requires_grad=True)
s = torch.sigmoid(u)
if rumus == "sat": # rumus asli 2014: -log(1 - D)
loss = -torch.log(1 - s)
else: # non-saturating: -log D
loss = -torch.log(s)
loss.backward()
hasil.append((s.item(), loss.item(), abs(u.grad.item())))
return hasil[0], hasil[1]
print("=== Gradien loss G terhadap logit D (autograd) ===")
print("logit u D(G(z)) loss -log(1-s) loss -log(s)"
" |grad| sat |grad| non")
for u_nilai in (2.2, 0.0, -2.2, -4.6, -6.9):
(s1, l1, g1), (s2, l2, g2) = ukur_gradien(u_nilai)
print(f"{u_nilai:6.1f} {s1:7.4f} {l1:13.5f} {l2:11.5f}"
f" {g1:10.5f} {g2:10.5f}")
print("(u sangat negatif = D sangat yakin sampel itu palsu)")
print()
# ---------- 3. Tiga duel ----------
bce = nn.BCELoss()
ITER, CEK = 2000, (0, 500, 1000, 1500, 1999)
def latih_duel(judul, seed, d_dominan, saturasi):
"""Latih GAN; d_dominan mengubah D dan lr; saturasi mengubah loss G."""
torch.manual_seed(seed)
G = nn.Sequential(
nn.Linear(2, 32), nn.Tanh(), nn.Linear(32, 32), nn.Tanh(), nn.Linear(32, 2)
)
if d_dominan: # D besar (2 hidden layer) + 2 langkah D per langkah G
D = nn.Sequential(
nn.Linear(2, 128), nn.Tanh(),
nn.Linear(128, 128), nn.Tanh(), nn.Linear(128, 1), nn.Sigmoid(),
)
langkahD, lrG, lrD = 2, 5e-4, 2e-3
else: # D seimbang, 1 langkah D per langkah G
D = nn.Sequential(
nn.Linear(2, 32), nn.Tanh(), nn.Linear(32, 1), nn.Sigmoid()
)
langkahD, lrG, lrD = 1, 2e-3, 2e-3
optG = torch.optim.Adam(G.parameters(), lr=lrG)
optD = torch.optim.Adam(D.parameters(), lr=lrD)
target_G = torch.zeros(128, 1) if saturasi else torch.ones(128, 1)
print(f"=== {judul} ===")
print(f"D {sum(p.numel() for p in D.parameters())} parameter,"
f" langkahD={langkahD}, lrG={lrG}, lrD={lrD},"
f" lossG={'saturating' if saturasi else 'non-saturating'}")
print(f"{'iter':>5s} {'lossD':>6s} {'lossG':>7s} {'D(palsu)':>9s}"
f" mean sampel G / std / fraksi tiap mode")
for it in range(ITER):
if it == int(ITER * 0.6): # redam osilasi menjelang akhir
for grup in optG.param_groups:
grup["lr"] = lrG / 4
for grup in optD.param_groups:
grup["lr"] = lrD / 4
for _ in range(langkahD):
idx = torch.randint(0, len(Xt), (128,))
z = torch.randn(128, 2)
lossD = bce(D(Xt[idx]), torch.ones(128, 1)) + bce(
D(G(z)), torch.zeros(128, 1)
)
optD.zero_grad()
lossD.backward()
optD.step()
z = torch.randn(128, 2)
lossG = bce(D(G(z)), target_G)
optG.zero_grad()
lossG.backward()
optG.step()
if it in CEK:
with torch.no_grad():
xs = G(torch.randn(2000, 2, generator=gen_stat)).numpy()
f = fraksi_mode(xs)
print(f"{it:5d} {lossD.item():6.3f} {lossG.item():7.3f}"
f" {D(G(z)).mean().item():9.4f}"
f" mean {np.round(xs.mean(0).astype(np.float64), 2).tolist()}"
f" std {np.round(xs.std(0).astype(np.float64), 2).tolist()}"
f" mode {f[0]:.2f}/{f[1]:.2f}/{f[2]:.2f}")
# statistik akhir + gradien G pada loss yang dipakai konfigurasi ini
with torch.no_grad():
xs = G(torch.randn(4000, 2, generator=gen_stat)).numpy()
d_asli = D(Xt).numpy()
d_gen = D(torch.tensor(xs, dtype=torch.float32)).numpy()
z = torch.randn(128, 2)
lossG = bce(D(G(z)), target_G)
optG.zero_grad()
lossG.backward()
gradG = sum((p.grad**2).sum().item() for p in G.parameters()) ** 0.5
f = fraksi_mode(xs)
n_mode = sum(1 for x in f if x >= 0.10)
akurasi = ((d_asli > 0.5).mean() + (d_gen <= 0.5).mean()) / 2
print(f"AKHIR: mode {n_mode}/3 ({f[0]:.2f}/{f[1]:.2f}/{f[2]:.2f})"
f" | akurasi D {akurasi:.3f} | D(x) asli {d_asli.mean():.3f}"
f" | D(x) palsu {d_gen.mean():.5f}")
print(f" |gradG| {gradG:.2e} | lossD akhir {lossD.item():.4f}"
f" | mean {np.round(xs.mean(0).astype(np.float64), 2).tolist()}"
f" | std {np.round(xs.std(0).astype(np.float64), 2).tolist()}")
print()
return n_mode, akurasi, lossD.item(), d_gen.mean(), gradG
hasil = []
hasil.append(("sehat", *latih_duel("Duel 1: sehat (D seimbang)", 7, False, False)))
hasil.append(
("collapse", *latih_duel("Duel 2: mode collapse (D dominan, G lambat)", 1, True, False))
)
hasil.append(
("D menang", *latih_duel("Duel 3: D menang mutlak (loss saturating)", 1, True, True))
)
# ---------- 4. Ringkasan diagnosis ----------
print("=== Ringkasan diagnosis (sampel 4000, radius mode 1.0) ===")
print(f"{'konfigurasi':12s} {'mode':>5s} {'akurasiD':>9s} {'lossD':>8s}"
f" {'D(x)palsu':>10s} {'|gradG|':>9s}")
for nama, n_mode, akurasi, lossD, dgen, gradG in hasil:
print(f"{nama:12s} {n_mode:>3d}/3 {akurasi:9.3f} {lossD:8.4f}"
f" {dgen:10.5f} {gradG:9.2e}")Output yang Dihasilkan:
data: 4000 titik, 3 mode [[-2.2, 1.6], [2.2, 1.6], [0.0, -1.8]], std 0.45 per mode
mean total [0.02, 0.49], std total [1.86, 1.65]
=== Gradien loss G terhadap logit D (autograd) ===
logit u D(G(z)) loss -log(1-s) loss -log(s) |grad| sat |grad| non
2.2 0.9002 2.30508 0.10508 0.90025 0.09975
0.0 0.5000 0.69315 0.69315 0.50000 0.50000
-2.2 0.0998 0.10508 2.30508 0.09975 0.90025
-4.6 0.0100 0.01000 4.61000 0.00995 0.99005
-6.9 0.0010 0.00101 6.90101 0.00101 0.99899
(u sangat negatif = D sangat yakin sampel itu palsu)
=== Duel 1: sehat (D seimbang) ===
D 129 parameter, langkahD=1, lrG=0.002, lrD=0.002, lossG=non-saturating
iter lossD lossG D(palsu) mean sampel G / std / fraksi tiap mode
0 1.398 0.624 0.5384 mean [0.08, 0.14] std [0.05, 0.17] mode 0.00/0.00/0.00
500 1.238 0.748 0.4812 mean [0.07, 0.42] std [0.52, 1.26] mode 0.00/0.00/0.31
1000 1.461 0.766 0.4857 mean [-0.39, -0.15] std [1.46, 1.89] mode 0.31/0.11/0.32
1500 1.402 0.695 0.5005 mean [0.15, 0.54] std [1.81, 1.77] mode 0.30/0.33/0.23
1999 1.408 0.696 0.4995 mean [-0.05, 0.52] std [1.78, 1.52] mode 0.32/0.32/0.27
AKHIR: mode 3/3 (0.33/0.32/0.25) | akurasi D 0.425 | D(x) asli 0.494 | D(x) palsu 0.50126
|gradG| 1.30e-01 | lossD akhir 1.4075 | mean [-0.08, 0.56] | std [1.79, 1.51]
=== Duel 2: mode collapse (D dominan, G lambat) ===
D 17025 parameter, langkahD=2, lrG=0.0005, lrD=0.002, lossG=non-saturating
iter lossD lossG D(palsu) mean sampel G / std / fraksi tiap mode
0 1.326 1.026 0.3604 mean [0.27, -0.32] std [0.12, 0.11] mode 0.00/0.00/0.00
500 0.696 1.324 0.2820 mean [0.03, -1.86] std [0.44, 0.49] mode 0.00/0.00/0.94
1000 0.709 1.443 0.2430 mean [-0.32, -1.78] std [0.45, 0.47] mode 0.00/0.00/0.86
1500 0.701 1.357 0.2574 mean [0.05, -1.99] std [0.49, 0.42] mode 0.00/0.00/0.88
1999 0.825 1.372 0.2540 mean [0.09, -1.89] std [0.43, 0.48] mode 0.00/0.00/0.90
AKHIR: mode 1/3 (0.00/0.00/0.91) | akurasi D 0.838 | D(x) asli 0.758 | D(x) palsu 0.25495
|gradG| 1.09e-01 | lossD akhir 0.8248 | mean [0.09, -1.88] | std [0.43, 0.47]
=== Duel 3: D menang mutlak (loss saturating) ===
D 17025 parameter, langkahD=2, lrG=0.0005, lrD=0.002, lossG=saturating
iter lossD lossG D(palsu) mean sampel G / std / fraksi tiap mode
0 1.326 0.444 0.3568 mean [0.29, -0.34] std [0.12, 0.11] mode 0.00/0.00/0.00
500 0.000 0.000 0.0001 mean [0.6, 0.18] std [0.06, 0.04] mode 0.00/0.00/0.00
1000 0.000 0.000 0.0000 mean [0.33, 0.57] std [0.02, 0.06] mode 0.00/0.00/0.00
1500 0.000 0.000 0.0000 mean [0.3, 0.58] std [0.01, 0.06] mode 0.00/0.00/0.00
1999 0.000 0.000 0.0000 mean [0.28, 0.59] std [0.01, 0.06] mode 0.00/0.00/0.00
AKHIR: mode 0/3 (0.00/0.00/0.00) | akurasi D 1.000 | D(x) asli 1.000 | D(x) palsu 0.00002
|gradG| 7.43e-05 | lossD akhir 0.0000 | mean [0.28, 0.59] | std [0.01, 0.06]
=== Ringkasan diagnosis (sampel 4000, radius mode 1.0) ===
konfigurasi mode akurasiD lossD D(x)palsu |gradG|
sehat 3/3 0.425 1.4075 0.50126 1.30e-01
collapse 1/3 0.838 0.8248 0.25495 1.09e-01
D menang 0/3 1.000 0.0000 0.00002 7.43e-05Mari kita telaah kode di atas baris per baris:
- Baris 20-22: tiga sumber keacakan dipisah — seed global pelatihan,
default_rnguntuk data, dangen_statkhusus menarik sampel statistik, sehingga pencetakan di tengah loop tidak mengubah lintasan pelatihan. - Baris 25-31: data campuran tiga Gaussian; mean dan std totalnya dicetak sebagai pembanding —
Gyang sehat harus mendekati[0.02, 0.49]dan[1.86, 1.65]. - Baris 35-38:
fraksi_mode()menghitung proporsi sampel dalam radius 1,0 dari tiap mode — alat ukur utama mode collapse. - Baris 42-54:
ukur_gradien()membuat tensor skalarudenganrequires_grad, menghitung kedua rumus loss, lalu memanggilbackward()dan membacau.grad— gradien nyata dari autograd, bukan rumus yang diketik manual. - Baris 72-91: setup duel —
Gselalu identik (1218 parameter); cabangd_dominanmenggantiD(17.025 vs 129 parameter), jumlah langkahD, danlrG;target_Gmemilih lossG: label 0 berarti saturating, label 1 berarti non-saturating. - Baris 98-117: loop pelatihan — learning rate dibagi empat pada iterasi 1200; langkah
DdiulanglangkahDkali (sumber dominasiD); langkahGmemakaitarget_G— satu-satunya perbedaan duel 2 vs duel 3 ada di baris ini. - Baris 127-147: statistik akhir — norm gradien
Gdihitung dari loss yang benar-benar dipakai konfigurasi tersebut; jumlah mode dihitung dengan ambang fraksi 0,10. - Baris 150-157: ketiga duel dipanggil dan hasilnya dikumpulkan untuk tabel ringkasan diagnosis.
Membaca Ketiga Hasil #
Konfigurasi sehat menutup pelatihan dengan seluruh tanda keseimbangan dari chapter Generative Adversarial Network
: tiga mode tercakup (0,33/0,32/0,25), akurasi D 0,425, lossD 1,4075 di sekitar 2 ln 2 ≈ 1,386, D memberi peluang asli 0,494 untuk data versus 0,501 untuk tiruan, dan norm gradien G 1.30e-01 — sinyal pembelajaran tetap mengalir.
Konfigurasi collapse gagal dengan cara lain. Pada iterasi ke-500 mean sampel G sudah menempel di [0.03, −1.86] — persis lokasi mode ketiga — dan bertahan: fraksi 0,00/0,00/0,91, std sampel [0.43, 0.47] yang hanya menyamai std lokal satu mode (0,45), jauh dari std total [1.86, 1.65]. Norm gradien G masih sehat (1.09e-01) dan G terus belajar — masalahnya bukan gradien melainkan insentif: loss per-sampel tidak menghargai keragaman, jadi setiap z didorong ke mode yang saat itu paling mudah diterima D, dan G yang lambat (lrG 5e-4) tidak sempat menjelajah keluar.
Konfigurasi D menang paling tajam. lossD sudah 0,000 pada iterasi ke-500 dan bertahan; akurasi D sempurna 1,000; D(x) bernilai 1,000 untuk data asli dan 0,00002 untuk tiruan. Sampel G merosot menjadi satu titik degenerate di [0.28, 0.59] dengan std [0.01, 0.06] — bukan mode mana pun. Norm gradien G tinggal 7.43e-05, sekitar 1.750 kali lebih kecil daripada konfigurasi sehat: gradien praktis hilang, G berhenti belajar.
Obat Praktis: Gejala, Diagnosis, dan Penanganan #
| Gejala teramati | Diagnosis | Obat lini pertama |
|---|---|---|
lossD menuju 0, akurasi D menuju 1, sampel G beku di satu titik | D menang mutlak; gradien ke G hilang | ganti ke loss non-saturating; label smoothing; perkecil lrD; perlambat D (update tiap N langkah G) |
Sampel G menumpuk di satu mode; “semua keluaran serupa” | Mode collapse | kurangi dominasi D (kapasitas dan jumlah langkah); naikkan lrG; seimbangkan kapasitas |
lossG naik terus tanpa perbaikan sampel | D jenuh memotong gradien G | label smoothing; pastikan loss G non-saturating |
lossD dan lossG berosilasi lebar tanpa arah | Ritme kedua pihak tidak seimbang | turunkan kedua learning rate; ritme update 1:1; decay menjelang akhir |
Empat obat lini pertama pada tabel sudah kita buktikan angkanya: loss non-saturating menyelamatkan duel 2 dari nasib duel 3, label smoothing menahan keyakinan D pada 0,8964 alih-alih 0,9857 di chapter Generator dan Discriminator
, dan keseimbangan kapasitas 1218 vs 129 parameter adalah pembeda duel 1 dengan duel 2. Dua obat lanjutan menyasar akar masalah yang lebih dalam:
- WGAN-GP (arXiv:1704.00028
) mengganti permainan klasifikasi dengan estimasi jarak Wasserstein antara distribusi data dan tiruan, lalu menambahkan gradient penalty agar kritikus (pengganti
D) tidak menjadi terlalu tajam. Loss-nya menjadi searah — turun berarti distribusi sungguh mendekat — sehingga tidak ada mode kegagalan “menang mutlak” seperti duel 3. - Spectral norm (arXiv:1802.05957
) membatasi konstanta Lipschitz
Ddengan menormalkan nilai singular terbesar tiap matriks bobot pada setiap langkah. PyTorch menyediakannya melaluitorch.nn.utils.parametrizations.spectral_normyang dibungkuskan pada lapisanD.
Diagram ini merangkum alur diagnosis tiga duel: amati dulu pasangan lossD-akurasi D, baru periksa sebaran sampel G. Mode collapse juga menjadi kegagalan yang paling diperhitungkan dalam evaluasi model generatif — pembahasannya berlanjut di chapter Evaluasi Generative Model
.
Ketiga kegagalan bermuara pada satu sifat mendasar: GAN melatih dua jaringan yang saling mengejar tanpa jaminan konvergensi. Keluarga model berikutnya memilih stabilitas sejak desain — diffusion model membuang permainan dua pihak dan menggantinya dengan satu tugas regresi bertahap, sebagaimana kita lihat pada chapter Diffusion Model .