TEKNİK YAZI / Yapay Zekâ Entegrasyonları

GAN'ler: Generator ve Discriminator ile Veri Üretimi

GAN'ler, generator ile discriminator'ın minimax oyunuyla veri üretir; PyTorch ile DCGAN, non-saturating kayıp, WGAN-GP, mode collapse, IS ve FID anlatılıyor.

GAN (Generative Adversarial Network, çekişmeli üretici ağ), Ian Goodfellow ve arkadaşlarının 2014’te önerdiği, iki sinir ağının birbirine karşı eğitildiği bir üretici model yaklaşımıdır. Generator rastgele gürültüden örnek üretir, discriminator ise gelen örneğin gerçek veriden mi yoksa generator’dan mı geldiğini ayırt etmeye çalışır. İki ağ aynı amaç fonksiyonunun zıt yönlerini optimize eder.

Aşağıda minimax amaç fonksiyonu, PyTorch ile bir DCGAN uygulaması, eğitim döngüsü, mode collapse ve kararsızlık sorunları, WGAN-GP, değerlendirme metrikleri (Inception Score, FID, precision ve recall) ve GAN’lerin 2026 itibarıyla üretici modeller arasındaki yeri ele alınıyor.

GAN nasıl çalışır?

Generator’ın veri dağılımına doğrudan erişimi yoktur; öğrenme sinyalini yalnız discriminator’ın gradyanlarından alır. Model açık bir olasılık değeri vermez. Bu yüzden PixelCNN gibi olasılığı açıkça modelleyen yaklaşımların aksine GAN’ler log-olabilirlikle değerlendirilemez.

Amaç fonksiyonu: minimax oyunu

Goodfellow ve arkadaşları iki ağın oynadığı oyunu tek bir değer fonksiyonuyla tanımlar:

min_G max_D V(D, G) = E_{x~p_veri}[log D(x)] + E_{z~p_z}[log(1 − D(G(z)))]

Discriminator bu ifadeyi en büyüklemeye, generator en küçüklemeye çalışır. Generator sabitken en iyi discriminator D*(x) = p_veri(x) / (p_veri(x) + p_g(x)) olur. Bu discriminator yerine konduğunda generator’ın amacı şuna dönüşür:

C(G) = −log 4 + 2 · JSD(p_veri ‖ p_g)

Jensen-Shannon divergence (JSD), iki dağılımın ortalamasına olan Kullback-Leibler divergence değerlerinden kurulur ve yalnız iki dağılım eşitken sıfırdır. Kuramsal en iyi nokta bu yüzden p_g = p_veri durumudur.

Doymayan kayıp (non-saturating loss)

Eğitimin başında generator zayıftır ve discriminator sahte örnekleri kolayca reddeder. Bu durumda log(1 − D(G(z))) doyuma ulaşır ve generator’a giden gradyan çok küçülür. Makale aynı denge noktasını koruyan ama başlangıçta daha güçlü gradyan veren bir değişiklik önerir: generator log(1 − D(G(z))) değerini en küçüklemek yerine log D(G(z)) değerini en büyükler. Pratikte bu, generator adımında sahte örnekleri “gerçek” etiketiyle ikili cross-entropy kaybına vermek demektir; aşağıdaki eğitim döngüsü bunu yapar.

PyTorch ile DCGAN

DCGAN (Radford, Metz ve Chintala, 2015), GAN’i konvolüsyonel ağlarla kararlı eğitmek için bir dizi mimari kural koydu: pooling yerine generator’da transpoze, discriminator’da adımlı (strided) konvolüsyon; generator çıkışı ve discriminator girişi dışında iki ağda da BatchNorm; generator’da ReLU (çıkışta Tanh), discriminator’da eğimi 0,2 olan LeakyReLU; gizli katmanlarda tam bağlı katman yok. Makaleye göre bütün ağırlıklar ortalaması 0, standart sapması 0,02 olan normal dağılımla başlatılır ve Adam optimizer 0,0002 öğrenme oranı ve β1 = 0,5 ile kullanılır.

import torch
import torch.nn as nn

NOISE_DIM = 100


class Generator(nn.Module):
    def __init__(self, noise_dim=NOISE_DIM, img_channels=3, fg=64):
        super().__init__()

        def block(cin, cout, stride, padding):
            return nn.Sequential(
                nn.ConvTranspose2d(cin, cout, 4, stride, padding, bias=False),
                nn.BatchNorm2d(cout),
                nn.ReLU(True),
            )

        self.net = nn.Sequential(
            block(noise_dim, fg * 8, 1, 0),  # (B, 512, 4, 4)
            block(fg * 8, fg * 4, 2, 1),  # (B, 256, 8, 8)
            block(fg * 4, fg * 2, 2, 1),  # (B, 128, 16, 16)
            block(fg * 2, fg, 2, 1),  # (B, 64, 32, 32)
            nn.ConvTranspose2d(fg, img_channels, 4, 2, 1),  # (B, 3, 64, 64)
            nn.Tanh(),  # piksel aralığı [-1, 1]
        )

    def forward(self, z):  # z: (B, noise_dim, 1, 1)
        return self.net(z)


class Discriminator(nn.Module):
    def __init__(self, img_channels=3, fd=64):
        super().__init__()

        def block(cin, cout):
            return nn.Sequential(
                nn.Conv2d(cin, cout, 4, 2, 1, bias=False),
                nn.BatchNorm2d(cout),
                nn.LeakyReLU(0.2, True),
            )

        self.net = nn.Sequential(
            nn.Conv2d(img_channels, fd, 4, 2, 1),  # (B, 64, 32, 32)
            nn.LeakyReLU(0.2, True),
            block(fd, fd * 2),  # (B, 128, 16, 16)
            block(fd * 2, fd * 4),  # (B, 256, 8, 8)
            block(fd * 4, fd * 8),  # (B, 512, 4, 4)
            nn.Conv2d(fd * 8, 1, 4, 1, 0),  # (B, 1, 1, 1): sigmoid öncesi logit
        )

    def forward(self, x):  # x: (B, 3, 64, 64)
        return self.net(x).view(-1)  # (B,)


def weights_init(m):
    # Konvolüsyon ağırlıkları DCGAN makalesindeki gibi N(0, 0.02);
    # BatchNorm başlatması PyTorch DCGAN eğitimindeki tercihtir.
    if isinstance(m, (nn.Conv2d, nn.ConvTranspose2d)):
        nn.init.normal_(m.weight, 0.0, 0.02)
    elif isinstance(m, nn.BatchNorm2d):
        nn.init.normal_(m.weight, 1.0, 0.02)
        nn.init.zeros_(m.bias)

Discriminator’ın son katmanında Sigmoid yoktur; çıkış bir logit’tir ve kayıp nn.BCEWithLogitsLoss ile hesaplanır. Bu, Sigmoid ile nn.BCELoss kullanmakla matematiksel olarak aynıdır ama sayısal olarak daha kararlıdır. BatchNorm başlatması makalede belirtilmez; buradaki değerler PyTorch DCGAN eğitiminden alınmıştır.

Eğitim döngüsü

Her adımda önce discriminator, ardından generator güncellenir. Modeller ve veriler aynı cihazda (CPU ya da GPU) olmalıdır; aksi hâlde PyTorch cihaz uyuşmazlığı hatası verir.

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

transform = transforms.Compose(
    [
        transforms.Resize(64),
        transforms.ToTensor(),
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),  # [-1, 1]
    ]
)
dataset = datasets.CIFAR10("data", train=True, download=True, transform=transform)
loader = DataLoader(dataset, batch_size=128, shuffle=True, drop_last=True)


def train_gan(loader, num_epochs=25, lr=2e-4):
    device = "cuda" if torch.cuda.is_available() else "cpu"
    gen = Generator().to(device)
    disc = Discriminator().to(device)
    gen.apply(weights_init)
    disc.apply(weights_init)

    opt_g = torch.optim.Adam(gen.parameters(), lr=lr, betas=(0.5, 0.999))
    opt_d = torch.optim.Adam(disc.parameters(), lr=lr, betas=(0.5, 0.999))
    bce = nn.BCEWithLogitsLoss()

    for epoch in range(num_epochs):
        for real, _ in loader:
            real = real.to(device)  # (B, 3, 64, 64), [-1, 1]
            b = real.size(0)
            ones = torch.ones(b, device=device)
            zeros = torch.zeros(b, device=device)

            # 1) Discriminator: log D(x) + log(1 - D(G(z))) en büyüklenir
            z = torch.randn(b, NOISE_DIM, 1, 1, device=device)
            fake = gen(z)
            loss_d = bce(disc(real), ones) + bce(disc(fake.detach()), zeros)
            opt_d.zero_grad()
            loss_d.backward()
            opt_d.step()

            # 2) Generator: non-saturating kayıp, log D(G(z)) en büyüklenir
            loss_g = bce(disc(fake), ones)
            opt_g.zero_grad()
            loss_g.backward()
            opt_g.step()

        print(f"epoch {epoch}: loss_d={loss_d.item():.3f} loss_g={loss_g.item():.3f}")
    return gen


generator = train_gan(loader)

fake.detach() discriminator adımında gradyanın generator’a akmasını engeller. Generator adımında aynı fake tensörü yeniden kullanılır ve bu kez gradyan generator parametrelerine kadar iner. Kayıp değerleri sınıflandırma problemlerindeki gibi düzenli azalmaz; iki ağ birbirine göre değiştiği için dalgalanmaları normaldir ve eğitimin ilerleyişi örneklere bakarak ya da aşağıdaki metriklerle izlenir.

GAN türleri

Eğitim zorlukları

Kararsızlık ve kaybolan gradyan

Discriminator generator’dan çok daha hızlı öğrenirse orijinal kayıp doyuma ulaşır ve generator anlamlı gradyan alamaz. Non-saturating kayıp bu sorunu hafifletir ama ortadan kaldırmaz. Heusel ve arkadaşlarının iki zaman ölçekli güncelleme kuralı (TTUR), generator ve discriminator için ayrı öğrenme oranları kullanmayı önerir; hangi ağın daha yüksek oranla eğitileceği mimariye ve kayıp fonksiyonuna göre değişir.

Mod çöküşü (mode collapse)

Generator, veri dağılımının yalnız birkaç bölgesini (mode) üretip discriminator’ı kandırmayı öğrenebilir. Örneğin on rakamlı bir veri setinde yalnız birkaç rakam üretir. Bunu üretilen görüntülerin byte düzeyindeki kopyalarını sayarak yakalamak mümkün değildir, çünkü GAN örnekleri neredeyse hiçbir zaman piksel piksel aynı çıkmaz. Kullanılan yöntemler şunlardır:

WGAN-GP

WGAN, discriminator yerine “critic” adı verilen ve olasılık değil sınırsız bir skor üreten bir ağ kullanır. Amaç, iki dağılım arasındaki Wasserstein-1 mesafesini tahmin etmektir; bunun için critic’in 1-Lipschitz olması gerekir. Gulrajani ve arkadaşları (2017) bu kısıtı ağırlık kırpma yerine bir gradyan cezasıyla (gradient penalty) uygular:

L_critic = E[D(x̃)] − E[D(x)] + λ · E[(‖∇_x̂ D(x̂)‖₂ − 1)²]
L_G      = −E[D(x̃)]
x̃ = G(z),   x̂ = ε·x + (1 − ε)·x̃,   ε ~ U[0, 1],   λ = 10

Bu kayıp yukarıdaki DCGAN discriminator’ına doğrudan eklenemez. Critic’in çıkışında Sigmoid olmamalıdır, çünkü değer bir olasılık değil skordur. Critic’te BatchNorm da kullanılmaz: BatchNorm bir örneğin çıktısını aynı batch’teki diğer örneklere bağlar ve örnek başına hesaplanan gradyan cezasını geçersiz kılar. Makale bunun yerine layer normalization önerir.

class Critic(nn.Module):
    def __init__(self, img_channels=3, fd=64):
        super().__init__()

        def block(cin, cout):
            return nn.Sequential(
                nn.Conv2d(cin, cout, 4, 2, 1),
                nn.GroupNorm(1, cout),  # tek grup: örnek başına (C, H, W) normalizasyonu
                nn.LeakyReLU(0.2, True),
            )

        self.net = nn.Sequential(
            nn.Conv2d(img_channels, fd, 4, 2, 1),  # (B, 64, 32, 32)
            nn.LeakyReLU(0.2, True),
            block(fd, fd * 2),  # (B, 128, 16, 16)
            block(fd * 2, fd * 4),  # (B, 256, 8, 8)
            block(fd * 4, fd * 8),  # (B, 512, 4, 4)
            nn.Conv2d(fd * 8, 1, 4, 1, 0),  # (B, 1, 1, 1): sınırsız skor
        )

    def forward(self, x):
        return self.net(x).view(-1)  # (B,)


def gradient_penalty(critic, real, fake):
    b = real.size(0)
    eps = torch.rand(b, 1, 1, 1, device=real.device)  # örnek başına karışım oranı
    x_hat = (eps * real + (1 - eps) * fake).requires_grad_(True)
    scores = critic(x_hat)  # (B,)
    (grad,) = torch.autograd.grad(outputs=scores.sum(), inputs=x_hat, create_graph=True)
    grad_norm = grad.flatten(1).norm(2, dim=1)  # (B,)
    return ((grad_norm - 1) ** 2).mean()


def wgan_gp_step(gen, critic, opt_g, opt_c, real, n_critic=5, lam=10.0):
    b = real.size(0)
    for _ in range(n_critic):  # her generator adımına karşı 5 critic adımı
        z = torch.randn(b, NOISE_DIM, 1, 1, device=real.device)
        fake = gen(z).detach()
        loss_c = (
            critic(fake).mean()
            - critic(real).mean()
            + lam * gradient_penalty(critic, real, fake)
        )
        opt_c.zero_grad()
        loss_c.backward()
        opt_c.step()

    z = torch.randn(b, NOISE_DIM, 1, 1, device=real.device)
    loss_g = -critic(gen(z)).mean()
    opt_g.zero_grad()
    loss_g.backward()
    opt_g.step()
    return loss_c.item(), loss_g.item()

Makalede önerilen optimizer ayarı Adam, öğrenme oranı 0,0001, β1 = 0 ve β2 = 0,9’dur: torch.optim.Adam(params, lr=1e-4, betas=(0.0, 0.9)). GroupNorm(1, C) her örneği kendi içinde normalleştirir ve konvolüsyon katmanlarında layer normalization yerine yaygın olarak kullanılır.

Değerlendirme metrikleri

Inception Score (IS)

Salimans ve arkadaşlarının (2016) önerdiği Inception Score, üretilen her görüntüyü önceden eğitilmiş Inception-v3 sınıflandırıcısından geçirir ve sınıf olasılıklarını p(y|x) kullanır:

IS = exp( E_x [ D_KL( p(y|x) ‖ p(y) ) ] ),   p(y) = E_x [ p(y|x) ]

Her görüntü tek bir sınıfa güvenle atanıyorsa (p(y|x) düşük entropili) ve üretilen görüntüler birçok sınıfa yayılıyorsa (p(y) yüksek entropili) skor yükselir. Üst sınır sınıf sayısıdır (ImageNet için 1000).

import numpy as np


def inception_score(probs, eps=1e-12):
    # probs: (N, 1000), her satır bir görüntü için p(y|x)
    p_y = probs.mean(axis=0, keepdims=True)  # (1, 1000), marjinal p(y)
    kl = (probs * (np.log(probs + eps) - np.log(p_y + eps))).sum(axis=1)  # (N,)
    return float(np.exp(kl.mean()))

Makale, ölçüt çeşitliliği de içerdiği için yeterince büyük bir örnek kümesiyle (50.000 görüntü) hesaplanması gerektiğini belirtir; yazarların yayımladığı kod skoru örnekleri 10 parçaya bölerek hesaplar ve ortalama ile standart sapmayı verir. IS gerçek veriye hiç bakmaz ve sınıflandırıcı ImageNet sınıflarıyla eğitildiği için bu sınıflarla ilgisiz alanlarda (yüzler, tıbbi görüntüler) yorumlanması zordur.

Fréchet Inception Distance (FID)

Heusel ve arkadaşlarının (2017) FID ölçütü, gerçek ve üretilmiş görüntülerin Inception-v3 özniteliklerini birer çok değişkenli normal dağılım olarak modeller ve aralarındaki Fréchet mesafesini hesaplar. Düşük değer daha iyidir:

FID = ‖μ_r − μ_g‖² + Tr(Σ_r + Σ_g − 2 (Σ_r Σ_g)^{1/2})
def fid(feats_real, feats_gen):
    # feats_*: (N, 2048), Inception-v3 havuzlama katmanı öznitelikleri
    mu_r, mu_g = feats_real.mean(axis=0), feats_gen.mean(axis=0)  # (2048,)
    cov_r = np.cov(feats_real, rowvar=False)  # (2048, 2048)
    cov_g = np.cov(feats_gen, rowvar=False)  # (2048, 2048)
    # Tr((cov_r cov_g)^(1/2)), cov_r @ cov_g özdeğerlerinin kareköklerinin toplamıdır
    eig = np.linalg.eigvals(cov_r @ cov_g).real.clip(min=0)
    tr_covmean = np.sqrt(eig).sum()
    return float(
        ((mu_r - mu_g) ** 2).sum() + np.trace(cov_r) + np.trace(cov_g) - 2 * tr_covmean
    )

Bu iki fonksiyon formülü göstermek içindir; öznitelik çıkarma adımını içermez. Gerçek ölçümde sonuçların karşılaştırılabilir olması için referans bir uygulama, örneğin TorchMetrics FrechetInceptionDistance, kullanılmalı ve örnek sayısı sabit tutulmalıdır. FID makalesinde model dağılımı için 50.000 üretilmiş görüntü kullanılır.

Uygulama alanları

Riskler

GAN’ler var olmayan kişilere ait gerçekçi yüz fotoğrafları üretebildiği için sahte hesap ve kimlik sahteciliğinde kötüye kullanılabilir. Deepfake video ve seslerde de üretici modeller kullanılır. Üretilen içeriği etiketlemek, eğitim verisinin lisansını ve kişisel veri içerip içermediğini kontrol etmek, modeli yayımlamadan önce kötüye kullanım senaryolarını değerlendirmek gerekir.

2026 itibarıyla GAN’lerin yeri

Dhariwal ve Nichol 2021’de diffusion modellerinin ImageNet’te görüntü kalitesinde GAN’leri geçtiğini gösterdi (arXiv:2105.05233). O tarihten bu yana metinden görüntü üretiminde diffusion ve FLUX.1 gibi flow tabanlı modeller baskın yaklaşım hâline geldi. GAN’lerin üstünlüğü tek ileri geçişle örnek üretmeleridir; bu yüzden düşük gecikme gereken vocoder ve süper çözünürlük gibi işlerde, ayrıca diffusion modellerini az adımlı üretime indirgeyen çekişmeli damıtma (Adversarial Diffusion Distillation, Sauer ve ark., 2023, arXiv:2311.17042) gibi yöntemlerde kullanılmaya devam ediyor.

Sonuç

GAN’ler, olasılık hesaplamadan örnek üretmeyi öğrenen çekişmeli eğitim fikrini ve DCGAN, WGAN-GP, StyleGAN gibi etkili mimarileri getirdi. Tek adımda hızlı üretim, görüntüden görüntüye çeviri veya bir başka modelin içinde çekişmeli kayıp gerektiğinde hâlâ uygun bir seçimdir. Kararlı eğitim, ölçülebilir olasılık ya da metinden geniş kapsamlı görüntü üretimi hedefleniyorsa diffusion ve flow tabanlı modellerle başlamak daha uygundur. Deney yapmak için küçük bir veri setinde (MNIST, CIFAR-10) DCGAN iyi bir başlangıçtır ve eğitim için GPU önerilir.

Birincil kaynaklar