Kapitel 4. Generative adversarische Netze

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

In 2014 präsentierten Ian Goodfellow et al. einen Vortrag mit dem Titel "Generative Adversarial Nets"1 auf der Konferenz Neural Information Processing Systems (NeurIPS) in Montreal vor. Die Einführung generativer adversarialer Netze (oder GANs, wie sie allgemein genannt werden) wird heute als entscheidender Wendepunkt in der Geschichte der generativen Modellierung angesehen, da die in diesem Papier vorgestellten Kernideen einige der erfolgreichsten und beeindruckendsten generativen Modelle hervorgebracht haben, die jemals entwickelt wurden.

In diesem Kapitel werden wir zunächst die theoretischen Grundlagen von GANs erläutern und dann sehen, wie wir mit Keras unser eigenes GAN bauen können.

Einführung

Beginnen wir mit einer kurzen Geschichte, um einige der grundlegenden Konzepte zu veranschaulichen, die im GAN-Trainingsprozess verwendet werden.

Die Geschichte von Brickki Ziegeln und den Fälschern beschreibt den Trainingsprozess eines generativen kontradiktorischen Netzwerks.

Ein GAN ist ein Kampf zwischen zwei Gegnern, dem Generator und dem Diskriminator. Der Generator versucht, zufälliges Rauschen in Beobachtungen umzuwandeln, die so aussehen, als ob sie aus dem Originaldatensatz stammen, und der Diskriminator versucht vorherzusagen, ob eine Beobachtung aus dem Originaldatensatz stammt oder eine der Fälschungen des Generators ist. Beispiele für die Eingänge und Ausgänge der beiden Netzwerke sind in Abbildung 4-2 dargestellt.

Abbildung 4-2. Eingänge und Ausgänge der beiden Netzwerke in einem GAN

Zu Beginn des Prozesses gibt der Generator verrauschte Bilder aus und der Diskriminator sagt sie zufällig voraus. Der Schlüssel zu GANs liegt in der Art und Weise, wie wir die beiden Netze abwechselnd trainieren, so dass der Generator immer geschickter darin wird, den Diskriminator zu täuschen, und der Diskriminator sich anpassen muss, um weiterhin in der Lage zu sein, korrekt zu erkennen, welche Beobachtungen gefälscht sind. Das bringt den Generator dazu, neue Wege zu finden, den Diskriminator zu täuschen, und so geht der Kreislauf weiter.

Deep Convolutional GAN (DCGAN)

Um in Aktion zu sehen, bauen wir unseren ersten GAN in Keras, um Bilder von Ziegelsteinen zu erzeugen.

Wir werden eine der ersten großen Arbeiten über GANs, "Unsupervised Representation Learning with Deep Convolutional Generative AdversarialNetworks", aufmerksam verfolgen.2 In dieser Arbeit aus dem Jahr 2015 zeigen die Autoren, wie man ein Deep Convolutional GAN entwickelt, um realistische Bilder aus einer Vielzahl von Datensätzen zu erzeugen. Außerdem führen sie mehrere Änderungen ein, die die Qualität der erzeugten Bilder deutlich verbessern.

Ausführen des Codes für dieses Beispiel

Den Code für dieses Beispiel findest du im Jupyter-Notizbuch unter notebooks/04_gan/01_dcgan/dcgan.ipynb im Buch-Repository.

Der Datensatz für Ziegelsteine

Zuerst musst du die Trainingsdaten herunterladen. Wir werden den Datensatz Images of LEGO Bricks verwenden, der bei Kaggle erhältlich ist. Dabei handelt es sich um eine computerberechnete Sammlung von 40.000 fotografischen Bildern von 50 verschiedenen Spielzeugsteinen, die aus verschiedenen Blickwinkeln aufgenommen wurden. Einige Beispielbilder von Brickki-Produkten sind in Abbildung 4-3 zu sehen.

Abbildung 4-3. Beispiele für Bilder aus dem Bricks-Datensatz

Du kannst den Datensatz herunterladen, indem du das Kaggle-Datensatz-Downloader-Skript im Buch-Repository ausführst, wie in Beispiel 4-1 gezeigt. Dadurch werden die Bilder und die dazugehörigen Metadaten lokal im Ordner /data gespeichert.

Beispiel 4-1. Herunterladen des Datensatzes "Bricks
bash scripts/download_kaggle_data.sh joosthazelzet lego-brick-images

Wir verwenden die Keras-Funktion image_dataset_from_directory, um einen TensorFlow-Datensatz zu erstellen, der auf das Verzeichnis verweist, in dem die Bilder gespeichert sind, wie in Beispiel 4-2 gezeigt. So können wir die Bilder nur bei Bedarf in den Speicher einlesen (z. B. während des Trainings), so dass wir mit großen Datensätzen arbeiten können und uns keine Sorgen machen müssen, dass der gesamte Datensatz in den Speicher passt. Außerdem werden die Bilder auf 64 × 64 verkleinert, wobei zwischen den Pixelwerten interpoliert wird.

Beispiel 4-2. Erstellen eines TensorFlow-Datensatzes aus Bilddateien in einem Verzeichnis
train_data = utils.image_dataset_from_directory(
    "/app/data/lego-brick-images/dataset/",
    labels=None,
    color_mode="grayscale",
    image_size=(64, 64),
    batch_size=128,
    shuffle=True,
    seed=42,
    interpolation="bilinear",
)

Die Originaldaten sind im Bereich [0, 255] skaliert, um die Pixelintensität zu bezeichnen. Beim Training von GANs skalieren wir die Daten in den Bereich [-1, 1] um, damit wir die tanh-Aktivierungsfunktion auf der letzten Schicht des Generators verwenden können, die tendenziell stärkere Gradienten liefert als die Sigmoid-Funktion(Beispiel 4-3).

Beispiel 4-3. Vorverarbeitung des Bricks-Datensatzes
def preprocess(img):
    img = (tf.cast(img, "float32") - 127.5) / 127.5
    return img

train = train_data.map(lambda x: preprocess(x))

Schauen wir uns nun an, wie wir den Diskriminator aufbauen.

Der Diskriminer

Das Ziel des Diskriminators ist es, vorherzusagen, ob ein Bild echt oder gefälscht ist. Da es sich um ein überwachtes Bildklassifizierungsproblem handelt, können wir eine ähnliche Architektur wie in Kapitel 2 verwenden: gestapelte Faltungsschichten mit einem einzigen Ausgangsknoten.

Die vollständige Architektur des Diskriminators, den wir bauen werden, ist in Tabelle 4-1 dargestellt.

Tabelle 4-1. Modellzusammenfassung des Diskriminators
Schicht (Typ) Form der Ausgabe Param #

InputLayer

(Keine, 64, 64, 1)

0

Conv2D

(Keine, 32, 32, 64)

1,024

LeakyReLU

(Keine, 32, 32, 64)

0

Aussteiger

(Keine, 32, 32, 64)

0

Conv2D

(Keine, 16, 16, 128)

131,072

BatchNormalisierung

(Keine, 16, 16, 128)

512

LeakyReLU

(Keine, 16, 16, 128)

0

Ausstieg

(Keine, 16, 16, 128)

0

Conv2D

(Keine, 8, 8, 256)

524,288

BatchNormalisierung

(Keine, 8, 8, 256)

1,024

LeakyReLU

(Keine, 8, 8, 256)

0

Ausstieg

(Keine, 8, 8, 256)

0

Conv2D

(Keine, 4, 4, 512)

2,097,152

BatchNormalisierung

(Keine, 4, 4, 512)

2,048

LeakyReLU

(Keine, 4, 4, 512)

0

Aussteiger

(Keine, 4, 4, 512)

0

Conv2D

(Keine, 1, 1, 1, 1)

8,192

Abflachen

(Keine, 1)

0

Total Parameter

2,765,312

Trainierbare Parameter

2,763,520

Nicht-trainierbare Parameter

1,792

Der Keras-Code zur Erstellung des Diskriminators ist in Beispiel 4-4 zu finden.

Beispiel 4-4. Der Diskriminator
discriminator_input = layers.Input(shape=(64, 64, 1)) 1
x = layers.Conv2D(64, kernel_size=4, strides=2, padding="same", use_bias = False)(
    discriminator_input
) 2
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
    128, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum = 0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
    256, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum = 0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
    512, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum = 0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Dropout(0.3)(x)
x = layers.Conv2D(
    1,
    kernel_size=4,
    strides=1,
    padding="valid",
    use_bias = False,
    activation = 'sigmoid'
)(x)
discriminator_output = layers.Flatten()(x) 3

discriminator = models.Model(discriminator_input, discriminator_output) 4
1

Definiere die Input Ebene des Diskriminators (das Bild).

2

Staple die Schichten Conv2D übereinander, dazwischen liegen die Schichten BatchNormalization, LeakyReLU activation und Dropout.

3

Flache die letzte Faltungsschicht ab - zu diesem Zeitpunkt ist die Form des Tensors 1 × 1 × 1, so dass eine letzte Dense Schicht nicht mehr nötig ist.

4

Das Keras-Modell, das den Diskriminator definiert - ein Modell, das ein Eingabebild nimmt und eine einzelne Zahl zwischen 0 und 1 ausgibt.

Beachte, wie wir einen Stride von 2 in einigen der Conv2D Schichten verwenden, um die räumliche Form des Tensors zu reduzieren, während er das Netzwerk durchläuft (64 im Originalbild, dann 32, 16, 8, 4 und schließlich 1), während wir die Anzahl der Kanäle erhöhen (1 im Graustufen-Eingangsbild, dann 64, 128, 256 und schließlich 512), bevor er zu einer einzigenVorhersage zusammenfällt.

Wir verwenden eine Sigmoid-Aktivierung in der letzten Schicht Conv2D, um eine Zahl zwischen 0 und 1 auszugeben.

Der Generator

Lass uns den Generator erstellen. Die Eingabe für den Generator ist ein Vektor, der aus einer multivariaten Standardnormalverteilung gezogen wird. Die Ausgabe ist ein Bild, das die gleiche Größe hat wie ein Bild in den ursprünglichen Trainingsdaten.

Diese Beschreibung erinnert dich vielleicht an den Decoder in einem Variations-Autoencoder. In der Tat erfüllt der Generator eines GAN genau denselben Zweck wie der Decoder eines VAE: Er wandelt einen Vektor im latenten Raum in ein Bild um. Das Konzept des Mappings von einem latenten Raum zurück in den ursprünglichen Bereich ist in der generativen Modellierung sehr verbreitet, da es uns die Möglichkeit gibt, Vektoren im latenten Raum zu manipulieren, um High-Level-Merkmale von Bildern im ursprünglichen Bereich zu verändern.

Die Architektur des Generators, den wir bauen werden, ist in Tabelle 4-2 dargestellt.

Tabelle 4-2. Modellzusammenfassung des Generators
Schicht (Typ) Form der Ausgabe Param #

InputLayer

(Keine, 100)

0

Umgestalten

(Keine, 1, 1, 100)

0

Conv2DTranspose

(Keine, 4, 4, 512)

819,200

BatchNormalisierung

(Keine, 4, 4, 512)

2,048

ReLU

(Keine, 4, 4, 512)

0

Conv2DTranspose

(Keine, 8, 8, 256)

2,097,152

BatchNormalisierung

(Keine, 8, 8, 256)

1,024

ReLU

(Keine, 8, 8, 256)

0

Conv2DTranspose

(Keine, 16, 16, 128)

524,288

BatchNormalisierung

(Keine, 16, 16, 128)

512

ReLU

(Keine, 16, 16, 128)

0

Conv2DTranspose

(Keine, 32, 32, 64)

131,072

BatchNormalisierung

(Keine, 32, 32, 64)

256

ReLU

(Keine, 32, 32, 64)

0

Conv2DTranspose

(Keine, 64, 64, 1)

1,024

Total Parameter

3,576,576

Trainierbare Parameter

3,574,656

Nicht-trainierbare Parameter

1,920

Der Code für die Erstellung des Generators ist in Beispiel 4-5 angegeben.

Beispiel 4-5. Der Generator
generator_input = layers.Input(shape=(100,)) 1
x = layers.Reshape((1, 1, 100))(generator_input) 2
x = layers.Conv2DTranspose(
    512, kernel_size=4, strides=1, padding="valid", use_bias = False
)(x) 3
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    256, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    128, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
x = layers.Conv2DTranspose(
    64, kernel_size=4, strides=2, padding="same", use_bias = False
)(x)
x = layers.BatchNormalization(momentum=0.9)(x)
x = layers.LeakyReLU(0.2)(x)
generator_output = layers.Conv2DTranspose(
    1,
    kernel_size=4,
    strides=2,
    padding="same",
    use_bias = False,
    activation = 'tanh'
)(x) 4
generator = models.Model(generator_input, generator_output) 5
1

Definiere die Input Schicht des Generators - einen Vektor der Länge 100.

2

Wir verwenden eine Reshape Schicht, um einen 1 × 1 × 100 Tensor zu erhalten, so dass wir mit den Faltungstranspositionsoperationen beginnen können.

3

Wir lassen es durch vier Conv2DTranspose Schichten laufen, dazwischen liegen BatchNormalization und LeakyReLU Schichten.

4

Die letzte Schicht Conv2DTranspose verwendet eine tanh-Aktivierungsfunktion, um die Ausgabe in den Bereich [-1, 1] zu transformieren, damit sie dem ursprünglichen Bildbereich entspricht.

5

Das Keras-Modell, das den Generator definiert - ein Modell, das einen Vektor der Länge 100 annimmt und einen Tensor der Form [64, 64, 1] ausgibt.

Beachte, wie wir einen Stride von 2 in einigen der Conv2DTranspose Schichten verwenden, um die räumliche Form des Tensors zu erhöhen, während er das Netzwerk durchläuft (1 im ursprünglichen Vektor, dann 4, 8, 16, 32 und schließlich 64), während wir die Anzahl der Kanäle verringern (512, dann 256, 128, 64 und schließlich 1, um die Graustufenausgabe anzupassen).

Ausbildung des DCGAN

Wie wir unter gesehen haben, sind die Architekturen des Generators und des Diskriminators in einem DCGAN sehr einfach und unterscheiden sich nicht so sehr von den VAE-Modellen, die wir in Kapitel 3 betrachtet haben. Der Schlüssel zum Verständnis von GANs liegt im Verständnis des Trainingsprozesses für den Generator und den Diskriminator.

Wir können den Diskriminator trainieren, indem wir eine Trainingsmenge erstellen, bei der einige der Bilder echte Beobachtungen aus der Trainingsmenge und einige gefälschte Ausgaben aus dem Generator sind. Wir behandeln dies dann als ein überwachtes Lernproblem, bei dem die Kennzeichnungen 1 für die echten Bilder und 0 für die gefälschten Bilder sind, mit binärer Kreuzentropie alsVerlustfunktion.

Wie sollen wir den Generator trainieren? Wir müssen einen Weg finden, jedes erzeugte Bild zu bewerten, damit er sich auf Bilder mit hoher Punktzahl konzentrieren kann. Zum Glück haben wir einen Diskriminator, der genau das kann! Wir können eine Reihe von Bildern erzeugen und diese durch den Diskriminator leiten, um für jedes Bild eine Bewertung zu erhalten. Die Verlustfunktion für den Generator ist dann einfach die binäre Kreuzentropie zwischen diesen Wahrscheinlichkeiten und einem Vektor aus Einsen, denn wir wollen den Generator darauf trainieren, Bilder zu erzeugen, die der Diskriminator für echt hält.

Entscheidend ist, dass wir diese beiden Netze abwechselnd trainieren und sicherstellen, dass wir immer nur die Gewichte eines Netzes aktualisieren. Während des Generator-Trainings werden zum Beispiel nur die Gewichte des Generators aktualisiert. Wenn wir zulassen würden, dass sich die Gewichte des Diskriminators ebenfalls ändern, würde sich der Diskriminator so anpassen, dass er die erzeugten Bilder mit höherer Wahrscheinlichkeit als echt vorhersagt, was nicht das gewünschte Ergebnis ist. Wir wollen, dass die generierten Bilder nahe bei 1 (echt) liegen, weil der Generator stark ist, nicht weil der Diskriminator schwach ist.

Ein Diagramm des Trainingsprozesses für den Diskriminator und den Generator ist in Abbildung 4-5 dargestellt.

Abbildung 4-5. Training des DCGAN - graue Kästen zeigen an, dass die Gewichte während des Trainings eingefroren sind

Keras bietet uns die Möglichkeit, eine eigene train_step Funktion zu erstellen, um diese Logik zu implementieren. Beispiel 4-7 zeigt die vollständige Modellklasse DCGAN.

Beispiel 4-7. Kompilieren des DCGAN
class DCGAN(models.Model):
    def __init__(self, discriminator, generator, latent_dim):
        super(DCGAN, self).__init__()
        self.discriminator = discriminator
        self.generator = generator
        self.latent_dim = latent_dim

    def compile(self, d_optimizer, g_optimizer):
        super(DCGAN, self).compile()
        self.loss_fn = losses.BinaryCrossentropy() 1
        self.d_optimizer = d_optimizer
        self.g_optimizer = g_optimizer
        self.d_loss_metric = metrics.Mean(name="d_loss")
        self.g_loss_metric = metrics.Mean(name="g_loss")

    @property
    def metrics(self):
        return [self.d_loss_metric, self.g_loss_metric]

    def train_step(self, real_images):
        batch_size = tf.shape(real_images)[0]
        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim)
        ) 2

        with tf.GradientTape() as gen_tape, tf.GradientTape() as disc_tape:
            generated_images = self.generator(
                random_latent_vectors, training = True
            ) 3
            real_predictions = self.discriminator(real_images, training = True) 4
            fake_predictions = self.discriminator(
                generated_images, training = True
            ) 5

            real_labels = tf.ones_like(real_predictions)
            real_noisy_labels = real_labels + 0.1 * tf.random.uniform(
                tf.shape(real_predictions)
            )
            fake_labels = tf.zeros_like(fake_predictions)
            fake_noisy_labels = fake_labels - 0.1 * tf.random.uniform(
                tf.shape(fake_predictions)
            )

            d_real_loss = self.loss_fn(real_noisy_labels, real_predictions)
            d_fake_loss = self.loss_fn(fake_noisy_labels, fake_predictions)
            d_loss = (d_real_loss + d_fake_loss) / 2.0 6

            g_loss = self.loss_fn(real_labels, fake_predictions) 7

        gradients_of_discriminator = disc_tape.gradient(
            d_loss, self.discriminator.trainable_variables
        )
        gradients_of_generator = gen_tape.gradient(
            g_loss, self.generator.trainable_variables
        )

        self.d_optimizer.apply_gradients(
            zip(gradients_of_discriminator, discriminator.trainable_variables)
        ) 8
        self.g_optimizer.apply_gradients(
            zip(gradients_of_generator, generator.trainable_variables)
        )

        self.d_loss_metric.update_state(d_loss)
        self.g_loss_metric.update_state(g_loss)

        return {m.name: m.result() for m in self.metrics}

dcgan = DCGAN(
    discriminator=discriminator, generator=generator, latent_dim=100
)

dcgan.compile(
    d_optimizer=optimizers.Adam(
        learning_rate=0.0002, beta_1 = 0.5, beta_2 = 0.999
    ),
    g_optimizer=optimizers.Adam(
        learning_rate=0.0002, beta_1 = 0.5, beta_2 = 0.999
    ),
)

dcgan.fit(train, epochs=300)
1

Die Verlustfunktion für den Generator und den Diskriminator ist BinaryCrossentropy.

2

Um das Netzwerk zu trainieren, entnimmst du zunächst eine Reihe von Vektoren aus einer multivariaten Standardnormalverteilung.

3

Als Nächstes lässt du sie durch den Generator laufen, um eine Reihe von Bildern zu erzeugen.

4

Bitte nun den Diskriminator, die Echtheit des Stapels von echten Bildern vorherzusagen.

5

...und den Stapel der erzeugten Bilder.

6

Der Unterscheidungsverlust ist die durchschnittliche binäre Kreuzentropie über die echten Bilder (mit dem Label 1) und die gefälschten Bilder (mit dem Label 0).

7

Der Generatorverlust ist die binäre Kreuzentropie zwischen den Diskriminatorvorhersagen für die erzeugten Bilder und einem Label von 1.

8

Aktualisiere die Gewichte des Diskriminators und des Generators separat.

Der Diskriminator und der Generator kämpfen ständig um die Vorherrschaft, was den DCGAN-Trainingsprozess instabil machen kann. Im Idealfall findet der Trainingsprozess ein Gleichgewicht, das es dem Generator ermöglicht, sinnvolle Informationen vom Diskriminator zu lernen und die Qualität der Bilder zu verbessern. Nach einer ausreichenden Anzahl von Epochen neigt der Diskriminator dazu, zu dominieren, wie in Abbildung 4-6 zu sehen ist. Das ist jedoch kein Problem, da der Generator zu diesem Zeitpunkt vielleicht schon gelernt hat, ausreichend hochwertige Bilder zu erzeugen.

Abbildung 4-6. Verlust und Genauigkeit von Diskriminator und Generator während des Trainings

Hinzufügen von Rauschen zu den Labels

Ein nützlicher Trick beim Training von GANs ist es, den Trainingslabels eine kleine Menge an Zufallsrauschen hinzuzufügen. Das hilft, die Stabilität des Trainingsprozesses zu verbessern und die erzeugten Bilder zu schärfen. Die Glättung der Beschriftungen dient dazu, den Diskriminator zu zähmen, so dass er mit einer anspruchsvolleren Aufgabe konfrontiert wird und den Generator nicht überfordert.

Analyse des DCGAN

Wenn du unter die Bilder betrachtest, die der Generator zu bestimmten Epochen während des Trainings erzeugt hat(Abb. 4-7), wird deutlich, dass der Generator immer geschickter darin wird, Bilder zu erzeugen, die auch aus der Trainingsmenge stammen könnten.

Abbildung 4-7. Ausgabe des Generators zu bestimmten Epochen während des Trainings

Es grenzt an ein Wunder, dass ein neuronales Netzwerk in der Lage ist, zufälliges Rauschen in etwas Sinnvolles umzuwandeln. Es lohnt sich, daran zu erinnern, dass wir dem Modell außer den rohen Pixeln keine weiteren Merkmale zur Verfügung gestellt haben, so dass es ganz allein Konzepte wie das Zeichnen von Schatten, Quadern und Kreisen entwickeln muss.

Eine weitere Voraussetzung für ein erfolgreiches generatives Modell ist, dass es nicht nur Bilder aus der Trainingsmenge reproduziert. Um dies zu überprüfen, können wir das Bild aus der Trainingsmenge finden, das einem bestimmten generierten Beispiel am nächsten kommt. Ein gutes Maß für den Abstand ist der L1-Abstand, der wie folgt definiert ist:

def compare_images(img1, img2):
    return np.mean(np.abs(img1 - img2))

Abbildung 4-8 zeigt die nächstgelegenen Beobachtungen in der Trainingsmenge für eine Auswahl der erzeugten Bilder. Wir sehen, dass die generierten Bilder zwar eine gewisse Ähnlichkeit mit der Trainingsmenge aufweisen, aber nicht identisch sind. Das zeigt, dass der Generator diese übergeordneten Merkmale verstanden hat und Beispiele generieren kann, die sich von denen unterscheiden, die er bereits gesehen hat.

Abbildung 4-8. Engste Übereinstimmungen der erzeugten Bilder aus der Trainingsmenge

GAN-Schulung: Tipps und Tricks

GANs sind zwar ein großer Durchbruch für die generative Modellierung, aber sie sind auch bekanntermaßen schwierig zu trainieren. In diesem Abschnitt werden wir einige der häufigsten Probleme und Herausforderungen beim Training von GANs sowie mögliche Lösungen vorstellen. Im nächsten Abschnitt werden wir uns mit einigen grundlegenden Anpassungen des GAN-Rahmens befassen, die wir vornehmen können, um viele dieser Probleme zu lösen.

Diskriminator übersteuert den Generator

Wenn der Diskriminator zu stark wird, wird das Signal der Verlustfunktion zu schwach, um eine sinnvolle Verbesserung des Generators zu bewirken. Im schlimmsten Fall lernt der Diskriminator perfekt, echte Bilder von gefälschten Bildern zu unterscheiden, und die Gradienten verschwinden vollständig, was zu keinerlei Training führt, wie in Abbildung 4-9 zu sehen ist.

Abbildung 4-9. Beispiel einer Ausgabe, wenn der Diskriminator den Generator übersteuert

Wenn du feststellst, dass die Verlustfunktion deines Diskriminators zusammenbricht, musst du Wege finden, den Diskriminator zu schwächen. Probiere die folgenden Vorschläge aus:

  • Erhöhe den rate Parameter der Dropout Schichten im Diskriminator, um die Menge an Informationen, die durch das Netz fließt, zu dämpfen.

  • Verringere die Lernrate des Diskriminators.

  • Reduziere die Anzahl der Faltungsfilter im Diskriminator.

  • Füge beim Training des Diskriminators Rauschen zu den Etiketten hinzu.

  • Drehe die Beschriftungen einiger Bilder beim Training des Diskriminators nach dem Zufallsprinzip um.

Der Generator übersteuert den Diskriminator

Wenn der Diskriminator nicht stark genug ist, findet der Generator Wege, den Diskriminator mit einer kleinen Auswahl fast identischer Bilder auszutricksen. Dies wird als mode collapse bezeichnet.

Nehmen wir zum Beispiel an, wir würden den Generator über mehrere Stapel hinweg trainieren, ohne den Diskriminator zwischendurch zu aktualisieren. Der Generator würde dazu neigen, eine einzige Beobachtung (auch Modus genannt) zu finden, die den Diskriminator immer täuscht, und er würde beginnen, jeden Punkt im latenten Eingaberaum diesem Bild zuzuordnen. Außerdem würden die Gradienten der Verlustfunktion auf einen Wert nahe 0 zusammenbrechen, sodass er sich von diesem Zustand nicht mehr erholen könnte.

Selbst wenn wir versuchen würden, den Diskriminator neu zu trainieren, damit er sich nicht mehr von diesem einen Punkt täuschen lässt, würde der Generator einfach einen anderen Modus finden, der den Diskriminator täuscht, da er bereits gefühllos gegenüber seinem Input geworden ist und daher keinen Anreiz hat, seinen Output zu diversifizieren.

Die Auswirkung des Modenkollapses ist in Abbildung 4-10 zu sehen.

Abbildung 4-10. Beispiel für den Zusammenbruch des Modus, wenn der Generator den Diskriminator übersteuert

Wenn du feststellst, dass dein Generator unter einem Modus-Kollaps leidet, kannst du versuchen, den Diskriminator zu stärken, indem du die gegenteiligen Vorschläge als die im vorherigen Abschnitt aufgeführten verwendest. Außerdem kannst du versuchen, die Lernrate beider Netze zu verringern und die Stapelgröße zu erhöhen.

Uninformierter Verlust

Da das Deep-Learning-Modell so erstellt wird, dass die Verlustfunktion minimiert wird, wäre es naheliegend zu denken, dass die Qualität der erzeugten Bilder umso besser ist, je kleiner die Verlustfunktion des Generators ist. Da der Generator jedoch nur gegen den aktuellen Diskriminator bewertet wird und dieser sich ständig verbessert, können wir die Verlustfunktion, die zu verschiedenen Zeitpunkten im Trainingsprozess bewertet wird, nicht vergleichen. In Abbildung 4-6 nimmt die Verlustfunktion des Generators mit der Zeit sogar zu, obwohl sich die Qualität der Bilder deutlich verbessert. Diese fehlende Korrelation zwischen der Verlustfunktion des Generators und der Bildqualität macht es manchmal schwierig, das GAN-Training zu überwachen.

Hyperparameter

Wie wir unter gesehen haben, gibt es selbst bei einfachen GANs eine große Anzahl von Hyperparametern, die eingestellt werden müssen. Neben der Gesamtarchitektur des Diskriminators und des Generators sind auch die Parameter für die Stapelnormalisierung, den Dropout, die Lernrate, die Aktivierungsschichten, die Faltungsfilter, die Kernelgröße, das Striding, die Stapelgröße und die Größe des latenten Raums zu berücksichtigen. GANs reagieren sehr empfindlich auf geringfügige Änderungen all dieser Parameter, und es ist oft eher eine Frage von Versuch und Irrtum, die richtigen Parameter zu finden, als einer festgelegten Richtlinie zu folgen.

Deshalb ist es wichtig, das Innenleben des GAN zu verstehen und zu wissen, wie die Verlustfunktion zu interpretieren ist, damit du sinnvolle Anpassungen der Hyperparameter identifizieren kannst, die die Stabilität des Modells verbessern.

GAN-Herausforderungen angehen

In den letzten Jahren haben einige wichtige Fortschritte die Gesamtstabilität von GAN-Modellen drastisch verbessert und die Wahrscheinlichkeit einiger der oben genannten Probleme, wie z. B. den Zusammenbruch der Modi, verringert.

Im weiteren Verlauf dieses Kapitels werden wir das Wasserstein GAN mit Gradient Penalty (WGAN-GP) untersuchen, das einige wichtige Anpassungen an dem bisher untersuchten GAN-Rahmenwerk vornimmt, um die Stabilität und Qualität der Bilderzeugung zu verbessern.

Wasserstein GAN mit Gradient Penalty (WGAN-GP)

Unter werden wir einen WGAN-GP erstellen, um Gesichter aus dem CelebA-Datensatz zu erzeugen, den wir in Kapitel 3 verwendet haben.

Ausführen des Codes für dieses Beispiel

Den Code für dieses Beispiel findest du im Jupyter-Notebook unter notebooks/04_gan/02_wgan_gp/wgan_gp.ipynb im Buch-Repository.

Der Code von wurde dem hervorragenden WGAN-GP-Tutorial von Aakash Kumar Nain entnommen, das auf der Keras-Website verfügbar ist.

Die Wasserstein GAN (WGAN), die 2017 in einem Artikel von Arjovsky et al,4 war einer der ersten großen Schritte zur Stabilisierung des GAN-Trainings. Mit ein paar Änderungen konnten die Autoren zeigen, wie man GANs trainieren kann, die die folgenden zwei Eigenschaften haben (Zitat aus der Veröffentlichung):

  • Eine aussagekräftige Verlustmetrik, die mit der Konvergenz des Generators und der Probenqualität korreliert

  • Verbesserte Stabilität des Optimierungsprozesses

Im Einzelnen wird die Wasserstein-Verlustfunktion sowohl für den Diskriminator als auch für den Generator eingeführt. Die Verwendung dieser Verlustfunktion anstelle der binären Kreuzentropie führt zu einer stabileren Konvergenz des GAN.

In diesem Abschnitt werden wir die Wasserstein-Verlustfunktion definieren und dann sehen, welche weiteren Änderungen wir an der Modellarchitektur und am Trainingsprozess vornehmen müssen, um unsere neue Verlustfunktion zu integrieren.

Die vollständige Modellklasse findest du im Jupyter-Notebook unter chapter05/wgan-gp/faces/train.ipynb im Buch-Repository.

Wasserstein Verlust

Erinnern wir uns zunächst unter an die Definition des binären Cross-Entropie-Verlusts - die Funktion, die wir derzeit zum Trainieren des Diskriminators und Generators des GAN verwenden(Gleichung 4-1).

Gleichung 4-1. Binärer Kreuzentropieverlust
- 1 n i=1 n ( y i loggen ( p i ) + ( 1 - y i ) loggen ( 1 - p i ) )

Um den GAN-Diskriminator zu trainieren D zu trainieren, berechnen wir den Verlust beim Vergleich der Vorhersagen für echte Bilder p i = D ( x i ) auf die Antwort y i = 1 und Vorhersagen für erzeugte Bilder p i = D ( G ( z i ) ) auf die Antwort y i = 0 . Daher kann die Minimierung der Verlustfunktion für den GAN-Diskriminator wie in Gleichung 4-2 dargestellt beschrieben werden.

Gleichung 4-2. GAN-Diskriminator-Verlustminimierung
min D - ( 𝔼 xp X [ loggen D ( x ) ] + 𝔼 zp Z [ loggen ( 1 - D ( G ( z ) ) ) ] )

Um den GAN-Generator zu trainieren G zu trainieren, berechnen wir den Verlust beim Vergleich der Vorhersagen für die erzeugten Bilder p i = D ( G ( z i ) ) auf die Antwort y i = 1 . Für den GAN-Generator kann die Minimierung der Verlustfunktion daher wie in Gleichung 4-3 dargestellt geschrieben werden.

Gleichung 4-3. Minimierung der GAN-Generatorverluste
min G - ( 𝔼 zp Z [ loggen D ( G ( z ) ) ] )

Vergleichen wir dies nun mit der Wasserstein-Verlustfunktion.

Erstens erfordert der Wasserstein-Verlust, dass wir y i = 1 und y i = -1 statt 1 und 0. Außerdem entfernen wir die sigmoide Aktivierung aus der letzten Schicht des Diskriminators, so dass die Vorhersagen p i nicht mehr zwingend in den Bereich [0, 1] fallen müssen, sondern jetzt jede Zahl im Bereich ( - , ). Aus diesem Grund wird derDiskriminator in einem WGAN in der Regel als Kritiker bezeichnet, der eine Punktzahl und nicht eineWahrscheinlichkeit ausgibt.

Die Wasserstein-Verlustfunktion ist wie folgt definiert:

- 1 n i=1 n ( y i p i )

Um den WGAN-Kritiker zu schulen D zu trainieren, berechnen wir den Verlust beim Vergleich von Vorhersagen für echte Bilder p i = D ( x i ) auf die Antwort y i = 1 und Vorhersagen für erzeugte Bilder p i = D ( G ( z i ) ) auf die Antwort y i = -1. Für die WGAN-Kritik kann die Minimierung der Verlustfunktion daher wie folgt geschrieben werden:

min D - ( 𝔼 xp X [ D ( x ) ] - 𝔼 zp Z [ D ( G ( z ) ) ] )

Mit anderen Worten: Der WGAN-Kritiker versucht, die Differenz zwischen seinen Vorhersagen für reale Bilder und generierte Bilder zu maximieren.

Um den WGAN-Generator zu trainieren, berechnen wir den Verlust beim Vergleich der Vorhersagen für die erzeugten Bilder p i = D ( G ( z i ) ) auf die Antwort y i = 1 . Für den WGAN-Generator kann die Minimierung der Verlustfunktion daher wie folgt geschrieben werden:

min G - ( 𝔼 zp Z [ D ( G ( z ) ) ] )

Mit anderen Worten: Der WGAN-Generator versucht, Bilder zu erzeugen, die vom Kritiker so hoch wie möglich bewertet werden (d. h., der Kritiker wird getäuscht, damit er sie für echt hält).

Die Lipschitz-Beschränkung

Es mag dich überraschen, dass wir dem Kritiker jetzt erlauben, jede Zahl im Bereich ( - , ) ausgeben können, anstatt die Ausgabe mit einer Sigmoidfunktion auf den üblichen Bereich [0, 1] zu beschränken. Der Wasserstein-Verlust kann daher sehr groß sein, was beunruhigend ist - normalerweise sollten große Zahlen in neuronalen Netzen vermieden werden!

Die Autoren des WGAN-Papiers zeigen, dass die Wasserstein-Verlustfunktion nur dann funktioniert, wenn wir eine zusätzliche Bedingung für die Kritik aufstellen. Insbesondere muss die kritische Funktion eine stetige 1-Lipschitz-Funktion sein. Nehmen wir das mal auseinander, um zu verstehen, was das im Einzelnen bedeutet.

Der Kritiker ist eine Funktion D die ein Bild in eine Vorhersage umwandelt. Wir sagen, dass diese Funktion 1-Lipschitz ist, wenn sie die folgende Ungleichung für zwei beliebige Eingangsbilder erfüllt, x 1 und x 2 :

|D(x 1 )-D(x 2 )| |x 1 -x 2 | 1

Hier, | x 1 - x 2 | ist die durchschnittliche pixelweise absolute Differenz zwischen zwei Bildern und | D ( x 1 ) - D ( x 2 ) | ist die absolute Differenz zwischen den Vorhersagen der Kritiker. Im Wesentlichen verlangen wir eine Begrenzung der Geschwindigkeit, mit der sich die Vorhersagen des Kritikers zwischen zwei Bildern ändern können (d. h. der absolute Wert des Gradienten darf überall höchstens 1 betragen). Wir sehen dies in Abbildung 4-11bei einer Lipschitz-kontinuierlichen 1D-Funktion : Ankeinem Punkt tritt die Linie in den Kegel ein, egal wo du den Kegel auf der Linie platzierst. Mit anderen Worten: Es gibt eine Grenze für die Geschwindigkeit, mit der die Linie an jedem Punkt steigen oder fallen kann.

Abbildung 4-11. Eine Lipschitz-kontinuierliche Funktion (Quelle: Wikipedia)
Tipp

Für diejenigen, die tiefer in die mathematische Logik eintauchen wollen, warum der Wasserstein-Verlust nur funktioniert, wenn diese Einschränkung erzwungen wird, bietet Jonathan Hui eine hervorragende Erklärung.

Erzwingen der Lipschitz-Beschränkung

In zeigen die Autoren, wie man die Lipschitz-Bedingung durchsetzen kann, indem man die Gewichte des Kritikers nach jedem Training in einem kleinen Bereich,[-0,01, 0,01], beschneidet.

Einer der Kritikpunkte an diesem Ansatz ist, dass die Lernfähigkeit des kritischen Systems stark eingeschränkt wird, da wir seine Gewichte beschneiden. Schon in der ursprünglichen WGAN-Veröffentlichung schreiben die Autoren: "Weight clipping is a clearly terrible way to enforce a Lipschitz constraint". Ein starker Kritiker ist entscheidend für den Erfolg eines WGAN, denn ohne genaue Gradienten kann der Generator nicht lernen, wie er seine Gewichte anpassen muss, um bessere Proben zu produzieren.

Deshalb haben andere Forscher nach alternativen Wegen gesucht, um die Lipschitz-Bedingung durchzusetzen und die Fähigkeit des WGAN, komplexe Merkmale zu lernen, zu verbessern. Eine solche Methode ist das Wasserstein GAN mit Gradient Penalty.

In der Arbeit, in der diese Variante vorgestellt wird, zeigen die5 zeigen die Autoren, wie die Lipschitz-Bedingung direkt durchgesetzt werden kann, indem sie einen Gradienten-Strafterm in die Verlustfunktion für den Kritiker aufnehmen, der das Modell bestraft, wenn die Gradientennorm von 1 abweicht. Dies führt zu einem wesentlich stabileren Trainingsprozess.

Im nächsten Abschnitt werden wir sehen, wie wir diesen zusätzlichen Term in die Verlustfunktion für unseren Kritiker einbauen.

Der Gradient Penalty Verlust

Abbildung 4-12 ist ein Diagramm des Trainingsprozesses für den Kritiker eines WGAN-GP. Wenn wir dies mit dem ursprünglichen Diskriminator-Trainingsprozess aus Abbildung 4-5 vergleichen, können wir sehen, dass der wichtigste Zusatz der Gradienten-Strafverlust ist, der als Teil der gesamten Verlustfunktion neben dem Wasserstein-Verlust aus den echten und gefälschten Bildern enthalten ist.

Abbildung 4-12. Der Prozess der WGAN-GP Kritikschulung

Der Gradientenstrafverlust misst die quadratische Differenz zwischen der Norm des Gradienten der Vorhersagen in Bezug auf die Eingangsbilder und 1. Das Modell wird natürlich dazu neigen, Gewichte zu finden, die sicherstellen, dass der Gradientenstrafterm minimiert wird, wodurch das Modell ermutigt wird, die Lipschitz-Bedingung einzuhalten.

Da es schwierig ist, diesen Gradienten während des Trainings überall zu berechnen, wertet das WGAN-GP den Gradienten nur an einer Handvoll Punkte aus. Um eine ausgewogene Mischung zu gewährleisten, verwenden wir eine Reihe von interpolierten Bildern, die an zufällig ausgewählten Punkten entlang von Linien liegen, die den Stapel echter Bilder mit dem Stapel gefälschter Bilder paarweise verbinden, wie in Abbildung 4-13 dargestellt.

Abbildung 4-13. Interpolieren zwischen Bildern

In Beispiel 4-8 zeigen wir, wie die Gradientenstrafe im Code berechnet wird.

Beispiel 4-8. Die Gradientenstrafverlustfunktion
def gradient_penalty(self, batch_size, real_images, fake_images):
    alpha = tf.random.normal([batch_size, 1, 1, 1], 0.0, 1.0) 1
    diff = fake_images - real_images
    interpolated = real_images + alpha * diff 2

    with tf.GradientTape() as gp_tape:
        gp_tape.watch(interpolated)
        pred = self.critic(interpolated, training=True) 3

    grads = gp_tape.gradient(pred, [interpolated])[0] 4
    norm = tf.sqrt(tf.reduce_sum(tf.square(grads), axis=[1, 2, 3])) 5
    gp = tf.reduce_mean((norm - 1.0) ** 2) 6
    return gp
1

Jedes Bild im Stapel erhält eine Zufallszahl zwischen 0 und 1, die als Vektor alpha gespeichert wird.

2

Es wird eine Reihe von interpolierten Bildern berechnet.

3

Der Kritiker wird gebeten, jedes dieser interpolierten Bilder zu bewerten.

4

Die Steigung der Vorhersagen wird in Bezug auf die Eingangsbilder berechnet.

5

Die L2-Norm dieses Vektors wird berechnet.

6

Die Funktion liefert den durchschnittlichen quadratischen Abstand zwischen der L2-Norm und 1.

Ausbildung des WGAN-GP

Ein entscheidender Vorteil der Wasserstein-Verlustfunktion ist, dass wir uns nicht mehr darum kümmern müssen, den Kritiker und den Generator gleichmäßig zu trainieren - bei Verwendung der Wasserstein-Verlustfunktion muss der Kritiker bis zur Konvergenz trainiert werden, bevor der Generator aktualisiert wird, um sicherzustellen, dass die Gradienten für die Generatoraktualisierung genau sind. Dies steht im Gegensatz zu einem Standard-GAN, bei dem es wichtig ist, den Diskriminator nicht zu stark werden zu lassen.

Daher können wir bei Wasserstein-GANs die Kritik einfach mehrmals zwischen den Generator-Updates trainieren, um sicherzustellen, dass sie nahe an der Konvergenz ist. Ein typisches Verhältnis sind drei bis fünf Kritiker-Updates pro Generator-Update.

Wir haben nun die beiden Schlüsselkonzepte des WGAN-GP vorgestellt - den Wasserstein-Verlust und den Gradienten-Strafterm, der in der kritischen Verlustfunktion enthalten ist. Der Trainingsschritt des WGAN-Modells, der all diese Ideen beinhaltet, wird in Beispiel 4-9 gezeigt.

Beispiel 4-9. Training des WGAN-GP
def train_step(self, real_images):
    batch_size = tf.shape(real_images)[0]

    for i in range(3): 1
        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim)
        )

        with tf.GradientTape() as tape:
            fake_images = self.generator(
                random_latent_vectors, training = True
            )
            fake_predictions = self.critic(fake_images, training = True)
            real_predictions = self.critic(real_images, training = True)

            c_wass_loss = tf.reduce_mean(fake_predictions) - tf.reduce_mean(
                real_predictions
            ) 2
            c_gp = self.gradient_penalty(
                batch_size, real_images, fake_images
            ) 3
            c_loss = c_wass_loss + c_gp * self.gp_weight 4

        c_gradient = tape.gradient(c_loss, self.critic.trainable_variables)
        self.c_optimizer.apply_gradients(
            zip(c_gradient, self.critic.trainable_variables)
        ) 5

    random_latent_vectors = tf.random.normal(
        shape=(batch_size, self.latent_dim)
    )
    with tf.GradientTape() as tape:
        fake_images = self.generator(random_latent_vectors, training=True)
        fake_predictions = self.critic(fake_images, training=True)
        g_loss = -tf.reduce_mean(fake_predictions) 6

    gen_gradient = tape.gradient(g_loss, self.generator.trainable_variables)
    self.g_optimizer.apply_gradients(
        zip(gen_gradient, self.generator.trainable_variables)
    ) 7

    self.c_loss_metric.update_state(c_loss)
    self.c_wass_loss_metric.update_state(c_wass_loss)
    self.c_gp_metric.update_state(c_gp)
    self.g_loss_metric.update_state(g_loss)

    return {m.name: m.result() for m in self.metrics}
1

Führe drei kritische Updates durch.

2

Berechne den Wasserstein-Verlust für den Kritiker - die Differenz zwischen der durchschnittlichen Vorhersage für die gefälschten Bilder und die echten Bilder.

3

Berechne den Term der Gradientenstrafe (siehe Beispiel 4-8).

4

Die kritische Verlustfunktion ist eine gewichtete Summe aus dem Wasserstein-Verlust und der Gradientenstrafe.

5

Aktualisiere die Gewichte des Kritikers.

6

Berechne den Wassersteinverlust für den Generator.

7

Aktualisiere die Gewichte des Generators.

Batch-Normalisierung in einem WGAN-GP

Eine letzte Überlegung, die wir vor dem Training eines WGAN-GP anstellen sollten, ist, dass die Stapel-Normalisierung nicht in der Kritik verwendet werden sollte. Der Grund dafür ist, dass die Stapelnormalisierung eine Korrelation zwischen den Bildern desselben Stapels erzeugt, wodurch der Verlust durch die Gradientenstrafe weniger effektiv wird. Experimente haben gezeigt, dass WGAN-GPs auch ohne Batch-Normalisierung in der Kritik hervorragende Ergebnisse erzielen können.

Wir haben nun alle wichtigen Unterschiede zwischen einem Standard-GAN und einem WGAN-GP behandelt. Um es noch einmal zusammenzufassen:

  • Ein WGAN-GP verwendet den Wasserstein-Verlust.

  • Das WGAN-GP wird mit Labels von 1 für echt und -1 für falsch trainiert.

  • In der letzten Schicht des Kritikers gibt es keine sigmoide Aktivierung.

  • Füge in die Verlustfunktion für den Kritiker einen Gradientenstrafterm ein.

  • Trainiere den Kritiker mehrmals für jede Aktualisierung des Generators.

  • In der Kritik gibt es keine Batch-Normalisierungsschichten.

Analyse des WGAN-GP

Schauen wir uns unter einige Beispielausgaben des Generators nach 25 Epochen Training an(Abbildung 4-14).

Abbildung 4-14. Beispiele für WGAN-GP-Gesichter

Das Modell hat die wichtigsten Merkmale eines Gesichts gelernt, und es gibt keine Anzeichen für einen Moduszusammenbruch.

Wir können auch sehen, wie sich die Verlustfunktionen des Modells im Laufe der Zeit entwickeln(Abbildung 4-15) - die Verlustfunktionen sowohl des Kritikers als auch des Generators sind sehr stabil und konvergent.

Wenn wir die WGAN-GP-Ausgabe mit der VAE-Ausgabe aus dem vorherigen Kapitel vergleichen, sehen wir, dass die GAN-Bilder im Allgemeinen schärfer sind - vor allem die Abgrenzung zwischen Haar und Hintergrund. Das gilt generell: VAEs erzeugen eher weichere Bilder, bei denen die Farbgrenzen verschwimmen, während GANs bekanntermaßen schärfere, klarere Bilder erzeugen.

Abbildung 4-15. WGAN-GP-Verlustkurven: Der kritische Verlust (epoch_c_loss) wird in den Wasserstein-Verlust (epoch_c_wass) und den Gradientenstrafverlust (epoch_c_gp) zerlegt.

Es stimmt auch, dass GANs im Allgemeinen schwieriger zu trainieren sind als VAEs und es länger dauert, bis sie eine zufriedenstellende Qualität erreichen. Dennoch basieren viele der modernsten generativen Modelle heute auf GANs, da es sich lohnt, große GANs über einen längeren Zeitraum auf GPUs zu trainieren.

Bedingtes GAN (CGAN)

Bisher haben wir in diesem Kapitel GANs entwickelt, die realistische Bilder aus einer vorgegebenen Trainingsmenge erzeugen können. Allerdings können wir nicht bestimmen, welche Art von Bild wir erzeugen möchten, z. B. ein männliches oder weibliches Gesicht oder einen großen oder kleinen Ziegelstein. Wir können zwar einen zufälligen Punkt aus dem latenten Raum auswählen, aber wir können nicht ohne Weiteres nachvollziehen, welche Art von Bild bei der Wahl der latenten Variable erzeugt wird.

Unter werden wir uns im letzten Teil dieses Kapitels dem Aufbau eines GAN widmen, bei dem wir die Ausgabe kontrollieren können - ein sogenanntes bedingtes GAN. Diese Idee wurde erstmals in "Conditional Generative Adversarial Nets" von Mirza und Osindero (2014) vorgestellt,6 vorgestellt wurde, ist eine relativ einfache Erweiterung der GAN-Architektur.

Ausführen des Codes für dieses Beispiel

Den Code für dieses Beispiel findest du im Jupyter-Notizbuch unter notebooks/04_gan/03_cgan/cgan.ipynb imBuch-Repository.

Der Code wurde dem hervorragenden CGAN-Tutorial von Sayak Paul entnommen, das auf der Keras-Website verfügbar ist.

CGAN-Architektur

In werden wir unser CGAN auf das Attribut blondes Haar des Gesichter-Datensatzes konditionieren. Das heißt, wir können explizit angeben, ob wir ein Bild mit blonden Haaren erzeugen wollen oder nicht. Dieses Label wird als Teil des CelebA-Datensatzes bereitgestellt.

Die CGAN-Architektur auf hoher Ebene ist in Abbildung 4-16 dargestellt.

Abbildung 4-16. Eingänge und Ausgänge des Generators und des Kritikers in einem CGAN

Der Hauptunterschied zwischen einem Standard-GAN und einem CGAN besteht darin, dass wir bei einem CGAN dem Generator und dem Kritiker zusätzliche Informationen über das Label übergeben. Der Generator fügt diese Informationen einfach als kodierten Ein-Hot-Vektor an das latente Raummuster an. Beim Kritiker fügen wir die Label-Informationen als zusätzliche Kanäle zum RGB-Bild hinzu. Dazu wiederholen wir den kodierten One-Hot-Vektor, um die gleiche Form wie die Eingangsbilder zu erhalten.

CGANs funktionieren, weil der Kritiker nun Zugang zu zusätzlichen Informationen über den Inhalt des Bildes hat. Daher muss der Generator sicherstellen, dass seine Ausgabe mit dem angegebenen Label übereinstimmt, um den Kritiker weiterhin zu täuschen. Würde der Generator perfekte Bilder produzieren, die nicht mit dem Bildlabel übereinstimmen, könnte der Kritiker erkennen, dass es sich um eine Fälschung handelt, weil Bild und Label nicht übereinstimmen.

Tipp

In unserem Beispiel hat unser kodiertes One-Hot-Label die Länge 2, weil es zwei Klassen gibt (Blond und Nicht-Blond). Du kannst jedoch so viele Labels haben, wie du willst - zum Beispiel könntest du ein CGAN auf dem Fashion-MNIST-Datensatz trainieren, um einen der 10 verschiedenen Modeartikel auszugeben, indem du einen One-Hot-codierten Label-Vektor der Länge 10 in die Eingabe des Generators und 10 weitere One-Hot-codierte Label-Kanäle in die Eingabe des Kritikers einfügst.

Die einzige Änderung, die wir an der Architektur vornehmen müssen, ist die Verkettung der Label-Informationen mit den bestehenden Eingängen des Generators und des Kritikers, wie in Beispiel 4-10 gezeigt.

Beispiel 4-10. Eingabeschichten im CGAN
critic_input = layers.Input(shape=(64, 64, 3)) 1
label_input = layers.Input(shape=(64, 64, 2))
x = layers.Concatenate(axis = -1)([critic_input, label_input])
...
generator_input = layers.Input(shape=(32,)) 2
label_input = layers.Input(shape=(2,))
x = layers.Concatenate(axis = -1)([generator_input, label_input])
x = layers.Reshape((1,1, 34))(x)
...
1

Die Bildkanäle und die Beschriftungskanäle werden dem Kritiker getrennt übergeben und verkettet.

2

Der latente Vektor und die Label-Klassen werden separat an den Generator übergeben und vor der Umformung miteinander verbunden.

Ausbildung der CGAN

Wir müssen auch einige Änderungen an der train_step des CGAN vornehmen, um die neuen Eingabeformate des Generators und des Kritikers anzupassen, wie in Beispiel 4-11 gezeigt.

Beispiel 4-11. Die train_step der CGAN
def train_step(self, data):
    real_images, one_hot_labels = data 1

    image_one_hot_labels = one_hot_labels[:, None, None, :] 2
    image_one_hot_labels = tf.repeat(
        image_one_hot_labels, repeats=64, axis = 1
    )
    image_one_hot_labels = tf.repeat(
        image_one_hot_labels, repeats=64, axis = 2
    )

    batch_size = tf.shape(real_images)[0]

    for i in range(self.critic_steps):
        random_latent_vectors = tf.random.normal(
            shape=(batch_size, self.latent_dim)
        )

        with tf.GradientTape() as tape:
            fake_images = self.generator(
                [random_latent_vectors, one_hot_labels], training = True
            ) 3

            fake_predictions = self.critic(
                [fake_images, image_one_hot_labels], training = True
            ) 4
            real_predictions = self.critic(
                [real_images, image_one_hot_labels], training = True
            )

            c_wass_loss = tf.reduce_mean(fake_predictions) - tf.reduce_mean(
                real_predictions
            )
            c_gp = self.gradient_penalty(
                batch_size, real_images, fake_images, image_one_hot_labels
            ) 5
            c_loss = c_wass_loss + c_gp * self.gp_weight

        c_gradient = tape.gradient(c_loss, self.critic.trainable_variables)
        self.c_optimizer.apply_gradients(
            zip(c_gradient, self.critic.trainable_variables)
        )

    random_latent_vectors = tf.random.normal(
        shape=(batch_size, self.latent_dim)
    )

    with tf.GradientTape() as tape:
        fake_images = self.generator(
            [random_latent_vectors, one_hot_labels], training=True
        ) 6
        fake_predictions = self.critic(
            [fake_images, image_one_hot_labels], training=True
        )
        g_loss = -tf.reduce_mean(fake_predictions)

    gen_gradient = tape.gradient(g_loss, self.generator.trainable_variables)
    self.g_optimizer.apply_gradients(
        zip(gen_gradient, self.generator.trainable_variables)
    )
1

Die Bilder und Beschriftungen werden aus den Eingabedaten ausgepackt.

2

Die One-Hot-codierten Vektoren werden zu One-Hot-codierten Bildern erweitert, die die gleiche räumliche Größe wie die Eingangsbilder haben (64 × 64).

3

Der Generator wird nun mit einer Liste von zwei Eingaben gefüttert - den zufälligen latenten Vektoren und den kodierten One-Hot-Label-Vektoren.

4

Der Kritiker wird nun mit einer Liste von zwei Eingaben gefüttert - den gefälschten/echten Bildern und den kodierten Markierungskanälen.

5

Die Gradientenstraffunktion erfordert auch, dass die One-Hot-kodierten Label-Kanäle durchlaufen werden, da sie den Kritiker verwendet.

6

Die Änderungen, die für den Schritt der Kritikschulung vorgenommen wurden, gelten auch für den Schritt der Generatorschulung.

Analyse der CGAN

Wir können die CGAN-Ausgabe steuern, indem wir ein bestimmtes kodiertes One-Hot-Label in die Eingabe des Generators eingeben. Um zum Beispiel ein Gesicht mit nicht blonden Haaren zu erzeugen, geben wir den Vektor [1, 0] ein. Um ein Gesicht mit blonden Haaren zu erzeugen, geben wir den Vektor [0, 1] ein.

Das Ergebnis des CGAN ist in Abbildung 4-17 zu sehen. Hier behalten wir die latenten Zufallsvektoren in allen Beispielen bei und ändern nur den bedingten Label-Vektor. Es ist klar, dass das CGAN gelernt hat, den Label-Vektor zu verwenden, um nur die Haarfarbe der Bilder zu kontrollieren. Beeindruckend ist, dass sich der Rest des Bildes kaum verändert - ein Beweis dafür, dass GANs in der Lage sind, Punkte im latenten Raum so zu organisieren, dass einzelne Merkmale voneinander entkoppelt werden können.

Abbildung 4-17. Ausgabe des CGAN, wenn die Vektoren Blond und Nicht-Blond an die latente Stichprobe angehängt werden
Tipp

Wenn für deinen Datensatz Labels zur Verfügung stehen, ist es in der Regel eine gute Idee, sie als Eingabe in dein GAN aufzunehmen, auch wenn du die erzeugte Ausgabe nicht unbedingt von den Labels abhängig machen musst, da sie die Qualität der erzeugten Bilder verbessern. Du kannst dir die Beschriftungen als eine hochinformative Erweiterung der Pixel-Eingabe vorstellen.

Zusammenfassung

In diesem Kapitel haben wir drei verschiedene generative adversarische Netzmodelle (GAN) untersucht: das Deep Convolutional GAN (DCGAN), das anspruchsvollere Wasserstein GAN with Gradient Penalty (WGAN-GP) und das Conditional GAN (CGAN).

Alle GANs zeichnen sich durch eine Architektur mit Generator und Diskriminator (oder Kritiker) aus, wobei der Diskriminator versucht, den Unterschied zwischen echten und gefälschten Bildern zu erkennen, und der Generator versucht, den Diskriminator zu täuschen. Durch ein ausgewogenes Training dieser beiden Gegenspieler kann der GAN-Generator allmählich lernen, ähnliche Beobachtungen wie im Trainingsset zu machen.

Wir haben zunächst gesehen, wie man ein DCGAN trainiert, um Bilder von Spielzeugbausteinen zu erzeugen. Es war in der Lage zu lernen, wie man 3D-Objekte realistisch als Bilder darstellt, einschließlich genauer Darstellungen von Schatten, Form und Textur. Wir haben auch die verschiedenen Möglichkeiten untersucht, wie das GAN-Training fehlschlagen kann, z. B. das Zusammenbrechen des Modus oder das Verschwinden von Gradienten.

Anschließend haben wir untersucht, wie die Wasserstein-Verlustfunktion viele dieser Probleme behebt und das GAN-Training berechenbarer und zuverlässiger macht. Der WGAN-GP stellt die 1-Lipschitz-Anforderung in den Mittelpunkt des Trainingsprozesses, indem er einen Term in die Verlustfunktion aufnimmt, der die Gradientennorm gegen 1 zieht.

Wir haben das WGAN-GP auf das Problem der Gesichtsgenerierung angewandt und gesehen, wie wir durch die Auswahl von Punkten aus einer Standard-Normalverteilung neue Gesichter erzeugen können. Dieses Sampling-Verfahren ist einer VAE sehr ähnlich, allerdings sind die von einem GAN erzeugten Gesichter ganz anders - oft schärfer und mit einer stärkeren Unterscheidung zwischen den verschiedenen Teilen des Bildes.

Schließlich haben wir ein CGAN entwickelt, mit dem wir die Art des erzeugten Bildes steuern können. Dies funktioniert, indem wir dem Kritiker und dem Generator das Etikett als Eingabe übergeben und so dem Netzwerk die zusätzlichen Informationen geben, die es braucht, um die erzeugte Ausgabe an ein bestimmtes Etikett zu konditionieren.

Insgesamt haben wir gesehen, dass das GAN-Framework extrem flexibel ist und an viele interessante Problembereiche angepasst werden kann. Vor allem im Bereich der Bilderzeugung haben GANs mit vielen interessanten Erweiterungen des zugrunde liegenden Frameworks bedeutende Fortschritte gemacht, wie wir in Kapitel 10 sehen werden.

Im nächsten Kapitel werden wir eine andere Familie generativer Modelle untersuchen, die sich ideal für die Modellierung sequenzieller Daten eignet - autoregressive Modelle.

1 Ian J. Goodfellow et al., "Generative Adversarial Nets," June 10, 2014, https://arxiv.org/abs/1406.2661

2 Alec Radford et al., "Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks," January 7, 2016, https://arxiv.org/abs/1511.06434.

3 Augustus Odena et al., "Deconvolution and Checkerboard Artifacts," October 17, 2016, https://distill.pub/2016/deconv-checkerboard.

4 Martin Arjovsky et al., "Wasserstein GAN," January 26, 2017, https://arxiv.org/abs/1701.07875.

5 Ishaan Gulrajani et al., "Improved Training of Wasserstein GANs," March 31, 2017, https://arxiv.org/abs/1704.00028.

6 Mehdi Mirza und Simon Osindero, "Conditional Generative Adversarial Nets," November 6, 2014, https://arxiv.org/abs/1411.1784.

Get Generatives Deep Learning, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.