Kurs
Convolutional Neural Networks sind die stärkste Klasse neuronaler Netze für Bildklassifikation und -analyse. Ihre Einsatzmöglichkeiten haben viele Grenzen gesprengt und sind zum zentralen Baustein zahlreicher Deep-Learning-Anwendungen geworden, die wir heute sehen. Auf hoher Ebene können CNNs innere Merkmalsrepräsentationen der Bilder lernen, die ihnen eingespeist werden. Das macht sie so mächtig. Dieses besondere Merkmal von CNNs ist nicht nur für Aufgaben wie Bildklassifikation nützlich, sondern auch für die Bildkonstruktion. Anwendungen wie Deep Dream und Neural Style Transfer komponieren Bilder basierend auf Schichtaktivierungen in CNNs und den daraus extrahierten Merkmalen.
In diesem Tutorial erfährst du, wie Neural Style Transfer funktioniert und wie du es mit TensorFlow 2.0 implementierst.
Hinweis: Um diesem Tutorial zu folgen, solltest du wissen, wie CNNs funktionieren. Falls du eine kurze Auffrischung brauchst, probiere diese Ressourcen aus:
- DataCamps Kurs Convolutional Neural Networks for Image Processing
- CS231n: Convolutional Neural Networks for Visual Recognition
Was ist Neural Style Transfer?
Für Einsteiger in Computer Vision und Deep Learning kann der Begriff Neural Style Transfer zunächst überwältigend wirken. Um jede Komponente des Begriffs zu verstehen, betrachte die folgenden zwei Bilder:

Im Kontext des Neural Style Transfer wird das linke Bild als Content-Bild und das rechte als Style-Bild bezeichnet. Du möchtest ein Bild (hier: das linke) anhand eines anderen Bildes (rechts) stilisieren. Das erklärt die letzten beiden Wörter im Begriff – Style Transfer. Für den Prozess wird ein neuronales Netz (CNN) darauf trainiert, eine benutzerdefinierte Verlustfunktion zu optimieren – daher das erste Wort: Neural. Wenn die beiden Bilder per Neural Style Transfer verschmolzen werden, sieht das Ergebnis (rechts) so aus:

Wozu dient Neural Style Transfer? Stell dir vor, du hättest ein Bild einer Zeichnung von Vincent van Gogh. Du möchtest sehen, wie dieser Stil auf eine deiner eigenen Zeichnungen wirken würde. Das ist ein typischer Anwendungsfall. Ein weiteres Beispiel sind Foto-Filter-Apps wie Prisma, die Neural Style Transfer mit einer intuitiven Oberfläche ermöglichen.
Trennung von Inhalt und Stil
Bis hierher hast du gelernt: Neural Style Transfer kombiniert den Inhalt eines Bildes mit dem Stil eines anderen. Das trifft zu. In diesem Abschnitt klären wir, was Inhalt und Stil im Kontext von CNNs bedeuten.
Ein CNN besteht meist aus mehreren Convolution- und Pooling-Schichten. Convolution-Schichten extrahieren hochkomplexe Merkmale aus einem Bild, während Pooling-Schichten detaillierte räumliche Informationen verwerfen, die für eine Bildklassifikationsaufgabe nicht relevant sind. Dadurch lernt das CNN eher den Inhalt eines Bildes, statt sehr spezifische Eigenschaften wie Farbe oder Textur. Je tiefer man in ein CNN geht, desto komplexer werden die Merkmale; die tieferen Convolution-Schichten werden oft als Inhaltsrepräsentationen bezeichnet.
Ein Beispiel für Stil sind bildspezifische Eigenschaften wie Textur, Farbe usw. – etwa die starke Nutzung einer bestimmten Farbe in einer Zeichnung. Wie extrahiert man den Stil eines Bildes? Indem man Korrelationen zwischen Convolution-Schichten berechnet. Korrelationen messen, wie ähnlich/verwandt zwei oder mehr Variablen sind. Stell dir eine gelernte Convolution-Schicht mit mehreren Feature Maps vor. Für jede Feature Map kannst du messen, wie stark ihre erkannten Merkmale mit den anderen Feature Maps derselben Schicht zusammenhängen. So schätzt du Dinge wie: Ist ein bestimmter Farbton in der ersten Feature Map ähnlich zu einem in einer anderen? Gibt es Formen, die über mehrere Feature Maps hinweg auftreten? Solche Gemeinsamkeiten definieren den Stil eines Bildes. Das Messen der Ähnlichkeiten zwischen Inhalten mehrerer Feature Maps innerhalb einer Convolution-Schicht hilft dem Netzwerk, eine Multiskalen-Repräsentation des Bildes zu lernen, die räumliche Eigenschaften wie Textur und Farbe betont.
Wichtig ist außerdem: Beim Neural Style Transfer musst du sicherstellen, dass der Inhalt eines Bildes erhalten bleibt, während der gewünschte Stil des anderen Bildes übertragen wird. Wie das gelingt, siehst du später. Im nächsten Abschnitt lernst du, wie du Merkmale aus einem (Content-)Bild extrahierst und den Content Loss berechnest.
Den Content quantifizieren und den Content Loss berechnen
Der Neural-Style-Transfer-Algorithmus wurde erstmals 2015 von Gatys et al. im Paper A Neural Algorithm of Artistic Style vorgestellt. Dieses Tutorial orientiert sich jedoch an Image Style Transfer Using Convolutional Neural Networks, einer Fortführung des vorherigen Papiers.
Laut Image Style Transfer Using Convolutional Neural Networks wird eine VGG-19-Architektur verwendet, um sowohl Inhalts- als auch Stilmerkmale aus Content- und Style-Bild zu extrahieren. Für die Inhaltsmerkmale nutzt man die zweite Convolution-Schicht aus dem vierten Block (der Convolution-Blöcke), benannt als conv4_2. Sobald du die Inhaltsmerkmale hast, vergleichst du sie mit einem Zielbild, um den Content Loss zu messen. Was ist ein Zielbild? Warum ist die Berechnung des Content Loss nötig? Schauen wir uns das kurz an.
Um Inhalt und Stil in einem einzigen Bild zu vereinen, startest du mit einem Zielbild (target image) – entweder leer oder eine Kopie des Content-Bildes. Damit ein CNN sowohl Inhalt als auch Stil effektiv lernt, brauchst du eine benutzerdefinierte Verlustfunktion, die du optimierst, um ein harmonisches stilisiertes Bild aus Content- und Style-Bild zu erzeugen. Diese Verlustfunktion setzt sich im Kern aus zwei Teilen zusammen:
- Content Loss, der sicherstellt, dass der Inhalt erhalten bleibt.
- Style Loss, der steuert, wie viel Stil auf das Zielbild übertragen wird.
Zurück zum Content Loss: Er wird wie folgt definiert –
Dabei bezeichnet $T_c$ das Zielbild und $C_c$ das Content-Bild. Die Verlustfunktion misst also, wie weit die Merkmale von Content- und Zielbild auseinanderliegen. Das Netzwerk versucht, diesen Verlust zu minimieren. Im nächsten Abschnitt lernst du, wie der Style Loss bestimmt wird.
Style Loss und Gram-Matrix
Für den Style Loss nimmst du die Repräsentationen (letztlich Zahlen) aus den folgenden Schichten und berechnest die Gram-Matrizen der Feature Maps innerhalb dieser Schichten.
'conv1_1'
'conv2_1'
'conv3_1'
'conv4_1'
'conv5_1'
Der Vorteil, mehrere Schichten für den Style Loss zu nutzen: Das Netzwerk lernt eine Multiskalen-Repräsentation des im Bild enthaltenen Stils. Die Gram-Matrizen zeigen, wie ähnlich sich die Merkmale über die Feature Maps einer einzelnen Convolution-Schicht sind. So entsteht nicht-lokalisierte Information über das Bild, die dessen Stil prägt. Eine Gram-Matrix wird in diesem Zusammenhang wie folgt berechnet.
Angenommen, die Dimension eines konvolvierten Bildes ist 8x8x16, also 16 Feature Maps. Du möchtest nun die Ähnlichkeiten der Merkmale über diese Feature Maps mittels Gram-Matrix bestimmen. Dazu flachst du die ersten beiden Dimensionen zu einem 1D-Vektor ab. Im Beispiel enthält der Vektor 64 Einträge. Das wiederholst du für alle Feature Maps. Du erhältst eine Matrix mit der Dimension 16x64 (nennen wir sie Matrix A). Diese transponierst du (64x16) und multiplizierst sie mit A. Die resultierende Gram-Matrix hat hier die Dimension 16x16. Ein bestimmter Wert in der Gram-Matrix gibt die Ähnlichkeit zwischen zwei Feature Maps an. Anschaulich sieht das so aus:

Um den Style Loss zwischen Ziel- und Style-Bild zu berechnen, nimmst du die mittleren quadratischen Abstände zwischen ihren Gram-Matrizen in jedem der Convolution-Blöcke. Formal lässt sich das so definieren:
Hier ist T(s,i) die Gram-Matrix des Zielbildes im Block i und S(s,i) die Gram-Matrix des Style-Bildes im Block i. Mit wi kannst du einzelnen Convolution-Blöcken unterschiedliche Gewichte geben, um den Stil feiner zu steuern. Schließlich ist $a$ eine Konstante, die die Werte je Schicht im Block berücksichtigt. Fassen wir beide Verluste zusammen, um den Gesamtloss zu definieren, den das Netzwerk beim Neural Style Transfer optimiert.
Den Gesamtloss definieren
Der Gesamtloss ist nun eine gewichtete Summe –
wobei $\alpha$ das Content-Gewicht und $\beta$ das Style-Gewicht ist. So hältst du die Balance zwischen Inhalt und dem Umfang des übertragenen Stils. Die Wirkung verschiedener Kombinationen von $\alpha$ und $\beta$ ist im Paper zu sehen – schau es dir an. Damit hast du alle Bausteine, um Neural Style Transfer zu implementieren. Weiter geht’s im nächsten Abschnitt.
Neural Style Transfer in Aktion
Für die Implementierung verwendest du TensorFlow 2.0. Das Release bringt viele großartige Neuerungen und ist eines der umfassendsten Updates der Bibliothek. Wenn dich ein Überblick interessiert, lies diesen Artikel.
Die aktuelle Version von TensorFlow (zum Zeitpunkt dieses Tutorials) ist 2.0.0-beta0. Falls noch nicht installiert, folge zuerst den Anweisungen hier. Starte nun mit dem Import der notwendigen Pakete sowie Content- und Style-Bild.
# Packages
import tensorflow as tf
from tensorflow.keras.applications.vgg19 import preprocess_input
from tensorflow.keras.models import Model
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(7)
%matplotlib inline
print(tf.__version__)
2.0.0-beta0
# Load the content and style images
content = plt.imread('Content.jpeg')
style = plt.imread('Style.jpg')
# Display the images
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))
# Content and style images side-by-side
ax1.imshow(content)
ax1.set_title('Content Image')
ax2.imshow(style)
ax2.set_title('Style Image')
plt.show()

Im nächsten Schritt schreibst du eine Hilfsfunktion, die die beiden Bilder als Zahlenarrays lädt (denn Computer verstehen nur Zahlen) und sie für die Nutzung im Modell zuschneidet und formatiert.
def load_image(image):
image = plt.imread(image)
img = tf.image.convert_image_dtype(image, tf.float32)
img = tf.image.resize(img, [400, 400])
# Shape -> (batch_size, h, w, d)
img = img[tf.newaxis, :]
return img
# Use load_image of content and style images
content = load_image('Content.jpeg')
style = load_image('Style.jpg')
# Verify the shapes
content.shape, style.shape
(TensorShape([1, 400, 400, 3]), TensorShape([1, 400, 400, 3]))
Die Bilder sind umgeformt. Jetzt lädst du ein vortrainiertes VGG19-Modell, um Merkmale zu extrahieren. Da wir das Modell nur zur Merkmalsextraktion nutzen, brauchen wir den Klassifikator-Teil nicht.
vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
vgg.trainable = False
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg19_weights_tf_dim_ordering_tf_kernels_notop.h5
80142336/80134624 [==============================] - 5s 0us/step
Die Autor:innen haben die Blocknamen angegeben, die sie zur Extraktion von Inhalts- und Stilmerkmalen genutzt haben. Das geladene VGG19 verwendet dieselben Bezeichnungen. Um das zu nutzen, gibst du zunächst die Namen aller Schichten im Netzwerk aus.
# Print the layer names for convenience
for layer in vgg.layers:
print(layer.name)
input_1
block1_conv1
block1_conv2
block1_pool
block2_conv1
block2_conv2
block2_pool
block3_conv1
block3_conv2
block3_conv3
block3_conv4
block3_pool
block4_conv1
block4_conv2
block4_conv3
block4_conv4
block4_pool
block5_conv1
block5_conv2
block5_conv3
block5_conv4
block5_pool
Für die Stilmerkmale interessieren dich insbesondere diese:
'conv1_1'
'conv2_1'
'conv3_1'
'conv4_1'
'conv5_1'
Für die Inhaltsmerkmale brauchst du conv4_2. Speichere die Namen entsprechend in Variablen.
# Content layer
content_layers = ['block4_conv2']
# Style layer
style_layers = ['block1_conv1',
'block2_conv1',
'block3_conv1',
'block4_conv1',
'block5_conv1']
num_content_layers = len(content_layers)
num_style_layers = len(style_layers)
Erstelle nun ein benutzerdefiniertes VGG-Modell, das aus den angegebenen Schichten besteht. So kannst du Vorwärtsdurchläufe auf den Bildern ausführen und unterwegs die benötigten Merkmale extrahieren.
def mini_model(layer_names, model):
outputs = [model.get_layer(name).output for name in layer_names]
model = Model([vgg.input], outputs)
return model
Eine Gram-Matrix ist in TensorFlow schnell definiert – so geht’s:
# Gram matrix
def gram_matrix(tensor):
temp = tensor
temp = tf.squeeze(temp)
fun = tf.reshape(temp, [temp.shape[2], temp.shape[0]*temp.shape[1]])
result = tf.matmul(temp, temp, transpose_b=True)
gram = tf.expand_dims(result, axis=0)
return gram
Jetzt definierst du ein benutzerdefiniertes Modell mit der Funktion mini_model(). Es gibt die Inhalts- und Stilmerkmale der jeweiligen Bilder zurück.
class Custom_Style_Model(tf.keras.models.Model):
def __init__(self, style_layers, content_layers):
super(Custom_Style_Model, self).__init__()
self.vgg = mini_model(style_layers + content_layers, vgg)
self.style_layers = style_layers
self.content_layers = content_layers
self.num_style_layers = len(style_layers)
self.vgg.trainable = False
def call(self, inputs):
# Scale back the pixel values
inputs = inputs*255.0
# Preprocess them with respect to VGG19 stats
preprocessed_input = preprocess_input(inputs)
# Pass through the mini network
outputs = self.vgg(preprocessed_input)
# Segregate the style and content representations
style_outputs, content_outputs = (outputs[:self.num_style_layers],
outputs[self.num_style_layers:])
# Calculate the gram matrix for each layer
style_outputs = [gram_matrix(style_output)
for style_output in style_outputs]
# Assign the content representation and gram matrix in
# a layer by layer fashion in dicts
content_dict = {content_name:value
for content_name, value
in zip(self.content_layers, content_outputs)}
style_dict = {style_name:value
for style_name, value
in zip(self.style_layers, style_outputs)}
return {'content':content_dict, 'style':style_dict}
Jetzt, da das Custom-Modell definiert ist, verwenden wir es auf den Bildern, um die jeweiligen Content- und Style-Merkmale zu erhalten –
# Note that the content and style images are loaded in
# content and style variables respectively
extractor = Custom_Style_Model(style_layers, content_layers)
style_targets = extractor(style)['style']
content_targets = extractor(content)['content']
In the paper, optimization was done using the L-BFGS algorithm, but you can use Adam also.
opt = tf.optimizers.Adam(learning_rate=0.02)
Definieren wir nun die globalen Gewichte für Content und Stil sowie die Gewichte für die einzelnen Stilrepräsentationen, wie zuvor besprochen. Hinweis: Diese Hyperparameter solltest du experimentell anpassen.
# Custom weights for style and content updates
style_weight=100
content_weight=10
# Custom weights for different style layers
style_weights = {'block1_conv1': 1.,
'block2_conv1': 0.8,
'block3_conv1': 0.5,
'block4_conv1': 0.3,
'block5_conv1': 0.1}
Jetzt kommt der wichtigste Teil, der Neural Style Transfer so spannend macht – die Verlustfunktion.
# The loss function to optimize
def total_loss(outputs):
style_outputs = outputs['style']
content_outputs = outputs['content']
style_loss = tf.add_n([style_weights[name]*tf.reduce_mean((style_outputs[name]-style_targets[name])**2)
for name in style_outputs.keys()])
# Normalize
style_loss *= style_weight / num_style_layers
content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2)
for name in content_outputs.keys()])
# Normalize
content_loss *= content_weight / num_content_layers
loss = style_loss + content_loss
return loss
Als Nächstes schreibst du eine Funktion, die Folgendes erledigt:
- Die Gradienten der soeben definierten Verlustfunktion berechnen.
- Mit diesen Gradienten das Zielbild aktualisieren.
Mit GradientTape nutzt du automatische Differenzierung, die Gradienten anhand der Funktionszusammensetzung berechnet. Außerdem verwenden wir den tf.function-Dekorator, um die Ausführung zu beschleunigen. Mehr dazu hier.
@tf.function()
def train_step(image):
with tf.GradientTape() as tape:
# Extract the features
outputs = extractor(image)
# Calculate the loss
loss = total_loss(outputs)
# Determine the gradients of the loss function w.r.t the image pixels
grad = tape.gradient(loss, image)
# Update the pixels
opt.apply_gradients([(grad, image)])
# Clip the pixel values that fall outside the range of [0,1]
image.assign(tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0))
Bevor du das Netzwerk trainierst, fehlt nur noch das Zielbild. Hier verwenden wir das Content-Bild.
target_image = tf.Variable(content)
Jetzt bist du startklar fürs Training.
epochs = 10
steps_per_epoch = 100
step = 0
for n in range(epochs):
for m in range(steps_per_epoch):
step += 1
train_step(target_image)
plt.imshow(np.squeeze(target_image.read_value(), 0))
plt.title("Train step: {}".format(step))
plt.show()
WARNING: Logging before flag parsing goes to stderr.
W0617 16:21:34.491543 140709216896896 deprecation.py:323] From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/math_grad.py:1205: add_dispatch_support.<locals>.wrapper (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.where in 2.0, which has the same broadcast rule as np.where
Spiele ruhig mit Content- und Style-Gewichten, um die Veränderungen im Zielbild zu beobachten.
Abschließende Hinweise
Das hier referenzierte Paper geht nicht auf eine weitere Optimierung der Bildqualität ein. Wie du in den Ergebnissen gesehen hast, braucht es teils mehr Glättung in der räumlichen Beziehung zwischen Inhalt und Stil des generierten Bildes. Um dieses Problem anzugehen, wurde der total_variational_loss eingeführt, der analog zu Regularisierung wirkt. Vereinfacht gesagt bestraft total_variational_loss hochfrequente Artefakte, die durch den ursprünglichen Neural-Style-Transfer-Algorithmus entstehen. Wenn du das implementieren willst, sieh dir dieses Tutorial an. Die folgenden Ressourcen wurden für dieses Tutorial herangezogen:
- Hands-On Transfer Learning with Python
- Neural style transfer
- Lesson on Style Transfer from Udacity
- Deep Learning for Computer Vision by Adrian Rosebrock (Part II)
Wenn du deine Deep-Learning-Kompetenzen ausbauen möchtest, schau dir diese DataCamp-Kurse und -Tutorials an:
