Backpropagation
Kurz gesagt: Backpropagation berechnet mit der Kettenregel die Steigung des Fehlers für alle Gewichte auf einmal, von der Ausgabe zurück zur Eingabe.
Problem
Neun Zahlen. Welche bewegst du?
Das Netz aus der Lektion über Schichten hat die Regel der verschiedenen Länder gelöst, weil seine Gewichte von Hand gewählt waren. Ein echtes Netz beginnt mit Gewichten, die nichts bedeuten, und muss sie selbst finden.
Hier ist dasselbe Netz mit Startgewichten. Ein Kunde im Ausland zahlt mit einer ausländischen Karte von einer ausländischen IP, Fall (1, 1). Das ist in Ordnung, das Label ist 0. Das Netz sagt 0,4489.
Das Netz hat neun Zahlen: vier Gewichte und zwei Biases in der verborgenen Schicht, zwei Gewichte und einen Bias an der Ausgabe. Welche bewegst du, in welche Richtung und wie weit? Wenn du blind probierst, ist jeder Versuch ein Vorwärtsdurchlauf. Bei einem Netz mit einer Million Gewichten (ein Beispiel für die Größenordnung) sind das Millionen Versuche für einen Schritt.
Die Idee
Das Gewicht anstupsen und den Fehler beobachten
Der Fehler für eine Zahlung ist (p − y)², das Quadrat der Differenz. Nimm ein Gewicht, v[0], und bewege es ein wenig nach oben und ein wenig nach unten, um 0,0001. Jedes Mal berechnest du den Fehler neu.
Das Netz als Klasse mit den neun Zahlen, die Funktionen aus der vorigen Lektion und der Fehler:
class Net(double[][] w, double[] b, double[] v, double c){ public double[][] W = w; // die verborgene Schicht: Gewichte und Biases public double[] B = b; public double[] V = v; // die Ausgabe: Gewichte und Bias public double C = c;
public static Net Zeros() => new([[0, 0], [0, 0]], [0, 0], [0, 0], 0);}static double[] Hidden(Net net, double[] x) => [Neuron(x, net.W[0], net.B[0]), Neuron(x, net.W[1], net.B[1])];
static double Forward(Net net, double[] x) => Neuron(Hidden(net, x), net.V, net.C);
static double Loss(Net net, double[] x, double y){ var d = Forward(net, x) - y; return d * d;}Die Startgewichte und die Zahlung:
// die Startgewichte: das Netz hat noch nicht gelerntvar net = new Net([[1.5, 1.0], [2.5, 2.0]], [-0.5, -3.0], [1.5, -1.5], -0.3);double[] x = [1, 1]; // eine ausländische Karte von einer ausländischen IP: ein Kunde im Auslanddouble y = 0;Console.WriteLine($"p = {Forward(net, x):F4}, Fehler = {Loss(net, x, y):F6}");var old = net.V[0];net.V[0] = old + 0.0001;var up = Loss(net, x, y);net.V[0] = old - 0.0001;var down = Loss(net, x, y);net.V[0] = old;Console.WriteLine($"v[0] + 0.0001: Fehler = {up:F6}");Console.WriteLine($"v[0] - 0.0001: Fehler = {down:F6}");Console.WriteLine($"Steigung = {(up - down) / 0.0002:F4}");p = 0.4489, Fehler = 0.201500v[0] + 0.0001: Fehler = 0.201520v[0] - 0.0001: Fehler = 0.201481Steigung = 0.1956Nach oben wächst der Fehler, nach unten fällt er. Die Differenz geteilt durch den Abstand der beiden Punkte (0,0002) ist die Steigung: 0,1956. Positiv heißt „ein größeres v[0] ergibt einen größeren Fehler“. Also muss v[0] nach unten. Das ist die Steigung aus dem Gradientenabstieg, nur für eines der neun Gewichte.
Es funktioniert, braucht aber für jedes Gewicht zwei Vorwärtsdurchläufe. Wir brauchen einen Weg, die Steigung zu bekommen, ohne das Gewicht anzufassen.
Die Idee
Die Kettenregel mit Zahlen
v[0] berührt den Fehler nicht direkt. Es berührt die Summe z an der Ausgabe, z berührt p, und p berührt den Fehler. Eine Kette aus drei Gliedern. Wenn du weißt, wie stark sich jedes Glied bewegt, wenn du das vorige bewegst, ist die Steigung ihr Produkt:
- Der Fehler (p − y)² bewegt sich um 2·(p − y), wenn du p bewegst.
- p = σ(z) bewegt sich um p·(1 − p), wenn du z bewegst. Das ist die Steigung der Sigmoid-Funktion.
- z = v[0]·h[0] + v[1]·h[1] + c bewegt sich um h[0], wenn du v[0] bewegst.
var h = Hidden(net, x);var p = Neuron(h, net.V, net.C);var a = 2 * (p - y); // wie sich der Fehler mit p ändertvar s = p * (1 - p); // wie sich p mit seiner Summe z ändertvar dz = h[0]; // wie sich z mit v[0] ändertConsole.WriteLine($"2(p - y) = {a:F4}");Console.WriteLine($"p(1 - p) = {s:F4}");Console.WriteLine($"h[0] = {dz:F4}");Console.WriteLine($"Steigung für v[0] = {a * s * dz:F4}");2(p - y) = 0.8978p(1 - p) = 0.2474h[0] = 0.8808Steigung für v[0] = 0.19560,8978 × 0,2474 × 0,8808 = 0,1956. Dieselbe Zahl wie beim Anstupsen, aber ohne eine einzige neue Berechnung des Netzes. Das ist die Kettenregel: Die Steigung entlang einer Kette ist das Produkt der Steigungen ihrer Glieder.
Die Idee
Der Fehler läuft zurück
Die ersten beiden Glieder sind allem gemeinsam, was vor der Ausgabe liegt. Ihr Produkt ist der Anteil der Ausgabe, dOut = 2·(p − y)·p·(1 − p) = 0,2221. Von dort:
- Die Gewichte der Ausgabe: Die Steigung für v[j] ist dOut × h[j].
- Der Bias der Ausgabe: Die Steigung für c ist dOut selbst, denn z bewegt sich um 1, wenn du c bewegst.
- Die verborgenen Neuronen: h[j] geht mit dem Gewicht v[j] in z ein. Sein Anteil ist dH = dOut × v[j] × h[j]·(1 − h[j]): der Anteil der Ausgabe am Fehler, mal die Stärke der Kante, mal die Steigung der eigenen Sigmoid-Funktion.
- Die verborgenen Gewichte: Die Steigung für w[j][i] ist dH × x[i], und für b[j] ist sie dH.
Der Anteil am Fehler läuft über dieselben Kanten zurück, über die das Signal vorwärts ging, und jedes Gewicht bekommt seinen Teil. Das ist die Rückwärtsausbreitung des Fehlers (kurz Backprop): ein Durchlauf vorwärts, um h und p zu kennen, und einer rückwärts, für alle Steigungen auf einmal.
static Net Backward(Net net, double[] x, double y){ var h = Hidden(net, x); var output = Neuron(h, net.V, net.C); var g = Net.Zeros(); // der Anteil der Ausgabe var dOut = 2 * (output - y) * output * (1 - output); g.C = dOut; for (var j = 0; j < 2; j++) { g.V[j] = dOut * h[j]; // der Anteil des verborgenen Neurons j var dH = dOut * net.V[j] * h[j] * (1 - h[j]); g.B[j] = dH; for (var i = 0; i < 2; i++) { g.W[j][i] = dH * x[i]; } } return g;}Net.Zeros() ist ein Netz aus Nullen mit derselben Form: Es gibt für jedes Gewicht eine Steigung, also werden die Steigungen in derselben Klasse gehalten.Sieh es dir an
Anstupsen gegen Backprop, für alle neun Gewichte
Damit eine Schleife die neun Zahlen durchgehen kann, nummerieren get und set sie von 0 bis 8. nudge ist das Anstupsen von oben, für das Gewicht mit der Nummer k:
string[] names = ["w[0][0]", "w[0][1]", "w[1][0]", "w[1][1]", "b[0]", "b[1]", "v[0]", "v[1]", "c"];
// k: w[0][0], w[0][1], w[1][0], w[1][1], b[0], b[1], v[0], v[1], cstatic double Get(Net n, int k) => k < 4 ? n.W[k / 2][k % 2] : k < 6 ? n.B[k - 4] : k < 8 ? n.V[k - 6] : n.C;
static void Set(Net n, int k, double value){ if (k < 4) n.W[k / 2][k % 2] = value; else if (k < 6) n.B[k - 4] = value; else if (k < 8) n.V[k - 6] = value; else n.C = value;}static double Nudge(Net net, int k, double[] x, double y){ var old = Get(net, k); Set(net, k, old + 0.0001); var up = Loss(net, x, y); Set(net, k, old - 0.0001); var down = Loss(net, x, y); Set(net, k, old); return (up - down) / 0.0002;}var g = Backward(net, x, y);Console.WriteLine(" Anstupsen backprop");for (var k = 0; k < 9; k++){ Console.WriteLine($"{names[k],-8}{Nudge(net, k, x, y),10:F4}{Get(g, k),10:F4}");} Anstupsen backpropw[0][0] 0.0350 0.0350w[0][1] 0.0350 0.0350w[1][0] -0.0497 -0.0497w[1][1] -0.0497 -0.0497b[0] 0.0350 0.0350b[1] -0.0497 -0.0497v[0] 0.1956 0.1956v[1] 0.1816 0.1816c 0.2221 0.2221Alle neun stimmen bis zur vierten Nachkommastelle überein. Die Eingaben sind (1, 1), deshalb haben die zwei Gewichte und der Bias eines verborgenen Neurons dieselbe Steigung: Jedes von ihnen bewegt das z dieses Neurons um 1. Und die beiden verborgenen Neuronen haben entgegengesetzte Vorzeichen, weil v[0] = 1,5 und v[1] = −1,5 in entgegengesetzter Richtung in die Ausgabe eingehen.
Sieh es dir an
Das Signal vorwärts, der Fehler zurück
static Net Backward(Net net, double[] x, double y){ var h = Hidden(net, x); var output = Neuron(h, net.V, net.C); var g = Net.Zeros(); // der Anteil der Ausgabe var dOut = 2 * (output - y) * output * (1 - output); g.C = dOut; for (var j = 0; j < 2; j++) { g.V[j] = dOut * h[j]; // der Anteil des verborgenen Neurons j var dH = dOut * net.V[j] * h[j] * (1 - h[j]); g.B[j] = dH; for (var i = 0; i < 2; i++) { g.W[j][i] = dH * x[i]; } } return g;}Schritt für Schritt, in Worten
- Die Startgewichte und der Fall x = (1, 1): beide aus dem Ausland, also in Ordnung, y = 0.
- Erst vorwärts: h1 = 0,8808, h2 = 0,8176, p = 0,4489.
- Der Fehler: (0,4489 − 0)² = 0,2015. Das Netz sollte weniger sagen.
- Zurück von der Ausgabe: Der Anteil ist 2(p − y) · p(1 − p) = 0,8978 × 0,2474 = 0,2221, zugleich die Steigung für c.
- Die Steigung für v[j] ist der Anteil mal h[j]: 0,2221 × 0,8808 = 0,1956 und 0,2221 × 0,8176 = 0,1816.
- Zurück entlang v und durch σ: h1 bekommt 0,2221 × 1,5 × 0,1050 = 0,0350, h2 bekommt −0,0497. Das sind auch die Steigungen für b.
- Die Steigung für w[j][i] ist der Anteil von h[j] mal x[i]. Beide Eingaben sind 1: 0,0350 und −0,0497.
- Neun Steigungen aus einem Durchlauf rückwärts, dieselben wie beim Anstupsen. Ein Gewicht mit positiver Steigung geht nach unten.
Werte laufen vorwärts, Steigungen laufen zurück. Die Steigung jeder Kante ist der Anteil des Neurons, zu dem die Kante führt, mal der Wert, der über sie lief.
Probier es aus
Sag die Ausgabe voraus
Der Anteil der Ausgabe ist 0,4, das Gewicht zur Ausgabe ist 1,5, und das verborgene Neuron hat 0,5 geliefert. Was wird ausgegeben: die Steigung für dieses Gewicht und der Anteil des verborgenen Neurons?
var dOut = 0.4; // der Anteil der Ausgabevar v = 1.5;var h = 0.5;var dH = dOut * v * h * (1 - h);Console.WriteLine($"{dOut * h:F2} {dH:F2}");Die Steigung für das Gewicht ist dOut × h = 0,4 × 0,5 = 0,20. Der Anteil des verborgenen Neurons ist 0,4 × 1,5 × 0,5 × (1 − 0,5) = 0,15. Ohne den Faktor h·(1 − h), die Steigung der Sigmoid-Funktion, käme 0,30 heraus.
Probier es aus
Finde den Fehler
Ein Kollege schreibt backward selbst. Das Netz lernt, aber langsam und seltsam. Die Kontrolle mit dem Anstupsen zeigt, dass die Steigungen der Ausgabe stimmen, die verborgenen aber 7- bis 10-mal zu groß sind. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
Dem Anteil des verborgenen Neurons fehlt die Steigung seiner Sigmoid-Funktion, h[j]·(1 − h[j]). Der Kette fehlt ein Glied. Hier ist h·(1 − h) etwa 0,1 bis 0,15, also kommen die Steigungen 7- bis 10-mal zu groß heraus. Das Netz lernt trotzdem, weil die Richtung ungefähr stimmt, und deshalb lebt ein solcher Fehler lange. Die Kontrolle mit dem Anstupsen fängt ihn sofort.
Sieh es dir an
Ein Lernschritt
Die Steigungen geben die Richtung. Der Schritt ist der aus dem Gradientenabstieg: Jede Zahl bewegt sich gegen ihre Steigung, mal die Lernrate. Die Steigung ist der Mittelwert über die vier Zahlungen, denn das Netz muss für alle richtig sein:
static double MeanLoss(Net net, double[][] cases, double[] labels){ var s = 0.0; for (var n = 0; n < cases.Length; n++) { s += Loss(net, cases[n], labels[n]); } return s / cases.Length;}double[][] cases = [[0, 0], [1, 0], [0, 1], [1, 1]];double[] labels = [0, 1, 1, 0];// die mittlere Steigung über die vier Zahlungenvar mean = Net.Zeros();for (var n = 0; n < 4; n++){ var g = Backward(net, cases[n], labels[n]); for (var k = 0; k < 9; k++) { Set(mean, k, Get(mean, k) + Get(g, k) / 4); }}var rate = 2.0; // die Lernratevar before = MeanLoss(net, cases, labels);for (var k = 0; k < 9; k++){ var next = Get(net, k) - rate * Get(mean, k); Console.WriteLine($"{names[k],-8}{Get(net, k),8:F4} → {next:F4}"); Set(net, k, next);}var after = MeanLoss(net, cases, labels);Console.WriteLine($"mittlerer Fehler: {before:F4} → {after:F4}");w[0][0] 1.5000 → 1.5147w[0][1] 1.0000 → 1.0210w[1][0] 2.5000 → 2.4863w[1][1] 2.0000 → 1.9926b[0] -0.5000 → -0.4947b[1] -3.0000 → -3.0367v[0] 1.5000 → 1.4987v[1] -1.5000 → -1.5266c -0.3000 → -0.3285mittlerer Fehler: 0.2236 → 0.2218Der mittlere Fehler sinkt von 0,2236 auf 0,2218. Ein bisschen, aber in die richtige Richtung. Training ist genau dieser Schritt, tausendfach wiederholt: Vorwärtsdurchlauf, Rückwärtsdurchlauf, Schritt. Die ganze Schleife und wie der Fehler fällt, zeigt die nächste Lektion.
Probier es aus
Ergänze
Wie lautet der Schritt für jede Zahl des Netzes?
for (var k = 0; k < 9; k++)
{
Set(net, k, Get(net, k) rate * Get(g, k));
}Minus: Eine positive Steigung bedeutet, dass der Fehler mit dem Gewicht wächst, also geht das Gewicht nach unten. Mit einem Plus klettert das Netz bergauf, und der Fehler wächst mit jedem Schritt.
Die Idee
Wann Backprop schadet
- Die Steigung verschwindet. Die Steigung der Sigmoid-Funktion p·(1 − p) ist höchstens 0,25. Durch zehn Schichten mit einer Sigmoid-Funktion ergeben die Faktoren höchstens 0,25¹⁰, etwa 0,000001. Die ersten Schichten lernen kaum. Deshalb benutzen verborgene Schichten heute ReLU: Ihre Steigung ist 1 für jedes positive z.
- Die Steigung explodiert. Sind die Gewichte groß, wachsen die Produkte durch die Schichten, und der Schritt wirft die Gewichte weit weg. Eine kleinere Lernrate und Normalisierung helfen.
- Ein Fehler in den Steigungen stürzt nicht ab, er lernt schlecht. Das Netz bewegt sich trotzdem, nur langsamer oder zu etwas Schlechterem. Deshalb wird dein eigenes Backprop zuerst mit dem Anstupsen verglichen, wie im Programm mit den neun Gewichten.
- Das Anstupsen ist nur eine Kontrolle. Es ist genau, braucht aber für jedes Gewicht zwei Durchläufe. Zum Lernen ist es viel zu langsam.
- Die Steigung sagt nur „wo ist hier unten“. Der Schritt kann zu groß sein und über das Ziel hinausschießen, und ein kleiner ist langsam. Das ist die Lernrate aus dem Gradientenabstieg.
Genauer, für Neugierige
Die Formel. Mit L = (p − y)² und p = σ(z):
∂L/∂v₀ = ∂L/∂p · ∂p/∂z · ∂z/∂v₀ = 2(p − y) · p(1 − p) · h₀
∂ liest sich „partielle Ableitung“: wie stark sich die linke Seite bewegt, wenn du nur die rechte bewegst und alles andere stehen bleibt. Für die verborgene Schicht ist die Kette zwei Glieder länger: ∂z/∂h₀ = v₀ und ∂h₀/∂z₀ = h₀(1 − h₀), dann ∂z₀/∂w₀ᵢ = xᵢ.
Warum die Steigung der Sigmoid-Funktion p(1 − p) ist. Wenn σ(z) = 1 / (1 + e^(−z)), ist ihre Ableitung σ(z)·(1 − σ(z)). Das ist praktisch: Der Wert p ist im Vorwärtsdurchlauf schon berechnet, und die Steigung kommt ohne neues exp.
Das Anstupsen genauer. Das Programm bewegt das Gewicht nach oben und nach unten (ein zentraler Differenzenquotient). Der Fehler dieser Schätzung liegt in der Größenordnung h², bei h = 0,0001 etwa 0,00000001. Bewegst du nur nach oben, liegt der Fehler in der Größenordnung h, und die Zahlen stimmen auf weniger Nachkommastellen überein.
In echten Bibliotheken. Niemand schreibt backward für jedes Netz von Hand. PyTorch und Ähnliche zeichnen jede Operation des Vorwärtsdurchlaufs auf und gehen dann selbst rückwärts an ihr entlang (automatisches Differenzieren): In PyTorch ist das loss.backward(). Die Regel darin ist dieselbe Kettenregel.
Große Netze trainieren (mit Grafikkarten und vielen Maschinen) und in Betrieb nehmen ist Infrastruktur. Das gehört in den Kurs ZeroToOps.
Übung
Backprop für ein Neuron
Löse es in deinem Editor.
Die Zahlung aus der Neuron-Lektion: 230 €, ein neues Konto, eine ausländische Karte, Gewichte 0,9, 1,4 und 1,8 und ein Bias von −4. Die Zahlung stellte sich als Betrug heraus: y = 1. Das Neuron sagte 0,7807. Berechne mit der Kettenregel die Steigung des Fehlers (p − y)² für jedes Gewicht und für den Bias: dz = 2·(p − y)·p·(1 − p), die Steigung für w[i] ist dz × x[i], und für b ist sie dz. Kontrolliere die Gewichte durch Anstupsen (± 0,0001).
p = 0.7807, Fehler = 0.0481w[0]: backprop -0.1727, Anstupsen -0.1727w[1]: backprop -0.0751, Anstupsen -0.0751w[2]: backprop -0.0751, Anstupsen -0.0751b: backprop -0.0751Die Steigungen sind negativ: Größere Gewichte ergeben ein größeres p, näher an 1, und der Fehler sinkt. Das Gewicht des Betrags bekommt den größten Anteil am Fehler, weil der Betrag die größte Eingabe ist (2,3).
Weitere Aufgaben
- Ein Schritt. Mache einen Schritt mit einer Lernrate von 0,5 und gib das neue p aus. Es sollte näher an 1 liegen.
- Eine andere Zahlung. Mach dasselbe für die Zahlung über 40 €, die in Ordnung ist (y = 0). Welche Steigungen werden null, und warum?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: Gradientenabstieg · Klassifikation: Betrug oder nicht · Das Neuron · Schichten
- Geht weiter in: Das Training von Anfang bis Ende · Die Ziffern auf dem Kassenbon
- Aufgaben zum Thema: Ein neuronales Netz von innen