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 — G berhenti 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 — lossD merosot menuju 0 dan akurasi D menuju 1: D memisahkan data asli dari tiruan dengan sempurna. Permainan berhenti dan G berhenti belajar.
  • Gradien hilang saat D terlalu yakin — sinyal pembelajaran G harus mengalir menembus D, dan turunan sigmoid yang jenuh mendekati nol. Semakin yakin D, semakin tipis jalur gradien menuju G — tepat ketika G paling 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:

python
"""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:

text
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-05

Mari kita telaah kode di atas baris per baris:

  1. Baris 20-22: tiga sumber keacakan dipisah — seed global pelatihan, default_rng untuk data, dan gen_stat khusus menarik sampel statistik, sehingga pencetakan di tengah loop tidak mengubah lintasan pelatihan.
  2. Baris 25-31: data campuran tiga Gaussian; mean dan std totalnya dicetak sebagai pembanding — G yang sehat harus mendekati [0.02, 0.49] dan [1.86, 1.65].
  3. Baris 35-38: fraksi_mode() menghitung proporsi sampel dalam radius 1,0 dari tiap mode — alat ukur utama mode collapse.
  4. Baris 42-54: ukur_gradien() membuat tensor skalar u dengan requires_grad, menghitung kedua rumus loss, lalu memanggil backward() dan membaca u.grad — gradien nyata dari autograd, bukan rumus yang diketik manual.
  5. Baris 72-91: setup duel — G selalu identik (1218 parameter); cabang d_dominan mengganti D (17.025 vs 129 parameter), jumlah langkah D, dan lrG; target_G memilih loss G: label 0 berarti saturating, label 1 berarti non-saturating.
  6. Baris 98-117: loop pelatihan — learning rate dibagi empat pada iterasi 1200; langkah D diulang langkahD kali (sumber dominasi D); langkah G memakai target_G — satu-satunya perbedaan duel 2 vs duel 3 ada di baris ini.
  7. Baris 127-147: statistik akhir — norm gradien G dihitung dari loss yang benar-benar dipakai konfigurasi tersebut; jumlah mode dihitung dengan ambang fraksi 0,10.
  8. 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 teramatiDiagnosisObat lini pertama
lossD menuju 0, akurasi D menuju 1, sampel G beku di satu titikD menang mutlak; gradien ke G hilangganti 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 collapsekurangi dominasi D (kapasitas dan jumlah langkah); naikkan lrG; seimbangkan kapasitas
lossG naik terus tanpa perbaikan sampelD jenuh memotong gradien Glabel smoothing; pastikan loss G non-saturating
lossD dan lossG berosilasi lebar tanpa arahRitme kedua pihak tidak seimbangturunkan 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 D dengan menormalkan nilai singular terbesar tiap matriks bobot pada setiap langkah. PyTorch menyediakannya melalui torch.nn.utils.parametrizations.spectral_norm yang dibungkuskan pada lapisan D.

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 .