Gradientenabstieg
Kurz gesagt: Der Gradientenabstieg findet die Gewichte eines Modells Schritt für Schritt: Er berechnet, in welche Richtung der Fehler wächst, und verschiebt die Gewichte ein Stück in die andere Richtung, so weit es die Lernrate sagt.
Problem
Die beste Gerade, ohne Raten
Die Kollegen in der Lektion über lineare Regression haben eine Gerade nach Augenmaß gewählt. Jetzt soll das Programm die beste selbst finden. Eine genaue Formel gibt es nur für die Gerade, und die nächsten Modelle (neuronale Netze) haben keine. Wir brauchen einen Weg, der immer funktioniert.
Die Aufgabe ist vorerst kleiner: Das Lager ist bekannt (6 Stunden), und wir suchen nur, wie viele Stunden je 100 km dazukommen, also w. Die Entfernung steht in Hundert km, damit die Zahlen klein bleiben:
// Entfernung in Hundert km (0.3 sind 30 km) und Stunden bis zur Lieferungdouble[] dist = [0.3, 0.5, 0.8, 1.0, 1.3, 1.6, 2.0, 2.4, 2.7, 3.0];double[] hours = [10, 11, 17, 17, 23, 24, 31, 33, 40, 44];const double B = 6; // das Lager: 6 Stunden, das ist bekanntconst double Nudge = 0.0001;Der Verlust ist derselbe MSE wie in der linearen Regression, jetzt nur als Funktion von w:
// der MSE der Geraden Stunden = w · Entfernung + 6double Loss(double w){ double sum = 0; for (var i = 0; i < dist.Length; i++) { var error = w * dist[i] + B - hours[i]; sum += error * error; } return sum / dist.Length;}Wir könnten w = 0, 1, 2, … bis 100 probieren und den kleinsten Verlust nehmen. Bei einem w klappt das. Bei 10 Gewichten sind das 100¹⁰ Versuche, und ein neuronales Netz hat Millionen Gewichte.
Die Idee
Anstupsen und schauen
Statt alles auszuprobieren, stellst du dich auf ein w und fragst: Wenn ich ein Stück nach rechts gehe, sinkt der Verlust oder wächst er?
var before = Loss(0);var after = Loss(0.01);Console.WriteLine($"w = 0: Verlust {before:F2}");Console.WriteLine($"w = 0.01: Verlust {after:F2}");Console.WriteLine($"Steigung: {(after - before) / 0.01:F2}");w = 0: Verlust 485.00w = 0.01: Verlust 484.21Steigung: -78.97Der Stups um 0,01 hat den Verlust um 0,79 gesenkt. Geteilt durch den Stups ist das die Steigung: −78,97, wie stark sich der Verlust pro Einheit w ändert. Das Minus heißt „rechts geht es bergab“.
Mit einem kleineren Stups (0,0001) ist die Steigung genauer. Hier an vier Stellen:
// die Steigung: stups w an und sieh, wie stark sich der Verlust ändertdouble Slope(double w) => (Loss(w + Nudge) - Loss(w)) / Nudge;foreach (double w in new double[] { 0, 6, 12, 18 }){ Console.WriteLine($"w = {w}: Verlust {Loss(w):F2}, Steigung {Slope(w):F2}");}w = 0: Verlust 485.00, Steigung -79.00w = 6: Verlust 127.21, Steigung -40.26w = 12: Verlust 1.83, Steigung -1.53w = 18: Verlust 108.87, Steigung 37.21Die Steigung sagt zwei Dinge. Das Vorzeichen ist die Richtung: Minus heißt, w muss wachsen, Plus heißt, w muss schrumpfen (bei w = 18 sind wir schon am Boden vorbei). Die Größe sagt, wie weit wir sind: −79 ist weit vom Boden, −1,53 ist fast am Boden.
Die Idee
Ein Schritt gegen die Steigung
Die Regel ist eine Zeile: w -= Lernrate · Steigung. Gegen die Steigung, weil die Steigung bergauf zeigt. Bei negativer Steigung wird w größer, bei positiver kleiner. Je steiler, desto größer der Schritt.
Ein Durchgang (Steigung berechnen, einen Schritt machen) ist eine Epoche. Wir wiederholen:
double w = 0;var rate = 0.1;for (var epoch = 1; epoch <= 10; epoch++){ w -= rate * Slope(w); Console.WriteLine($"Epoche {epoch}: w = {w:F2}, Verlust {Loss(w):F2}");}Epoche 1: w = 7.90, Verlust 62.36Epoche 2: w = 10.70, Verlust 9.28Epoche 3: w = 11.69, Verlust 2.61Epoche 4: w = 12.04, Verlust 1.77Epoche 5: w = 12.17, Verlust 1.67Epoche 6: w = 12.21, Verlust 1.65Epoche 7: w = 12.23, Verlust 1.65Epoche 8: w = 12.23, Verlust 1.65Epoche 9: w = 12.24, Verlust 1.65Epoche 10: w = 12.24, Verlust 1.65Der erste Schritt ist groß (von 0 auf 7,90), weil die Kurve steil ist. Zum Boden hin wird die Steigung kleiner, und mit ihr die Schritte: Ab Epoche 7 bewegt sich w kaum noch. Der Verlust von 1,65 ist kleiner als die 1,83 der Geraden, die die Kollegen nach Augenmaß gewählt haben.
Das ist der Gradientenabstieg: Schritt für Schritt die Steigung des Verlusts hinunter, bis du am Boden bist. „Gradient“ ist die Steigung, wenn es mehr als einen Parameter gibt.
Sieh es dir an
Die Kugel auf der Kurve
double Slope(double w) => (Loss(w + Nudge) - Loss(w)) / Nudge;
double w = 0;var rate = 0.1;for (var epoch = 1; epoch <= 10; epoch++){ w -= rate * Slope(w);}double Slope(double w) => (Loss(w + Nudge) - Loss(w)) / Nudge;
double w = 0;var rate = 0.1;for (var epoch = 1; epoch <= 10; epoch++){ w -= rate * Slope(w);}double Slope(double w) => (Loss(w + Nudge) - Loss(w)) / Nudge;
double w = 0;var rate = 0.01;for (var epoch = 1; epoch <= 10; epoch++){ w -= rate * Slope(w);}double Slope(double w) => (Loss(w + Nudge) - Loss(w)) / Nudge;
double w = 0;var rate = 0.35;for (var epoch = 1; epoch <= 10; epoch++){ w -= rate * Slope(w);}Schritt für Schritt, in Worten
- Unten: der Verlust für jedes w; die Kugel startet bei w = 0. Oben: die Gerade mit diesem w.
- Stups w ein Stück an: Der Verlust sinkt. Die Steigung ist -79,00: Bergab geht es nach rechts.
- Ein Schritt: w -= 0,1 · (-79,00), also wird w zu 7,90. Gegen die Steigung, also bergab.
- Jeder Durchgang ist eine Epoche. Zum Boden hin wird die Steigung kleiner, und mit ihr die Schritte. Nach 10: w = 12,24.
- Lernrate 0,01: Die graue Kugel kriecht. Nach 10 Epochen steht sie erst bei w = 5,96.
- Lernrate 0,35: Die rote Kugel springt über den Boden, jedes Mal höher. Der Verlust wächst: Sie divergiert.
- Mit 0,1 liegt die Kugel am Boden: w = 12,24, Verlust 1,65. Niemand hat w geraten: Es wurde gefunden.
Unten steht der Verlust für jedes w, und die Kugel ist das aktuelle w. Oben ist die Gerade mit diesem w über den Bestellungen: Wenn die Kugel den Boden erreicht, geht die Gerade durch die Punkte. Die graue und die rote Kugel sind dieselbe Regel mit einer anderen Lernrate.
Die Idee
Die Lernrate
Die Zahl, mit der die Steigung multipliziert wird, ist die Lernrate. Sie bestimmt, wie lang der Schritt ist. Drei Lernraten, je 20 Epochen (w nach den ersten drei und nach der zwanzigsten):
foreach (var rate in new[] { 0.01, 0.1, 0.35 }){ double w = 0; var seen = new List<string>(); for (var epoch = 1; epoch <= 20; epoch++) { w -= rate * Slope(w); if (epoch <= 3 || epoch == 20) seen.Add($"{w:F2}"); } Console.WriteLine($"Lernrate {rate:F2}: w = {string.Join(", ", seen)}, Verlust {Loss(w):F2}");}Lernrate 0.01: w = 0.79, 1.53, 2.22, 9.02, Verlust 35.14Lernrate 0.10: w = 7.90, 10.70, 11.69, 12.24, Verlust 1.65Lernrate 0.35: w = 27.65, -7.18, 36.69, -1224.61, Verlust 4938139.70- 0,01 ist zu klein. Die Richtung stimmt, aber nach 20 Epochen ist w erst 9,02. Es erreicht den Boden, aber nach Hunderten Epochen.
- 0,1 ist richtig. Drei Schritte, und w ist nah dran, dann pendelt es sich bei 12,24 ein.
- 0,35 ist zu groß. Der erste Schritt schießt über den Boden hinaus auf 27,65. Dort ist die Steigung noch größer, der nächste Schritt ist noch länger, und w fliegt auf −7,18, dann auf 36,69. Der Verlust wird zu Millionen: Der Abstieg divergiert.
Eine gute Lernrate wird nicht berechnet, sondern ausprobiert. Üblich ist, mit 0,1 oder 0,01 anzufangen und den Verlust pro Epoche zu beobachten: Wenn er wächst oder springt, ist die Lernrate zu groß; wenn er kaum sinkt, ist sie zu klein.
Die Idee
w und b zusammen
Bisher war b bekannt. Jetzt suchen wir es auch: Jeder Parameter hat seine eigene Steigung (du stupst nur ihn an), und jeder macht in derselben Epoche seinen eigenen Schritt.
// der MSE der Geraden Stunden = w · Entfernung + bdouble Loss(double w, double b){ double sum = 0; for (var i = 0; i < dist.Length; i++) { var error = w * dist[i] + b - hours[i]; sum += error * error; } return sum / dist.Length;}double w = 0, b = 0, rate = 0.1;for (var epoch = 1; epoch <= 1000; epoch++){ var slopeW = (Loss(w + Nudge, b) - Loss(w, b)) / Nudge; var slopeB = (Loss(w, b + Nudge) - Loss(w, b)) / Nudge; w -= rate * slopeW; b -= rate * slopeB; if (epoch == 1 || epoch == 10 || epoch == 100 || epoch == 1000) Console.WriteLine($"Epoche {epoch}: w = {w:F2}, b = {b:F2}, Verlust {Loss(w, b):F2}");}Epoche 1: w = 9.77, b = 5.00, Verlust 29.77Epoche 10: w = 12.21, b = 6.03, Verlust 1.66Epoche 100: w = 12.41, b = 5.65, Verlust 1.62Epoche 1000: w = 12.41, b = 5.64, Verlust 1.62Nach 1.000 Epochen ist w = 12,41 Stunden pro 100 km und b = 5,64 Stunden. Die Formel für die beste Gerade aus der linearen Regression ergibt 0,1241 Stunden pro km und 5,64 Stunden: dieselbe Gerade. Der Abstieg kennt die Formel nicht; er geht einfach bergab.
Die Idee
Die Skala der Merkmale
Warum stand die Entfernung in Hundert km? Hier sind dieselben Bestellungen in km:
// dieselben Bestellungen, aber die Entfernung ist in kmdouble[] km = [30, 50, 80, 100, 130, 160, 200, 240, 270, 300];double[] hours = [10, 11, 17, 17, 23, 24, 31, 33, 40, 44];const double Nudge = 0.0001;Bei einer Lernrate von 0,1 divergiert der Abstieg schon in der ersten Epoche:
double w = 0, b = 0, rate = 0.1;for (var epoch = 1; epoch <= 3; epoch++){ var slopeW = (Loss(w + Nudge, b) - Loss(w, b)) / Nudge; var slopeB = (Loss(w, b + Nudge) - Loss(w, b)) / Nudge; w -= rate * slopeW; b -= rate * slopeB; Console.WriteLine($"Lernrate 0.1, Epoche {epoch}: w = {w:F0}");}Lernrate 0.1, Epoche 1: w = 977Lernrate 0.1, Epoche 2: w = -6304921Lernrate 0.1, Epoche 3: w = 40699231079Die Steigung für w wächst mit der Größe des Merkmals: Jedes w wird mit 300 statt mit 3 multipliziert, und bei w = 0 ist die Steigung fast −10.000. Also senken wir die Lernrate, bis es nicht mehr divergiert:
double w = 0, b = 0, rate = 0.00002;for (var epoch = 1; epoch <= 1000; epoch++){ var slopeW = (Loss(w + Nudge, b) - Loss(w, b)) / Nudge; var slopeB = (Loss(w, b + Nudge) - Loss(w, b)) / Nudge; w -= rate * slopeW; b -= rate * slopeB; if (epoch == 10 || epoch == 100 || epoch == 1000) Console.WriteLine($"Epoche {epoch}: w = {w:F4}, b = {b:F4}, Verlust {Loss(w, b):F2}");}Epoche 10: w = 0.1513, b = 0.0013, Verlust 9.44Epoche 100: w = 0.1513, b = 0.0063, Verlust 9.42Epoche 1000: w = 0.1510, b = 0.0563, Verlust 9.28Jetzt divergiert es nicht mehr, aber b bewegt sich kaum: Nach 1.000 Epochen ist es 0,06 statt 5,64, und w steht bei 0,151 statt bei 0,124. Für b ist der Schritt zu klein. Eine Lernrate passt nicht für beide. Die Lösung ist die Skala: Die Merkmale werden vor dem Training auf einen ähnlichen Bereich gebracht (hier durch 100 geteilt; oft zieht man den Mittelwert ab und teilt durch die Streuung). Ohne das schafft keine Lernrate die Aufgabe.
Probier es aus
Sag die Ausgabe voraus
w ist 2, die Steigung dort ist −20, und die Lernrate ist 0,1. Was gibt ein Schritt aus?
double w = 2;double slope = -20;var rate = 0.1;w -= rate * slope;Console.WriteLine($"{w:F1}");0,1 · (−20) = −2, und w -= −2 addiert 2: w wird 4.0. Eine negative Steigung heißt, w wächst. 0.0 wäre ein Schritt mit der Steigung (bergauf), und −18.0 wäre das Abziehen der Steigung ohne die Lernrate.
Probier es aus
Ergänze
Welche Operation macht den Schritt gegen die Steigung?
for (var epoch = 1; epoch <= 10; epoch++)
{
w rate * Slope(w);
}-=: Wir ziehen die Lernrate mal die Steigung ab. Die Steigung zeigt dorthin, wo der Verlust wächst, und wir wollen das Gegenteil.
Probier es aus
Finde den Fehler
Ein Kollege schreibt einen Abstieg, aber der Verlust pro Epoche wächst immer schneller. In welcher Zeile steckt der Fehler?
Wähle die Zeile mit dem Fehler.
+= macht einen Schritt mit der Steigung, also bergauf auf der Kurve. Es muss -= sein: gegen die Steigung.
Die Idee
Wenn der Abstieg wehtut
- Die falsche Lernrate. Zu groß divergiert, zu klein kriecht. Wenn der Verlust pro Epoche wächst oder springt, senke zuerst die Lernrate.
- Merkmale auf verschiedenen Skalen. km und die Zahl der Pakete in einer Zeile: Die Steigungen unterscheiden sich um das Hundertfache, und keine Lernrate passt. Skaliere die Merkmale vor dem Training.
- Nicht nur ein Tal. Der Verlust einer Geraden ist eine glatte Schüssel mit einem Boden. Bei neuronalen Netzen hat die Kurve viele Täler und Plateaus: Der Abstieg erreicht irgendeinen Boden, nicht unbedingt den tiefsten, und das Ergebnis hängt vom Start-w ab.
- Wann man aufhört. Hier sind die Epochen fest. In der Praxis hörst du auf, wenn der Verlust nicht mehr sinkt, und ob das Modell für neue Bestellungen gut ist, misst man getrennt (das Modell auswerten).
- Die Kosten. Jede Epoche geht durch alle Daten. Bei Millionen Beispielen und Millionen Gewichten läuft das Training Stunden auf speziellen Servern; diese Infrastruktur (Maschinen für das Training, das Modell in Produktion betreiben) wird im DevOps-Kurs ZeroToOps gelehrt.
Genauer, für Neugierige
Die Steigung per Formel. Das Anstupsen ist eine Näherung. Für den MSE hat die Steigung eine exakte Formel (die Ableitung): 2 · Mittelwert von Fehler · Entfernung. Hier beide Wege nebeneinander:
// die Steigung per Formel: 2 · Mittelwert von Fehler · Entfernungdouble SlopeByFormula(double w){ double sum = 0; for (var i = 0; i < dist.Length; i++) { var error = w * dist[i] + B - hours[i]; sum += error * dist[i]; } return 2 * sum / dist.Length;}foreach (double w in new double[] { 0, 12 }){ Console.WriteLine($"bei w = {w}: mit Zahlen {Slope(w):F2}, per Formel {SlopeByFormula(w):F2}");}bei w = 0: mit Zahlen -79.00, per Formel -79.00bei w = 12: mit Zahlen -1.53, per Formel -1.53Sie stimmen auf die zweite Stelle überein. Die Formel ist schneller (ein Durchgang statt zwei pro Parameter), deshalb berechnen Bibliotheken Ableitungen automatisch aus dem Code des Modells. Das ist Backpropagation, aus der Lektion über das Training eines Netzes.
Der Gradient. Bei vielen Parametern sind ihre Steigungen zusammen der Gradient (ein Vektor). Der Schritt ist für jeden derselbe: Parameter -= Lernrate · seine Steigung.
Teile der Daten. Bei Millionen Beispielen ist eine Epoche langsam. Deshalb wird die Steigung auf einem kleinen zufälligen Teil berechnet (einem Mini-Batch), und es gibt viel mehr Schritte. Das ist der stochastische Gradientenabstieg (SGD). Seine verbesserten Versionen (zum Beispiel Adam) behalten außerdem einen „Schwung“ der vorherigen Schritte.
Übung
Der Kurier, selbst gelernt
Löse die Aufgabe in deinem Editor.
In der Lektion über lineare Regression haben die Kollegen drei Geraden für den Preis des Kuriers verglichen. Jetzt soll das Programm die beste selbst finden:
// Pakete: Gewicht in kg und Preis des Kuriers in €double[] kg = [0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0, 6.0];double[] eur = [4.10, 4.60, 5.00, 5.60, 6.40, 7.50, 8.30, 9.40];const double Nudge = 0.0001;Schreibe die Verlustfunktion (MSE) für Preis = w · kg + b, dann 1.000 Epochen Gradientenabstieg für w und b zusammen, ab w = 0 und b = 0, mit einer Lernrate von 0,05 und einem Stups von 0,0001. Gib w, b und den Verlust mit je 4 Stellen aus.
w = 0.9538, b = 3.6204, MSE 0.0038Weitere Aufgaben
- Die Lernrate. Lass es mit einer Lernrate von 0,1 laufen. Was passiert? Finde die größte Lernrate, bei der der Verlust noch sinkt.
- Aufhören. Statt 1.000 Epochen hörst du auf, wenn beide Steigungen betragsmäßig kleiner als 0,001 sind. Nach wie vielen Epochen?
- Vergleich. Um wie viel ist der Verlust kleiner als bei der besten Geraden der Kollegen (A, MSE 0,0144)?
Test
Teste dich
Verwandte Lektionen
Abschnitt „Verwandte Lektionen“- Baut auf: static: was alle Objekte teilen · Listen · Was maschinelles Lernen ist · Lineare Regression
- Geht weiter in: Klassifikation: Betrug oder nicht · Auswertung: Die Genauigkeit lügt · Überanpassung: Das Modell, das auswendig lernt · Backpropagation
- Aufgaben zum Thema: Wie ein Modell lernt