10  Architekturen

Nachdem wir die Grundmechanismen von Konvolutionsnetzen kennengelernt haben, sehen wir uns in diesem Kapitel einige ausgewählte Architekturen an. Einerseits sieht man so die Evolution der Architekturen, andererseits bekommt man eine Intuition dafür, wie man CNN selbst entwirft oder optimiert, z.B. wieviele und welche Schichten man verwendet. Schließlich lernen wir die Functional API von Keras kennen, die es erlaubt, die Verarbeitung zwischen den Schichten differenzierter zu steuern, um etwa skip connections wie im ResNet zu realisieren.

Nutzen Sie die Lernziele, um Ihr Wissen zu überprüfen:

  • Sie können den grundlegenden Aufbau eines realen Konvolutionsnetzes erklären
  • Sie können den Mechnismus hinter Skip Connections konzeptionell und mathematisch erklären
  • Sie können die Keras Functional API anwenden, um Netze zu bauen
  • Sie können die Unterschiede zwischen 1D-, 2D- und 3D-Konvolution erklären und können beispielhafte Berechnungen durchführen
Konzepte in diesem Kapitel

Skip Connections, Residual Network, Keras Functional API, 1D-Konvolution, 3D-Konvolution

  • 08.06.2024: Kleine Umformulierung in 10.2.3
  • 17.05.2024: Abschnitt 10.3 aus Kap. 11 hierher geschoben

10.1 Konvolutionsnetze

Natürlich stellt man sich die Frage, welche Schichten man wie anordnen soll. Leider gibt es kein generelles Rezept zum Konstruieren guter Netze, aber es hilft, sich erfolgreiche Architekturen aus der Forschung anzusehen. Insbesondere kann man oft erfolgreiche Netze aus einer Domäne in anderen Domänenen in ähnlicher Weise anwenden oder als Ausgangspunkt für eigene Weiterentwicklungen nehmen.

Wir sehen uns in diesem Abschnitt die folgenden Netzwerke an:

  • LeNet-5
  • AlexNet
  • VGG-16
  • ResNet

Eine ähnliche Darstellung finden Sie in dem Artikel The evolution of image classification explained von Afshine Amidi und Shervine Amidi (dort werden die Netze LeNet, AlexNet, VGGNet, GoogLeNet, ResNet und DenseNet besprochen).

10.1.1 LeNet-5 (1998)

LeNet-5 war der Vorläufer aller Konvolutionsnetze, die Publikation hat den Titel Gradient-Based Learning Applied to Document Recognition (Y. LeCun et al. 1998). Zwei bekannte Persönlichkeiten des Deep Learning, Yann LeCun und Yoshua Bengio, waren daran beteiligt. LeNet-5 ist ein Netz zur Handschriftenerkennung und realisiert die in Kapitel 8 vorgestellten Techniken:

  • Konvolutionsschichten mit mehreren Filtern
  • Pooling-Schichten
  • Backpropagation auf den Konv-Schichten

LeNet-5 kombiniert Konv- und Pooling-Schichten mit nachgelagerten FC-Schichten. Die Idee dahinter ist, dass die vorgelagerten Schichten Bildinformationen aus dem Bild extrahieren und verdichten und somit “gute” Eingabefeatures selbständig generieren. Diese Eingabefeatures werden anschließend in einem “konventionellen” Netz mit FC-Schichten zur Klassifikation benutzt.

Hier ein Auszug aus dem Abstract von Y. LeCun et al. (1998):

Multilayer neural networks trained with the back-propagation algorithm constitute the best example of a successful gradient based learning technique. Given an appropriate network architecture, gradient-based learning algorithms can be used to synthesize a complex decision surface that can classify high-dimensional patterns, such as handwritten characters, with minimal preprocessing. This paper reviews various methods applied to handwritten character recognition and compares them on a standard handwritten digit recognition task. Convolutional neural networks, which are specifically designed to deal with the variability of 2D shapes, are shown to outperform all other techniques. […]

Daten

Ziel des Netzes war die Erkennung von handgeschriebenen Ziffern in Form des MNIST-Datensatzes, wo die Bilder als 32x32-Bilder in Graustufen vorliegen, mit 60000 Trainingsbeispielen und 10000 Testbilder (in Keras liegen die Bilder im Format 28x28 vor, siehe Abschnitt 5.3.2).

LeNet-5 nutzte bereits Data Augmentation, bei der die Trainingsdaten um künstlich verzerrte Varianten (durch Translation, Skalierung, Scherung) erweitert werden, um Overfitting entgegen zu wirken (siehe Abschnitt 9.1).

Hier sieht man Beispiele der erzeugten Varianten:

Data Augmentation bei LeNet-5 (Quelle: Y. LeCun et al. 1998)

Architektur

Wie der Name LeNet-5 schon andeutet, hat das Netz fünf parametrisierte Schichten: Conv1, Conv2, FC1, FC2 und FC3.

LeNet-5 - Vorläufer aller Konvolutionsnetze

Es kommen zunächst zwei Konvolutionsschichten zum Einsatz, jeweils gefolgt von einer Pooling-Schicht, die hier den Durchschnitt verwenden, nicht das Maximum. Damals war das Konzept des Padding noch nicht präsent, so dass sich bei jeder Konvolutionsschicht die Bildgröße reduziert.

Am Ende der Pipeline sehen wir noch zwei FC-Schichten und die Ausgabeschicht mit Softmax-Funktion.

Das Netz hatte etwa 60.000 Parameter und ist somit für heutige Standards relativ bescheiden.

Training

Es wurden 20 Epochen für das Training auf 60.000 Beispielen durchlaufen (es wurden immer 60.000 Exemplare aus den Varianten der Data Augmentation herausgenommen).

Besonderheiten

Eine wichtige Systematik ist, dass entlang der Pipeline die Bildgröße immer weiter sinkt (von Kantenlänge 32 auf 28 auf 14 auf 10 auf 5), wohingegen die Zahl der Kanäle steigt (von 1 auf 6 auf 16).

Interessant ist, dass schon hier Data Augmentation verwendet wurde, allerdings wurde immer nur eine Teilmenge der erzeugten Varianten benutzt.

Geschichtliches

Das Paper von Y. LeCun et al. (1998) wurde zu einer Zeit geschrieben, als es CNNs erst im Entstehen waren, so dass sich das Paper etwas schwieriger liest als modernere Publikationen. Dieses Paper (und LeNet-5) wird fast immer als Ursprung von CNNs zitiert, aber der Begriff wurde schon deutlich vor 1998 eingeführt. Interessant ist es, Y. LeCun et al. (1989) zu lesen (Titel: Backpropagation Applied to Handwritten Zip Code Recognition), wo bereits von “feature maps” und “weight sharing” in der Funktionsweise der versteckten Schichten die Rede ist. Das erste Paper von LeCun, wo “Convolutional Networks” im Titel erscheinen, ist Yann LeCun and Bengio (1998): Convolutional Networks for Images, Speech, and Time-Series.

Kurzgefasst

LeNet-5
Jahr 1998
Input 32x32x1
Schichten 5
Epochen 20
Parameter 60 Tsd.

10.1.2 AlexNet (2012)

AlexNet kommt aus der Arbeitsgruppe des dritten Deep-Learning-Pioneers Geoffrey Hinton (Krizhevsky, Sutskever, and Hinton 2012). Das Netz ist offensichtlich an LeNet-5 angelehnt, aber war deutlich erfolgreicher und erzielte den Durchbruch der CNNs in der Domäne der Bildverarbeitung, genauer beim ImageNet-Wettbewerb ILSVRC-2012. AlexNet wurde nach dem Erstautoren Alex Krizhevsky benannt.

Die dazugehörige Publikation von Krizhevsky, Sutskever, and Hinton (2012) ist sehr berühmt und auch relativ leicht zu lesen.

Im Vergleich zu dem 10 Jahre älteren LeNet-5 führt AlexNet viele wichtige Neuerungen ein:

  • Mehr Schichten also eine größere Tiefe: 8 Schichten statt 5
  • GPU: Nutzung von Grafikkarten
  • Aktivierungsfunktion ReLU
  • Regularisierungsmethoden: Data Augmentation und Dropout
  • Komplexere und größere Menge an Daten (ImageNet)

Wenn Sie sich das Abstract des Papers ansehen, müssten Sie eigentlich alles verstehen:

We trained a large, deep convolutional neural network to classify the 1.2 million high-resolution images in the ImageNet LSVRC-2010 contest into the 1000 different classes. On the test data, we achieved top-1 and top-5 error rates of 37.5% and 17.0% which is considerably better than the previous state-of-the-art. The neural network, which has 60 million parameters and 650,000 neurons, consists of five convolutional layers, some of which are followed by max-pooling layers, and three fully-connected layers with a final 1000-way softmax. To make training faster, we used non-saturating neurons and a very efficient GPU implementation of the convolution operation. To reduce overfitting in the fully-connected layers we employed a recently-developed regularization method called “dropout” that proved to be very effective. We also entered a variant of this model in the ILSVRC-2012 competition and achieved a winning top-5 test error rate of 15.3%, compared to 26.2% achieved by the second-best entry.

Daten

ImageNet ist eine große Datenbank für die Community der Bilderkennung mit über 15 Millionen gelabelten hochauflösenden Bildern in etwa 22000 Kategorien. Seit 2010 führt das Team um ImageNet einen jährlichen Wettbewerb auf jeweils einer Teilmenge des Gesamtdatensatzes durch, genannt ILSVRC (ImageNet Large-Scale Visual Recognition Challenge). Die Datensätze der jeweiligen Challenge heißen dann ILSVRC-2010, ILSVRC-2011 etc.

Für AlexNet wurde ILSVRC-2010 verwendet, um das Netz zu evaluieren, da für diesen Datensatz die Testdaten bereits verfügbar waren (für die jeweils aktuelle Challenge wird der Testdatensatz natürlich geheim gehalten). ILSVRC-2010 ist eine Teilmenge von ca. 1,2 Million Bildern mit etwa 1000 Bildern in einer von 1000 Kategorien (= Label). Die Bilder wurden auf die Größe 256x256 runtergerechnet und lagen in Farbe (RGB, 3 Kanäle) vor.

Hier sehen wir Beispielbilder aus ILSVRC:

Acht Beispielbilder aus ILSVRC, bereits mit Klassifikationen durch AlexNet (Quelle: Krizhevsky, Sutskever, and Hinton 2012)

Architektur

Das gesamte Netz ist deutlich größer als LeNet-5 und hat acht Schichten mit Parametern, davon fünf Konvolutionsschichten. Warum die Eingabe die Größe 224x224 hat, erklären wir unten.

Wir sehen in der ersten Abbildung nur die Konvolutions- und Pooling-Schichten.

AlexNet - Das Konvolutionsnetz, das den Durchbruch für Neuronale Netze in der Bilderkennung bewirkte

Auch hier haben wir am Ende der Pipeline wieder FC-Schichten. Auch diese deutlich größer dimensioniert als bei LeNet-5. Zwischen FC1 und FC2 wurde Dropout eingesetzt (s.u.).

Das Netz hat ca. 60 Millionen Parameter, also das 100-fache der Parameterzahl in LeNet-5.

Hier sehen wir einige der gelernten Filter:

Gelernte Filter in der ersten Konvolutionsschicht (Quelle: Krizhevsky, Sutskever, and Hinton 2012)

Training

Für das Training wurde SGD verwendet mit Batchgröße 128, Momentum 0.9 und Weight Decay von 0.0005. Die Lernrate wurde auf 0.01 gesetzt und manuell um Faktor 10 reduziert, wenn die Accuracy auf den Validierungsdaten stagnierte (wurde 3x durchgeführt). Insgesamt wurde etwas 90 Epochen lang trainiert. Das Training auf den 1.2 Mill. Bildern dauerte 5-6 Tage auf zwei NVIDIA GTX 580 3GB GPUs.

Besonderheiten

AlexNet benutzte im Gegensatz zu LeNet-5 die mittlerweile sehr populäre ReLU-Aktivierungsfunktion.

Zudem werden zwei Methoden verwendet, um Overfitting abzumildern. Einerseits kam die im gleichen Team entwickelte Dropout-Methode zwischen den beiden FC-Schichten FC1 und FC2 zum Einsatz (mit \(p=0.5\)). Andererseits Data Augmentation eingesetzt, also der Datensatz künstlich um Varianten vergrößert. Eine einfache Form der Data Augmentation ist es, zufällige 224x244-Teilbilder des 256x256-Bildes zu nehmen. Zusätzlich werden horizontal gespiegelte Varianten erzeugt. Daher die veränderte Eingabedimension.

Ein wichtiger Aspekt des Systems war eine parallele Verarbeitung auf zwei GPUs, die das Training eines solchen Netzwerks in tolerabler Zeit erst ermöglichte.

Kurzgefasst - Netze im Vergleich

LeNet-5 AlexNet
Jahr 1998 2012
Input 32x32x1 256x256x3
Schichten 5 8
Epochen 20 90
Parameter 60 Tsd. 60 Mill.

10.1.3 VGG-16 (2015)

Beim dem Netz VGG-16 versuchten die Autoren systematisch Netzwerke mit sehr vielen Schichten zu konstruieren, die dennoch noch trainierbar sind (Simonyan and Zisserman 2015). Dazu wurde die Struktur der einzelnen Schichten bewusst einfach und gleichbleibend gewählt. Mehrere (2-3) gleiche, relativ einfache Konvolutionsschichten wurden jeweils hintereinandergeschaltet, so dass man diese als “Module” sehen kann. VGG steht für die Arbeitsgruppe der Autoren, der Visual Geometry Group der Oxford-Universität, UK. Das Netz wurde wie AlexNet auf den ILSVRC-Daten trainiert und getestet.

Im Vergleich zu AlexNet führte VGG-16 folgende Neuerungen ein:

  • Noch größere Tiefe mit 16 Schichten statt 8
  • Kleine Filter von 3x3 statt Filter bis zu 11x11 in AlexNet
  • Homogene Architektur mit vielen ähnlichen, sich wiederholenden Strukturen

Architektur

Das Netz hat 16 Schichten mit Parametern hat (13 Konvolutionsschichten und 3 FC-Schichten), deshalb auch der Name VGG-16.

Um die vielen Schichten handhabbar zu bekommen, verwendet jede Konvolutionsschicht einen minimal kleinen 3x3-Filter mit Stride 1 und Same-Padding. Hier ist die Idee, dass dies die kleinste Größe ist, die noch räumliche Struktur überhaupt noch abbilden kann. Jede Pooling-Schicht verwendet Max-Pooling mit einem 2x2-Filter und Stride 2.

In der Abbildung bedeutet 3x Conv, dass drei Konvolutionsschichten hintereinander geschaltet sind. Ansonsten sind die fixen Aspekte der Schichten (s.o.) weggelassen. Auch hier kam zwischen den FC-Schichten FC1 und FC2 Dropout mit \(p=0.5\) zum Einsatz.

Wir schauen auch hier in den Abstract des Papers hinein:

In this work we investigate the effect of the convolutional network depth on its accuracy in the large-scale image recognition setting. Our main contribution is a thorough evaluation of networks of increasing depth using an architecture with very small (3 × 3) convolution filters, which shows that a significant improvement on the prior-art configurations can be achieved by pushing the depth to 16–19 weight layers. These findings were the basis of our ImageNet Challenge 2014 submission, where our team secured the first and the second places in the localisation and classification tracks respectively. […]

VGG-16 - Versuch, möglichst tiefe Netze zu verwenden

Dieses Netz hat ca. 138 Millionen Parameter, also doppelt so viele Parameter wie AlexNet.

Training

Für das Training wurde SGD verwendet mit Batchgröße 256, Momentum 0.9 und Weight Decay von 0.0005. Die Lernrate wurde auf 0.01 gesetzt und manuell um Faktor 10 reduziert, wenn die Accuracy auf den Validierungsdaten stagnierte (wurde 3x durchgeführt). Insgesamt wurde 74 Epochen lang trainiert. Das Training dauerte auf 4 NVIDIA Titan Black GPUs ca. 2-3 Wochen.

Besonderheiten

Auch hier reduziert sich die Bildgröße sukzessive in der Pipeline (von Kantenlänge 224 bis 7), wohingegen sich die Zahl der Kanäle steigt, sich sogar immer wieder verdoppelt (von 3 zu 64 zu 128 zu 256 zu 512).

Das Nachfolgenetzwerk VGG-19 ist nochmal tiefer hat aber keine deutliche bessere Performance.

Kurzgefasst - Netze im Vergleich

LeNet-5 AlexNet VGG-16
Jahr 1998 2012 2015
Input 32x32x1 256x256x3 256x256x3
Schichten 5 8 16
Epochen 20 90 74
Parameter 60 Tsd. 60 Mill. 138 Mill.

10.1.4 ResNet (2015)

ResNet stammt von Microsoft Research und gewann den ILSVRC 2015 Wettbewerb (He et al. 2016). Die Neuerungen des ResNet sind

  • Größere Tiefe mit bis zu 152 Schichten
  • Einführung von skip connections (shortcut connections), die Schichte überspringen (man spricht auch von residual learning)
  • Verwendung von Batch Normalization siehe 9.4

Wir schauen uns wieder das Abstract des Papers an (in Auszügen):

Deeper neural networks are more difficult to train. We present a residual learning framework to ease the training of networks that are substantially deeper than those used previously. […] The depth of representations is of central importance for many visual recognition tasks. Solely due to our extremely deep representations, we obtain a 28% relative improvement on the COCO object detection dataset. Deep residual nets are foundations of our submissions to ILSVRC & COCO 2015 competitions, where we also won the 1st places on the tasks of ImageNet detection, ImageNet localization, COCO detection, and COCO segmentation.

Ein Grund, warum man nicht beliebig tiefe Netze konstruiert (abgesehen von der Trainingsdauer), ist das Problem des Vanishing Gradient (verschwindender Gradient), das Sepp Hochreiter 1991 in seiner Diplomarbeit bei Jürgen Schmidhuber an der TU München zum ersten Mal formal beschrieben hat. Es geht darum, dass beim Lernen mit Backpropagation die Gradienten (und damit die Gewichtsänderung) über die Schichten hinweg so klein werden, dass sie verschwinden und die Gewichte in den vorderen Schichten sich nicht mehr ändern. Das Training stagniert in diesem Fall. Es ist empirisch nachgewiesen, dass das Hinzufügen von Schichten sogar der Performance schadet. In der folgenden Abbildung sehen Sie - nur als Beispiel - die Performance von zwei Netzen im Vergleich. Das Netz mit 56 Schichten ist deutlich schwächer als das Netz mit 20 Schichten (die y-Achse stellt hier den Fehler dar).

Quelle: He et al. (2016)

Das Phänomen des “vanishing gradient” (Werte werden zu klein) bzw. des “exploding gradient” (Werte werden zu groß) tritt sowohl bei Forward Propagation auf (hier sind die Aktivierungen das Problem) sowie bei Backpropagation auf (dort sind es die Gradienten).

Bei einem Residual Network - kurz ResNet - ist die zentrale Idee, dass eine Verbindung zwischen zwei Neuronen-Schichten überspringen kann. Man nennt solche Verbindungen skip connections oder shortcut connections. Das Überspringen soll zu kleine Werte verhindern, indem Werte zur übernächsten Schicht “hinübergerettet” werden.

Wir erinnern uns an die Forward-Propagation-Formeln eines FNN, wo \(l\) die Schicht bezeichnet:

\[ \begin{align} z^{(l)} &:= W^{(l)}\, a^{(l-1)} + b^{(l)}\tag{Roheingabe}\\[3mm] a^{(l)} &:= g(z^{(l)}) \tag{Aktivierung} \end{align} \]

Skip Connections und Residual Block

In einem ResNet erlauben wir den Neuronen in Schicht \(l\) die folgende Schicht \(l+1\) zu überspringen. Die Aktivierungen \(a^{(l)}\) werden einfach auf den Rohinput der übernächsten Schicht \(z^{(l+2)}\) addiert. Wir nennen die Schichten \(l+1\) und \(l+2\), wo ein solcher Sprung verwendet wird, einen Residual Block. Die Sprünge nennen wir Skip Connections (dieses Konzept gab es schon vor He et al. 2016).

Jetzt müssen wir die Formel für \(l+2\) anpassen, indem wir \(a^{(l)}\) dazu addieren:

\[ z^{(l+2)} := W^{(l+2)}\, a^{(l+1)} + b^{(l+2)} + a^{(l)} \]

In diesem Fall muss die Länge von \(a^{(l)}\) und die Länge von \(z^{(l+2)}\) gleich sein, d.h. Schichten \(l\) und \(l+2\) müssen gleich viele Neuronen enthalten.

Vielleicht wird das klarer, wenn wir uns konkrete Zahlen ansehen. In der folgenden Abbildung hat die Schicht \(l\) zwei Neuronen, Schicht \(l+1\) drei Neuronen und Schicht \(l+2\) wieder zwei Neuronen. Sie sehen, dass Sie die Vektoren \(a^{(l)}\) und \(z^{(l+2)}\) addieren können, da beide die Länge 2 haben. Hätten wir in Schicht \(l+2\) zum Beispiel 4 Neuronen, würde das nicht gehen.

Wenn das nicht der Fall ist, kann man mit Hilfe eines einfachen Mappings über eine Matrix \(W_s\) mit Dimension \((n_{l+2}, n_l)\) die Größen angleichen:

\[ z^{(l+2)} := W^{(l+2)}\, a^{(l+1)} + b^{(l+2)} + W_s \, a^{(l)} \]

\(W_s\) könnte Werte einfach zusammenaddieren oder fehlende Werte mit Null besetzen (Padding).

In unserem konkreten Beispiel oben könnten wir uns vorstellen, wir hätten 4 Neuronen in Schicht \(l+2\). Wir müssten dann diese Vektoren addieren:

\[ a^{(l)} = \begin{pmatrix} a^{(l)}_1 \\ a^{(l)}_2 \end{pmatrix} \quad\quad z^{(l+2)} = \begin{pmatrix} z^{(l+2)}_1 \\ z^{(l+2)}_2 \\ z^{(l+2)}_3 \\ z^{(l+2)}_4 \end{pmatrix} \]

Dazu bräuchten wir eine 4x2-Matrix, zum Beispiel:

\[ W_s = \begin{pmatrix} 1 & 0 \\ 0 & 1 \\ 0 & 0 \\ 0 & 0 \end{pmatrix} \]

Prüfen Sie gern nach, was genau \(W_s\) mit dieser Belegung bewirkt.

Architektur

ResNet hat bis zu 152 Schichten. Das ResNet wurde so konstruiert, dass ein herkömmliches FNN (plain network) in ein ResNet verwandelt wurde, indem Residual Blocks eingebaut wurden. Hier ist ein Beispiel mit 34 Schichten:

ResNet - sehr tiefe Netze mit Verbindungen, die Schichten überspringen (Quelle: He et al. (2016))

(Quelle: He et al. 2016)

Training

Für das Training wurde SGD verwendet mit Batchgröße 256, Momentum 0.9 und Weight Decay von 0.0001. Die Lernrate wurde auf 0.01 gesetzt und manuell um Faktor 10 reduziert, wenn die Accuracy auf den Validierungsdaten stagnierte.

Netze im Vergleich

LeNet-5 AlexNet VGG-16 ResNet
Jahr 1998 2012 2015 2015
Input 32x32x1 256x256x3 256x256x3 256x256x3
Schichten 5 8 16 152
Epochen 20 90 74
Parameter 60 Tsd. 60 Mill. 138 Mill.

10.2 Keras 4: Functional API und ResNet

Mit den Mechanismen, die wir bisher in Keras gesehen haben, kann man ein ResNet nicht erzeugen. Wir schauen uns daher die Functional API an.

Normalerweise baut man in Keras ein Modell mit der Klasse Sequential Schicht für Schicht auf. Es ist implizit, dass jede neue Schicht mit der zuletzt hinzugefügten verbunden ist (mit Ausnahme der ersten Schicht). Mit “Verbindung” meinen wir hier alle Verbindungen zwischen den Neuronen zweier Schichten. Man kann also nicht zum Beispiel eine Schicht überspringen.

Die Functional API erlaubt es, die Verbindungen zwischen den Schichten explizit zu konfigurieren. Dies wird zum Beispiel für skip connections (residual connections) in ResNets benötigt. Aus der Keras-Doku:

In addition to models with multiple inputs and outputs, the functional API makes it easy to manipulate non-linear connectivity topologies – these are models with layers that are not connected sequentially. Something the Sequential API can not handle. A common use case for this is residual connections.

Allgemein gesprochen erlaubt die Functional API die Definition eines directed acyclic graph (DAG) von Schichten (d.h. ein Knoten entspricht einer Schicht, eine Kante einer Verbindung zwischen Schichten).

Siehe auch die Keras-Doku Functional API

10.2.1 Konvolutionsnetz mit Sequential

Wir sehen uns ein Beispiel für ein kleines CNN an, das wir zunächst auf die gewohnte Weise mit der Klasse Sequential definieren, damit wir später den Unterschied zur Functional API besser verstehen.

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import Sequential
from tensorflow.keras import layers
from tensorflow.keras.layers import Dense

model = Sequential()
model.add(Dense(64, name='FC_1', activation='relu', input_shape=(28,28,3)))
model.add(Dense(64, name='FC_2', activation='relu'))
model.add(Dense(10, name='FC_3', activation='softmax'))

model.summary()
Model: "sequential_1"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 FC_1 (Dense)                (None, 28, 28, 64)        256       
                                                                 
 FC_2 (Dense)                (None, 28, 28, 64)        4160      
                                                                 
 FC_3 (Dense)                (None, 28, 28, 10)        650       
                                                                 
=================================================================
Total params: 5,066
Trainable params: 5,066
Non-trainable params: 0
_________________________________________________________________

10.2.2 Gleiches Netz mit der Functional API

Wir bauen jetzt Schritt für Schritt das gleiche Netz mit Hilfe der Functional API auf.

Wir erzeugen zunächst die Eingabeschicht.

from tensorflow.keras import Input

inputs = Input(shape=(28,28,3))
inputs.shape
TensorShape([None, 28, 28, 3])

Wir erzeugen unsere erste versteckte Schicht:

fc1 = Dense(64, name='FC_1', activation='relu')

Erst hier ziehen wir die Verbindung zwischen den Inputs und der ersten Schicht:

out1 = fc1(inputs)

Die Variable out1 enthält jetzt den Ausgang von FC 1.

Jetzt können wir die zweite FC-Schicht definieren und direkt durch den “Aufruf” auf out1 angeben, dass out1 die Eingabe für FC 2 ist.

out2 = Dense(64, name='FC_2', activation='relu')(out1)

Man beachte, dass durch diesen Aufruf der Ausgang von FC 2 zurückgegeben wird, nicht die Schicht FC 2. Vielleicht hilft der Vergleich mit der alternativen “Langversion”, wo erst die Schicht erzeugt wird und dann der Aufruf durchgeführt wird.

fc2 = Dense(64, name='FC_2', activation='relu')
out2 = fc2(out1)

In der vorigen Version sparen wir uns die Variable fc2, da wir sie später nicht mehr benötigen.

Wir definieren die Ausgabeschicht mit out2 als Eingabe:

outputs = Dense(10, name='FC_3', activation='softmax')(out2)

Jetzt spezifizieren wir das Modell durch Angabe von Inputs und Outputs:

from tensorflow.keras import Model

model = Model(inputs=inputs, outputs=outputs, name='SimpleCNN')

model.summary()
Model: "SimpleCNN"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_1 (InputLayer)        [(None, 28, 28, 3)]       0         
                                                                 
 FC_1 (Dense)                (None, 28, 28, 64)        256       
                                                                 
 FC_2 (Dense)                (None, 28, 28, 64)        4160      
                                                                 
 FC_3 (Dense)                (None, 28, 28, 10)        650       
                                                                 
=================================================================
Total params: 5,066
Trainable params: 5,066
Non-trainable params: 0
_________________________________________________________________

Jetzt noch einmal in einem Stück und etwas kompakter (die Variable out wird hier mehrfach verwendet):

inputs = Input(shape=(28,28,3))
out = Dense(64, name='FC_1', activation='relu')(inputs)
out = Dense(64, name='FC_2', activation='relu')(out)
out = Dense(10, name='FC_3', activation='softmax')(out)
model = Model(inputs=inputs, outputs=out, name='SimpleCNN2')
model.summary()
Model: "SimpleCNN2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 input_2 (InputLayer)        [(None, 28, 28, 3)]       0         
                                                                 
 FC_1 (Dense)                (None, 28, 28, 64)        256       
                                                                 
 FC_2 (Dense)                (None, 28, 28, 64)        4160      
                                                                 
 FC_3 (Dense)                (None, 28, 28, 10)        650       
                                                                 
=================================================================
Total params: 5,066
Trainable params: 5,066
Non-trainable params: 0
_________________________________________________________________

Wir sehen uns als nächstes ein echtes ResNet an, wo tatsächlich Skip Connections zum Einsatz kommen. Die Functional API wird auch manchmal eingesetzt, obwohl nur sequentielle Schichten benutzt werden. Man sollte diese “Schreibweise” also kennen.

10.2.3 Ein ResNet mit der Functional API

Wir sehen uns das Beispiel-ResNet aus der Keras-Dokumentation an. Ich habe die Variablen im Vergleich zum Original umbenannt, um (hoffentlich) die Zuweisungslogik etwas klarer zu machen.

Achten Sie auf die Stellen mit layers.add: Dort werden die Ausgaben von zwei Schichten einfach zusammenaddiert. Hier wird die Logik der skip connections realisiert.

from tensorflow.keras.layers import Conv2D, MaxPooling2D, GlobalAveragePooling2D, Dropout

inputs = Input(shape=(32, 32, 3), name="image")
out1 = Conv2D(32, 3, name='Conv_1', activation="relu")(inputs)
out2 = Conv2D(64, 3, name='Conv_2',activation="relu")(out1)
block1out = MaxPooling2D(3, name='Pool_1')(out2)

out3 = Conv2D(64, 3, name='Conv_3', activation="relu", padding="same")(block1out)
out4 = Conv2D(64, 3, name='Conv_4', activation="relu", padding="same")(out3)
block2out = layers.add([out4, block1out])

out5 = Conv2D(64, 3, name='Conv_5', activation="relu", padding="same")(block2out)
out6 = Conv2D(64, 3, name='Conv_6', activation="relu", padding="same")(out5)
block3out = layers.add([out6, block2out])

out7 = Conv2D(64, 3, name='Conv_7', activation="relu")(block3out)
out8 = GlobalAveragePooling2D(name='Pool_2')(out7)
out9 = Dense(256, name='FC_1', activation="relu")(out8)
out10 = Dropout(0.5)(out9)
outputs = Dense(10, name='FC_2')(out10)

model = Model(inputs, outputs, name="toy_resnet")
model.summary()
Model: "toy_resnet"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
image (InputLayer)              [(None, 32, 32, 3)]  0                                            
__________________________________________________________________________________________________
Conv_1 (Conv2D)                 (None, 30, 30, 32)   896         image[0][0]                      
__________________________________________________________________________________________________
Conv_2 (Conv2D)                 (None, 28, 28, 64)   18496       Conv_1[0][0]                     
__________________________________________________________________________________________________
Pool_1 (MaxPooling2D)           (None, 9, 9, 64)     0           Conv_2[0][0]                     
__________________________________________________________________________________________________
Conv_3 (Conv2D)                 (None, 9, 9, 64)     36928       Pool_1[0][0]                     
__________________________________________________________________________________________________
Conv_4 (Conv2D)                 (None, 9, 9, 64)     36928       Conv_3[0][0]                     
__________________________________________________________________________________________________
add (Add)                       (None, 9, 9, 64)     0           Conv_4[0][0]                     
                                                                 Pool_1[0][0]                     
__________________________________________________________________________________________________
Conv_5 (Conv2D)                 (None, 9, 9, 64)     36928       add[0][0]                        
__________________________________________________________________________________________________
Conv_6 (Conv2D)                 (None, 9, 9, 64)     36928       Conv_5[0][0]                     
__________________________________________________________________________________________________
add_1 (Add)                     (None, 9, 9, 64)     0           Conv_6[0][0]                     
                                                                 add[0][0]                        
__________________________________________________________________________________________________
Conv_7 (Conv2D)                 (None, 7, 7, 64)     36928       add_1[0][0]                      
__________________________________________________________________________________________________
Pool_2 (GlobalAveragePooling2D) (None, 64)           0           Conv_7[0][0]                     
__________________________________________________________________________________________________
FC_1 (Dense)                    (None, 256)          16640       Pool_2[0][0]                     
__________________________________________________________________________________________________
dropout (Dropout)               (None, 256)          0           FC_1[0][0]                       
__________________________________________________________________________________________________
FC_2 (Dense)                    (None, 10)           2570        dropout[0][0]                    
==================================================================================================
Total params: 223,242
Trainable params: 223,242
Non-trainable params: 0
__________________________________________________________________________________________________

Visualisierung

Sie können plot_model benutzen, um sich den Graphen zeichnen zu lassen. Zuvor müssen Sie pydot installieren.

from tensorflow.keras.utils import plot_model

plot_model(model, "functional_api_net.png", show_shapes=True)

Empfohlen sei noch dieser Artikel mit interessanten Netz-Beispielen (shared layers, multiple input, multiple output): https://machinelearningmastery.com/keras-functional-api-deep-learning

10.3 Konvolutionsnetze in 1D und 3D

Sie mögen sich gefragt haben, warum die Konv-Schicht in Keras Conv2D heißt. Wenn Sie in der Keras-Doku unter Convolution layers nachsehen, finden Sie dort auch Conv1D und Conv3D. Was hat es damit auf sich?

Um unser Gedächtnis aufzufrischen, schauen wir uns die 2D-Konvolution an. Hier mit einem 16x16-Bild mit 3 Kanälen und einem 3x3-Filter. Wichtig ist, dass die Kanäle nicht als dritte Dimension betrachtet wird, sondern als “Stapel” für unterschiedliche Ausprägungen des selben 2D-Bildes.

Wir erinnern uns, dass der 3x3x3-Filter die 3 Kanäle “zusammendampft”. Daher haben wir in der Ausgabe nur einen Kanal. Hätten wir mehrere Filter, zum Beispiel 16 Filter, hätten wir in der Ausgabe eine 14x14x16-Matrix.

Außerdem zu beachten: Da wir kein Padding verwenden, verlieren wir an jeder Seite einen Pixel, daher die Reduktion von 16x16 auf 14x14.

Schematisch stellen wir das als Konv-Schicht wie folgt dar:

10.3.1 Eindimensionale Konv-Schicht

Stellen Sie sich einen Audio-Stream vor (z.B. ein Musikstück, ein Interview oder ein Hörbuch). Audiodaten werden als Serie von Zahlen (sogenannten Samples) gespeichert. Das ist ein typisches Beispiel für eindimsionale Daten: ein Array von Zahlen.

Wir können die Konvolutionsoperation sehr einfach auf 1D-Daten übertragen. Als Beispiel nehmen wir einen Eingabe-Array der Länge 16 und einen Filter der Länge 3. Wie im 2-Dimensionalen verlieren wir einen Pixel an jedem Ende des Arrays, so dass das Ergebnis die Länge 14 hat.

Auch hier könnten wir mehrere Kanäle haben (z.B. bei einem Stereo-Signal “Kanal links” und “Kanal rechts”). Entsprechend passt sich der Filter an und dampft die zwei Kanäle ein, so dass die Ausgabe wieder nur einen Kanal hat.

Jetzt kehren wir wieder zu dem Beispiel mit einem Kanal zurück:

Sehen wir uns die Möglichkeit an, mehrere Filter - zum Beispiel 5 Filter - parallel anzuwenden. Dann wirkt sich das, wie in 2D, auf die Kanaltiefe der Ausgabe aus, die dann auch 5 beträgt.

Schematisch kann man eine 1-dimensionale Konv-Schicht so darstellen.

Nachdem wir 1-dimensionale und 2-dimensionale Konvolution kennen, schauen wir uns das ganze in 3D an.

10.3.2 Dreidimensionale Konv-Schicht

Wie schon eingangs erwähnt, hatten wir es bislang nur mit 2D-Bildern zu tun. Die Farbkanäle (oder auch andere Kanäle wie IR) kodieren eine andere Art der Information und zählen somit nicht als dritte Bilddimension. Echte 3D-Daten treffen wir hingegen in der Medizintechnik oder bei Geodaten an.

Ein “echtes” 3D-Bild ist zum Beispiel ein CT-Scan eines Gehirns (CT = Computertomographie). Bei einem solchen Scan wird quasi eine 2D-Ebene durch das Gehirn geschoben und in regelmäßigen Abständen eine 2D-Aufnahme auf dieser Ebene angefertigt. Anschließend liegt die Aufnahme als eine Reihe hintereinander liegender “Scheiben” vor, insgesamt ergibt sich so ein 3D-Bild. Beim CT-Scan liegt - im Gegensatz zum 2D-Bild mit Farbkanälen - in allen drei Dimensionen die gleiche Art räumlicher Information vor.

Hier eine typische CT-Aufnahme mit vier “Scheiben”:

Quelle: Zhenyu Pan, Guozi Yang, Tingting Yuan, Lihua Dong, Lihua Dong, Lizenz: CC-BY 2.0

Wenn wir uns der Konvolution zuwenden, haben wir in 3D statt Matrizen eher so etwas wie Würfel oder Quader vor Augen. Man spricht auch von volumetrischen Daten. Statt Pixel für eine Zelle spricht man hier von einem Voxel.

Wir sehen uns mal an einem noch einfacherem Beispiel an, wie Konvolution berechnet wird. Die Eingabe ist ein 3x3x3-Tensor, der Filter hat die Größe 2x2x2. Die Kanaltiefe setzen wir auf eins und wir wenden auch nur einen Filter an.

Der 3D-Filter (2x2x2) läuft nicht nur nach rechts (x-Achse) und nach unten (y-Achse), sondern auch in die Tiefe (z-Achse) durch den 3D-Eingabetensor.

Das Ergebnis ist ein 2x2x2-Tensor.

Man kann sich hoffentlich vorstellen, welche Auswirkungen es hat, wenn man:

  1. Mehrere Kanäle hat: Zum Beispiel 2 Kanäle - man stelle sich einen Stapel mit 2 Würfeln vor. Außerdem hat man 2 Filter, welche die 2 Kanäle eindampfen.
  2. Mehrere Filter hat: Zum Beispiel 5 Filter - man stelle sich einen Stapel mit 5 Filterwürfeln vor. Entsprechend hat man als Ausgabe einen Stapel von 5 Würfeln.

Wichtig ist, dass klar ist, dass eine echte 3D-Eingabe mit Kanaltiefe 1 nicht äquivalent ist zu einer 2D-Eingabe mit Kanaltiefe 3. Die folgenden Abbildungen versuchen, das zu verdeutlichen:

Schematisch kann man eine 3-dimensionale Konv-Schicht wie folgt darstellen:

Damit sollten Sie gut gerüstet sein, Konvolutionen im 1-, 2- und 3-Dimensionalen einzusetzen.

10.4 Weiterführende Themen

10.4.1 Was lernen Konvolutionsnetze?

Wie kann ich herausfinden, wofür genau ein bestimmtes Neuron in einer versteckten Schicht “zuständig” ist? Zeiler and Fergus (2014) hatten dazu folgenden Gedankengang:

  • eine hohe Aktivierung ist ein Zeichen dafür, dass ein Neuron auf ein Eingabemuster reagiert
  • in einem CNN ist ein einzelnes Neuron nur für ein paar wenige Eingabepixel zuständig

Wenn Sie sich nochmal diese Abbildung ansehen:

Das Neuron \(z_1\) hat nur ein kleines Feld (\(a_1, a_2, a_4, a_5\)), von dem es Input bekommt. Genauer gesagt hat dieses Feld die Größe des Filters. Bei eine 5x5-Filter bekommt das Neuron 25 Inputs.

Daher kann man bei einem trainierten Netz alle Trainingsbeispiele durchlaufen lassen und solche Muster heraussuchen, die eine besonders hohe Aktivierung des Neurons verursachen. Diese Muster (z.B. die neun Muster mit der höhsten Aktivierung) kann man anschließend darstellen.

In dem Paper wird experimentell untersucht, welche Muster in den Filtern gelernt werden. Hier sehen wir einige Filter (links) und die dazugehörigen Bildteile aus den Daten (rechts). Zunächst einer “frühen” Schicht:

Man sieht, dass hier eher abstrakte Muster gelernt werden.

In einer späteren Schicht werden dann schon deutlich komplexere Muster gelernt:

Beide Abbildungen sind aus Zeiler and Fergus (2014).

10.4.2 Transfer Learning

Unter Transfer Learning versteht man die Idee, dass ein Modell, das für Aufgabe A trainiert wurde, auch für eine andere Aufgabe B verwendet werden kann. Alternativ kann das Modell auch auf Aufgabe A vortrainiert werden (pre-training) und dann auf Aufgabe B weitertrainiert werden (siehe u.a. Yosinski et al. 2014; Donahue et al. 2014).

In der Bildverarbeitung beruht diese Idee auf der Vorstellung, dass frühe Schichten (näher an der Inputschicht) eher abstraktere Informationen abbilden (z.B. Diagonalen) und diese Schichten daher auch für andere Aufgaben nützlich sind; wie wir in dem Abschnitt oben und bei Zeiler and Fergus (2014) gesehen haben.

Mehr Infos finden Sie in dem Artikel A Gentle Introduction to Transfer Learning for Deep Learning von Jason Brownlee (2017).