Updates dieser Seite:
Wir lernen die grundlegenden Konzepte Neuronaler Netze für Klassifikationsprobleme anhand des Perzeptrons kennen. Wir sehen uns die Berechnung des Outputs an und insbesondere den Lernschritt und das Lernverfahren. Das Lernverfahren leiten wir auch mit Hilfe von Gradientenabstieg her. Wir nehmen eine eigene Implementierung in Python vor, um die Verarbeitung der Daten und die Umsetzung des Lernverfahrens zu verstehen. Dann führen wir das Python-Framework Keras (innerhalb von TensorFlow) ein und lernen, wie man dort Netze erstellt und trainiert.
Nutzen Sie die Lernziele, um Ihr Wissen zu überprüfen:
Iris
import numpy as np
import matplotlib.pyplot as plt
Nachdem wir gesehen haben, wie wir mit Regression einen Wert vorhersagen können, kommen wir jetzt zur Frage, wie man eine Klasse, Kategorie oder Label vorhersagt. Genauer gesagt: Wir suchen zu einem Feature-Vektor die dazugehörige Klasse oder Kategorie. Ein klassisches Beispiel ist die Klassifikation von e-Mails in eine der zwei Klassen SPAM oder nicht-SPAM. Bei zwei möglichen Klassen spricht man von binärer Klassifikation.
Wir nutzen das Szenario der binären Klassifikation, um unser erstes künstliche Neuronales Netz einzuführen, das Perzeptron.
Formal haben einen Datensatz von $N$ gelabelten Beispielen $(x^{k}, y^{k})$ mit $k = 1, \ldots, N$. Das $x^k \in \mathbb{R}^n$ ist ein $n$-dimensionaler Feature-Vektor. Im Fall der binären Klassifikation ist der Ausgabewert $y \in \{0, 1\}$ ein binärer Wert, z.B. SPAM (= 1) oder nicht-SPAM (= 0).
Zwei klassische Beispiele sind
Künstliche Neuronale Netze sind inspiriert von biologischen Gehirnen, z.B. das des Menschen oder das der Katze. Die wichtigsten Bestandteile eines biologischen Gehirns sind die Gehirnzellen, auch Neuronen genannt, und die Verbindungen zwischen den Neuronen. Ein wichtiger Teil einer Verbindung ist die Synapse.
Das menschliche Gehirn besteht aus etwa 86 Milliarden Neuronen und ca. $10^{14}$ (100 Trillionen) Verbindungen, also Synapsen. Zum Vergleich: ein Schimpanse hat 7 Milliarden, eine Katze 250 Millionen, eine Fruchtfliege 100 Tausend Neuronen.
Wir beschäftigen uns hier kurz mit dem Übergang vom biologischen zum künstlichen Neuron.
Die Zahl der Neuronen stammt aus Herculano-Houzel (2009).
Die Neuronen sind die atomaren Einheiten des Gehirns. Neuronen empfangen elektrische Signale über Dendriten und laden sich gewissermaßen auf, man spricht vom Aktionspotential oder Erregung. Erst wenn ein bestimmter Schwellwert erreicht ist, "feuert" das Neuron über das Axon ein Signal nach außen. Beim Feuern wird eine (elektrische) Erregung von einem Neuron auf ein anderes übertragen.
Die Erregung eines Neurons wird über sein Axon weitergegeben und über Synapsen auf die Dendriten anderer Neuronen weitergegeben. Eine Synapse bezeichnet eine Stelle mit einer physikalischen Lücke, die durch Neurotransmitter - das sind chemische Botenstoffe - überbrückt werden. Neurotransmitter können das Signal verstärken (exzitatorisch) oder hemmen (inhibitorisch). Diese Veränderbarkeit der Informationsübertragung spiegelt sich in den Gewichten künstlicher Neuronaler Netze wider.
Das ist natürlich eine stark vereinfachte Darstellung der Funktionsweise biologischer Neuronen. Der Wikipedia-Artikel über die Nervenzelle geht etwas mehr ins Detail.
Zu den am besten erforschten Regionen im Gehirn gehört der visuelle Cortex, auf den wir im Kapitel über Konvolutionsnetze noch sprechen werden. Ein besonders interessantes ungeklärtes Phänomen im Gehirn ist die Frage, wie verschiedene Hirnareale sich untereinander koordinieren. Eine Hypothese ist, dass die Frequenz des implusartigen Feuerns der Neuronen damit zusammenhängt. Hier sei der kurze Wikipedia-Artikel zur Functional integration) empfohlen. Dies ist auch ein gutes Beispiel für ein Phänomen, das bislang nicht in künstlichen Neuronalen Netzen abgebildet ist.
Bereits 1943 schlugen Warren McCulloch und Walter Pitts ein informationstechnisches Modell des biologischen Neurons vor: das McCulloch-Pitts-Neuron (McCulloch & Pitts, 1943). Das Modell verarbeitete eingehende Signale durch Aufsummieren und kontrollierte die Weiterleitung durch einen Schwellwert. Es wurde gezeigt, dass die grundlegenden booleschen Operatoren (AND, OR, NOT) realisiert werden können. Ein Aspekt, der noch nicht vorhanden war, war die Frage, wie Lernen funktioniert.
Das Perzeptron (engl. perceptron) hatte bereits ein verfeinertes Modell und vor allem einen Lernalgorithmus (allerdings nur für ein Perzeptron mit einer Schicht). Es wurde 1958 von Frank Rosenblatt in einer Publikation der Öffentlichkeit vorgestellt (Rosenblatt, 1958). Er hatte es bereits 1957 in einem technischen Bericht beschrieben. 1969 gewann das Perzeptron größere Bekanntheit durch eine Buchpublikation der KI-Pioniere Marvin Minsky und Seymour Papert (Minsky & Papert, 1969). Durch das Buch wurden auch die Grenzen des Perzeptrons (sogenanntes XOR-Problem) bekannt. Das Adaline (ADAptive Linear NEuron) wurde 1960 von Bernard Widrow eingeführt (Widrow, 1960). Es handelt sich um eine Parallelentwicklung zu Rosenblatts Perzeptron (laut Widrow & Lehr, 1990) und unterscheidet sich lediglich durch eine andere Aktivierungsfunktion, die im Gegensatz zur Stufenfunktion beim Perzeptron differenzierbar ist.
Im weiteren sprechen wir einfach von Neuronalen Netzen (kurz NN) und meinen damit künstliche Neuronale Netze.
Auch wenn künstliche NN von biologischen Neuronen inspiriert sind, ist es doch wichtig festzuhalten, dass die Funktionsweise eines künstlichen NN wenig Rückschlüsse auf das menschliche Denken oder auf die Funktionsweise des menschlichen Hirns zulässt. In einem natürlichen Neuron spielen viele Faktoren (biologische, chemische, physikalische) eine Rolle, die in einem künstlichen Neuronalen Netz nicht modelliert sind. Das Lernverfahren "Backpropagation", mit dem künstliche NN trainiert werden, hat wenig mit biologischen Vorgängen zu tun. Mit der Frage, inwiefern menschliches Denken mit Hilfe von informationstechnischen Modellen erforscht werden kann, beschäftigt sich die Kognitionswissenschaft. Für Modelle, die von biologischen neuronalen Netzen inspiriert sind, hat sich der Konnektionismus als Unterdisziplin der Kognitionswissenschaften herausgebildet (sehr guter Wikipedia-Artikel).
Das Perzeptron ist nach dem McCulloch-Pitts-Neuron der erste Versuch, einen lernendes Netzwerk nach dem Vorbild biologischer Neuronen zu erstellen. Es wurde von Frank Rosenblatt entwickelt und 1958 der Öffentlichkeit vorgestellt. Stark verwandt mit dem Perzeptron ist das ADALINE-Netz von Widrow (1960). Die Inhalte dieses Kapitels sind eine Kombination beider Verfahren. Wir nennen das Netz aber der Einfachheit halber immer Perzeptron.
Das Perzeptron ist ein Netzwerk aus Neuronen und gerichteten Verbindungen. Die Neuronen sind in Schichten (engl. layers) organisiert, der Eingabeschicht und der Ausgabeschicht. Die Verbindungen laufen von Eingabeschicht zu Ausgabeschicht. Ein solches Netz ist somit ein gerichteter azyklischer Graph (engl. directed acyclic graph, kurz DAG).
Die Ausgabeschicht besteht aus einem einzigen Neuron. Das Netz kann zur binären Klassifikation verwendet werden, d.h. für einen Input $x$ (wir nennen dies auch Featurevektor, z.B. die Pixel eines Bildes) kann das Netz entscheiden, ob dieser Input zu einer bestimmten Kategorie gehört (Katzenbild) oder nicht (kein Katzenbild). Dies wird entsprechend mit dem Wert $y \in \{0, 1\}$ ausgedrückt.
Hinweis: Manchmal wird das Perzeptron auch mit der Outputmenge $y \in \{-1, 1\}$ eingeführt.
Der Wert $z$ ist eine "Zwischenberechnung", der so genannte Rohinput, den wir gleich erklären.
Wir haben gerade von "Schichten" gesprochen. Es zeigt sich, dass es sinnvoll ist, eine Schicht so zu definieren, dass eine Schicht mehrere Parameter/Gewichte enthält. In diesem Sinne hat das Perzeptron nur eine Schicht. Die Eingabe zählt nicht als eigenständige Schicht.
Der Input durch die Eingabeneuronen wird repräsentiert durch einen Vektor $x = (x_1, \ldots, x_n)$ der Länge $n$, wobei jedes einzelne Feature $x_i \in \mathbb{R}$. Auch wenn wir im Text Vektoren oft als Zeilenvektor schreiben, ist in Berechnungen immer ein Spaltenvektor gemeint, d.h. Vektor $x$ sieht so aus:
$$ x = \left( \begin{array}{c} x_1 \\ \vdots \\ x_n \end{array} \right) $$Der gewünschte Output am Ausgabeneuron ist ein Skalar $y$, das entweder 0 oder 1 ist.
Die Parameter des Perzeptrons nennen wir Gewichte (engl. weights). Sie sind ein Vektor $w$, wobei $w_i \in \mathbb{R}$. Auch hier gilt in den Berechnungen, dass es sich um einen Spaltenvektor handelt:
$$ w = \left( \begin{array}{c} w_1 \\ \vdots \\ w_n \end{array} \right) $$Wenn wir an den Eingabeneuronen Werte in Form des Vektors $x$ anlegen, wie wird der Output $y$ berechnet? Diese Verarbeitungsrichtung nennt man auch Forward Propagation.
In einem ersten Schritt berechnen wir für einen gegebenen Featurevektor $x$ die Roheingabe $z$ (engl. net input) des Ausgabeneurons (siehe Abb. oben). Dies ist die Summe der Werte der Eingabeneuronen, jeweils multipliziert mit den jeweiligen Gewichten.
$$ \begin{align*} z & = \sum_{i=1}^n w_i \: x_i\\[3mm] &= w_1 x_1 + \ldots + w_n x_n \end{align*} $$In Vektorform können wir die Vektoren $x$ und $w$ wie folgt multiplizieren:
$$ \begin{align*}\tag{Z} z & = w^T x\\[2mm] &= (w_1, \ldots, w_n) \left( \begin{array}{c} x_1 \\ \vdots \\ x_n \end{array} \right) \\[3mm] &= w_1 x_1 + \ldots + w_n x_n \end{align*} $$Vektor $w^T$ ist dabei der transponierte Vektor von $w$ und daher ein Zeilenvektor. Nur so ist die Multiplikation zulässig (wir sehen hier alles als Matrizenmultiplikation).
Im zweiten Schritt berechnen wir die Aktivierung $y$ des Ausgabeneurons. Dies ist gleichzeitig der Gesamtoutput des Neuronalen Netzes.
Die Aktivierung wird berechnet, indem wir eine Aktivierungsfunktion $g$ auf die Roheingabe $z$ anwenden.
$$ \tag{Y} y = g(z) $$Für $g$ verwenden wir die sogenannte Heaviside- oder Stufenfunktion (auch step oder threshold function):
$$ g_\theta(z) = \begin{cases} 1 & \quad \text{falls } z \geq \theta \\[3mm] 0 & \quad \text{sonst} \end{cases} $$wobei $\theta$ (griech. Buchstabe Theta) auch der Schwellwert (engl. threshold) genannt wird.
Wenn der Schwellwert $\theta = 0$ ist, sieht die Aktivierungsfunktion so aus:
x = [-10, -5, 0, 0, 5, 10]
y = [0, 0, 0, 1, 1, 1]
plt.plot(x,y,'r')
plt.xlabel('x')
plt.ylabel('y')
plt.title('Heaviside-Aktivierungsfunktion')
plt.xticks([-10,-5,0,5,10])
plt.yticks([0,.5,1])
plt.grid()
plt.show()
Diese Funktion ist nicht differenzierbar bei $x = 0$.
Das Perzeptron kann grundlegende logische Operationen nachbilden. Wir können etwa den logischen Operator AND mit einem Perzeptron modellieren:
Bei einem AND haben wir einen sehr überschaubaren Datensatz:
x1 | x2 | y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
Ein Perzeptron mit den folgenden Parametern und Schwellwert leistet genau die gewünscht Operation:
$$ \begin{align*} w_1 &= 1\\ w_2 &= 1\\ \theta &= 2 \end{align*} $$Ähnlich wie mit dem AND verhält es sich mit dem OR. Hier der Datensatz:
x1 | x2 | y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
Im Vergleich zum AND-Perzeptron kann man z.B. einfach den Schwellwert absenken:
$$ \begin{align*} w_1 &= 1\\ w_2 &= 1\\ \theta &= 1 \end{align*} $$Es gibt natürlich noch weitere Lösungen für AND und OR.
Wenn Sie probieren, das XOR mit diesem Netz zu lösen, werden Sie feststellen, dass es nicht geht. Hier der dazugehörige Datensatz:
x1 | x2 | y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Dies zeigten auch 1969 Marvin Minsky und Seymour Papert in ihrem Buch Perceptrons: an introduction to computational geometry).
Man benötigt für das XOR eine weitere Schicht. Probieren Sie doch mal, ein solches Netz zu entwerfen. Vergessen Sie nicht, Gewichte und Schwellwert anzugeben.
Der Schwellwert $\theta$ ist etwas umständlich für weitere Berechnungen. Deshalb versuchen wir, ihn leicht zu verschieben.
Unsere Aktivierungsfunktion $g$ prüft, ob
$$ z = w_1 x_1 + \ldots + w_n x_n \geq \theta $$Jetzt können wir das $\theta$ auf die andere Seite holen:
$$ -\theta + w_1 x_1 + \ldots + w_n x_n \geq 0 $$Wir erweitern jetzt einfach die Vektoren $x$ und $w$ um jeweils eine Stelle mit Index $0$. Dabei ist $x_0 = 1$ und $w_0 = -\theta$:
$$ w_0 x_0 + w_1 x_1 + \ldots + w_n x_n \geq 0 $$Man nennt das Neuron $x_0$, das immer gleich $1$ ist, auch das Bias-Neuron.
Jetzt können wir die Funktion $g$ immer mit Null als Schwellwert formulieren:
$$ g(z) = \begin{cases} 1 & \quad \text{falls } z \geq 0 \\ 0 & \quad \text{sonst} \end{cases} $$Die Formeln (Z) und (Y) können wir einfach so beibehalten in dem Wissen, dass die Komponente 0 in $x$ immer gleich eins ist.
Unser Netz sieht mit Bias-Neuron jetzt so aus:
Lernen bedeutet, dass die Gewichte $w$ mit Hilfe von Trainingsdaten schrittweise angepasst werden, so dass unser Netzwerk sich der gewünschten (idealen) Funktion $h^*$ annähert.
Trainingsdaten sind z.B. eine Reihe von Bildern mit Label (z.B. Katze und Nicht-Katze). Wir gehen von $N$ Traningsdaten aus. Für $N$ Traningsdaten schreiben wir die Paare von Featurevektor und Label so:
$$(x^k, y^k) \quad\quad k \in \{1,\ldots , N\}$$Bei einem Neuronalen Netz müssen wir zwischen dem berechneten Output des aktuellen Netzwerks und dem korrekten Output eines Traningsbeispiels unterscheiden.
Wir schreiben:
Sowohl $y$ als auch $\hat{y}$ sind aus der Menge $\{0,1\}$.
Lernen bedeutet, dass wir für jedes Trainingsbeispiel die Ausgabe $\hat{y}$ berechnen und anschließend die Gewichte anpassen. Man nennt dies einen Lernschritt.
Für ein konkretes Trainingsbeispiel $(x^k, y^k)$ passen wir alle Gewichte $w = (w_0, \ldots, w_n)$ an, indem wir zu jedem $w_i$ ein $\Delta w_i$ addieren:
$$ w_i := w_i + \Delta w_i $$Dies gilt für die meisten Neuronalen Netze. Der Unterschied besteht in der Berechnung des Delta.
Beim Perzeptron berechnet sich das Delta wie folgt:
$$ \Delta w_i := \alpha \, (y^k - \hat{y}^k ) x_i^k $$wobei $\alpha \in [0, 1]$ die Lernrate ist.
Überlegen wir uns, ob diese Regel plausibel ist:
Man beachte, dass für das Gewicht des Bias-Neurons $x_0$ gilt:
$$ \Delta w_0 := \alpha \, (y^k - \hat{y}^k ) $$da immer $x_0 = 1$.
Eine zu hohe Lernrate kann dazu führen, dass das Optimum immer wieder "übersprungen" wird, so dass das Lernen doch wieder langsamer wird oder sogar nie das Optimum erreicht. Eine zu niedrige Lernrate kann zu sehr langsamen Lernprozessen führen. Als Daumenregel sollte man mit niedrigen Lernraten beginnen und diese dann schrittweise erhöhen. Erfahrungsgemäß funktionieren Lernraten zwischen $0.1$ und $0.3$ gut als Startpunkt.
Jetzt formulieren wir den Lernalgorithmus in einer ersten Version:
Man beachte, dass die Gewichte immer nach der Verarbeitung eines einzelnen Trainingsbeispiels angepasst werden.
Wenn wir die Formel unten per Gradientenabstieg herleiten, passen wir diesen Algorithmus noch einmal an (siehe Lernalgorithmus II).
Die Perzeptron-Lernregel ist nicht wirklich hergeleitet, Sie wurde Ihnen einfach vorgestellt und sie erscheint plausibel. Jetzt fragen wir uns: Gibt es eine mathematische Herleitung für den Lernalgorithmus? Eine solche Herleitung würde uns nicht nur zeigen, dass auch wirklich Lernen stattfindet, sondern gibt uns potentiell auch für komplexere Netze ein "Rezept", mit dem wir Lernen errreichen können.
Unsere Herleitung folgt der Idee des Gradientenabstieg und dieses Verfahren bedeutet etwas ganz Einfaches: Wenn wir einen Parameter $w$ haben und diesen leicht vergrößern, wird dann der Fehler kleiner oder größer? Diese Frage wird genau mit dem Gradienten beantwortet und erlaubt uns die zielgerichtete Anpassung von $w$.
Für unsere Herleitung ist es leichter, wenn wir als Aktivierungsfunktion $g$ die Identität nehmen, also:
$$ \tag{A} g(z) = z $$Sie erinnern sich: Das originale Perzeptron hat hier die Heaviside-Funktion. Der entscheidende Unterschied ist, dass die Identitäts-Funktion differenzierbar ist, d.h. man kann eine Ableitung bilden. Dies ist für das Verfahren des Gradientenabstiegs wesentlich.
Hinweis: Man beachte, dass die berechnete Ausgabe $\hat{y} = g(z) = z$ jetzt eine Dezimalzahl $\in \mathbb{R}$ ist, da wir den Rohinput einfach "durchschleifen".
Dazu müssen wir die Abweichung zwischen den berechneten Ouputs und den korrekten Outputs berechnen. Dies tut man über eine Zielfunktion $J$. Diese wird auch Fehlerfunktion genannt, im Englischen meistens loss function.
Hinweis: Wir folgen hier der Literatur, wo die Fehlerfunktion sehr oft mit dem Buchstaben $J$ repräsentiert wird. Das hängt vermutlich mit dem Konzept der Jacobi-Matrix zusammen. Die Jacobi-Matrix einer Funktion $f$ enthält alle partiellen Ableitungen von $f$. Insofern ist die Benennung der Fehlerfunktion mit $J$ nicht ganz logisch, weil wir später die Jacobi-Matrix von $J$ betrachten und diese dann $\nabla J$ heißt.
Für unser $J$ wählen wir den Mittelwert der Fehlerquadrate (engl. mean of squared errors oder MSE). Der Faktor $\frac{1}{2}$ dient nur der Kosmetik (weil sich die 2 im Nenner bei der Ableitung rauskürzt) und beeinträchtigt nicht den Nutzen von $J$:
$$ \tag{J} J(w) = \frac{1}{2N} \sum_{k=1}^N \left( y^k - g(z^k) \right)^2 $$Vergegenwärtigen Sie sich, dass dieser Fehler in der Hauptsache davon abhängt, in welcher "Konfiguration" sich das Netz befindet, also wie die Gewichte $w$ eingestellt ist. Wir sind daran interessiert, die Gewichte so einzustellen, dass der Gesamtfehler $J$ möglichst klein wird. Im Training soll das automatisiert ablaufen.
Diese Abhängigkeit von $w$ zu $J$ spannt eine Fehlerlandschaft auf. Bei den folgenden beiden Beispielen kann man sich zwei beliebige Gewichte (z.B. $w_1$ und $w_2$) als 2-dimensionale Ebene vorstellen, wohingegen der Fehlerwert $J$ nach oben zeigt. Da wir einen möglichst geringen Fehler anstreben, suchen wir also nach dem tiefsten Tal.
Jetzt kommt der Gradientenabstieg zum Zug. Wir wollen die Gewichte so anpassen, dass der Fehler sich verringert. Dazu gehen wir in Richtung des negativen Gradienten, denn der Gradient gibt uns den größten Anstieg. Den Verlauf der Updates kann man sich anhand der Fehlerlandschaft so vorstellen:
Die Update-Regel bleibt gleich, wir sehen hier Vektoren statt der Komponenten:
$$ w := w + \Delta w $$Das Delta definieren wir jetzt aber über den Gradienten, genauer gesagt als negativen Gradienten des Fehlers $J$, modifiziert durch die Lernrate $\alpha$:
$$ \Delta w := - \alpha \nabla J(w) = - \alpha \left( \begin{array}{c} \frac{\partial J}{\partial w_1} (w) \\ \vdots \\ \frac{\partial J}{\partial w_n} (w) \end{array} \right) $$Hinweis: Das $w$ in $ - \alpha \nabla J(w) $ deutet an, dass wir in die Ableitungen immer konkrete Werte einsetzen, nämlich die aktuelle "Konfiguration" des Netzwerks in Form der Gewichte $w$, um das $\Delta w$ zu berechnen. Die Ableitungen $\frac{\partial J}{\partial w_i} (w)$ sind ja auch Funktionen mit Parameter $w$ und werden an der aktuellen Stelle $w$ evaluiert.
Wie man in der Gleichung oben sieht, benötigen wir für $\nabla J$ alle partiellen Ableitungen von $J$ hinsichtlich der Einzelgewichte $w_1, \ldots, w_n$.
Für eine einzelne partielle Ableitung des Gradienten gilt:
$$ \tag{G} \frac{\partial J}{\partial w_i} (w) = - \frac{1}{N} \sum_{k=1}^N \left( y^k - g(z^k) \right) x_i^k $$Woher kommt diese Formel? Die Herleitung für (G) zeigen wir weiter unten.
Für ein Gewicht $w_i$ sieht das Delta entsprechend wie folgt aus:
$$ \begin{align*} \Delta w_i &= - \alpha \frac{\partial J}{\partial w_i} (w) \\[1mm] &= \alpha \frac{1}{N} \sum_{k=1}^N \left( y^k - g(z^k) \right) x_i^k \\[1mm] &= \alpha \frac{1}{N} \sum_{k=1}^N \left( y^k - \hat{y}^k \right) x_i^k \end{align*} $$Die Update-Regel sieht genauso aus wie zuvor, nur dass wir die Regel über den Gradientenabstieg hergeleitet haben und dass wir hier davon ausgehen, dass wir erst nach Abarbeiten aller Trainingsdaten ein Update durchführen. Das heißt übrigens, dass wir die Deltas für jedes Samples aufaddieren müssen, um dann am Schluss das finale Delta auf ein Gewicht anzuwenden.
Angesichts unserer Herleitung passen wir den Lernalgorithmus I von oben an. Man beachte, dass wir das $\Delta w_i$ innerhalb einer Epoche als Speicher benutzen, um alle Deltas für alle Trainingsdaten "einzusammeln".
Der Unterschied zum Lernalgorithmus beim originalen Perzeptron ist, dass erst alle Trainingsdaten durchlaufen werden, bevor die Gewichte angepasst werden.
Gesucht: Wir suchen alle partiellen Ableitungen von $J$, also $\frac{\partial J}{\partial w_i}$ für alle $i\in {0, \ldots, n}$.
Ausgangspunkt ist die Fehlerfunktion (J), die wir erstmal umformen.
$$ J(w) = \frac{1}{2N} \sum_{k=1}^N \left( y^k - g(z^k) \right)^2 $$Da $g$ die Identitätsfunktion ist (A):
$$ J(w) = \frac{1}{2N} \sum_{k=1}^N \left( y^k - z^k \right)^2 $$Jetzt setzen wir $z^k$ ein, wie in (Z) definiert:
$$ J(w) = \frac{1}{2N} \sum_{k=1}^N \left( y^k - w^T x^k \right)^2 $$Jetzt können wir $\frac{\partial J}{\partial w_i}$ berechnen:
$$ \begin{align*} \frac{\partial J}{\partial w_i} & = \frac{\partial}{\partial w_i} \frac{1}{2N} \sum_{k=1}^N \left( y^k - w^T x^k \right)^2 \\[2mm] &= \frac{1}{2N} \sum_{k=1}^N \frac{\partial}{\partial w_i} \left( y^k - w^T x^k \right)^2\\[2mm] &= \frac{1}{2N} \sum_{k=1}^N 2 \left( y^k - w^T x^k \right) \frac{\partial}{\partial w_i} - w^T x^k\\[2mm] &= - \frac{1}{N} \sum_{k=1}^N \left( y^k - w^T x^k\right) x_i^k \\[2mm] &= - \frac{1}{N} \sum_{k=1}^N \left( y^k - g(z^k) \right) x_i^k \end{align*} $$Damit wäre (G) gezeigt.
Jetzt möchten wir ein Perzeptron in Python programmieren. Dazu nehmen wir einen Datensatz aus der Scikit-learn-Bibliothek. Wir nutzen den beliebten, sehr kleinen Iris-Datensatz, der aus dem Jahr 1936 stammt. Bei diesem Datensatz geht es um Blumen, genauer gesagt um die Gattung der "Schwertlilien", deren lateinischer Name Iris lautet. Der Datensatz besteht nur aus 150 Trainingsbeispielen. Siehe auch: https://en.wikipedia.org/wiki/Iris_flower_data_set
In den Daten geht es um die Unterscheidung von drei Klassen (Pflanzenarten) von Schwertlilien:
Wir nutzen nur die ersten zwei Klassen und haben somit ein binäres Klassifikationsproblem (Klassen 0 und 1).
Als Features wurden vier Eigenschaften der Blüte gemessen ("sepal" ist das Kelchblatt und "petal" das Kronblatt; siehe https://de.wikipedia.org/wiki/Kelchblatt):
Wir nutzen zur Vereinfachung nur zwei Features (Features 0 und 1). Zwei Features kann man besser visualisieren.
In der Sammlung sind 150 Dateneinträge, jeweils 50 pro Klasse. Jeder Eintrag besteht aus einem Inputvektor der Länge 4 und einer Zahl 0, 1, 2 für die Klasse.
Zunächst beziehen wir die Daten:
Wir laden die Daten aus der Bibliothek Scikit-learn.
from sklearn import datasets
iris = datasets.load_iris()
Wir erhalten ein Dictionary (Hashtabelle) und sehen uns die Schlüssel an:
iris.keys()
dict_keys(['data', 'target', 'frame', 'target_names', 'DESCR', 'feature_names', 'filename', 'data_module'])
Die Featurevektoren sind in data, die Labels in der Spalte target.
Wir prüfen den Umfang des Datensatzes:
len(iris.data)
150
Jetzt schauen wir uns die ersten drei Featurevektoren an:
iris.data[:3]
array([[5.1, 3.5, 1.4, 0.2], [4.9, 3. , 1.4, 0.2], [4.7, 3.2, 1.3, 0.2]])
Und werfen noch einen Blick auf die Label, die mit 0, 1, 2 kodiert sind:
iris.target
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
Diese Zahlen bilden den Index für den Array von Klassennamen. Das stellt sicher, dass kein Fehler bei der Zuordnung von Zahl und Klasse unterläuft.
iris.target_names
array(['setosa', 'versicolor', 'virginica'], dtype='<U10')
Das gleiche gilt für die Bezeichnungen und die Reihenfolge der vier Features:
iris.feature_names
['sepal length (cm)', 'sepal width (cm)', 'petal length (cm)', 'petal width (cm)']
Für unser Beispiel möchten wir nur zwei Features verwenden:
Dazu transponieren wir die Daten, dann erhalten wir vier Vektoren mit jeweils allen Daten für ein Feature.
features = iris.data.T
features[0][:50]
array([5.1, 4.9, 4.7, 4.6, 5. , 5.4, 4.6, 5. , 4.4, 4.9, 5.4, 4.8, 4.8, 4.3, 5.8, 5.7, 5.4, 5.1, 5.7, 5.1, 5.4, 5.1, 4.6, 5.1, 4.8, 5. , 5. , 5.2, 5.2, 4.7, 4.8, 5.4, 5.2, 5.5, 4.9, 5. , 5.5, 4.9, 4.4, 5.1, 5. , 4.5, 4.4, 5. , 5.1, 4.8, 5.1, 4.6, 5.3, 5. ])
Jetzt picken wir uns die zwei Features 0 und 2 heraus:
features = features[[0,2]]
features
array([[5.1, 4.9, 4.7, 4.6, 5. , 5.4, 4.6, 5. , 4.4, 4.9, 5.4, 4.8, 4.8, 4.3, 5.8, 5.7, 5.4, 5.1, 5.7, 5.1, 5.4, 5.1, 4.6, 5.1, 4.8, 5. , 5. , 5.2, 5.2, 4.7, 4.8, 5.4, 5.2, 5.5, 4.9, 5. , 5.5, 4.9, 4.4, 5.1, 5. , 4.5, 4.4, 5. , 5.1, 4.8, 5.1, 4.6, 5.3, 5. , 7. , 6.4, 6.9, 5.5, 6.5, 5.7, 6.3, 4.9, 6.6, 5.2, 5. , 5.9, 6. , 6.1, 5.6, 6.7, 5.6, 5.8, 6.2, 5.6, 5.9, 6.1, 6.3, 6.1, 6.4, 6.6, 6.8, 6.7, 6. , 5.7, 5.5, 5.5, 5.8, 6. , 5.4, 6. , 6.7, 6.3, 5.6, 5.5, 5.5, 6.1, 5.8, 5. , 5.6, 5.7, 5.7, 6.2, 5.1, 5.7, 6.3, 5.8, 7.1, 6.3, 6.5, 7.6, 4.9, 7.3, 6.7, 7.2, 6.5, 6.4, 6.8, 5.7, 5.8, 6.4, 6.5, 7.7, 7.7, 6. , 6.9, 5.6, 7.7, 6.3, 6.7, 7.2, 6.2, 6.1, 6.4, 7.2, 7.4, 7.9, 6.4, 6.3, 6.1, 7.7, 6.3, 6.4, 6. , 6.9, 6.7, 6.9, 5.8, 6.8, 6.7, 6.7, 6.3, 6.5, 6.2, 5.9], [1.4, 1.4, 1.3, 1.5, 1.4, 1.7, 1.4, 1.5, 1.4, 1.5, 1.5, 1.6, 1.4, 1.1, 1.2, 1.5, 1.3, 1.4, 1.7, 1.5, 1.7, 1.5, 1. , 1.7, 1.9, 1.6, 1.6, 1.5, 1.4, 1.6, 1.6, 1.5, 1.5, 1.4, 1.5, 1.2, 1.3, 1.4, 1.3, 1.5, 1.3, 1.3, 1.3, 1.6, 1.9, 1.4, 1.6, 1.4, 1.5, 1.4, 4.7, 4.5, 4.9, 4. , 4.6, 4.5, 4.7, 3.3, 4.6, 3.9, 3.5, 4.2, 4. , 4.7, 3.6, 4.4, 4.5, 4.1, 4.5, 3.9, 4.8, 4. , 4.9, 4.7, 4.3, 4.4, 4.8, 5. , 4.5, 3.5, 3.8, 3.7, 3.9, 5.1, 4.5, 4.5, 4.7, 4.4, 4.1, 4. , 4.4, 4.6, 4. , 3.3, 4.2, 4.2, 4.2, 4.3, 3. , 4.1, 6. , 5.1, 5.9, 5.6, 5.8, 6.6, 4.5, 6.3, 5.8, 6.1, 5.1, 5.3, 5.5, 5. , 5.1, 5.3, 5.5, 6.7, 6.9, 5. , 5.7, 4.9, 6.7, 4.9, 5.7, 6. , 4.8, 4.9, 5.6, 5.8, 6.1, 6.4, 5.6, 5.1, 5.6, 6.1, 5.6, 5.5, 4.8, 5.4, 5.6, 5.1, 5.1, 5.9, 5.7, 5.2, 5. , 5.2, 5.4, 5.1]])
Anschließend transponieren wir das ganze zurück:
iris_x = features.T
iris_x[:5] # Testausgabe
array([[5.1, 1.4], [4.9, 1.4], [4.7, 1.3], [4.6, 1.5], [5. , 1.4]])
Wir beschränken uns auf die ersten 100 Vektoren, da wir nur 2 der 3 Klassen betrachten (jeweils 50 Daten pro Klasse).
iris_x = iris_x[:100] # nur die ersten 100 Elemente
Jetzt beschränken wir noch die Klassen auf die ersten 100 Daten. Zufälligerweise sind die Label der ja genau 0 und 1, so dass wir die Labelwerte nicht weiter anpassen müssen.
iris_y = iris.target[:100]
iris_y
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
Da wir nur zwei Features haben, können wir diese auf x- und y-Achse abbilden und für die zwei Klassen eine jeweils andere Farbe wählen.
plt.scatter(features[0][:50], features[1][:50], color='red', marker='o', label='setosa')
plt.scatter(features[0][50:], features[1][50:], color='blue', marker='x', label='versicolor')
plt.xlabel('sepal length')
plt.ylabel('petal length')
plt.legend(loc='upper left')
plt.show()
Wir sehen, dass die Daten sehr schön "linear separierbar" sind, d.h. man kann die zwei Klassen durch eine Gerade trennen.
Die Daten müssen später als NumPy-Arrays vorliegen:
iris_x = np.array(iris_x)
iris_y = np.array(iris_y)
class Perceptron():
# Konstruktor mit Defaultwerten
def __init__(self, alpha=0.01):
self.alpha = alpha
# Training mit x (Matrix von Featurevektoren) und y (Labels)
def fit(self, x, y, epochs=5):
ran = np.random.RandomState(42)
n_samples = x.shape[0]
print(f"Train on {n_samples} samples")
# Gewichte: 1 + dim(x) für den Bias
self.w = ran.normal(loc=0, scale=0.01, size=1 + x.shape[1])
# Speicher für Kosten (loss function) pro Epoche
self.loss = []
# Epochen durchlaufen
for i in range(epochs):
z = self.net_input(x) # Rohinput für alle Trainingsdaten
y_hat = self.activation(z) # Aktivierung für alle Trainingsdaten
diff = y - y_hat # Fehlervektor für alle Trainingsdaten
# Update der Gewichte
self.w[1:] += self.alpha * x.T.dot(diff)
# Update des Gewichts für das Bias-Neuron
self.w[0] += self.alpha * diff.sum()
# Kosten für diese Epoche (SSE) berechnen und in Liste speichern
l = (diff**2).sum() / 2.0
self.loss.append(l)
print(f"Epoch {i+1}/{epochs} - loss: {l:.4f}")
return self
# Aktivierungsfunktion: Identität (linear)
def activation(self, z):
return z
def net_input(self, x):
return np.dot(x, self.w[1:]) + self.w[0]
def predict(self, x):
return np.where(self.net_input(x) >= 0, 1, 0)
Besonders interessant ist natürlich die Methode fit, wo das Training in folgender Schleife stattfindet:
for i in range(epochs):
z = self.net_input(x)
y_hat = self.activation(z)
diff = y - y_hat
self.w[1:] += self.alpha * x.T.dot(diff)
self.w[0] += self.alpha * diff.sum()
Zu beachten ist hier, dass die Matrix $X$ alle Featurevektoren der Trainingsdaten enthält. Dadurch erreichen wir ganz elegant die Batch-Verarbeitung im Training, d.h. alle Trainingsdaten werden durchlaufen, bevor wir ein Update an den Gewichten vornehmen.
Man kann sich das so vorstellen, dass alle Featurevektoren übereinander gestapelt sind und somit eine Nx3-Matrix $X$ ergeben (siehe Abbildung). Man bedenke, dass die erste Spalte der Matrix $X$ das Bias-Neuron $x_0$ repräsentiert, also nur Einsen enthält. Bei der Matrixmultiplikation der Nx3-Eingabematrix $X$ mit dem 3x1-Gewichtsvektor $w$ erhalten wir einen Nx1-Ausgabevektor $\hat{Y}$, der alle Ausgaben für alle Trainingsbeispiele enthält.
Die Berechnung der Roheingabe ist in (Z) definiert:
$$ z = w^T x = w_1 x_1 + \ldots + w_n x_n $$Im Code wird das in net_input berechnet:
def net_input(self, x):
return np.dot(x, self.w[1:]) + self.w[0]
Hinweis: Die Multiplikationsreihenfolge bei Formel vs. Code ist unterschiedlich. In der Formel wird $x$ von rechts mit $w^T$ multipliziert, während im Code $x$ von links an $w$ multipliziert wird. Dann muss man eigentlich schreiben $x^T w$, damit das $x$ ein Zeilenvektor ist und $w$ der Spaltenvektor. Und wenn Sie die Beispiele unten ansehen, werden Sie merken, dass die x-Vektoren tatsächlich Zeilenvektoren sind. Beide Repräsentationen sind möglich. Die Implementation folgt dem Konzept $x^T w$.
Jetzt sehen wir uns an einem Beispiel an, wie die Berechnung im Code durchgeführt wird. Die folgende Abbildung zeigt das schematisch. Die x-Vektoren sind "übereinander gestapelt". Man beachte, dass es Zeilenvektoren sind, also streng genommen ist jedes $x$ eigentlich $x^T$.
Die Abbildung entspricht noch nicht ganz dem Code, denn im Code wird das Gewicht des Bias-Neurons anders verarbeitet. Das schauen wir uns im Folgenden an.
Im Code ist das mit dem Bias-Neuron etwas anders realisiert. Wir gehen jetzt davon aus, dass $X$ die Matrix der übereinandergestapelten Featurevektoren ohne die jeweilige 1 für das Biasneuron ist. Wir können uns die ersten 3 Featurevektoren der Irisdaten ansehen:
iris_x[:3]
array([[5.1, 1.4], [4.9, 1.4], [4.7, 1.3]])
Wir möchten keine neue Matrix bauen, wo die erste Spalte aus Einsen besteht. Stattdessen vollziehen wir eine Matrixmultiplikation mit unserer Nx2-Eingabematrix $X$ und dem 2x1-Gewichtsvektor $w$ (wir lassen das $w_0$ für das Biasneuron raus). Wir bekommen einen Nx1-"Pseudoausgabevektor". Hier fehlt an jeder Stelle noch das $x_0 \cdot w_0 = w_0$, das wir noch addieren müssen.
Für die Addition müssten wir eigentlich einen Vektor aus lauter $w_0$ bauen, der so lang ist, wie es Trainingsbeispiele gibt. Stattdessen können wir komponentenweise Addition anwenden, ausgedrückt durch das Plus im Kreis. In Numpy geht das ganz leicht mit broadcasting.
Das ist also gemeint mit
np.dot(x, self.w[1:]) + self.w[0]
Kehren wir zurück zur Trainingsschleife:
z = self.net_input(x)
y_hat = self.activation(z)
diff = y - y_hat
self.w[1:] += self.alpha * x.T.dot(diff)
self.w[0] += self.alpha * diff.sum()
Oben haben wir gesehen, dass in $z$ alle Rohinputs stecken. Mit y_hat ist hier $\hat{y} = g(z)$ gemeint. Dieser Vektor enthält alle berechneten Ausgaben für alle Trainingsbeispiele. Entsprechend enthält der Vektor diff alle Differenzen zwischen berechneter Ausgabe und echter Ausgabe.
Hier findet das Gewichtsupdate statt (ausgenommen das Bias-Neuron, das in der nächsten Zeile mit w[0] geupdatet wird), das auch den entsprechenden Gradienten beinhaltet.
self.w[1:] += self.alpha * x.T.dot(diff)
Der Code folgt den Formeln (A 4) und (A 7), in denen das Update definiert wurde:
$$ w := w + \Delta w $$$$ \Delta w_i = - \alpha \frac{\partial J}{\partial w_i} = \alpha \frac{1}{N} \sum_k \left( y^k - g(z^k) \right) x_i^k $$Wenn wir das mit dem Code vergleichen, sollte also dieser Teil
x.T.dot(diff)
dieser Formel entsprechen
$$ \sum_k \left( y^k - g(z^k) \right) x_i^k $$Die Matrix $X$ wird zunächst transponiert:
Anschließend wird $X^T$ mit dem Vektor aller Differenzen über alle Trainingsbeispiele multipliziert:
x.T.dot(diff)
Hier als Matrizen:
Die rechte Matrix enthält in Zeile 1 alle addierten Anpassungen für $w_1$ für alle Trainingsbeispiele. Zeile 2 entsprechend für $w_2$. Daher
self.w[1:] += self.alpha * x.T.dot(diff)
Die Codezeile
self.w[0] += self.alpha * diff.sum()
updatet das Bias-Neuron-Gewicht $w_0$. Hier reicht es, die Differenzen zusammen zu addieren (Abb. oben, der mittlere Vektor), da ja alle $x_i = 1$ sind.
Jetzt, wo wir die Perceptron-Klasse verstanden haben, wenden wir uns dem konkreten Einsatz zu.
Wir trainieren zwei Perzeptron-Netze, eines mit Lernrate 0.01, eines mit Lernrate 0.0001. Die Entwicklung der Kostenfunktion wird dabei in den jeweiligen Objekten mitprotokolliert (Instanzvariable loss).
EPOCHS = 20
model1 = Perceptron(alpha=0.01)
model1.fit(iris_x, iris_y, epochs=EPOCHS)
Train on 100 samples Epoch 1/20 - loss: 23.7989 Epoch 2/20 - loss: 24334.9132 Epoch 3/20 - loss: 37957919.7157 Epoch 4/20 - loss: 59208969090.5766 Epoch 5/20 - loss: 92357592414365.8594 Epoch 6/20 - loss: 144064742347031200.0000 Epoch 7/20 - loss: 224720561081757958144.0000 Epoch 8/20 - loss: 350532196498533588664320.0000 Epoch 9/20 - loss: 546780500149173669010079744.0000 Epoch 10/20 - loss: 852900014120760577106351489024.0000 Epoch 11/20 - loss: 1330403029897247964531781365923840.0000 Epoch 12/20 - loss: 2075239996079037720535038940854353920.0000 Epoch 13/20 - loss: 3237080000981913939835227627413569536000.0000 Epoch 14/20 - loss: 5049385590368112287706191504279880492646400.0000 Epoch 15/20 - loss: 7876325216702475760091903151316391867944271872.0000 Epoch 16/20 - loss: 12285950005006588998342183226694253397848432312320.0000 Epoch 17/20 - loss: 19164339126757914840331203369558938099531433098870784.0000 Epoch 18/20 - loss: 29893650390545246871046127287161095178251919536785719296.0000 Epoch 19/20 - loss: 46629853905289572200217874598033507686980745771985403904000.0000 Epoch 20/20 - loss: 72735957195657475175019364582358249508301312339071839319883776.0000
<__main__.Perceptron at 0x7f8ec9b81d90>
Was man hier sieht ist, dass der Fehler immer mehr ansteigt.
model2 = Perceptron(alpha=0.0001)
model2.fit(iris_x, iris_y, epochs=EPOCHS)
Train on 100 samples Epoch 1/20 - loss: 23.7989 Epoch 2/20 - loss: 13.5561 Epoch 3/20 - loss: 9.8254 Epoch 4/20 - loss: 8.4030 Epoch 5/20 - loss: 7.8000 Epoch 6/20 - loss: 7.4894 Epoch 7/20 - loss: 7.2845 Epoch 8/20 - loss: 7.1190 Epoch 9/20 - loss: 6.9696 Epoch 10/20 - loss: 6.8278 Epoch 11/20 - loss: 6.6907 Epoch 12/20 - loss: 6.5571 Epoch 13/20 - loss: 6.4267 Epoch 14/20 - loss: 6.2992 Epoch 15/20 - loss: 6.1746 Epoch 16/20 - loss: 6.0526 Epoch 17/20 - loss: 5.9334 Epoch 18/20 - loss: 5.8168 Epoch 19/20 - loss: 5.7027 Epoch 20/20 - loss: 5.5911
<__main__.Perceptron at 0x7f8ec9b813a0>
Wir zeichnen den Verlauf der Kostenfunktion über die Epochen. Das erste Netz mit Lernrate 0.01 hat steigende Kosten, d.h. das Netz wird schlechter. Das zweite Netz mit Lernrate 0.0001 zeigt dagegen sinkende Kosten.
Wir verwenden eine logarithmische Darstellung der Werte (log10), damit der Verlauf darstellbar bleibt.
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10,4))
ax[0].plot(range(1, len(model1.loss)+1), np.log10(model1.loss), marker='o')
ax[0].set_xlabel('Epochen')
ax[0].set_ylabel('log(SSE)')
ax[0].set_title('Perceptron, alpha=0.01')
ax[1].plot(range(1, len(model2.loss)+1), np.log10(model2.loss), marker='o')
ax[1].set_xlabel('Epochen')
ax[1].set_ylabel('log(SSE)')
ax[1].set_title('Perceptron, alpha=0.0001')
plt.show()
In den obigen Graphen sehen wir zwei Probleme:
Eine Maßnahme, die Abhilfe schafft, ist das Skalieren der Features. Es kann nämlich ein Problem sein, wenn ein Feature sich im Bereich 1000-2000 bewegt und ein anderes Feature im Bereich 0.001 bis 0.0015. Das erschwert den Gradientenabstieg, wie folgende Darstellung illustriert. Man sieht die Gewichte als 2D-Darstellung. Der Fehler wird hier mit "Höhenlinien" dargestellt. Der Gradientenabstieg vollzieht sich mit sehr langgestreckten Vektoren, was es erschwert, das Verfahren über eine einheitliche Lernrate zu kontrollieren.
Es gibt zwei Arten der Skalierung: Normalisieren und Standadisieren.
Allgemein nennt man das Vorverarbeiten der Features auch Feature Engineering.
Beim Normalisieren skaliert man alle Features auf den Bereich $[0, 1]$. Bei Featurevektoren $x^k$ berechnet man die normalisierten Vektoren $\bar{x}^k$ für jedes einzelne Feature $i$ einzeln:
$$ \bar{x}_i^k = \frac{x_i^k - min_i}{max_i - min_i} $$Hier stehen $min_i$ und $max_i$ für das Minimum/Maximum eines Features $i$ über alle Vektoren $x^k$.
Wenn die ursprünglichen Werte bei 0 beginnen (d.h. $min_i = 0$), was gar nicht so selten der Fall ist, ist diese Umformung natürlich extrem leicht:
$$ \bar{x}_i^k = \frac{x_i^k}{max_i} $$Ein Praxisbeispiel aus der Bildverarbeitung. Hier liegen die Daten häufig als Grauwert im Bereich 0..255 (oder auch als Wert für einen Farbkanal R, G oder B).
Wenn wir einen NumPy-Array haben:
data = np.array([13, 120, 5, 211])
data
array([ 13, 120, 5, 211])
Können wir ausnutzen, dass NumPy die komponentenweise Anwendung von mathematischen Operationen mit dieser eleganten Formulierung erlaubt:
data = data/255
data
array([0.05098039, 0.47058824, 0.01960784, 0.82745098])
Achten Sie darauf, dass ein "data/255" allein (ohne das "data =") nicht ausreicht, weil in dem Fall das Array nicht verändert wird.
Beim Standardisieren will man erreichen, dass die Featurewerte sich gemäß einer Normalverteilung verhalten, mit Mittelwert $0$ und einer Standardabweichung von $1$. Dazu muss man für ein Feature $i$ den Mittelwert $\mu_i$ und die Standardabweichung $\sigma_i$ berechnen. Dann skaliert man jedes Feature wie folgt:
$$ \bar{x}_i^k = \frac{x_i^k - \mu_i}{\sigma_i} $$Auch hier ein Praxisbeispiel: In NumPy lässt sich beides leicht realisieren, da es Funktionen wie mean (Mittelwert) und std (Standardabweichung) gibt, die man einfach auf NumPy-Arrays anwenden kann.
iris_x_st = np.copy(iris_x)
iris_x_st[:3]
array([[5.1, 1.4], [4.9, 1.4], [4.7, 1.3]])
Wir müssen darauf achten, dass wir jedes der beiden Feature getrennt behandeln. Das Feature wird über den zweiten Index gesteuert.
iris_x[:10,0]
array([5.1, 4.9, 4.7, 4.6, 5. , 5.4, 4.6, 5. , 4.4, 4.9])
Wir standardisieren das erste Feature:
iris_x_st[:, 0] = (iris_x[:,0] - iris_x[:,0].mean()) / iris_x[:,0].std()
iris_x_st[:3]
array([[-0.5810659 , 1.4 ], [-0.89430898, 1.4 ], [-1.20755205, 1.3 ]])
Und jetzt das zweite Feature:
iris_x_st[:, 1] = (iris_x[:,1] - iris_x[:,1].mean()) / iris_x[:,1].std()
iris_x_st[:3]
array([[-0.5810659 , -1.01297765], [-0.89430898, -1.01297765], [-1.20755205, -1.08231219]])
Wir erstellen ein drittes Netz und trainieren es mit den standardisierten Daten und Lernrate 0.01.
model3 = Perceptron(alpha=0.01)
model3.fit(iris_x_st, iris_y, epochs=EPOCHS)
Train on 100 samples Epoch 1/20 - loss: 24.4906 Epoch 2/20 - loss: 8.2845 Epoch 3/20 - loss: 5.6750 Epoch 4/20 - loss: 3.9525 Epoch 5/20 - loss: 2.8155 Epoch 6/20 - loss: 2.0650 Epoch 7/20 - loss: 1.5696 Epoch 8/20 - loss: 1.2426 Epoch 9/20 - loss: 1.0267 Epoch 10/20 - loss: 0.8842 Epoch 11/20 - loss: 0.7902 Epoch 12/20 - loss: 0.7281 Epoch 13/20 - loss: 0.6871 Epoch 14/20 - loss: 0.6601 Epoch 15/20 - loss: 0.6422 Epoch 16/20 - loss: 0.6304 Epoch 17/20 - loss: 0.6227 Epoch 18/20 - loss: 0.6175 Epoch 19/20 - loss: 0.6141 Epoch 20/20 - loss: 0.6119
<__main__.Perceptron at 0x7f8ea917ebb0>
Wir sehen uns die Kostenentwicklung im direkten Vergleich mit dem zweiten Netz an. Wir können jetzt den Logarithmus weglassen, weil keine extrem hohen Werte vorkommen.
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10,4))
ax[0].plot(range(1, len(model2.loss)+1), model2.loss, marker='o')
ax[0].set_xlabel('Epochen')
ax[0].set_ylabel('SSE')
ax[0].grid()
ax[0].set_ylim([0, 25]) # gleiche y-Skala anlegen
ax[0].set_title('Perceptron, alpha=0.0001')
ax[1].plot(range(1, len(model3.loss)+1), model3.loss, marker='o')
ax[1].set_xlabel('Epochen')
ax[1].set_ylabel('SSE')
ax[1].grid()
ax[0].set_ylim([0, 25]) # gleiche y-Skala anlegen
ax[1].set_title('Perceptron standardisiert, alpha=0.01')
plt.show()
Man sieht klar, dass das Netz mit den standardisierten Daten (rechts) schneller lernt, auch weil die Lernrate mit $0.01$ deutlich höher ist.
Zur Erinnerung: Auf den nicht-standardisierten Daten hat eine Lernrate von $0.01$ dazu geführt, dass der Fehler sich ständig vergrößert (siehe oben).
Oben sind wir davon ausgegangen, dass wir erst alle Trainingsbeispiele abarbeiten, die Deltas für jedes Einzelgewicht aufsummieren, und am Ende die Gewichte anpassen. Das kann dazu führen, dass das System sehr langsam lernt, da sich wichtige Gewichtsänderungen am Ende einer Epoche "rausmitteln". Man nennt diese Vorgehensweise manchmal Batch-Learning, aber dieser Begriff ist missverständlich, weil man auch manchmal Minibatch-Learning damit meint. In der Praxis kommt es kaum noch vor, dass alle Trainingsbeispiele vor dem Update der Gewichte durchlaufen werden.
Beim Perzeptron haben wir die Gewichte nach jedem einzelnen Sample angepasst, das ist im Grunde eine Form von SGD.
Siehe auch: https://machinelearningmastery.com/gentle-introduction-mini-batch-gradient-descent-configure-batch-size/
SGD bedeutet, dass man die Gewichte nach jedem einzelnen Trainingsbeispiel anpasst. Das nennt man manchmal auch Online-Learning. Der Begriff "Online" bezieht sich auf Szenarien, wo sich eine System "live" im Einsatz befindet und auf jede neue Einzelinformation (z.B. aus der Umwelt) eine Anpassung vornimmt. Da die Samples bei dieser Methode zufällig ausgewählt werden (anstatt in immer dergleichen Reihenfolge), spricht man von Stochastic Gradient Descent.
Der Nachteil dieser Methode ist, dass mehr Rechenleistung zum Einsatz kommt. Bei 60000 Trainingsbeispielen wird pro Epoche nicht ein einziges Update durchgeführt, sondern es werden 60000 Updates durchgeführt. Technisch führt dies dazu, dass man nicht mehrere Trainingsbeispiele zu einer Matrix $X$ zusammenfassen kann, wie oben skizziert, und die damit einhergehenden Performancevorteile nicht zum Zuge kommen.
Darüber hinaus schanken die Gewichte stark, da jedes Trainingsbeispiel (auch z.B. "schlechte" Beispiele) direkt die Gewichte beeinflussen. Schlechte Einflüsse werden also nicht "herausgemittelt". Also muss man eher eine sehr niedrige Lernrate wählen, was wiederum das Training verlangsamt.
Anstatt die Gewichte entweder nach jedem Beispiel zu updaten oder erst nach Durchlaufen aller Beispiele, kann man eine Teilmenge der Trainingsbeispiele durchlaufen, bevor ein Update durchgeführt wird. Für große Datensätze ist das die Methode der Wahl. Diese Teilmengen nennt man auch Batches und bei Bibliotheken gibt man häufig die Größe dieser Batches an (batch size), z.B. 32 oder 64.
Die Trainingsdaten werden also in Batches aufgeteilt. In einer Epoche werden alle Batches durchlaufen.
Wir werden Minibatch im nächsten Kapitel beim Thema "Backpropagation" im Aktion sehen.
Siehe auch die Videos von Andrew Ng: Mini Batch Gradient Descent und Understanding Mini-Batch Gradient Descent
SGD nennt man in Keras auch Optimierungsmethode, da es noch einige Varianten gibt (z.B. Adam oder RMSprop). In Keras wird im Grunde immer Minibatch durchgeführt, es sei denn, man zwingt Keras dazu, nach jedem einzelnen Trainingsbeispiel ein Update durchzuführen.
Sie sehen dies daran, dass die Methode fit
einen Parameter batch\_size
anbieten, der per Default auf 32 eingestellt ist. Dies ist genau die Batchgröße in Minibatch.
Bei der obigen Herleitung handelt es sich um binäre Klassifikation mit einem Ouput $\in [0, 1]$. Wie bekommen wir eine Klassifizierung für mehrere Klassen?
Im nächsten Kapitel werden wir neuronale Netze betrachten, die auf "ganz natürliche Weise" einen Mehrklassen-Output haben, aber hier möchte ich kurz eine generelle Denkweise vorstellen für den Fall, dass wir nur binäre Klassifikatoren zur Verfügung haben (unabhängig davon, ob es ein neuronales Netz ist oder z.B. Logistische Regression).
Die Grundidee ist, dass man bei mehreren ($N$) Klassen $i\in{1,\ldots,N}$ für jede Klasse $i$ einen eigenen binären Klassifikator $h_w^{(i)}$ erstellt. Bei 4 Klassen hätten wir also vier Klassifikatoren $h_w^{(1)}, h_w^{(2)},h_w^{(3)},h_w^{(4)}$. Die Trainingsdaten werden für jeden binären Klassifikator entsprechend präpariert: Für Klassifikator $h_w^{(1)}$ nimmt man alle Trainingsbeispiele der Klasse 1 als Positivbeispiele ($y=1$) und alle anderen als Negativbeispiele ($y=0$). Entsprechend für die anderen drei Klassifikatoren. Im nächsten Abschnitt sehen Sie das anhand eines praktischen Beispiels.
Um eine Vorhersage auf Eingabedaten $x$ zu erhalten, steckt man $x$ in alle Klassifikatoren $h_w^{(i)}$ und wählt die Klasse des Klassifikators mit dem höchsten Ausgabewert.
$$ \underset{i}{\arg \max} \, h_w^{(i)}(x) $$Diese Methode nennt man auch One-vs-All (OvA) oder One-vs-Rest.
Hinweis: Die Funktion arg max funktioniert wie folgt. Sie bekommt eine Funktion mit einem Parameter und gibt den Parameterwert zurück, mit dem die Funktion maximal wird. Beispiel: Einen Vektor $v = (v_1, \ldots, v_4)$ kann man auch als Funktion über seine Indizes betrachten. In diesem Fall gibt arg max den Index des Vektorelements mit dem höchsten Wert zurück. Als Beispiel sei Vektor $v = (5, 2, 18, -3)$ gegeben; dann ist $\underset{i}{\arg \max} \, v_i = 3$.
Im praktischen Teil werden wir hauptsächlich mit Keras - innerhalb von TensorFlow - arbeiten. Wir sehen uns hier ganz simple Netze innerhalb von Keras/TensorFlow an.
TensorFlow ist eine Open-Source-Bibliothek für Python (mit Bestandteilen in C++) für den Themenbereich des Maschinellen Lernens, inklusive Neuronaler Netze. TensorFlow wurde ursprünglich vom Google Brain Team für Google-eigenen Projekte entwickelt, wurde aber 2015 unter einer Open-Source-Lizenz (Apache License 2.0) für die Allgemeinheit frei gegeben. Mittlerweile dürfte TensorFlow das mit Abstand das wichtigste Framework im Bereich Maschinelles Lernen sein.
Noch ein paar Worte zum Entwicklungsteam hinter TensorFlow: Google Brain begann 2011 als ein sogenanntes "Google X"-Projekt als Kooperation zwischen Google-Fellow Jeff Dean und Stanford-Professor Andrew Ng. Seit 2013 ist Deep-Learning-Pioneer Geoffrey Hinton als leitender Wissenschaftler im Team. Weitere bekannte Wissenschaftler bei Google Brain sind Alex Krizhevsky und Ilya Sutskever (Autoren von AlexNet), Christopher Olah (bekannt durch seinen Blog) und Chris Lattner (Erfinder von Apple's Programmiersprache Swift).
Webseite: https://www.tensorflow.org
Keras ist ebenfalls eine Open-Source-Bibliothek für Python von François Chollet, die erstmals 2015 als Python-API für verschiedene "Backends" (u.a. TensorFlow) veröffentlicht wurde. Es ist ein objektorientiertes Framework für das Erstellen, Trainieren und Evaluieren Neuronaler Netze. Das Buch Deep Learning with Python [Chollet 2017] ist vom Erfinder von Keras und daher von besonderer Relevanz (einschränkend muss man sagen, dass die Theorie in dem Buch ein wenig zu kurz kommt). Eine Idee von Keras ist, dass es auch als Interface für verschiedene Backends in anderen Kontexten (z.B. Robotik oder mobile) genutzt werden kann. Daher liegt ein Fokus von Keras auf einer intuitiven, modularen und erweiterbaren Systematik.
Keras wurde unabhängig von TensorFlow entwickelt, wurde dann aber 2017 in TensorFlow 1.4 als Teil der TensorFlow Core API aufgenommen, d.h. alle Konzepte und Daten von Keras stehen in TensorFlow zur Verfügung. François Chollet arbeitet seit 2015 für Google und hat dort auch die Einbindung in TensorFlow unterstützt.
Webseite: https://keras.io
Wir wollen Keras nutzen, um ein Perzeptron-Netz zu bauen. Dazu stellen wir zunächst die Datensätze für die Operatoren AND und OR her. Wie wir schon weiter oben gesehen haben, ist der Raum der Daten für AND und OR sehr klein.
Für AND haben wir folgenden Datensatz:
x1 | x2 | y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
Für OR den folgenden:
x1 | x2 | y |
---|---|---|
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
Die x-Werte sind also für beide Operatoren gleich:
x_data = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
x_data
array([[0, 0], [0, 1], [1, 0], [1, 1]])
Die Operatoren haben lediglich unterschiedliche y-Werte (Label):
y_and = np.array([0, 0, 0, 1])
y_or = np.array([0, 1, 1, 1])
print("y-Werte AND: ", y_and)
print("y-Werte OR: ", y_or)
y-Werte AND: [0 0 0 1] y-Werte OR: [0 1 1 1]
In Keras heißt die Basisklasse für Neuronale Netze Sequential
. Der Name weist darauf hin, dass ein Sequential-Objekt eine geordnete Reihe (Sequenz) von Schichten enthält.
Wir erstellen zunächst ein Neuronales Netz für AND. Also stellen wir eine Instanz von Sequential
her, dies wäre unser Modell.
from tensorflow.keras.models import Sequential
model_and = Sequential()
2023-04-24 08:44:58.373422: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: SSE4.1 SSE4.2 To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags. 2023-04-24 08:45:00.673075: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: SSE4.1 SSE4.2 To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
Siehe: Getting started with the Keras Sequential model
Jetzt kann man Schichten als Objekte dem Modell hinzufügen. Es gibt verschiedene Arten von Schichten. Die wichtigste ist die Klasse Dense
. Eine Schicht vom Typ Dense
verbindet sich mit allen Neuronen aus der "Vorgängerschicht". In anderen Kontexten nennt man solche Schicchten auch fully connected layer oder FC layer.
Wenn die Schicht, wie hier, die erste Schicht ist, dann verbindet sich die Schicht mit der Eingabeschicht, die nicht explizit hinzugefügt wird, sondern automatisch vorhanden ist. Mit input_dim
gibt man an, wieviele Neuronen die Inputschicht hat. In unserem Fall sind es zwei Eingabeneuronen.
Zusätzlich geben wir hier die Initialisierung von Gewichten mit kernel_initializer
und des Bias-Gewichts mit bias_initializer
an. In unserem Fall setzen wir alle Gewichte auf Null.
Mit activation
kann man die Aktivierungsfunktion spezifizieren. Wir wählen die Funktion linear, also die einfache Identität $g(z) = z$.
Unsere Dense-Schicht ist gleichzeitig auch die Ausgabeschicht.
from tensorflow.keras.layers import Dense
model_and.add(Dense(1, input_dim=2,
kernel_initializer='zeros',
bias_initializer='zeros',
activation='linear'))
Unser Netz sieht derzeit so aus:
Mit der Methode summary
geben wir uns eine Zusammenfassung unserer Netzwerk-Architektur aus, was später bei komplexeren Netzen hilfreich ist.
model_and.summary()
Model: "sequential" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense (Dense) (None, 1) 3 ================================================================= Total params: 3 Trainable params: 3 Non-trainable params: 0 _________________________________________________________________
Man kann das Netzwerkobjekt auch programmatisch untersuchen. Die Eigenschaft layers
enthält alle Schichtenobjekte in iterierbarer Form. Hier wenden wir die Methode get_config
auf jede Schicht an, die uns ein Dictionary zurückgibt, das wir hier ausgeben.
for l in model_and.layers:
for key in l.get_config():
print("{}: {}".format(key, l.get_config()[key]))
name: dense trainable: True batch_input_shape: (None, 2) dtype: float32 units: 1 activation: linear use_bias: True kernel_initializer: {'class_name': 'Zeros', 'config': {}} bias_initializer: {'class_name': 'Zeros', 'config': {}} kernel_regularizer: None bias_regularizer: None activity_regularizer: None kernel_constraint: None bias_constraint: None
Besonders interessant sind natürlich die Gewichte, zumindest bei kleineren Netzen (später werden die Gewichte nicht mehr handhabbar).
Mit der Methode get_weights kann man sich die Gewichte und Bias-Gewichte aller Schichten ausgeben lassen.
w, b = model_and.get_weights()
print(f'WEIGHTS {w[0]} {w[1]} BIAS {b}')
WEIGHTS [0.] [0.] BIAS [0.]
Die Methode gibt es auch für jede Schicht:
for l in model_and.layers:
w, b = l.get_weights()
print(f'WEIGHTS {w[0]} {w[1]} BIAS {b}')
WEIGHTS [0.] [0.] BIAS [0.]
Damit man ein Keras-Netzwerk trainieren kann, muss es zunächst mit der Methode compile konfiguriert werden.
Mit compile
legen wir Hyperparameter wie die Lernrate und die Optimierungsmethode fest, aber auch Metriken wie die Art der Zielfunktion und die Evaluationsmaße.
Wir verwenden hier stochastic gradient descent (SGD) als Optimierungsmethode, mit einer Lernrate von 0.1. Als Zielfunktion wählen wir MSE (gemittelte Fehlerquadrate) und als Evaluationsmaß Accuracy.
Hinweis: Die "Sum of squared errors" (SSE), die wir verwendet haben, wird in Keras nicht angeboten, aber MSE ist ja sehr ähnlich.
from tensorflow.keras.optimizers import SGD
sgd = SGD(learning_rate=0.1)
model_and.compile(optimizer=sgd,
loss='mean_squared_error',
metrics=['acc'])
# Alternative:
# Hier benötigt man das Objekt in sgd nicht, kann allerdings auch
# nicht die Lernrate einstellen
#
# model_and.compile(optimizer='sgd', loss='mean_squared_error', metrics=['acc'])
Hinweis: Wie wir später noch sehen, muss man nicht unbedingt ein Objekt für den Optimizer herstellen, sondern kann auch einfach in der Methode compile optimizer='sgd' angeben. Das Vorgehen hier benötigt man nur, wenn man den Optimizer konfigurieren will (wie hier mit der Lernrate).
Das eigentliche Training, also die Updates der Paramter mit Hilfe der Trainingsdaten, findet in der Methode fit
statt.
for _ in range(10):
model_and.fit(x_data, y_and, epochs=1, batch_size=4)
w, b = model_and.get_weights()
print(f'WEIGHTS {w[0]} {w[1]} BIAS {b}')
1/1 [==============================] - 0s 143ms/step - loss: 0.2500 - acc: 0.7500 WEIGHTS [0.05] [0.05] BIAS [0.05] 1/1 [==============================] - 0s 2ms/step - loss: 0.1863 - acc: 0.7500 WEIGHTS [0.08750001] [0.08750001] BIAS [0.08] 1/1 [==============================] - 0s 2ms/step - loss: 0.1544 - acc: 0.7500 WEIGHTS [0.11637501] [0.11637501] BIAS [0.09649999] 1/1 [==============================] - 0s 2ms/step - loss: 0.1375 - acc: 0.7500 WEIGHTS [0.13926876] [0.13926876] BIAS [0.103925] 1/1 [==============================] - 0s 2ms/step - loss: 0.1276 - acc: 0.7500 WEIGHTS [0.15798594] [0.15798594] BIAS [0.10528625] 1/1 [==============================] - 0s 2ms/step - loss: 0.1212 - acc: 0.7500 WEIGHTS [0.17375943] [0.17375943] BIAS [0.10263181] 1/1 [==============================] - 0s 2ms/step - loss: 0.1164 - acc: 0.7500 WEIGHTS [0.18743233] [0.18743233] BIAS [0.09735357] 1/1 [==============================] - 0s 2ms/step - loss: 0.1126 - acc: 0.7500 WEIGHTS [0.19958213] [0.19958213] BIAS [0.09039639] 1/1 [==============================] - 0s 2ms/step - loss: 0.1092 - acc: 0.7500 WEIGHTS [0.21060517] [0.21060517] BIAS [0.08240069] 1/1 [==============================] - 0s 2ms/step - loss: 0.1062 - acc: 1.0000 WEIGHTS [0.22077432] [0.22077432] BIAS [0.07379951]
Jetzt kann man mit predict
Vorhersagen mit dem trainierten Netz berechnen.
model_and.predict(x_data)
1/1 [==============================] - 0s 42ms/step
array([[0.07379951], [0.29457384], [0.29457384], [0.51534814]], dtype=float32)
Da wir kontinuierliche Werte bekommen, legen wir eine Schwellwertfunktion an, um die Ausgabe auf die Werte {0, 1} zu zwingen.
pred_and = model_and.predict(x_data)
pred_and = [(1 if x>=0.5 else 0) for x in pred_and]
pred_and
1/1 [==============================] - 0s 14ms/step
[0, 0, 0, 1]
Die Konfusionsmatrix kann mit confusion_matrix
aus Scikit-learn erstellt werden.
import sklearn
from sklearn import metrics
cm_and = metrics.confusion_matrix(y_and, pred_and)
cm_and
array([[3, 0], [0, 1]])
Wir wiederholen alle Schritte, um ein Neuronales Netz für OR zu entwickeln.
model_or = Sequential()
model_or.add(Dense(1, input_dim=2,
kernel_initializer='zeros',
bias_initializer='zeros',
activation='linear'))
model_or.summary()
Model: "sequential_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= dense_1 (Dense) (None, 1) 3 ================================================================= Total params: 3 Trainable params: 3 Non-trainable params: 0 _________________________________________________________________
w, b = model_or.get_weights()
print(f'WEIGHTS {w[0]} {w[1]} BIAS {b}')
WEIGHTS [0.] [0.] BIAS [0.]
from tensorflow.keras.optimizers import SGD
sgd = SGD(learning_rate=0.1)
model_or.compile(optimizer=sgd,
loss='mean_squared_error',
metrics=['acc'])
for _ in range(5):
model_or.fit(x_data, y_or, epochs=1, batch_size=4)
w, b = model_or.get_weights()
print(f'WEIGHTS {w[0]} {w[1]} BIAS {b}')
1/1 [==============================] - 0s 112ms/step - loss: 0.7500 - acc: 0.2500 WEIGHTS [0.1] [0.1] BIAS [0.15] 1/1 [==============================] - 0s 2ms/step - loss: 0.3925 - acc: 0.2500 WEIGHTS [0.17] [0.17] BIAS [0.25] 1/1 [==============================] - 0s 2ms/step - loss: 0.2258 - acc: 0.5000 WEIGHTS [0.2195] [0.2195] BIAS [0.31599998] 1/1 [==============================] - 0s 2ms/step - loss: 0.1479 - acc: 1.0000 WEIGHTS [0.25497502] [0.25497502] BIAS [0.35889998] 1/1 [==============================] - 0s 2ms/step - loss: 0.1110 - acc: 1.0000 WEIGHTS [0.28083876] [0.28083876] BIAS [0.38612497]
pred_raw = model_or.predict(x_data)
pred_raw
1/1 [==============================] - 0s 25ms/step
array([[0.38612497], [0.6669637 ], [0.6669637 ], [0.9478025 ]], dtype=float32)
pred_or = model_or.predict(x_data)
pred_or = [(1 if x>=0.5 else 0) for x in pred_or]
pred_or
1/1 [==============================] - 0s 13ms/step
[0, 1, 1, 1]
Sanity check: Prüfen, ob der Vorhersagewert übereinstimmt mit den selbst berechneten Wert:
w, b = model_or.get_weights()
print(f'WEIGHTS {w[0]} {w[1]} BIAS {b}')
WEIGHTS [0.28083876] [0.28083876] BIAS [0.38612497]
b
array([0.38612497], dtype=float32)
print(f"Ausgabewert für x=(1, 1) selbst berechnet: {w[0] + w[1] + b}")
print(f"Ausgabewert für x=(1, 1) vom Modell: {pred_raw[3][0]}")
Ausgabewert für x=(1, 1) selbst berechnet: [0.9478025] Ausgabewert für x=(1, 1) vom Modell: 0.9478024840354919
Wir möchten jetzt noch das Experiment mit den Iris-Daten mit einem Keras-Netz wiederholen.
Wir zeigen nochmal die Daten als 2D-Plot:
plt.scatter(features[0][:50], features[1][:50], color='red', marker='o', label='setosa')
plt.scatter(features[0][50:], features[1][50:], color='blue', marker='x', label='versicolor')
plt.xlabel('sepal length')
plt.ylabel('petal length')
plt.legend(loc='upper left')
plt.show()
iris_x[:3]
array([[5.1, 1.4], [4.9, 1.4], [4.7, 1.3]])
Wir erstellen unser Modell. Wir haben zwei Eingabeneuronen und ein Ausgabeneuron. Die Aktivierungsfunktion ist die Identität.
model_iris = Sequential()
model_iris.add(Dense(1, input_dim=2,
kernel_initializer='zeros',
bias_initializer='zeros',
activation='linear'))
Wir wählen eine relativ hohe Lernrate von 0.1.
sgd = SGD(learning_rate=0.1)
model_iris.compile(optimizer=sgd,
loss='mean_squared_error',
metrics=['acc'])
history = model_iris.fit(iris_x,
iris_y,
epochs=EPOCHS)
Epoch 1/20 4/4 [==============================] - 0s 1ms/step - loss: 1927.3512 - acc: 0.4400 Epoch 2/20 4/4 [==============================] - 0s 990us/step - loss: 11619229696.0000 - acc: 0.4800 Epoch 3/20 4/4 [==============================] - 0s 1ms/step - loss: 70291026744442880.0000 - acc: 0.5200 Epoch 4/20 4/4 [==============================] - 0s 1ms/step - loss: 341680537681225549086720.0000 - acc: 0.4800 Epoch 5/20 4/4 [==============================] - 0s 1ms/step - loss: 1068573687639875652810577018880.0000 - acc: 0.5000 Epoch 6/20 4/4 [==============================] - 0s 917us/step - loss: inf - acc: 0.5000 Epoch 7/20 4/4 [==============================] - 0s 1ms/step - loss: inf - acc: 0.4800 Epoch 8/20 4/4 [==============================] - 0s 1ms/step - loss: inf - acc: 0.4400 Epoch 9/20 4/4 [==============================] - 0s 893us/step - loss: inf - acc: 0.5200 Epoch 10/20 4/4 [==============================] - 0s 1ms/step - loss: inf - acc: 0.5200 Epoch 11/20 4/4 [==============================] - 0s 962us/step - loss: inf - acc: 0.4600 Epoch 12/20 4/4 [==============================] - 0s 1ms/step - loss: nan - acc: 0.5400 Epoch 13/20 4/4 [==============================] - 0s 1ms/step - loss: nan - acc: 0.5000 Epoch 14/20 4/4 [==============================] - 0s 985us/step - loss: nan - acc: 0.5000 Epoch 15/20 4/4 [==============================] - 0s 1ms/step - loss: nan - acc: 0.5000 Epoch 16/20 4/4 [==============================] - 0s 1ms/step - loss: nan - acc: 0.5000 Epoch 17/20 4/4 [==============================] - 0s 1ms/step - loss: nan - acc: 0.5000 Epoch 18/20 4/4 [==============================] - 0s 1ms/step - loss: nan - acc: 0.5000 Epoch 19/20 4/4 [==============================] - 0s 905us/step - loss: nan - acc: 0.5000 Epoch 20/20 4/4 [==============================] - 0s 958us/step - loss: nan - acc: 0.5000
Wir sehen uns die Entwicklung von Loss und Accuracy an.
epochs = range(1, len(history.history['acc']) + 1)
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10,4))
ax[0].plot(epochs, history.history['loss'], 'r', label='Loss')
ax[0].set_xlabel('Epochen')
ax[0].set_ylabel('Loss')
ax[0].set_title('Loss')
ax[1].plot(epochs, history.history['acc'], 'b', label='Accuracy')
ax[1].set_xlabel('Epochen')
ax[1].set_ylabel('Accuracy')
ax[1].set_title('Accuracy')
plt.show()
Man sieht hier, dass das Netz mit Lernrate 0.1 offenbar nicht zum Ziel kommt. Stattdessen "oszilliert" es und die Zielfunktion geht gegen Unendlich. Das heißt, die Lernrate ist zu hoch.
Wir versuchen das ganze nochmal mit Lernrate 0.01.
model_iris = Sequential()
model_iris.add(Dense(1, input_dim=2,
kernel_initializer='zeros',
bias_initializer='zeros',
activation='linear'))
sgd = SGD(learning_rate=0.01)
model_iris.compile(optimizer=sgd,
loss='mean_squared_error',
metrics=['acc'])
history = model_iris.fit(iris_x,
iris_y,
epochs=EPOCHS)
Epoch 1/20 4/4 [==============================] - 0s 1ms/step - loss: 0.3047 - acc: 0.7900 Epoch 2/20 4/4 [==============================] - 0s 1ms/step - loss: 0.1467 - acc: 0.8400 Epoch 3/20 4/4 [==============================] - 0s 935us/step - loss: 0.1185 - acc: 1.0000 Epoch 4/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0998 - acc: 1.0000 Epoch 5/20 4/4 [==============================] - 0s 981us/step - loss: 0.0881 - acc: 1.0000 Epoch 6/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0746 - acc: 1.0000 Epoch 7/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0666 - acc: 1.0000 Epoch 8/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0586 - acc: 1.0000 Epoch 9/20 4/4 [==============================] - 0s 922us/step - loss: 0.0540 - acc: 1.0000 Epoch 10/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0432 - acc: 1.0000 Epoch 11/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0401 - acc: 1.0000 Epoch 12/20 4/4 [==============================] - 0s 939us/step - loss: 0.0343 - acc: 1.0000 Epoch 13/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0305 - acc: 1.0000 Epoch 14/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0299 - acc: 1.0000 Epoch 15/20 4/4 [==============================] - 0s 999us/step - loss: 0.0256 - acc: 1.0000 Epoch 16/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0244 - acc: 1.0000 Epoch 17/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0234 - acc: 1.0000 Epoch 18/20 4/4 [==============================] - 0s 815us/step - loss: 0.0232 - acc: 1.0000 Epoch 19/20 4/4 [==============================] - 0s 1ms/step - loss: 0.0202 - acc: 1.0000 Epoch 20/20 4/4 [==============================] - 0s 818us/step - loss: 0.0176 - acc: 1.0000
w, b = model_iris.get_weights()
print(f'WEIGHTS {w[0]} {w[1]} BIAS {b}')
WEIGHTS [-0.06356977] [0.31815472] BIAS [-0.03466483]
epochs = range(1, len(history.history['acc']) + 1)
fig, ax = plt.subplots(nrows=1, ncols=2, figsize=(10,4))
ax[0].plot(epochs, history.history['loss'], 'r', label='Loss')
ax[0].set_xlabel('Epochen')
ax[0].set_ylabel('Loss')
ax[0].set_title('Loss')
ax[1].plot(epochs, history.history['acc'], 'b', label='Accuracy')
ax[1].set_xlabel('Epochen')
ax[1].set_ylabel('Accuracy')
ax[1].set_title('Accuracy')
plt.show()