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:
"""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:
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:
- Baris 18-20: tiga sumber keacakan dipisah — seed global untuk pelatihan,
default_rnguntuk data, dangen_statkhusus untuk menarik sampel statistik; pencetakan di tengah loop karenanya tidak mengubah lintasan pelatihan. - Baris 23-29: data Gaussian 2D dengan kovarian penuh — mean
[0.95, 1.97], std[1.61, 1.01], korelasi0.77sebagai target konvergensi. - Baris 33-37: dua jaringan —
Ghanya 6 parameter (satuLinear),D33 parameter denganTanhdan keluaranSigmoid(peluang “asli”). - Baris 41-45: dua optimizer Adam terpisah — masing-masing jaringan punya kecepatan belajar sendiri, dan
nn.BCELossmenjadi wasit kedua pihak. - Baris 57-61: learning rate diturunkan 5e-3 → 1e-3 di iterasi 1000 — pelatihan GAN memang osilatif; memperlambat langkah menjelang akhir menenangkan angka-angkanya.
- Baris 62-72: langkah D — satu batch data asli dan satu batch tiruan;
lossDmenjumlahkan dua suku BCE: asli dihukum jikaD(x) < 1, tiruan dihukum jikaD(x) > 0. Hanya bobotDyang diperbarui di langkah ini. - Baris 73-78: langkah G — batch noise baru diumpankan sampai keluaran
D;lossG = BCE(D(G(z)), 1)adalah bentuk non-saturating:Gberhasil ketika tiruannya dinilai asli. Hanya bobotGyang diperbarui. - Baris 88-111: evaluasi akhir — 4000 sampel dari
Gdibandingkan statistiknya dengan data, laluDdiuji 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.