Kapitel 4: Feedforward Netze I

Updates dieser Seite:

Überblick

Wir lernen die grundlegenden Konzepte Neuronaler Netze anhand von zwei sehr einfachen Netzen - Perzeptron und Adaline - kennen. Dabei betrachten wir eigenen Implementierungen der Netze in Python. Wir führen auch das Keras-Framework (innerhalb von TensorFlow) ein und lernen, wie man dort Netze erstellt und trainiert.

Konzepte

Künstliche Neuronen, Gewichte, Bias-Neuronen, Aktivierung, Matrixdarstellung der Gewichte, Matrixdarstellung der Featurevektoren.

Datensätze

Iris

Importe

1 Künstliche Neuronale Netze

Künstliche Neuronale Netze sind inspiriert vom menschlichen Gehirn und seinen Gehirnzellen, den sogenannten Neuronen.

Das menschliche Gehirn besteht aus etwa 86 Milliarden Neuronen. Die Verbindungen zwischen den Hirnzellen werden über sogenannte Synapsen hergestellt. Jeder Mensch hat ca. $10^{14}$ (100 Trillionen) 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).

1.1 Biologische Neuronen

Die Neuronen sind die atomaren Einheiten des Gehirns. Neuronen empfangen elektrische Signale über Dendriten und laden sich gewissermaßen auf (man spricht vom Aktionspotential). 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.

Nervenzelle

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 Convolutioal Neural Networks 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.

1.2 Künstliche Neuronen

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.

Das Perzeptron und Adaline werden in diesem Kapitel ausführlich behandelt. 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).

Artikel zum Thema

2 Perzeptron

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.

2.1 Modell

Das Perzeptron ist ein Netzwerk aus Neuronen und gerichteten Verbindungen. Die Neuronen sind in zwei 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, siehe https://en.wikipedia.org/wiki/Directed_acyclic_graph)

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 $\{-1, 1\}$ eingeführt.

Notation und Vektordarstellung

Der Input durch die Eingabeneuronen wird repräsentiert durch einen Vektor $x = (x_1, \ldots, x_n)$ der Länge $n$, wobei $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 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 Berechnungen, dass es sich um einen Spaltenvektor handelt:

$$ w = \left( \begin{array}{c} w_1 \\ \vdots \\ w_n \end{array} \right) $$

2.2 Verarbeitung

Roheingabe

In einem ersten Schritt berechnen wir für einen gegebenen Featurevektor $x$ die Roheingabe $z$ (engl. net input) des Ausgabeneurons. Dies ist die gewichtete Summe der Werte der Eingabeneuronen 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:

$$ \tag{P 1} \begin{align} 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).

Aktivierung und Ausgabe

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{P 2} y = g(z) $$

Für $g$ verwenden wir die sogenannte Heaviside- oder Stufenfunktion (auch step oder threshold function):

$$ g(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:

Diese Funktion ist nicht differenzierbar bei $x = 0$.

Modellierung des logischen AND

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} $$

Modellierung des logischen OR

Ä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.

XOR

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.

Bias-Neuron

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:

$$ \tag{P 3} g(z) = \begin{cases} 1 & \quad \text{falls } z \geq 0 \\ 0 & \quad \text{sonst} \end{cases} $$

Die Formeln (P 1) und (P 2) 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:

2.3 Lernen

Lernen bedeutet, dass die Gewichte $w$ mit Hilfe von Trainingsdaten angepasst werden, so dass unser Netzwerk sich der gewünschten Funktion $h^*$ annähert.

Trainingsdaten

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 ($k \in \{1,\ldots , N\}$):

$$(x^k, y^k)$$

Bei einem Neuronalen Netz müssen wir zwischen dem berechneten Output des aktuellen Netzwerks und dem korrekten Output eines Traningsbeispiels unterscheiden.

Wir schreiben:

Anpassung der Gewichte

Lernen bedeutet, dass wir für jedes Trainingsbeispiel die Ausgabe $\hat{y}$ berechnen und anschließend die Gewichte anpassen. 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:

$$ \tag{P 4} 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:

$$ \tag{P 5} \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.

Perzeptron-Lernalgorithmus

Jetzt formulieren wir den Lernalgorithmus:

  1. Initialisiere alle Gewichte $w = (w_0, \ldots, w_n)$, z.B. mit 0
  2. Für jede Epoche:
    • Für jedes Trainingsbeispiel $(x^k, y^k)$ mit $k = 1,\ldots, N$:
      • berechne Output $\hat{y}^k$
      • berechne $\Delta w_i := \alpha \, (y^k - \hat{y}^k ) x_i^k $
      • berechne für das Bias-Neuron: $\Delta w_0 := \alpha \, (y^k - \hat{y}^k ) $
      • passe alle Gewichte an mit $w_i := w_i + \Delta w_i$

Man beachte, dass die Gewichte immer nach der Verarbeitung eines einzelnen Trainingsbeispiels angepasst werden.

3 Adaline

Das Adaline (ADAptive Linear NEuron) wurde 1960 von Bernard Widrow eingeführt. Es hat große Ähnlichkeiten mit dem Perzeptron, hat aber den Vorteil, dass die mathematische Herleitung der Lernregel bereits auf die allgemeinen Feedforward-Netze hinführt.

Auch hier gilt zur Berechnung des Rohinputs:

$$ \tag{A 1} z = w^T x = w_1 x_1 + \ldots + w_n x_n $$

Der Unterschied zum Perzeptron zeigt sich in der Aktivierungsfunktion (P 3).

3.1 Aktivierungsfunktion

Im Vergleich zum Perzeptron, hat das Adaline-Netz eine andere Aktivierungsfunktion $g$. Es handelt sich um die Identität, also eine ganz einfache, lineare Funktion:

$$ \tag{A 2} g(z) = z $$

Zur Erinnerung: Beim Perzeptron hatten wir die Heaviside-Funktion genutzt:

$$ g_{perz}(z) = \begin{cases} 1 & \quad \text{falls } z \geq 0 \\ 0 & \quad \text{sonst} \end{cases} $$

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.

Beim Perzeption haben wir die Lernregel (Update-Regel) lediglich "behauptet", aber nicht hergeleitet. Beim Adaline-Netz werden wir die Regel über den Gradientenabstieg herleiten.

3.2 Lernen durch Gradientenabstieg

Lernen funktioniert ja so, dass die Trainingsbeispiele betrachtet werden und die Gewichte entsprechend angepasst werden. Beim Perzeptron haben wir für jedes Sample ein Update der Gewichte durchgeführt. Beim Adaline-Netz führen wir ein Update erst durch, nachdem wir alle Samples betrachtet haben. Warum das so ist, sehen wir bei der Herleitung.

Zielfunktion

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 die Summe der Fehlerquadrate (engl. sum of squared errors oder SSE). 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{A 3} J(w) = \frac{1}{2} \sum_{k=1}^N \left( y^k - g(z^k) \right)^2 $$

Diese Funktion spannt eine Fehlerlandschaft auf. Bei den folgenden beiden Beispielen kann man sich die Gewichte 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.

Quelle: https://blog.paperspace.com/intro-to-optimization-in-deep-learning-gradient-descent

Update der Gewichte

Wie bei der logistischen Regression verwenden wir Gradientenabstieg. 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:

Quelle: https://blog.paperspace.com/part-2-generic-python-implementation-of-gradient-descent-for-nn-optimization

Die Update-Regel ist vom Prinzip her wie beim Perzeptron (wir sehen hier Vektoren statt der Komponenten):

$$ \tag{A 4} w := w + \Delta w $$

Allerdings nehmen wir ein anderes Delta, nämlich den negativen Gradienten von $J$, modifiziert durch die Lernrate $\alpha$:

$$ \tag{A 5} \Delta w := - \alpha \nabla J(w) = - \alpha \left( \begin{array}{c} \frac{\partial J}{\partial w_1} \\ \vdots \\ \frac{\partial J}{\partial w_n} \end{array} \right) $$

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$.

Eine einzelne Ableitung sieht wie folgt aus:

$$ \tag{A 6} \frac{\partial J}{\partial w_i} = - \sum_k \left( y^k - g(z^k) \right) x_i^k $$

Die Herleitung für (A 6) finden Sie weiter unten.

Für Gewicht $w_i$ sieht das Delta also so aus:

$$ \tag{A 7} \Delta w_i = - \alpha \frac{\partial J}{\partial w_i} = \alpha \sum_k \left( y^k - g(z^k) \right) x_i^k $$

Die Formel ist ähnlich der Formel der logistischen Regression.

Darüber hinaus sieht beim Adaline-Netz die Update-Regel genauso aus wie beim Perzeptron, 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.

Lernalgorithmus

Lernen im Adaline-Netz erfolgt nach dem folgendem Algorithmus. Man beachte, dass wir das $\Delta w_i$ innerhalb einer Epoche als Speicher benutzen, um alle Deltas für alle Trainingsdaten "einzusammeln".

  1. Initialisiere alle Gewichte $w = (w_0, \ldots, w_n)$, z.B. mit 0 oder niedrigen Werten
  2. Für jede Epoche:
    • setze $\Delta w_i := 0$
    • Für jedes Trainingsbeispiel $(x^k, y^k)$, $k = 1,\ldots, N$:
      • berechne Output $\hat{y}^k$
      • berechne $\Delta w_i := \Delta w_i + (y - \hat{y})\, x_i$
    • führe ein Update aller Gewichte durch mit $w_i := w_i + \alpha \Delta w_i$

Der Unterschied zum Lernalgorithmus beim Perzeptron ist, dass erst alle Trainingsdaten durchlaufen werden, bevor die Gewichte angepasst werden.

Herleitung von (A 6)

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 (A 3):

$$ J(w) = \frac{1}{2} \sum_{k=1}^N \left( y^k - g(z^k) \right)^2 $$

Da $g$ die Identitätsfunktion ist (A 2):

$$ J(w) = \frac{1}{2} \sum_{k=1}^N \left( y^k - z^k \right)^2 $$

Jetzt setzen wir $z^k$ ein, wie in (A 1) definiert:

$$ J(w) = \frac{1}{2} \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}{2} \sum_{k=1}^N \left( y^k - w^T x^k \right)^2 \\[2mm] &= \frac{1}{2} \sum_{k=1}^N \frac{\partial}{\partial w_i} \left( y^k - w^T x^k \right)^2\\[2mm] &= \frac{1}{2} \sum_{k=1}^N 2 \left( y^k - w^T x^k \right) \frac{\partial}{\partial w_i} - w^T x^k\\[2mm] &= - \sum_{k=1}^N \left( y^k - w^T x^k\right) x_i^k \\[2mm] &= - \sum_{k=1}^N \left( y^k - g(z^k) \right) x_i^k \end{align*} $$

Damit wäre (A 6) gezeigt.

3.3 Iris-Daten

Jetzt möchten wir ein Adaline in Python programmieren. Dazu werden wir Daten benötigen, um das Netz zu trainieren. Wir nutzen dazu den Iris-Datensatz.

In Kapitel 3 (Abschnitt 2.1) wurde der Datensatz im Zusammenhang mit Logistischer Regression eingeführt. Schlagen Sie gern noch einmal nach, wie der Datensatz aufgebaut ist.

Daten

Wir laden die Daten aus der Bibliothek Scikit-learn.

Wir erhalten ein Dictionary (Hashtabelle) und sehen uns die Schlüssel an:

Die Featurevektoren sind in data, die Labels in der Spalte target.

Wir prüfen den Umfang des Datensatzes:

Jetzt schauen wir uns die ersten drei Featurevektoren an:

Und werfen noch einen Blick auf die Label, die mit 0, 1, 2 kodiert sind:

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.

Das gleiche gilt für die Bezeichnungen und die Reihenfolge der vier Features:

Für unser Beispiel möchten wir nur zwei Features verwenden: sepal length (Index 0) und petal length (Index 2).

Dazu transponieren wir die Daten, dann erhalten wir vier Vektoren mit jeweils allen Daten für ein Feature.

Jetzt picken wir uns die zwei Features 0 und 2 heraus:

Anschließend transponieren wir das ganze zurück:

Wir beschränken uns auf die ersten 100 Vektoren, da wir nur 2 der 3 Klassen betrachten (jeweils 50 Daten pro Klasse).

Jetzt beschränken wir noch die Label 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.

Visualisierung der Daten

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.

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:

3.4 Implementierung eines Adaline-Netzes in Python

Wir implementieren das Adaline-Netz in Python, um die Funktionsweise besser zu verstehen. Dazu verwenden wir die Iris-Daten.

Klasse Adaline

Wir definieren zunächst die Klasse Adaline. Der Code ist eine angepasste Version von [Raschka 2019].

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()

Eine Matrix für alle Trainingsdaten

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 (Ada 1) 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.

Alternative Behandlung des Bias-Neurons

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:

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] 

Gewichts-Updates

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.

Training

Jetzt, wo wir die Adaline-Klasse verstanden haben, wenden wir uns dem konkreten Einsatz zu.

Wir trainieren zwei Adaline-Netze, eines mit Lernrate 0.01, eines mit Lernrate 0.0001. Die Entwicklung der Kostenfunktion wird dabei in den jeweiligen Objekten mitprotokolliert (Instanzvariable loss).

Lernrate 0.01

Was man hier sieht ist, dass der Fehler immer mehr ansteigt.

Lernrate 0.0001

Visualisierung

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.

Probleme

In den obigen Graphen sehen wir zwei Probleme:

  1. Bei einer Lernrate von $0.01$ scheint das Netz ständig über das Minimum hinauszuschießen (Overshoot) und entfernt sich sogar davon, so dass der Gesamtfehler sich im Training sogar immer weiter erhöht.
  2. Bei einer kleineren Lernrate von $0.0001$ reduziert sich der Gesamtfehler, aber aufgrund der kleinen Lernrate könnte es viele Epochen dauern, bevor das echte Minimum erreicht ist.

3.5 Feature-Scaling: Normalisieren und Standardisieren

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.

Quelle: https://www.jeremyjordan.me/batch-normalization

Es gibt zwei Arten der Skalierung: Normalisieren und Standadisieren.

Allgemein nennt man das Vorverarbeiten der Features auch Feature Engineering.

Normalisieren

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:

Können wir ausnutzen, dass NumPy die komponentenweise Anwendung von mathematischen Operationen mit dieser eleganten Formulierung erlaubt:

Achten Sie darauf, dass ein "data/255" allein (ohne das "data =") nicht ausreicht, weil in dem Fall das Array nicht verändert wird.

Standardisieren

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.

Wir müssen darauf achten, dass wir jedes der beiden Feature getrennt behandeln. Das Feature wird über den zweiten Index gesteuert.

Wir standardisieren das erste Feature:

Und jetzt das zweite Feature:

Trainieren mit standardisierten Daten

Wir erstellen ein drittes Netz und trainieren es mit den standardisierten Daten und Lernrate 0.01.

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.

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).

3.6 Trainingsmethoden: SGD und Minibatch

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/

Stochastic Gradient Descent (SGD)

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 die höhere Rechenbelastung. Bei 60000 Trainingsbeispielen wird pro Epoche nicht ein Update durchgeführt, sondern 60000 Updates. Außerdem kann es sein, dass die Gewichte stark schwanken, da jedes Trainingsbeispiel (auch z.B. "schlechte" Beispiele) direkt die Gewichte beeinflussen. Schlechte Einflüsse werden also nicht "rausgemittelt". Also muss man eher eine sehr niedrige Lernrate wählen.

Minibatch

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

4 TensorFlow und Keras

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.

4.1 Hintergrund

TensorFlow

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

Keras ist ebenfalls eine Open-Source-Bibliothek für Python von François Chollet. 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

4.2 Daten

Wir wollen Keras nutzen, um ein Adaline-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:

Die Operatoren haben lediglich unterschiedliche y-Werte (Label):

4.3 Adaline-Netze

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.

Neuronales Netz für AND

Wir stellen eine Instanz von Sequential her. Wir nennen das unser Modell.

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". Man nennt das auch fully connected 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 und bilden damit das Adaline-Modell ab.

Unsere Dense-Schicht ist gleichzeitig auch die Ausgabeschicht.

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.

Schichten und Parameter

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 eine Hashtable (dictionary) zurückgibt, die wir ausgeben.

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.

Die Methode gibt es auch für jede Schicht:

Training

Damit man ein Keras-Netzwerk trainieren kann, muss es zunächst mit der Methode compile konfiguriert werden.

Konfiguration mit Compile

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 beim Adaline verwendet haben, wird in Keras nicht angeboten, aber MSE ist ja sehr ähnlich.

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).

Training mit Fit

Das eigentliche Training, also die Updates der Paramter mit Hilfe der Trainingsdaten, findet in der Methode fit statt.

Evaluation

Jetzt kann man mit predict Vorhersagen mit dem trainierten Netz berechnen.

Da wir kontinuierliche Werte bekommen, legen wir eine Schwellwertfunktion an, um die Ausgabe auf die Werte {0, 1} zu zwingen.

Konfusionsmatrix

Die Konfusionsmatrix kann mit confusion_matrix aus Scikit-learn erstellt werden.

Neuronales Netz für OR

Wir wiederholen das ganze für OR:

Sanity check: Prüfen, ob der Vorhersagewert übereinstimmt mit den selbst berechneten Wert:

4.4 Neuronales Netz für den Iris-Datensatz

Wir möchten jetzt noch das Experiment mit den Iris-Daten mit einem Keras-Netz wiederholen.

Wir zeigen nochmal die Daten als 2D-Plot:

NN mit Lernrate 0.1

Versuch mit höherer Lernrate:

Visualisierung

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.

NN mit Lernrate 0.01

Wir versuchen das ganze nochmal mit Lernrate 0.01.

Visualisierung

Mit Lernrate 0.01 stabilisiert sich das Netz bereits in der 4. Epoche und erreicht 100% Accuracy.

5 Literatur

Herculano-Houzel, Suzana (2009) The human brain in numbers: a linearly scaled-up primate brain. In: Front Hum Neurosci 3: 31.

McCulloch, Warren; Pitts, Walter (1943) A logical calculus of the ideas immanent in nervous activity. In: Bulletin of Mathematical Biophysics, vol. 5, 115-133. Paper bei SpringerLink

Minsky, Marvin; Papert, Seymour (1969) Perceptrons: An Introduction to Computational Geometry, MIT Press. [2nd edition 1972]

Raschka, Sebastian; Mirjalili, Vahid (2019) Python Machine Learning - Machine Learning and Deep Learning with Python, scikit-learn, and TensorFlow 2, 3rd Edition, Packt Publishing

Rosenblatt, Frank (1957) The Perceptron - A perceiving and recognizing automaton. In: Report 85-460-1, Cornell Aeronautical Laboratory.

Rosenblatt, Frank (1958) The Perceptron: A probabilistic model for information storage and organization in the brain. In: Psychological Review. 65 (6): 386–408.

Widrow, Bernard (1960) An Adaptive "ADALINE" Neuron using Chemical "Mimistors". In: Technical Report 1553-2, Solid-State Electronics Laboratory, Stanford University.

Widrow, Bernard; Lehr, Michael A. (1990) 30 Years of Adaptive Neural Networks: Perceptron, Madaline, and Backpropagation. In: Proceedings of the IEEE, vol. 78, no. 9, pp. 1415 - 1442.