- 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:
- Training des Diskriminators: G wird fixiert, D wird mit echten Samples und von G generierten gefaelschten Samples trainiert, um echt von falsch zu unterscheiden
- 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:
| Richtlinie | Generator | Diskriminator |
|---|---|---|
| Up-/Downsampling | Transponierte Faltung (Stride 2) | Strided Convolution (Stride 2) |
| Normierung | BatchNorm (ausser Ausgabeschicht) | BatchNorm (ausser Eingabeschicht) |
| Aktivierungsfunktion | ReLU (Ausgabe: Tanh) | LeakyReLU(0.2) |
| Vollvernetzte Schichten | Nicht verwendet | Nicht 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:
| Modell | Jahr | Wichtigste Innovation | Auswirkung |
|---|---|---|---|
| ProGAN[5] | 2018 | Progressives Training (4x4 → 1024x1024) | Erstmalige fotorealistische Gesichter in 1024² |
| StyleGAN[6] | 2019 | Mapping-Netzwerk + AdaIN-Stilinjektion | Feine, schichtweise Stilkontrolle |
| StyleGAN2[7] | 2020 | Weight Demodulation + Path Length Regularisierung | Beseitigung von Tropfen-Artefakten, glaetterer latenter Raum |
| StyleGAN3[18] | 2021 | Aliasing-Behebung, kontinuierliche Signalverarbeitung | Sub-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:
| Technik | Kernidee | Wirkung |
|---|---|---|
| Spektralnormierung[12] | Division jeder Gewichtsmatrix des Diskriminators durch ihren groessten Singulaerwert | Leichtgewichtige Lipschitz-Beschraenkung mit vernachlaessigbarem Rechenaufwand |
| Progressives Training[5] | Start mit niedriger Aufloesung, schrittweises Hinzufuegen von Schichten | Stabilere 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-Schichten | Reduktion von Mode Collapse |
| Zwei-Zeitskalen-Regel[15] | D und G verwenden unterschiedliche Lernraten | Theoretische Konvergenz zu einem lokalen Nash-Gleichgewicht |
IX. Bewertungsmetriken: Wie misst man die Generierungsqualitaet?
| Metrik | Berechnungsweise | Was wird gemessen? | Einschraenkungen |
|---|---|---|---|
| IS[14] | KL-Divergenz der Inception-Modell-Vorhersagen | Schaerfe + Diversitaet | Kein Vergleich mit der echten Verteilung |
| FID[15] | Frechet-Distanz der Inception-Merkmale zwischen echten und generierten Bildern | Qualitaet + Diversitaet + Naehe zur echten Verteilung | Benoetigt grosse Stichprobenmengen, Abhaengigkeit von Inception |
| LPIPS | Distanz im perzeptuellen Merkmalsraum | Perzeptuelle Aehnlichkeit | Paarweiser 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:
| Dimension | GAN | Diffusionsmodelle |
|---|---|---|
| Generierungsgeschwindigkeit | Ein Forward Pass (~20ms) | Dutzende bis Hunderte Iterationsschritte (~2-10s) |
| Trainingsstabilitaet | Adversariales Training instabil | Einfaches Denoising-Ziel, stabil |
| Modenabdeckung | Anfaellig fuer Mode Collapse | Bessere Verteilungsabdeckung |
| Bildqualitaet | Extrem hoch (StyleGAN-Reihe) | Extrem hoch (Imagen, DALL-E 3) |
| Steuerbarkeit | W-Raum von StyleGAN | Textbedingte Guidance |
| Anwendungsszenarien | Echtzeit-Rendering, Video, Super-Resolution | Text-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:
- Theoretische Grundlagen: Das Minimax-Spielframework[1] bot ein elegantes Paradigma fuer generative Modellierung
- Stabilisierungsdurchbruch: WGAN[3] und Spektralnormierung[12] verwandelten das Training von „Kunst" in „Ingenieurwesen"
- Qualitaetshoehepunkt: Die StyleGAN-Reihe[6][7] erreichte eine Generierungsqualitaet, die vom menschlichen Auge nicht mehr zu unterscheiden ist
- Anwendungsexplosion: Pix2Pix[9] und CycleGAN[10] eroeffneten endlose Moeglichkeiten in der Bilduebersetzung
- Paradigmenwettbewerb: Diffusionsmodelle[16] uebertrafen GAN in der Qualitaet, doch GAN behaelt seinen Geschwindigkeitsvorteil
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.



