Einleitung
Generative Adversarial Networks (GANs) gehören zu den spannendsten Entwicklungen im Bereich des Deep Learning. Sie ermöglichen es Computern, realistische Bilder, Musik oder sogar Texte zu generieren, die täuschend echt aussehen.
Das Hauptziel eines GANs ist es, eine künstliche Intelligenz zu trainieren, die lernen kann, neue Inhalte zu erstellen, die sich nicht von echten Daten unterscheiden. Das kann für viele Bereiche spannend sein, wie z. B.:
- Bilderzeugung (z. B. DeepFake-Technologie, Kunstwerke)
- Datenaugmentation (z. B. in der medizinischen Bildgebung)
- Stiltransfer (z. B. Cartoon-Bilder in Fotos umwandeln)
- Text-zu-Bild-Generierung (z. B. DALL·E)
Allerdings ist das Training von GANs extrem schwierig. Ohne die richtigen Methoden kann es passieren, dass das Modell nur Rauschen produziert oder immer wieder die gleichen Bilder erzeugt (Mode Collapse).
1. Was ist ein GAN?
Ein Generative Adversarial Network (GAN) besteht aus zwei neuronalen Netzen, die gegeneinander arbeiten. Man kann sich das wie ein Spiel zwischen einem Fälscher (Generator) und einem Detektiven (Diskriminator) vorstellen:
Erstes neuronales Netz: Generator
- Erstellt zufällige Bilder aus Rauschen und versucht, den Diskriminator zu täuschen.
Zweites neuronales Netz: Diskriminator
- Bewertet, ob ein Bild echt (aus dem Datensatz) oder gefälscht (vom Generator) ist.
Der Trainingsprozess
Der Trainingsprozess funktioniert wie folgt:
1) Der Generator produziert zufällige Bilder aus einer Zufallsverteilung.
2) Der Diskriminator bewertet diese Bilder, ob sie echt oder gefälscht sind.
3) Der Generator verbessert sich, indem er lernt, den Diskriminator zu täuschen.
4) Der Diskriminator verbessert sich, indem er lernt, echte von falschen Bildern zu unterscheiden.
Der Diskriminator hat dabei die einfachere Aufgabe. Er muss die Bilder nur in zwei Kategorien einteilen (eine sehr klassische Klassifkationsaufgabe für NNs). Daher verbessert er sich typischerweise schneller als der Generator (sein Training muss sogar verlangsamt werden). Das Training läuft so lange weiter, bis der Generator so gut ist, dass der trainierte Diskriminator nicht mehr zwischen echten und generierten Bildern unterscheiden kann.
2. Warum ist das Training von GANs so schwierig?
GANs gehören zu den schwersten Deep-Learning-Modellen, weil:
- Das Gleichgewicht zwischen Generator und Diskriminator entscheidend ist.
- Ist der Diskriminator zu stark, lernt der Generator nichts.
- Ist der Generator zu stark, verliert der Diskriminator an Bedeutung.
- Gradienten können zusammenbrechen → Der Generator produziert nur noch Rauschen.
- Mode Collapse → Der Generator produziert immer nur die gleiche Art von Bildern.
- Zu hohe oder zu niedrige Lernraten können das Training instabil machen.
Lösung für stabileres Training
– Batch-Normalisierung: Stabilisiert das Training und verhindert Explodierende Gradienten.
– Dropout im Diskriminator: Verhindert, dass der Diskriminator zu stark wird.
– tf.GradientTape()
für stabileres Training: Ermöglicht präzisere Gradientenberechnung.
– Adaptive Lernraten für Generator und Diskriminator: Hält das Training ausgeglichen.
3. GAN in TensorFlow – Schritt für Schritt
Für das Erzeugen von handgeschriebenen Zahlen (unter Nutzung des MNIST-Datensatzes), wird ein Deep Convolutional GAN (DCGAN) genutzt was für die Verarbeitung besser geeignet ist als ein Vanilla-NN.
1. Importieren der benötigten Bibliotheken
pythonCopyEditimport tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import numpy as np
import matplotlib.pyplot as plt
import time
2. Laden und Vorbereiten des MNIST-Datensatzes
pythonCopyEdit(train_images, _), (_, _) = keras.datasets.mnist.load_data()
train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
train_images = (train_images - 127.5) / 127.5 # Normalisierung auf [-1, 1]
BUFFER_SIZE = 60000
BATCH_SIZE = 256
# Batch- und Shuffle-Daten
train_dataset = tf.data.Dataset.from_tensor_slices(train_images).shuffle(BUFFER_SIZE).batch(BATCH_SIZE)
Warum müssen die Bilder normalisiert werden?
- Pixelwerte von MNIST liegen zwischen 0 und 255.
- Die Pixelwerte sollten aber zwischen -1 und 1 normalisiert werden, da der Generator ebenfalls Pixelwerte zwischen -1 und 1 in seinen Bildern erzeugt (wegen
tanh
als Aktivierungsfunktion in seiner letzten Schicht).
3. Generator-Modell (CNN)
pythonCopyEditdef build_generator():
model = keras.Sequential([
layers.Dense(7 * 7 * 256, use_bias=False, input_shape=(100,)),
layers.BatchNormalization(),
layers.LeakyReLU(),
layers.Reshape((7, 7, 256)),
layers.Conv2DTranspose(128, (5, 5), strides=(1, 1), padding='same', use_bias=False),
layers.BatchNormalization(),
layers.LeakyReLU(),
layers.Conv2DTranspose(64, (5, 5), strides=(2, 2), padding='same', use_bias=False),
layers.BatchNormalization(),
layers.LeakyReLU(),
layers.Conv2DTranspose(1, (5, 5), strides=(2, 2), padding='same', use_bias=False, activation='tanh')
])
return model
generator = build_generator()
Besondere Methoden im Generator:
Conv2DTranspose()
(Transponierte Convolution)- Vergrößert die Bilder in drei Schritten von
7x7
auf28x28 Pixel
. - Funktioniert wie das Gegenteil von
Conv2D()
. - Wird für Upsampling in GANs verwendet.
- Vergrößert die Bilder in drei Schritten von
BatchNormalization()
- Stabilisiert das Training, indem es Mittelwert & Varianz normalisiert.
LeakyReLU()
- Eine verbesserte Version von
ReLU
, die negative Eingangswerte nicht komplett auf 0 setzt.
- Eine verbesserte Version von

4. Diskriminator-Modell (CNN)
pythonCopyEditdef build_discriminator():
model = keras.Sequential([
layers.Conv2D(64, (5, 5), strides=(2, 2), padding='same', input_shape=[28, 28, 1]),
layers.LeakyReLU(),
layers.Dropout(0.3),
layers.Conv2D(128, (5, 5), strides=(2, 2), padding='same'),
layers.LeakyReLU(),
layers.Dropout(0.3),
layers.Flatten(),
layers.Dense(1)
])
return model
discriminator = build_discriminator()
Besondere Methoden im Diskriminator:
Dropout(0.3)
- Verhindert Overfitting, indem 30% der Neuronen deaktiviert werden.
Flatten()
- Wandelt 2D-Feature-Maps in einen 1D-Vektor um.
4. Verlustfunktionen & Optimierer
Das Herzstück eines jeden neuronalen Netzes ist die Verlustfunktion.
In GANs werden zwei verschiedene Verlustfunktionen benötigt:
- Eine für den Generator (er soll bessere Bilder erzeugen)
- Eine für den Diskriminator (er soll echte und gefälschte Bilder unterscheiden)
Binary Crossentropy (Kreuzentropie) als Verlustfunktion
pythonCopyEditcross_entropy = keras.losses.BinaryCrossentropy(from_logits=True)
Was ist Binary Crossentropy?
Binary Crossentropy ist eine mathematische Funktion, die misst, wie gut ein Modell echte vs. falsche Entscheidungen trifft.
In unserem Fall:
- Der Diskriminator klassifiziert Bilder als echt (1) oder gefälscht (0).
- Die Binary Crossentropy berechnet, wie falsch seine Vorhersagen sind.
- Der Generator möchte den Diskriminator täuschen, also ist sein Ziel, dass alle Fake-Bilder als echt (1) bewertet werden.
1. Verlustfunktion für den Diskriminator
pythonCopyEditdef discriminator_loss(real_output, fake_output):
real_loss = cross_entropy(tf.ones_like(real_output), real_output) # Echte Bilder sollten als 1 erkannt werden
fake_loss = cross_entropy(tf.zeros_like(fake_output), fake_output) # Falsche Bilder sollten als 0 erkannt werden
return real_loss + fake_loss
Erklärung:
- Der Diskriminator bekommt zwei Inputs:
- A) Echte Bilder aus dem Datensatz → Er sollte sie als 1 (echt) klassifizieren.
- B) Falsche Bilder vom Generator → Er sollte sie als 0 (Fake) klassifizieren.
- Wenn der Diskriminator falsche Vorhersagen macht, bekommt er eine hohe Verlustzahl.
- Sein Ziel ist es, diesen Verlust zu minimieren.
2. Verlustfunktion für den Generator
pythonCopyEditdef generator_loss(fake_output):
return cross_entropy(tf.ones_like(fake_output), fake_output) # Der Generator will, dass seine Bilder als echt (1) erkannt werden
Erklärung:
- Der Generator erzeugt als Output Fake-Bilder.
- Diese Fake-Bilder werden vom Diskriminator klassifiziert (entweder Fake (0) oder Echt (1)).
- Der Generator möchte, dass seine Fake-Bilder als echt erkannt werden.
- Deshalb nutzt er die Binary Crossentropy mit dem Label „1“, um den Diskriminator zu täuschen.
- Wenn der Diskriminator glaubt, das Bild sei echt, dann ist der Generator erfolgreich!
5. Optimierer für das Training
pythonCopyEditgenerator_optimizer = keras.optimizers.Adam(1e-4)
discriminator_optimizer = keras.optimizers.Adam(1e-4)
Warum Adam-Optimizer?
GANs brauchen spezielle Optimierer, um stabile Ergebnisse zu erzielen. Die Eigenschaften des Adam-Optimizers sind:
- Funktioniert gut mit nicht-stationären Daten (also Daten, deren Mittelwert, Varianz oder Verteilung sich mit der Zeit verändern. Dies passiert u.a. weil der Generator zuerst Rauschen erzeugt und später Zahlen).
- Anpassbare Lernrate → Verhindert, dass das Modell zu schnell oder zu langsam lernt.
- Momentum-Mechanismus → Glättet die Gradienten und verhindert starke Schwankungen.
- Gut für GANs geeignet → Stabilisiert das Training besser als SGD oder RMSprop.
6. Trainings-Loop mit tf.GradientTape()
Das Training eines GANs ist komplexer als bei normalen neuronalen Netzen, weil zwei Netze gleichzeitig optimiert werden.
Daher wird eine spezielle Methode benötigt, um den Gradientenfluss zu überwachen:
pythonCopyEdit@tf.function
def train_step(images):
noise = tf.random.normal([BATCH_SIZE, 100])
with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
generated_images = generator(noise, training=True)
real_output = discriminator(images, training=True)
fake_output = discriminator(generated_images, training=True)
gen_loss = generator_loss(fake_output)
disc_loss = discriminator_loss(real_output, fake_output)
gradients_gen = gen_tape.gradient(gen_loss, generator.trainable_variables)
gradients_disc = disc_tape.gradient(disc_loss, discriminator.trainable_variables)
generator_optimizer.apply_gradients(zip(gradients_gen, generator.trainable_variables))
discriminator_optimizer.apply_gradients(zip(gradients_disc, discriminator.trainable_variables))
return gen_loss, disc_loss
TensorFlow nutzt tf.GradientTape()
, um automatische Gradientenberechnungen zu ermöglichen.
Warum ist das wichtig?
- In GANs müssen zwei Modelle gleichzeitig geupdated werden.
- TensorFlow speichert alle Berechnungen und ermöglicht dann eine automatische Gradientenberechnung.
- Ohne
tf.GradientTape()
müssten die Gradienten manuell berechnet werden!
Ablauf
- Ein zufälliges
noise
-Tensor wird erstellt. - Mit
GradientTape()
werden alle Berechnungen überwacht. - Der Generator erzeugt Fake-Bilder aus Rauschen.
- Der Diskriminator klassifiziert echte und Fake-Bilder.
- Verlust wird für Generator und Diskriminator berechnet.
- TensorFlow berechnet automatisch die Gradienten.
- Die Optimierer aktualisieren die Gewichte.
7. Das Training
pythonCopyEditdef train(dataset, epochs):
for epoch in range(epochs):
for image_batch in dataset:
g_loss, d_loss = train_step(image_batch)
if epoch % 1000 == 0:
print(f"Epoch {epoch}, Generator Loss: {g_loss:.4f}, Discriminator Loss: {d_loss:.4f}")
generate_and_save_images(generator, epoch + 1, seed)
# Generierte Bilder speichern
def generate_and_save_images(model, epoch, test_input):
predictions = model(test_input, training=False)
fig = plt.figure(figsize=(4, 4))
for i in range(predictions.shape[0]):
plt.subplot(4, 4, i + 1)
plt.imshow(predictions[i, :, :, 0] * 127.5 + 127.5, cmap='gray')
plt.axis('off')
plt.savefig(f'image_at_epoch_{epoch}.png')
plt.show()
# **Start des Trainings**
EPOCHS = 10000
train(train_dataset, EPOCHS)
Warum sollte man die Bilder während des Trainings speichern?
- Um zu sehen, ob das GAN Fortschritte macht.
- Wenn die Bilder schärfer werden, ist das Training erfolgreich!
- Falls nur Rauschen generiert wird, muss das Modell neu angepasst werden.
Ergebnisse innerhalb von 90 Minuten Training (Google Colab mit GPU)
Epochen-Counter steht in der Klammer.

Die ersten 10 Epochen sind noch sehr unklar, aber danach beginnen die Bilder nach Handschrift auszusehen (wenn auch nicht nach Zahlen).

Ab ca. Epoche 25 kann man in einigen Feldern Zahlen erkennen. Sie sehen zwar handschriftlich aus, aber die Handschrift wirkt sehr krumm bzw. wie die eines Erstklässers.

Bei Epoche 50 ändern sich die Zeichen nur noch langsam, aber noch sehen nicht alle Zeichen nach Zahlen aus.

Bei Epoche 100 sind nur noch leichte Verbesserungen erkennbar (z.B. gegenüber Epoche 50).

Nach 90 Minuten Training mit einer GPU wurde das Training bei Epoche 434 beendet. Die Verbesserungen sind nur noch marginal, wenn überhaupt noch erkennbar. Allerdings sehen nicht alle Bilder nach einer Zahl aus (z.B. bei Bild 4 in Epoche 434 / roter Rahmen). Wenn man hier weiter optimieren möchte, müsste man beim Diskriminator ansetzen, so dass er solche Bilder sauber als Fake-Bilder klassifiziert.