VAE menghasilkan data lewat satu jaringan yang mengejar satu loss gabungan. Generative Adversarial Network (GAN) mengambil jalan yang sama sekali berbeda: dua jaringan yang dilatih bergantian dengan tujuan yang saling berlawanan. Generator belajar mengubah noise menjadi data tiruan; discriminator belajar membedakan data asli dari tiruan. Tidak ada formula kepadatan peluang P(x) yang ditulis di mana pun — kualitas generasi muncul dari kompetisi yang berjalan lama. Pendekatan ini diperkenalkan Goodfellow dkk. pada 2014 (arXiv:1406.2661 ) dan menjadi fondasi hampir seluruh generator gambar modern. Chapter ini membedah rumus minmax-nya, lalu melatih GAN mini di PyTorch dan mengamati sampel generator konvergen ke statistik data, angka demi angka.

Dua Jaringan yang Bersaing #

Bayangkan dua pihak yang terus meningkat dalam satu arena. Generator G menerima vektor noise z ~ N(0, I) dan menghasilkan G(z) — tiruan data. Discriminator D menerima satu titik data dan mengeluarkan peluang bahwa titik itu asli. G ingin membodohi D; D ingin menangkap tiruan. Ketika G berhasil sempurna, distribusi tiruan menempel pada distribusi data, dan D tidak bisa berbuat lebih baik daripada menebak acak — itulah titik keseimbangan yang dicari pelatihan.

Kedua jaringan memakai loss yang sama tapi dibaca dengan tanda berlawanan — inilah inti adversarial: kemajuan satu pihak adalah kemunduran pihak lain.

Loss Adversarial: Minmax #

Rumus asli GAN adalah permainan nilai minmax:

min_G max_D V(D, G) = E_x~data[log D(x)] + E_z~noise[log(1 − D(G(z)))]

Bacanya dua arah. Dari sisi D (bagian max): D ingin memberi D(x) = 1 untuk data asli dan D(G(z)) = 0 untuk tiruan — kedua log itu maksimum justru ketika D klasifikasinya tepat. Dari sisi G (bagian min): G ingin suku kedua membesar, artinya D(G(z)) mendekati 1 — tiruan dinilai asli. Untuk G tetap, discriminator optimal punya bentuk tertutup: D*(x) = p_data(x) / (p_data(x) + p_g(x)) — peluang Bayesian bahwa x berasal dari data. Titik keseimbangan global tercapai ketika p_g = p_data; saat itu D*(x) = 0.5 di mana-mana dan D menebak acak.

Praktiknya, G jarang memakai suku log(1 − D(G(z))) karena gradiennya mengecil ketika D menang telak di awal pelatihan. Standar industri memakai bentuk non-saturating: G meminimalkan −log D(G(z)) — tujuan sama, gradien tetap sehat.

Demo: GAN Mini pada Gaussian 2D #

Data kita satu Gaussian 2D anisotropik. Pilihan ini sengaja: keluaran G linear Wz + b dari noise Gaussian selalu Gaussian, sehingga G mampu mereplikasi data secara tepat — sisanya menguji apakah kompetisi menemukan solusi itu. Satu catatan desain: D perlu satu hidden layer kecil, karena discriminator linear hanya peka terhadap selisih rata-rata, tidak terhadap selisih varians. Berkas gan_mini.py:

python
"""GAN mini: generator linear vs discriminator kecil pada Gaussian 2D.

G memetakan noise z ~ N(0,I) 2D menjadi data palsu; D menilai peluang
"asli" untuk tiap titik 2D. Pelatihan alternating: satu langkah D
(asli vs palsu, BCE), satu langkah G (non-saturating: -log D(G(z))).
Karena G linear, G secara tepat dapat mereplikasi Gaussian mana pun --
syaratnya D harus cukup ekspresif untuk "melihat" selisih varians,
sehingga D memakai satu hidden layer kecil. Sampel G dibandingkan
statistiknya dengan data (mean, std, korelasi). Deterministik, CPU.
"""

import math

import numpy as np
import torch
import torch.nn as nn

torch.manual_seed(3)
rng = np.random.default_rng(11)
gen_stat = torch.Generator().manual_seed(99)  # noise khusus statistik

# ---------- 1. Data: Gaussian 2D anisotropik ----------
MU = np.array([1.0, 2.0])
KOV = np.array([[2.5, 1.2], [1.2, 1.0]])
X = rng.multivariate_normal(MU, KOV, size=4000)
Xt = torch.tensor(X, dtype=torch.float32)
print(f"data: {len(X)} titik, mean {np.round(X.mean(0).astype(np.float64), 2).tolist()},"
      f" std {np.round(X.std(0).astype(np.float64), 2).tolist()},"
      f" korelasi {np.corrcoef(X.T)[0, 1]:.2f}")
print()

# ---------- 2. Dua jaringan ----------
G = nn.Linear(2, 2)
D = nn.Sequential(nn.Linear(2, 8), nn.Tanh(), nn.Linear(8, 1), nn.Sigmoid())
print(f"G: Linear(2 -> 2), {sum(p.numel() for p in G.parameters())} parameter")
print(f"D: Linear(2 -> 8) -> Tanh -> Linear(8 -> 1) -> Sigmoid,"
      f" {sum(p.numel() for p in D.parameters())} parameter")
print()

# ---------- 3. Pelatihan alternating ----------
optG = torch.optim.Adam(G.parameters(), lr=5e-3)
optD = torch.optim.Adam(D.parameters(), lr=5e-3)
bce = nn.BCELoss()
BATCH, ITER, DECAY = 128, 2000, 1000
cek = (0, 250, 500, 750, 1000, 1500, 1999)


def sampel_generator(n=4000):
    """Tarik n sampel dari G (noise dari generator terpisah)."""
    with torch.no_grad():
        return G(torch.randn(n, 2, generator=gen_stat)).numpy()


print("=== Pelatihan alternating (1 langkah D + 1 langkah G) ===")
print(f"{'iter':>5s} {'lossD':>6s} {'lossG':>6s}  sampel G: mean / std / korelasi")
for it in range(ITER):
    if it == DECAY:  # redam osilasi menjelang akhir
        for grup in optG.param_groups:
            grup["lr"] = 1e-3
        for grup in optD.param_groups:
            grup["lr"] = 1e-3
    # --- langkah D: klasifikasi asli (1) vs palsu (0) ---
    idx = torch.randint(0, len(Xt), (BATCH,))
    x_asli = Xt[idx]
    z = torch.randn(BATCH, 2)
    x_palsu = G(z)
    lossD = bce(D(x_asli), torch.ones(BATCH, 1)) + bce(
        D(x_palsu), torch.zeros(BATCH, 1)
    )
    optD.zero_grad()
    lossD.backward()
    optD.step()
    # --- langkah G: buat D menilai palsu sebagai asli ---
    z = torch.randn(BATCH, 2)
    lossG = bce(D(G(z)), torch.ones(BATCH, 1))
    optG.zero_grad()
    lossG.backward()
    optG.step()
    if it in cek:
        xs = sampel_generator()
        print(f"{it:5d} {lossD.item():6.3f} {lossG.item():6.3f}  "
              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" korr {np.corrcoef(xs.T)[0, 1]:.2f}")
print(f"(learning rate 5e-3 diturunkan menjadi 1e-3 pada iterasi {DECAY})")
print()

# ---------- 4. Evaluasi akhir ----------
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()
print("=== Hasil akhir: 4000 sampel G vs data ===")
print(f"{'statistik':12s} {'data':>16s} {'sampel G':>16s}")
for nama, f in (
    ("mean x1", lambda M: M[:, 0].mean()),
    ("mean x2", lambda M: M[:, 1].mean()),
    ("std x1", lambda M: M[:, 0].std()),
    ("std x2", lambda M: M[:, 1].std()),
    ("korelasi", lambda M: np.corrcoef(M.T)[0, 1]),
):
    print(f"{nama:12s} {f(X):>16.3f} {f(x_gen):>16.3f}")
print()
print(f"D(x) data asli  : rata-rata {d_asli.mean():.3f}")
print(f"D(x) data palsu : rata-rata {d_gen.mean():.3f}")
akurasi_D = ((d_asli > 0.5).mean() + (d_gen <= 0.5).mean()) / 2
print(f"akurasi D       : {akurasi_D:.3f} (0.5 = D tidak bisa membedakan)")
print(f"lossD akhir     : {lossD.item():.3f} ~ 2 ln 2 = {2 * math.log(2):.3f}")
print()
print("5 sampel data     :", np.round(X[:5].astype(np.float64), 2).tolist())
print("5 sampel generator:", np.round(x_gen[:5].astype(np.float64), 2).tolist())

Output yang Dihasilkan:

text
data: 4000 titik, mean [0.95, 1.97], std [1.61, 1.01], korelasi 0.77

G: Linear(2 -> 2), 6 parameter
D: Linear(2 -> 8) -> Tanh -> Linear(8 -> 1) -> Sigmoid, 33 parameter

=== Pelatihan alternating (1 langkah D + 1 langkah G) ===
 iter  lossD  lossG  sampel G: mean / std / korelasi
    0  1.359  0.661  mean [-0.05, -0.62] std [0.89, 0.73] korr 0.89
  250  1.118  1.330  mean [-0.83, 1.18] std [1.49, 0.34] korr 0.99
  500  1.365  0.795  mean [0.07, 1.56] std [1.54, 0.67] korr 1.00
  750  1.362  0.714  mean [0.4, 1.68] std [1.38, 1.15] korr 0.69
 1000  1.379  0.728  mean [0.64, 1.81] std [1.76, 0.94] korr 0.48
 1500  1.374  0.686  mean [0.72, 1.83] std [1.76, 1.12] korr 0.86
 1999  1.374  0.687  mean [0.85, 1.88] std [1.64, 1.01] korr 0.78
(learning rate 5e-3 diturunkan menjadi 1e-3 pada iterasi 1000)

=== Hasil akhir: 4000 sampel G vs data ===
statistik                data         sampel G
mean x1                 0.953            0.838
mean x2                 1.973            1.875
std x1                  1.610            1.671
std x2                  1.007            1.031
korelasi                0.765            0.779

D(x) data asli  : rata-rata 0.503
D(x) data palsu : rata-rata 0.500
akurasi D       : 0.514 (0.5 = D tidak bisa membedakan)
lossD akhir     : 1.374 ~ 2 ln 2 = 1.386

5 sampel data     : [[0.57, 2.66], [-0.76, 0.69], [1.61, 1.99], [0.13, 1.48], [0.36, 0.42]]
5 sampel generator: [[-0.11, 1.4], [-0.07, 2.15], [-0.88, 1.34], [2.98, 1.93], [1.8, 2.68]]

Mari kita telaah kode di atas baris per baris, dengan fokus pada loop alternating yang menjadi inti GAN:

  1. Baris 18-20: tiga sumber keacakan dipisah — seed global untuk pelatihan, default_rng untuk data, dan gen_stat khusus untuk menarik sampel statistik; pencetakan di tengah loop karenanya tidak mengubah lintasan pelatihan.
  2. Baris 23-29: data Gaussian 2D dengan kovarian penuh — mean [0.95, 1.97], std [1.61, 1.01], korelasi 0.77 sebagai target konvergensi.
  3. Baris 33-37: dua jaringan — G hanya 6 parameter (satu Linear), D 33 parameter dengan Tanh dan keluaran Sigmoid (peluang “asli”).
  4. Baris 41-45: dua optimizer Adam terpisah — masing-masing jaringan punya kecepatan belajar sendiri, dan nn.BCELoss menjadi wasit kedua pihak.
  5. Baris 57-61: learning rate diturunkan 5e-3 → 1e-3 di iterasi 1000 — pelatihan GAN memang osilatif; memperlambat langkah menjelang akhir menenangkan angka-angkanya.
  6. Baris 62-72: langkah D — satu batch data asli dan satu batch tiruan; lossD menjumlahkan dua suku BCE: asli dihukum jika D(x) < 1, tiruan dihukum jika D(x) > 0. Hanya bobot D yang diperbarui di langkah ini.
  7. Baris 73-78: langkah G — batch noise baru diumpankan sampai keluaran D; lossG = BCE(D(G(z)), 1) adalah bentuk non-saturating: G berhasil ketika tiruannya dinilai asli. Hanya bobot G yang diperbarui.
  8. Baris 88-111: evaluasi akhir — 4000 sampel dari G dibandingkan statistiknya dengan data, lalu D diuji pada kedua jenis titik.

Hasil akhirnya konvergen dengan rapi. Mean sampel G [0.84, 1.88] berhadapan dengan data [0.95, 1.97]; std [1.67, 1.03] vs [1.61, 1.01]; korelasi 0,78 vs 0,77. Paling penting, tiga angka keseimbangan: D memberi peluang asli 0,503 untuk data dan 0,500 untuk tiruan — nyaris tak bisa membedakan — dengan akurasi 0,514 dan lossD 1,374, tepat di dekat 2 ln 2 = 1.386, nilai teoretis saat D menebak acak pada kedua kelas. Contoh lima sampel di baris terakhir memperlihatkan tiruan yang secara individual sulit dibedakan dari data.

GAN mini ini menangkap seluruh mekanisme permainan, tetapi kedua jaringannya masih anonim — nn.Linear bertumpuk tanpa wajah. Chapter berikutnya membedah anatomi kedua jaringan itu lebih dalam: struktur internal generator dan discriminator, jumlah parameter, efek kapasitas, dan label smoothing.