Machine Learning für Softwareentwickler. Paolo Perrotta
Чтение книги онлайн.
Читать онлайн книгу Machine Learning für Softwareentwickler - Paolo Perrotta страница 15
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