Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Ziel

Verstehen können, was im TensorFlow Playground geschieht.

Genau das Mühsame soll der Gradientenabstieg übernehmen. Dafür brauchen wir den Gradienten der Loss-Funktion nach allen Gewichten aller Schichten.

Training mehrerer Schichten: Motivation

Beispielsetup 1

Eingabedaten sind eindimensional, ein einzelnes Neuron mit Aktivierungsfunktion σ\sigma produziert eine Ausgabe, die stecken wir in ein zweites Neuron mit Aktivierungsfunktion id⁡\operatorname{id}. Also:

ypred=⟨wzweit, σ(⟨werst,x⟩)⟩.y_{\text{pred}} = \bigl\langle w_{\text{zweit}},\, \sigma(\langle w_{\text{erst}}, x \rangle) \bigr\rangle .
Beispielsetup 1: zwei Neuronen hintereinander.

Beispielsetup 1: zwei Neuronen hintereinander.

Wir planen eine Regression mit x,y∈Rx, y \in \mathbb{R}, daher nehmen wir den quadratischen Fehler – hier als reine Summe, ohne den Faktor 1N\frac1N des MSE aus dem Kapitel „Gradientenabstieg“. Der Faktor verschiebt das Minimum nicht, nur die Größe des Gradienten und damit die passende Lernrate:

L(x,y;W)=∑i=1N(yi−ypred,i)2⟹L′(W)=∑i=1N2 (yi−ypred,i)(−∂∂Wypred,i).L(x, y; W) = \sum_{i=1}^N (y_i - y_{\text{pred}, i})^2 \quad\Longrightarrow\quad L'(W) = \sum_{i=1}^N 2\,(y_i - y_{\text{pred}, i}) \left( -\frac{\partial}{\partial W} y_{\text{pred}, i} \right).

Die Frage ist also nur: Was ist ∂∂Wypred\frac{\partial}{\partial W} y_{\text{pred}}, die Ableitung der Netzausgabe nach den Gewichten?

Vorbereitung

Nehmen wir W=(werst,wzweit)W = (w_{\text{erst}}, w_{\text{zweit}}), so können wir ypredy_{\text{pred}} als Funktion von WW auffassen, ypred(W)=⟨wzweit,σ(⟨werst,x⟩)⟩y_{\text{pred}}(W) = \langle w_{\text{zweit}}, \sigma(\langle w_{\text{erst}}, x \rangle) \rangle, und die Ableitung nach WW wie zuvor in Komponenten berechnen. Dafür brauchen wir zwei Bausteine:

Damit:

∂∂werstypred(W)=⟨wzweit, σ′(⟨werst,x⟩) x⟩,∂∂wzweitypred(W)=σ(⟨werst,x⟩).\frac{\partial}{\partial w_{\text{erst}}} y_{\text{pred}}(W) = \bigl\langle w_{\text{zweit}},\, \sigma'(\langle w_{\text{erst}}, x \rangle)\, x \bigr\rangle, \qquad \frac{\partial}{\partial w_{\text{zweit}}} y_{\text{pred}}(W) = \sigma(\langle w_{\text{erst}}, x \rangle).

Die Update-Regeln

Eingesetzt in L′L' und in die Gradientenabstiegs-Regel Wneu=Walt−ρL′(Walt)W_{\text{neu}} = W_{\text{alt}} - \rho L'(W_{\text{alt}}):

wzweit,neu=wzweit,alt+ρ∑i=1N2 (yi−⟨wzweit,alt,σ(⟨werst,alt,xi⟩)⟩) σ(⟨werst,alt,xi⟩),w_{\text{zweit,neu}} = w_{\text{zweit,alt}} + \rho \sum_{i=1}^N 2\,\Bigl(y_i - \bigl\langle w_{\text{zweit,alt}}, \sigma(\langle w_{\text{erst,alt}}, x_i \rangle) \bigr\rangle\Bigr)\, \sigma(\langle w_{\text{erst,alt}}, x_i \rangle),
werst,neu=werst,alt+ρ∑i=1N2 (yi−⟨wzweit,alt,σ(⟨werst,alt,xi⟩)⟩) ⟨wzweit,alt,σ′(⟨werst,alt,xi⟩) xi⟩.w_{\text{erst,neu}} = w_{\text{erst,alt}} + \rho \sum_{i=1}^N 2\,\Bigl(y_i - \bigl\langle w_{\text{zweit,alt}}, \sigma(\langle w_{\text{erst,alt}}, x_i \rangle) \bigr\rangle\Bigr)\, \bigl\langle w_{\text{zweit,alt}}, \sigma'(\langle w_{\text{erst,alt}}, x_i \rangle)\, x_i \bigr\rangle .

Das funktioniert, aber es ist unübersichtlich – und man sieht schon, dass Teilausdrücke mehrfach vorkommen: der Residuenterm yi−ypred,iy_i - y_{\text{pred},i} und die Aktivierung σ(⟨werst,xi⟩)\sigma(\langle w_{\text{erst}}, x_i \rangle) der ersten Schicht tauchen in beiden Regeln auf.

Beispielsetups 2 und 3: mehr Dimensionen, mehr Neuronen

Setup 2: Eingabedaten sind zweidimensional, sonst wie oben. Jetzt neu: werst∈R2w_{\text{erst}} \in \mathbb{R}^2. An den Formeln ändert sich nichts, die Skalarprodukte sind nur länger.

Setup 3: Eingabedaten sind zweidimensional, zwei Neuronen mit Aktivierungsfunktion σ\sigma und Gewichten u,v∈R2u, v \in \mathbb{R}^2 produzieren Ausgaben, die stecken wir in ein drittes Neuron mit Aktivierungsfunktion id⁡\operatorname{id} und Gewichten w∈R2w \in \mathbb{R}^2:

ypred=⟨w, (σ⟨u,x⟩, σ⟨v,x⟩)⟩.y_{\text{pred}} = \bigl\langle w,\, (\sigma\langle u, x \rangle,\ \sigma\langle v, x \rangle) \bigr\rangle .

Layer

Mehrere Neuronen, die parallel verarbeitet werden, bilden eine Schicht (Layer). Mehrmals lineare Regression ist multiple lineare Regression ist ein Linear Layer ist eine Matrix, deren Zeilen die Gewichtsvektoren sind. Schreiben wir also u,vu, v als Zeilen in eine Matrix A∈R2×2A \in \mathbb{R}^{2 \times 2} und benutzen σ\sigma auf Vektoren komponentenweise, so ist

ypred=⟨w,σ(Ax)⟩=wTσ(Ax).y_{\text{pred}} = \langle w, \sigma(Ax) \rangle = w^T \sigma(Ax).

Die Ableitung nach einem einzelnen Gewicht u1=A11u_1 = A_{11} ist ∂∂u1=∂∂A11\frac{\partial}{\partial u_1} = \frac{\partial}{\partial A_{11}}, und für die ganze Matrix:

∂∂Aypred=∂∂AwTσ(Ax)=wT∂∂Aσ(Ax)=wTσ′(Ax) x,∂∂wypred=∂∂wwTσ(Ax)=σ(Ax).\frac{\partial}{\partial A} y_{\text{pred}} = \frac{\partial}{\partial A} w^T \sigma(Ax) = w^T \frac{\partial}{\partial A} \sigma(Ax) = w^T \sigma'(Ax)\, x, \qquad \frac{\partial}{\partial w} y_{\text{pred}} = \frac{\partial}{\partial w} w^T \sigma(Ax) = \sigma(Ax).

(Wobei bei wTσ′(Ax) xw^T \sigma'(Ax)\, x die Dimensionen passend zu lesen sind: σ′(Ax)\sigma'(Ax) ist eine Diagonalmatrix, und das Ergebnis ist eine 2×22 \times 2-Matrix, deren Eintrag (j,k)(j, k) gleich wjσ′((Ax)j)xkw_j \sigma'((Ax)_j) x_k ist.)

Idee: Berechne für ypredy_{\text{pred}} gleichzeitig die Daten für den Gradienten. Nach der Kettenregel ddxf(g(x))=f′(g(x)) g′(x)\frac{d}{dx} f(g(x)) = f'(g(x))\, g'(x) mit g(x)=σ(Ax)g(x) = \sigma(Ax) und f(z)=wTzf(z) = w^T z berechnen wir zuerst gg und g′g' vom ersten Layer, dann damit den nächsten Layer, usw. bis zum letzten. Die Zwischenergebnisse der Vorwärtsrechnung sind genau das, was die Rückwärtsrechnung braucht.

Backpropagation konkret

Beispiel 4: ein Netz mit drei Schichten

Wir betrachten

f(x)=b3+W3tanh⁡(b2+W2tanh⁡(b1+W1x))f(x) = b_3 + W_3 \tanh\bigl(b_2 + W_2 \tanh(b_1 + W_1 x)\bigr)

und schreiben die Vorwärtsrechnung (forward computation) schichtweise auf:

z1=x(input layer)z2=tanh⁡(b1+W1z1)(2nd layer)z3=tanh⁡(b2+W2z2)(3rd layer)ypred=b3+W3z3(output layer)r=ypred−y(residual)E=12 rTr(squared error)\begin{aligned} z_1 &= x && \text{(input layer)} \\ z_2 &= \tanh(b_1 + W_1 z_1) && \text{(2nd layer)} \\ z_3 &= \tanh(b_2 + W_2 z_2) && \text{(3rd layer)} \\ y_{\text{pred}} &= b_3 + W_3 z_3 && \text{(output layer)} \\ r &= y_{\text{pred}} - y && \text{(residual)} \\ E &= \tfrac12\, r^T r && \text{(squared error)} \end{aligned}

Der Faktor 12\tfrac12 ist reine Bequemlichkeit, er kürzt sich gleich gegen die 2 aus der Ableitung des Quadrats.

Rückwärts differenzieren

Wir verwenden die Notation der Differentiale: dEdE ist die lineare Änderung von EE bei einer infinitesimalen Änderung der Parameter, und ∇bE\nabla_b E bezeichnet den Gradienten von EE nach bb (mit der Konvention dE=⟨∇bE,db⟩dE = \langle \nabla_b E, db \rangle). Wir arbeiten uns von hinten nach vorn (backwards differentiation).

Output layer. Es ist

dE=d(12rTr)=12 d(rTr)=rT dr=rT dypred,dE = d\bigl(\tfrac12 r^T r\bigr) = \tfrac12 \, d(r^T r) = r^T\, dr = r^T\, dy_{\text{pred}},

denn yy ist konstant. Weiter ist dypred=d(b3+W3z3)=db3+dW3 z3+W3 dz3dy_{\text{pred}} = d(b_3 + W_3 z_3) = db_3 + dW_3\, z_3 + W_3\, dz_3. Für die Ableitung nach b3b_3 betrachten wir nur den ersten Term: dE=rTdb3dE = r^T db_3, also

∇b3E=r.\nabla_{b_3} E = r .

Für die Ableitung nach W3W_3 ist das Ergebnis

∇W3E=r z3T.\nabla_{W_3} E = r\, z_3^T .

Das ist ein äußeres Produkt: Der Gradient nach der Gewichtsmatrix ist Residuum mal Eingabe der Schicht, transponiert – so wie beim einzelnen Neuron der Gradient nach ww gleich Fehler mal xx war.

Nächste Schicht. Jetzt können wir b3b_3 und W3W_3 als Konstanten sehen: dE=rTW3 dz3+irrelevante TermedE = r^T W_3\, dz_3 + \text{irrelevante Terme}. Mit der Kettenregel gilt ddztanh⁡(f(z))=(1−tanh⁡(f(z))2) f′(z)\frac{d}{dz} \tanh(f(z)) = (1 - \tanh(f(z))^2)\, f'(z), also

dz3=dtanh⁡(b2+W2z2)=(1−z32)⊙(db2+dW2 z2+W2 dz2),dz_3 = d \tanh(b_2 + W_2 z_2) = (1 - z_3^2) \odot (db_2 + dW_2\, z_2 + W_2\, dz_2),

wobei z32z_3^2 und ⊙\odot komponentenweise zu lesen sind. Für b2b_2: dE=rTW3diag⁡(1−z32) db2dE = r^T W_3 \operatorname{diag}(1 - z_3^2)\, db_2, also

∇b2E=(1−z32)⊙W3Tr,∇W2E=(∇b2E) z2T.\nabla_{b_2} E = (1 - z_3^2) \odot W_3^T r, \qquad \nabla_{W_2} E = (\nabla_{b_2} E)\, z_2^T .

Und noch eine Schicht. Nach demselben Muster:

∇b1E=(1−z22)⊙W2T(∇b2E),∇W1E=(∇b1E) z1T.\nabla_{b_1} E = (1 - z_2^2) \odot W_2^T (\nabla_{b_2} E), \qquad \nabla_{W_1} E = (\nabla_{b_1} E)\, z_1^T .

Das Muster

Zusammengefasst, mit δk:=∇bkE\delta_k := \nabla_{b_k} E als „Fehlersignal“ der Schicht kk:

δ3=r,δ2=(1−z32)⊙W3Tδ3,δ1=(1−z22)⊙W2Tδ2,∇WkE=δk zkT.\delta_3 = r, \qquad \delta_2 = (1 - z_3^2) \odot W_3^T \delta_3, \qquad \delta_1 = (1 - z_2^2) \odot W_2^T \delta_2, \qquad \nabla_{W_k} E = \delta_k\, z_k^T .

Das Fehlersignal wird von Schicht zu Schicht rückwärts propagiert: mit der transponierten Gewichtsmatrix multipliziert und mit der Ableitung der Aktivierungsfunktion gewichtet. Man nutzt die Zwischenschritte zkz_k aus der Vorwärtsrechnung, um den vollen Gradienten effizient zu berechnen – ein Vorwärtsdurchlauf, ein Rückwärtsdurchlauf, fertig. Der Aufwand ist etwa das Doppelte einer Inferenz, unabhängig von der Anzahl der Parameter. Das ist Backpropagation, und danach kommt Gradientenabstieg.

Genau das tut loss.backward() in PyTorch (Kapitel „Autograd und Berechnungsgraph“): Beim Vorwärtsrechnen merkt sich jeder Tensor mit requires_grad, aus welcher Operation er entstanden ist (der Berechnungsgraph), und backward läuft diesen Graphen rückwärts ab und schreibt die Gradienten in die .grad-Attribute der Parameter. optimizer.step() macht dann W←W−ρ∇WEW \leftarrow W - \rho \nabla_W E.

Matrix-Differentialkalkül: Literaturtipps

Achtung: Das ist nicht notwendig für diese Veranstaltung, das ist weiterführend.

Backpropagation als Induktion

Noch einmal systematisch, als Wiederholung und Zusammenfassung aus der Übung – denn Backpropagation ist Klausurstoff, und die Formulierung als Induktion ist die, mit der man jede Architektur erschlägt.

Warum Backpropagation funktioniert

  1. Im Forward Pass speichern wir für jede Rechenoperation das Zwischenergebnis und merken uns die Ableitung der einzelnen Rechenoperation und auf welche vorangehenden Ergebnisse wir zurückgreifen müssen (Konstruktion des DAG).

  2. Im Backward Pass können wir für jede Rechenoperation jeweils nach den Gewichten ableiten, die für diese Operation relevant sind. Wenn wir nach Gewichten ableiten, von denen die vorangegangenen Rechenoperationen abhängen, müssen wir die Kettenregel anwenden. Dazu benötigen wir nur die Ableitung der neuen Rechenoperation (gespeichert im Forward Pass), den Wert der Rechenoperation (gespeichert im Forward Pass) und die Ableitung der vorangegangenen Rechenoperationen.

Mit vollständiger Induktion sehen wir also: Wir können alle Ableitungen berechnen mit einem Durchlauf durch den DAG. Insbesondere können wir die Loss-Funktion (das „Ende“ des DAG) nach allen Gewichten ableiten und so den vollständigen Gradienten berechnen.

Der Induktionsschritt, systematisch

Wir bezeichnen den Input als x∈Xx \in \mathcal{X}. Die „bisherigen Rechenoperationen“ sind eine Abbildung Y ⁣:X→YY \colon \mathcal{X} \to \mathcal{Y}, also y:=Y(x;wY)y := Y(x; w_Y), deren Ableitungen dYdwY(x;wY)\frac{dY}{dw_Y}(x; w_Y) wir per Induktion kennen. Die „nächste Rechenoperation“ ist eine Abbildung Z ⁣:Y→ZZ \colon \mathcal{Y} \to \mathcal{Z}, deren Ableitungen dZdy\frac{dZ}{dy} und dZdwZ\frac{dZ}{dw_Z} wir kennen. Die Kettenregel erlaubt uns, Z(Y(x;wY);wZ)Z(Y(x; w_Y); w_Z) nach wYw_Y abzuleiten:

ddwYZ(Y(x;wY);wZ)=dZdy(y;wZ)⋅dYdwY(x;wY),ddwZZ(Y(x;wY);wZ)=dZdwZ(y;wZ).\frac{d}{dw_Y} Z(Y(x; w_Y); w_Z) = \frac{dZ}{dy}(y; w_Z) \cdot \frac{dY}{dw_Y}(x; w_Y), \qquad \frac{d}{dw_Z} Z(Y(x; w_Y); w_Z) = \frac{dZ}{dw_Z}(y; w_Z).

Spezialfälle: Wenn die nächste Rechenoperation der Loss ist, gibt es keine „neuen Gewichte“, dann ist nur die Ableitung nach „bisherigen Gewichten“ relevant. Der Induktionsanfang ist gegeben, indem YY die Identitätsfunktion ist (Ableitung ist 1). In knapperer Notation, mit Kleinbuchstaben als Funktionsnamen:

dzdw=dzdy⋅dydw.\frac{dz}{dw} = \frac{dz}{dy} \cdot \frac{dy}{dw}.

Nicht konkret genug? Ein weiteres einfaches Beispiel

Wir wählen als Input x∈X⊆Rnx \in \mathcal{X} \subseteq \mathbb{R}^n. Im Beispiel soll y:=y(x;w):=w⋅xy := y(x; w) := w \cdot x sein, also ein linearer Layer. Die „nächste Rechenoperation“ ist z ⁣:R→Rz \colon \mathbb{R} \to \mathbb{R}, und zwar z:=z(y):=ReLU⁡(y)=max⁡(0,y)z := z(y) := \operatorname{ReLU}(y) = \max(0, y). Die Ableitung nach yy ist [y>0][y > 0]. Aus dzdw=dzdy⋅dydw\frac{dz}{dw} = \frac{dz}{dy} \cdot \frac{dy}{dw} wird also

dzdw=[y>0]⋅x.\frac{dz}{dw} = [y > 0] \cdot x .

Übung

Ein Netz x→y=ReLU(v x)→z=ReLU(w y)x \to y = \mathrm{ReLU}(v\,x) \to z = \mathrm{ReLU}(w\,y) mit v=2v = 2, w=−1w = -1, Eingabe x=3x = 3 und Ziel t=1t = 1; Loss L=12(z−t)2L = \tfrac12 (z-t)^2.

  1. Forward Pass: Berechnen Sie yy, zz und LL.

  2. Berechnen Sie ∂L∂w\frac{\partial L}{\partial w} und ∂L∂v\frac{\partial L}{\partial v}. Tipp: ∂L∂z=z−t\frac{\partial L}{\partial z} = z - t, die schwache Ableitung von ReLU an der Stelle aa ist [a>0][a > 0], und der Gradient eines Gewichts ist Eingabe des Neurons mal Ableitung des Fehlers nach seiner Ausgabe.

  3. Setzen Sie w=+1w = +1 und rechnen Sie noch einmal. Was hat sich an ∂L∂v\frac{\partial L}{\partial v} qualitativ geändert, und warum?

Antwortprobe: In genau einem der beiden Fälle ist ∂L∂v=0\frac{\partial L}{\partial v} = 0.

Und eine größere, wenn Sie Lust haben: Nehmen Sie das Beispiel aus dem Abschnitt „Nicht konkret genug?“ (y=⟨a,x⟩y = \langle a, x \rangle, dann z=ReLU⁡(y)z = \operatorname{ReLU}(y)) und geben Sie der ersten Schicht ein zweites Neuron, also eine zweite Zeile a′∈Rna' \in \mathbb{R}^n. Der Loss ist dann ∥z−zground truth∥2\|z - z_{\text{ground truth}}\|^2 mit z∈R2z \in \mathbb{R}^2. Leiten Sie nach beiden Zeilen ab und prüfen Sie, dass die beiden Gradienten sich nicht gegenseitig beeinflussen.