Eksperimen Dasar GANs (Generative Adversarial Networks) dengan Python

I Nyoman Donostia 1093 kata
Eksperimen Dasar GANs (Generative Adversarial Networks) dengan Python

Apa itu GANs?

GANs (Generative Adversarial Networks) adalah arsitektur machine learning yang melibatkan dua jaringan saraf "bermusuhan":

  • Generator: menghasilkan data palsu dari input random noise
  • Discriminator: menilai apakah data real atau palsu

Keduanya dilatih bersamaan sampai generatormampu menghasilkan data yang tidak dapat dibedakan dari data asli oleh diskriminator.

Analoginya: generator adalah pemalsu uang, diskriminator adalah polisi. Keduanya saling meningkatkan skill: polisi lebih pintar mendeteksi, pemalsu lebih pintar menyamarkan. Akhirnya? Uang palsu nyaris tidak terbedakan.

1. Setup Environment

# Buat virtual environment
python -m venv venv
source venv/bin/activate  # Linux/Mac
venv\Scripts\activate     # Windows

# Install packages
pip install torch torchvision torchaudio
pip install matplotlib numpy pillow

# Cek GPU (PyTorch)
python -c "import torch; print(torch.cuda.is_available())"

2. Dataset: MNIST Sederhana

MNIST berisi 70.000 gambar digit tulisan tangan (28x28 grayscale). Sempurna untuk eksperimen pertama.

import torch
import torchvision
from torchvision import transforms
from torch.utils.data import DataLoader

# Transform: normalize ke range [-1, 1]
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.5,), (0.5,))
])

# Load dataset
dataset = torchvision.datasets.MNIST(
    root='./data',
    train=True,
    download=True,
    transform=transform
)

dataloader = DataLoader(
    dataset,
    batch_size=64,
    shuffle=True,
    num_workers=2
)

3. Arsitektur Generator

Generator menerima noise random (latent vector) dan menghasilkan gambar 28x28.

import torch.nn as nn

class Generator(nn.Module):
    def __init__(self, latent_dim=100):
        super(Generator, self).__init__()
        
        self.latent_dim = latent_dim
        
        self.model = nn.Sequential(
            # Input: (batch, latent_dim, 1, 1)
            nn.ConvTranspose2d(latent_dim, 256, kernel_size=4, stride=1, padding=0, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(True),
            
            nn.ConvTranspose2d(256, 128, kernel_size=3, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(True),
            
            nn.ConvTranspose2d(128, 64, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(True),
            
            # Output: (batch, 1, 28, 28) — single channel grayscale
            nn.ConvTranspose2d(64, 1, kernel_size=4, stride=2, padding=1, bias=False),
            nn.Tanh()  # Output range [-1, 1]
        )
    
    def forward(self, x):
        # x: (batch, latent_dim, 1, 1)
        return self.model(x)

4. Arsitektur Diskriminator

Diskriminator menerima gambar (real atau palsu) dan output probabilitas (0 = palsu, 1 = real).

class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        
        self.model = nn.Sequential(
            # Input: (batch, 1, 28, 28)
            nn.Conv2d(1, 64, kernel_size=4, stride=2, padding=1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            
            nn.Conv2d(64, 128, kernel_size=4, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2, inplace=True),
            
            nn.Conv2d(128, 256, kernel_size=3, stride=2, padding=1, bias=False),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2, inplace=True),
            
            # Output: 1 value (probability)
            nn.Conv2d(256, 1, kernel_size=4, stride=1, padding=0, bias=False),
            nn.Sigmoid()  # Output range [0, 1]
        )
    
    def forward(self, x):
        return self.model(x).view(-1, 1).squeeze(1)

5. Loss Function dan Optimizer

GAN menggunakan BCELoss (Binary Cross Entropy) untuk kedua jaringan.

import torch.optim as optim

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

latent_dim = 100
G = Generator(latent_dim).to(device)
D = Discriminator().to(device)

criterion = nn.BCELoss()  # Binary Cross Entropy

# Optimizer — Adam dengan learning rate rendah dan beta1=0.5 (GAN best practice)
optimizerG = optim.Adam(G.parameters(), lr=0.0002, betas=(0.5, 0.999))
optimizerD = optim.Adam(D.parameters(), lr=0.0002, betas=(0.5, 0.999))

# Labels
real_label = 1.0
fake_label = 0.0

6. Training Loop

Inilah inti GAN training — alternating antara train diskriminator dan generator.

import matplotlib.pyplot as plt
import numpy as np

num_epochs = 50
fixed_noise = torch.randn(64, latent_dim, 1, 1, device=device)  # Untuk visualisasi
img_list = []

for epoch in range(num_epochs):
    for i, (real_images, _) in enumerate(dataloader):
        batch_size = real_images.size(0)
        real_images = real_images.to(device)
        
        # ============ Train Discriminator ============
        # max log(D(real)) + log(1 - D(G(z)))
        D.zero_grad()
        
        # Real images
        labels_real = torch.full((batch_size,), real_label, device=device)
        output_real = D(real_images)
        lossD_real = criterion(output_real, labels_real)
        lossD_real.backward()
        
        # Fake images
        noise = torch.randn(batch_size, latent_dim, 1, 1, device=device)
        fake_images = G(noise)
        labels_fake = torch.full((batch_size,), fake_label, device=device)
        output_fake = D(fake_images.detach())  # detach: tidak train G di sini
        lossD_fake = criterion(output_fake, labels_fake)
        lossD_fake.backward()
        
        lossD = lossD_real + lossD_fake
        optimizerD.step()
        
        # ============ Train Generator ============
        # min log(1 - D(G(z))) atau max log(D(G(z)))
        G.zero_grad()
        
        labels_g = torch.full((batch_size,), real_label, device=device)  # G ingin D(prediksi) = real
        output_g = D(fake_images)  # jangan detach — kembali gradient ke G
        lossG = criterion(output_g, labels_g)
        lossG.backward()
        optimizerG.step()
        
        # ============ Log progress ============
        if i % 100 == 0:
            print(f"Epoch [{epoch}/{num_epochs}] Batch [{i}] "
                  f"Loss_D: {lossD.item():.4f}, Loss_G: {lossG.item():.4f}")
    
    # Save image sample tiap epoch
    if epoch % 5 == 0:
        with torch.no_grad():
            fake = G(fixed_noise).cpu()
        img_list.append(torchvision.utils.make_grid(fake, padding=2, normalize=True))

7. Visualisasi Hasil

Animasi dari noise → digit yang jelas.

fig = plt.figure(figsize=(8,8))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=True)] for i in img_list]
import matplotlib.animation as animation
import IPython.display as ipd

ani = animation.ArtistAnimation(fig, ims, interval=200, repeat_delay=1000, blit=True)
ipd.HTML(ani.to_jshtml())

Image pertama muncul random noise. Setelah 50 epoch, gambar menyerupai digit tulisan tangan.

8. Mode Collapse: Masalah GANs

Mode collapse terjadi saat generator hanya menghasilkan variasi terbatas. Misal, generator hanya menghasilkan digit "1" padahal dataset punya 0-9.

Deteksi mode collapse:

  • Loss D dan G mendekati 0 (over-converged)
  • Generated samples terlihat identic

Solusi yang bisa dipraktikkan:

  1. Wasserstein GAN (WGAN): gunakan loss berbeda (EM distance) - lebih stabil

    # Ganti criterion dengan WLoss
    # Loss_D: E[D(real)] - E[D(fake)] (negative)
    # Loss_G: -E[D(fake)]
    # Ganti sigmoid output D: tidak ada activation (output langsung)
    # Optimizer: RMSprop lr=5e-5
    
  2. Label smoothing: real_label = 0.9 (bukan 1.0) — D tidak over-confident

  3. Add noise to inputs: input D ditambah noise → konvergen lebih lambat dan stabil

  4. Conditional GAN (cGAN): kondisikan G/D dengan class label — generator tahu digit apa yang harus dibuat

class CondGenerator(nn.Module):
    def __init__(self, latent_dim=100, num_classes=10):
        super().__init__()
        
        # Embed label ke latent space
        self.label_embedding = nn.Embedding(num_classes, num_classes)
        
        # Latent + label → input
        self.model = nn.Sequential(
            nn.Linear(latent_dim + num_classes, 256),
            # ...
        )
    
    def forward(self, noise, labels):
        emb = self.label_embedding(labels)
        x = torch.cat([noise, emb], dim=1)
        return self.model(x)

9. Evaluasi: FID dan IS

Untuk mengukur kualitas generasi:

  • FID (Fréchet Inception Distance): semakin rendah semakin baik. Mengukur jarak distribusi antara real dan fake.
  • IS (Inception Score): semakin tinggi semakin baik. Mengukur kualitas dan diversity.
# Pakai library pytorch-fid
pip install pytorch-fid
python -m pytorch_fid real_images/ fake_images/

10. Tips Practical

  • Batch size: 64-128 optimal. Terlalu kecil: tidak stabil. Terlalu besar: konvergen lambat
  • Learning rate: 0.0002 untuk Adam adalah sweet spot. Tingkatkan perlahan ke 0.001 untuk eksperimen
  • LeakyReLU: gunakan leak=0.2 di discriminator (mencegah sparse gradients)
  • Stride convolutions: selalu gunakan stride 2 (atau 4) untuk downsampling — lebih baik dari pooling
  • Batch normalization: wajib di generator dan diskriminator, kecuali layer output G dan input D

Kesimpulan

GANs adalah fundamental generatif AI yang powerful. Eksperimen pertama dengan MNIST adalah cara terbaik untuk memahami fundamental: musuh vs musuh, generator maju dengan feedback dari diskriminator, dan akhirnya menghasilkan data sintesis yang realistis.

Setelah menguasai DCGAN ini, eksplorasi lebih lanjut:

  • Progressive GAN: training bertahap dari 4x4 hingga 1024x1024
  • StyleGAN: kontrol style konten gambar
  • CycleGAN: image-to-image translation tanpa paired data (horse ↔ zebra)
  • BigGAN: high-fidelity class-conditional generation

Kode lengkap artikel ini tersedia di repo GitHub saya. Selamat berexperiment!

I Nyoman Donostia

I Nyoman Donostia

Fullstack Developer - Bali, Indonesia

Artikel Terkait