Updates dieser Seite:

  • 20.03.2022: v1 neues Semester PDF

Überblick

Nachdem wir die Grundmechanismen von CNNs kennengelernt haben, sehen wir uns in diesem Kapitel die Methode Dropout und einige ausgewählte CNN-Architekturen an. Einerseits wollen wir so die historische Entwicklung der CNN-Architekturen kennen lernen, andererseits bekommen wir einen Eindruck davon, wie die Schichten in einem CNN aufgebaut sein müssen. Schließlich lernen wir die Functional API von Keras kennen, die es erlaubt, die Verarbeitung zwischen den Schichten differenzierter zu steuern.

Konzepte

Dropout, Data Augmentation, Skip Connections, Residual Network, Keras Functional API

1 Methoden gegen Overfitting

1.1 Data Augmentation

Das Problem des Overfittings besteht darin, dass ein Modell sich zu stark an die Trainingsdaten anpasst. So werden bestimmte Eigenarten eines Trainingsbildes zu bestimmenden Faktoren bei der Klassifikation. Klar ist, dass eine größere Menge an Daten immer hilft und daher ist eine Idee, die Menge der Trainingsdaten künstlich zu erhöhen, indem man systematisch Varianten von jedem Trainingsbeispiel herstellt.

Bei Bildern kommen zum Beispiel folgende Manipulationen in Frage, um Varianten herzustellen:

  • Verschiebung
  • Drehung
  • Skalierung (Vergrößern, Verkleinern)
  • Verzerrung (z.B. Scherung oder nur entlang einer Achse vergrößern)
  • Spiegelung

Diese Operationen können einzeln oder in Kombination angewandt werden, in der Regel mit zufälligen Parametern (z.B. Drehwinkel oder Verschiebungsvektor).

Zu beachten ist, dass die Bildgröße dabei gleich bleiben muss. Bei einigen Operationen wie Drehung oder Scherung entstehen "Lücken" im Bild, die entsprechend einer Systematik aufgefüllt werden müssen, z.B. indem man die nächsten Randpixel einfach kopiert.

Die folgende Abbildung zeigt ein Originalbild (links oben) und entsprechende Varianten.

Quelle: https://towardsdatascience.com/image-augmentation-for-deep-learning-histogram-equalization-a71387f609b2

Data Augmentation wurde u.a. bei den Netzen LeNet-5 und AlexNet eingesetzt, die Sie auch in diesem Kapitel beschrieben finden.

1.2 Data Augmentation in Keras [optional]

In Keras stehen Ihnen Mechanismen zur Verfügung, um Data Augmentation relativ leicht einzusetzen.

Keras bietet dazu spezialisierte Preprocessing-Schichten an, die eine Vorverarbeitung wie Skalierung im Prozess der Forward Propagation einbauen. In diesem Fall findet also die Variantenbildung im Netz selbst statt (natürlich nur beim Training).

Siehe dazu: https://www.tensorflow.org/tutorials/images/data_augmentation

Ein zweite Möglichkeit ist die Variantenbildung während der Bereitstellung der Trainingsdaten. Die Klasse ImageDataGenerator (Paket tensorflow.keras.preprocessing.image) stellt beim Training die Trainingsbeispiele bereits (als Batches) und nimmt die Variantenbildung automatisiert vor. Man kann dort verschiedene Operationen mit entsprechenden Grenzwerten (z.B. für den Drehwinkel) definieren und beim Training werden dann die Varianten on-the-fly gebildet.

Siehe auch: https://keras.io/api/preprocessing/image

1.3 Dropout

Dropout ist eine sehr effektive Methode gegen Overfitting aus der Arbeitsgruppe von Geoffrey Hinton (University of Toronto) mit der Idee, ein Netz in jedem Batch-Durchlauf nur mit einer zufälligen Teilmenge der Neuronen zu trainieren (Hinton et al. 2012, Srivastava et al., 2014).

Ein Gedanke dahinter ist, dass ein einzelnes Neuron so lernen muss, in verschiedenen Kontexten zu arbeiten, weil mal dieses Nachbarneuron ausfällt und mal jenes. Es kann sich also nicht darauf verlassen, mit einem ganz bestimmten Neuron eine klare Aufgabenteilung einzurichten (das nennt man auch co-adaptation, daher das preventing co-adaptation im Titel von Hinton et al. 2012). Umgekehrt heißt das, die Fähigkeit zur Generalisierung bzw. zur Zusammenarbeit mit vielen andern Neuronen, wird gestärkt.

Methode

Jedes Neuron wird mit einer bestimmten Wahrscheinlichkeit ein- oder ausgeschaltet. Wir nehmen $p$ als Wahrscheinlichkeit, dass ein Neuron aktiv ist. Wenn z.B. $p = 0.5$ ist, dann ist im Mittel immer nur die Hälfte des Netzwerks aktiv, d.h. das Netz ist ausgedünnt.

Hier sehen wir ein Beispiel:

(Quelle: Srivastava et al. 2014)

In jedem Trainingsdurchlauf wird das ausgedünnte Netz erst neu bestimmt und dann trainiert. Man kann sich überlegen, dass es bei $n$ Neuronen $2^n$ verschiedene Varianten von ausgedünnten Netzen gibt. Man kann sich also vorstellen, dass $2^n$ verschiedene Netze gleichzeitig trainiert werden und sich dabei die Gewichte teilen.

In unseren Formeln für Forward Propagation haben wir folgende Formel für die Aktivierungen:

$$ a^{(l)} = g(z^{(l)}) = \left( \begin{array}{c} g(z_1^{(l)}) \\ \vdots \\ g(z_{n_l}^{(l)}) \end{array} \right)$$

Hier würden wir einen Bernoulli-Vektor $B(p)$ einbauen, der Nullen und Einsen mit der Wahrscheinlichkeitsverteilung $p$ (für die Einsen) enthält. Ein Beispiel für einen Bernoulli-Vektor mit $p=0.75$ wäre

$$ B(0.75) = \begin{pmatrix} 1 \\ 1 \\ 0 \\ 1 \end{pmatrix} $$

Dann können wir die Aktivierung $\tilde{a}$ unseres ausgedünnten Netzes schreiben als:

$$ \tilde{a}^{(l)} = B(p) \odot \left( \begin{array}{c} g(z_1^{(l)}) \\ \vdots \\ g(z_{n_l}^{(l)}) \end{array} \right)$$

Dabei ist $\odot$ die elementweise Multiplikation, d.h. es werden ganz einfach einige der Komponenten des rechten Vektors "ausgeschaltet" (= auf Null gesetzt).

Backpropagation

Beim Training wird für jeden Minibatch-Durchlauf eine Konfiguration über die Bernoulli-Vektoren hergestellt und Backpropagation wird genau auf diesem ausgedünnten Netz durchgeführt. Das heißt, es werden bei Backpropagation nur die Gewichte zwischen den "aktiven" Neuronen angepasst.

Methoden wie Momentum und Decay können auch zusammen mit Dropout angewendet werden.

Vorhersagen

Beim Testen - oder allgemein beim Berechnen einer Vorhersage - können für eine Eingabe nicht alle $2^n$ möglichen Netze durchlaufen werden. Stattdessen wird das vollständige Netz genommen und jedes Gewicht mit dem Faktor $p$ multipliziert, also effektiv runterskaliert. Das ist notwendig, weil beim Training die Rohinputs eine um $p$ reduzierte "Masse" an Eingaben der vorigen Schicht bekommen haben (also zum Beispiel nur die Hälfte bei $p=0.5$).

In unseren Formeln für Forward Propagation müsste $p$ also beim Rohinput eingebaut werden, zum Beispiel so:

$$ z_i^{(l)} = p \, \sum_{j=1}^{n_{l-1}} w^{(l-1)}_{i,j} \; a_j^{(l-1)} + b_i^{(l-1)} $$

Wie schon erwähnt brauchen wir dies, weil im Training weniger Neuronen/Gewichte aktiv waren und beim vollständigen Netz sonst viel mehr Aktivierung aufkommt als im Training. Bei $p=0.75$ sind nur 75% aller Neuronen im Training aktiv gewesen, also reduzieren wir die Aktivität bei der Vorhersage um 25%.

1.4 Dropout in Keras

In Keras kann man Dropout an bestimmte Schichten hängen. Dropout wird als eigene Schicht umgesetzt - eine sogenannte Regularisierungsschicht. Für jede Dropout-Schicht legt man den Wert $p$ fest.

Wichtig ist, dass im Gegensatz zur obigen Darstellung das $p$ bei Keras bestimmt, mit welcher Wahrscheinlichkeit Neuronen deaktiviert werden. Das heißt bei $p=0.25$ sind 75% der Neuronen aktiv.

Hier ein Beispielnetz mit Dropout an zwei Stellen (jeweils $p=0.25$):

model = Sequential()
model.add(Conv2D(filters=30, 
          kernel_size=5, 
          activation='relu', 
          padding='same', 
          input_shape=x_train[0].shape))
model.add(MaxPool2D(pool_size=2))
...
model.add(MaxPool2D(pool_size=2, strides=2))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(200, activation='relu'))
model.add(Dropout(0.25))
model.add(Dense(10, activation='softmax'))


Siehe: https://keras.io/api/layers/regularization_layers/dropout

Einsatz und p-Wert

Dropout wird ausschließlich zwischen FC-Schichten eingesetzt, um dort die Dichte und effektive Parameterzahl zu reduzieren. Dies ist bei Konvolutionsschichten nicht sinnvoll, da wir dort eine reduzierte Parameterzahl haben, die auch mehrfach angewendet werden.

Dropout reduziert Overfitting, führt aber auch zu einer längeren Trainingszeit. In AlexNet wird Dropout zwischen zwei FC-Schichten eingesetzt und Krizhevsky et al. (2014) sagen dazu: "Without dropout, our network exhibits substantial overfitting. Dropout roughly doubles the number of iterations required to converge."

Welchen Wert wählt man für $p$? Wir sehen hier $p$ im Sinne von Keras, also für den Anteil der deaktivierten Neuronen. Hinten et al. (2012) haben Dropout mit $p=0.5$ eingeführt und dieser Wert wird auch in Srivastava et al. (2014) empfohlen. In den beiden Papers werden umfangreiche Experimente mit $p=0.5$ vorgestellt. In der Praxis sieht man auch Werte unter $0.5$, zum Beispiel $p = 0.25$.

Publikationen

Geoffrey E. Hinton, Nitish Srivastava, Alex Krizhevsky, Ilya Sutskever, Ruslan Salakhutdinov (2012) Improving neural networks by preventing co-adaptation of feature detectors In: CoRR abs/1207.0580

Nitish Srivastava, Geoffrey Hinton, Alex Krizhevsky, Ilya Sutskever, and Ruslan Salakhutdinov (2014) Dropout: a simple way to prevent neural networks from overfitting. In: J. Mach. Learn. Res. 15, 1, 1929–1958.

2 CNN-Architekturen

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.

2.1 LeNet-5 (1998)

LeNet-5 war der Vorläufer aller CNN (LeCun et al., 1998). Zwei bekannte Persönlichkeiten des Deep Learning, Yann LeCun und Yoshua Bengio, waren daran beteiligt. Die dazugehörige Publikation ist

Yann LeCun, Leon Bottou, Yoshua Bengio, Patrick Haffner (1998) Gradient-based learning applied to document recognition. In: Proceedings of the IEEE 86: 11, pp. 2278–2324.

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).

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. Hier sieht man Beispiele der erzeugten Varianten:

(Quelle: LeCun et al., 1998)

Architektur

Das Netz hat fünf Schichten mit Parametern. Es kommen zwei Konvolutionsschichten zum Einsatz, jeweils gefolgt von einer Pooling-Schicht, die hier den Durchschnitt nehmen, 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 ca. 60000 Parameter und ist somit für heutige Standards relativ bescheiden.

Training

Es wurden 20 Epochen für das Training auf 60000 Beispielen durchlaufen (es wurden immer 60000 Exemplare aus den Varianten 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.

Das Paper von LeCun et al. (1998) wurde zu einer Zeit geschrieben, als es CNNs quasi "noch nicht gab", so dass sich das Paper etwas schwieriger liest als modernere Publikationen.

2.2 AlexNet (2012)

AlexNet kommt aus der Arbeitsgruppe des dritten Deep-Learning-Pioneers Geoffrey Hinton (Krizhevsky et al. 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 ist sehr berühmt und auch relativ leicht zu lesen:

Alex Krizhevsky, Ilya Sutskever, Geoffrey E. Hinton (2012) ImageNet classification with deep convolutional neural networks. In: Communications of the ACM 60: 6, pp. 84–90.

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 8 Beispielbilder aus ILSVRC (bereits mit Klassifikationen durch AlexNet):

(Quelle: Krizhevsky et al., 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.

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 in der ersten Konvolutionsschicht:

(Quelle: Krizhevsky et al., 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.

2.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 & Zisserman, 2015). Dazu wurde die Struktur der einzelnen Schichten bewusst einfach und gleichbleibend gewählt. 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.

Architektur

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

Jede Konvolutionsschicht verwendet einen relativ kleinen 3x3-Filter mit Stride 1 und Same-Padding. 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.

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.

2.4 ResNet (2015)

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

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-1)}\, a^{(l-1)} + b^{(l-1)}\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+1)}\, a^{(l+1)} + b^{(l+1)} + a^{(l)} $$

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

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+1)}\, a^{(l+1)} + b^{(l+1)} + 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 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:

(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.

3 Keras: Functional API (ResNet) [optional]

Siehe auch die Keras-Doku: https://keras.io/guides/functional_api

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 und nicht eine einzelne Verbindung zwischen zwei Neuronen.

Die Functional API erlaubt es, die Verbindungen zwischen den Schichten explizit zu konfigurieren. Dies wird zum Beispiel für skip connections in ResNets benötigt.

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).

Einfaches CNN 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.

In [1]:
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"
_________________________________________________________________
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
_________________________________________________________________

CNN mit der Functional API

In der Functional API sieht das so aus. Wir erzeugen zunächst die Eingabeschicht.

In [2]:
from tensorflow.keras import Input

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

Wir erzeugen unsere erste versteckte Schicht:

In [3]:
fc1 = Dense(64, name='FC_1', activation='relu')

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

In [4]:
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.

In [5]:
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:

In [6]:
outputs = Dense(10, name='FC_3', activation='softmax')(out2)

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

In [7]:
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
_________________________________________________________________

ResNet

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.

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

In [8]:
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.

In [10]:
from tensorflow.keras.utils import plot_model

plot_model(model, "functional_api_net.png", show_shapes=True)
Out[10]:

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

4 Weiterführende Themen [optional]

4.1 Was lernen CNNs?

Wie kann ich herausfinden, wofür genau ein bestimmtes Neuron in einer versteckten Schicht "zuständig" ist? Zeiler und 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.

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.

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.

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

5 Literatur

He, Kaiming; Zhang, Xiangyu; Ren, Shaoqing; Sun, Jian (2016) Deep Residual Learning for Image Recognition. In: IEEE Conference on Computer Vision and Pattern Recognition (CVPR), pp. 770-778.

Hinton, Geoffrey E.; Srivastava, Nitish; Krizhevsky, Alex; Sutskever, Ilya; Salakhutdinov, Ruslan (2012) Improving neural networks by preventing co-adaptation of feature detectors In: Computing Research Repository (CoRR).

Krizhevsky, Alex; Sutskever, Ilya; Hinton, Geoffrey E. (2012) ImageNet classification with deep convolutional neural networks. In: Communications of the ACM 60: 6, pp. 84–90.

LeCun, Yann; Bottou, Leon; Bengio, Yoshua; Haffner, Patrick (1998) Gradient-based learning applied to document recognition. In: Proceedings of the IEEE 86: 11, pp. 2278–2324.

Simonyan, Karen; Zisserman, Andrew (2015) Very Deep Convolutional Networks for Large-Scale Image Recognition. In: International Conference on Learning Representations (ICLR).

Srivastava, Nitish; Hinton, Geoffrey; Krizhevsky, Alex; Sutskever, Ilya; Salakhutdinov, Ruslan (2014) Dropout: a simple way to prevent neural networks from overfitting. In: Journal of Machine Learning Research (JMLR) 15: 1, pp. 1929–1958.

Zeiler, Matthew D.; Fergus, Rob (2014) Visualizing and Understanding Convolutional Networks. In: Proc. of the European Conference on Computer Vision (ECCV), pp. 818-833. arXiv