Читать книгу Machine Learning für Softwareentwickler - Paolo Perrotta - Страница 13
Оглавление4
Hyperräume
In den beiden vorherigen Kapiteln haben wir eine Ausgabe aus einer Eingabe vorhergesagt, nämlich die Pizzaverkäufe aus der Anzahl der Reservierungen. Bei Aufgabenstellungen in der Praxis gibt es jedoch gewöhnlich mehr als nur eine Eingabe. Selbst so einfache Dinge wie Pizzaverkäufe hängen von mehr als nur einer Variablen wie hier der Anzahl der Reservierungen ab. Beispielsweise wird das Restaurant wahrscheinlich mehr Pizzas verkaufen, wenn sich Touristen in der Stadt befinden, auch wenn die Anzahl der Reservierungen gleich bleibt.
Wenn es schon bei Pizza mehrere Variablen gibt, dann versuchen Sie sich einmal vorzustellen, wie viele wir bei komplexeren Aufgaben wie der Bilderkennung abdecken müssen. Ein Lernprogramm, das nur eine Variable berücksichtigt, könnte solche kniffligen Probleme niemals lösen. Dazu müssen wir unser Programm so ausbauen, dass es mit mehreren Eingabevariablen umgehen kann.
Zum Lernen aus mehreren Eingabevariablen verwenden wir eine erweiterte Version der linearen Regression: die multiple lineare Regression. In diesem Kapitel werden wir unser Programm so umschreiben, dass es dieses Verfahren nutzt. Außerdem lernen Sie einige nützliche Fertigkeiten wie Matrixoperationen und verschiedene NumPy-Funktionen kennen. Fangen wir also gleich an!
Ein schweres Kapitel
Eine Warnung vorab: Für die meisten Leser dürfte dies das schwierigste Kapitel im ganzen Buch sein, und zwar aus zwei Gründen. Erstens sind die nächsten Seiten sehr mathematiklastig, und zweitens ist der Code zwar nicht umfangreich, hat es dafür aber in sich. An einer Stelle brauche ich fast zwei Seiten, nur um eine einzige Codezeile zu erklären.
Lassen Sie sich davon aber bitte nicht abschrecken. Die hier vorgestellten Prinzipien und Techniken sind für Machine Learning von entscheidender Bedeutung, weshalb es sich lohnt, sie zu lernen. Denken Sie auch immer daran, dass es nach diesem Kapitel leichter wird!
Noch mehr Dimensionen
Im letzten Kapitel haben wir eine Version unseres Programms geschrieben, die sich das Gradientenverfahren zunutze macht. Diese Version lässt sich auf komplexere Modelle mit mehr als einer Variablen skalieren.
In einem Anfall von Leichtsinn haben wir das auch unserem Freund Roberto gesagt. Das war ein Fehler, denn nun ist Roberto begierig darauf, bei der Vorhersage der Pizzaverkäufe neben den Reservierungen auch noch andere Eingabevariablen wie das Wetter oder die Anzahl der Touristen in der Stadt zu berücksichtigen.
Für uns bedeutet das natürlich mehr Arbeit. Dennoch können wir Roberto schon verstehen, denn je mehr Variablen wir in dem Modell berücksichtigen, umso genauer können wir die Pizzaverkäufe vorhersagen.
Als Ausgangspunkt verwenden wir eine erweiterte Version der alten Datei pizza.txt. Die ersten Zeilen dieser neuen Datenmenge sehen wie folgt aus:
04_hyperspace/pizza_2_vars.txt
Reservierungen | Temperatur | Pizzas |
13 | 26 | 44 |
2 | 14 | 23 |
14 | 20 | 28 |
Roberto vermutet, dass an wärmeren Tagen mehr Menschen seine Pizzeria besuchen, weshalb er auch die Temperatur aufgezeichnet hat. Die Labels (die Anzahl der verkauften Pizzas) befinden sich jetzt in der dritten Spalte, während die ersten beiden die Eingabevariablen enthalten.
Sehen wir uns als Erstes an, wie sich die lineare Regression ändert, wenn wir von einer zu zwei Eingabevariablen übergehen. Bei der linearen Regression werden die Beispiele bekanntlich wie folgt durch eine Gerade angenähert:
Zur Erinnerung finden Sie hier noch einmal die Formel für diese Gerade:
ŷ = x * w + b
Wenn wir eine zweite Eingabevariable hinzufügen (in unserem Fall die Temperatur), liegen die Beispiele nicht mehr auf einer Ebene, sondern sind Punkte im dreidimensionalen Raum. Zu ihrer Annäherung können wir das um eine Dimension erweiterte Äquivalent einer Geraden verwenden, nämlich eine Ebene, wie die nächste Abbildung zeigt.
Zur Berechnung von ŷ müssen wir jetzt eine Ebenengleichung verwenden. Sie ähnelt einer Geradengleichung, hat aber zwei Eingabevariablen, x1 und x2, sowie zwei Gewichte, w1 und w2:
ŷ = x1 * w1 + x2 * w2 + b
Wir brauchen getrennte Gewichte für die beiden Eingabevariablen. In Robertos Datenmenge ist x1 die Anzahl der Reservierungen und x2 die Temperatur, und es ist einleuchtend, dass diese Faktoren unterschiedliche Auswirkungen auf die Anzahl der verkauften Pizzas nehmen, weshalb sie auch mit unterschiedlichen Gewichten in die Berechnung eingehen müssen.
In der Geradengleichung hat der Bias b den Abstand vom Ursprung angegeben. Das Gleiche gilt auch für die Ebene. Ohne b würde sie durch den Ursprung verlaufen. Um das zu beweisen, setzen Sie alle Eingabevariablen auf 0. Ohne Bias wäre ŷ ebenfalls 0. Mit dem Bias aber können wir die Ebene vertikal beliebig verschieben, bis wir die Position gefunden haben, in der sie die Punkte bestmöglich annähert.
Was aber geschieht, wenn Roberto seiner Datenmenge noch eine weitere Spalte hinzufügt?
04_hyperspace/pizza_3_vars.txt
Die neue Eingabevariable gibt die Touristendichte in der Stadt an, die Roberto von der Website des örtlichen Fremdenverkehrsamts heruntergeladen hat. Die Werte reichen von 1 (»keine Seele in der Stadt«) bis 10 (»Touristeninvasion«).
Zu Anfang haben wir zweidimensionale Beispiele mit einem eindimensionalen Modell angenähert. Dann sind wir zu dreidimensionalen Beispielen mit einem zweidimensionalen Modell übergegangen. Jetzt haben wir schon vierdimensionale Beispiele, zu deren Annäherung wir ein dreidimensionales Modell benötigen. Dieser Prozess setzt sich fort, wenn wir weitere Eingabevariablen hinzufügen. Allgemein ist eine (n -1)-dimensionale Form erforderlich, um Beispiele in n Dimensionen anzunähern.
Mit Ausnahme einiger Figuren von H. P. Lovecraft können Menschen allerdings nicht mehr als drei räumliche Dimensionen wahrnehmen. In der Mathematik stellt der Umgang mit solchen mehrdimensionalen Räumen, die einen um den Verstand bringen könnten, jedoch kein Problem dar. Sie werden als Hyperräume bezeichnet und mit denselben Gleichungen beschrieben wie zwei- und dreidimensionale Räume. Unabhängig davon, wie viele Dimensionen wir insgesamt haben, können wir einfach Eingabevariablen und Gewichte zu der Geraden- oder Ebenenformel hinzufügen:
ŷ = x1 * w1 + x2 * w2 + x3 * w3 + ... + b
Die vorstehende Formel ist die gewichtete Summe der Eingaben. Die Geradengleichung ist nur ein Sonderfall davon, nämlich die gewichtete Summe einer einzigen Eingabe. Unser Plan zur Erweiterung des ML-Programms auf mehrere Eingabevariablen klingt daher ganz einfach: Wir müssen nur die Geradengleichung durch eine allgemeinere Formel für gewichtete Summen ersetzen.
Um diesen Plan zu verwirklichen, brauchen wir jedoch einige mathematische Operationen, die wir uns in dem folgenden kleinen Exkurs ansehen werden.
Matrizenrechnung
Um unser System auf mehrere Variablen zu erweitern, brauchen wir zwei Matrizenoperationen. Ich möchte die Einführung dieser Operationen nicht auf den Zeitpunkt verschieden, an dem wir schon mit der Programmierung genug zu tun haben, sondern hier einige Seiten einschalten, um diese Hürde aus dem Weg zu räumen.
Eine Matrix ist ein zweidimensionales Arrays. Beispielsweise zeigt die folgende Abbildung eine (4, 3)-Matrix mit vier Zeilen und drei Spalten:
Die beiden Operationen, mit denen wir uns hier beschäftigen, sind die Matrizenmultiplikation und die Transposition. Beide sind im Machine Learning allgegenwärtig und verdienen daher jeweils ihren eigenen Abschnitt.
Mathematischer Hintergrund: Lineare Algebra
Matrizenoperationen wie die Matrizenmultiplikation und die Transposition gehören zu einem Teilgebiet der Mathematik, das als lineare Algebra bezeichnet wird. Wenn Sie mehr darüber erfahren wollen, schauen Sie sich die Lektionen der Khan Academy an.1
Matrizen multiplizieren
Haben Sie sich jemals gefragt, warum für ML gewöhnlich große Anlagen mit vielen Grafikprozessoren verwendet werden? Das liegt daran, dass ML-Systeme den Großteil der Zeit mit einer einzigen Operation beschäftigt sind, die sich auf Grafikprozessoren besonders schnell ausführen lässt, nämlich mit der Multiplikation großer Matrizen.
Die »goldene Regel« der Matrizenmultiplikation lautet: Zwei Matrizen können genau dann miteinander multipliziert werden, wenn die Anzahl der Zeilen in der zweiten Matrix gleich der Anzahl der Spalten in der ersten ist.
M1 ist eine (4, 3)-Matrix und M2 eine (3, 2)-Matrix. Laut der obigen Regel können wir sie also miteinander multiplizieren. Doch was steckt hinter dieser Regel? Um diese Frage zu beantworten, schreiben wir die Operation wie folgt:
Wenn die inneren Dimensionen gleich sind – wie in diesem Fall –, ist die Multiplikation zulässig. Das Ergebnis ist dann eine Matrix mit den äußeren Dimensionen, hier also (4, 2):
Schauen wir uns das an einigen konkreten Beispielen genauer an. Der einfachste Fall ist dabei eine Matrix mit einer einzigen Zeile, die mit einer Matrix mit einer einzigen Spalte multipliziert wird:
Als Erstes prüfen wir wieder, ob die Multiplikation zulässig ist. Wir haben hier eine (1, 3)- und eine (3, 1)-Matrix, und nach unserer goldenen Regel ist die Multiplikation (1, 3)·(3, 1) zulässig und ergibt eine (1, 1)-Matrix, also eine Matrix mit nur einem Element.
Um dieses eine Element zu berechnen, multiplizieren wir jedes Element von M1 mit dem korrespondierenden Element von M2, also das erste mit dem ersten, das zweite mit dem zweiten usw., und addieren schließlich die einzelnen Produkte:
2 * 2.5 + 3 * 4 + 5 * 1 = 22
Die Multiplikation sieht also wie folgt aus:
Vielleicht ist Ihnen schon die Ähnlichkeit zwischen dieser Multiplikation und der Formel am Ende des Abschnitts »Noch mehr Dimensionen« auf Seite 58 aufgefallen. Diese Ähnlichkeit ist natürlich kein Zufall. Wenn wir uns hinreichend mit der Matrizenmultiplikation beschäftigt haben, werden wir sie zur Implementierung unseres Modells mit linearer Regression verwenden.
Was aber müssen wir tun, wenn die Matrizen mehrere Zeilen und Spalten aufweisen? In diesem Fall gehen wir im Prinzip genauso vor wie im ersten Beispiel, multiplizieren aber jede Zeile der ersten Matrix mit jeder Spalte der zweiten. Jedes Element (i, j) der resultierenden Matrix enthält das Produkt von Zeile i von M1 und Spalte j von M2:
M3[i][j] = i-te_Zeile_von_M1 · j-te_Spalte_von_M2
Konkret sieht das wie folgt aus:
Prüfen wir das anhand eines der Elemente von M3 nach, z. B. M3[0][1]. Dieses Element hat den Wert 40. Nach unserer Rechenregel muss M3[0][1] gleich dem Produkt von Zeile 0 von M1 und Spalte 1 von M2 sein:
Das Produkt dieser Zeile mit dieser Spalte ist wie erwartet 2 * -3 + 3 * 12 + 5 * 2 = 40. Auf die gleiche Weise können Sie auch alle anderen Elemente von M3 überprüfen.
Anders als bei der regulären Multiplikation kommt es bei der Matrizenmultiplikation auf die Reihenfolge an. Wenn wir die beiden Matrizen vertauschen, erhalten wir im Allgemeinen ein anderes Ergebnis; in den meisten Fällen ist die Multiplikation dann nicht einmal mehr zulässig. In unserem Beispiel können wir M2 nicht mit M1 multiplizieren, da bei (3, 2)·(4, 3) die inneren Dimensionen ungleich sind.
In diesem Buch kommen sehr viele Matrizenmultiplikationen vor, allerdings werden wir sie nicht selbst ausrechnen, sondern dazu NumPy heranziehen. Es erleichtert die Sache jedoch, wenn Sie sich die goldene Regel merken: Die inneren Dimensionen müssen bei der Matrizenmultiplikation identisch sein, und die äußeren sind die Dimensionen der resultierenden Matrix. In unserem Beispiel haben wir (4, 3)·(3, 2) gerechnet und als Ergebnis eine (4, 2)-Matrix erhalten.
Das war es auch schon zum Thema Matrizenmultiplikation. Wenden wir uns nun der Transposition zu.
Matrizen transponieren
Im Vergleich zur Matrizenmultiplikation ist die Transposition einfach. Dabei spiegeln Sie die Matrix lediglich an der Diagonale, die von oben links nach unten rechts verläuft. In der folgenden Abbildung wird dies durch die verschiedenen Farbschattierungen der einzelnen Zellen deutlich gemacht:
Beim Transponieren einer Matrix werden aus den Zeilendaten Spaltendaten und umgekehrt. Die Dimensionen der Matrix werden vertauscht. In dem vorstehenden Beispiel wird aus der (4, 3)-Matrix durch Transponieren eine (3, 4)-Matrix.
Jetzt wissen Sie, wie die Matrizenmultiplikation und die Transposition funktionieren. Beide Operationen werden wir in Kürze benötigen. Kehren wir nun aber wieder zu unserem Code zurück.
Das ML-Programm erweitern
Nach diesem mathematischen Exkurs kommen wir wieder zu unserer praktischen Arbeit zurück. Wir möchten unser ML-Programm so erweitern, dass es mit mehreren Eingabevariablen umgehen kann. Um den Überblick zu behalten, stellen wir zunächst einen Plan dafür auf:
Erstens müssen wir die mehrdimensionalen Daten laden und aufbereiten, sodass wir sie in den Lernalgorithmus einspeisen können.
Anschließend müssen wir alle Funktionen in unserem Code so anpassen, dass sie das neue Modell nutzen. Wie im Abschnitt »Noch mehr Dimensionen« auf Seite 58 beschrieben, gehen wir von einer Geradengleichung zu einer allgemeinen gewichteten Summe über.
Diesen Plan werden wir nun Schritt für Schritt abarbeiten. Sie können die Befehle dabei gern in einen Python-Interpreter eingeben und selbst damit herumexperimentieren. Wenn Sie keinen Interpreter zu Hand haben, ist das jedoch auch kein Beinbruch, denn ich werde die Ausgaben aller wichtigen Befehle hier wiedergeben (wenn auch teilweise aus Platzgründen etwas bearbeitet).
Die Daten aufbereiten
Ich wünschte, ich könnte Ihnen jetzt erzählen, dass sich bei ML alles darum dreht, fantastische KIs zu konstruieren und dabei den coolen Experten zu geben. In Wirklichkeit besteht ein Großteil der Arbeit jedoch darin, Daten für den Lernalgorithmus aufzubereiten. Dabei gehen wir wieder von der Datei mit unserer Datenmenge aus:
In den vorherigen Kapiteln bestand diese Datei aus zwei Spalten, die wir mit der NumPy-Funktion loadtxt() in zwei Arrays geladen haben. Mehr war dazu nicht nötig. Da wir jetzt mehrere Eingabevariablen haben, hat X nun aber die Form einer Matrix:
Jede Zeile von X ist ein Beispiel und jede Spalte eine Eingabevariable.
Wenn wir die Datei wie zuvor mit loadtxt() laden, erhalten wir für jede Spalte ein NumPy-Array:
import numpy as np
x1, x2, x3, y = np.loadtxt("pizza_3_vars.txt", skiprows=1, unpack=True)
Arrays sind das Merkmal, bei dem NumPy richtig glänzt! Es handelt sich um flexible Objekte, die alles Mögliche von einem Skalar (einer einzelnen Zahl) bis zu mehrdimensionalen Strukturen darstellen können. Diese Flexibilität macht den Umgang mit den Arrays jedoch zu Anfang auch etwas schwer. Ich werde Ihnen zeigen, wie Sie diese vier Arrays zu den Variablen X und Y umformen, die wir benötigen, aber wenn Sie diese Arbeiten selbst erledigen, ist es am besten, die Dokumentation von NumPy zur Hand zu haben.
Um zu sehen, welche Dimensionen ein Array aufweist, verwenden wir die Operation shape():
x1.shape # => (30, )
Alle vier Spalten haben 30 Elemente, eines für jedes Beispiel in pizza_3_vars.txt. Das Komma am Ende ist die NumPy-Schreibweise dafür, dass diese Arrays nur jeweils eine Dimension aufweisen. Sie sind also weniger Matrizen als vielmehr das, was man sich gewöhnlich unter einem Array vorstellt.
Wir wollen nun die Matrix X zusammenstellen, indem wir die ersten drei Arrays miteinander verbinden:
X = np.column_stack((x1, x2, x3))
X.shape # => (30, 3)
Die ersten beiden Zeilen von X sehen nun wie folgt aus:
X[:2] # => array([[13., 26., 9.], [2., 14., 6.]])
Mit der Indizierung in NumPy lässt sich sehr viel anfangen, allerdings kann sie manchmal auch etwas verwirrend wirken. Die Schreibweise [:2] ist eine Abkürzung für [0:2] und bedeutet »die Zeilen vom Index 0 bis ausschließlich Index 2«, also die ersten beiden Zeilen.
So viel zu X. Kümmern wir uns nun um y, das immer noch die eindimensionale Form (30, ) aufweist. Grundsätzlich sollten es NumPy-Matrizen nicht mit eindimensionalen Arrays vermischen. Code, in dem beides vorkommt, legt manchmal überraschendes Verhalten an den Tag. Aus diesem Grunde ist es am besten, eindimensionale Arrays mit der Funktion reshape() wie folgt in eine Matrix umzuwandeln. Dieser kleine Trick hat mir schon manchmal den Hals gerettet.
Y = y.reshape(-1, 1)
reshape() nimmt die Dimensionen des neuen Arrays entgegen. Wenn eine dieser Dimensionen –1 ist, dann setzt NumPy sie auf einen passenden Wert für die anderen Dimensionen. Die obige Zeile bedeutet also: »Gestalte Y zu einer Matrix mit einer Spalte und so vielen Zeilen um, wie für die aktuelle Anzahl der Elemente nötig sind.« Dadurch ergibt sich eine (30, 1)-Matrix:
Y.shape # => (30, 1)
Damit haben wir unsere Daten jetzt sauber in einer Matrix X für Eingabevariablen und einer Matrix Y für die Labels angeordnet. Den Punkt »Datenaufbereitung« können wir damit abhaken. Als Nächstes ändern wir die Funktionen unseres Lernsystems. Dabei fangen wir mit der Funktion predict() an.
Die Vorhersagefunktion anpassen
Da wir jetzt mehrere Eingabevariablen haben, müssen wir die Vorhersageformel von einer einfachen Geradengleichung in eine gewichtete Summe ändern:
ŷ = x1 * w1 + x2 * w2 + x3 * w3 + ...
Wahrscheinlich ist Ihnen aufgefallen, dass in dieser Formel etwas fehlt. Zur Vereinfachung habe ich vorübergehend den Bias b weggelassen. Er wird aber bald wieder zurück sein.
Jetzt können wir die angegebene gewichtete Summe in eine mehrdimensionale Version von predict() umwandeln. In der eindimensionalen Version sah diese Formel wie folgt aus (hier ohne Bias):
def predict(X, w):
return X * w
Die neue Version von predict() nimmt nach wie vor X und w entgegen, wobei diese Variablen jetzt aber mehr Dimensionen haben. Bisher war X ein Vektor aus m Elementen, wobei m der Anzahl der Beispiele entsprach. Jetzt ist es eine (m, n)-Matrix, wobei n die Anzahl der Eingabevariablen ist. In dem hier betrachteten Fall haben wir 30 Beispiele und drei Eingabevariablen, weshalb X eine (830, 3)-Matrix ist.
Wie sieht es mit w aus? Pro Eingabevariable brauchen wir jetzt nicht nur ein x, sondern auch ein w. Anders als die x-Werte sind die w-Werte jedoch für jedes Beispiel gleich. Für die Gewichte können wir daher eine (n, 1)- oder eine (1, n)-Matrix verwenden. Wie wir gleich sehen werden, ist eine (n, 1)-Matrix besser geeignet, also eine Matrix mit einer einzigen Spalte und je einer Zeile pro Eingabevariable.
Diese Matrix müssen wir nun initialisieren. Bisher haben wir w einfach mit 0 initialisiert, doch da w nun eine Matrix ist, müssen wir alle ihre Elemente auf 0 setzen. Dafür gibt es in NumPy die Funktion zeros():
w = np.zeros((X.shape[1], 1))
w.shape # => (3, 1)
X.shape[0] gibt die Anzahl der Zeilen und X.shape[1] die Anzahl der Spalten in X an. Der vorstehende Code besagt also, dass w so viele Zeilen haben soll, wie es Spalten in X gibt, hier also drei.
An dieser Stelle kommt nun die Matrizenmultiplikation ins Spiel. Schauen Sie sich noch einmal die gewichtete Summe an:
ŷ = x1 * w1 + x2 * w2 + x3 * w3
Wenn X nur eine Zeile hätte, würde diese Formel die Multiplikation von X mit w beschreiben:
Allerdings hat X nicht nur eine Zeile, sondern eine Zeile pro Beispiel – es ist eine (30, 3)-Matrix. Wenn wir sie mit w multiplizieren – einer (3, 1)-Matrix –, erhalten wir eine (30, 1)-Matrix mit einer Zeile pro Beispiel und einer einzigen Spalte, die die Vorsagen für diese Beispiele enthält. Mit einer einzigen Matrizenmultiplikation erhalten wir also auf einen Streich die Vorhersagen für sämtliche Beispiele.
Um die Vorhersagefunktion für mehrere Eingabevariablen umzuschreiben, müssen wir also lediglich X und w multiplizieren, wobei uns die NumPy-Funktion matmul() zu Hilfe kommt:
def predict(X, w):
return np.matmul(X, w) <
Es hat eine Weile gedauert, um zu dieser winzigen Funktion zu kommen, und dabei haben wir die Sache noch vereinfacht, indem wir den Bias vorläufig ignoriert haben. Der Aufwand hat sich aber schon gelohnt: predict() implementiert jetzt das Modell der multiplen linearen Regression in einer einzigen, kurzen Codezeile.
Die Verlustfunktion anpassen
Kommen wir nun zur Funktion loss(). Zur Berechnung des Verlusts hatten wir den mittleren quadratischen Fehler herangezogen:
def loss(X, Y, w):
return np.average((predict(X, w) - Y) ** 2)
Auch habe ich wieder den Bias b herausgenommen, damit wir uns für den Anfang mit einem Problem weniger herumschlagen müssen. Der Bias wird aber in Kürze wieder zurückkehren. Schauen wir uns zunächst aber an, wie wir diese abgespeckte Version von loss() an mehrdimensionale Eingaben anpassen.
Ich weiß noch, wie frustrierend Matrizenoperationen für mich waren, als ich meine ersten ML-Programme schrieb. Insbesondere schienen die Dimensionen der Matrizen nie zueinanderzupassen. Mit der Zeit lernte ich jedoch, dass diese Dimensionen in Wirklichkeit eine gute Sache waren, denn wenn ich sie sorgfältig im Auge behielt, halfen sie mir sogar dabei, meinen Code zu schreiben. Auch hier wollen wir die Dimensionen der Matrizen als Richtschnur für unseren Code nutzen.
Fangen wir bei den Labels an. Im vorstehenden Code haben wir zwei Matrizen für Labels: Y mit der Grundwahrheit der Datenmenge sowie die von predict() berechnete Matrix y_hat (Y-Dach!). Beide sind nun (m, 1)-Matrizen, haben also eine Zeile pro Beispiel, aber nur eine einzige Spalte. Da wir 30 Beispiele haben, sind es in unserem Fall (30, 1)-Matrizen. Wenn wir Y von y_hat subtrahieren, prüft NumPy zunächst, ob beide Matrizen die gleiche Größe haben, und subtrahiert dann jedes Element von Y vom entsprechenden Element von y_hat. Das Ergebnis ist ebenfalls eine (30, 1)-Matrix.
Anschließend wollen wir alle Elemente in dieser Matrix quadrieren. Da Matrizen als NumPy-Arrays implementiert sind, können wir eine Besonderheit von NumPy nutzen, die als Broadcasting bezeichnet wird. Sie kam sogar schon in früheren Kapiteln zum Tragen. Wenn wir eine arithmetische Operation auf ein NumPy-Array anwenden, dann wird sie wie von einem Rundfunksender an alle Elemente des Arrays übertragen. Das heißt also, wenn wir die gesamte Matrix quadrieren, wendet NumPy diese Operation pflichteifrig auf alle Elemente der Matrix an. Auch das Ergebnis dieses Vorgangs ist wieder eine (30, 1)-Matrix.
Schließlich rufen wir noch average() auf, um den Durchschnitt aller Elemente in der Matrix zu bilden. Diese Funktion gibt einen einzelnen Skalar zurück:
a_number = loss(X, Y, w)
a_number.shape # => ()
Das leere Klammernpaar ist die NumPy-Schreibweise für: »Dies ist ein Skalar, hat also keine Dimensionen.«
Fazit: Wir müssen die Berechnung des Verlusts überhaupt nicht ändern. Unsere Code für den mittleren quadratischen Fehler funktioniert bei mehreren Eingabevariablen genauso wie bei einer einzigen Variablen.
Die Gradientenfunktion anpassen
Jetzt bleibt nur noch eines zu tun: Wir müssen den Verlustgradienten auf mehrere Variablen erweitern. Ich gehe hier gleich in die Vollen und zeige Ihnen die Matrizenversion der Funktion gradient():
def gradient(X, Y, w):
return 2 * np.matmul(X.T, (predict(X, w) - Y)) / X.shape[0]
X.T bedeutet »X transponiert« (siehe »Matrizen transponieren« auf Seite 65).
Siehe »Of Gradients and Matrices« auf ProgML.
Es hat mich einiges an Zeit gekostet, um von der alten Version von gradient() zu dieser zu kommen. Ausnahmsweise werde ich hier nicht auf die Einzelheiten der Funktion eingehen, weil sie einfach zu viel Platz einnehmen würden. Wenn Sie neugierig sind, können Sie auf ProgML2, der Begleitwebsite zu diesem Buch, mehr darüber erfahren. Sie können sich auch selbst vergewissern, dass sich diese neue Version von gradient() genauso verhält wie die alte, aber mehrere Eingabevariablen entgegennimmt.
Der Code im Ganzen
Prüfen wir, ob wir alle Einzelteile zusammenhaben:
Wir haben den Code zur Aufbereitung der Daten geschrieben.
Wir haben predict() angepasst.
Wir haben festgestellt, dass wir loss() nicht anpassen müssen.
Wir haben gradient() angepasst.
Es ist alles erledigt. Damit können wir nun unser ML-Programm wie folgt umschreiben:
04_hyperspace/multiple_regression_without_bias.py
import numpy as np
def predict(X, w):
return np.matmul(X, w) <
def loss(X, Y, w):
return np.average((predict(X, w) - Y) ** 2)
def gradient(X, Y, w):
return 2 * np.matmul(X.T, (predict(X, w) - Y)) / X.shape[0] <
def train(X, Y, iterations, lr):
w = np.zeros((X.shape[1], 1)) <
for i in range(iterations):
print("Iteration %4d => Loss: %.20f" % (i, loss(X, Y, w)))
w -= gradient(X, Y, w) * lr
return w
x1, x2, x3, y = np.loadtxt("pizza_3_vars.txt", skiprows=1, unpack=True) <
X = np.column_stack((x1, x2, x3)) <
Y = y.reshape(-1, 1) <
w = train(X, Y, iterations=100000, lr=0.001) <
Wir haben eine ganze Reihe von Seiten gebraucht, um fertig zu werden, und doch ist der Code dem aus dem vorherigen Kapitel ziemlich ähnlich. Abgesehen von dem Teil, in dem wir die Daten laden und aufbereiten, mussten wir nur drei Zeilen ändern. Die Funktionen sind jetzt allgemein nutzbar: Sie können nicht nur Robertos Datenmenge mit drei Variablen verarbeiten, sondern mit Eingabevariablen beliebiger Anzahl umgehen.
Wenn wir dieses Programm ausführen, erhalten wir folgende Ausgabe:
Iteration 0 => Loss: 1333.56666666666660603369
Iteration 1 => Loss: 151.14311361881479456315
Iteration 2 => Loss: 64.99460808656147037254
...
Iteration 99999 => Loss: 6.89576133146784187034
Der Verlust wird bei jedem Durchlauf kleiner, was schon ein Hinweis darauf ist, dass das Programm tatsächlich lernt. Allerdings ist unsere Arbeit noch nicht getan: Schließlich haben wir zu Anfang den Bias-Parameter entfernt, um die Sache zu vereinfachen, wobei wir ohne den Bias jedoch keine genauen Vorhersagen erwarten dürfen. Zum Glück ist es viel einfacher, den Bias wieder einzuführen, als es den Anschein hat.
Bye-bye, Bias!
Im letzten Abschnitt haben wir die folgende Vorhersageformel implementiert:
ŷ = x1 * w1 + x2 * w2 + x3 * w3
Jetzt wollen wir aber den Bias wieder in unser System aufnehmen:
ŷ = x1 * w1 + x2 * w2 + x3 * w3 + b
Wir könnten jetzt einfach den Bias wie im vorherigen Kapitel wieder überall in den Code einfügen. Allerdings gibt es einen Trick, mit dem wir ihn auf weit weniger aufwendige Weise einfließen lassen können.
Schauen Sie sich die vorstehende Formel noch einmal genau an. Was ist der Unterschied zwischen dem Bias und den Gewichten? Die Gewichte werden mit einer Eingabevariablen multipliziert, der Bias dagegen nicht. Wenn wir nun in unserem System eine weitere Eingabevariable namens x0 hätten, die stets den Wert 1 hat, so könnten wir die Formel wie folgt schreiben:
ŷ = x1 * w1 + x2 * w2 + x3 * w3 + x0 * b
Jetzt gibt es überhaupt keinen Unterschied mehr zwischen dem Bias und den Gewichten. Der Bias ist einfach nur das Gewicht einer Eingabevariablen mit dem konstanten Wert 1. Der Trick, den ich eben erwähnt habe, besteht darin, eine Pseudo-Eingabevariable hinzuzufügen, die stets 1 ist. Dann brauchen wir den expliziten Bias nicht mehr.
Wir könnten dazu in die Datei pizza_3_vars.txt wie folgt eine Spalte mit lauter Einsen aufnehmen:
Allerdings ist es immer besser, nicht in den ursprünglichen Daten herumzupfuschen. Stattdessen fügen wir die Spalte mit den Einsen in X ein, nachdem wir die Daten geladen haben. Die Position dieser Biasspalte spielt keine Rolle, allerdings wird sie vereinbarungsgemäß meistens an erster Stelle eingefügt:
04_hyperspace/multiple_regression_final.py
x1, x2, x3, y = np.loadtxt("pizza_3_vars.txt", skiprows=1, unpack=True)
X = np.column_stack((np.ones(x1.size), x1, x2, x3)) <
Y = y.reshape(-1, 1)
w = train(X, Y, iterations=100000, lr=0.001)
print("\nWeights: %s" % w.T) <
print("\nA few predictions:") <
for i in range(5): <
print("X[%d] -> %.4f (label: %d)" % (i, predict(X[i], w), Y[i])) <
Ich habe diese Gelegenheit genutzt, dem Programm noch einige print-Befehle beizufügen. Sie geben zunächst die Gewichtsmatrix aus (transponiert, sodass sie in eine Zeile passt) und dann die vorhergesagten Werte und die Labels der ersten fünf Beispiele, sodass wir sie vergleichen können.
Damit ist unser Programm für multiple lineare Regression fertig. Zeit für einen letzten Test!
Ein letzter Testlauf
Wenn wir das Programm ausführen, erhalten wir folgende Ausgabe:
Iteration 0 => Loss: 1333.56666666666660603369
Iteration 1 => Loss: 152.37148173674077611395
...
Iteration 99999 => Loss: 6.69817817063803833122
Weights: [[ 2.41178207 1.23368396 -0.02689984 3.12460558]]
A few predictions:
X[0] -> 45.8717 (label: 44)
X[1] -> 23.2502 (label: 23)
X[2] -> 28.5192 (label: 28)
X[3] -> 58.2355 (label: 60)
X[4] -> 42.8009 (label: 42)
Werfen wir zunächst einen Blick auf den Verlust. Wie erwartet ist er niedriger als bei der Version ohne Bias.
Eine Betrachtung der Gewichte ist sehr aufschlussreich. Bei dem ersten Gewicht handelt es sich lediglich um den Bias, den wir durch den »Einserspalten-Trick« zu einem regulären Gewicht gemacht haben. Die restlichen Gewichte dagegen sind diejenigen für die drei Eingabevariablen, also für Reservierungen, Temperatur und Touristendichte. Die Touristendichte hat ein sehr hohes Gewicht, die Temperatur dagegen ein ziemlich kleines. Das zeigt, dass die Pizzaverkäufe sehr stark von der Anzahl der Touristen beeinflusst werden, aber kaum mit dem Wetter schwanken.
Die letzten Zeilen der Ausgabe zeigen die Vorhersagen und Labels für die ersten fünf Beispiele. Keine der Vorhersagen weicht um mehr als eine oder zwei Pizzas ab. Roberto scheint recht gehabt zu haben: Die Erweiterung auf mehrere Variablen hat unsere Vorhersagefähigkeit verbessert.
Herzlichen Glückwunsch – damit haben Sie das schwerste Kapitel in diesem Buch durchgearbeitet! Fassen wir kurz zusammen, was Sie hier gelernt haben.
Zusammenfassung
In diesem Kapitel ging es um multiple lineare Regression. Wir haben unser Programm auf Datenmengen mit mehr als einer Eingabevariablen erweitert. Dazu verwenden wir jetzt auch mehrere Gewichte, nämlich jeweils für die einzelnen Variablen. Außerdem haben wir den expliziten Bias in ein weiteres Gewicht umgewandelt. Unser ML-Programm ist jetzt leistungsfähig genug, um damit auch Probleme aus der Praxis anzugehen, obwohl es nicht komplizierter aussieht als zuvor.
Des Weiteren haben Sie auch die Matrizenmultiplikation und die Transposition kennengelernt. Diese mathematischen Operationen sind für ML von großer Bedeutung. Diese Kenntnisse werden Ihnen noch viele Jahre lang von Nutzen sein.
In diesem Kapitel haben wir uns auch intensiver mit NumPy beschäftigt. Mich persönlich verbindet eine Hassliebe mit dieser Bibliothek: Während ich ihre Vielseitigkeit schätze, verwirrt mich ihre Schnittstelle. Nichtsdestoweniger ist NumPy ein unverzichtbares Werkzeug für ML, weshalb es wichtig ist, sich damit vertraut zu machen. Wir werden diese Bibliothek im ganzen Buch verwenden.
Es ist jetzt an der Zeit, neue Weichen zu stellen. Alles, was Sie in den ersten Kapiteln kennengelernt haben, waren lediglich die Grundlagen für etwas ganz anderes und viel Faszinierenderes. Im nächsten Kapitel werden wir den ausgetretenen Pfad der linearen Regression verlassen und einen weniger stark befahrenen Weg einschlagen – den Weg zum maschinellen Sehen.
Praktische Übung: Statistik in der Praxis
Da Sie nun ein Programm haben, das mit mehreren Variablen umgehen kann, sollten Sie versuchen, es anhand von echten Daten aus der Praxis auszuprobieren. Im Verzeichnis data/life-expectancy im Begleitmaterial zu diesem Buch finden Sie eine Datenmenge, die die Lebenserwartung in verschiedenen Ländern angibt.
Dasselbe Verzeichnis enthält auch die Datei readme.txt mit zusätzlichen Informationen, darunter einem Experiment, das sie anhand dieser Datenmenge durchführen können. Dabei sollen Sie das Programm für multiple lineare Regression mit diesen Daten trainieren und seine Vorhersagen mit der Grundwahrheit vergleichen.
Viel Spaß beim Herumspielen mit echten Daten!