GAN mini pada chapter sebelumnya berakhir seimbang, tetapi kedua jaringannya masih kita perlakukan sebagai kotak hitam: nn.Linear yang bertumpuk tanpa wajah. Padahal generator dan discriminator punya anatomi yang sama sekali berbeda — satu membangun data dari noise, satu menilai keaslian data — dan keputusan desain pada keduanya (dimensi noise, jumlah lapisan, ukuran hidden layer) menentukan hasil kompetisi. Chapter ini membedah kedua jaringan lapisan demi lapisan: berapa parameter di tiap lapisan, mengapa kapasitas yang kurang membuat generator terkunci, dan bagaimana label smoothing mencegah discriminator menjadi terlalu yakin. Seluruh angka berasal dari anatomi_gan.py yang dijalankan pada CPU (PyTorch 2.14).

Generator: Membangun Data dari Noise #

Generator G menerima vektor noise z ~ N(0, I) berdimensi kecil dan harus menghasilkan G(z) yang menyerupai data. Untuk data tabular 2D pada demo kita, satu lapisan Linear sudah cukup: keluaran Wz + b dari noise Gaussian selalu berdistribusi Gaussian, sehingga G linear secara tepat mampu mereplikasi Gaussian tunggal mana pun — fakta yang kita pakai di chapter Generative Adversarial Network . Untuk distribusi multi-mode, diperlukan non-linearitas: G berupa MLP dengan aktivasi Tanh, sehingga pemetaan z ke data bisa melengkung dan bercabang.

Pada data gambar, pola besarannya sama tetapi arsitekturnya khas upsampling: z berdimensi 100-512 diproyeksikan menjadi peta fitur kecil (misalnya 7×7×256), lalu lapisan ConvTranspose2d dengan stride 2 menggandakan sisi peta fitur (7 → 14 → 28 → …) sampai resolusi keluaran tercapai. Tiap kernel convolution terbalik belajar “cara mengisi piksel baru” dari nilai tetangganya; ukuran latent yang jauh lebih kecil dari gambar inilah yang membuat ruang noise menjadi ringkas namun ekspresif.

Discriminator: Classifier Biner Penilai Keaslian #

Discriminator D menerima satu titik data — asli atau tiruan — dan mengeluarkan satu angka hasil Sigmoid: peluang bahwa titik itu asli. Struktur ini persis classifier biner yang kita evaluasi di chapter Metrik Klasifikasi ; bedanya hanya labelnya menjadi “asli vs palsu”. Karena itu seluruh alat baca di sana berlaku untuk D versi detektor keaslian: presisi menjawab “dari sampel yang dinilai asli, berapa yang memang asli”, recall menjawab “berapa proporsi data asli yang lolos penilaian”. Titik keseimbangan GAN tercapai ketika D tidak bisa membedakan — akurasinya jatuh ke sekitar 0,5.

Diagram di atas memakai arsitektur demo kita (lihat G besar dan D pada keluaran skrip di bawah). Anatominya berbeda menurut domain data:

DomainMasukan GLapisan khas GKeluaran GBentuk khas D
Tabular 2D (demo)z 2DLinear / MLP keciltitik 2DMLP + Sigmoid
Tabular umumz 16-128MLPvektor fiturMLP
Gambarz 100-512ConvTranspose bertingkat + aktivasitensor H×W×Cconvolution + pooling, Sigmoid
Teksz atau noise per langkahembedding + decoder sekuensialurutan tokenencoder sekuensial + Sigmoid

Demo: Bedah Lapisan, Duel Kapasitas, dan Label Smoothing #

Berkas anatomi_gan.py menjalankan tiga pemeriksaan pada data campuran dua Gaussian: bedah struktur kedua jaringan beserta jumlah parameternya, dua duel GAN dengan prosedur pelatihan identik tetapi ukuran G berbeda, dan efek label smoothing pada D yang dilatih pada tugas tetap:

python
"""Anatomi generator dan discriminator: bedah lapisan, kapasitas, smoothing.

Tiga bagian pada data campuran dua Gaussian 2D:
1. Anatomi: parameter per lapisan untuk G kecil (linear), G besar (MLP),
   dan D (MLP classifier dengan sigmoid).
2. Efek kapasitas: dua duel GAN dengan prosedur pelatihan identik --
   hanya ukuran G yang berbeda. G linear tidak bisa memisahkan dua mode;
   G MLP menangkap keduanya (fraksi sampel per mode).
3. Label smoothing: dua D dilatih pada tugas tetap (data asli vs tiruan
   generator beku) -- label keras membuat sigmoid jenuh, label smoothing
   memberi langit-langit keyakinan. Deterministik (seed tetap), 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)

# ---------- 1. Data: campuran dua Gaussian ----------
MODE = np.array([[-2.5, -1.5], [2.5, 1.5]])
lbl = rng.integers(0, 2, size=4000)
X = MODE[lbl] + rng.normal(0, 0.5, size=(4000, 2))
Xt = torch.tensor(X, dtype=torch.float32)
print(f"data: 4000 titik, dua mode {MODE.tolist()}, std 0.50")
print()


def fraksi_mode(x_gen, radius=1.2):
    """Fraksi sampel di sekitar tiap mode dan di antara keduanya."""
    d = np.sqrt(((x_gen[:, None, :] - MODE[None, :, :]) ** 2).sum(-1))
    a = float((d[:, 0] < radius).mean())
    b = float((d[:, 1] < radius).mean())
    return a, b, 1.0 - a - b


def buat_G_kecil():
    return nn.Linear(2, 2)


def buat_G_besar():
    return nn.Sequential(
        nn.Linear(2, 32), nn.Tanh(), nn.Linear(32, 32), nn.Tanh(), nn.Linear(32, 2)
    )


def buat_D():
    return nn.Sequential(nn.Linear(2, 32), nn.Tanh(), nn.Linear(32, 1), nn.Sigmoid())


# ---------- 2. Anatomi lapisan demi lapisan ----------
def bedah(model, nama):
    total = sum(p.numel() for p in model.parameters())
    baris = []
    for mod in model.modules():
        if isinstance(mod, nn.Linear):
            baris.append(f"Linear({mod.in_features} -> {mod.out_features})")
        elif isinstance(mod, (nn.Tanh, nn.Sigmoid, nn.ReLU)):
            baris.append(mod.__class__.__name__)
    print(f"{nama:9s}: {' + '.join(baris)} = {total} parameter")


print("=== Anatomi (lapisan demi lapisan) ===")
bedah(buat_G_kecil(), "G kecil")
bedah(buat_G_besar(), "G besar")
bedah(buat_D(), "D")
print()

# ---------- 3. Duel kapasitas: prosedur sama, ukuran G berbeda ----------
bce = nn.BCELoss()
ITER = 2000
CEK = (0, 400, 800, 1200, 1600, 1999)


def latih_duel(buat_G, judul):
    """Pelatihan alternating standar; seed di-reset agar duel setara."""
    torch.manual_seed(7)
    G = buat_G()
    D = buat_D()
    optG = torch.optim.Adam(G.parameters(), lr=2e-3)
    optD = torch.optim.Adam(D.parameters(), lr=2e-3)
    print(f"=== {judul} ===")
    print(f"{'iter':>5s} {'lD_asli':>8s} {'lD_palsu':>9s} {'lG':>6s}"
          f" {'modeA':>6s} {'modeB':>6s} {'antara':>7s}")
    for it in range(ITER):
        if it == int(ITER * 0.6):  # redam osilasi menjelang akhir
            for grup in optG.param_groups:
                grup["lr"] = 5e-4
            for grup in optD.param_groups:
                grup["lr"] = 5e-4
        idx = torch.randint(0, len(Xt), (128,))
        x_asli = Xt[idx]
        z = torch.randn(128, 2)
        x_palsu = G(z)
        d_asli, d_palsu = D(x_asli), D(x_palsu)
        lossD_asli = bce(d_asli, torch.ones(128, 1))
        lossD_palsu = bce(d_palsu, torch.zeros(128, 1))
        lossD = lossD_asli + lossD_palsu
        optD.zero_grad()
        lossD.backward()
        optD.step()
        z = torch.randn(128, 2)
        lossG = bce(D(G(z)), torch.ones(128, 1))
        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()
            a, b, t = fraksi_mode(xs)
            print(f"{it:5d} {lossD_asli.item():8.3f} {lossD_palsu.item():9.3f}"
                  f" {lossG.item():6.3f} {a:6.2f} {b:6.2f} {t:7.2f}")
    with torch.no_grad():
        x_gen = G(torch.randn(4000, 2)).numpy()
        d_asli = D(Xt).numpy()
        d_gen = D(torch.tensor(x_gen, dtype=torch.float32)).numpy()
    a, b, t = fraksi_mode(x_gen)
    akurasi = ((d_asli > 0.5).mean() + (d_gen <= 0.5).mean()) / 2
    print(f"akhir: mode A {a:.3f} | mode B {b:.3f} | antara {t:.3f}"
          f" | akurasi D {akurasi:.3f} | lossD {lossD.item():.3f}")
    print()


latih_duel(buat_G_kecil, "Duel 1: G kecil (linear, 6 parameter)")
latih_duel(buat_G_besar, "Duel 2: G besar (MLP, 1218 parameter)")

# ---------- 4. Label smoothing pada discriminator ----------
torch.manual_seed(21)
G_beku = buat_G_kecil()  # tanpa pelatihan -> tiruan mudah dibedakan dari data
with torch.no_grad():
    x_palsu_beku = G_beku(torch.randn(4000, 2))
EPOCH_HALUS = 600


def latih_D(label_asli, label_palsu):
    """Latih D pada tugas tetap; kembalikan keyakinan rata-rata."""
    torch.manual_seed(21)
    D = buat_D()
    opt = torch.optim.Adam(D.parameters(), lr=2e-3)
    target_asli = torch.full((4000, 1), label_asli)
    target_palsu = torch.full((4000, 1), label_palsu)
    for _ in range(EPOCH_HALUS):
        loss = bce(D(Xt), target_asli) + bce(D(x_palsu_beku), target_palsu)
        opt.zero_grad()
        loss.backward()
        opt.step()
    with torch.no_grad():
        return D(Xt).mean().item(), D(x_palsu_beku).mean().item()


print("=== Label smoothing: D dilatih pada tugas tetap ===")
a_keras, p_keras = latih_D(1.0, 0.0)
a_halus, p_halus = latih_D(0.9, 0.1)
print(f"label keras (1/0)      : D(x) asli {a_keras:.4f}, D(x) palsu {p_keras:.4f}")
print(f"label smoothing (0.9)  : D(x) asli {a_halus:.4f}, D(x) palsu {p_halus:.4f}")

Output yang Dihasilkan:

text
data: 4000 titik, dua mode [[-2.5, -1.5], [2.5, 1.5]], std 0.50

=== Anatomi (lapisan demi lapisan) ===
G kecil  : Linear(2 -> 2) = 6 parameter
G besar  : Linear(2 -> 32) + Tanh + Linear(32 -> 32) + Tanh + Linear(32 -> 2) = 1218 parameter
D        : Linear(2 -> 32) + Tanh + Linear(32 -> 1) + Sigmoid = 129 parameter

=== Duel 1: G kecil (linear, 6 parameter) ===
 iter  lD_asli  lD_palsu     lG  modeA  modeB  antara
    0    0.676     0.763  0.635   0.00   0.00    1.00
  400    0.429     0.569  1.797   0.29   0.08    0.63
  800    0.374     0.520  1.911   0.29   0.07    0.64
 1200    0.311     0.428  2.362   0.16   0.05    0.79
 1600    0.295     0.576  2.160   0.17   0.06    0.78
 1999    0.278     0.514  2.310   0.15   0.05    0.80
akhir: mode A 0.148 | mode B 0.055 | antara 0.797 | akurasi D 0.851 | lossD 0.792

=== Duel 2: G besar (MLP, 1218 parameter) ===
 iter  lD_asli  lD_palsu     lG  modeA  modeB  antara
    0    0.644     0.782  0.625   0.00   0.00    1.00
  400    0.726     0.677  0.712   0.42   0.42    0.17
  800    0.696     0.691  0.695   0.48   0.47    0.05
 1200    0.707     0.704  0.687   0.51   0.46    0.03
 1600    0.697     0.692  0.701   0.50   0.47    0.03
 1999    0.700     0.689  0.695   0.51   0.47    0.02
akhir: mode A 0.499 | mode B 0.474 | antara 0.027 | akurasi D 0.497 | lossD 1.389

=== Label smoothing: D dilatih pada tugas tetap ===
label keras (1/0)      : D(x) asli 0.9857, D(x) palsu 0.0116
label smoothing (0.9)  : D(x) asli 0.8964, D(x) palsu 0.1035

Mari kita telaah kode di atas baris per baris:

  1. Baris 18-20: tiga sumber keacakan dipisah — seed global, default_rng untuk data, dan gen_stat khusus menarik sampel statistik, sehingga pencetakan di tengah loop tidak mengubah lintasan pelatihan.
  2. Baris 23-27: data 4000 titik dari campuran dua Gaussian di [-2.5, -1.5] dan [2.5, 1.5] dengan std 0,50 — dua mode inilah target yang harus dicapai G.
  3. Baris 39-50: tiga pabrik jaringan — G linear 6 parameter, G MLP, dan D classifier biner dengan Sigmoid.
  4. Baris 54-62: fungsi bedah() menghitung total parameter lewat p.numel() dan merangkai nama lapisan dari model.modules() — inilah yang mencetak anatomi tiga baris pertama keluaran.
  5. Baris 77-108: latih_duel() menjalankan pelatihan alternating standar; seed di-reset di baris 79 agar kedua duel benar-benar setara — satu-satunya perbedaan adalah ukuran G.
  6. Baris 104-108: langkah G memakai loss non-saturating BCE(D(G(z)), 1) — bentuk yang menjadi standar sejak makalah GAN pertama.
  7. Baris 115-122: statistik akhir — fraksi mode, akurasi D, dan lossD untuk diagnosis keseimbangan.
  8. Baris 130-157: eksperimen label smoothing — G dibekukan tanpa pelatihan sehingga D menghadapi tugas tetap yang mudah, lalu dua D dilatih dengan label keras 1/0 versus label halus 0.9/0.1.

Membaca Hasil: Kapasitas Menentukan Bentuk Distribusi #

Hitungan parameternya bisa kita verifikasi manual. Linear(m, n) menyimpan m×n bobot plus n bias: G besar = (2×32+32) + (32×32+32) + (32×2+2) = 96 + 1056 + 66 = 1218 parameter; D = 2×32+32 + 32×1+1 = 129 parameter. Pada Duel 1, G linear hanya bisa mengeluarkan satu blob Gaussian tunggal — satu Gaussian tidak mungkin menempati dua mode sekaligus, sehingga solusi terbaiknya adalah menempatkan blob di koridor antara kedua mode: 79,7% sampel jatuh di wilayah “antara”. D menjawab dengan akurasi 0,851 karena blob tunggal jelas berbeda dari data dua mode — kemenangan D di sini bukan karena D hebat, melainkan karena G kekurangan kapasitas. Pada Duel 2, G dengan 1218 parameter memisahkan kedua mode (49,9% dan 47,4%, sisanya 2,7% di antara), akurasi D jatuh ke 0,497, dan lossD berhenti di 1,389 — tepat di sekitar nilai keseimbangan teoretis 2 ln 2 ≈ 1,386.

Label Smoothing: Menutup Langit-langit Keyakinan #

Bagian terakhir keluaran membandingkan dua D pada tugas yang sama. Dengan label keras 1/0, D terdorong sampai keyakinan ekstrem: D(x) asli 0,9857 dan D(x) palsu 0,0116 — sigmoid beroperasi di wilayah jenuh, jauh dari 0,5. Dengan label smoothing 0.9/0.1, keyakinan berhenti di 0,8964 dan 0,1035, persis dekat langit-langit label. Selisih ini terlihat kecil, tetapi konsekuensinya besar: gradien bagi G harus mengalir melalui D, dan turunan sigmoid yang jenuh mendekati nol — jalur gradien menyempit tepat ketika D paling yakin. Label smoothing menjaga D tetap memberi sinyal yang bisa dipelajari G.

Dengan anatomi dan alat diagnosis ini, kita siap menghadapi sisi rapuh GAN: mode collapse, discriminator yang menang mutlak, dan gradien yang hilang. Chapter berikutnya, Stabilitas Training GAN , memicu ketiganya secara nyata lalu menyusun tabel gejala, diagnosis, dan obatnya.