Kapitel 5: Feedforward Netze II

Updates dieser Seite:

Überblick

Nachdem wir im letzten Kapitel Neuronale Netze mit nur Eingabe- und Ausgabeschicht kennen gelernt haben, die nur ein einziges Ausgabeneuron für binäre Klassifikation genutzt haben, geht es in diesem Kapitel um Netze mit mehreren Schichten, sogenannte Feedforward-Neuronale-Netze (FNN). Ihre Ausgabeschicht besteht in der Regel aus mehreren Neuronen. Zum Lernen der Parameter stellen wir das Verfahren Backpropagation vor, vielleicht das wichtigste Verfahren im Bereich neuronaler Netze. Wir lernen auch, wie man in Keras FNN erstellt, trainiert und evaluiert.

Konzepte

Softmax, Aktivierungsfunktionen (logistisch, tanh, ReLU), Backpropagation, L2-Regularisierung, Weight Decay, Optimierungsmethoden (Momentum, RMSprop, Adagrad, Adadelta, Adam, Nadam)

Datensätze

Name Daten Anz. Klassen Klassen Trainings-/Testdaten Ort
MNIST s/w-Bilder (28x28) 10 handgeschriebene Ziffern 0...9 60000/10000 Abschnitt 4 und 5
CIFAR-10 Farbbilder (32x32) 10 z.B. Flugzeug, Auto, Katze, Hund 50000/10000 Übung 05
CIFAR-100 Farbbilder (32x32) 100 z.B. Bieber, Delphin, Apfel, Orange 50000/10000 Übung 05

Notation

In diesem Kapitel werden wir folgende Notation verwenden.

Symbol Bedeutung
$n$ Anzahl der Eingabeneuronen
$m$ Anzahl der Ausgabeneuronen
$L$ Anzahl der Schichten
$n_l$ Anzahl der Neuronen in Schicht $l$
$N$ Anzahl der Trainingsbeispiele
$(x^k, y^k)$ $k$-tes Trainingsbeispiel mit Featurevektor $x^k$ und korrektem Output $y^k$ (auch: Label, Klasse)
$X$ Matrix der Featurevektoren aller Trainingsbeispiele
$W^{(l)}$ Gewichtsmatrix von Schicht $l$ zu Schicht $l+1$
$b^{(l)}$ Bias-Gewichte (Vektor) von Schicht $l$ zu Schicht $l+1$
$z^{(l)}$ Rohinput (Vektor) der Neuronen in Schicht $l$
$g$ Aktivierungsfunktion
$a^{(l)}$ Aktivierung (Vektor) der Neuronen in Schicht $l$
$\hat{y}$ Berechnete Ausgabe (Vorhersage) des Netzwerks bei einer Eingabe $x$ mit den aktuellen Gewichten
$J$ Zielfunktion in Form einer Fehler- oder Kostenfunktion (loss function), die es zu minimieren gilt
$\lambda$ Regularisierungsparameter (kommt in $J$ zum Einsatz)
$\delta^{(l)}$ Fehlerwerte (Vektor) an den Neuronen in Schicht $l$
$\Delta w^{(l)}_{i,j}$ Änderungswert von Gewicht $w_{i,j}$ in Schicht $l$ und Einzelwert der Matrix $\Delta W^{(l)}$
$\Delta b^{l}_i$ Änderungswert von Bias-Gewicht $b_i$ in Schicht $l$ und Einzelwert des Vektors $\Delta b^{(l)}$
$\alpha$ Lernrate $\in [0, 1]$
$\beta$ Momentum-Parameter $\in [0, 1]$

Importe

1 Feedforward-Neurale-Netze (FNN)

Wir gehen über zu Netzwerken, die im Gegensatz zu Perzeptron/Adaline zusätzliche Schichten zwischen Input- und Outputschicht haben. Diese werden auch versteckte Schichten (engl. hidden layers) genannt. Man kann solche Netze als Feedforward Neural Nets (FNNs) oder als Multilayer Perceptrons (MLP) bezeichnen. Wir werden hier den Begriff FNN verwenden.

Feedforward-Netze bestehen aus Neuronen und gerichteten Verbindungen. Sie sind also gerichtete azyklische Graphen (DAG), dürfen also keine Zyklen enthalten. Keine Zyklen bedeutet, dass es kein Neuron gibt, von dem aus man über Verbindungen wieder zum selben Neuron gelangen kann.

Zusätzlich sind die Neuronen in geordneten Schichten organisiert. Das bedeutet, dass jede Schicht eine Indexzahl $l$ besitzt und jedes Neuron in Schicht $l$ nur Verbindungen zu Neuronen in Schicht $l+1$ haben darf.

1.1 Notation für FNN

Beispielnetz

Als durchgängiges Beispiel betrachten wir ein Netzwerk mit drei Schichten (siehe Abbildung):

  1. die Inputschicht
  2. eine versteckte Schicht (engl. hidden layer)
  3. die Outputschicht

Hinweis: Man beachte, dass es bei drei Schichten nur zwei "Schichten" mit Gewichten gibt, in Beispiel die $w^{(1)}$ und die $w^{(2)}$. Aus Implementierungssicht gibt es nur zwei Schichten, in denen wirklich etwas "passiert" (d.h. es werden Gewichte gelernt). Daher wird in Bibliotheken wie Keras und PyTorch die Eingabeschicht gar nicht als Schicht gezählt und das unten abgebildete Netzwerk wäre ein Netz mit zwei Schichten.

Allgemeine Darstellung

Unsere Notation definieren wir unabhängig von der Anzahl der Schichten. Daher hier noch eine allgemeine Darstellung eines FNN mit $L$ Schichten:

Die Anzahl der Schichten nennen wir $L$. Wir zählen dabei Eingabe- und Ausgabeschicht mit. Bei dem Beispielnetz hat ist also $L = 3$. Wir verwenden Index $l$, um eine bestimmte Schicht anzuzeigen, also ist $l \in \{1, \ldots, L\}$.

Die Anzahl der Neuronen in Schicht $l$ wird mit $n_l$ bezeichnet. Im obigen Beispielnetz (abgebildet in blau) ist z.B. $n_2 = 3$.

Die Eingabe wird durch einen Vektor der Länge $n$ repräsentiert:

$$x = (x_1, \ldots, x_n)$$

Die Ausgabe des Netzes wird durch einen Vektor der Länge $m$ repräsentiert:

$$y = (y_1, \ldots, y_m)$$

Jedes $y_i$ repräsentiert eine Kategorie bzw. ein Label. Wo die Unterscheidung notwendig ist, bezeichnen wir mit $y$ die korrekte Ausgabe (eines Trainingsbeispiels) und mit $\hat{y}$ die berechnete Ausgabe eine Netzwerks mit den aktuellen Gewichten.

Die Roheingabe (net input) des $i$-ten Neurons in Schicht $l$ wird durch $z_i^{(l)}$ dargestellt.

Die Aktivierung des $i$-ten Neurons in Schicht $l$ wird durch $a_i^{(l)}$ dargestellt. Die Aktivierung der ersten Schicht $a_1^{(1)}, \ldots, a_n^{(1)}$ ist identisch mit dem Eingabevektor $x_1, \ldots, x_n$.

Die Gewichte von Schicht $l$ zu Schicht $l+1$ werden als Matrix $W^{(l)}$ dargestellt. Ein Einzelgewicht von Quellneuron $i$ in Schicht $l$ zu Zielneuron $j$ in Schicht $l+1$ wird durch $w_{j,i}^{(l)}$ repräsentiert. Dazu später mehr.

1.2 Forward Propagation

Bei einer konkreten Eingabe $x$ findet eine Vorwärtsverarbeitung statt, um Ausgabe $\hat{y}$ zu berechnen. Das nennt man auch Forward Propagation und funktioniert analog zu Perzeptron und Adaline. Dabei geht man schrittweise durch alle Schichten und berechnet pro Schicht erst Roheingabe und dann Aktivierung der Neuronen. Die Ausgabe $\hat{y}$ ist die Aktivierung der letzten Schicht $L$.

Roheingabe

Die Roheingabe für das i-te Neuron in Schicht $l$ berechnet sich aus der gewichteten Summe der Aktivierungen der vorgeschalteten Schicht $l-1$.

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

wobei $l \in \{2, \ldots, L\}$ und $i \in \{1, \ldots, n_l\}$.

Betrachtet man $z$ als Funktion über die Aktivierungen $a$ der Vorgängerschicht, handelt es sich um eine lineare Funktion (die Gewichte übernehmen dabei die Rolle von konstanten Koeffizienten).

Hier examplarisch der Rohinput für das erste Neuron der zweiten Schicht im Beispielnetz:

$$ \begin{align*} z_1^{(2)} & = \sum_{j=1}^{2} w^{(1)}_{1,j} \; a_j^{(1)} \\[2mm] & = \sum_{j=1}^{2} w^{(1)}_{1,j} \; x_j\\[2mm] & = w^{(1)}_{1,1} \; x_1 + w^{(1)}_{1,2} \; x_2 \end{align*} $$

Aktivierung

Die Roheingabe $z$ wird jetzt in die Aktivierungsfunktion $g$ gegeben, um die Aktivierung des Neurons zu berechnen.

Für ein einzelnes Neuron $i$ in Schicht $l$ ist das:

$$ a_i^{(l)} = g(z_i^{(l)}) $$

Als Aktivierungsfunktion für $g$ wählen wir die Sigmoidfunktion (auch logistische Funktion genannt). Man beachte, dass es sich um eine nicht-lineare Funktion handelt.

$$ g(z) = \frac{1}{1 + e^{-z}} $$

(Es sind aber auch andere Aktivierungsfunktionen möglich, siehe unten.)

In der Vektordarstellung werden die Roheingaben für Schicht $l$ als Vektor $z^{(l)}$ und die Aktivierungen als Vektor $a^{(l)}$ dargestellt. Dann können wir die Anwendung der Aktivierungsfunktion wie folgt schreiben:

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

Die Funktion $g$ wird hier also komponentenweise angewendet.

Output

Die Aktivierung der letzten Schicht $L$ entspricht dem Gesamtoutput $\hat{y}$ des FNN.

Für ein einzelnes Ausgabeneuron $i$ in Schicht $L$ also:

$$ \hat{y}_i = a_i^{(L)} = g(z_i^{(L)}) $$

In Vektorschreibweise:

$$ \hat{y} = g(z^{(L)}) $$

Matrixdarstellung der Gewichte

Die Gewichte von Neuronen der Schicht $l$ zu Neuronen der Schicht $l+1$ werden als Matrix $W^{(l)}$ repräsentiert.

Das Einzelgewicht $w_{j, i}^{(l)}$ ist das Gewicht zwischen Quellneuron $i$ in Schicht $l$ und dem Zielneuron $j$ in Schicht $l+1$.

Hinweis: Man beachte die Reihenfolge der Indizes, die man vielleicht umgekehrt erwarten würde. Je nach Fachbuch wird auch die umgekehrte Reihenfolge gewählt, dann ändern sich die Gleichungen.

Für das Beispielnetz sieht die Gewichtsmatrix $W^{(1)}$ von der Eingabeschicht (Schicht 1) zur versteckten Schicht (Schicht 2) so aus:

$$ \begin{pmatrix} w_{1,1}^{(1)} & w_{1,2}^{(1)} \\ w_{2,1}^{(1)} & w_{2,2}^{(1)} \\ w_{3,1}^{(1)} & w_{3,2}^{(1)} \end{pmatrix} $$

Es handelt sich also um eine 3x2-Matrix. Die Anzahl der Zeilen steht für die Anzahl der Zielneuronen, hier für 3 Neuronen in Schicht 2. Die Anzahl der Spalten steht für die Anzahl der Quellneuronen, hier für 2 Neuronen in Schicht 1.

Hier sehen wir nochmal den relevanten Teil des Beispielnetzwerks. Vergleichen Sie die Gewichte mit der Matrix oben.

Wenden wir uns der Verarbeitung zwischen erster und zweiter Schicht zu. Dazu betrachten wir $a^{(1)}$ und $z^{(2)}$ als Vektoren. Wir können den Rohinput der zweiten Schicht $z^{(2)}$ als Multiplikation der Gewichtsmatrix $W^{(1)}$ mit dem Aktivierungsvektor $a^{(1)}$ der Schicht 1 darstellen. Zur Erinnerung: In der ersten Schicht entspricht die Aktivierung $a^{(1)}$ dem Eingabevektor $x$.

Im Beispiel wäre das die folgende Rechnung:

$$\left( \begin{array}{c} z_1^{(2)} \\ z_2^{(2)} \\ z_3^{(2)} \end{array} \right) = \begin{pmatrix} w_{1,1}^{(1)} & w_{1,2}^{(1)} \\ w_{2,1}^{(1)} & w_{2,2}^{(1)} \\ w_{3,1}^{(1)} & w_{3,2}^{(1)} \end{pmatrix} \left( \begin{array}{c} a_1^{(1)} \\ a_2^{(1)} \end{array} \right) = \left( \begin{array}{c} w_{1,1}^{(1)} \; a_1^{(1)} + w_{1,2}^{(1)} \; a_2^{(1)} \\ w_{2,1}^{(1)} \; a_1^{(1)} + w_{2,2}^{(1)} \; a_2^{(1)} \\ w_{3,1}^{(1)} \; a_1^{(1)} + w_{3,2}^{(1)} \; a_2^{(1)} \end{array} \right) $$

Wir können das kompakt formulieren:

$$ z^{(2)} = W^{(1)} \; a^{(1)} $$

Allgemein gilt für eine beliebige Schicht $l$:

$$ z^{(l)} = W^{(l-1)} \; a^{(l-1)} $$

wobei $l \in {2,\ldots,L}$.

Hinweis: Wie oben schon erwähnt, wird in einigen Fachbüchern auch die folgende Form gewählt $ z^{(l)} = (a^{(l-1)})^T \; W^{(l-1)} $. In dieser Formulierung drehen sich die Indices von $W$ um.

Bias-Neuronen

Jetzt fügen wir sogenannte Bias-Neuronen hinzu (engl. bias = Tendenz/Neigung/Verzerrung). Jede Schicht $l$ - außer der Ausgabeschicht - hat genau ein Bias-Neuron mit dem konstanten Aktivierungswert $+1$. Die Einflussstärke des Bias-Neurons auf das Zielneuron $i$ der nächsten Schicht $l+1$ wird dann über das Gewicht $b_i^{(l)}$ gesteuert.

Innerhalb einer Schicht können wir die Gewichte des Bias-Neurons als Vektor schreiben. Man beachte, dass die Länge des Vektors der Anzahl der Zielneuronen der nächsten Schicht entspricht:

$$b^{(l)} = \left( \begin{array}{c} b_1^{(l)} \\ \vdots \\ b_{n_{l+1}}^{(l)} \end{array} \right) $$

Hinweis: Im Gegensatz zum Perzeptron packen wir das Gewicht des Bias-Neurons nicht in die Gewichtsmatrix. Dies ist der üblichere Weg in der Literatur, vielleicht weil man dann in den Gleichungen nicht ständig das $x$ mit einer "1" erweitern und die Bias-Gewichte in der Matrix gesondert betrachten muss. Beim Implementieren ist dann wieder die Lösung mit der Gewichtsmatrix potentiell interessant.

Die Roheingabe wird jetzt durch den Input des Bias-Neurons erweitert:

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

In Vektorform addieren wir einfach den Bias-Vektor zum Ergebnis der Matrizenmultiplikation. Das ist unsere erste Berechnungsformel für Forward Propagation:

$$ \label{forward1}\tag{FP1} z^{(l)} = W^{(l-1)} \; a^{(l-1)} + b^{(l-1)} $$

Warum gibt es überhaupt diese Bias-Neuronen, die unsere schöne Formel oben noch ein bisschen komplexer machen? Kann man die nicht weglassen? Wenn Sie sich die Abbildung oben anschauen, sehen Sie, dass das Bias-Neuron im Gegensatz zu den anderen Neuronen derselben Schicht nicht von der Eingabe $x$ abhängt. Das heißt, über das Bias-Gewicht kann ich den Wert eines Neurons "global" nach oben oder unten schieben, egal, wie der Input aussieht. Das entspricht tatsächlich in der klassischen Geradengleichung $f(x) = ax + b$ dem $b$ als y-Achsenabschnitt (wobei die Tatsache, dass das hier auch $b$ heißt, Zufall ist). Auch dort schiebt das $b$ den Wert nach oben oder nach unten, unabhängig von Eingabe $x$.

Anzahl der Parameter (Gewichte)

Wir überlegen uns kurz, wie viele Parameter unser Netz hat, denn die Komplexität des Netzwerks und damit sowohl Dauer als auch Speicherbedarf im Training hängen entscheidend davon ab. Parameter sind alle Größen, die im Training angepasst werden. Beim FNN sind das Gewichte in Gewichtsmatrizen und Bias-Vektoren.

Wir sehen uns nochmal das Netz von oben an:

Zwischen Schicht 1 und 2 haben wir eine 3x2-Gewichtsmatrix und einen Bias-Vektor der Länge 3. Daraus ergeben sich 3x2 + 3 = 9 Parameter. Zwischen Schicht 2 und 3 haben wir eine 2x3 Gewichtsmatrix und einen Bias-Vektor der Länge 2, also 2x3 + 2 = 8 Parameter. Insgesamt hat das Beispielnetz also 17 Parameter. Zum Vergleich: heutige CNNs haben mehrere Millionen Parameter.

Allgemein kann man die Anzahl der Parameter für eine Schicht $l$ ($l > 1 $) mit folgender Formel angeben:

$$ P_l = n_{l-1} n_l + n_l = n_l (n_{l-1} + 1) $$

Die Gesamtzahl der Parameter $P$ ist also:

$$ P = \sum_{l=2}^L P_l = n_l (n_{l-1} + 1) $$

Forward Propagation im Beispielnetz

In unserem Beispielnetz mit drei Schichten läuft Forward Propagation wie folgt:

$$ x = a^{(1)} \, \overset{W^{(1)}}{\longrightarrow} \, z^{(2)} \, \overset{g}{\longrightarrow} \, a^{(2)} \, \overset{W^{(2)}}{\longrightarrow} \, z^{(3)} \, \overset{g}{\longrightarrow} \, a^{(3)} = \hat{y} $$

In Schicht $1$ gilt:

$$ a^{(1)} = x $$

In Schicht $2$ rechnen wir:

$$ \begin{align*} z^{(2)} & = W^{(1)} \; a^{(1)} + b^{(1)} \\[2mm] & = W^{(1)} \; x + b^{(1)} \\[2mm] a^{(2)} & = g(z^{(2)}) \end{align*} $$

Dann berechnen wir Schicht $3$ und Gesamtausgabe $\hat{y}$:

$$ \begin{align*} z^{(3)} & = W^{(2)} \; a^{(2)} + b^{(2)} \\[2mm] a^{(3)} & = g(z^{(3)}) \\[2mm] \hat{y} & = a^{(3)} \end{align*} $$

Abstrakte Darstellung der Schichten

Der Zusammenhang zwischen Rohinputs $z^{(l)}$, Aktivierungsfunktion $g$ und den Aktivierungen $a^{(l)}$ kann etwas verwirrend sein. Hier nochmal die Gesamtdarstellung der drei Schichten.

In der Abbildung sind Vektoren dargestellt.

Eingabevektor $x$ entspricht dabei Aktivierung $a^{(1)}$ und Ausgabevektor $\hat{y}$ entspricht $a^{(3)}$.

1.3 Aktivierungsfunktion

Die Aktivierungsfunktion $g$ ist von entscheidender Bedeutung für den Erfolg Neuronaler Netze, denn diese Funktion führt eine Nicht-Linearität in die Gesamtrechnung ein. Man mache sich klar, dass die Weiterleitung der Aktivierung über die Gewichte zur sogenannten Roheingabe eine lineare Abbildung ist:

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

Erst wenn die Aktivierungsfunktion $g$ zum Zug kommt, wird eine (in der Regel) nicht-lineare Funktion eingeführt. Diese Berechnung ist die zweite wichtige Formel bei Forward Propagation:

$$ \label{forward2}\tag{FP2} a_i^{(l)} = g(z_i^{(l)}) $$

Oben haben wir eine sigmoide Funktion für unser FNN gewählt. Man beachte, dass für die Ausgabe oft eine andere Aktivierungsfunktion gewählt wird als für die Zwischenschichten. Wir betrachten hier die Zwischenschichten.

Sigmoide Funktionen

Eine Sigmoidfunktion - wie die logistische Funktion - ist eine oft gewählte Aktivierungsfunktion für Zwischenschichten. Die Funktion ist so definiert:

$$ g(z) = \frac{1}{1 + e^{-z}} $$

Die logistische Funktion bildet den Input auf das Interval [0, 1] ab.

Alternativ kann man auch eine Funktion wählen, die auf das Interval [-1, 1] abbildet, das leistet z.B. der Tangens hyperbolicus (tanh).

Streng genommen nennt man beide Funktionen (logistisch und tanh) sigmoide Funktionen. Oft wird die logistische Funktion gleichgesetzt mit dem Begriff "Sigmoidfunktion" (z.B. auch in Keras). Der tanh kommt deutlich seltener zum Einsatz.

In der folgenden Abbildung sehen wir beide Funktionen im Vergleich:

Rectified Linear Unit (ReLU)

Eine in den letzten Jahren wohl am meisten genutzte Funktion für Zwischenschichten ist das Rectified Linear Unit (ReLU). Diese ist definiert als

$$ g(z) := \max(0, z) $$

Für manche ist diese Darstellung vielleicht intuitiver:

$$ g(z) := \begin{cases} z &\mbox{wenn}\quad z > 0\\ 0 &\mbox{wenn}\quad z \leq 0 \end{cases} $$

Zu beachten ist, dass die Funktion nicht differenzierbar in 0 ist, dass dies aber in der Praxis kein Problem ist. Im Vergleich zur Heaviside-Funktion ist es aber so, dass die Steigung im positiven x-Raum natürlich beim Gradientenabstieg wichtig ist. Eine zweite Beobachtung ist, dass die Funktion zwar abschnittsweise linear ist, aber insgesamt wieder nicht-linear.

Lineare Funktion

Der Vollständigkeit halber nochmal die lineare Funktion (Identität), wie sie beim Adaline-Netz verwendet wird und auch bei Keras angeboten wird. Natürlich wird hier keine Nicht-Linearität eingeführt. Aber die lineare Funktion wird heutzutage auch nicht mehr verwendet.

1.4 Softmax für die Ausgabe

Wie bereits erwähnt, wählt man für die Ausgabe oft eine spezielle Aktivierungsfunktion: die sogenannte Softmax-Funktion.

FNN werden in der Regel für die Klassifikation mit mehreren Klassen verwenden. Jede Klasse wird dabei durch eine Komponente im Ausgabevektor $\hat{y} = (\hat{y}_1, \ldots, \hat{y}_m)$ repräsentiert. Theoretisch ist es egal, in welchem Bereich diese Werte sich bewegen, denn mit der Funktion arg max wählen wir einfach die Klasse mit dem höchsten Wert:

$$ \underset{i}{\arg \max} \, \hat{y}_i $$

Diese Methodik funktioniert und ist pragmatisch, aber oft möchte man die Werte $\hat{y}_i$ als Wahrscheinlichkeiten interpretieren. Dazu müssen zwei Eigenschaften erfüllt sein:

  1. alle $\hat{y}_i$ liegen im im Interval [0, 1]
  2. die Summe aller Werte $\hat{y}_1, \ldots, \hat{y}_m$ ist $1$, d.h. $\sum_i \hat{y}_i = 1$

Die Softmax-Funktion formt die Werte so um, dass diese beiden Bedingungen erfüllt sind. Die Softmax-Funktion ist wie folgt definiert (wobei $i$ ein Wert aus $\{1,\ldots,m\}$):

$$ g_{softmax}(z_i) := \frac{e^{z_i}}{\sum_{j=1}^m e^{z_j}} $$

Sehen wir uns vier Beispielwerte an, die als Rohinput $z_1, z_2, z_3, z_4$ bei vier Ausgabeneuronen ankommen könnten, zusammen mit der jeweiligen Softmax-Umrechnung (siehe auch den Code unten):

z softmax
-10.36 1.07 e-12 (= 0)
0.71 6.87 e-08 (= 0)
15.41 0.17
17.02 0.83

Sie sehen, dass die Werte im Interval [0,1] liegen und die Unterschiede akzentuiert werden.

Die Softmax-Funktion ist weniger kompliziert, als es scheint (siehe die Abbildung der Funktion unten). Jeder Wert $z$ wird zunächst mal in den positiven Bereich geschoben und Abstände zwischen den Werten werden im höheren positiven Bereich vergrößert. Die Summe im Nenner führt dazu, dass jedes $s_i$ in [0, 1] ist und dass sich alle $s_i$ zu $1$ addieren:

$$ \sum_{i=1}^m g_{softmax}(z_i) = \sum_{i=1}^m \frac{e^{z_i}}{\sum_{j=1}^m e^{z_j}} = \frac{\sum_{i=1}^m e^{z_i}}{\sum_{j=1}^m e^{z_j}} = 1$$

Aufgrund dieser Eigenschaften wird $g_{softmax}$ meistens als Aktivierungsfunktion der Ausgabeschicht verwendet. Das Resultat der arg max Funktion bleibt übrigens gleich, egal, ob man die Sigmoid-Funktion oder die Softmax-Funktion verwendet:

$$ \underset{i}{\arg \max} \, \hat{y}_i = \underset{i}{\arg \max} \, g_{sigmoid}(z_i) = \underset{i}{\arg \max} \, g_{softmax}(z_i)$$

Hinweis: Im Unterschied zu den bisherigen Aktivierungsfunktionen (z.B. logistische Funktion oder ReLU) wird bei Softmax die gesamte Werteverteilung in die Rechnung mit einbezogen (durch die Summe im Nenner), d.h. die Werte sind immer relativ zu allen anderen Werten.

Funktion $e^x$

Wir schauen uns zunächst die Funktion $e^x$ an, die hier in Python realisiert ist (Funktion math.exp oder np.exp). Man beachte, dass alle Werte ins Positive gemappt werden. Im Bereich ab $x>1$ werden Unterschiede stark akzentuiert.

Softmax in Python und Beispiele

Wir sehen uns die Funktion in Python an und geben gleich ein paar Beispiele für Rohinput-Verteilungen aus.

1.5 Formeln im Überblick

Wir schauen uns nochmal alle Formeln, die wir für die Vorwärtsverarbeitung (Forward Propagation) benötigen, im Überblick an. Dabei bezeichnet $L$ die Anzahl der Schichten, $n_l$ die Zahl der Neuronen in Schicht $l$ und $m$ die Anzahl der Ausgabeneuronen.

Komponentenschreibweise

Zunächst schreiben wir die Formeln so, dass nur Skalare erscheinen. Index $i$ bezieht sich auf ein Neuron der jeweiligen Schicht $l$.

$$ \begin{align} a^{(1)}_i &:= x_i \tag{Eingabeschicht}\\[3mm] z^{(l)}_i &:= \sum_{j=1}^{n_{l-1}}\Big[ w_{i, j}^{(l-1)} \; a_j^{(l-1)}\Big] + b_i^{(l-1)} \tag{Roheingabe}\\[3mm] a^{(l)}_i &:= g(z^{(l)}_i) \tag{Aktivierung}\\[3mm] \hat{y}_i &:= a^{(L)}_i\tag{Ausgabe} \end{align} $$

Vektorschreibweise

Jetzt sehen wir uns die Schreibweise mit Vektoren und Matrizen an.

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

Aktivierungsfunktionen

Für die Aktivierungsfunktion $g$ wählen wir zwei unterschiedliche Funktionen. Wir definieren beide in Komponentenschreibweise.

Wir wählen die Sigmoidfunktion für die Zwischenschichten $l = 2, \ldots, L-1$:

$$ g(z_i) := \frac{1}{1 + e^{-z_i}} $$

Wir wählen die Softmax-Funktion für die Ausgabeschicht $L$:

$$ g_{softmax}(z_i) := \frac{e^{z_i}}{\sum_{j=1}^m e^{z_j}} $$

2 Lernen mit Backpropagation

Backpropagation ist eine Methode, um die Gewichte sukzessive so anzupassen, dass sie sich einer optimalen Lösung annähern. Bei Backpropagation wird wieder das Verfahren des Gradientenabstiegs eingesetzt, um die optimalen Gewichte zu finden. Wie bei jedem Optimierungsproblem müssen wir auch hier eine Zielfunktion definieren, auch Kosten-, Fehler- oder Verlustfunktion genannt. Diese Zielfunktion gilt es zu minimieren.

Die Verfahren hinter Backpropagation wurden bereits in den 60er Jahren im Bereich Kontrolltheorie/Regelungstheorie entwickelt, aber erst in den 1974 für die Anwendung in Neuronalen Netzen durch Paul Werbos (1974, 1982) "wiederentdeckt". Wirkliche Verbreitung findet die Methode in den 80ern dann durch Rumelhart, Hinton und Willams (1985, 1988) , die auch den Begriff Backpropagation prägen. Siehe auch Schmidhuber (2015) für einen Überblick zur Geschichte des Verfahrens.

Geoffrey E. Hinton ist wissenschaftlich auch heute noch sehr aktiv und gilt als einer der Pioniere des Deep Learning. Er erhielt 2018 den renommierten Turing Award der ACM, zusammen mit Yann LeCun und Yoshua Bengio.

2.1 Zielfunktion

Da wir es wieder mit einem Optimierungsproblem zu tun haben, benötigen wir eine Zielfunktion (engl. objective function), die wir minimieren oder maximieren.

Wir entscheiden uns für das Minimieren und suchen also eine Kostenfunktion, engl. loss function. Dabei sei $m$ die Anzahl der Kategorien, d.h. die Dimension des Outputvektors.

Wir wählen die Cross-Entropy-Funktion, die wir bereits von der logistischen Regression aus Kapitel 3 kennen.

Kosten für ein Trainingsbeispiel

Zunächst definieren wir die Kostenfunktion $J$ für ein einziges Trainingsbeispiel $(x, y)$ und die entsprechende Ausgabe $\hat{y}$. Wir verwenden die Cross-Entropy-Funktion und addieren die Fehler jeder Komponente $i$ (entspricht der Ausgabe des $i$-ten Neurons in der Ausgabeschicht):

$$ \tag{CE1} J_k = - \sum_{i=1}^m y_i \; log(\hat{y}_i) + (1 - y_i) \; log(1 - \hat{y}_i) $$

Der Index $k$ bei $J_k$ soll anzeigen, dass es sich um den Fehler eines einzigen Trainingsbeispiels handelt.

(Wir haben den Index $k$ aber bei $y$ und $\hat{y}$ weggelassen.)

Intuition [optional]

Vergegenwärtigen Sie sich nochmal, dass die Formel (CE1) nicht so kryptisch ist, wie sie scheint. Der Zielwert $y_i$ ist immer 0 oder 1, wohingegen der berechnete Wert $\hat{y}_i$ eine Dezimalzahl zwischen 0 und 1 ist.

Wir versuchen, uns das anhand einer Wertetabelle klar zu machen. Nehmen wir an, der Zielwert ist $y_i = 1$. Dann fällt der rechte Summand in (CE1) weg, da $(1-y_i)$ ja Null ist. Es bleibt also nur $log(\hat{y}_i)$ übrig ($y_i$ ist ja 1). Welche Beiträge zu $J$ ergeben sich, wenn der errechnte Wert $\hat{y}$ unterschiedlich ausfällt?

Zielwert $y_i$ Vorhersage $\hat{y_i}$ Fehlerbeitrag $log(\hat{y}_i)$
1 1 0
1 0.9 -0.11
1 0.5 -0.69
1 0.1 -2.30
1 0.00001 -11.51

Wenn $\hat{y}$ also "richtig liegt" (gleich 1), dann ist der Fehlerbeitrag Null. Je weiter der vorhergesagte Wert vom Zielwert abweicht, umso "größer" der Fehlerbeitrag. Das Minuszeichen wird in der Summe ja umgedreht, siehe (CE1).

Man bedenke, dass $log(0)$ nicht definiert ist (minus unendlich), deshalb haben wir einen sehr kleinen Wert genommen. Die Null muss man in einer Implementierung entsprechend abfangen.

Der Vollständigkeit halber das gleiche für den Zielwert $y_i = 0$. Hier verschwindet der linke Summand und wir schauen uns den rechten Summanden $log(1 - \hat{y}_i) $ an.

Zielwert $y_i$ Vorhersage $\hat{y}$ Fehlerbeitrag $log(1 - \hat{y}_i) $
0 0 0
0 0.1 -0.11
0 0.5 -0.69
0 0.9 -2.30
0 0.99999 -11.51

Auch hier haben wir einen Fehlerbeitrag von Null, wenn die Vorhersage korrekt ist ($\hat{y_i} = 0$), und ansonsten steigende Fehlerbeiträge, wenn die Abweichung zunimmt. Hier kann man den Fall $\hat{y}_i = 1$ nicht darstellen, da $log(0)$ nicht definiert ist.

Kosten bei mehreren Trainingsbeispielen

Wir haben oben nur den Fehler bei einem einzigen Trainingsbeispiel definiert (CE1). Nehmen wir an, wir haben $N$ Trainingsbeispiele $(x^k, y^k)$. Jetzt addieren wir die Kosten aller Trainingsbeispiele auf und mitteln sie:

$$ \tag{CE2} J = - \frac{1}{N} \sum_{k=1}^N \sum_{i=1}^m y_i^k \; log(\hat{y}_i^k) + (1 - y_i^k) \; log(1 - \hat{y}_i^k) $$

Das entspricht der Summe der Fehlerfunktionen für einzelne Trainingsbeispiele:

$$ J = \frac{1}{N} \sum_{k=1}^N J_k $$

Diese Eigenschaft wird bei der Umsetzung von Backpropagation noch wichtig sein. Sie erlaubt uns, bei der Herleitung der Backpropagation-Formeln die einfachere Variante $J_k$ zu nehmen.

Da $J$ von den aktuellen Gewichten $W$ abhängt, schreibt man auch $J(W)$ oder $J_W$. Wir lassen diesen Hinweis aber i.d.R. weg.

2.2 Regularisierung

Man fügt den folgenden Term zur Kostenfunktion dazu, um zu verhindern, dass die individuelle Gewichte zu dominant werden. Man kann zeigen, dass dies den Effekt des Overfitting verringert. Man kann sich das so vorstellen, dass ein zu schnelles Abdriften in eine extreme Gewichtsverteilung (ein paar Gewichte werden sehr groß) dadurch verhindert wird, dass dieser "Strafterm" zu den Kosten hinzuaddiert wird. Effektiv wird dadurch die absolute Größe der Gewichte verringert, daher spricht man auch von weight decay (siehe auch unten unter "Effekt").

Wir addieren die Beträge aller Gewichtsmatrizen von allen Schichten:

$$ \frac{\lambda}{2} \: \sum_{l=1}^{L-1} \left\Vert W^{(l)} \right\Vert^2 $$

wobei die L2-Norm einer Matrix $W$ definiert ist als Wurzel der quadrierten Komponenten:

$$ \tag{N} \left\Vert W \right\Vert = \left( \sum_i \sum_j (w_{i,j})^2 \right)^{\frac{1}{2}}$$

Dies nennt man auch L2-Regularisierung, da hier die L2-Norm angewandt wird. Die L1-Regularisierung würde die Beträge der Gewichte verwenden (= L1-Norm). Der Faktor $\lambda$ (griechischer Buchstabe Lambda) steuert die Stärke der Regularisierung.

Finale Zielfunktion

Wir addieren den L2-Regularisierungsterm zu unserer Kostenfunktion $J$ und lösen dann den Regularisierungsterm mit Hilfe von (N) auf.

$$ \begin{align*} J_\text{reg} &= J + \frac{\lambda}{2} \sum_{l=1}^{L-1} \left\Vert W^{(l)} \right\Vert^2 \\ &= J + \frac{\lambda}{2} \: \sum_{l=1}^{L-1} \sum_{j=1}^{n_l} \sum_{h=1}^{n_{l+1}} (w^{(l)}_{h, j})^2 \\ &= - \frac{1}{N} \sum_{k=1}^N \sum_{i=1}^m \left[ y_i^k \; log(\hat{y}_i^k) + (1 - y_i^k) \; log(1 - \hat{y}_i^k) \right]\, + \frac{\lambda}{2} \: \sum_{l=1}^{L-1} \sum_{j=1}^{n_l} \sum_{h=1}^{n_{l+1}} (w^{(l)}_{h, j})^2 \end{align*} $$

Dies wäre also unsere vollständige Zielfunktion.

Effekt der Regularisierung (Weight Decay)

Wir möchten überlegen, wie genau die Regularisierung wirkt. Dazu unterscheiden wir die Kostenfunktion ohne Regularisierung $\tilde{J}$ und die Kostenfunktion mit Regularisierung $J$. Der Zusammenhang ist:

$$ \begin{align*} \tilde{J} & := - \frac{1}{N} \sum_{k=1}^N \sum_{i=1}^m y_i^k \; log(\hat{y}_i^k) + (1 - y_i^k) \; log(1 - \hat{y}_i^k) \\ J & := \tilde{J} + \frac{\lambda}{2} \: \sum_{l=1}^{L-1} \left\Vert W^{(l)} \right\Vert^2 \end{align*} $$

Jetzt schauen wir uns den Update-Schritt der Gewichte an:

$$ w_{i,j} := w_{i,j} + \alpha \Delta w_{i,j} \tag{*}\label{update}$$

Das Delta berechnet sich wie folgt:

$$ \Delta w_{i,j} = - \frac{\partial}{\partial w_{i,j}} J $$

Wir setzen unser ergänztes $J$ ein:

$$ \begin{align*} \Delta w_{i,j} &= - \frac{\partial}{\partial w_{i,j}} \left[ \tilde{J} + \frac{\lambda}{2} \: \sum_{l=1}^{L-1} \left\Vert W^{(l)} \right\Vert^2 \right] \\ &= - \left[ \frac{\partial}{\partial w_{i,j}} \frac{\lambda}{2} \: \sum_{l=1}^{L-1} \left\Vert W^{(l)} \right\Vert^2 + \frac{\partial}{\partial w_{i,j}} \tilde{J} \right] \end{align*} \tag{**}\label{delta} $$

Jetzt gilt Folgendes aufgrund von (N):

$$ \frac{\partial}{\partial w_{i,j}} \frac{\lambda}{2} \: \sum_{l=1}^{L-1} \left\Vert W^{(l)} \right\Vert^2 = \lambda\,w_{i,j} $$

(Das $w_{i,j}$ gehört zu einer bestimmten Schicht $h$, was wir hier nicht explizit hingeschrieben haben. Daher fallen alle Terme anderer Schichten weg.)

Wir setzen dies in (\ref{delta}) ein:

$$ \Delta w_{i,j} = - \left[ \lambda\,w_{i,j} + \frac{\partial}{\partial w_{i,j}} \tilde{J} \right] $$

Das setzen wir wieder in (\ref{update}) ein und formen ein wenig um:

$$ \begin{align*} w_{i,j} &:= w_{i,j} - \alpha \left[ \lambda\,w_{i,j} + \frac{\partial}{\partial w_{i,j}} \tilde{J} \right]\\ &= \left(1 - \alpha \lambda \right) w_{i,j} - \alpha \frac{\partial}{\partial w_{i,j}} \tilde{J} \end{align*} $$

Wir haben jetzt wieder ungefähr die Formel in (\ref{update}). Im Unterschied zur ursprünglichen Formel sieht man aber, dass das Gewicht $w_{i,j}$ in jedem Schritt ein wenig reduziert wird, denn $(1- \alpha \lambda)$ ist ja ein Faktor, der echt kleiner als eins ist. Deshalb wird auch von weight decay gesprochen.

2.3 Unterschied: Binary Cross-Entropy und Categorical Cross-Entropy

Binary Cross-Entropy

Wir haben in Abschnitt 2.1 den Fehler $J$ mit Hilfe von Cross-Entropy definiert (CE1 und CE2). Diese Formeln sind eigentlich für den Fall einer binären Klassifikation konzipiert, d.h. das Netz hat nur ein einziges Ausgabeneuron. In Keras verwenden Sie daher in einer solchen Situation die loss function binary_crossentropy.

model.compile(optimizer='sgd', 
              loss='binary_crossentropy', 
              metrics=['acc'])

Die Wahl dieser Funktion hat uns die Herleitung von Backpropagation erleichtert. In den meisten Fällen haben wir es aber mit Mehrklassen-Klassifikation zu tun und da verwendet man normalerweise Categorical Cross-Entropy.

Categorical Cross-Entropy

Bei einer Mehrklassen-Klassifikation mit $m$ Kategorien und One-Hot-Encoding, ist die Fehlerfunktion für ein Trainingsbeispiel $k$ wie folgt definiert:

$$ \tag{CCE1} J_k = - \sum_{i=1}^m y_i\, log(\hat{y}_i) $$

Die Idee ist hier, dass alle Komponenten, die nicht der Zielklasse entsprechen, ignoriert werden. Zum Beispiel: Die Daten enthalten 5 Kategorien ($m=5$) und bei einem Trainingsbeispiel $k=3$ ist die korrekte Ausgabe wäre die 2. Kategorie, also

$$ y^3 = \begin{pmatrix} 0\\1\\0\\0\\0 \end{pmatrix} $$

Dann ist der Fehlerbeitrag für dieses Trainingsbeispiel

$$J_3 = - log(\hat{y}_2) $$

Das heißt, man schaut sich in diesem Fall nur den Output von Ausgabeneuron Nr. 2 an und nimmt dort noch den Logarithmus.

Für den Gesamtfehler lautet entsprechend die Formel für Categorical Cross-Entropy:

$$ \tag{CCE2} J = - \sum_{k=1}^N \sum_{i=1}^m y_i\, log(\hat{y}_i) $$

In Keras gibt man categorical_crossentropy für die loss function an:

model.compile(optimizer='sgd', 
              loss='categorical_crossentropy', 
              metrics=['acc'])

2.4 Fehlerberechnung

Jetzt berechnen wir die "Fehler" an jedem Neuron. Diese Fehler verwenden wir später, um die Gewichte anzupassen, wie wir das schon bei Perzeptron und Adaline getan haben.

Den Fehler in der Output-Schicht zu definieren, ist relativ offensichtlich, aber weniger klar bei den versteckten Neuronen. Alle Fehler werden hier mit $\delta$ (griechisches kleines Delta) bezeichnet, da "Delta" in der Mathematik immer auf eine Differenz hindeutet.

In der folgenden Abbildung sehen wir in unserem Beispielnetz, wo die Fehler $\delta^{(2)}$ und $\delta^{(3)}$ berechnet werden. In der Eingabeschicht gibt es keinen Fehler, weil dort ja das Trainingsbeispiel anliegt. Die Biasneuronen ignorieren wir hier.

Berechnung

Grob gesehen haben wir die folgende Verarbeitungskette in unserem 3-Schicht-Netzwerk:

  1. berechne $ \delta^{(3)}$ unter Verwendung der errechneten Ausgabe $\hat{y} $
  2. berechne $ \delta^{(2)} $ unter Verwendung des errechneten $ \delta^{(3)} $

Wie man sieht, läuft die Berechnung rückwärts durch die Schichten, von Schicht 3 zu Schicht 2.

Fehler in der Ausgabeschicht

Allgemein definieren wir zunächst den Fehler $\delta$ für die Ausgabeschicht $L$. Hier zunächst mal für jedes Neuron $i$:

$$ \delta_i^{(L)} = \hat{y}_i - y_i $$

In der Vektordarstellung können wir den Index weglassen:

$$ \delta^{(L)} = \hat{y} - y $$

Die Herleitung finden Sie unter (BD1).

Fehler in beliebiger Schicht

Für eine beliebige Schicht $l$, außer Aus- oder Eingabeschicht, d.h. $l \in \{2, \ldots, L-1\}$, berechnet sich $ \delta^{(l)}$ wie folgt:

$$ \delta^{(l)} = (W^{(l)})^T \; \delta^{(l+1)} \odot g'(z^{(l)}) $$

wobei der Operator $\odot$ die elementweise Multiplikation von Vektoren ausdrückt, auch bekannt als Hadamard product). Ein Beispiel:

$$\left( \begin{array}{c} a_1 \\ a_2 \\ a_3 \end{array} \right) \odot \left( \begin{array}{c} b_1 \\ b_2 \\ b_3 \end{array} \right) = \left( \begin{array}{c} a_1 b_1\\ a_2 b_2\\ a_3 b_3\end{array} \right) $$

Die Herleitung der obigen Formel für $\delta^{(l)}$ finden Sie unter (BD2).

Etwas kompakter schreiben wir:

$$ \label{backprop1}\tag{BP1} \delta^{(l)} = \begin{cases} \hat{y} - y & \mbox{wenn}\quad l = L\\[3mm] (W^{(l)})^T \; \delta^{(l+1)} \odot g'(z^{(l)}) & \mbox{wenn}\quad l \in \{2, \ldots, L-1\} \end{cases} $$

Wenn wir uns das schematisch ansehen, sehen wir, dass jedes Neuron seinen eigenen "Fehlerwert" $\delta$ hat. Ausgenommen sind die Eingabeneuronen, was sinnvoll ist, denn die Eingabedaten sind ja die Features eines Trainingsbeispiels und können daher nicht "falsch" sein.

Wie bereits in Kap. 2 gezeigt, ist die Ableitung $g'$ für die logistische Funktion:

$$g'(z^{(l)}) = a^{(l)} \odot (1 - a^{(l)})$$

so dass wir schreiben können:

$$ \tag{BP1-b}\delta^{(l)} = \begin{cases} \hat{y} - y & \mbox{wenn}\quad l = L\\[3mm] (W^{(l)})^T \; \delta^{(l+1)} \odot a^{(l)} \odot (1 - a^{(l)})& \mbox{wenn}\quad l \in \{2, \ldots, L-1\} \end{cases} $$

Man sieht, dass man zur Berechnung von $\delta^{(l)}$ lediglich den Fehler der direkt nachgelagerten Schicht $l+1$, die Aktivierung der eigenen Schicht $l$ und der Gewichte zwischen den Schichten $l$ und $l+1$ benötigt. Mit dieser Formel ist somit es möglich, ein $\delta$ für jedes Neuron zu berechnen, indem man schichtweise rückwärts von Outputschicht zu Inputschicht vorgeht. Daher der Name Backpropagation (Rückwärtsverarbeitung).

Hinweis: Formel (BP1) ist allgemeiner, da verschiedene Aktivierungsfunktionen $g$ zum Einsatz kommen können. Für die Herleitung betrachten wir aber das speziellere (BP1-b).

Intuition

Wenn wir uns eine einzelne Komponente $i$ eines $\delta$ ansehen, erkennen wir, welche Werte in die Rechnung eingehen:

$$ \delta_i^{(l)} = a_i^{(l)} \: (1 - a_i^{(l)}) \: \sum_j w_{j,i}^{(l)} \: \delta_j^{l+1}$$

Schauen wir uns ein konkretes $\delta_2^{(2)}$ an (siehe Abb. unten). Dies soll den Einfluss des Neurons 2 in Schicht 2 auf die Fehler in der Ausgabeschicht 3 erfassen.

$$ \delta_2^{(2)} = a_2^{(2)} \: (1 - a_2^{(2)}) \: \sum_{j=1}^2 w_{j,2}^{(2)} \: \delta_j^{(3)}$$

Schematisch sehen wir, welche Daten für die Berechnung von $\delta_2^{(2)}$ benötigt werden, nämlich insbesondere alle Fehlerwerte der nachfolgenden Schicht, aber auch die Gewichte dorthin und die Aktivierung des Neurons.

Zunächst scheint die Summe plausibel: man gewichtet die zwei Komponenten im Fehler $\delta^{(3)}$ je nachdem, wie hoch das Gewicht - also der Einfluss des Neurons - auf die jeweilige Fehlerkomponente war.

Der Term $a\: (1 - a)$ entspricht der Ableitung der Aktivierungsfunktion, also $g'(z)$. Dies ist also die "Richtung" der Änderung und ist als Teil des Gradienten dieser Schicht für den Gradientenabstieg wesentlich.

2.5 Gewichtsanpassung

Jetzt wollen wir unsere Fehlerwerte nutzen, um die Gewichte anzupassen.

Anpassung des Gewichts $w_{i,j}^{(l)}$ bedeutet, ein Delta zu addieren. Wie gewohnt steuern wir den Grad der Änderung mit einem Faktor $\alpha \in [0, 1]$, der Lernrate.

$$ w_{i,j}^{(l)} := w_{i,j}^{(l)} + \alpha \; \Delta w_{i,j}^{(l)} $$

Wir müssen $ \Delta w_{i,j}^{(l)} $ für jedes Gewicht berechnen, wie die folgende Abbildung illustriert.

Wir benutzen auch hier wieder Gradientenabstieg, d.h. wir fragen uns, in welche Richtung und wie stark wir $w_{i,j}$ ändern müssen, indem wir die partielle Ableitung der Zielfunktion hinsichtlich $w_{i,j}$ bilden.

Wir werden später noch zeigen, dass gilt:

$$ \frac{\partial J}{\partial w_{i,j}^{(l)}} = a_j^{(l)} \: \delta_i^{(l+1)} $$

Wir müssten hier noch einen Term für die L2-Regularisierung addieren, lassen ihn aber um der Lesbarkeit willen weg. Die Formel oben gilt also für den Fall $\lambda = 0$.

Diese Ableitung ist genau das, was wir für unser Delta benötigen, denn das Delta soll ja in Richtung negative Richtung des Gradienten zeigen.

$$ \Delta w_{i,j}^{(l)} = - \frac{\partial J}{\partial w_{i,j}^{(l)}} = - a_j^{(l)} \: \delta_i^{(l+1)} $$

Wir können uns schematisch ansehen, wie für eine Gewichtsänderung $\Delta w^{(1)}_{2,1}$ sowohl der Fehlerwert des Zielneurons $\delta^{(2)}_2$ als auch die Aktivierung des Quellneurons $a^{(1)}_1$ hinzugezogen wird. Die Herleitung zur obigen Formel finden Sie unter (BW1).

Für die Bias-Gewichte $b^{(l)}$ gilt analog:

$$ \Delta b_i^{(l)} = - \frac{\partial J}{\partial b_i^{(l)}} = - \delta_i^{(l+1)} $$

Man kann sich vorstellen, dass die "Aktivierung" $a$ hier ja immer gleich eins ist. Die Herleitung zu dieser Formel finden Sie unter (BW2).

Vektorform

Wenn wir die Berechnungen mit Matrizen durchführen möchten, erstellen wir pro Schicht eine Matrix $\Delta W^{(l)}$, wobei $l \in \{1,\ldots,L-1\}$. Diese Matrizen beinhalten alle Änderungen aller Gewichte. Zusätzlich benötigen wir Vektoren $\Delta b^{(l)}$ für die Biasneuronen.

Die Matrix kann elegant berechnet werden mit

$$ \label{backprop2}\tag{BP2} \Delta W^{(l)} = - \delta^{(l+1)} \: (a^{(l)})^T $$

Die Vektoren für die Biasgewichte sind ganz einfach:

$$ \label{backprop3}\tag{BP3} \Delta b^{(l)} = - \delta^{(l+1)} $$

Wir möchten (BP2) mit Hilfe unseres Beispielnetzwerks prüfen: Zwischen Schicht 2 und 3 haben die 2x3-Gewichtsmatrix $W^{(2)}$ und die entsprechende 2x3-Deltamatrix $\Delta W^{(2)}$. Jetzt multiplizieren wir den Fehlervektor der Ausgabeschicht (2x1-Matrix) mit dem transponierten Aktivierungsvektor der 3 versteckten Neuronen (1x3-Matrix):

$$ \Delta W^{(2)} = - \left( \begin{array}{c} \delta_1^{(k+1)} \\ \delta_2^{(k+1)} \end{array} \right) \; ( a_1^{(2)} \; a_2^{(2)} \; a_3^{(2)} ) = - \begin{pmatrix} \delta_1^{(k+1)} \: a_1^{(2)} & \delta_1^{(k+1)} \: a_2^{(2)} & \delta_1^{(k+1)} \: a_3^{(2)} \\ \delta_2^{(k+1)} \: a_1^{(2)} & \delta_2^{(k+1)} \: a_2^{(2)} & \delta_2^{(k+1)} \: a_3^{(2)} \end{pmatrix}$$

Die Ergebnismatrix hat also die Form 2x3, genau wir es für $\Delta W^{(2)}$ benötigen.

Update mit allen Trainingsbeispielen

Die obige Formel gilt für ein Trainingsbeispiel, wenn man sich die Herleitung ansieht.

Wenn wir den Fehler für Trainingsbeispiel $k$ mit $J_k$ bezeichnen, gilt:

$$ \Delta W^{(l), k} = - \frac{\partial J_k}{\partial W^{(l)}} $$

Nun gilt für unsere Fehlerfunktion $J$, dass der Gesamtfehler die gemittelte Summe der Fehler auf den einzelnen Beispielen ist:

$$ J = \frac{1}{N} \sum_{k=1}^N J_k $$

Dann möchten wir die Änderung für alle Trainingsbeispiele mit Matrix $D^{(l)}$ ausdrücken:

$$ D^{(l)} := - \frac{\partial J}{\partial W^{(l)}} $$

Jetzt können wir einsetzen und auflösen:

$$ \begin{align*} D^{(l)} &= - \frac{\partial J}{\partial W^{(l)}} \\[2mm] &= - \frac{\partial}{\partial W^{(l)}} \frac{1}{N} \sum_{k=1}^N J_k\\[2mm] &= \frac{1}{N} \sum_{k=1}^N - \frac{\partial J_k}{\partial W^{(l)}} \\[2mm] &= \frac{1}{N} \sum_{k=1}^{N} \Delta W^{(l), k} \end{align*} $$

In der Praxis durchlaufen wir alle Trainingsbeispiele $1,\ldots , N$, addieren wir alle Deltamatrizen auf und teilen das Ergebnis durch $N$:

$$ D^{(l)} = \frac{1}{N} \sum_{k=1}^{N} \Delta W^{(l), k} $$

Dann verwenden wir $D$, um die Gewichte anzupassen:

$$ W^{(l)} := W^{(l)} + \alpha \: D^{(l)} $$

2.6 Formeln im Überblick

Hier nochmal alle relevanten Formeln in Vektorschreibweise.

Forward Propagation

Für die Vorwärtsverarbeitung berechnen wir schichtweise den Rohinput $z$ und die Aktivierung $a$.

$$ \begin{align} \label{fp1}\tag{FP1} z^{(l)} &= W^{(l-1)} \; a^{(l-1)} + b^{(l-1)} \\[3mm] \label{fp2}\tag{FP2} a^{(l)} &= g(z^{(l)}) \end{align} $$

Backpropagation

Für die Rückwärtsverarbeitung berechnen wir rückwärts-schichtweise die Fehlerterme $\delta$ und die Updates für Gewichtsmatrizen $\Delta W$ und die Updates für die Bias-Vektoren $\Delta b$.

$$ \begin{align} \label{bp1}\tag{BP1-b}\delta^{(l)} & = \begin{cases} \hat{y} - y & \mbox{wenn}\quad l = L\\[3mm] (W^{(l)})^T \; \delta^{l+1} \odot a^{(l)} \odot (1 - a^{(l)})& \mbox{wenn}\quad l \in \{2, \ldots, L-1\} \end{cases}\\[5mm] \label{bp2}\tag{BP2} \Delta W^{(l)} & = - \delta^{(l+1)} \: (a^{(l)})^T \\[3mm] \label{bp3}\tag{BP3} \Delta b^{(l)} & = - \delta^{(l+1)} \end{align} $$

2.7 Backpropagation-Algorithmus

Standardalgorithmus

Nochmal zusammengefasst der Algorithmus für $N$ Trainingsbeispiele $(x^k, y^k)$:

Für jede Epoche:

  1. Initialisiere für alle Trainingsbeispiele Sammelmatrizen $\Delta W^{(l), k} := 0$ und Sammelvektoren $\Delta b^{(l),k} := 0 \quad$ (jeweils für $l = 1,\ldots, L-1$ und $k= 1, \ldots, N$)
  2. Für alle Trainingsbeispiele $k = 1, \ldots, N$:
    • Forward Propagation:
      • Setze $a^{(1), k} := x^k\quad$ (Wir lassen im weiteren den Index $k$ weg)
      • Berechne $a^{(2)}, \ldots, a^{(L)} = \hat{y} \quad$ (FP1 & FP2)
    • Backpropagation:
      • Berechne Fehler $\delta^{(L)} \ldots, \delta^{(3)}, \delta^{(2)} \quad$ (BP1)
      • Erhöhe alle $\Delta W^{(l),k}$ und $\Delta b^{(l),k} \quad$ (BP2 & BP3)
  3. Passe für alle $l$ die Gewichtsmatrizen an mit
$$ \begin{align*} D^{(l)} &:= \frac{1}{N} \sum_{k=1}^{N} \Delta W^{(l), k} \\[3mm] W^{(l)} &:= W^{(l)} + \alpha \: D^{(l)} \end{align*} $$
  1. Passe für alle $l$ die Biasvektoren an mit
$$ \begin{align*} B^{(l)} &:= \frac{1}{N} \sum_{k=1}^{N} \Delta b^{(l), k} \\[3mm] b^{(l)} &:= b^{(l)} + \alpha \: B^{(l)} \end{align*} $$

Hinweis: Bei einer Implementation würde man natürlich nicht pro Trainingsbeispiel eine Sammelmatrix $\Delta W^{(l)}$ verwalten, sondern die Werte direkt in $D^{(l)}$ speichern und später mitteln.

Mini-Batch

Die obige Variante ist klassische Batch-Training. Bei Mini-Batch würde man in Schritt 2 nicht alle $N$ Beispiele durchlaufen, sondern nur einen Teil davon, und dann Schritte 3+4 (Updates) durchführen. Die Epoche ist dann aber noch nicht zu Ende. Erst würde man alle Mini-Batches durchlaufen (und jeweils die Gewichte anpassen).

Wir definieren Mini-Batch als Algorithmus. Die Trainingsdaten $T$ seien aufgeteilt auf $M$ Teilmengen $(T_1, \ldots, T_M)$:

Für jede Epoche:

Siehe auch die Videos von Andrew Ng: Mini Batch Gradient Descent und Understanding Mini-Batch Gradient Descent

2.8 Parallele Verarbeitung mehrerer Trainingsbeispiele

In der Regel stellt man sich die Verarbeitung so vor, dass man zunächst das erste Trainingsbeispiel verwendet, um eine komplette Forward Propagation zu berechnen und dann das nächste Beispiel usw. Es stellt sich aber heraus, dass es deutlich effizienter ist, erst für alle Trainingsbeispiele die Ausgabe der ersten Schicht zu berechnen, dann für alle Beispiele die zweite Schicht usw.

(Wenn wir hier von "alle Trainingsbeispiele" die Rede ist, dann gilt das analog auch für eine Teilmenge, also einen "Batch".)

Seien $(x^k, y^k)$ die Trainingsbeispiele, wobei $k \in \{1,\ldots, N\}$.

Schauen wir uns das Beispielnetz an:

Vorwärtsverarbeitung für ein Trainingsbeispiel

Betrachten wir den ersten Verarbeitungsschritt von Schicht $1$ zu Schicht $2$ (ohne Bias):

$$ z^{(2)} = W^{(1)} \; x $$

$ W^{(1)} $ ist eine $3\times 2$ Matrix und $x$ ist eine $2\times 1$ Matrix, so dass das Ergebnis $z^{(2)}$ eine $3\times 1$ Matrix ist. Wir lassen hier den Index für das Trainingsbeispiel weg:

$$ \label{ein_sample}\tag{a} \begin{pmatrix} w_{1,1}^{(1)} & w_{1,2}^{(1)} \\ w_{2,1}^{(1)} & w_{2,2}^{(1)} \\ w_{3,1}^{(1)} & w_{3,2}^{(1)} \end{pmatrix} \left( \begin{array}{c} x_1 \\ x_2 \end{array} \right) = \left( \begin{array}{c} z_1^{(2)} \\ z_2^{(2)} \\ z_3^{(2)} \end{array} \right) $$

Es ist jetzt egal, ob wir für dieses Trainingsbeispiel die nächste Schicht berechnen (was auch der Netzoutput wäre) oder ob wir für das zweite Trainingsbeispiel ebenfalls die erste Schicht berechnen. Wir müssen natürlich sicherstellen, dass die Ergebnisse der zweiten Schicht $z^{(2)}$ separat gespeichert werden.

Vorwärtsverarbeitung für mehrere Trainingsbeispiele

Ähnlich wie beim Adaline-Netz in Kapitel 3 bilden wir aus den Eingabevektoren aller $N$ Trainingsbespiele eine $2\times N$ Matrix $X$. Das heißt, jedes Trainingsbeispiel bekommt eine Spalte in der Matrix $X$.

Wir müssen hier einen Index für die Trainingsbeispiele (von $1$ bis $N$) hinzufügen. Zur Erinnerung: der obere Index in Klammern gibt die Schicht an. Der zweite obere Index gibt jetzt die Nummer des Trainingsbeispiels an. Sie sehen gut, dass die Trainingsbeispiele in Spalten nebeneinander stehen:

$$ X = \begin{pmatrix} x_1^{(1), 1} & \ldots & x_1^{(1), N} \\ x_2^{(1), 1} & \ldots & x_2^{(1), N} \end{pmatrix} $$

Wir schauen uns die Rechnung von oben an, wobei wir das einzelne Trainingsbeispiel $x$ durch die Matrix $X$ mit allen Trainingsbeispielen ersetzen:

$$ W^{(1)} \; X = \begin{pmatrix} w_{1,1}^{(1)} & w_{1,2}^{(1)} \\ w_{2,1}^{(1)} & w_{2,2}^{(1)} \\ w_{3,1}^{(1)} & w_{3,2}^{(1)} \end{pmatrix} \begin{pmatrix} x_1^{(1), 1} & \ldots & x_1^{(1), N} \\ x_2^{(1), 1} & \ldots & x_2^{(1), N} \end{pmatrix} = \begin{pmatrix} z_1^{(2), 1} & \ldots & z_1^{(2), N} \\ z_2^{(2), 1} & \ldots & z_2^{(2), N} \\ z_3^{(2), 1} & \ldots & z_3^{(2), N} \end{pmatrix} = Z^{(2)} $$

Vergleichen Sie diese Rechnung mit der Rechung (\ref{ein_sample}). Was Sie hier sehen ist, dass wir eine Matrix $Z^{(2)}$ erhalten, die die Roheingaben aller $N$ Trainingsbeispiele für Schicht $2$ erhält, die sich aus der "gleichzeitigen" Verarbeitung aller Trainingsvektoren $x^1, \ldots , x^N$ ergibt. Und das alles mit einer einzigen Matrixmultiplikation. Es ist, als hätten wir mit $N$ Netzen gleichzeitig die $N$ Trainingseingaben verrechnet. Man beachte aber, dass dies nur zulässig ist, wenn bei allen Berechnungen die gleichen Gewichte gelten.

In gleicher Weise kann man die weiteren Schichten verarbeiten (nicht vergessen, die Aktivierungsfunktion $f$ noch auf alle Elemente von $Z^{(2)}$ anzuwenden, dann weiter zur Schicht $3$), um schließlich die Gesamtausgabe in einer Matrix $Y$ zu erhalten. Die Spalten von $Y$ entsprechen den Ausgabevektoren $\hat{y}^1,\ldots , \hat{y}^N$ für alle $N$ Trainingsbeispiele.

Berücksichtigung des Bias-Gewichtsvektors

Wir haben oben den Bias-Gewichtsvektor ignoriert. Bei einem Trainingsbeispiel addieren wir eigentlich noch Vektor $b$:

$$ \label{ein_sample} \begin{pmatrix} w_{1,1}^{(1)} & w_{1,2}^{(1)} \\ w_{2,1}^{(1)} & w_{2,2}^{(1)} \\ w_{3,1}^{(1)} & w_{3,2}^{(1)} \end{pmatrix} \left( \begin{array}{c} x_1 \\ x_2 \end{array} \right) + \left( \begin{array}{c} b_1 \\ b_2 \\ b_3 \end{array} \right) = \left( \begin{array}{c} z_1^{(2)} \\ z_2^{(2)} \\ z_3^{(2)} \end{array} \right) $$

Wenn wir die Eingabevektoren zur Matrix $X$ zusammenfassen, haben wir folgende Rechnung ohne Bias:

$$ \begin{pmatrix} w_{1,1}^{(1)} & w_{1,2}^{(1)} \\ w_{2,1}^{(1)} & w_{2,2}^{(1)} \\ w_{3,1}^{(1)} & w_{3,2}^{(1)} \end{pmatrix} \begin{pmatrix} x_1^{(1), 1} & \ldots & x_1^{(1), N} \\ x_2^{(1), 1} & \ldots & x_2^{(1), N} \end{pmatrix} = \begin{pmatrix} z_1^{(2), 1} & \ldots & z_1^{(2), N} \\ z_2^{(2), 1} & \ldots & z_2^{(2), N} \\ z_3^{(2), 1} & \ldots & z_3^{(2), N} \end{pmatrix} $$

Die Ergebnismatrix hat $N$ Spalten, eben eine Spalte für jedes Trainingsbeispiel. Man kann hier keinen Vektor addieren. Um den Bias hinzuzufügen, müsste man eigentlich eine Matrix $B$ addieren, wo der Bias-Gewichtsvektor $N$-mal kopiert in $N$ Spalten vorliegt:

$$ \begin{pmatrix} w_{1,1}^{(1)} & w_{1,2}^{(1)} \\ w_{2,1}^{(1)} & w_{2,2}^{(1)} \\ w_{3,1}^{(1)} & w_{3,2}^{(1)} \end{pmatrix} \begin{pmatrix} x_1^{(1), 1} & \ldots & x_1^{(1), N} \\ x_2^{(1), 1} & \ldots & x_2^{(1), N} \end{pmatrix} + \begin{pmatrix} b_1 & \ldots & b_1 \\ b_2 & \ldots & b_2 \\ b_3 & \ldots & b_3 \end{pmatrix} = \begin{pmatrix} z_1^{(2), 1} & \ldots & z_1^{(2), N} \\ z_2^{(2), 1} & \ldots & z_2^{(2), N} \\ z_3^{(2), 1} & \ldots & z_3^{(2), N} \end{pmatrix} $$

Technisch gesehen gibt es zwei Möglichkeiten, das zu tun, ohne tatsächlich die Matrix $B$ zu konstruieren (was evtl. wertvolle Rechenzeit kostet).

Option 1: Man erweitert die Matrizen $W$ und $X$ wie folgt:

$$ \begin{pmatrix} w_{1,1}^{(1)} & w_{1,2}^{(1)} & b_1 \\ w_{2,1}^{(1)} & w_{2,2}^{(1)} & b_2 \\ w_{3,1}^{(1)} & w_{3,2}^{(1)} & b_3 \end{pmatrix} \begin{pmatrix} x_1^{(1), 1} & \ldots & x_1^{(1), N} \\ x_2^{(1), 1} & \ldots & x_2^{(1), N} \\ 1 & \ldots & 1 & \end{pmatrix} = \begin{pmatrix} z_1^{(2), 1} & \ldots & z_1^{(2), N} \\ z_2^{(2), 1} & \ldots & z_2^{(2), N} \\ z_3^{(2), 1} & \ldots & z_3^{(2), N} \end{pmatrix} $$

Option 2: Man nutzt das Broadcasting in NumPy, so dass Vektor $b$ auf korrekte Weise $N$-mal kopiert wird bei der Addition:

$$ \begin{pmatrix} w_{1,1}^{(1)} & w_{1,2}^{(1)} \\ w_{2,1}^{(1)} & w_{2,2}^{(1)} \\ w_{3,1}^{(1)} & w_{3,2}^{(1)} \end{pmatrix} \begin{pmatrix} x_1^{(1), 1} & \ldots & x_1^{(1), N} \\ x_2^{(1), 1} & \ldots & x_2^{(1), N} \end{pmatrix} \oplus \left( \begin{array}{c} b_1 \\ b_2 \\ b_3 \end{array} \right) = \begin{pmatrix} z_1^{(2), 1} & \ldots & z_1^{(2), N} \\ z_2^{(2), 1} & \ldots & z_2^{(2), N} \\ z_3^{(2), 1} & \ldots & z_3^{(2), N} \end{pmatrix} $$

Wir haben das Broadcasting mit dem Symbol $\oplus$ angedeutet. Die Bedeutung ist, dass der Vektor $b$ auf jede Spalte der links stehenden Matrix addiert wird.

3 Optimierungsmethoden

Wie bereits erwähnt ist die Lernrate $\alpha$ ein relativ grober Mechanismus, um den Gradientenabstieg zu steuern. Man kann ich vorstellen, dass man $\alpha$ eigentlich dynamisch während des Trainings anpassen müsste und auch die vielen Gewichte mit quasi individuellen Lernraten behandeln müsste. Genau darum geht es bei den folgenden Verfahren.

Wir erinnern uns an den Updateschritt der Parameter. Wir berechnen zunächst ein Delta, das sich aus dem Gradienten und der Lernrate $\alpha$ ergibt:

$$ \Delta w^t := - \alpha \frac{\partial J}{\partial w^t} $$

Wir benutzen Index $t$, um den Updateschritt zu kennzeichnen.

Mit Hilfe des Deltas führen wir das Update der Gewichte durch:

$$ w^{t+1} := w^t + \Delta w^t $$

Siehe auch: Den exzellenten Übersichtsartikel An overview of gradient descent optimization algorithms von [Ruder 2016].

Ein aktuelles Video Deep Learning: Loss and Optimization - Part 3 von Andreas Maier (FAU Erlangen) geht genau auf die hier aufgezählten Techniken ein.

Wir sehen uns zunächst die Methoden theoretisch an und vergleichen sie später in Keras.

3.1 Momentum

Ein Problem beim Gradientenabstieg ist, dass das Verfahren an bestimmten Stellen ("ravines") der Fehlerlandschaft in oszillierende Bewegungen verfallen und nur langsamen Fortschritt erzielen. In der folgenden Abbildung sehen wir zwei Gewichte im Koordinatensystem. Die dritte Achse (auf den Betrachter zulaufend) stellt den Fehler dar. Die zackige Linie zeigt den Weg der Gewichte beim Grandientenabstieg. Das nennt man "Oszillieren" und führt hier sogar dazu, dass das Optimum (i.d.R. das Minimum) nicht erreicht wird.

(Abb. von der Webseite Why Momentum Really Works (Gabriel Goh) mit alpha = 0.0042, beta = 0)

In der nächsten Abbildung sehen wir zwei Schritte auf dem Weg zum Minimum genauer an. Man sieht, dass die w1-Komponente beide Male in die "richtige" Richtung weist, wohingegen dei w2-Komponente in unterschiedliche Richtungen deutet (mathematisch: unterschiedliche Vorzeichen). Wenn bei den nächsten Schritten die w2-Komponente immer wieder so vorkommt, läuft man quasi "im zick-zack" und daher sehr langsam auf das Minimum zu.

Nun kann man den Updates einen "Schwung" (engl. momentum) mitzugeben, indem ein Anteil des vorherigen Update-Schritts mitaddiert wird (Qian 1999, Ruder 2016). In der Abbildung kann man das so verstehen: Wenn die Schritte 1 und 2 im nächsten Schritt berücksichtigt werden, hat die w1-Komponente einen deutlich größeren Einfluss, da die w2-Komponenten unterschiedliche Vorzeichen haben und daher subtrahiert werden.

Hier sehen Sie die erste Abbildung nochmal mit einem Momentum der Stärke $\beta = 0.8$.

(Abb. von der Webseite Why Momentum Really Works (Gabriel Goh) mit alpha = 0.0042, beta = 0.8)

Beim Verfahren Momentum führen wir bei der Berechnung von $\Delta w$ einen Term $s_w$ ein ($s$ wie smoothed), über den wir den Gradienten modifizieren können.

$$\Delta w^t := - \alpha \, s_w^t $$

Das $s_w$ repräsentiert den Gradienten $\frac{\partial J}{\partial w}$, der aber mit dem Gradienten des vorigen Zeitschritts $t-1$ linearkombiniert wird, sozusagen eine "Glättung", wobei $\beta \in [0, 1]$:

$$s_w^t := \beta\, s_w^{t-1} + (1 - \beta)\, \frac{\partial J}{\partial w^t}$$

Gesteuert wird die Trägheit durch einen Faktor $\beta$. Je höher $\beta$, um so stärker die Trägheit, d.h. der aktuelle Gradient geht weniger stark in die Berechnung des Updates ein. Der Faktor $\beta$ wird in der Regel relativ hoch gewählt, z.B. $0.9$.

Diese Methode führt also dazu, dass eine Bewegung in die selbe Richtung mit der Zeit an Fahrt gewinnt. Nur wenn sich die Richtung ändert, verliert sich auch der Schwung.

Wir probieren das anhand der obigen Abbildung nachzurechnen. Für Zeitschritt 1 könnten die beiden Komponenten folgende Werte haben:

$$ \begin{align*} \frac{\partial J}{\partial w_1^1} &= 3\\[2mm] \frac{\partial J}{\partial w_2^1} &= 6 \end{align*} $$

Für Zeitschritt 2 dann die folgenden Werte:

$$ \begin{align*} \frac{\partial J}{\partial w_1^2} &= 2\\[2mm] \frac{\partial J}{\partial w_2^2} &= -4 \end{align*} $$

Jetzt sehen wir uns die Entwicklung von $s_{w_1^t}$ an. Beim null-ten Zeitschritt ist $s_{w_1^0}=0$:

$$ \begin{align*} s_{w_1^1} &:= 0.9 \cdot s_{w_1^0} + 0.1 \cdot \frac{\partial J}{\partial w_1^1} = 0.1 \cdot 3 = 0.3\\[2mm] s_{w_1^2} &:= 0.9 \cdot s_{w_1^1} + 0.1 \cdot \frac{\partial J}{\partial w_1^2} = 0.9 \cdot 0.3 + 0.1 \cdot 2 = 0.47 \end{align*} $$

Analog für die zweite Komponente $s_{w_2^t}$. Auch hier ist $s_{w_2^0}=0$:

$$ \begin{align*} s_{w_2^1} &:= 0.9 \cdot s_{w_2^0} + 0.1 \cdot \frac{\partial J}{\partial w_2^1} = 0.1 \cdot 6 = 0.6\\[2mm] s_{w_2^2} &:= 0.9 \cdot s_{w_2^1} + 0.1 \cdot \frac{\partial J}{\partial w_2^2} = 0.9 \cdot 0.6 + 0.1 \cdot (-4) = 0.14 \end{align*} $$

Man sieht hier auch nach erst zwei Beispielen, dass die erste komponente eher "an Fahrt gewinnt", wohingegen die zweite Komponente durch die entgegengesetzte Bewegung ausgebremst wird.

Weitere Erklärungen von Momentum

Andrew Ng hat dazu ein schönes Erklärvideo: Gradient Descent With Momentum.

Die Abbildungen oben wurden mit einer interaktiven Visualisierung auf der Webseite Why Momentum Really Works von Gabriel Goh (2017) angefertigt. Dort finden Sie auch Erklärungen und Gedanken zur Momentum-Methode.

Publikationen

Rumelhart, David E.; Hinton, Geoffrey E.; Williams, Ronald J. (1986). Learning representations by back-propagating errors. In: Nature. 323 (6088): 533–536.

Ning Qian (1999) On the momentum term in gradient descent learning algorithms. In: Neural networks: the official journal of the International Neural Network Society, 12(1): 145–151.

Yurii Nesterov. A method for unconstrained convex minimization problem with the rate of convergence o(1/k2). Doklady ANSSSR (translated as Soviet.Math.Docl.), 269:543–547.

Ilya Sutskever, James Martens, George Dahl, and Geoffrey Hinton (2013) On the importance of initialization and momentum in deep learning. In: Proceedings of the 30th International Conference on International Conference on Machine Learning - Volume 28 (ICML’13).

3.2 Weitere Verfahren [optional]

Nesterov accelerated gradient (NAG)

Eine Variante von Momentum ist Nesterov accelerated gradient (NAG). Hier sei nur auf den ausführlichen Online-Artikel Nesterov Accelerated Gradient and Momentum von James Melville (2016) hingewiesen.

Adagrad

Adagrad steht für Adaptive Gradient [Duchi et al. 2011]. Ein Grund, warum eine Lernrate $\alpha$ suboptimal sein könnte, ist, dass alle Parameter $w_i$ gleich behandelt werden, dabei könnte es sein, dass wir für Parameter $w_3$ in kleineren Schritten gehen sollten als für $w_5$. Adagrad individualisiert die Lernrate nach Parametern. Das Prinzip dabei lautet: je häufiger ein Parameter schon geupdatet wurde, umso kleiner das nächste Update.

Schauen wir uns das Delta für jedes Einzelgewicht $w_i$ an:

$$ \Delta w_i^t := - \alpha \nabla J(w_i^t) $$

Adagrad reduziert das $\alpha$ basierend auf der Häufigkeit vergangener Updates von $w_i$:

$$ \Delta w_i^t := - \frac{\alpha}{\sqrt{G_{t, ii} + \epsilon}} \nabla J(w_i^t) $$

Dabei ist $G_t$ eine Matrix, die auf der Diagonalen die Summe der Quadrate aller bisherigen Gradienten nach jeweils $w_i$ enthält. Das $\epsilon$ soll verhindern, dass durch Null dividiert wird und ist sehr klein (z.B. $10^{-8}$). Laut [Ruder 2016] performt das Verfahren deutlich schlechter, wenn man die Wurzel weglässt.

Der Vorteil der Methode ist die automatische Anpassung der Lernrate. Der Nachteil ist, dass die Lernrate im Verlauf des Lernens auf praktisch Null sinkt und somit kein Lernen mehr stattfindet.

Publikation

John Duchi, Elad Hazan, and Yoram Singer (2011) Adaptive Subgradient Methods for Online Learning and Stochastic Optimization. In: Journal of Machine Learning Research, 12:2121–2159.

Adadelta und RMSprop

Die Verfahren Adadelta behebt den Nachteil von Adagrad, indem nicht alle vergangenen Gradientenwerte addiert, sondern nur eine begrenzte Historie betrachtet, wo ältere Werte über die Zeit immer geringeren Einfluss haben [Zeiler 2012].

Statt der Matrix $G$ verwenden wir als Speicher $E$, wo die Quadrate der Gradienten immer aufsummiert werden, wobei die alten Werte über die Zeit verblassen.

$$ E_i^t = \gamma E_i^{t-1} + (1-\gamma) \nabla J(w_i^t)$$

Bei der Berechnung des Delta verwenden wir wie bei Adagrad das $E$ im Nenner, um die Gewichtsänderung zu dämpfen:

$$ \Delta w_i^t := - \frac{\alpha}{\sqrt{E_i^t + \epsilon}} \nabla J(w_i^t) $$

Für weitere Details verweisen wir auf das Paper von [Ruder 2016].

RMSprop steht für Root Mean Square Propagation und ist eine sehr ähnliche, parallel entwickelte, Methode. Sie wurde nicht publiziert, sondern lediglich von Geoffrey Hinton in einer Vorlesung vorgestellt. Sie können sich die Folien der Vorlesung anschauen (ab Folie 26).

Siehe auch RMSProp (Andrew Ng)

Publikationen

Matthew D. Zeiler (2012) ADADELTA: An Adaptive Learning Rate Method. arXiv preprint arXiv:1212.5701.

Adam

Adam steht für Adaptive Moment Estimation. Auch dieses Verfahren berechnet die adaptive Lernrate für jeden Parameter [Kingma, Ba, 2015]. Im Unterschied zu Adagrad wird hier zusätzlich noch Momentum verwendet und auch das Momentum wird anhand früherer Anpassungen modifiziert.

Siehe auch Adam Optimization Algorithm (Andrew Ng)

Publikation

Diederik P. Kingma and Jimmy Lei Ba (2015) Adam: a Method for Stochastic Optimization In: International Conference on Learning Representations, pp. 1–13.

Nadam

Nadam steht für Nesterov-accelerated Adaptive Moment Estimation und kombiniert Adam und NAG [Dozat 2016].

Publikation

Timothy Dozat (2016) Incorporating Nesterov Momentum into Adam. In: ICLR Workshop, (1):2013–2016.

3.3 Welche Methode nutzen?

[Ruder 2016] empfiehlt Adam als derzeit bestes Gesamtpaket, betont aber, dass viele Methoden eine ähnlich gute Performance haben. In einem Vergleich, den wir zum Ende des Kapitels anstellen, schneiden Adam, RMSprop und Nadam am besten ab. Die Performance hängt aber sicherlich auch von der Problemstellung, also den Daten, ab.

4 FNN in Keras

Im letzten Kapitel haben wir Keras kennen gelernt und sehr einfach Netze erstellt, die lediglich Eingabe- und Ausgabeschicht hatten. Jetzt sehen wir uns an, wie man FNN mit mehreren Zwischenschichten erstellt und trainiert.

Wir werden uns insgesamt drei Netze ansehen. Damit wir alle mit der gleichen Anzahl Epochen und der gleichen Lernrate trainieren, definieren wir zwei globale Variabeln vorab:

4.1 MNIST-Daten und One-Hot-Encoding

Wir ziehen den Datensatz MNIST hinzu, bei dem es um die Erkennung von handgeschriebenen Ziffern (0..9) geht. Wir haben diese Daten bereits in Kapitel 2 genutzt. Der Datensatz wird sowohl mit der Scikit-learn-Bibliothek als auch mit der Keras-Bibliothek ausgeliefert. Wir verwenden hier die Keras-Variante.

Siehe: https://keras.io/datasets/#mnist-database-of-handwritten-digits

Daten einlesen

Die Daten kommen bereits separiert in Trainings- und Testdaten.

Wir sehen uns die Struktur an. Die Bilder sind als 28x28-Matrizen hinterlegt.

Daten vorverarbeiten

Für unsere Zwecke möchten wir die Bildmatrizen linearisieren zu Vektoren der Länge 784 (= 28*28). Außerdem möchten wir die Pixelwerte (0..255) auf das Interval [0, 1] normalisieren.

Kleiner Check, ob die Linearisierung geklappt hat:

Datenvisualisierung

Wir visualisieren das jeweils erste Bild jeder Kategorie.

One-Hot Encoding

Bislang hatten wir es mit binärer Klassifikation zu tun, d.h. es gab eine Ausgabe $y$, die z.B. mit $0$ die erste Kategorie und mit $1$ die zweite Kategorie anzeigt.

Wenn wir z.B. drei Kategorien haben, ist es nicht ratsam, wieder eine Ausgabe $y$ mit drei möglichen Werten (z.B. 1, 2 und 3) zu nehmen. Warum? Eine solche Repräsentation impliziert Zusammenhänge, die nicht die Wirklichkeit widerspiegeln, in dieser Fall, dass die Kategorien 2 und 1 "näher" beieinander liegen als die Kategorien 3 und 1. Bei unserem Beispiel der Ziffern liegt die Ziffer "3" direkt neben der "4", aber von der Ähnlichkeit her liegt sie näher an der "8".

Um dieses Problem zu umgehen, wählen wir für drei Kategorien die folgende Repräsentation:

$$ \left( \begin{array}{c} 1 \\0 \\ 0 \end{array} \right) \quad \left( \begin{array}{c} 0 \\1 \\ 0 \end{array} \right) \quad \left( \begin{array}{c} 0 \\ 0 \\ 1 \end{array} \right) $$

Wir haben also einen Ausgabevektor $y = (y_1, y_2, y_3)$, der jede Kategorie so repräsentiert, dass eine Stelle des Vektors auf 1 gesetzt ist. Dies nennt man One-Hot Encoding. Man beachte, dass $y_1$ und $y_2$ nicht irgendwie "näher" sind als $y_1$ und $y_3$, denn die Position der Ausgabeneuronen geht in keiner Weise in die Verarbeitung ein.

Wir wandeln die Labels in eine One-Hot-Enkodierung um und schreiben diese Kodierung zurück in die Labellisten. Dazu verwenden wir die Funktion to_categorical von Keras.

Wir prüfen, ob die Label-Daten die richtige Form haben.

Das passt: Es sind Vektoren der Länge 10.

4.2 FNN mit 3 Schichten und Batchtraining

Wir bauen jetzt unser Modell mit einer versteckten Schicht mit 20 Neuronen und wenden beim Lernen sogenanntes Batchtraining an, d.h. wir verarbeiten alle Trainingsbeispiele, bevor wir ein Update durchführen.

Das Netz hat also 3 Schichten:

Schematisch sieht das so aus (ohne Bias-Neuronen bzw. -Vektor):

Netz mit 20 versteckten Neuronen in Keras

Modell

In Keras erstellen wir das Netz als Instanz der Klasse Sequential.

Wir fügen dem Objekt zwei Schichten vom Typ Dense (fully connected) hinzu. Die Eingabeschicht ist implizit und wird über den Parameter input_dim quasi mitgeneriert. Wir wählen die Sigmoid-Funktion für die Aktivierung der versteckten Schicht. Bei der Ausgabeschicht geben wir an, dass wir die Softmax-Funktion anwenden möchten.

Anzahl der Parameter

Wie viele Parameter hat das Netz? Die Gewichtsmatrix $W^{(1)}$ von Input- zu versteckter Schicht hat 784x20 Einträge:

Es kommen 20 Gewichte für das Bias-Neuron dazu. Insgesamt also 15700 Parameter. Die Gewichtsmatrix $W^{(2)}$ von versteckter zu Ausgabeschicht enthält 10x20 = 200 Einträge plus den 10 Gewichten für das Bias-Neuron zur Ausgabe, macht 210 Parameter. Alles in allem also 15910 Parameter.

Wir schauen uns das Netz in der Kurzübersicht mit Hilfe der Methode summary an. Dort steht auch nochmal die Anzahl der Parameter:

Gewichtsmatrizen und Biasvektoren

Mit get_weights können wir auf die Parameter des Netzwerks zugreifen. Für jede Schicht gibt es jeweils

Wir lassen uns hier in einer Schleife jeweils die Dimensionen der Gewichtsmatrix und des Bias-Vektor für unsere zwei Schichten (Hidden + Output) ausgeben:

Wir sehen hier, dass die Gewichtsmatrix $W^{(1)}$ von der Eingabeschicht zur versteckten Schicht die Dimensionen 784x20 hat. In unserer theoretischen Behandlung haben wir eine 20x784-Matrix verwendet, also die transponierte Version. Beide Varianten sind möglich und üblich.

Der Bias-Vektor von Input zu Hidden hat eine Länge von 20. Auch das ist plausibel, wenn Sie sich in der Abbildung oben ein Bias-Neuron in der Inputschicht vorstellen, dass mit allen 20 versteckten Neuronen verbunden ist. Dies muss offensichtlich 20 Gewichte haben.

Training

Beim Training wollen wir die ursprüngliche Variante des Backpropagation verwenden, wo alle Trainingsbeispiele vor einem Update durchlaufen werden. Wir nennen diese Variante Batchtraining (nicht zu verwechseln mit "Minibatch", siehe unten).

In Keras spezifizieren wir dazu Stochastic Gradient Descent, kurz SGD, als Optimierungsmethode und geben als Batchgröße die Anzahl der Trainingsdaten an. Als Zielfunktion wählen wir wie im Theorieteil die Cross-Entropy-Funktion.

Compile

Mit compile konfigurieren wir das Training zunächst nur. Wir erstellen ein eigenes SGD-Objekt (stochastic gradient descent), damit wir die Lernrate auf 0.1 einstellen können. Alternativ kann man als Parameter den String 'sgd' übergeben, dann wird standardmäßig 0.01 als Lernrate gewählt. Als Metrik für unsere Messungen wählen wir Accuracy, kurz acc.

Siehe auch: Keras SGD

Fit

Das eigentliche Training wird mit fit durchgeführt. Wichtig ist, dass fit den Verlauf der Metriken pro Epoche in einem History-Objekt zurückgibt. Will man die Fehler- und Performance-Entwicklung später visualisieren, muss man dieses Objekt speichern.

Die Methode hat außer den Daten und Epochen noch ein paar Parameter:

Um Batchtraining zu realisieren, wählen wir eine Batchgröße von 60000, und bilden damit das originale Backpropagation nach, wo in einer Epoche alle Trainingsbeispiele verarbeitet werden, bevor ein Update der Gewichte durchgeführt wird.

Wir unterdrücken die Ausgabe (verbose=0), messen aber die Trainingsdauer mit Hilfe der Funktion time aus dem gleichnamigen Paket (gibt die Sekunden seit 1.1.1970 0:00 Uhr zurück).

Die Trainingsdauer im Batchtraining ist mit 20 Sekunden für 50 Epochen sehr kurz.

Evaluation

Wir sehen uns die Entwicklung von Zielfunktion (Loss) und Accuracy an.

Dazu definieren wir vorab eine Hilfsfunktion.

Achten Sie bei den Plots immer auf die Grenzen der y-Achse (im Code set_ylim). Diese werden immer so gewählt, dass man die Kurve möglichst gut sieht. Hier haben wir das Interval 0 bis 0.6 für die Accuracy. Weiter unten haben wir ein anderes Interval. Dies muss man beim Vergleich der Kurven natürlich beachten.

Mit evaluate können wir die Performanz unseres Modells auf den Testdaten messen. Wir bekommen ein Tupel mit Fehler und Accuracy zurück.

Mit Batchtraining erhalten wir nach 50 Epochen Training eine enttäuschende Accuracy von nur 58.7% auf den Testdaten.

4.3 Einfluss der Batchgröße

Mit Batchtraining (Batchgröße = 60000) haben wir ein enttäuschendes Ergebnis erzielt. Wir sehen uns jetzt das reine SGD und eine Variante von Minibatch-Training an.

Reines SGD (Batchgröße = 1)

Wenn wir die Batchgröße auf 1 setzen, erreichen wir "reines" Stochastic Gradient Descent, wo die Gewichte nach jedem Trainingsbeispiel angepasst werden.

Wir erzeugen eine neue Instanz des Netzwerks mit den gleichen Eigenschaften (20 versteckte Neuronen, gleiche Aktivierungsfunktionen). Anschließend trainieren wir es unter gleichen Bedingungen. Der einzige Unterschied ist die Batchgröße, die wir hier auf 1 setzen.

Wir sehen, dass das Training mit reinem SGD mit 45 Minuten deutlich länger dauert als beim Batchtraining mit seinen 20 Sekunden - in beiden Fällen für 50 Epochen.

Das liegt daran, dass hier in jeder Epoche 60000 Mal sämtliche Gewichte angepasst werden, wohingegen vorher nur ein einziges Update pro Epoche stattfand.

Jetzt schauen wir uns die Performance an. Achten Sie auf den Wertebereich, der bei der Accuracy-Kurve zwischhen 0.9 und 1.0 liegt.

Wir sehen einen typischen Verlauf der Accuracy: Auf den Trainingsdaten steigt die Kurve immer weiter an, aber auf den Testdaten bleibt die Kurve in einem bestimmten Bereich. Sobald die Kurve auf den Testdaten sich einpendelt, kann man das Training beenden, weil das Training anschließend Overfitting betreibt.

Wir schauen uns jetzt noch einmal den finalen Accuracy-Wert auf den Testdaten an:

Mit 95.1% Accuracy auf den Testdaten erreicht das mit reinem SGD trainierte Netz einen deutlich besseren Wert als das vorigen Netz, dass wir mit Batchtraining trainiert haben (58.7%).

Minibatch (Batchgröße = 32)

Nachdem wir Batchtraining (alle Trainingsbeispiele pro Update) und reines SGD (Batchgröße = 1) ausprobiert haben, setzen wir jetzt die eigentliche Idee von Minibatch um, dass eine bestimmte Anzahl von (zufällig ausgewählten) Trainingsdaten durchlaufen wird, bevor dann ein Update durchgeführt wird. Es handelt sich also um einen Kompromiss zwischen Batchtraining und reinem SGD.

Im Keras ist bei der Methode SGD eine Batchgröße von 32 standardmäßig eingestellt. Diese probieren wir jetzt aus. Den Parameter batch_size lassen wir weg, damit die Standardgröße von 32 genommen wird. Alles andere bleibt wie in den beiden anderen Versuchen gleich.

Minibatch liegt mit einer Dauer von ca. 1:40 Minuten zwischen Batch-Training (20 Sek.) und reinem SGD (45 Min.). Interessant ist, dass es doch erheblich schneller ist als reines SGD.

Wir sehen uns wieder die Performance an:

Die Performance für unser Minibatch-Training mit Batchgröße 32 ist mit einer 95.4% Accuracy auf den Testdaten praktisch gleich zur Performance mit reinem SGD.

Konklusion

Schauen wir uns die Resultate nochmal im Vergleich an (unten sieht man auch die jeweilige Accuracy als Kurve über die Epochen).

Trainingsdauer Accuracy (Test)
Batchtraining 0:20 Min 58.7%
Reines SGD 45:00 Min 95.1%
Minibatch (32) 1:40 Min 95.4%

Es scheint - zumindest bei diesen Daten - so zu sein, dass man mit Minibatch-Training mit der voreingestellten Batchgröße am besten fährt, da man eine hohe Accuracy bei tolerierbarer Trainingsdauer erzielt.

Wichtig ist immer, sich die Rahmenbedingungen des obigen "Experiments" vor Augen zu führen. Wir haben die Optimierungsmethode Stochastic Gradient Descent mit einer Lernrate von 0.1 verwendet und für 50 Epochen trainiert. Wir hatten ein FNN mit einer versteckten Schicht mit 20 Neuronen und haben auf den MNIST-Daten gearbeitet. Sollte sich einer dieser Umstände ändern, kann das die Ergebnisse verändern.

Zudem müsste man jede der drei Variante mehrfach testen (z.B. 10x) und dann den Mittelwert von Dauer und Accuracy nehmen, um statistische Glaubwürdigkeit zu erreichen. In unserem Beispiel sind die Unterschiede allerdings so deutlich, dass wir uns das ersparen.

4.4 Vergleich der Optimierungsmethoden

Wir haben im Theorieteil verschiedene Optimierungsmethoden kennen gelernt. Wir vergleichen jetzt das gleiche Netz mit den gleichen Daten, aber unterschiedlichen Optimierungsmethoden.

Hyperparameter

Hyperparameter sind Parameter, die sich während des Trainings nicht verändern. Das können ganz verschiedene Aspekte eines Netzwerks sein:

Wenn wir aber fünf verschiedene Netze $N_1, \ldots, N_5$ mit verschiedenen Hyperparametern trainieren, wie vergleichen wir sie? Wir könnten die Testdaten nehmen. Aber wenn wir uns aufgrund der Testdaten-Performance für Netz $N_3$ entscheiden, womit berechnen wir dann die endgültige Performance auf einem "neutralen" (d.h. ungesehenen) Datensatz?

Um dies zu umgehen, nimmt man aus den Trainingsdaten einen Satz Validierungsdaten. Man vergleicht die Netze $N_1, \ldots, N_5$ auf den Validierungsdaten und berechnet die finale Performance mit dem Gewinnernetzwerk auf den Testdaten.

Optimierungsmethoden in Keras

In TensorFlow/Keras wird die Optimierungsmethode durch ein parametrisierbares Optimizer-Objekt bestimmt, zu finden im Paket tensorflow.keras.optimizers.

Es gibt folgende Optimizer zur Auswahl:

In der Dokumentation unter https://keras.io/optimizers findet man für jeden Optimizer die entsprechenden Parameter.

Beispiel

Hier ein Beispiel mit SGD. In dem Beispiel wird zuerst ein SGD-Objekt erzeugt (mit allen Parametern) und dann der Methode compile übergeben.

from tensorflow.keras.optimizers import SGD

model = Sequential()
model.add(...)
model.add(...)

sgd = SGD(learning_rate=0.01, momentum=0.9, nesterov=True)

model.compile(loss='categorical_crossentropy', 
              optimizer=sgd)

Alternativ kann man den Optimizer der Methode compile als String übergeben. Dann werden Standardwerte benutzt.

model.compile(loss='mean_squared_error', 
              optimizer='sgd')

Experiment

Wir möchten diese Verfahren mit Hilfe mehrerer Netzwerke, die sich nur in der Optimierungsmethode unterscheiden, miteinander vergleichen. Dazu müssen wir einen Teil der Trainingsdaten (10%) als Validierungsdaten entnehmen. Diese Validierungsdaten nehmen wir, um uns für eine Optimierungsmethode zu entscheiden und messen dann damit die Performance auf den Testdaten.

Globale Parameter

Wir definieren für unser "Experiment" ein paar globale Parameter, damit wir leicht Änderungen vornehmen könnten.

Die Ergebnisse speichern wir in einem Array. Wie die Einträge strukturiert sind, sehen wir gleich.

Modell

Da wir viele Netzwerke erstellen, schreiben wir uns eine Funktion, die ein Netz erstellt, trainiert und evaluiert. Es wird ein assoziatives Array erzeugt, dass alle wichtigen Informationen zu dem Netz-Testlauf enhälte: Titel, Trainingshistorie und Evaluation auf den Testdaten.

Sie sehen hier den Paramter validation_split. Hier kann man einen Prozentsatz (als Zahl zwischen 0 und 1) angeben, den man als Validierungsdaten vorhalten möchte.

Trainingsdurchläufe

Wir durchlaufen alle Optimierungsmethoden in unserem Array und erzeugen einen Testlauf, dessen Ergebnis wir in unseren Ergebnisarray packen.

Evaluation

Hier sehen wir uns die Entwicklung der Accuracy im Training an: einmal für die Trainingsdaten, einmal für die Validierungsdaten.

Da man oben wenig erkennen kann, sehen wir uns die Einzelverläufe an: