6  Backpropagation

In diesem Kapitel leiten wir das Backpropagation-Verfahren aus dem letzten Kapitel her. Genauer gesagt zeigen wir, dass Backpropagation das Konzept des Gradientenabstiegs realisiert. Zunächst entwickeln wir eine Intuition für die Formeln, dann sehen wir uns die mathematische Herleitung der Backpropagation-Formeln an. Dabei wird der Zusammenhang zwischen Zielfunktion, partieller Ableitungen (Gradienten) und Gewichtsanpassung klar. Eine integrale Rolle spielt die Kettenregel und das Konzept der Fehlerwerte \(\delta\).

  • Sie verstehen den Zusammenhang zwischen Zielfunktion, partieller Ableitung und Gewichtsanpassung
  • Sie können die Herleitung von Backpropagation mathematisch nachvollziehen und könnten entsprechende Teilschritte erläutern
  • Sie verstehen, wie und warum Backpropagation über die Kettenregel von der letzten Schicht rückwärts arbeitet
  • Sie können erklären, welche Funktion die Fehlerwerte \(\delta\) einnehmen
  • Sie können den Unterschied zwischen den Bias-Gewichten und den “regulären” Gewichten erklären
  • 16.05.2024: Massive Überarbeitung von 6.7
  • 09.05.2024: Ergänzung zu Karpathy (ganz unten), Korrektur Schichten in 6.3
  • 04.05.2024: Zwei Diagramme hinzugefügt (6.1.2, 6.1.3)
  • 03.05.2024: Weitere Anpassungen, Erläuterung Delta (6.2.3)
  • 30.04.2024: Viele kleine Anpassungen (Formulierungen, Größe von Abbildungen) und - neu - Tipps zu den Formeln

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\)
\(W^{(l)}\) Gewichtsmatrix von Schicht \(l-1\) zu Schicht \(l\)
\(b^{(l)}\) Vektor der Bias-Gewichte von Schicht \(l-1\) zu Schicht \(l\)
\(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 Verlustfunktion (loss function), die es zu minimieren gilt
\(\lambda\) Regularisierungsparameter (kommt in \(J\) zum Einsatz)
\(\delta^{(l)}\) Fehlerwerte (Vektor) an den Neuronen in Schicht \(l\)

In diesem Kapitel versuchen wir, die Formeln für Backpropagation mathematisch herzuleiten. Wir behalten (hoffentlich) dabei stets im Blick, wofür wir diese Formeln eigentlich brauchen.

Es ist tatsächlich nicht leicht, gute Herleitungen zu finden, egal ob in Fachbüchern oder im Internet. Oft werden die Gedankengänge und Herleitungen stark verkürzt dargestellt. Die Darstellung von Michael Nielsen (2015) in seinem Online-Buch Neural Networks and Deep Learning (Chapter 2) ist da eine positive Ausnahme und sei hiermit als Begleitlektüre empfohlen.

Schauen Sie unbedingt auch in Abschnitt 6.7, um einen alternativen, allgemeineren Ansatz zu sehen.

6.1 Was haben wir, was müssen wir zeigen?

Bevor wir starten sammeln wir nochmal alle Formeln ein, die relevant sind. Inbesondere listen wir diejenigen Formeln, die wir herleiten möchten.

6.1.1 Verlustfunktion (J)

Wir definieren wir die Verlustfunktion \(J\) für zunächst nur für ein einziges Trainingsbeispiel mit der entsprechend berechnten Ausgabe. Wir summieren über alle \(m\) Ausgabeneuronen:

\[ \tag{J} J_k = - \sum_{i=1}^m y_i^k \; log(\hat{y}_i^k) \]

Diese Funktion heißt Categorical Cross Entropy (siehe Abschnit 5.2.1). Wir werden uns im Folgenden nur mit \(J\) auf diese Funktion beziehen.

Die tatsächliche Kostenfunktion muss alle Trainingsbeispiele \((x^k, y^k)\) mit \(k \in \{1, \ldots, N\}\) berücksichtigen und lautet:

\[ \tag{J2} J = - \frac{1}{N} \sum_{k=1}^N \sum_{i=1}^m y_i^k \; log(\hat{y}_i^k) \]

Das entspricht der Summe der Verluste für die einzelnen Trainingsbeispiele, d.h. der Zusammenhang zwischen (J) und (J2) ist wie folgt:

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

Für unsere Herleitung lassen wir den L2-Regularisierungsterm weg bzw. setzen \(\lambda = 0\).

6.1.2 Forward Propagation (Z, A, SM)

Forward Propagation schematisch

Für die Vorwärtsverarbeitung berechnen wir für jede Schicht \(l\) den Rohinput \(z^{(l)}\) und die Aktivierung \(a^{(l)}\). Diese Formeln müssen wir nicht herleiten, diese haben wir quasi als Funktionsprinzip eines Netzwerks definiert.

Zunächst in der Komponentenschreibweise:

\[ \begin{align*} \tag{Z} z^{(l)}_i &:= \sum_{j=1}^{n_{l-1}}\Big[ w_{i, j}^{(l)} \; a_j^{(l-1)}\Big] + b_i^{(l)} \\[3mm] \tag{A} a_i^{(l)} &= g(z_i^{(l)})\\[3mm] \hat{y}_i &= a^{(L)}_i \end{align*} \]

für alle \(i \in {1, \ldots, n_l}\)

Jetzt in der kompakten Vektorschreibweise:

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

Für die Ausgabeschicht \(L\) nutzen wir die Softmax-Funktion:

\[ \tag{SM} \hat{y}_i = a^{(L)}_i = g_{sm}(z_i^{(L)}) = \frac{e^{z_i^{(L)}}}{\sum_{j=1}^m e^{z_j^{(L)}}} \]

6.1.3 Backpropagation - herzuleitende Formeln (D1, D2, W, B)

Backpropagation schematisch

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\). Dies sind die Formeln, die wir herleiten möchten.

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

Im Folgenden formulieren wir die Formeln leicht um, damit wir uns in der Herleitung leichter tun.

D1: Fehler an der Ausgabeschicht

Diese Formel berechnet den Fehler (Delta) an der Ausgabeschicht \(L\):

\[ \label{D1}\tag{D1} \delta_j^{(L)} = \hat{y}_j^{(L)} - y_j \]

D2: Fehler an einer Zwischenschicht

Die nächste Formel gibt uns die Möglichkeit, den Fehler an einer Zwischenschicht \(l\) (nicht Ein- oder Ausgabeschicht) zu berechnen:

\[ \delta^{(l)} = \left[ \left( W^{(l+1)} \right)^T \delta^{(l+1)} \right] \odot g'(z^{(l)}) \]

für \(l \in \{1,\ldots, L-1\}\).

Wir leiten die Formel in Komponentenschreibweise her:

\[ \tag{D2}\delta^{(l)}_i = g'(z^{(l)}_i) \sum_{p=1}^{n_{l+1}} w^{(l+1)}_{p,i}\, \delta^{(l+1)}_p \]

W: Gradient der Gewichte

Vielleicht das Wichtigste: Wir wollen anhand der Fehlerterme \(\delta\) die eigentliche Änderungsrate der Gewichte berechnen. Dies ist schließlich der Betrag, den wir für das Update benötigen. Wir betrachten also die Änderung der Gewichte mit Hilfe der Fehlerterme \(\delta\):

\[ \frac{\partial J}{\partial w^{(l)}_{j,k}} = a^{(l-1)}_k \delta^{(l)}_j \label{W}\tag{W} \]

für \(l \in \{1,\ldots, L\}\).

Die Gesamtheit dieser Ableitungen bildet den Gradienten, der in der “Fehlerlandschaft” in Richtung eines hohen Fehlers zeigt. Da wir den Fehler natürlich minimieren wollen, werden bei den eigentlichen Änderungswerten, den \(\Delta w\)’s, die Ableitungen mit einem Minuszeichen versehen (siehe DelW).

B: Gradient der Bias-Gewichte

Ähnlich wie bei (W) betrachten wir die Änderung der Gewichte der Bias-Neuronen:

\[ \frac{\partial J}{\partial b^{(l)}_i} = \delta^{(l)}_i \label{B}\tag{B} \]

für \(l \in \{1,\ldots, L\}\).

Auch hier wird bei den entsprechenden Änderungswerten, den \(\Delta b\)’s (siehe DelB) die Ableitungen negiert.

Jetzt leiten wir die zentralen Backpropagation-Formeln (D1), (D2), (W) und (B) her.

6.2 Intuition und Vorbereitung

6.2.1 Bedeutung des Gradienten

Worum geht es genau? Wir möchten unser Netzwerk so optimieren, dass der Fehler so gering wie möglich wird. Das, was wir verändern, sind die Gewichte des Netzes.

Der Fehler wird mit \(J\) gemessen. Die Frage ist, wie wir die Gewichte \(w\) verändern, damit \(J\) stets etwas kleiner wird. Wir wissen, dass die Ableitung von \(J\) bezüglich eines speziellen \(w\) in die Richtung zeigt, in der \(J\) größer wird. Also nehmen wir den negativen Wert der Ableitung, um \(w\) anzupassen.

Im Kern der Methode steht also, die Ableitung von \(J\) in Bezug auf jedes Gewicht \(w\) zu berechnen.

Genauer gesagt suchen wir alle partiellen Ableitungen

\[ \frac{\partial J}{\partial w^{(l)}_{i,j}} \]

für alle Schichten \(l\in\{1,\ldots,L\}\), wobei \(j \in \{1, \ldots, n_{l-1}\}\) und \(i \in \{1, \ldots, n_{l}\}\). Diese Ableitungen zusammen genommen bilden den Gradienten.

Da wir die Bias-Neuronen gesondert betrachten, wollen wir außerdem die Ableitung der Fehlerfunktion nach den Bias-Gewichten \(b\) herleiten:

\[ \frac{\partial J}{\partial b^{(l)}_{i}} \]

wobei das \(i \in \{1, \ldots, n_l\}\).

Sehen wir uns das in dieser Abbildung an:

Zusammenhang zwischen einzelnem Gewicht und dem Fehler für ein Trainingsbeispiel

Figure 6.1: Zusammenhang zwischen einem einzelnen Gewicht und dem Fehler \(J\) für ein Trainingsbeispiel

In der Abbildung sehen wir für ein Trainingsbeispiel den Input \(x\) und den berechneten Output \(\hat{y}\). Der Fehler \(J\) arbeitet mit dem korrekten Output \(y\) und dem berechneten Output \(\hat{y}\).

Der Gradient für ein ganz konkretes Gewicht (in diesem Fall \(w_{3,2}^{(1)}\)) ist genau der Zusammenhang zwischen diesem Gewicht und dem Fehler. Ist der Gradient positiv, dann nimmt der Fehler zu, wenn wir das Gewicht erhöhen. Ist der Gradient negativ, dann nimmt der Fehler ab, wenn wir das Gewicht erhöhen. Entsprechend passen wir das Gewicht im Update-Schritt an.

6.2.2 Mehrere Trainingsbeispiele

Nachdem die Bedeutung der Ableitung für ein einziges Gewicht klar geworden ist, machen wir jetzt noch den Schritt zu mehreren Trainingsbeispielen.

Der Fehler \(J\) berechnet sich ja in der Regel aus mehreren Trainingsbeispielen. Wir nehmen mal an, es gäbe nur drei Trainingsbeispiele. Dann ist der Gesamtfehler \(J\) die gemittelte Summe der drei Beispiele:

\[ J = \frac{1}{3} ( J_1 + J_2 + J_3 ) \]

Wenn wir die Ableitung eines Gewichts nehmen, spiegelt diese Ableitung den Einfluss auf diesen Gesamtfehler wieder:

Zusammenhang zwischen einzelnem Gewicht und dem Gesamtfehler

Figure 6.2: Zusammenhang zwischen einem einzelnen Gewicht und dem Gesamtfehler \(J\) (hier beispielhaft für drei Trainingsbeispiele)

In dem Bild oben sollten Sie beachten, dass für jeden Trainingsbeispiel jeweils eigene Gradienten berechnet werden. Das heißt für das rot markierte Gewicht gibt es den Gradienten für Beispiel 1, 2 und 3.

Im Training werden diese drei Gradienten aufgesammelt und dann gemittelt zum Gewichtsupdate genutzt.

Sehen wir uns das konkreter an (wir lassen den Faktor \(\frac{1}{3}\) weg):

\[ \begin{align*} J &= J_1 + J_2 + J_3\\ &= - \sum_{i=1}^m y_i^1 \; log(\hat{y}_i^1) - \sum_{i=1}^m y_i^2 \; log(\hat{y}_i^2) - \sum_{i=1}^m y_i^3 \; log(\hat{y}_i^3) \end{align*} \]

Jetzt nehmen wir an, dass die beiden korrekten Kategorien \(i_1, i_2\) und \(i_3\) sind (da sind die entsprechenden \(y\)’s gleich 1):

\[ J = - log(\hat{y}_{i_1}^1) -log(\hat{y}_{i_2}^2) -log(\hat{y}_{i_3}^2) \]

Jetzt überlegen Sie, wie Sie hier sukzessive die Formeln einsetzen, um diese drei Summanden für drei Trainingsbeispiele aufzulösen:

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

Und bedenken Sie, dass bei den Matrizenmultiplikationen mit den \(W\)’s und den \(a\)’s ganz normale Summen entstehen:

\[ z^{(l)}_i = \sum_{j=1}^{n_{l-1}}\Big[ w_{i, j}^{(l)} \; a_j^{(l-1)}\Big] b_i^{(l)} \]

Dann haben Sie, wenn alle Gleichungen eingesetzt sind und alle Summen aufgelöst sind, tatsächlich eine lange Summe da stehen, vielleicht durchsetzt von Exponentialfunktionen (wegen der Aktivierungsfunktion). Aber Sie können sich vorstellen, dass man diesen langen Term relativ leicht bezüglich eines ganz bestimmten Gewichts ableiten kann. Und darauf basiert Gradientenabstieg und Backpropagation.

6.2.3 Bedeutung des \(\delta\)

Da wir bereits den Backpropagation-Algorithmus gesehen haben, dürften Sie sich zu recht fragen, was das \(\delta\) eigentlich bedeutet, das bislang als “Fehler” bezeichnet wurde. Tatsächlich handelt es sich um die Ableitung von \(J\) nach den Rohinputs der einzelnen Neuronen.

Formal:

\[\delta^{(l)}_i := \frac{\partial J}{\partial z_i^{(l)}} \]

Man kann auch sagen, die Deltawert-Vektoren sind die Gradienten der Rohinput-Vektoren.

Was wir ja eigentlich suchen, ist die Ableitung von \(J\) nach den jeweiligen Gewichten, aber die Deltawerte sind ein nützliches Zwischenergebnis bei der schrittweisen Berechnung der Gewichtsgradienten. Das hängt wiederum mit der Kettenregel zusammen. Wir sehen hoffentlich im Verlauf des Kapitels, warum diese Definition der Deltawerte sinnvoll ist.

6.2.4 Netz mit zwei Schichten

Wir betrachten zunächst ein Netz mit nur zwei Schichten und verallgemeinern im Anschluss auf ein Netz mit beliebig vielen Schichten. Sie werden sehen, dass man mit drei Schichten alle Fälle abdeckt.

Die folgende Abbildung ist eine reduzierte Darstellung des Beispielnetzes. Die Anzahl der Neuronen pro Schicht ist aber allgemein gehalten über die Bezeichnung \(n_l\), wobei \(l\in\{1,2\}\) die Schicht angibt.

Zwei-Schichten-Netz

Warum betrachten wir ein 2-Schichten-Netz? In der Herleitung von Backpropagation unterscheidet man auch im allgemeinen Fall (mit beliebig vielen Schichten) nur zwischen den Gewichten zur Ausgabeschicht und allen anderen Gewichten. Dies kann man auch bei einem 2-Schichten-Netzwerk tun.

Wir gehen in zwei Schritten vor. Zunächst betrachten wir ein Gewicht zur letzten Schicht.

6.3 Schritt 1: Gewicht zur Ausgabeschicht

Da Backpropagation von Schicht 2 zu Schicht 1 arbeitet, wählen wir zunächst eines der “hinteren” Gewichte, also ein Gewicht aus \(W^{(2)}\). Wir nennen dieses einzelne Gewicht \(w^{(2)}_{i,j}\), es befindet sich irgendwo zwischen der ersten und zweiten Schicht.

Gewicht zur Ausgabeschicht

Figure 6.3: In unserem Beispielnetz fokussieren wir auf ein Gewicht zwischen Schicht 1 und Schicht 2.

Um \(w^{(2)}_{i,j}\) zu verändern, wollen wir wissen, wie sich die Fehlerfunktion \(J\) verhält, wenn wir \(w^{(2)}_{i,j}\) ein bisschen kleiner oder größer machen. Genau das sagt uns die Ableitung von \(J\) nach \(w^{(2)}_{i,j}\), also

\[ \tag{a} \frac{\partial J}{\partial w^{(2)}_{i,j}} \]

Wir schauen uns die Fehlerfunktion \(J\) an (wir haben den Index für die Summe hier \(p\) genannt, damit es keine Verwechslungen mit dem \(i\) in \(w^{(2)}_{i,j}\) gibt; außerdem lassen wir die Summe über alle Trainingsbeispiele weg):

\[ J = - \sum_{p=1}^m y_p \; log(\hat{y}_p) \]

Wir sehen, dass \(w^{(2)}_{i,j}\) nicht direkt in \(J\) enthalten ist. Wie können wir also Ableitung (a) finden? Dazu benötigen wir die Kettenregel. Hier wiederum müssen wir zunächst klären, welche Parameter sich in welchen anderen Parametern “verstecken”. Ausgangspunkt ist die berechnete Ausgabe \(\hat{y}\). Zur Erinnerung: \(y\) ist der vom Trainingsbeispiel vorgegebene Zielwert, ist also eine Konstante.

Wenn man - wie in Abbildung 6.3 - vom Gewicht \(w^{(2)}_{i,j}\) ausgeht, dann steckt das Gewicht in der Berechnung eines Rohinputs \(z_i^{(2)}\), der wiederum zu \(\hat{y}_i\) führt. Genauer gesagt zeigt unser Gewicht auf Neuron \(i\) in Schicht 2 und geht also dort in die Berechnung des Rohinputs des i-ten Neurons ein, also in \(z^{(2)}_i\). Wir sehen das an der Berechnungsformel (Z):

\[ z_i^{(2)} = \sum_{p=1}^{n_{1}} w^{(2)}_{i,p} \; a_p^{(1)} \]

Daher können wir die Ableitung (a) mit Hilfe der Kettenregel umformulieren:

\[ \tag{b} \frac{\partial J}{\partial w^{(2)}_{i,j}} = \underbrace{ \frac{\partial J}{\partial z^{(2)}_i}}_{(**)} \, \underbrace{\frac{\partial z^{(2)}_i} {\partial w^{(2)}_{i,j}}}_{(*)}\]

Wir sehen uns zunächst mal den Faktor \((*)\) an: Hier setzen wir die Formel (Z) für \(z^{(2)}_i\) ein und lösen auf.

\[ \frac{\partial z^{(2)}_i}{\partial w^{(2)}_{i,j}} = \frac{\partial }{\partial w^{(2)}_{i,j}} \left( \sum_{p=1}^{n_1} w^{(2)}_{i,p} \; a_p^{(1)} + b_i^{(2)} \right) = a_j^{(1)} \]

Ihnen ist der Schritt zu \(a_j^{(1)}\) nicht klar? Vielleicht hilft Ihnen diese Darstellung der Summe:

\[ \sum_{p=1}^{n_1} w^{(2)}_{i,p} \; a_p^{(1)} + b_i^{(2)} = w^{(2)}_{i,1} \; a_1^{(1)} + w^{(2)}_{i,2} \; a_2^{(1)} + \ldots + w^{(2)}_{i,n_l} \; a_{n_l}^{(1)} + b_i^{(2)} \]

Bei der Ableitung “nach \(w^{(2)}_{i,j}\)” fällt das \(b_i^{(2)}\) weg (Konstante) und von den anderen Summanden bleibt nur der Faktor vor \(w^{(2)}_{i,j}\) übrig:

\[ \frac{\partial }{\partial w^{(2)}_{i,j}} \left( w^{(2)}_{i,1} \; a_1^{(1)} + w^{(2)}_{i,2} \; a_2^{(1)} + \ldots + w^{(2)}_{i,n_l} \; a_{n_l}^{(1)} + b_i^{(2)} \right) = a_j^{(1)} \]

Formel (b) wird durch die obige Formel zu:

\[ \tag{c} \frac{\partial J}{\partial w^{(2)}_{i,j}} = \underbrace{\frac{\partial J}{\partial z^{(2)}_i}}_{(**)} \, a_j^{(1)}\]

Wir lösen jetzt den Faktor \((**)\) auf: Dazu setzen wir die Formel (J) für die Verlustfunktion \(J\) ein. Wir lassen die Summe über die Trainingsbeispiele erst einmal weg.

\[ \begin{align*} \frac{\partial J}{\partial z^{(2)}_i} &= \frac{\partial }{\partial z^{(2)}_i}\Big( - \sum_{p=1}^m y_p \; log(\hat{y}_p) \Big)\\[4mm] &= - \sum_{p=1}^m y_p \; \frac{\partial }{\partial z^{(2)}_i} log(\hat{y}_p)\\[4mm] &= - \sum_{p=1}^m \frac{y_p}{\hat{y}_p} \frac{\partial \hat{y}_p}{\partial z^{(2)}_i} \end{align*} \]

An dieser Stelle verwenden wir bei \(\hat{y}_p\) die Ableitung der Softmax-Funktion (SM-Ab), die wir unten herleiten:

\[ \begin{align*} \frac{\partial J}{\partial z^{(2)}_i} &= - \sum_{p=1}^m \frac{y_p}{\hat{y}_p} \frac{\partial \hat{y}_p}{\partial z^{(2)}_i} \\[4mm] &= - \sum_{p=1}^m \frac{y_p}{\hat{y}_p} \, \hat{y}_p (1_{i=p} - \hat{y}_i) \tag{SM-Ab} \\[4mm] &= - \sum_{p=1}^m y_p \, (1_{i=p} - \hat{y}_i)\\[4mm] &= - \sum_{p=1}^m y_p \, 1_{i=p} + \sum_{p=1}^m y_p \hat{y}_i \\[4mm] &= - y_i + \hat{y}_i \tag{*} \\[4mm] &= \hat{y}_i - y_i \end{align*} \]

Ihnen ist der Übergang auf Zeile \((*)\) nicht klar?

Erste mögliche Problemstelle:

\[ \sum_{p=1}^m y_p \, 1_{i=p} = y_i \]

Der Ausdruck \(1_{i=p}\) bedeutet, dass er dann \(1\) ist, wenn der Laufindex \(p\) der Summe das \(i\) erreicht und sonst einfach \(0\) wird (das wird unten bei der Ableitung von Softmax erklärt). Sehen wir uns die Summe ausgeschrieben an:

\[ \sum_{p=1}^m y_p \, 1_{i=p} = y_1 \, 1_{i=1} + y_2 \, 1_{i=2} + \ldots + y_m \, 1_{i=m} \]

Das \(1_{i=p}\) wird fast immer Null, nur nicht an der Stelle \(i\). Also bleibt nur das \(y_i\) übrig.

Zweite mögliche Problemstelle:

\[ \sum_{p=1}^m y_p \hat{y}_i = \hat{y}_i \]

Hier müssen Sie daran denken, dass \(y\) ein One-Hot-Vektor ist, d.h. genau eine Komponente ist \(1\), alle anderen sind \(0\). Beachten Sie außerdem, dass der Index \(i\) nichts mit der Summe zu tun hat (dort ist der Laufparameter ja \(p\)). Dann können Sie das so hinschreiben:

\[ \begin{align*} \sum_{p=1}^m y_p \hat{y}_i &= \hat{y}_i \sum_{p=1}^m y_p \\ &= \hat{y}_i \end{align*} \]

Wenn wir das von oben in (c) einsetzen, bekommen wir:

\[ \tag{d}\frac{\partial J}{\partial w^{(2)}_{i,j}} = \big( \hat{y}_i - y_i \big) \, a_j^{(1)}\]

6.3.1 Ableitung der Softmax-Funktion

In der Ausgabeschicht \(L\) verwenden wir die Softmax-Funktion (SM), um den Output \(\hat{y}\) zu definieren. Die Rohinputs sind \(z_1^{(L)}, \ldots, z_m^{(L)}\), wir schreiben aber der Einfachheit halber nur \(z_1, \ldots, z_m\). Dann gilt:

\[ \hat{y}_p = \frac{e^{z_p}}{\sum_{j=1}^m e^{z_j}} \]

Wir benötigen die folgende partielle Ableitung (für alle \(i \in {1, \ldots, m}\)):

\[ \frac{\partial \hat{y}_p}{\partial z_i} \]

Wir unterscheiden zwei Fälle:

Fall \(i = p\):

Wir wenden hier hauptsächlich die Quotientenregel beim Ableiten an.

\[ \begin{align*} \frac{\partial \hat{y}_i}{\partial z_i} &= \frac{\partial }{\partial z_i} \frac{e^{z_i}}{\sum_{j=1}^m e^{z_j}}\\[4mm] &= \frac{e^{z_i} \sum_{j=1}^m e^{z_j} - e^{z_i} e^{z_i}}{(\sum_{j=1}^m e^{z_j})^2}\\[4mm] &= \frac{e^{z_i} }{\sum_{j=1}^m e^{z_j}} \Big( 1 - \frac{e^{z_i} }{\sum_{j=1}^m e^{z_j}} \Big)\\[4mm] &= \hat{y}_i ( 1- \hat{y}_i) \end{align*} \]

Fall \(i \neq p\):

\[ \begin{align*} \frac{\partial \hat{y}_p}{\partial z_i} &= \frac{\partial }{\partial z_i} \frac{e^{z_p}}{\sum_{j=1}^m e^{z_j}}\\[4mm] &= \frac{- e^{z_p} e^{z_i}}{(\sum_{j=1}^m e^{z_j})^2}\\[4mm] &= - \frac{e^{z_p}}{\sum_{j=1}^m e^{z_j}} \frac{e^{z_i}}{\sum_{j=1}^m e^{z_j}}\\[4mm] &= - \hat{y}_p \hat{y}_i \end{align*} \]

Ist dieser Übergang nicht klar?

\[ \frac{\partial }{\partial z_i} \frac{e^{z_p}}{\sum_{j=1}^m e^{z_j}} = \frac{- e^{z_p} e^{z_i}}{(\sum_{j=1}^m e^{z_j})^2} \]

Sie müssen hier ebenfalls die Quotientenregel anwenden. Ansonsten machen Sie sich folgende Identitäten klar, dann müssten Sie es verstehen.

Da \(z_p\) für diese Ableitung eine Konstante ist, gilt:

\[ \frac{\partial }{\partial z_i} e^{z_p} = 0 \]

Und bei der folgenden Summe bleibt nur der Summand an der Stelle \(i\) übrig, die anderen werden Null. Man sollte natürlich auch wissen, dass die Ableitung von \(e^x\) wieder \(e^x\) ist:

\[ \frac{\partial }{\partial z_i} {\sum_{j=1}^m e^{z_j}} = e^{z_i} \]

Um das Ergebnis kompakt zu halten, verwenden wir folgende Schreibweise:

\[ 1_{i=p} := \begin{cases} 1 &\mbox{wenn}\quad i = p\\ 0 &\mbox{wenn}\quad i \neq p \end{cases} \]

Dann können wir schreiben:

\[ \tag{SM-Ab} \frac{\partial \hat{y}_p}{\partial z_i} = \hat{y}_p (1_{i=p} - \hat{y}_i) \]

Siehe dazu auch den Artikel von Thomas Kurbiel (Medium 2021).

6.3.2 Fehlerterm \(\delta\)

Betrachten wir noch einmal das obige Ergebnis (c) und definieren den ersten Faktor als Fehlerterm \(\delta^{(2)}_i\).

\[ \frac{\partial J}{\partial w^{(2)}_{i,j}} = \underbrace{\frac{\partial J}{\partial z^{(2)}_i}}_{\delta_i^{(2)}} \, a_j^{(1)}\]

Das heißt wir definieren

\[\tag{D1} \delta^{(2)}_i := \frac{\partial J}{\partial z_i^{(2)}} \]

Für unsere konkrete Situation bedeutet das (siehe d):

\[ \delta^{(2)}_i = \hat{y}_i - y_i \]

Jetzt können wir die Ableitung von \(J\) so darstellen:

\[\frac{\partial J}{\partial w^{(2)}_{i,j}} = a_j^{(1)} \, \delta^{(2)}_i \]

Im nächsten Schritt wird klar, warum diese Definition eines Fehlerterms sinnvoll ist.

6.3.3 Verallgemeinerung (D1, W’)

Da wir in unserer Herleitung immer von der “letzten Schicht” gesprochen haben, können die obige Herleitung auf ein Netz mit \(L\) Schichten verallgemeinern.

Wir halten fest:

\[ \begin{align} \tag{D1}\delta^{(L)}_i &= \hat{y}_i - y_i \\[3mm] \tag{W'}\frac{\partial J}{\partial w^{(L)}_{i,j}} &= a_j^{(L-1)}\, \delta^{(L)}_i \end{align} \]

Wir haben somit (D1) hergeleitet und (W) für den Fall, dass \(l = L\) ist, hergeleitet.

6.4 Schritt 2: Gewicht zu einer Zwischenschicht

Jetzt betrachten wir in unserem Netz ein beliebiges Gewicht zwischen Eingabeschicht und Schicht 1. Wie wir später sehen, ist dieses Gewicht repräsentativ für das Gewicht einer Verbindung, die nicht in die Ausgabeschicht zeigt. Das heißt, wie benötigen nur diese zwei Schritte für die komplette Herleitung.

Gewicht zu einer Zwischenschicht

Man beachte, dass die Indizes \(i\) und \(j\) jetzt möglicherweise andere Indizes sind als beim Gewicht in Schritt 1.

6.4.1 Ziel

Wir erinnern uns wieder an unser Ziel: Die Ableitung der Fehlerfunktion \(J\) nach einem Gewicht \(w^{(1)}_{i,j}\):

\[ \tag{a} \frac{\partial J}{\partial w^{(1)}_{i,j}} \]

Auch hier fragen wir uns wieder: Welche Abhängigkeiten gibt es auf dem “Weg” von Gewicht \(w^{(1)}_{i,j}\) in Richtung Ausgabeschicht bis zu \(J\)? Anders gesagt: In welche Berechnungen (bei Forward Propagation) fließt unser \(w^{(1)}_{i,j}\) ein?

6.4.2 Abhängigkeiten

In der folgenden Abbildung sehen wir, dass der Weg von \(w^{(1)}_{i,j}\) in Richtung Ausgabe zunächst zum Rohinput \(z^{(1)}_i\) führt. Von Schicht 1 zu Schicht 2 sieht man aber, dass der Wert in alle Neuronen der nächsten Schicht eingeht (graue Pfeile). Das muss sich später in den Formeln natürlich widerspiegeln.

Abhängigkeiten

6.4.3 Kettenregel: Zwischen Eingabe und Schicht 1

Wir wenden auch hier wieder die Kettenregel schrittweise an. Wir betrachten jetzt den roten Pfeil und pflegen in Formel (a) den Einfluss auf \(z^{(1)}_i\) ein (in anderen Worten: \(w^{(1)}_{i,j}\) ist in der Formel \(z^{(1)}_i\) enthalten):

\[ \tag{b} \frac{\partial J}{\partial w^{(1)}_{i,j}} = \frac{\partial J}{\partial z^{(1)}_i} \frac{\partial z^{(1)}_i}{\partial w^{(1)}_{i,j}}\]

Wir bleiben beim roten Pfeil und bilden den Einfluss auf \(a^{(1)}_i\) ab:

\[ \tag{c} \frac{\partial J}{\partial w^{(1)}_{i,j}} = \frac{\partial J}{\partial a^{(1)}_i} \frac{\partial a^{(1)}_i}{\partial z^{(1)}_i}\frac{\partial z^{(1)}_i}{\partial w^{(1)}_{i,j}}\]

6.4.4 Kettenregel: Zwischen Schicht 1 und 2

Der nächste Schritt sind die grauen Pfeile rechts. Das ist schwieriger, denn - wie man sieht - hat das \(a^{(1)}_i\) Einfluss auf alle Rohinputs \(z^{(2)}_p\) der Schicht 2. Wir können das durch den Vektor \(z^{(2)}\) ausdrücken:

\[ \frac{\partial J}{\partial w^{(1)}_{i,j}} = \underbrace{\frac{\partial J}{\partial z^{(2)}} \frac{\partial z^{(2)}}{\partial a^{(1)}_i}}_{\text{Einfluss auf Schicht 2}} \quad \frac{\partial a^{(1)}_i}{\partial z^{(1)}_i} \quad \frac{\partial z^{(1)}_i}{\partial w^{(1)}_{i,j}} \]

Das kann man erstmal so hinschreiben, sollte aber beachten, dass die ersten zwei Faktoren Vektoren sind, d.h. es handelt sich um ein Skalarprodukt.

Wir gehen wie schon beim ersten Schritt die drei Faktoren durch:

\[ \tag{d} \frac{\partial J}{\partial w^{(1)}_{i,j}} = \underbrace{\frac{\partial J}{\partial z^{(2)}} \frac{\partial z^{(2)}}{\partial a^{(1)}_i}}_{(1)} \quad \underbrace{\frac{\partial a^{(1)}_i}{\partial z^{(1)}_i}}_{(2)} \quad \underbrace{\frac{\partial z^{(1)}_i}{\partial w^{(1)}_{i,j}}}_{(3)} \]

Zu (1):

Jetzt muss man sich erstmal vor Augen führe, wie diese zwei Vektoren aussehen. Beide haben die Länge \(n_2\) = Anzahl der Neuronen in der zweiten Schicht:

\[ \frac{\partial J}{\partial z^{(2)}} = \begin{pmatrix} \frac{\partial J}{\partial z^{(2)}_1} \\ \vdots \\ \frac{\partial J}{\partial z^{(2)}_{n_2}} \end{pmatrix} \quad \quad \frac{\partial z^{(2)}}{\partial a^{(1)}_i} = \begin{pmatrix} \frac{\partial z^{(2)}_1}{\partial a^{(1)}_i} \\ \vdots \\ \frac{\partial z^{(2)}_{n_2}}{\partial a^{(1)}_i} \end{pmatrix} \]

Jetzt kann man das Skalarprodukt aus (d) wie folgt hinschreiben:

\[ \frac{\partial J}{\partial z^{(2)}} \frac{\partial z^{(2)}}{\partial a^{(1)}_i} = \sum_{p=1}^{n_2} \underbrace{\frac{\partial J}{\partial z^{(2)}_p}}_{\delta^{(2)}_p} \, \frac{\partial z^{(2)}_p}{\partial a^{(1)}_i} \]

Sie sehen es schon oben: Der linke Faktor ist das, was wir in Schritt 1 in (D1) als \(\delta^{(2)}_p\) definiert hatten. Schauen wir uns den rechten Faktor an und setzen dort die Formel (Z) für den Rohinput \(z^{(2)}_p\) ein:

\[ \frac{\partial z^{(2)}_p}{\partial a^{(1)}_i} = \frac{\partial}{\partial a^{(1)}_i} \left( \sum_{h=1}^{n_1} w^{(2)}_{p,h} \; a_h^{(1)} + b_p^{(1)} \right) = w^{(2)}_{p,i} \]

Unser Skalarprodukt sieht also wie folgt aus:

\[\tag{1} \frac{\partial J}{\partial z^{(2)}} \frac{\partial z^{(2)}}{\partial a^{(1)}_i} = \sum_{p=1}^{n_2} w^{(2)}_{p,i}\, \delta^{(2)}_p \]

Zu (2):

Wie schon in Schritt 1 ist dieser Term einfach die Ableitung der Aktivierungsfunktion \(g\). Das kann z.B. die logistische Funktion oder ReLU sein.

\[ \tag{2} \frac{\partial a_i^{(1)}}{\partial z^{(1)}_i} = \frac{\partial}{\partial z^{(1)}_i} g(z^{(1)}_i) = g'(z^{(1)}_i) \]

In diesem Fall lassen wir das \(g'\) stehen und lösen dies nicht weiter auf, so dass die Lösung unabhängig von der konkreten Aktivierungsfunktion bleibt.

Zu (3):

Wie in Schritt 1 setzen wir die Formel für \(z^{(2)}_i\) ein und lösen auf.

\[ \tag{3} \frac{\partial z^{(1)}_i}{\partial w^{(1)}_{i,j}} = \frac{\partial }{\partial w^{(1)}_{i,j}} \left( \sum_{p=1}^{n_0} w^{(1)}_{i,p} \; a_p^{(0)} + b_p^{(1)}\right) = a_j^{(0)} \]

Wir führen jetzt die Einzelergebnisse (1), (2), und (3) zusammen, indem wir sie in (d) einsetzen:

\[ \begin{align} \frac{\partial J}{\partial w^{(1)}_{i,j}} &= \underbrace{ \underbrace{\frac{\partial J}{\partial z^{(2)}} \frac{\partial z^{(2)}}{\partial a^{(1)}_i}}_{(1)} \quad \underbrace{\frac{\partial a^{(1)}_i}{\partial z^{(1)}_i}}_{(2)} }_{\frac{\partial J}{\partial z_i^{(1)}}} \quad \underbrace{\frac{\partial z^{(1)}_i}{\partial w^{(1)}_{i,j}}}_{(3)} \\[4mm] \tag{e} &= \left[ \sum_{p=1}^{n_2} \delta^{(2)}_p \, w^{(2)}_{p,i} \right] \quad g'(z^{(1)}_i) \quad a_j^{(0)} \end{align} \]

6.4.5 Fehlerterm \(\delta\)

Wir definieren jetzt wie schon bei Schritt 1 (D1) den Fehlerterm \(\delta\) als Ableitung von \(J\) nach \(z\) (das sind die Teile (1) und (2) oben zusammengenommen):

\[\tag{D2} \delta^{(1)}_i := \frac{\partial J}{\partial z_i^{(1)}} \]

Wie wir in der Zeile über (e) sehen, besteht dieses Delta aus den Teilen (1) und (2), d.h.:

\[ \tag{f} \delta^{(1)}_i = g'(z^{(1)}_i) \sum_{p=1}^{n_2} w^{(2)}_{p,i}\, \delta^{(2)}_p \]

Jetzt können wir die Ableitung von \(J\) in (e) also mit dem Delta jetzt so darstellen:

\[\tag{g}\frac{\partial J}{\partial w^{(1)}_{i,j}} = a_j^{(0)}\, \delta^{(1)}_i \]

6.4.6 Verallgemeinerung (D2, W)

Wir verallgemeinern wieder die Gleichungen (f) und (g) für Delta und Ableitung. Wir stellen uns vor, das Gewicht tritt an einer beliebigen Schicht \(l\) auf, wobei dieses \(l\) nicht die Schicht direkt vor der Ausgabeschicht ist, d.h. \(l\in\{1,\ldots,L-2\}\).

Dann können wir allgemein formulieren:

\[ \begin{align} \tag{D2}\delta^{(l)}_i &= g'(z^{(l)}_i) \sum_{p=1}^{n_{l+1}} w^{(l+1)}_{p,i}\, \delta^{(l+1)}_p \\[3mm] \tag{W''}\frac{\partial J}{\partial w^{(l)}_{i,j}} &= a_j^{(l-1)}\, \delta^{(l)}_i \end{align} \]

Wir haben somit (D2) hergeleitet und (W) jetzt auch für alle Schichten \(l\) hergeleitet (W’ und W’’ zusammen genommen).

6.5 Bias-Neuronen

Wir wenden die obige Herleitung jetzt auf die Gewichte der Bias-Neuronen an (B). Wir müssen hier nicht unterscheiden, ob wir in der Ausgabeschicht sind. Sehen wir uns also die Situation zwischen Eingabeschicht und Schicht 1 an.

Bias-Neuronen

Wir wenden auch hier wieder die Kettenregel schrittweise an.

\[ \frac{\partial J}{\partial b^{(1)}_i} = \frac{\partial J}{\partial z^{(1)}_i} \frac{\partial z^{(1)}_i}{\partial b^{(1)}_i}\]

Wir bilden den Einfluss auf \(a^{(1)}_i\) ab:

\[ \frac{\partial J}{\partial b^{(1)}_i} = \frac{\partial J}{\partial a^{(1)}_i} \frac{\partial a^{(1)}_i}{\partial z^{(1)}_i}\frac{\partial z^{(1)}_i}{\partial b^{(1)}_i}\]

Der nächste Schritt sind wieder die grauen Pfeile rechts, also der Einfluss auf alle Rohinputs \(z^{(2)}_p\) der Schicht 2.

\[ \frac{\partial J}{\partial b^{(1)}_i} = \underbrace{\frac{\partial J}{\partial z^{(2)}} \frac{\partial z^{(2)}}{\partial a^{(1)}_i}}_1 \quad \underbrace{\frac{\partial a^{(1)}_i}{\partial z^{(1)}_i}}_2 \quad \underbrace{\frac{\partial z^{(1)}_i}{\partial b^{(1)}_i}}_3 \]

Tatsächlich unterscheiden sich die Rechnungen 1 und 2 nicht von der obigen Herleitung zu den Gewichten.

Einen Unterschied gibt es aber in 3: Wir setzen wir die Formel (Z) für \(z^{(1)}_i\) ein und lösen auf.

\[ \frac{\partial z^{(1)}_i}{\partial b^{(1)}_i} = \frac{\partial }{\partial b^{(1)}_i} \left( \sum_{p=1}^{n_1} w^{(1)}_{i,p} \; a_p^{(1)} + b_p^{(1)}\right) = 1 \]

Wir führen jetzt die Einzelergebnisse 1, 2, und 3 wieder zusammen:

\[ \begin{align} \frac{\partial J}{\partial b^{(1)}_i} &= \underbrace{\frac{\partial J}{\partial z^{(2)}} \frac{\partial z^{(2)}}{\partial a^{(1)}_i}}_1 \quad \underbrace{\frac{\partial a^{(1)}_i}{\partial z^{(1)}_i}}_2 \quad \underbrace{\frac{\partial z^{(1)}_i}{\partial b^{(1)}_i}}_3 \\[4mm] &= \left[ \sum_{p=1}^{n_3} \delta^{(2)}_p \, w^{(2)}_{p,i} \right] \quad g'(z^{(1)}_i) \quad 1 \end{align} \]

Unser Delta ist laut (D2) wie folgt definiert:

\[ \delta^{(1)}_i = g'(z^{(1)}_i) \sum_{p=1}^{n_2} w^{(1)}_{p,i}\, \delta^{(2)}_p \]

Wir setzen ein und bekommen:

\[ \frac{\partial J}{\partial b^{(1)}_i} = \delta^{(1)}_i \]

Wir verallgemeinern wieder und stellen uns vor, das Bias-Gewicht tritt an einer beliebigen Schicht \(l\in\{1,\ldots,L\}\) auf:

\[ \frac{\partial J}{\partial b^{(l)}_i} = \delta^{(l)}_i \]

Somit haben wir auch (B) hergeleitet.

6.6 Missing Pieces

6.6.1 Fehler über alle Trainingsbeispiele

Betrachten wir den Fehler an einer Zwischenschicht:

\[ \begin{align} \tag{D2}\delta^{(l)}_i &= g'(z^{(l)}_i) \sum_{p=1}^{n_{l+1}} w^{(l+1)}_{p,i}\, \delta^{(l+1)}_p \\[3mm] \tag{W}\frac{\partial J}{\partial w^{(l)}_{i,j}} &= a_j^{(l-1)}\, \delta^{(l)}_i \end{align} \]

Wir haben bislang immer nur den Fehler für ein Trainingsbeispiel \(k\) betrachtet, d.h. eigentlich müsste (W) so heißen:

\[ \frac{\partial J_k}{\partial w^{(l)}_{i,j}} = a_j^{(l-1)}\, \delta^{(l)}_i \]

Der Zusammenhang zwischen dem Fehler \(J_k\) für ein einziges Trainingsbeispiel und dem Gesamtfehler \(J\) ist:

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

Wenn wir jetzt (J2) verwenden, um alle Trainingsbeispiele zu berücksichtigen, dann wird die Ableitung für ein Gewicht wie folgt:

\[ \begin{align*} \frac{\partial J}{\partial w^{(l)}_{i,j}} &= \frac{\partial }{\partial w^{(l)}_{i,j}} \frac{1}{N} \sum_{k=1}^N J_k \\[4mm] &= \frac{1}{N} \sum_{k=1}^N \frac{\partial J_k}{\partial w^{(l)}_{i,j}} \end{align*} \]

Was man hoffentlich klar sehen kann: Die Ableitung kann ohne weiteres in die Summe hineingezogen werden. Das bedeutet, dass man Backpropagation so verwenden kann, dass man alle Trainingsbeispiele durchläuft, bevor man ein Update durchführt. Dazu berechnet man die einzelnen \(\Delta w\) für jedes Tainingsbeispiel und mittelt am Ende einer Epoche.

Genauso besagt der Zusammenhang oben, dass man Mini-Batch durchführen kann (siehe Abschnitt 5.2.6). Das heißt, man nimmt eine Teilmenge der Trainingsdaten, berechnet die einzelnen \(\Delta w\) nur für diese Daten und mittelt nur über diese Menge.

6.6.2 Regularisierung

Bei unserer Fehlerfunktion \(J\) haben wir den Regularisierungsterm weggelassen (siehe Abschnitt 5.2.2). Mit Regularisierung sieht unsere Verlustfunktion \(J_\text{reg}\) so aus:

\[ J_\text{reg} = J + \frac{\lambda}{2} \sum_{l=1}^{L} \left\Vert W^{(l)} \right\Vert^2 \]

Im letzten Kapitel haben wir in Abschnitt 5.2.2 gezeigt, dass dieser Summand bei der Ableitung aber nur dahingehend eine Rolle spielt, dass er das ursprüngliche Gewicht \(w^{(l)}_{i,j}\) um einen bestimmten Betrag reduziert (weight decay). Man müsste dies bei einer Implementierung natürlich berücksichtigen und die Backpropagation-Formeln entsprechend anpassen.

6.7 Berechnungsgraphen & Automatic Differentiation

Wenn man sich die Herleitung in diesem Kapitel ansieht, könnte man denken, dass Backpropagation sehr speziell auf die Struktur eines Neuronalen Netzes und seinen Schichten mit Gewichtsmatrizen zugeschnitten ist. Tatsächlich ist es aber so, dass man Backpropagation viel allgemeiner verstehen kann, wenn man die Berechnungen in einem Feedforward-Netz als Berechnungsgraph versteht (computational graph). In diesem Berechnungsgraphen können dann Gradienten für jeden “Knoten” im Graphen bei jeder Rechnung automatisch berechnet werden, das nennt man auch Automatic Differentiation und wird bei Keras und PyTorch als “Motor” für Backpropagation benutzt. Bei PyTorch heißt dieses Feature auch Autograd. Diese Gradienten entsprechen den partiellen Ableitungen, die wir besprochen haben, und auch bei Automatic Differentiation gibt es immer eine Vorwärtsberechnung und eine Rückwärtsberechnung.

Ein allgemeines Verständnis von Gradienten und Backpropagation, das nicht direkt an Feedforward-Netze gekoppelt ist, wird ganz konkret relevant bei Techniken wie

Überall dort werden an vielen Stellen Parameter eingeführt, die durch Training “gelernt” werden und man fragt sich an diesen Stellen vielleicht, wie denn genau das umgesetzt wird.

Tipp: Andrej Karpathy erklärt in seinem Video zu Backpropagation (s. unten) das Konzept von Automatic Differentiation mit verblüffend einfachen Mitteln, indem er den Berechnungsgraphen und die Gradientenberechnung in wenigen Zeilen Code programmiert. Bei diesem Erklärungsansatz versteht man sehr intuitiv, dass man für beliebige Rechenoperationen die entsprechenden Paramter über Backpropagation “trainieren” kann.

Insofern kann ich Ihnen das Video nur sehr ans Herz legen. Der Code zu Karpathy’s Video liegt bei GitHub unter karpathy/micrograd (MIT-Lizenz).

Andrej Karpathy ist Informatiker und Spezialist für KI und Computer Vision. Er hat 2016 an der Stanford Universität bei Fei-Fei Li promoviert. 2017-2022 hat er die KI-Abteilung von Tesla aufgebaut, weltweit eine der führenden KI-Abteilungen, die die Autopilot-Mechanismen entwickelt. Er arbeitet seit 2023 wieder bei OpenAI, wo er bereits 2015-17 Research Scientist war. Alle Erklär-Videos seines YouTube-Kanals sind sehr sehenswert.

Ein gute Gelegenheit, Karpathy kennenzulernen, ist auch das Interview von Lex Fridman aus dem Jahr 2022 (etwas 3,5 Stunden, auch als Podcast verfügbar):

6.7.1 Berechnungsgraph

Wir können alle Funktionen und ihre Formeln, also insbesondere die Formeln zur Berechnung des Outputs \(\hat{y}\) eines Neuronalen Netzes, als Berechnungsgraphen verstehen. Auch die Berechnung der Verlustfunktion \(J\) ist im Grunde genommen eine große Berechnungsformel und kann somit mit einem Berechnungsgraphen dargestellt werden (das sollte auch aus den Abbildungen 6.1 und 6.2 hervorgehen).

Um zu verstehen, was mit einem Berechnungsgraphen gemeint ist, sehen wir uns das einfache Beispiel aus Karpathy’s Video an.

Nehmen wir folgende Gleichung, in der drei Variablen vorkommen:

\[ e = a \cdot b \]

Und jetzt nehmen wir an, dass die Variablen \(a\) und \(b\) festgelegte Werte haben.

\[ \begin{align*} a &:= 2 \\[2mm] b &:= -3 \end{align*} \]

Die Formel kann man in einen Berechnungsgraphen überführen, der die Variablen, ihre Werte und die Rechenoperation darstellt.

Wir könnten den Graphen benutzen, um den Wert von \(e\) zu berechnen (grüne Zahl). Wir gehen dazu von links nach rechts. Das nennt man auch Forward Pass (pass im Sinne von Durchgang):

Wir unterscheiden in unserem Graphen also bislang:

  • Variablen, die festgelegte Werte haben (Box gelb unterlegt); in der Welt der Graphen sind diese Boxen sogenannte Blätter (engl. leaf), wohingegen die anderen Boxen Zwischenknoten sind
  • Werte, die berechnet werden (grüne Zahl)

Jetzt ergänzen wir zwei weitere Formeln:

\[ \begin{align*} e &= a \cdot b \\[2mm] d &= e + c \\[2mm] L &= d \cdot f \end{align*} \]

Die Berechnung von \(d\) hängt ab von \(e\) und die Berechnung von \(L\) hängt von \(d\) ab. Einige dieser Variablen (die Blätter) sollen wieder festgelegte Werte haben:

\[ \begin{align*} a &:= 2 \\[2mm] b &:= -3 \\[2mm] c &:= 10 \\[2mm] f &:= -2 \end{align*} \]

Wir können alles in einem einzigen Graphen darstellen, die Blätter sind wieder gelb markiert.

Wir führen einen Forward Pass von links nach rechts durch, um alle Werte zu berechnen. Die berechneten Werte sind wieder grün.

Man kann diesen Vorgang wie beim Neuronalen Netz auch Forward Propagation nennen.

6.7.2 Gradienten

Jetzt möchten wir sehen, wie der Einfluss einer Variablen wie \(a\) oder \(c\) auf unsere “Ausgangsvariable” \(L\) ist. Wenn wir Variable \(a\) ein wenig ändern, also von \(2\) zum Bespiel auf \(2.01\) oder auf \(1.99\), welche Auswirkung hätte das auf \(L\)?

Natürlich könnte man einfach \(a\) ändern und die Auswirkung quasi “messen”, aber das wäre umständlich, wenn man mehrere Milliarden von Parametern hat. Zum Glück wird dieser Zusammenhang eben genau von einer (partiellen) Ableitung beantwortet. Wie bekommen wir diese Ableitungen? Wir fangen zunächst “hinten” an mit unseren Überlegungen. Was ist der Wert der folgenden Ableitung?

\[ \frac{\partial L}{\partial L} \]

Ganz einfach: Diese Ableitung hat den Wert 1. Denn wenn ich \(L\) um 1 erhöhe, erhöht sich \(L\) um 1. Das ist genau der Zusammengang, den eine Ableitung bemisst.

Welchen Wert hat jetzt diese Ableitung?

\[ \frac{\partial L}{\partial d} \]

Hier nehmen leiten wir ganz einfach ab und setzen ein:

\[ \begin{align*} L &= d \cdot f \\[1mm] \frac{\partial L}{\partial d} &= \frac{\partial }{\partial d} (d \cdot f ) \\[1mm] &= f \\[1mm] &= -2.0 \end{align*} \]

Im Graphen sehen wir, dass wir für diese Ableitung den berechneten Wert des Geschwisterknotens benötigen.

In der Abbildung unten sehen Sie auch die Berechnung von

\[ \frac{\partial L}{\partial c} \]

Dieser Fall ist besonders interessant, weil hier zum ersten Mal die Kettenregel zum Zug kommt. Wir verwenden hier nämlich \(\frac{\partial L}{\partial d}\), das wir ja bereits berechnet haben. Bei der Berechnung der Gradienten müssen wir also rückwärts arbeiten, um der Kettenregel Rechnung zu tragen.

Wenn wir die Berechnung der Gradienten für alle Werte fortführen, sieht unser Graph so aus:

Man nennt die Berechnung der Gradienten auch Backward Pass oder Backpropagation.

Karathy zeigt in seinem Video (ab 1:09:00) mit seinem Code Micrograd, wie man diese Gradienten ganz leicht berechnen kann. Dazu berechnet man für jede Rechenoperation (Addition, Multiplikation, Potenz, …) den entsprechenden Gradienten auf Basis der Werte von Geschwisterknoten und mit Hilfe des Gradienten des Elternknotens (wegen der Kettenregel). Die Implementation besprechen wir hier nicht, aber in dem Video wird das ganz ausgezeichnet erläutert.

6.7.3 Autograd in PyTorch

Wie bereits erwähnt, verfügt PyTorch über einen internen Mechanismus, um Gradienten automatisch zu berechnen. Wir zeigen das hier mit den obigen Beispielformeln und -werten.

Zunächst importieren wir PyTorch.

import torch

Wir legen zunächst die Blätter an:

\[ \begin{align*} a &:= 2 \\[2mm] b &:= -3 \\[2mm] c &:= 10 \\[2mm] f &:= -2 \end{align*} \]

Bei diesen Variablen müssen wir PyTorch signalisieren, dass der jeweilige Gradient immer mitberechnet werden soll.

f = torch.tensor(-2.0, requires_grad=True)
a = torch.tensor(2.0, requires_grad=True)
b = torch.tensor(-3.0, requires_grad=True)
c = torch.tensor(10.0, requires_grad=True)

PyTorch kann nur für “Blätter” den Gradienten speichern. Der Grund ist, dass man bei Optimierungsproblemen nur die Gradienten von Blättern benötigt und PyTorch die Ressourcen schonen will. Parameter wie \(w_{i,j}\) oder \(b_i\) sind ja Blätter. Zwischenknoten wären z.B. die Rohinputs \(z\) oder die Aktivierungen \(a\) und die dortigen Gradienten sind nicht relevant (außer als Zwischenergebnis). Siehe auch die Discussion unter Why cant I see .grad of an intermediate variable?

Bei Keras gibt es diese Einschränkung nicht.

Jetzt berechnen wir die anderen Werte mit Hilfe dieser Formeln:

\[ \begin{align*} e &= a \cdot b \\[2mm] d &= e + c \\[2mm] L &= d \cdot f \end{align*} \]

Diese Variablen sind automatisch PyTorch-Tensoren. Hier findet natürlich direkt die Forward Propagation statt, denn die Werte der Variablen werden ausgerechnet.

e = a * b
d = e + c
L = d * f

print('e = ', e)
print('d = ', d)
print('L = ', L)
e =  tensor(-6., grad_fn=<MulBackward0>)
d =  tensor(4., grad_fn=<AddBackward0>)
L =  tensor(-8., grad_fn=<MulBackward0>)

Mit der Funktion backward stoßen wir die Berechnung der Gradienten an. Wir geben uns diese auch direkt aus.

L.backward()

print('a.grad = ', a.grad)
print('b.grad = ', b.grad)
print('c.grad = ', c.grad)
print('f.grad = ', f.grad)
a.grad =  tensor(6.)
b.grad =  tensor(-4.)
c.grad =  tensor(-2.)
f.grad =  tensor(4.)

Sie sehen, dass die Werte mit unserer Rechnung übereinstimmt:

6.7.4 Autodiff in TensorFlow

Auch in Keras bzw. TensorFlow gibt es Automatic Differentiation. Sinnvolle Dokumentationen finden Sie unter Introduction to gradients and automatic differentiation und tf.GradientTape.

Zunächst importieren wir TensorFlow.

import tensorflow as tf

Jetzt legen wir Variablen an. Dies sind unsere Blätter. Bei diesen Variablen legt TensorFlow bei Berechnungen im Hintergrund einen Graphen an.

a = tf.Variable(2.0)
b = tf.Variable(-3.0)
c = tf.Variable(10.0)
f = tf.Variable(-2.0)

In TensorFlow benutzt man jetzt GradientTape als eine “Umgebung”, innerhalb derer Gradienten mitberechnet werden sollen. Wir können dann mit tape.gradient(L, a) die partielle Ableitung von \(L\) bezüglich \(a\) berechnen lassen.

with tf.GradientTape(persistent=True) as tape:
    e = a * b
    d = e + c
    L = d * f
    
print('a grad = ', tape.gradient(L, a))
print('b grad = ', tape.gradient(L, b))
print('c grad = ', tape.gradient(L, c))
print('f grad = ', tape.gradient(L, f))

print('\nd grad = ', tape.gradient(L, d))
print('e grad = ', tape.gradient(L, e))
print('L grad = ', tape.gradient(L, L))
a grad =  tf.Tensor(6.0, shape=(), dtype=float32)
b grad =  tf.Tensor(-4.0, shape=(), dtype=float32)
c grad =  tf.Tensor(-2.0, shape=(), dtype=float32)
f grad =  tf.Tensor(4.0, shape=(), dtype=float32)

d grad =  tf.Tensor(-2.0, shape=(), dtype=float32)
e grad =  tf.Tensor(-2.0, shape=(), dtype=float32)
L grad =  tf.Tensor(1.0, shape=(), dtype=float32)

Nach dem ersten Aufruf von gradient (hier tape.gradient(L, a)) kann man standardmäßig keine weiteren Gradienten abfragen. Daher haben wir hier persistent=True eingestellt, damit wir alle Gradienten auslesen können. Da tape persistent ist, kann man tape.gradient auch außerhalb des with aufrufen (das wird sogar ausdrücklich empfohlen).

Auch bei diesem TensorFlow-Beispiel können Sie kurz checken, ob die berechneten Werte stimmen (Gradienten in rot).

Sie sehen hier, dass auch an den Zwischenknoten \(d\), \(e\) und \(L\) die Gradienten ausgegeben werden können, auch wenn das in der Anwendung nicht notwendig ist (Gewichte sind alle Blätter).

6.7.5 Relevanz für Neuronale Netze und Backpropagation

Nachdem wir viele Beispiele für ein paar sehr simple Gleichungen gesehen haben, kommen wir nochmal auf die Neuronalen Netze zurück. Führen wir uns nochmal die Formeln für Forward Propagation vor Augen:

\[ \begin{align*} z^{(0)} &= x \\[3mm] z^{(l)} &= W^{(l)} \; a^{(l-1)} + b^{(l)} \\[3mm] a^{(l)} &= g(z^{(l)}) \\[3mm] \hat{y} &= a^{(L)} \\[3mm] J &= - \frac{1}{N} \sum_{k=1}^N \sum_{i=1}^m y_i^k \; log(\hat{y}_i^k) \end{align*} \]

Sie sehen vielleicht, dass dies einem großen Berechnungsgraphen entspricht. Sie müssen sich diesen Graphen so vorstellen, dass “links” in der Eingabe aller Trainingsbeispiele eines Batch (also z.B. 32 Trainingsbeispiele) anliegen. Auf der Ausgabeseite, also “rechts”, haben wir eine große Summe in Form der Fehlerformel \(J\).

Einfaches Netz

Wir sehen uns jetzt ein Beispiel an, das zwar auch stark vereinfacht ist, aber eher an ein Neuronales Netz erinnert. Wir nehmen uns zwei Eingabeneuronen \(x_1\) und \(x_2\), die über Gewichte direkt mit dem Output \(\hat{y}\) verbunden sind:

\[ \hat{y} = w_1 x_1 + w_2 x_2 \]

Wir würden das wie folgt in einen Graphen übertragen:

Wir haben Zwischenvariablen \(z_1\) und \(z_2\) eingefügt. Sie erinnern ein bisschen an den Rohinput.

Jetzt wollen wir noch eine Art Fehlerfunktion einbauen:

\[ J = \left( \hat{y} - y \right)^2 \]

Diese Fehlerfunktion ist wirklich sehr einfach. Sie summiert nicht über alle Trainingsbeispiele, sondern misst nur den Fehler des aktuellen Beispiels. Für die Darstellung im Graphen spendieren wir noch eine Zwischenvariable \(d\) für die Differenz \(\hat{y} - y\):

Dieser Graph hat schon einige Ähnlichkeit mit dem, was in einem Neuronalen Netz passiert. Interessant ist hier die Unterscheidung zwischen “Blatt” (gelb) und “Zwischenknoten” (weiß). Die beiden Parameter \(w_1\) und \(w_2\), die wir ja am Ende des Tages anpassen wollen, sind beide Blätter. Die anderen Blätter – \(x_1\), \(x_2\) und \(y\) – kommen aus den Trainingsbeispielen.

Jetzt sehen wir uns die Berechnung für zwei Trainingsbeispiele an. Die Gewichte sind dabei immer gleich. Wir setzen sie auf:

\[ \begin{align*} w_1 &= 0.1\\[3mm] w_2 &= 0.8 \end{align*} \]

Trainingsbeispiel 1

Das erste Trainingsbeispiel hat folgende Werte für Input \(x\) und gewünschten Output \(y\):

\[ \begin{align*} x_1 &= 1.0 \\[3mm] x_2 &= 0.5 \\[3mm] y &= 1 \end{align*} \]

Wir übertragen die Werte für alle “Blätter” in unseren Graphen:

Im Graphen werden jetzt von links nach rechts die Werte an allen Zwischenknoten berechnet:

Anschließend werden über Backpropagation für alle Knoten die Gradienten berechnet. Sie sehen, dass für die Werte des Trainingsbeispiels, also \(x_1\), \(x_2\) und \(y\), die Gradienten uninteressant sind und deshalb hier weggellassen sind.

Sie sehen auch, dass die Gradienten bei \(w_1\) und \(w_2\) uns sagen, ob wir die Gewichte erhöhen oder verringern sollen. In beiden Fällen ist der Gradient negativ, d.h. wenn wir die Gewichte verringern erhöht sich der Fehler. Daher müssen wir die Gewichte erhöhen (umgekehrter Gradient).

Trainingsbeispiel 2

Wir behalten immer die alten Gewichte, aber ändern das Trainingsbeispiel, also das \(x\) und das \(y\):

\[ \begin{align*} x_1 &= 0.1 \\[3mm] x_2 &= 0.8 \\[3mm] y &= 0 \end{align*} \]

Der Graph sieht jetzt wie folgt aus (die alten Gradienten wurden gelöscht):

Im Graphen werden die Werte mit Forward Propagation berechnet:

Anschließend werden die Gradienten mit Backpropagation ermittelt:

Bei \(w_1\) sehen wir, dass das Gewicht keinen Einfluss hat. Das ist klar, weil \(x_1 = 0\) und daher keine Aktivierung transportiert werden kann, egal, wie hoch oder niedrig \(w_1\) wäre. Bei \(w_2\) müssen wir das Gewicht verringern, da der Gradient positiv ist.

Wir hatten oben gesagt, dass die Gradienten beim zweiten Trainingsbeispiel gelöscht wurden. Es kann sinnvoll sein, dies nicht zu tun und stattdessen den neuen Gradienten zum alten zu addieren. Dies wird beim Batch-Training für alle Beispiele im Batch getan, um anschließend den Gradienten über die Batchgröße zu mitteln.

6.7.6 Konklusion

Wie wir gesehen haben, haben Keras und PyTorch Mechanismen, wo die Gradienten an jedem Knoten automatisch berechnet werden. Diese Gradienten können wir direkt für Backpropagation einsetzen, da wir für Backpropagation im Grund “nur” für jedes Gewicht \(w\) die partielle Ableitung des Fehler bezüglich dieses Gewichts benötigen, also

\[ \frac{\partial J}{\partial w} \]

Genau dies bekommen wir aber durch Automatic Differentiation. Die automatische Berechnung von Gradienten in Berechnungsgraphen ist die Verallgemeinerung unseres Backpropagation-Verfahrens und daher für jegliche Art der Parameteranpassung geeignet.