Читать книгу Machine Learning für Softwareentwickler - Paolo Perrotta - Страница 12
Оглавление3
Am Gradienten entlang
Im letzten Kapitel haben wir bereits etwas geschafft, auf das wir stolz sein können: Wir haben Code geschrieben, der lernen kann. Sollten wir diesen Code jedoch von einem Informatiker überprüfen lassen, so würde dieser ihn als mangelhaft einstufen. Insbesondere beim Anblick der Funktion train() würde er den Kopf schütteln. »Für diese einfache Aufgabe mag der Code in Ordnung sein«, mag der gestrenge Informatiker sagen, »aber er lässt sich nicht auf reale Probleme skalieren.«
Womit er recht hätte. In diesem Kapitel werden wir zwei verschiedene Dinge tun, damit es nicht zu einer solchen Kritik kommt. Erstens verzichten wir darauf, den Code einem Informatiker zur Überprüfung vorzulegen. Zweitens schauen wir uns die Probleme der jetzigen Implementierung von train() genauer an und lösen sie mit einem der grundlegenden Prinzipien des Machine Learnings, nämlich einem Algorithmus, der als Gradientenverfahren, Gradientenabstieg oder Verfahren des steilsten Abstiegs bekannt ist. Ebenso wie unser bisheriger Code von train() dient dieses Verfahren dazu, das Minimum der Verlustfunktion zu finden, allerdings schneller, genauer und allgemeiner als mit dem Code aus dem vorherigen Kapitel.
Der Gradientenabstieg ist nicht nur für unser kleines Programm nützlich. Ohne dieses Verfahren werden Sie beim Machine Learning nicht weit kommen. Dieser Algorithmus wird uns in verschiedener Form das ganze Buch hindurch begleiten.
Schauen wir uns als Erstes das Problem an, das wir mit dem Gradientenverfahren lösen wollen.
Unser Algorithmus bringt es nicht
Unser Programm kann erfolgreich Pizzaverkaufszahlen vorhersagen. Aber warum sollten wir es dabei belassen? Vielleicht können wir mit demselben Code ja auch andere Dinge vorhersagen, etwa Bewegungen auf dem Aktienmarkt. Damit könnten wir über Nacht reich werden! (Um Ihnen die Enttäuschung zu ersparen: Nein, das funktioniert nicht.)
Allerdings stoßen wir schon bald auf ein Problem, wenn wir versuchen, unser Programm zur linearen Regression auf eine andere Aufgabe anzuwenden. Unser Code verwendet ein einfaches Geradenmodell mit zwei Parametern, nämlich dem Gewicht w und dem Bias b. In der Praxis sind jedoch meistens komplexe Modelle mit mehr Parametern erforderlich. Denken Sie zum Beispiel an das Ziel, das wir uns für Teil I dieses Buchs gesetzt haben, nämlich ein System zu konstruieren, das Bilder erkennen kann. Ein Bild ist viel komplizierter als eine einzelne Zahl, weshalb wir auch ein Modell mit weit mehr Parametern brauchen als für das Pizzaprogramm.
Wenn wir unserem Modell weitere Parameter hinzufügen, geht seine Leistung jedoch in den Keller. Betrachten Sie dazu noch einmal die Funktion train() aus dem letzten Kapitel:
def train(X, Y, iterations, lr):
w = b = 0
for i in range(iterations):
current_loss = loss(X, Y, w, b)
print("Iteration %4d => Loss: %.6f" % (i, current_loss))
if loss(X, Y, w + lr, b) < current_loss:
w += lr
elif loss(X, Y, w - lr, b) < current_loss:
w -= lr
elif loss(X, Y, w, b + lr) < current_loss:
b += lr
elif loss(X, Y, w, b - lr) < current_loss:
b -= lr
else:
return w, b
raise Exception("Couldn't converge within %d iterations" % iterations)
Bei jeder Iteration modifiziert dieser Algorithmus entweder w oder b und sucht nach den Werten, bei denen der Verlust möglichst gering ist. Das allerdings kann schiefgehen, denn wenn wir w optimieren, kann das den durch b verursachten Verlust erhöhen und umgekehrt. Um dieses Problem zu vermeiden und so nah wie möglich an den kleinstmöglichen Verlust heranzukommen, könnten wir beide Parameter gleichzeitig modifizieren. Je mehr Parameter wir haben, umso wichtiger wird das.
Um w und b gemeinsam zu optimieren, müssen wir alle möglichen Kombinationen ausprobieren: sowohl w als auch b vergrößern, w vergrößern und b verkleinern, w vergrößern und b unverändert lassen, w verkleinern und … usw. usf. Die Gesamtzahl möglicher Kombinationen einschließlich des Falls, bei dem alle Parameter unverändert bleiben, ist 3 hoch die Anzahl der Parameter. Bei zwei Parametern sind das 32 gleich 9 Kombinationen.
Es hört sich nicht weiter schlimm an, loss() pro Iteration neunmal aufzurufen. Bei zehn Parametern haben wir es allerdings schon mit 310 Kombinationen zu tun, also fast 60.000 Aufrufen pro Iteration. Eine Anzahl von zehn Parametern ist auch alles andere als übertrieben. Weiter hinten in diesem Buch verwenden wir Modelle mit Hunderttausenden von Parametern. Bei solch riesigen Modellen käme ein Algorithmus, der jede Parameterkombination ausprobiert, nicht mehr von der Stelle. Wir sollten uns daher lieber gleich von diesem langsamen Code verabschieden.
Außerdem weist train() in der jetzigen Form ein noch gewichtigeres Problem auf: Die Funktion ändert die Parameter in Schritten, die genauso groß sind wie die Lernrate. Bei großer lr ändern sich die Parameter schnell, was zwar den Trainingsvorgang beschleunigt, das Endergebnis aber weniger genau macht, da jeder Parameter jetzt ein Vielfaches des großen lr betragen muss. Um die Genauigkeit zu verbessern, brauchen wir eine kleine lr, die allerdings zu einem langsameren Training führt. Geschwindigkeit und Genauigkeit gehen jeweils auf Kosten des anderen, wobei wir jedoch beides brauchen.
Aus diesen Gründen ist unser bisheriger Code nichts weiter als ein Hack. Wir müssen ihn durch einen besseren Algorithmus ersetzen – einen, der train() sowohl schnell als auch genau macht.
Das Gradientenverfahren
Wir brauchen einen besseren Algorithmus für train(). Die Aufgabe dieser Funktion besteht darin, die Parameter zu finden, bei denen der Verlust minimal wird. Schauen wir uns also loss() genauer an:
def loss(X, Y, w, b):
return np.average((predict(X, w, b) - Y) ** 2)
Betrachten Sie die Argumente dieser Funktion. X und Y enthalten die Eingabevariablen und die Labels, ändern sich also nicht von einem Aufruf von loss() zum nächsten. Zur Vereinfachung wollen wir b vorübergehend auch konstant auf 0 setzen. Unsere einzige Variable ist damit w.
Wie ändert sich nun der Verlust mit der Veränderung von w? Ich habe ein kleines Programm geschrieben, das den Verlauf von loss() für w im Bereich von –1 bis 4 ausgibt und das Minimum mit einem grünen Kreuz markiert. Das sehen Sie in der folgenden Abbildung. (Den Code finden Sie wie üblich im Begleitmaterial zu diesem Buch.)
Eine hübsche Kurve – nennen wir sie die Verlustkurve. Sinn und Zweck von train() besteht darin, die markierte Stelle unten in dieser Kurve zu finden, also den Wert von w, der zu einem minimalen Verlust führt. Bei diesem w nähert sich unser Modell den Datenpunkten am besten an.
Stellen Sie sich diese Kurve als ein Tal vor, an dessen Hang irgendwo eine Wanderin steht, die zu ihrem Lager an der gekennzeichneten Stelle unterwegs ist. Allerdings ist es so dunkel, dass sie nur den Boden in unmittelbarer Nähe ihrer Füße sehen kann. Um das Lager zu finden, kann sie sich eines ganz einfachen Verfahrens bedienen: Sie geht immer in die Richtung des steilsten Abstiegs. Sofern es in dem Gelände keine Löcher oder Klippen gibt – was bei unserer Verlustfunktion nicht der Fall ist –, führt jeder Schritt die Wanderin näher an ihr Lager heran.
Um dieses Prinzip in Code umsetzen zu können, müssen wir die Steigung der Verlustkurve bestimmen. Ein Maß dafür ist der Gradient. Vereinbarungsgemäß ist der Gradient an einem gegebenen Punkt der Kurve ein »Pfeil«, der bergauf weist.
Um den Gradienten zu bestimmen, verwenden wir ein mathematisches Werkzeug: die Ableitung des Verlusts nach dem Gewicht, geschrieben ∂L/∂w. Formal ausgedrückt, bestimmt die Ableitung an einem gegebenen Punkt, wie stark sich L an diesem Punkt bei kleinen Abweichungen von w ändert. Was geschieht mit dem Verlust, wenn wir das Gewicht ein winziges bisschen erhöhen? In dem vorstehenden Diagramm ist die Ableitung negativ, da der Verlust abnimmt. Bei positiver Ableitung dagegen steigt der Verlust. Am Minimum der Kurve, also an dem mit dem Kreuz markierten Punkt, ist die Kurve eben und die Ableitung damit null.
Beachten Sie, dass unsere Wanderin in die dem Gradienten entgegengesetzte Richtung gehen muss, um das Minimum zu erreichen. An einem Punkt mit negativer Ableitung wie in dem Bild muss sie sich also in positiver Richtung bewegen. Ihre Schrittweite muss proportional zum Betrag der Ableitung sein. Ist die Ableitung betragsmäßig groß, verläuft die Kurve steil. Das Lager ist dann noch weit entfernt. Daher kann die Wanderin vertrauensvoll große Schritte machen. Wenn sie sich dem Lager nähert, wird die Ableitung jedoch kleiner und damit auch ihre Schrittweite.
Dieser Algorithmus ist das Gradientenverfahren oder Verfahren des steilsten Abstiegs. Zu seiner Implementierung ist ein bisschen Mathematik gefordert.
Ein wenig Mathematik
Als Erstes übersetzen wir unsere Formel für den mittleren quadratischen Fehler in die gute, alte mathematische Schreibweise:
Falls Ihnen diese Schreibweise nicht bekannt vorkommt: Das Symbol S ist das Summenzeichen, und das m steht für die Anzahl der Beispiele. Diese Formel bedeutet: »Summiere die quadrierten Fehler aller Beispiele von Beispiel 1 bis Beispiel m und dividiere das Ergebnis durch die Anzahl der Beispiele.«
Bei den verschiedenen x und y handelt es sich um die Eingabevariablen und Labels, also um Konstanten. Auch m ist konstant, da sich die Anzahl der Beispiele nicht ändert. Da wir b vorübergehend auf 0 gesetzt haben, ist auch dieser Wert konstant. Wir werden b in Kürze wieder benutzen, aber vorläufig ist w der einzige Wert in der Formel, der sich ändert.
Nun müssen wir Betrag und Richtung des Gradienten bestimmen, also die Ableitung von L nach w. Wenn Sie sich noch an die Analysis in der Oberstufe erinnern, können Sie die Ableitung selbst berechnen. Wenn nicht, ist das aber auch kein Beinbruch. Jemand anderes hat die Arbeit schon für uns erledigt:
Die Ableitung des Verlusts sieht ähnlich aus wie der Verlust selbst, allerdings ist die Quadrierung verloren gegangen. Außerdem wird jeder Summand mit x und das Endergebnis mit 2 multipliziert. In diese Formel können wir nun beliebige Werte für w eingeben und erhalten den Gradienten an diesem Punkt als Ergebnis.
Im Code sieht diese Formel wie folgt aus. Auch hier ist b wieder auf 0 fixiert.
03_gradient/gradient_descent_without_bias.py
def gradient(X, Y, w):
return 2 * np.average(X * (predict(X, w, 0) - Y))
Mit der Formel für den Gradienten können wir nun train() so umschreiben, dass die Funktion das Gradientenverfahren anwendet.
Abwärts
Mit den Änderungen für das Gradientenverfahren sieht train() wie folgt aus:
03_gradient/gradient_descent_without_bias.py
def train(X, Y, iterations, lr):
w = 0
for i in range(iterations):
print("Iteration %4d => Loss: %.10f" % (i, loss(X, Y, w, 0)))
w -= gradient(X, Y, w) * lr
return w
Diese Version von train() ist viel knapper als die vorherige. Bei Anwendung des Gradientenverfahrens brauchen wir keine if-Anweisungen mehr. Wir müssen lediglich w initialisieren und dann wiederholt in die dem Gradienten entgegengesetzte Richtung gehen (da der Gradient aufwärts zeigt, wir uns aber abwärts bewegen wollen). Der Hyperparameter lr ist immer noch da, gibt jetzt aber an, wie groß jeder einzelne Schritt im Verhältnis zum Gradienten sein soll.
Außerdem müssen wir entscheiden, wann wir aufhören wollen. Die alte Version von train() endete, wenn die Höchstzahl der Iterationen erreicht oder es nicht mehr möglich war, den Verlust weiter zu verringern. Beim Gradientenverfahren dagegen kann der Verlust theoretisch immer kleiner werden und sich in immer winzigeren Schritten dem Minimum nähern, ohne es jemals zu erreichen. Wann sollten wir diesen Vorgang abbrechen?
Wir können aufhören, wenn der Gradient ausreichend klein geworden ist, da das bedeutet, dass wir dem Minimum schon sehr nahe gekommen sind. Der vorstehende Code dagegen verfolgt einen weniger raffinierten Ansatz: Wenn Sie train() aufrufen, geben Sie an, wie viele Iterationen die Funktion durchlaufen soll. Mehr Iterationen bedeuten einen geringeren Verlust, aber da der Verlust in immer geringem Maße sinkt, ist irgendwann ein Punkt erreicht, an dem eine größere Präzision den Aufwand nicht mehr lohnt.
Weiter hinten in diesem Buch (in Kapitel 15, »Entwicklung«) erfahren Sie, wie Sie sinnvolle Werte für Hyperparameter wie iterations und lr auswählen. Vorläufig habe ich einfach verschiedene Werte ausprobiert und mich für diejenigen entschieden, die zu einem ausreichend geringen Verlust und genügender Genauigkeit führten:
X, Y = np.loadtxt("pizza.txt", skiprows=1, unpack=True)
w = train(X, Y, iterations=100, lr=0.001)
print("\nw=%.10f" % w)
Bei der Ausführung dieses Codes ergibt sich Folgendes:
Iteration 0 => Loss: 812.8666666667
Iteration 1 => Loss: 304.3630879787
Iteration 2 => Loss: 143.5265791020
...
Iteration 98 => Loss: 69.1209628275
Iteration 99 => Loss: 69.1209628275
w=1.8436928702
Wie beabsichtigt wird der Verlust mit jedem Durchlauf geringer. Nach 100 Iterationen kommt das Verfahren dem Minimum so nahe, dass wir schon keinen Unterschied mehr zwischen den beiden letzten Verlustwerten erkennen können. Unser Algorithmus scheint seine Aufgabe wie erwartet zu erfüllen.
Aber halt – wir haben ja nur den Parameter w verwendet! Was passiert, wenn wir b wieder mit ins Spiel nehmen?
Die dritte Dimension
Sehen wir uns die Verlustfunktion noch einmal in der mathematischen Schreibweise an:
Bis jetzt haben wir bis auf w alle Werte in dieser Formel als Konstanten behandelt. Vor allem haben wir b auf 0 festgelegt. Wenn wir b wieder zu einer Variablen machen, dann ist der Verlust keine zweidimensionale Kurve mehr, sondern eine Oberfläche:
Jetzt lebt unsere Wanderin nicht mehr im Plattland, sondern kann sich in drei Dimensionen bewegen. Die Werte von w und b bilden die beiden horizontalen Achsen und die Werte des Verlusts die vertikale. Anders ausgedrückt steht jeder Punkt auf dieser Oberfläche für den Fehler in einer Geraden unseres Modells. Wir wollen nun die Gerade mit dem geringsten Fehler finden: den mit dem Kreuz markierten Punkt.
Auch wenn der Verlust jetzt eine Oberfläche ist, können wir das Minimum immer noch mit dem Gradientenverfahren erreichen, wobei wir allerdings den Gradienten einer Funktion mit mehreren Variablen berechnen müssen. Das können wir mit der Technik der partiellen Ableitung erreichen.
Partielle Ableitung
Was ist eine partielle Ableitung, und wie kann sie uns weiterhelfen? Stellen Sie sich vor, dass Sie die Funktion mit einem Samuraischwert aufschlitzen und dann die Ableitung des Schlitzes berechnen. (Es muss kein Samuraischwert sein, aber damit sieht es besonders cool aus.) Beispielsweise haben wir bisher b auf 0 festgelegt und den Verlust damit wie folgt aufgeschlitzt:
Dieser Schlitz ist genau die Verlustkurve, die wir bei der ersten Anwendung des Gradientenverfahrens behandelt haben. Sie sieht hier ein bisschen flachgedrückt aus, da ich andere Intervalle und Maßstäbe für die Achsen verwendet habe, aber es ist genau dieselbe Funktion. Für jeden Wert von b gibt es eine solche Kurve, deren einzige Variable w ist. Ebenso gibt es aber auch für jeden Wert von w eine Kurve mit der Variablen b. Für w = 0 sieht sie wie folgt aus:
Für jeden dieser eindimensionalen »Schlitze« können wir den Gradienten berechnen, wie wir es bei unserer ursprünglichen Kurve getan haben. Das Schöne daran ist, dass wir die Gradienten dieser Schlitze kombinieren können, um die Gradienten der Oberfläche zu erhalten, wie die folgende Abbildung zeigt:
Mithilfe von partiellen Ableitungen können wir unser Problem mit zwei Variablen in zwei Probleme mit einer Variablen aufteilen. Wir brauchen also keinen neuen Algorithmus, um das Gradientenverfahren auf einer Oberfläche anzuwenden, sondern können diese Oberfläche einfach mithilfe partieller Ableitungen aufteilen und auf jedem Teil unser bisheriges Verfahren nutzen.
Mathematischer Hintergrund: Ableitungen und Analysis
Der Zweig der Mathematik, der sich mit Gradienten, Ableitungen und partiellen Ableitungen beschäftigt, ist die Analysis. Wenn Sie sich intensiver damit beschäftigen wollen, werfen Sie einen Blick auf die Khan Academy.1 Auch zu diesem Thema finden Sie dort weit mehr Informationen, als Sie zum Verständnis dieses Buchs benötigen.
Um die partiellen Ableitungen konkret zu berechnen, leiten Sie die Funktion jeweils nach einer der Variablen ab (in unserem Fall also w oder b) und tun dabei so, als ob dies die einzige Variable in der Funktion und jeder andere Wert konstant wäre. Die Hälfte dieser Arbeit haben wir bereits erledigt, als wir b auf 0 gesetzt und L nach w abgeleitet haben:
Jetzt müssen wir das Gleiche noch einmal tun, dabei aber w als Konstante betrachten und L nach b ableiten. Wenn Sie mit Analysis vertraut sind, können Sie sich selbst daran versuchen. Ansonsten finden Sie das Ergebnis hier:
Wie funktioniert das Gradientenverfahren nun auf einer zweidimensionalen Verlustfläche? Unsere Wanderin steht an einem Punkt mit einem gegebenen Wert von w und b und hat die Formeln für die Berechnung der partiellen Ableitung von L nach w und b. Sie gibt die aktuellen Werte von w und b in diese Formeln ein und erhält dadurch einen Gradienten für jede Variable. Anschließend wendet sie das Gradientenverfahren auf beide Gradienten an – und schon kann sie dem Gradienten der Oberfläche folgend absteigen.
Das reicht erst einmal an Mathematik für dieses Kapitel. Jetzt wollen wir den Algorithmus in Code umsetzen.
Die Probe aufs Exempel
In der Version mit zwei Variablen sieht unser Code für das Gradientenverfahren wie folgt aus, wobei die geänderten Zeilen wiederum durch kleine Pfeile gekennzeichnet sind:
03_gradient/gradient_descent_final.py
import numpy as np
def predict(X, w, b):
return X * w + b
def loss(X, Y, w, b):
return np.average((predict(X, w, b) - Y) ** 2)
def gradient(X, Y, w, b): <
w_gradient = 2 * np.average(X * (predict(X, w, b) - Y)) <
b_gradient = 2 * np.average(predict(X, w, b) - Y) <
return (w_gradient, b_gradient) <
def train(X, Y, iterations, lr):
w = b = 0 <
for i in range(iterations):
print("Iteration %4d => Loss: %.10f" % (i, loss(X, Y, w, b))) <
w_gradient, b_gradient = gradient(X, Y, w, b) <
w -= w_gradient * lr <
b -= b_gradient * lr <
return w, b <
X, Y = np.loadtxt("pizza.txt", skiprows=1, unpack=True)
w, b = train(X, Y, iterations=20000, lr=0.001) <
print("\nw=%.10f, b=%.10f" % (w, b))
print("Prediction: x=%d => y=%.2f" % (20, predict(20, w, b)))
Die Funktion gradient() gibt jetzt die partiellen Ableitungen des Verlusts nach w und b zurück. Anhand dieser Werte verändert train() gleichzeitig w und b. Außerdem habe ich die Anzahl der Iterationen heraufgesetzt, da das Programm jetzt zwei Variablen optimieren muss und daher länger benötigt, um in die Nähe des Minimums zu gelangen.
Um diese neue Version des Programms unter gleichartigen Bedingungen mit derjenigen aus dem vorherigen Kapitel zu vergleichen, führen wir zunächst Letztere mit einer großen Anzahl von Iterationen und einer ziemlich niedrigen lr von 0,0001 aus, sodass wir eine Genauigkeit auf vier Dezimalstellen erhalten:
...
Iteration 157777 => Loss: 22.842737
w=1.081, b=13.171
Prediction: x=20 => y=34.80
Unsere neue Implementierung mit Gradientenverfahren nähert sich dem Ergebnis auf spiralförmigem Kurs. Nach nur 20.000 Iterationen erhalten wir folgendes Resultat:
...
Iteration 19999 => Loss: 22.8427367616
w=1.0811301700, b=13.1722676564
Prediction: x=20 => y=34.79
Höhere Genauigkeit mit einem Zehntel an Iterationen – großartig! Für unser Pizzavorhersageproblem mag dieses schnellere und genauere ML-Programm zu viel des Guten sein, denn schließlich kauft niemand eine Hundertstel Pizza. Der Geschwindigkeitsgewinn jedoch wird sich bei anspruchsvolleren Problemen noch als äußerst wichtig erweisen.
Abschließend habe ich noch ein kurzes Visualisierungsprogramm geschrieben, um den Pfad auszugeben, den der Algorithmus von einem willkürlichen Ausgangspunkt zum Minimum des Verlusts nimmt. Sie wissen inzwischen zwar schon, wie das Gradientenverfahren abläuft, aber nichts geht über eine grafische Darstellung:
Die Wanderin hat nicht den kürzesten Weg zum Lager genommen, da sie die Route schließlich nicht im Voraus kannte. Stattdessen hat sie sich bei jedem Schritt an der Verlustfunktion orientiert. Nach zwei abrupten Richtungswechseln hat sie endlich die Talsohle erreicht und ist dem sanft absteigenden Pfad zum Lager gefolgt.
Probleme beim Gradientenverfahren
Bei der Anwendung des Gradientenverfahrens gibt es keine Erfolgsgarantie. Die gefundene Route ist nicht unbedingt die kürzeste. Es kann auch sein, dass wir am Lager vorbeigehen und wieder zurückmarschieren müssen oder dass wir uns sogar vom Lager entfernen.
Außerdem gibt es einige unglückselige Fälle, bei denen das Gradientenverfahren das Ziel völlig verfehlt. Einer davon hat mit der Lernrate zu tun. Wir werden ihn uns in der praktischen Übung am Ende dieses Kapitels noch genauer ansehen. Die meisten Probleme beim Gradientenverfahren gehen jedoch auf die Form der Verlustoberfläche zurück.
Mit etwas Fantasie lassen sich durchaus Oberflächen denken, auf denen unsere Wanderin auf dem Weg zum Lager zu Fall kommt. Was passiert, wenn die Verlustoberfläche wie in der folgenden Abbildung einen abrupten Absturz enthält, über dem die Wanderin wie Wile E. Coyote mit den Beinen rudernd in der Luft hängt?
Ein anderes Problem besteht darin, dass die Wanderin statt des globalen Minimums, zu dem sie unterwegs ist, nur ein lokales Minimum wie in der folgenden Abbildung erreicht.
Am Boden des lokalen Minimums beträgt die Steigung null. Wenn das Gradientenverfahren dort hingelangt, steckt es fest.
Langer Rede kurzer Sinn: Das Gradientenverfahren funktioniert gut, solange die Verlustoberfläche bestimmte Eigenschaften aufweist. Mathematisch ausgedrückt, muss eine gute Verlustfunktion konvex sein (keine lokalen Minima aufweisen), stetig (frei von Lücken und Abstürzen) und differenzierbar (glatt, also ohne Spitzen und andere eigentümliche Stellen, an denen es nicht möglich ist, die Ableitung zu berechnen). Unsere jetzige Verlustfunktion erfüllt alle drei Voraussetzungen, ist also ideal für den Gradientenabstieg geeignet. Wir werden das Verfahren später noch auf andere Funktionen anwenden, die wir dabei zunächst auf die genannten Voraussetzungen überprüfen müssen.
Das Gradientenverfahren ist auch der Hauptgrund dafür, dass wir den Verlust als mittleren quadratischen Fehler implementiert haben. Wir hätten auch den mittleren Absolutwert des Fehlers nehmen können, allerdings ist diese Funktion nicht gut für das Gradientenverfahren geeignet, da sie beim Wert 0 eine nicht differenzierbare Spitze aufweist. Außerdem werden die Fehlerwerte durch das Quadrieren noch größer, was dazu führt, dass die Oberfläche sehr steil wird, wenn wir uns vom Minimum entfernen. Umgekehrt bringt dieser steile Verlauf es natürlich mit sich, dass sich das Gradientenverfahren dem Minimum rasant nähert. Aufgrund der Glätte und Steilheit ist der mittlere quadratische Fehler sehr gut für dieses Verfahren geeignet.
Zusammenfassung
In diesem Kapitel haben wir uns mit dem Gradientenverfahren beschäftigt, dem am häufigsten verwendeten Algorithmus zur Minimierung des Verlusts. Wie kompliziert unser Modell und die Datenmenge auch sein mögen, das Gradientenverfahren funktioniert immer auf die gleiche Weise: Es geht immer einen Schritt in die entgegengesetzte Richtung des Verlustgradienten, bis dieser Gradient sehr klein geworden ist. Um den Gradienten zu finden, berechnen wir die partiellen Ableitungen des Verlusts nach w und b.
Das Gradientenverfahren hat jedoch seine Grenzen. Da es auf Ableitungen basiert, muss die Verlustfunktion glatt und lückenlos sein, damit die Ableitung überall berechnet werden kann. Des Weiteren ist es möglich, dass das Verfahren in einem lokalen Minimum stecken bleibt und das globale Minimum nicht mehr erreicht. Um diese Probleme zu vermeiden, verwenden wir glatte Verlustfunktionen mit einem einzigen Minimum.
Das Gradientenverfahren ist nicht das Nonplusultra der Algorithmen zur Verlustminimierung. Forscher suchen nach anderen Algorithmen, die für bestimmte Umstände besser geeignet sind. Es gibt auch Variationen des regulären Verfahrens, von denen wir in diesem Buch noch einige kennenlernen werden. Nichtsdestoweniger ist das Gradientenverfahren im modernen ML von entscheidender Bedeutung und wird es auch noch lange bleiben.
Wappnen Sie sich jetzt für eine Herausforderung! Zu Beginn des Kapitels habe ich gesagt, dass es durch Anwendung des Gradientenverfahrens möglich wird, unseren Code auch auf interessantere Modelle zu übertragen und eine Annäherung an kompliziertere Datenmengen zu erreichen. Im nächsten Kapitel werden wir uns ein solches Modell ansehen.
Praktische Übung: Über das Ziel hinaus
Kommen wir noch einmal auf die Lernrate zurück. Im letzten Beispiel dieses Kapitels haben wir eine Lernrate von 0,001 verwendet. Wenn Sie die Lernrate erhöhen, werden Sie feststellen, dass der Verlust schließlich zu steigen beginnt anstatt weiter zu sinken. Können Sie sich denken, warum das so ist?
Wenn Sie den Grund durch abstraktes Nachdenken nicht herausfinden können, versuchen Sie, die Verlustfunktion aufzuzeichnen. Was geschieht bei einer sehr großen Lernrate? Die Antwort finden Sie im Verzeichnis 03_gradient/solution.