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.

Konzepte in diesem Kapitel

Skip Connections, Residual Network, Keras Functional API

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.

10.1.1 LeNet-5 (1998)

LeNet-5 war der Vorläufer aller CNN (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 im letzten Kapitel vorgestellten Techniken:

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

LeNet-5 kombiniert Konv- und Pooling-Schichten mit FC-Schichten.

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

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

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.

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.

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. […]

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:

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: Functional API für ResNet (optional)

Siehe auch die Keras-Doku 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.

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.

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
_________________________________________________________________

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.

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 Weiterführende Themen (optional)

10.3.1 Was lernen CNNs?

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.

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