Key Findings
  • GAN[1] hat mit dem Nullsummenspiel zwischen Generator und Diskriminator die adversariale generative Modellierung begruendet und ist eines der einflussreichsten Frameworks in der Geschichte des Deep Learning
  • Von DCGAN[2] bis StyleGAN3[18] hat die GAN-Architektur fuenf Generationen der Evolution durchlaufen — die Bildgenerierungsqualitaet stieg von verschwommenem Rauschen zu fotorealistischer Qualitaet
  • WGAN[3] hat mit der Wasserstein-Distanz das Trainingsziel neu definiert und die Instabilitaet sowie den Mode Collapse des originalen GAN geloest; CycleGAN[10] ermoeglichte ungepaarte Bilduebersetzung
  • Dieser Artikel enthaelt zwei Google Colab Praxis-Labs: DCGAN-Handschriftengenerierung (mit FID-Bewertung) und SeqGAN-Textsequenzgenerierung (Training eines diskreten GAN mit Reinforcement Learning)

I. Die Kunst des Wettstreits: Die Kernphilosophie von GAN

Im Jahr 2014 stellten Ian Goodfellow und seine Kollegen in einer bahnbrechenden Arbeit[1] das Generative Adversarial Network (GAN) vor. Die Kernidee basiert auf einer genialen Analogie: das Spiel zwischen einem Geldfaelscher und der Polizei.

Der Faelscher (Generator G) versucht, taueschend echte Banknoten herzustellen; die Polizei (Diskriminator D) versucht, echt von falsch zu unterscheiden. Im Verlauf des Spiels wird der Faelscher immer geschickter im Faelschen und die Polizei immer geschickter im Erkennen — bis die Faelschungen so perfekt sind, dass selbst die Polizei sie nicht mehr unterscheiden kann.

Die mathematische Form dieses Spiels ist ein Minimax-Nullsummenspiel:

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

Dabei gilt:
  G(z): Generator, bildet zufaelliges Rauschen z auf gefaelschte Samples ab
  D(x): Diskriminator, gibt die Wahrscheinlichkeit aus, dass x ein echtes Sample ist
  p_data: Echte Datenverteilung
  p_z: Rauschverteilung (typischerweise Gauss- oder Gleichverteilung)

Optimaler Diskriminator: D*(x) = p_data(x) / (p_data(x) + p_g(x))
Nash-Gleichgewicht: p_g = p_data, D*(x) = 0.5 (voellig ununterscheidbar)

Das Training von GAN ist ein alternierender Zwei-Schritt-Prozess:

  1. Training des Diskriminators: G wird fixiert, D wird mit echten Samples und von G generierten gefaelschten Samples trainiert, um echt von falsch zu unterscheiden
  2. Training des Generators: D wird fixiert, G wird trainiert, Samples zu erzeugen, die D faelschlicherweise als echt klassifiziert

II. Der Alptraum des Trainings: Die drei grossen Herausforderungen von GAN

Die Theorie von GAN ist elegant, aber das Training ist aeusserst schwierig. Drei Kernprobleme plagten die Forschungsgemeinschaft jahrelang:

Mode Collapse

Der Generator „nimmt eine Abkuerzung" — er lernt nur, einige wenige spezifische Samples zu erzeugen, die ausreichen, um den Diskriminator zu taeuschen, anstatt die gesamte Datenverteilung abzudecken. Beispielsweise koennte ein auf MNIST trainiertes GAN nur die Ziffern 1 und 7 generieren.

Instabiles Training

Die Verlustfunktion des originalen GAN basiert auf der Jensen-Shannon-Divergenz. Wenn die echte Verteilung und die generierte Verteilung keine Ueberlappung haben (was in hochdimensionalen Raeumen fast sicher der Fall ist), ist die JS-Divergenz konstant log(2) — der Gradient ist null und der Generator kann nicht lernen.

Schwierige Bewertung

GAN hat keinen ELBO-Verlust wie VAE, der verfolgt werden kann. Der Trainingsverlust spiegelt nicht die Generierungsqualitaet wider — ein Diskriminator-Verlust nahe 0,5 kann sowohl Gleichgewicht als auch Zusammenbruch bedeuten.

III. DCGAN: Etablierung der Designrichtlinien fuer Bild-GANs

Radford et al.[2] stellten mit DCGAN (Deep Convolutional GAN) eine Reihe klassischer Designrichtlinien fuer Bild-GANs auf:

RichtlinieGeneratorDiskriminator
Up-/DownsamplingTransponierte Faltung (Stride 2)Strided Convolution (Stride 2)
NormierungBatchNorm (ausser Ausgabeschicht)BatchNorm (ausser Eingabeschicht)
AktivierungsfunktionReLU (Ausgabe: Tanh)LeakyReLU(0.2)
Vollvernetzte SchichtenNicht verwendetNicht verwendet

Diese scheinbar einfachen Richtlinien hatten weitreichende Bedeutung — sie verwandelten das GAN-Training von „zufaelligem Erfolg" in „reproduzierbare Ergebnisse". DCGAN zeigte auch, dass der latente Raum des Generators eine sinnvolle Struktur besitzt: z(Mann mit Brille) - z(Mann) + z(Frau) ≈ z(Frau mit Brille).

IV. WGAN: Neudefinition der Distanzmetrik

WGAN[3] (Wasserstein GAN) war ein Wendepunkt fuer die Trainingsstabilitaet von GAN. Arjovsky et al. aenderten das Trainingsziel von der JS-Divergenz zur Wasserstein-1-Distanz (Earth Mover's Distance):

W(p_data, p_g) = inf_{γ∈Π(p_data, p_g)} E_{(x,y)~γ}[||x - y||]

Intuitive Erklaerung: Der minimale „Arbeitsaufwand", um die Verteilung p_g in p_data zu „transportieren"

WGAN-Verlust (Kantorovich-Rubinstein-Dualitaet):
  L_critic = E_{x~p_data}[f_w(x)] - E_{z~p_z}[f_w(G(z))]
  L_G = -E_{z~p_z}[f_w(G(z))]

  f_w ist ein 1-Lipschitz-stetiger „Kritiker" (ersetzt den Diskriminator)

Der entscheidende Vorteil der Wasserstein-Distanz: Selbst wenn zwei Verteilungen keine Ueberlappung haben, liefert sie aussagekraeftige Gradienten. Der Verlust von WGAN korreliert positiv mit der Generierungsqualitaet — je niedriger der Verlust, desto besser die Generierung. Dies war mit dem originalen GAN nicht moeglich.

WGAN-GP[4] ersetzte ferner das Weight Clipping des originalen WGAN durch eine Gradient Penalty zur Durchsetzung der Lipschitz-Beschraenkung und erzielte damit ein noch stabileres Training.

V. Die StyleGAN-Reihe: Der Hoehepunkt der Bildgenerierung

Karras et al. bei NVIDIA trieben in einer Reihe von Forschungsarbeiten[5][6][7][18] die Bildgenerierungsqualitaet von GAN auf ein erstaunliches Niveau:

ModellJahrWichtigste InnovationAuswirkung
ProGAN[5]2018Progressives Training (4x4 → 1024x1024)Erstmalige fotorealistische Gesichter in 1024²
StyleGAN[6]2019Mapping-Netzwerk + AdaIN-StilinjektionFeine, schichtweise Stilkontrolle
StyleGAN2[7]2020Weight Demodulation + Path Length RegularisierungBeseitigung von Tropfen-Artefakten, glaetterer latenter Raum
StyleGAN3[18]2021Aliasing-Behebung, kontinuierliche SignalverarbeitungSub-Pixel-Aequivarianz, Beseitigung von „Texturhaftung"

Die zentrale Architekturinnovation von StyleGAN ist die Trennung von Inhalt und Stil:

StyleGAN Generator:
  z ∈ Z (512-dim)  →  Mapping Network (8-Schicht-MLP)  →  w ∈ W (512-dim)
                                                     ↓
  Constant 4×4     →  [AdaIN(w)] → Conv → [AdaIN(w)] → Upsample → ...
                       Grober Stil          Mittlerer Stil      Feiner Stil
                       (Pose/Gesichtsform)  (Merkmale/Frisur)   (Hautfarbe/Textur)

  Der W-Raum ist staerker „entwirrt" als der Z-Raum → Lineare Interpolation erzeugt sinnvolle Uebergaenge

VI. Bilduebersetzung: Pix2Pix und CycleGAN

Pix2Pix: Gepaarte Bilduebersetzung

Isola et al.[9] wandten mit Pix2Pix bedingte GAN[8] auf Bilduebersetzung an — Kantenbild→Foto, Semantik-Label→Strassenszene, Schwarz-Weiss→Farbe. Es verwendet einen U-Net-Generator und einen PatchGAN-Diskriminator (der nur die Echtheit von 70x70 Patches beurteilt, nicht des gesamten Bildes).

CycleGAN: Ungepaarte Bilduebersetzung

CycleGAN[10] loest ein noch anspruchsvolleres Problem: Bilduebersetzung ohne gepaarte Daten. Der Kern ist der Zyklus-Konsistenz-Verlust — wenn man ein Pferd in ein Zebra uebersetzt und dann das Zebra zurueckuebersetzt, sollte man das urspruengliche Pferd erhalten:

Zyklus-Konsistenz:
  x → G(x) → F(G(x)) ≈ x   (Vorwaertszyklus)
  y → F(y) → G(F(y)) ≈ y   (Rueckwaertszyklus)

  L_cycle = ||F(G(x)) - x||₁ + ||G(F(y)) - y||₁

VII. GAN fuer Text: Die Herausforderung diskreter Sequenzen

Die Anwendung von GAN auf Textgenerierung steht vor einem fundamentalen Hindernis: Das Sampling diskreter Tokens ist nicht differenzierbar. Backpropagation durch argmax oder Categorical Sampling ist nicht moeglich.

SeqGAN[11] loest dieses Problem elegant mit einem Reinforcement-Learning-Ansatz:

SeqGAN-Framework:
  Generator = Policy Network (Strategienetzwerk)
  Diskriminator = Reward Function (Belohnungsfunktion)
  Aktion = Auswahl des naechsten Tokens
  Zustand = Bereits generierte Token-Sequenz

  Trainingsablauf:
  1. G generiert autoregressiv vollstaendige Sequenzen
  2. D bewertet vollstaendige Sequenzen (echt/falsch)
  3. Monte-Carlo-Rollout schaetzt Q-Werte fuer Zwischenzustaende
  4. G wird mit REINFORCE Policy Gradient aktualisiert

Zhang et al.[17] schlugen einen alternativen Ansatz vor — adversariales Training im latenten Merkmalsraum statt im diskreten Token-Raum, wobei die Verteilungen durch kernelbasierte Divergenzmasse abgeglichen werden, um das Problem des diskreten Samplings zu umgehen.

VIII. Techniken zur Trainingsstabilisierung

Neben der WGAN-Reihe gibt es mehrere weitere wichtige Stabilisierungstechniken:

TechnikKernideeWirkung
Spektralnormierung[12]Division jeder Gewichtsmatrix des Diskriminators durch ihren groessten SingulaerwertLeichtgewichtige Lipschitz-Beschraenkung mit vernachlaessigbarem Rechenaufwand
Progressives Training[5]Start mit niedriger Aufloesung, schrittweises Hinzufuegen von SchichtenStabilere Generierung hochaufloesender Bilder
Truncation Trick[13]Einschraenkung des z-Bereichs bei der Inferenz (Truncation)Opfert Diversitaet fuer Qualitaet
Feature Matching[14]Abgleich der Statistiken mittlerer Diskriminator-SchichtenReduktion von Mode Collapse
Zwei-Zeitskalen-Regel[15]D und G verwenden unterschiedliche LernratenTheoretische Konvergenz zu einem lokalen Nash-Gleichgewicht

IX. Bewertungsmetriken: Wie misst man die Generierungsqualitaet?

MetrikBerechnungsweiseWas wird gemessen?Einschraenkungen
IS[14]KL-Divergenz der Inception-Modell-VorhersagenSchaerfe + DiversitaetKein Vergleich mit der echten Verteilung
FID[15]Frechet-Distanz der Inception-Merkmale zwischen echten und generierten BildernQualitaet + Diversitaet + Naehe zur echten VerteilungBenoetigt grosse Stichprobenmengen, Abhaengigkeit von Inception
LPIPSDistanz im perzeptuellen MerkmalsraumPerzeptuelle AehnlichkeitPaarweiser Vergleich, nicht auf Verteilungsebene

FID[15] ist die derzeit am weitesten verbreitete GAN-Bewertungsmetrik. Sie vergleicht Mittelwert und Kovarianz der echten und generierten Bilder im Inception-v3-Merkmalsraum — niedrigere Werte bedeuten bessere Generierungsqualitaet. Typische FID-Werte: StyleGAN2 auf FFHQ ca. 2,8, BigGAN[13] auf ImageNet 128x128 ca. 6,9.

X. GAN vs. Diffusionsmodelle: Paradigmenwechsel

Im Jahr 2021 veroeffentlichten Dhariwal und Nichol[16] die wegweisende Arbeit „Diffusionsmodelle Beat GANs on Image Synthesis", die zeigte, dass Diffusionsmodelle die besten GANs im FID umfassend uebertreffen. Doch das bedeutet nicht, dass GAN veraltet ist:

DimensionGANDiffusionsmodelle
GenerierungsgeschwindigkeitEin Forward Pass (~20ms)Dutzende bis Hunderte Iterationsschritte (~2-10s)
TrainingsstabilitaetAdversariales Training instabilEinfaches Denoising-Ziel, stabil
ModenabdeckungAnfaellig fuer Mode CollapseBessere Verteilungsabdeckung
BildqualitaetExtrem hoch (StyleGAN-Reihe)Extrem hoch (Imagen, DALL-E 3)
SteuerbarkeitW-Raum von StyleGANTextbedingte Guidance
AnwendungsszenarienEchtzeit-Rendering, Video, Super-ResolutionText-zu-Bild-Generierung, Bearbeitung

In Szenarien, die Echtzeit-Generierung erfordern (Spiele, Video, interaktive Anwendungen), bleibt die Single-Pass-Inferenzgeschwindigkeit von GAN unerreicht.

XI. Hands-on Lab 1: DCGAN Handschriftengenerierung (Google Colab)

Das folgende Experiment implementiert DCGAN von Grund auf, trainiert es auf MNIST zur Generierung handgeschriebener Ziffern und berechnet FID-Scores zur Ueberwachung der Trainingsqualitaet.

# ============================================================
# Lab 1: DCGAN — MNIST Handschriftengenerierung + FID-Bewertung
# Umgebung: Google Colab (GPU)
# ============================================================
# --- 0. Installation ---
!pip install -q torchvision scipy

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt
from torchvision.utils import make_grid
from scipy import linalg

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Device: {device}")

# --- 1. Daten laden ---
transform = transforms.Compose([
    transforms.Resize(32),
    transforms.ToTensor(),
    transforms.Normalize([0.5], [0.5])  # [-1, 1]
])
dataset = torchvision.datasets.MNIST(root='./data', train=True,
                                      transform=transform, download=True)
dataloader = torch.utils.data.DataLoader(dataset, batch_size=128,
                                          shuffle=True, num_workers=2)
print(f"Training images: {len(dataset)}")

# --- 2. Generator (DCGAN-Architektur) ---
nz = 100  # Dimension des latenten Vektors

class Generator(nn.Module):
    def __init__(self):
        super().__init__()
        self.main = nn.Sequential(
            # Eingabe: z [B, 100, 1, 1]
            nn.ConvTranspose2d(nz, 256, 4, 1, 0, bias=False),
            nn.BatchNorm2d(256),
            nn.ReLU(True),
            # [B, 256, 4, 4]
            nn.ConvTranspose2d(256, 128, 4, 2, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.ReLU(True),
            # [B, 128, 8, 8]
            nn.ConvTranspose2d(128, 64, 4, 2, 1, bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(True),
            # [B, 64, 16, 16]
            nn.ConvTranspose2d(64, 1, 4, 2, 1, bias=False),
            nn.Tanh()
            # [B, 1, 32, 32]
        )

    def forward(self, z):
        return self.main(z)

# --- 3. Diskriminator (DCGAN-Architektur) ---
class Discriminator(nn.Module):
    def __init__(self):
        super().__init__()
        self.main = nn.Sequential(
            # [B, 1, 32, 32]
            nn.Conv2d(1, 64, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            # [B, 64, 16, 16]
            nn.Conv2d(64, 128, 4, 2, 1, bias=False),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2, inplace=True),
            # [B, 128, 8, 8]
            nn.Conv2d(128, 256, 4, 2, 1, bias=False),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2, inplace=True),
            # [B, 256, 4, 4]
            nn.Conv2d(256, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, x):
        return self.main(x).view(-1)

# --- 4. Initialisierung ---
def weights_init(m):
    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)

G = Generator().to(device).apply(weights_init)
D = Discriminator().to(device).apply(weights_init)
print(f"G params: {sum(p.numel() for p in G.parameters()):,}")
print(f"D params: {sum(p.numel() for p in D.parameters()):,}")

criterion = nn.BCELoss()
optG = optim.Adam(G.parameters(), lr=2e-4, betas=(0.5, 0.999))
optD = optim.Adam(D.parameters(), lr=2e-4, betas=(0.5, 0.999))

fixed_noise = torch.randn(64, nz, 1, 1, device=device)

# --- 5. Vereinfachte FID-Berechnung ---
def compute_fid(real_images, fake_images, n_features=64):
    """Vereinfachte FID: Verwendet Pixel-Merkmale statt Inception"""
    real_flat = real_images.view(real_images.size(0), -1).cpu().numpy()
    fake_flat = fake_images.view(fake_images.size(0), -1).cpu().numpy()

    mu_r, sigma_r = real_flat.mean(axis=0), np.cov(real_flat, rowvar=False)
    mu_f, sigma_f = fake_flat.mean(axis=0), np.cov(fake_flat, rowvar=False)

    diff = mu_r - mu_f
    covmean = linalg.sqrtm(sigma_r @ sigma_f)
    if np.iscomplexobj(covmean):
        covmean = covmean.real

    fid = diff @ diff + np.trace(sigma_r + sigma_f - 2 * covmean)
    return fid

# --- 6. Trainingsschleife ---
n_epochs = 25
G_losses, D_losses, fid_scores = [], [], []

print("\nTraining DCGAN...")
for epoch in range(n_epochs):
    for i, (real, _) in enumerate(dataloader):
        B = real.size(0)
        real = real.to(device)
        real_label = torch.ones(B, device=device)
        fake_label = torch.zeros(B, device=device)

        # --- Diskriminator trainieren ---
        D.zero_grad()
        out_real = D(real)
        loss_real = criterion(out_real, real_label)

        noise = torch.randn(B, nz, 1, 1, device=device)
        fake = G(noise)
        out_fake = D(fake.detach())
        loss_fake = criterion(out_fake, fake_label)

        loss_D = loss_real + loss_fake
        loss_D.backward()
        optD.step()

        # --- Generator trainieren ---
        G.zero_grad()
        out_fake2 = D(fake)
        loss_G = criterion(out_fake2, real_label)
        loss_G.backward()
        optG.step()

    G_losses.append(loss_G.item())
    D_losses.append(loss_D.item())

    # FID berechnen (alle 5 Epochen)
    if (epoch + 1) % 5 == 0:
        G.eval()
        with torch.no_grad():
            fake_sample = G(torch.randn(500, nz, 1, 1, device=device))
        real_sample = torch.stack([dataset[i][0] for i in range(500)]).to(device)
        fid = compute_fid(real_sample, fake_sample)
        fid_scores.append((epoch + 1, fid))
        print(f"Epoch {epoch+1}/{n_epochs}: G_loss={loss_G.item():.4f}, "
              f"D_loss={loss_D.item():.4f}, FID={fid:.1f}")
        G.train()

# --- 7. Ergebnisse visualisieren ---
G.eval()
with torch.no_grad():
    generated = G(fixed_noise).cpu()

fig, axes = plt.subplots(1, 3, figsize=(18, 5))

# Generierte Bilder
axes[0].imshow(make_grid(generated, nrow=8, normalize=True, padding=2).permute(1, 2, 0),
               cmap='gray')
axes[0].set_title('Generated Digits (Epoch 25)', fontsize=13)
axes[0].axis('off')

# Verlustkurven
axes[1].plot(G_losses, label='Generator', color='#0077b6')
axes[1].plot(D_losses, label='Discriminator', color='#b8922e')
axes[1].set_xlabel('Epoch')
axes[1].set_ylabel('Loss')
axes[1].set_title('Training Loss', fontsize=13)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

# FID-Kurve
if fid_scores:
    epochs_f, fids = zip(*fid_scores)
    axes[2].plot(epochs_f, fids, 'o-', color='#e63946', linewidth=2)
    axes[2].set_xlabel('Epoch')
    axes[2].set_ylabel('FID (lower = better)')
    axes[2].set_title('FID Score', fontsize=13)
    axes[2].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# --- 8. Interpolation im latenten Raum ---
z1 = torch.randn(1, nz, 1, 1, device=device)
z2 = torch.randn(1, nz, 1, 1, device=device)
alphas = torch.linspace(0, 1, 10)

interpolated = []
with torch.no_grad():
    for a in alphas:
        z = z1 * (1 - a) + z2 * a
        interpolated.append(G(z).cpu())

fig, axes = plt.subplots(1, 10, figsize=(20, 2))
for i, img in enumerate(interpolated):
    axes[i].imshow(img[0, 0], cmap='gray')
    axes[i].set_title(f'α={alphas[i]:.1f}', fontsize=9)
    axes[i].axis('off')
plt.suptitle('Latent Space Interpolation', fontsize=14)
plt.tight_layout()
plt.show()

print("Lab 1 Complete!")

XII. Hands-on Lab 2: SeqGAN Textsequenzgenerierung (Google Colab)

Das folgende Experiment implementiert das Kernkonzept von SeqGAN — Training des Generators mit Policy Gradient zur Erzeugung diskreter Token-Sequenzen, waehrend der Diskriminator die Sequenzqualitaet bewertet.

# ============================================================
# Lab 2: SeqGAN Konzeptimplementierung — Textsequenzgenerierung
# Umgebung: Google Colab (GPU oder CPU)
# ============================================================
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import matplotlib.pyplot as plt

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"Device: {device}")

# --- 1. Datenvorbereitung: Einfache englische Satzmuster ---
# Regelbasiert generierte „echte" Saetze zum Training des Diskriminators
templates = [
    "the cat sat on the mat",
    "the dog ran in the park",
    "a bird flew over the tree",
    "the fish swam in the lake",
    "a boy read in the room",
    "the girl sang on the stage",
    "a man walked to the store",
    "the sun set in the west",
    "a car drove on the road",
    "the wind blew through the door",
]

# Datensatz erweitern
import random
random.seed(42)
subjects = ["the cat", "the dog", "a bird", "the fish", "a boy",
            "the girl", "a man", "the sun", "a car", "the wind"]
verbs = ["sat", "ran", "flew", "swam", "read", "sang", "walked", "set", "drove", "blew"]
preps = ["on", "in", "over", "through", "to", "under", "near", "past", "by", "from"]
objects = ["the mat", "the park", "the tree", "the lake", "the room",
           "the stage", "the store", "the west", "the road", "the door"]

real_sentences = []
for _ in range(2000):
    s = f"{random.choice(subjects)} {random.choice(verbs)} {random.choice(preps)} {random.choice(objects)}"
    real_sentences.append(s)

# Vokabular erstellen
all_words = set()
for s in real_sentences:
    all_words.update(s.split())
vocab = {"<pad>": 0, "<bos>": 1, "<eos>": 2}
for w in sorted(all_words):
    vocab[w] = len(vocab)
idx2word = {v: k for k, v in vocab.items()}
vocab_size = len(vocab)
SEQ_LEN = 8  # <bos> + 6 words + <eos>

print(f"Vocab size: {vocab_size}, Seq len: {SEQ_LEN}")

def encode_sentence(s):
    tokens = [1] + [vocab.get(w, 0) for w in s.split()][:SEQ_LEN-2] + [2]
    tokens += [0] * (SEQ_LEN - len(tokens))
    return tokens

real_data = torch.tensor([encode_sentence(s) for s in real_sentences]).to(device)
print(f"Real data shape: {real_data.shape}")

# --- 2. Generator (Rekurrentes Neuronales Netz Policy Network) ---
class SeqGenerator(nn.Module):
    def __init__(self, vocab_size, emb_dim=64, hidden_dim=128):
        super().__init__()
        self.emb = nn.Embedding(vocab_size, emb_dim, padding_idx=0)
        self.lstm = nn.LSTM(emb_dim, hidden_dim, batch_first=True)
        self.fc = nn.Linear(hidden_dim, vocab_size)
        self.hidden_dim = hidden_dim

    def forward(self, x, hidden=None):
        emb = self.emb(x)
        out, hidden = self.lstm(emb, hidden)
        logits = self.fc(out)
        return logits, hidden

    def generate(self, batch_size, max_len=SEQ_LEN):
        """Autoregressive Sequenzgenerierung"""
        x = torch.full((batch_size, 1), 1, dtype=torch.long, device=device)  # <bos>
        hidden = None
        sequences = [x]
        log_probs_list = []

        for _ in range(max_len - 1):
            logits, hidden = self.forward(x, hidden)
            probs = F.softmax(logits[:, -1], dim=-1)
            dist = torch.distributions.Categorical(probs)
            x = dist.sample().unsqueeze(1)
            log_probs_list.append(dist.log_prob(x.squeeze(1)))
            sequences.append(x)

        sequences = torch.cat(sequences, dim=1)
        log_probs = torch.stack(log_probs_list, dim=1)
        return sequences, log_probs

# --- 3. Diskriminator (Convolutional Neural Network Sequenzklassifikator) ---
class SeqDiscriminator(nn.Module):
    def __init__(self, vocab_size, emb_dim=64):
        super().__init__()
        self.emb = nn.Embedding(vocab_size, emb_dim, padding_idx=0)
        self.convs = nn.ModuleList([
            nn.Conv1d(emb_dim, 32, k, padding=k//2) for k in [2, 3, 4]
        ])
        self.fc = nn.Linear(32 * 3, 1)
        self.drop = nn.Dropout(0.2)

    def forward(self, x):
        emb = self.emb(x).transpose(1, 2)  # [B, emb, L]
        conv_outs = [F.relu(conv(emb)).max(dim=2).values for conv in self.convs]
        out = torch.cat(conv_outs, dim=1)
        return torch.sigmoid(self.fc(self.drop(out))).squeeze(1)

# --- 4. Initialisierung ---
G = SeqGenerator(vocab_size).to(device)
D = SeqDiscriminator(vocab_size).to(device)
optG = torch.optim.Adam(G.parameters(), lr=1e-3)
optD = torch.optim.Adam(D.parameters(), lr=1e-3)
print(f"G params: {sum(p.numel() for p in G.parameters()):,}")
print(f"D params: {sum(p.numel() for p in D.parameters()):,}")

# --- 5. Vortraining des Generators (MLE) ---
print("\n--- Pre-training Generator (MLE) ---")
for epoch in range(30):
    idx = torch.randperm(len(real_data))[:256]
    batch = real_data[idx]
    logits, _ = G(batch[:, :-1])
    loss = F.cross_entropy(logits.reshape(-1, vocab_size), batch[:, 1:].reshape(-1),
                           ignore_index=0)
    optG.zero_grad()
    loss.backward()
    optG.step()
    if (epoch + 1) % 10 == 0:
        print(f"  Epoch {epoch+1}: MLE loss = {loss.item():.4f}")

# --- 6. Vortraining des Diskriminators ---
print("\n--- Pre-training Discriminator ---")
for epoch in range(20):
    idx = torch.randperm(len(real_data))[:128]
    real_batch = real_data[idx]
    with torch.no_grad():
        fake_batch, _ = G.generate(128)

    real_score = D(real_batch)
    fake_score = D(fake_batch)
    loss_D = -torch.log(real_score + 1e-8).mean() - torch.log(1 - fake_score + 1e-8).mean()
    optD.zero_grad()
    loss_D.backward()
    optD.step()
    if (epoch + 1) % 10 == 0:
        print(f"  Epoch {epoch+1}: D loss = {loss_D.item():.4f}, "
              f"D(real)={real_score.mean():.3f}, D(fake)={fake_score.mean():.3f}")

# --- 7. Adversariales Training (SeqGAN Policy Gradient) ---
print("\n--- Adversarial Training (SeqGAN) ---")
g_rewards_history, d_scores_history = [], []

for epoch in range(50):
    # --- G trainieren (Policy Gradient) ---
    fake_seqs, log_probs = G.generate(64)
    with torch.no_grad():
        rewards = D(fake_seqs)  # [B]

    # REINFORCE: Reward Baseline
    baseline = rewards.mean()
    pg_loss = -((rewards - baseline).unsqueeze(1) * log_probs).mean()

    optG.zero_grad()
    pg_loss.backward()
    nn.utils.clip_grad_norm_(G.parameters(), 5.0)
    optG.step()

    # --- D trainieren ---
    idx = torch.randperm(len(real_data))[:64]
    real_batch = real_data[idx]
    with torch.no_grad():
        fake_batch, _ = G.generate(64)

    real_score = D(real_batch)
    fake_score = D(fake_batch)
    loss_D = -torch.log(real_score + 1e-8).mean() - torch.log(1 - fake_score + 1e-8).mean()
    optD.zero_grad()
    loss_D.backward()
    optD.step()

    g_rewards_history.append(rewards.mean().item())
    d_scores_history.append(fake_score.mean().item())

    if (epoch + 1) % 10 == 0:
        print(f"  Epoch {epoch+1}: G_reward={rewards.mean():.3f}, "
              f"D(fake)={fake_score.mean():.3f}, D(real)={real_score.mean():.3f}")

# --- 8. Generierung und Bewertung ---
G.eval()
print("\n=== Generated Sentences ===")
with torch.no_grad():
    seqs, _ = G.generate(20)
    for seq in seqs:
        words = [idx2word.get(t.item(), "?") for t in seq if t.item() > 2]
        sentence = " ".join(words)
        score = D(seq.unsqueeze(0)).item()
        print(f"  [{score:.3f}] {sentence}")

# --- 9. Visualisierung des Trainingsverlaufs ---
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(g_rewards_history, color='#0077b6', linewidth=1.5)
axes[0].set_xlabel('Adversarial Epoch')
axes[0].set_ylabel('Average Reward')
axes[0].set_title('Generator Reward (D score on fake)', fontsize=13)
axes[0].grid(True, alpha=0.3)

axes[1].plot(d_scores_history, color='#b8922e', linewidth=1.5, label='D(fake)')
axes[1].axhline(y=0.5, color='#e63946', linestyle='--', alpha=0.7, label='Equilibrium')
axes[1].set_xlabel('Adversarial Epoch')
axes[1].set_ylabel('D(fake)')
axes[1].set_title('Discriminator Score on Fake Sequences', fontsize=13)
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# --- 10. Vergleich: Echte vs. generierte Saetze ---
print("\n=== Real vs Generated Comparison ===")
print("Real sentences:")
for s in real_sentences[:5]:
    print(f"  {s}")
print("\nGenerated sentences:")
with torch.no_grad():
    seqs, _ = G.generate(5)
    for seq in seqs:
        words = [idx2word.get(t.item(), "?") for t in seq if t.item() > 2]
        print(f"  {' '.join(words)}")

print("\nLab 2 Complete!")

XIII. Fazit und Ausblick

Die Geschichte von GAN gehoert zu den faszinierendsten Kapiteln des Deep Learning. Vom originalen Paper von Goodfellow im Jahr 2014[1] bis zum fotorealistischen Niveau von StyleGAN3[18] hat GAN in weniger als einem Jahrzehnt unsere Vorstellung davon, ob „Maschinen kreativ sein koennen", grundlegend veraendert.

Rueckblick auf die Kernentwicklung:

Mit Blick auf die Zukunft ist GAN keineswegs abgetreten. Arbeiten wie GigaGAN skalieren GAN auf Milliarden-Parameter-Groesse; Destillationstechniken lassen Diffusionsmodelle Ein-Schritt-Generierung lernen (im Wesentlichen ein GAN werden); und die Diskriminator-Idee von GAN wird breit in Alignment-Techniken wie RLHF integriert. Das adversariale Denken — zwei Systeme im Wettbewerb gemeinsam voranbringen — wird die Entwicklungsrichtung der KI weiterhin praegen.