Читать книгу Machine Learning für Softwareentwickler - Paolo Perrotta - Страница 11
Оглавление2
Ihr erstes ML-Programm
Willkommen zum ersten Schritt auf dem Weg des Machine Learning. In diesem Kapitel schreiben Sie ein winziges Programm für überwachtes Lernen. Es ist noch weit weg von unserem Ziel: der Bilderkennung. Ehrlich gesagt, hat es überhaupt nichts mit maschinellem Sehen zu tun. Allerdings werden wir den Code in den nächsten Kapiteln immer mehr verfeinern, bis er dazu geeignet ist, Bilder zu erkennen.
Die erste Version unseres Programms basiert auf der Technik der linearen Regression. Wie Sie im ersten Kapitel erfahren haben, geht es beim überwachten Lernen darum, Daten mit einer Funktion anzunähern. Bei der linearen Regression wird dafür die einfachste Funktion herangezogen, die überhaupt möglich ist: eine gerade Linie.
Trotz dieser Einfachheit sollten Sie die vor Ihnen liegende Aufgabe nicht unterschätzen. Um Ihnen eine Einführung in lineare Regression zu geben, werden in diesem Kapitel viele verschiedene Begriffe und Prinzipien erwähnt – so viele, dass Sie sich schon überfordert fühlen mögen. Auch wenn wir anscheinend nur ein kurzes Programm schreiben, legen wir doch in Wirklichkeit die Grundlage für tiefere Kenntnisse in ML. Was Sie hier lernen, werden Sie im ganzen Rest des Buchs und darüber hinaus brauchen können.
Fangen wir mit einer praktischen Aufgabe an.
Die Aufgabenstellung
Unser Freund Roberto führt eine gemütliche kleine Pizzeria in Florenz. Mittags sieht er sich immer die Anzahl der Reservierungen an und entscheidet auf dieser Grundlage, wie viel Pizzateig er für den Abend vorbereiten soll. Wenn er zu viel Teig macht, wird der Überschuss schlecht; wenn er zu wenig macht, geht irgendwann die Pizza aus. In beiden Fällen verliert er Geld.
Es ist jedoch nicht so einfach, die Anzahl der Pizzas aus den Reservierungen abzulesen, denn erstens reservieren viele Gäste keinen Tisch, und zweitens essen viele auch gar keine Pizza, sondern etwas anderes. Roberto weiß, dass es eine Beziehung zwischen den beiden Zahlen gibt, da er bei mehr Reservierungen gewöhnlich auch mehr Pizzas verkauft. Die genaue Beziehung kennt er jedoch nicht.
Roberto träumt von einem Programm, das sich die alten Daten anschaut, die Relation zwischen Reservierungen und Pizzas erkennt und damit aus den Reservierungen für den Abend die Pizzaverkäufe am selben Abend vorhersagen kann. Er bittet uns, ein solches Programm für ihn zu schreiben.
Pizzavorhersage mit überwachtem Lernen
Nach dem, was ich im Abschnitt »Überwachtes Lernen« auf Seite 12 gesagt habe, können wir Robertos Pizzavorhersageproblem lösen, indem wir einen Algorithmus für überwachtes Lernen mit einer Reihe von mit Labels versehenen Beispielen trainieren. Wir bitten Roberto, uns die Zahlen der Reservierungen und Pizzaverkäufe einiger Tage als Beispiele mitzuteilen, und sammeln sie in einer Datei, deren erste Zeilen nun wie folgt aussehen:
02_first/pizza.txt
Reservierungen | Pizzas |
13 | 33 |
2 | 16 |
14 | 32 |
23 | 51 |
Jede Zeile ist ein Beispiel mit der Anzahl der Reservierungen als Eingabevariable und der Anzahl der Pizzas als numerisches Label. Sobald wir einen Algorithmus haben, können wir ihn anhand dieser Beispiele trainieren. In der Vorhersagephase übergeben wir dann die Anzahl der Reservierungen an den Algorithmus, damit er die zugehörige Anzahl von Pizzas ausgibt.
Fangen wir mit den Zahlen an, wie es Datenwissenschaftler gewöhnlich tun.
Zusammenhänge in den Daten erkennen
Schon beim ersten Blick auf Robertos Beispiele können wir erkennen, dass es eine Korrelation zwischen den Reservierungen und der Anzahl der Pizzas gibt. Um uns das genauer anzusehen, starten wir (mit dem Befehl python3) eine Python-Shell.
Die Bibliothek NumPy enthält eine komfortable Funktion, um durch Weißraum getrennte Daten aus Text zu importieren:
import numpy as np
X, Y = np.loadtxt("pizza.txt", skiprows=1, unpack=True)
Die erste Zeile importiert die Bibliothek NumPy, und die zweite lädt die Daten aus der Datei pizza.txt mithilfe der NumPy-Funktion loadtxt(). Ich habe hier die Kopfzeile übersprungen und die beiden Spalten in die getrennten Arrays X und Y »entpackt«, wobei X die Werte der Eingabevariablen enthält und Y die Labels. Die Namen der Arrays sind Großbuchstaben. Das ist eine Python-Konvention, um anzuzeigen, dass eine Variable als Konstante behandelt werden soll.
Sehen wir uns nun die Daten an, um uns zu vergewissern, dass sie korrekt geladen wurden. Wenn Sie die Beispiele nachvollziehen wollen, starten Sie einen Python-Interpreter, geben die beiden zuvor gezeigten Zeilen ein und prüfen dann die ersten Elemente von X und Y:
X[0:5]
[ 13. 2. 14. 23. 13.]
Y[0:5])
[ 33. 16. 32. 51. 27.]
Das sind die Zahlen aus Robertos Datei, allerdings ist es schwer, irgendwelche Zusammenhänge darin zu erkennen. In einem Diagramm werden die Beziehungen jedoch klarer:
Jetzt springt die Korrelation ins Auge: Je mehr Reservierungen, umso mehr Pizzas werden verkauft. Um der Wahrheit die Ehre zu geben: Ein Statistiker würde uns dafür schelten, dass wir aus einer Handvoll flüchtig zusammengesuchter Daten solche Schlussfolgerungen ziehen. Allerdings ist dies hier nicht als Forschungsprojekt gedacht. Ignorieren wir also unseren inneren Statistiker und schreiben wir stattdessen unser Pizzavorhersageprogramm.
Der Diagrammcode
Der Code für das vorstehende Diagramm sieht übrigens wie folgt aus:
02_first/plot.py
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set() # Aktiviert Seaborn
plt.axis([0, 50, 0, 50]) # Skaliert Achsen (0 bis 50)
plt.xticks(fontsize=15) # Legt Teilstriche der x-Achse fest
plt.yticks(fontsize=15) # Legt Teilstriche der y-Achse fest
plt.xlabel("Reservations", fontsize=30) # Legt Beschriftung der x-Achse fest
plt.ylabel("Pizzas", fontsize=30) # Legt Beschriftung der y-Achse fest
X, Y = np.loadtxt("pizza.txt", skiprows=1, unpack=True) # Lädt Daten
plt.plot(X, Y, "bo") # Stellt Daten im Diagramm dar
plt.show() # Zeigt das Diagramm an
Neben NumPy nutzt der vorstehende Code noch zwei andere Bibliotheken, nämlich Matplotlib, um das Diagramm zu zeichnen, und Seaborn, um das Erscheinungsbild des Diagramms zu verbessern. Die Funktion plot() stellt die Beispiele als blaue Kreise dar ('bo'), und der Rest des Codes richtet die Achsen ein, lädt die Datenpunkte und zeigt sie an. Wenn Sie wollen, können Sie dieses Programm auch selbst mit python3 plot.py ausführen.
Mit dem Diagrammcode müssen Sie sich jetzt nicht befassen, aber früher oder später werden Sie selbst ähnlichen Code schreiben. Sie können ja einen verregneten Nachmittag dafür reservieren, sich in die Grundlagen von Matplotlib und Seaborn einzuarbeiten, oder die Einzelheiten nach Bedarf Stück für Stück lernen.
Im weiteren Verlauf werde ich den Code zum Erstellen der Diagramme nicht mehr angeben. Sie finden ihn aber im Quellcode zu diesem Buch.
Eine lineare Regression programmieren
Unser Ziel besteht darin, ein Programm zu schreiben, dass die Anzahl der Pizzas aus der Anzahl der Reservierungen berechnet. Dabei soll dieses Programm der Vorgehensweise folgen, die wir im Abschnitt »Die Mathematik hinter dem Zaubertrick« auf Seite 14 besprochen haben: In der Trainingsphase soll es die Daten durch eine Funktion annähern und in der Vorhersagephase die Anzahl der Pizzas aus dieser Funktion schließen.
Eine solche Näherungsfunktion für eine Datenmenge zu finden, ist im Allgemeinen harte Arbeit. Allerdings haben wir in unserem Fall das Glück, dass die Datenpunkte so ausgerichtet sind, dass wir sie durch eine besonders einfache Funktion annähern können, nämlich eine Gerade.
Wie aber sieht diese Gerade aus? Für den Anfang wählen wir wie im folgenden Diagramm gezeigt eine Gerade aus, die durch den Ursprung verläuft, da dies die Sache einfacher macht.
Wenn wir die Gerade bestimmt haben, ist die Trainingsphase beendet. Die Gerade ist unser Modell der Relation zwischen Reservierungen und Pizzas.
Anschließend können wir zur Vorhersagephase übergehen, in der wir die Gerade dazu heranziehen, das Label zu einer gegebenen Eingabevariablen vorherzusagen, also etwa anzugeben, wie viele Pizzas wir bei 20 Reservierungen zu verkaufen erwarten können. Um diese Frage zu beantworten, habe ich von dem Punkt x = 20 auf der Achse der Reservierungen eine senkrechte Linie nach oben bis zu der Geraden eingezeichnet und bin von dem Kreuzungspunkt aus nach links zur Pizzaachse gegangen, wo ich beim Punkt y = 42 gelandet bin:
Da haben wir das Ergebnis: Bei 20 Reservierungen können wir etwa 42 Pizzabestellungen erwarten.
Diese Methode wurde von Statistikern lange schon genutzt, bevor es überwachtes Lernen gab. Sie wird als lineare Regression bezeichnet. »Regression« ist der statistische Begriff dafür, die Relation zwischen zwei Variablen zu finden, und da wir hier keine Kurve, sondern eine gerade Linie verwenden, ist diese Regression »linear«.
Zusammengefasst läuft überwachtes Lernen mit linearer Regression also wie folgt ab:
1 Trainingsphase: Wir bestimmen eine Gerade, die die Beispiele annähert.
2 Vorhersagephase: Wir sagen anhand dieser Geraden das Label zu einer gegebenen Eingabevariablen voraus.
Natürlich lässt sich nicht jede Relation durch eine gerade Linie annähern. Wenn sich unsere Beispiele entlang einer Kurve verteilen oder gar keine erkennbare Form bilden, können Sie diese einfache Methode nicht nutzen. Robertos Beispiele haben jedoch grob einen linearen Verlauf, weshalb die lineare Regression für unsere vorliegende Aufgabe gut genug ist.
Als Nächstes müssen wir die lineare Regression in Code umsetzen.
Das Modell definieren
Um die lineare Regression in ausführbaren Code zu übersetzen, brauchen wir eine Möglichkeit, um eine Gerade numerisch darzustellen. Dazu brauchen wir etwas Mathematik.
Eine Gerade durch den Ursprung wird durch folgende Gleichung beschrieben:
y = x * w
Vielleicht kennen Sie diese Gleichung noch aus der Schulzeit. Jede Gerade, die durch den Ursprung verläuft, ist durch einen einzigen Wert eindeutig bestimmt. Ich habe diesen Wert hier w für weight (Gewicht) genannt, aber Sie können ihn sich auch als Steigung der Geraden vorstellen. Je größer w, umso steiler die Gerade. Betrachten Sie dazu den folgenden Graphen:
Bevor wir weitermachen, möchte ich die Gleichung noch einmal umschreiben:
ŷ = x * w
Ich habe das Symbol y hier durch ŷ (sprich: Ypsilon-Dach) ersetzt, um Verwechselungen mit den y-Werten aus Robertos Datei zu vermeiden. Beide Symbole stehen für die Anzahl der Pizzas, aber es gibt einen wichtigen Unterschied zwischen ihnen: ŷ ist unsere Vorhersage dafür, wie viele Pizzas wir zu verkaufen erwarten, wohingegen es sich bei y um Labels aus Beobachtungen der Realität handelt, also das, was beim Machine Learning als Grundwahrheit bezeichnet wird.
Kommen wir nach dieser kurzen Abschweifung zur Schreibweise wieder zu dem entscheidenden Punkt zurück: w ist eine Konstante, die die Gerade beschreibt. Anders ausgedrückt ist w alles, was wir brauchen, um die Gerade im Code darzustellen.
Damit können wir nun also den Code schreiben. Wir beginnen dabei mit der Vorhersagephase, da sie einfacher ist als die Trainingsphase. Letztere werden wir im Anschluss angehen.
Mathematischer Hintergrund: Lineare Gleichungen
Wenn Sie mit einer Gleichung wie y = x * w nichts anzufangen wissen, können Sie sich die Videos der Khan Academy zu linearen Gleichungen ansehen.1 Diese Videos behandeln jedoch viel mehr Stoff, als Sie zur Lektüre dieses Buchs benötigen, weshalb Sie sich nicht alle ansehen müssen.
Eine Vorhersage treffen
Nehmen wir an, wir haben bereits die Gerade (also den Wert w) und wollen auf deren Grundlage nun den Wert ŷ aus x berechnen, also die Anzahl der Pizzas aus den Reservierungen. Das lässt sich in einer Zeile erledigen:
02_first/linear_regression.py
def predict(X, w):
return X * w
Die Funktion predict() sagt die Anzahl der Pizzas aus den Reservierungen voraus. Genauer gesagt nimmt sie die Eingabevariable und das Gewicht entgegen und berechnet daraus ŷ.
Diese kurze Funktion ist viel leistungsfähiger, als sie scheint. Vor allem kann X nicht nur eine einzelne Zahl, sondern ein ganzes Array mit Reservierungen sein. NumPy hat einen eigenen Arraytyp, der Broadcast-Operationen ermöglicht: Wenn wir ein Reservierungsarray mit w multiplizieren, so multipliziert NumPy jedes Element dieses Arrays mit w und gibt ein Array mit den vorhergesagten Pizzaverkaufsmengen zurück. Das ist eine praktische Möglichkeit, um mehrere Vorhersagen auf einmal durchzuführen.
Zu Anfang dieses Abschnitts haben wir von Hand Linien in den Graphen eingezeichnet, um von der Anzahl der Reservierungen zur Anzahl der Pizzas zu kommen. Die Funktion predict() erledigt den gleichen Vorgang, allerdings viel genauer. Wie viele Pizzas können wir bei 20 Reservierungen zu verkaufen erwarten? Nehmen wir an, unsere Gerade ist durch w = 2.1 definiert. Durch den Aufruf von predict(20, 2.1) erhalten wir die vorhergesagte Pizzamenge von 42.
Das ist auch schon alles, was wir für die zweite Phase der linearen Regression benötigen. Damit wollen wir uns jetzt der komplizierteren ersten Phase zuwenden. Das wird uns mehrere Seiten lang intensiv beschäftigen.
Das Training implementieren
Wir wollen nun den Code schreiben, um die erste Phase der linearen Regression zu implementieren. Er dient dazu, die Gerade w als Annäherung an die Menge der durch X und Y gegebenen Beispiele zu finden. Können Sie sich vorstellen, wie wir dazu vorgehen müssen? Halten Sie ruhig eine Minute inne und denken Sie darüber nach, wie Sie dieses Problem lösen würden.
Vielleicht denken Sie, dass die Mathematik eine einfache Möglichkeit bietet, um w zu finden. Schließlich muss es doch irgendeine Formel geben, die aus einer Liste von Datenpunkten eine passende Näherungsgerade berechnet. Wir könnten im Internet nach einer solchen Formel suchen und vielleicht sogar eine Bibliothek mit einer Implementierung dafür finden.
Es gibt tatsächlich eine solche Formel, allerdings werden wir sie hier nicht benutzen, da sie eine Sackgasse darstellt. Wenn wir hier eine Formel anwenden, die die Datenpunkte mit einer geraden Linie annähert, dann kommen wir später nicht weiter, wenn wir mit Datenmengen zu tun haben, bei denen verzwicktere Modellierungsfunktionen erforderlich sind. Daher ist es besser, nach einer allgemeineren Lösung Ausschau zu halten, die bei jedem Modell funktioniert.
So viel zur mathematischen Vorgehensweise. Schauen wir uns stattdessen an, was Programmierer in einem solchen Fall tun.
Wie falsch liegen wir?
Welche Strategie können wir verfolgen, um die Gerade zu finden, die sich bestmöglich an die Beispiele annähert? Nehmen wir an, wir haben eine Funktion, die die Beispiele (X und Y) und eine Gerade (w) entgegennimmt und den Fehler dieser Geraden misst. Je besser die Annäherung, umso geringer der Fehler. Mit einer solchen Funktion können wir verschiedene Geraden bewerten, bis wir eine mit einem ausreichend niedrigen Fehler finden; wobei ML-Experten jedoch nicht von »Fehler« sprechen, sondern von »Verlust«.
Wie schreiben wir nun diese Verlustfunktion? Nehmen Sie an, wir haben Zufallswert für w, sagen wir 1.5. Versuchen wir nun, mit diesem w vorherzusagen, wie viele Pizzas wir verkaufen können, wenn wir beispielsweise 14 Reservierungen haben. Der Aufruf von predict(14, 1.5) liefert das Ergebnis ŷ = 21 Pizzas.
Diese Vorhersage entspricht aber nicht der Grundwahrheit, also den realen Beispielen aus Robertos Datei. Betrachten Sie dazu noch einmal die ersten Beispiele:
02_first/pizza.txt
Reservierungen | Pizzas |
13 | 33 |
2 | 16 |
14 | 32 |
23 | 51 |
An dem Abend mit den 14 Reservierungen hat Roberto 32 Pizzas verkauft und nicht 21. Daraus können wir den Fehler als Differenz zwischen dem vorhergesagten Wert ŷ und der Grundwahrheit berechnen. Dieser Fehler ist der orangefarbene Balken im folgenden Graphen:
Im Code sieht diese Berechnung wie folgt aus:
error = predict(X, w) - Y
Es gibt jedoch ein kleines Problem bei dieser Berechnung: error kann hier null, positiv oder negativ sein. Allerdings sollte ein Fehler immer positiv sein, denn wenn Sie mehrere Fehler addieren, wie wir es in Kürze tun werden, dann sollen sich zwei entgegengesetzte Fehler nicht ausgleichen. Um zu garantieren, dass der Fehler stets positiv ist, quadrieren wir ihn:
squared_error = error ** 2
Statt des Quadrats könnten wir auch den Absolutwert des Fehlers verwenden. Allerdings bietet die Quadrierung noch weitere Vorteile, wie Sie im nächsten Kapitel sehen werden.
Wenn wir jetzt den Durchschnitt der quadrierten Fehler für alle Beispiele bilden, erhalten wir den Verlust. Diese Vorgehensweise zur Berechnung des Verlusts wird als mittlerer quadratischer Fehler bezeichnet und in der Statistik häufig verwendet. Im Code sieht dies wie folgt aus:
02_first/linear_regression.py
def loss(X, Y, w):
return np.average((predict(X, w) - Y) ** 2)
Da sowohl X als auch Y NumPy-Arrays sind, können wir den Code ziemlich knapp halten. In loss() multiplizieren wir jedes Element von X mit w, was ein Array aus Vorhersagen ergibt, berechnen dann für jede Vorhersage den Fehler als Differenz zwischen der Vorhersage und der Grundwahrheit, quadrieren den Fehler mit dem Potenzoperator ** und weisen NumPy schließlich an, den Durchschnitt der quadrierten Fehler zu ermitteln. Das alles geschieht in einer einzigen Zeile! Und schon haben wir den mittleren quadratischen Fehler.
Damit ist die Funktion loss() auch schon fertig, sodass wir uns der letzten Funktion unseres ML-Programms zuwenden können.
Zu viel Jargon?
»Mittlerer quadratischer Fehler«, »Modell«, »Verlust« … schon auf diesen paar Seiten werden Sie mit neuen Begriffen bombardiert. Eine Übersicht solcher Begriffe finden Sie in Anhang B, »Wörterbuch des Machine Learning«. Sollte ein Begriff dort nicht aufgeführt sein, schlagen Sie ihn im Index nach.
Näher und näher
Bei der Trainingsphase mit linearer Regression geht es darum, eine Gerade zur Annäherung der Beispiele zu finden. Mit anderen Worten, wir wollen w aus den Werten von X und Y berechnen. Dazu können wir einen iterativen Algorithmus verwenden:
02_first/linear_regression.py
def train(X, Y, iterations, lr):
w = 0
for i in range(iterations):
current_loss = loss(X, Y, w)
print("Iteration %4d => Loss: %.6f" % (i, current_loss))
if loss(X, Y, w + lr) < current_loss:
w += lr
elif loss(X, Y, w - lr) < current_loss:
w -= lr
else:
return w
raise Exception("Couldn't converge within %d iterations" % iterations)
Die Funktion train() durchläuft immer wieder die Beispiele, bis sie gelernt hat, wie sie sie annähern kann. Ihre Argumente sind X, Y, die Anzahl der Iterationen (iterations) und der Wert lr (dessen Bedeutung ich in Kürze erklären werde). Zu Anfang initialisiert der Algorithmus w willkürlich mit dem Wert 0. Dieses w stellt eine Gerade im Diagramm dar. Es ist zwar unwahrscheinlich, dass sie eine gute Annäherung an die Beispiele darstellt, aber sie bildet wenigstens einen Ausgangspunkt.
Anschließend tritt train() in eine Schleife ein. Jeder Durchlauf beginnt mit der Berechnung des aktuellen Verlusts. Anschließend wird eine alternative Gerade betrachtet, die dadurch zustande kommt, dass wir w um einen kleinen Betrag erhöhen. Dieser Betrag ist praktisch die »Schrittgröße«, allerdings verwende ich hier den Fachbegriff aus dem Bereich des Machine Learning, nämlich die Lernrate, was zu der Abkürzung lr führt.
Wenn wir die Lernrate zu w addieren, erhalten wir eine neue Gerade. Nun fragen wir, ob diese zu einem geringeren Verlust führt als die bisherige Gerade. Wenn ja, machen wir w + lr zum neuen w und setzen die Schleife fort. Anderenfalls probiert der Algorithmus die Gerade w - lr aus. Auch hier wiederum wird w entsprechend aktualisiert, wenn diese neue Gerade zu einem geringeren Verlust führt als das bisherige w, und die Schleife fortgesetzt.
Führen weder w + lr noch w -lr zu einem geringeren Verlust als das aktuelle w, sind wir fertig, denn dann haben wir die Beispiele bestmöglich angenähert. In diesem Fall geben wir w an den Aufrufer zurück.
Anschaulich gesagt, dreht dieser Algorithmus die Gerade in beiden Richtungen und macht sie bei jedem Durchlauf ein kleines bisschen steiler oder flacher und untersucht dabei jeweils den Verlust. Je höher die Lernrate, umso schneller bewegt das System die Gerade. Das können Sie sich so ähnlich vorstellen wie einen Funker der alten Schule, der ganz langsam einen Regler dreht, um den Empfang immer ein kleines bisschen klarer zu machen, bis er schließlich so deutlich ist, wie er nur werden kann.
Iterative Algorithmen können manchmal jedoch in einer Endlosschleife hängenbleiben (man sagt dann, dass sie nicht konvergieren). Informatiker können beweisen, dass dieses Problem bei dem vorliegenden Algorithmus nicht besteht. Nach ausreichend Zeit und Iterationen wird er stets konvergieren. Allerdings kann es sein, dass ihm schon vorher die Iterationsmöglichkeiten ausgehen. In einem solchen Fall gibt train() auf und endet mit einer Ausnahme.
Sie brennen bestimmt schon darauf, den Code auszuführen. Dann also los!
Los geht’s!
Der folgende Code lädt Robertos Beispiele und übergibt sie an train(). Zur klareren Darstellung werden dabei benannte Argumente verwendet (siehe die Erklärung im Abschnitt »Schlüsselwortargumente« auf Seite 334). Nach dem Aufruf von train() werden das ermittelte Gewicht und die Vorhersage für die Anzahl von Pizzas bei 20 Reservierungen ausgegeben.
02_first/linear_regression.py
# Importiert den datensatz
X, Y = np.loadtxt("pizza.txt", skiprows=1, unpack=True)
# Trainiert das System
w = train(X, Y, iterations=10000, lr=0.01)
print("\nw=%.3f" % w)
# Sagt die Anzahl der Pizzas vorher
print("Prediction: x=%d => y=%.2f" % (20, predict(20, w)))
Beim Aufruf von train() müssen wir Werte für iterations und lr bereitstellen. Vorläufig bestimmen wir sie durch Ausprobieren. Ich habe hier 10.000 Iterationen verlangt, was nach einem guten Ausgangswert aussieht. Für lr, also die Stärke der Änderung von w bei jedem Trainingsschritt, habe ich 0,01 angegeben, was genau genug erscheint, um Pizzas zu zählen.
Wenn wir das Programm ausführen, konvergiert train() bereits nach 200 Iterationen:
Iteration 0 => Loss: 812.866667
Iteration 1 => Loss: 804.820547
Iteration 2 => Loss: 796.818187
...
Iteration 184 => Loss: 69.123947
w=1.840
Prediction: x=20 => y=36.80
Der Verlust ist bei jeder Iteration geringer geworden, bis der Algorithmus schließlich nicht mehr versucht hat, ihn noch weiter einzuschränken. An diesem Punkt beträgt das Gewicht 1,84. Das ist die Anzahl der Pizzas, die Roberto für jede Reservierung zu verkaufen erwarten darf. Bei 20 Reservierungen können wir also mit etwa 36,80 Pizzas rechnen. (Roberto verkauft zwar keine Bruchteile von Pizzas, ist aber der genaue Typ, der lieber eine Dezimalstelle zu viel als zu wenig berücksichtigt.)
Mit der Berechnung von w hat unser Code gewissermaßen eine Gerade in das Diagramm eingezeichnet wie im folgenden Graphen. (Den Code für die Diagrammausgabe finden Sie wie immer im Begleitcode zu diesem Buch.)
Das ist schon ein schönes Ergebnis. Wir können es aber noch verbessern.
Bias hinzufügen
Wenn Sie sich das vorstehende Diagramm ansehen, können Sie erkennen, dass unsere Gerade nicht die beste Annäherung an die Beispiele darstellt. Die ideale Gerade würde flacher sein und nicht durch den Ursprung gehen, sondern die Pizza-Achse etwa beim Wert 10 schneiden.
Bis jetzt haben wir dafür gesorgt, dass die Gerade durch den Ursprung verläuft, um unser Modell so einfach wie möglich zu halten. Allerdings sollten wir diese Einschränkung jetzt lieber aufgeben. Um eine Gerade zu zeichnen, die nicht durch den Ursprung geht, müssen wir unserem Modell einen weiteren Parameter hinzufügen:
ŷ = x * w + b
Das kommt Ihnen vielleicht bekannt vor. Es handelt sich hierbei um die klassische lineare Funktion, die Sie im Mathematikunterricht in der Mittelstufe kennengelernt haben, gewöhnlich in der Form y = m * x + t, wobei m als Steigung und t als y-Achsenabschnitt bezeichnet wird. Hier wollen wir jedoch die ML-Terminologie verwenden und w das Gewicht und b den Bias nennen (eigentlich die »systematische Abweichung«).
Anschaulich betrachtet ist der Bias ein Maß dafür, wie weit die Gerade nach oben oder unten verschoben wird (siehe die folgende Abbildung). Die Gerade schneidet die y-Achse beim Wert b. Bei b = 0 ergibt sich wieder unser vorheriger Fall einer Geraden, die durch den Ursprung verläuft.
Unter Verwendung des neuen Modells mit zwei Parametern sieht das komplette Programm zur linearen Regression nun wie folgt aus (wobei die kleinen Pfeile die geänderten Zeilen kennzeichnen):
02_first/linear_regression_with_bias.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 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)
# Importiert den datensatz
X, Y = np.loadtxt("pizza.txt", skiprows=1, unpack=True)
# Trainiert das System
w, b = train(X, Y, iterations=10000, lr=0.01) <
print("\nw=%.3f, b=%.3f" % (w, b)) <
# Sagt die Anzahl der Pizzas vorher
print("Prediction: x=%d => y=%.2f" % (20, predict(20, w, b))) <
Die meisten Zeilen haben sich geändert, aber all diese Änderungen betreffen nur die Einführung des neuen Parameters b. Von besonderer Bedeutung sind die Änderungen in der Funktion predict(), die jetzt das neue Modell nutzt, und train(), in der b jetzt auf die gleiche Weise unter Berücksichtigung des Verlusts erhöht und erniedrigt wird wie w. Es war mir nicht ganz klar, wie ich w und b gleichzeitig verändern sollte, weshalb ich einen dieser provisorischen Hacks verwendet habe, die wir alle so lieben: Ich habe einfach ein paar neue Zweige zu der if-Anweisung hinzugefügt.
Probieren wir das Programm nun aus! Die Funktion train() braucht jetzt länger, um zu konvergieren, kommt aber schließlich doch zum Ziel:
Iteration 0 => Loss: 812.867
Iteration 1 => Loss: 804.821
Iteration 2 => Loss: 796.818
...
Iteration 1551 => Loss: 22.864
w=1.100, b=12.930
Prediction: x=20 => y=34.93
Der Graph mit der Näherungsgeraden sieht wie folgt aus:
Jetzt nähert sich die Gerade den Beispielen schon viel besser an! Vor allem ist der resultierende Verlust geringer als zuvor, was bedeutet, dass wir die Pizzaverkäufe auch viel genauer vorhersagen können. Roberto wird sich freuen.
Das war eine Menge Arbeit für die erste Version unseres Programms. Halten wir kurz inne, treten wir einen Schritt zurück und sehen wir uns das Gesamtbild an.
Hyperparameter
Beim überwachten Lernen resultiert die Trainingsphase in einem Satz von Werten, die wir im Modell verwenden können. In unserem Fall handelt es sich dabei um das Gewicht und den Bias, die wir in die Geradengleichung einsetzen. Diese Werte werden »Parameter« genannt, was in der Programmierung jedoch etwas unglücklich ist, da wir die Bezeichnung »Parameter« hier schon für etwas anderes verwenden. Es ist sehr leicht möglich, Parameter wie w und b mit den Parametern von Funktionen wie train() zu verwechseln.
Um das zu vermeiden, werden die Parameter der Funktion train() im ML-Umfeld als Hyperparameter bezeichnet, also als »Parameter höherer Ordnung«. Kurz gesagt, legen wir Hyperparameter wie iterations und lr fest, damit die Funktion train() Parameter wie w und b ermitteln kann.
Zusammenfassung
In den ersten beiden Kapiteln haben Sie eine Menge Informationen erhalten und viele neue Begriffe kennengelernt. Lassen Sie mich daher eine Zusammenfassung geben.
In diesem Kapitel haben wir unser erstes Programm zum überwachten Lernen geschrieben. Ein System für überwachtes Lernen wird mit Beispielen trainiert, die jeweils aus einer Eingabevariablen und einem Label bestehen. In unserem Fall waren die Eingabevariablen jeweils eine Anzahl von Reservierungen und die Labels jeweils eine Anzahl verkaufter Pizzas.
Beim überwachten Lernen werden die Beispiele mit einer Funktion angenähert, die oft als Modell bezeichnet wird. In unserem ersten Programm ist das Modell eine Gerade, die durch zwei Parameter bestimmt wird, nämlich Gewicht und Bias. Das Prinzip der Annäherung von Beispielen durch eine Gerade wird lineare Regression genannt.
Die erste Phase beim überwachten Lernen ist die Trainingsphase, in der das System die Parameter des Models anpasst, um eine Annäherung an die Beispiele zu erreichen. Bei dieser Suche richtet sich das System nach der Verlustfunktion, die die Abweichung des aktuellen Modells von der Grundwahrheit misst: je geringer der Verlust, umso besser das Modell. Unser Programm berechnet den Verlust mit der Formel für den mittleren quadratischen Fehler. Das Training resultiert in dem Gewicht und dem Bias, die zu dem geringsten Verlust führen, den das System finden konnte.
Die in der Trainingsphase ermittelten Parameter werden anschließend in der Vorhersagephase des überwachten Lernens genutzt. Dabei werden Eingaben ohne Labels an das parametrisierte Modell übergeben. Das Ergebnis ist eine Vorhersage wie: »Heute Abend können Sie erwarten, 42 Pizzas zu verkaufen.«
In gewisser Hinsicht entsprechen die Trainings- und die Vorhersagephase des überwachten Lernens der Kompilierungs- und Laufzeitphase in der Programmierung. Das Training ist gewöhnlich datenhungrig und erfordert umfassende Berechnungen, wohingegen die Vorhersage keinen großen Aufwand verursacht. Selbst in unserem kleinen Programm braucht die Funktion train() merkliche Zeit, um eine passende Gerade zu den Beispielen zu finden, während predict() einfach nur eine rasend schnelle Multiplikation durchführt.
In umfangreichen Systemen wird der Unterschied zwischen Training und Vorhersage noch deutlicher: Das Training eines Spracherkennungssystems kann Wochen dauern und die Verarbeitung von Millionen von Audiodateien auf mehreren Prozessoren erfordern. Anschließend können Sie das System auf einem Smartphone installieren und dazu verwenden, ohne großen Aufwand die Bedeutung einzelner Stichproben zu bestimmen.
Das war eine ganze Menge Stoff. Ich verspreche Ihnen aber, dass in keinem weiteren Kapitel so viele neue Begriffe auf einen Schlag eingeführt werden.
Auf der anderen Seite haben wir in diesem Kapitel von Grund auf ein Programm zum überwachten Lernen erstellt, was schon eine ziemliche Leistung ist. Heutzutage nutzt der Großteil der ML-Software, darunter auch die meisten der erstaunlichen Deep-Learning-Systeme, das Prinzip des überwachten Lernens. Diese Systeme sind natürlich viel komplizierter als unser Pizzavorhersageprogramm. Statt einer Liste von Reservierungszahlen nehmen sie hochauflösende Bilder als Eingabe entgegen, und statt eines einfachen Modells mit zwei Parametern haben sie komplexe Modelle mit Zehntausenden von Parametern. Allerdings funktionieren sie nach denselben Grundprinzipien wie unser bescheidenes Python-Programm.
Im nächsten Kapitel bauen wir auf dieser Grundlage auf und lernen einen der wichtigsten Algorithmen des Machine Learning kennen.
Praktische Übung: Die Lernrate optimieren
Bevor Sie fortfahren, können Sie noch ein wenig mit dem Code herumspielen. Das ist eine gute Möglichkeit, um die Prinzipien besser zu verinnerlichen.
Als Erstes können Sie sich dazu etwas besser mit den Hyperparametern des Systems vertraut machen (siehe den Kasten »Hyperparameter« auf Seite 37). Versuchen Sie, den Wert des Arguments lr von train() zu ändern. Was geschieht, wenn Sie lr auf einen kleineren Wert setzen – oder einen größeren? Welche Vor- und Nachteile ergeben sich dabei jeweils? Wenn Sie diese Experimente abgeschlossen haben, können Sie im Verzeichnis 02_first/solution nachschlagen, wo Sie klare Antworten auf diese Fragen erhalten.