Handgeschriebene Zahlen erkennen
Datenaufbereitung für neuronale Netzwerke:
Mehrdimensionale Arrays werden Tensoren genannt. Diese Tensoren werden von allen Systemen die etwas mit "Lernen von Maschinen" zu tun haben, als Basisdatenstrukturen verwendet. Ein Tensor ist ein Datenspeicher, der meistens für numerische Daten verwendet wird. Bekannt sind ja Matrizen, welche 2-Achsige Tensoren sind. Die Tensoren sind ähnlich, können aber beliebig viele Dimensionen annehmen. Dies Dimensionen werden oft auch als Achsen bezeichnet.
Das dazugehörige Notebook: DatenaufbereitungFuerNeuronaleNetzwerke.ipynb
Skalare sind Tensoren mit der Dimension 0:
Ein Tensor, der nur eine einzige Zahl beinhaltet wird als Skalar bezeichnet. Man sagt auch: Ein Tensor vom Rang 0.
Beispiel:
Skalar Wert von x: 12
Dimension von x: 0
Vektoren sind Tensoren mit der Dimension 1:
Ein eindimensionales Feld von Zahlen wird als Vektor bezeichnet. Dieser Vektor hat exakt eine Achse (Rang 1).
Beispiel:
Ausgabe:
Vektor: [12 3 6 14 7]
Dimension von x: 1
Matrizen sind Tensoren mit der Dimension 2 - (Rang 2):
Eine Matrix hat zwei Achsen und ist ein rechteckiges Zahlenfeld. Die Einträge in der ersten Achse werden Zeilen genannt und in der zweiten Achse sind es Spalten.
Matrix: 2
Tensoren vom Rang 3 und höher:
Packt man diese Matrizen in ein weiteres Array, dann erhält man einen Rang 3 Tensor. Diesen kann man sich als Würfel von Zahlen vorstellen.
Tensor: [[[ 5 78 2 34 0]
[ 6 79 3 35 1]
[ 7 80 4 36 2]]
[ [ 5 78 2 34 0]
[ 6 79 3 35 1]
[ 7 80 4 36 2]]
[ [ 5 78 2 34 0]
[ 6 79 3 35 1]
[ 7 80 4 36 2]]]
Tensor Rang: 3
Packt man diese Tensoren in ein weiteres Array, dann erhält man einen Rang 4 Tensor --- und so weiter. Üblicherweise werden Tensoren von 0 bis 4 verwendet, ausser bei Videodaten, hier benötigt man Tensoren vom Rang 5.
Schlüsselattribute - Key attributes:
Ein Tensor wird durch drei Schlüsselattribute festgelegt:
Anzahl der Achsen (Rang) - ein Rang 3 Tensor hat drei Achsen und eine Matrix hat zwei Achsen. Dies wird auch als des Tensors ndim bezeichnet.
Form - Shape: Dies sind Zahlenkombinationen, die beschreiben, wie viele Dimensionen ein Tensor entlang jeder Achse hat. Das vorranggegangene Beispiel hat eine Form von (3, 5). Ein Rang 3 Tensor hat beispielsweise die Form (3, 3, 5). Ein Vektor hat die Form mit einem einigen Element, zum Beispiel (5, ) und eine Skalar hat eine leere Form ().
Datentyp - Data type: Wird in Python Bibliotheken auch als dtype bezeichet. Die ist der Datentyp, den der Tensor beinhaltet. Möglich sind float16, float32, float64, uint8 und so weiter. In TensorFlow sind auch String - Tensoren möglich.
Um das konkreter zu machen, sehen wir uns das MNIST - Dataset genauer an:
Wir haben hier einen Tensor vom Rang 3 mit 8 bit Integers (ganzen Zahlen). Gan genau ein Array mit 60.000 Matrizen die aus 28 mal 28 Ganzzahlen bestehen. Jede dieser Matrizen ist ein Graustufenbild mit Werten zwischen 0 und 255. Lassen wir nun die vierte Zahl in unserem Rang 3 - Tensor anzeigen. Dazu verwenden wir die Matplotlob - Bibliothek:
Mit Tensoren in NumPy umgehen können:
Im vorigen Beispiel haben wir eine spezielle Zahl entlang der ersten Achse ausgesucht, unter Verwendung der Syntax: train_images[i]. Die Auswahl von speziellen Elementen in einem Tensor wird als "tensor slicing" bezeichnet. Nun einige Beispiele: Das folgende Beispiel wählt Zahlen von #10 bis #100 (#100 ist nicht dabei) und wandelt diese in ein Array der Form (90, 28, 28):
Slice der Zahlen von 0 bis 100: (90, 28, 28)
Detailiertere Notation für Slicing
Mit start - Index u. Index entlang der Tensorachse. Ein : ist gleichbedeutend mit der aktuellen Achse.
(90, 28, 28)
Auswahl von 14x14 Pixels von allen Bildern in der rechten unteren Ecke.
(60000, 14, 14)
Auch negative Indizes sind möglich
Es betrifft Positionen relativ zum Ende der aktuellen Achse. In diesem Beispiel geht es um die Bilder 14x14 im Zentrum der Mitte.
(60000, 14, 14)
Die Notation von Datenbatches (data batches - abzuarbeitende Daten)
Die erste Achse ist die Achse der Samples - oder auch Samples - Dimension. Im Mnist - Beispiel sind die Samples die Bilder der Zahlen. Die "Deep learning" Modelle bearbeiten nicht einen Datensatz nach dem anderen, sondern die Daten werden in kleine Häppchen aufgeteilt. Hier zeige ich einen Batch der Mnist Daten mit einer Batch - Size von 128:
Reale Beispiele von Daten - Tensoren
Vektor - Daten: Rang 2 Tensoren (samples, features), wobei jedes Sample ein Vektor von numerischen Attributen ist (features)
Zeitliche, oder sequentielle Daten: Rang 3 Tensoren (samples, timestamps, features), wobei jedes Sample eine Sequenz von feature - Vektoren ist.
Bilder - Rang 4 Tensoren (samples, height, width, channels), wobei jedes Sample ein 2D Gitter von Samples ist und jedes Pixel durch einen Vektor von Werten repräsentiert wird (channels).
Video - Rang 5 Tensoren (samples, frames, height, width, channels), wobei jedes Sample eine Sequenz von Bildern ist.
Vektor Daten
Dies ist der häufigste Fall. Jeder einzelne Datenpunkt kann als Vektor kodiert werden. Die erste Achse ist die Sample - Achse und die zweite die Feature - Achse. Beispiel: Ein Dataset von Personen, mit Alter, Geschlecht und Einkommen. Jede Person ist durch einen Vektor mit 3 Werten charakterisiert. Ein Datensatz mit 100.000 Personen kann in folgendem Rang 2 Tensor definiert werden: (100000, 3) Oder ein Dataset von Textdokumenten, wo in jedem Dokument gezählt werden soll, wie oft jedes Wort vorkommt. Als Basis dient ein Wörterbuch mit 20.000 der geläufigsten Wörter. Und wenn da zum Beispiel 500 Dokumente wären, dann kann ein Tensor mit folgender Form definiert werden: (500, 20000)
Zeitfolgen - sequentielle Daten
Wenn zeitliche Abfolgen in den Daten wichtig sind, dann macht es Sinn einen Rang 3 Tensor mit einer expliziten Zeitachse zu verwenden. Jedes Sample kann als Rang 2 Tensor eingesetzt werden und der Batch der Daten wird als Rang 3 Tensor festgelegt. Beispiel: Ein Datset von Aktienkursen. Jede Minunte wird der aktuelle Preis der Aktien gespeichert, gefolgt vom höchsten Preis in der letzten Minute, gefolgt vom niedrigsten Preis in der letzten Minute. Das heist, jede Minute gibt es einen 3D Vektor und ein Tag ist eine Matrix der Form (390, 3). An einem Handelstag gibt es 360 Minuten und gehandelt wird an 250 Tagen im Jahr. Ein Tensor der Form (250, 390, 3). Jedes Sample wäre die Daten des Tages. Oder ein Dataset von Tweets. Jedes Tweet hat eine Sequenz von 280 Zeichen aus einem Alphabet von 128 einzelnen Zeichen. In diesem Setting kann jedes Zeichen als binärer Vektor der Größe 128 festgelegt werden. Dann kann jedes Tweet als Rang 2 Tensor der Form (280, 128) festgelegt werden und ein Dataset von 1 Million Tweets kann in einem Tensor der Form (1000000, 280, 128) gespeichert werden.
Bild Daten
Normalerweise haben Bilder 3 Dimensionen: Höhe, Breite und die Farbtiefe. Nur Grauwertbilder haben nur einen Farbkanal und können in Rang 2 Tensoren gespeichert werden. Ein Batch von 128 Farbbildern kann in einem Tensor der Form (128, 256, 256, 3) gespeichert werden.
Video Daten
Hier benötigen wir Rang 5 Tensoren. Dies kommt sonst eigentlich nicht vor. Es handelt sich um ein Set von Frames, die aus Bildern bestehen. Jedes Frame kann als Rang 3 Tensor definiert werden (Höhe, Breite, Farbtiefe) und eine Sequenz von verschiedenen Videos kann in einem Rang 5 Tensor der Form (samples, frames, height, width, color_depth) gespeichert werden. Ein Youtube Video mit 60 Sekunden und 144 x 256 mit 4 Frames pro Sekunde wird in einem Tensor der Form (4, 249, 144, 256, 3) gespeichert. Dies sind 106.168.320 Werte. Und das einem Datentyp von float32 und das ergibt 405Mb.
Tensoroperationen
Alles was in einem digitalen neuronalen Netzwerk passiert, kann man auf eine handvoll Tensoroperationen herunterbrechen. Diese Operationen werden auf die numerischen Daten der Tensoren angewandt. Man kann Tensoren addieren, multiplizieren und so weiter.
Broadcasting
Passen die beiden Tensoren für die jeweilige Operation nicht zusammen, dann wird Broadcasting eingesetzt. Der kleinere Tensor wird dem größeren angepasst.
Tensorprodukt - dot product
Tensor - reshape
Gradientenbasierte Optimierung
Prinzipielles Ebenenmodell: output = relu(dot, W) + b) In diesem Ausdruck sind W und b Attribute der Ebene (kernel, bias). Dies sind die trainierbaren Parameter der Ebenen. Diese Gewichte enthalten die Informationen, welche das Modell von den Trainngsdaten gelernt hat. Bei der Initialisierung, also zu Beginn, werden diese Gewichte mit kleinen zufälligen Werten gefüllt (random initialization). Dies bedeutet nicht, dass diese Werte sinnvoll sind, aber es ist einmal ein guter Anfang. Auch die ersten Ergebnisse sind bedeutungslos, aber es ist einmal ein Ausgangspunkt. Als nächstes muss man diese Gewichte anpassen. Dies geschieht über das "Feedback" - Signal. Der Vorgang wird als trainieren bezeichnet. Das Lernen passiert in einer Trainingsschleife, die folgendermaßen arbeitet: 1 Verwende ein Paket von Trainigsdaten, x und die dazugehörigen Zielwerte y_true. 2 Lass das System für x laufen (forward pass) um Vorhersagen zu erhalten, y_pred. 3 Berechne die Fehler des Datenpaketes, Fehler zwischen y_pred und y_true. 4 Ändere alle Gewichte so, dass langsam die Fehlerquote sinkt. Möglicherweise endet dies in einem Modell, welches bei den Trainingsdaten eine geringe Fehlerquote aufweist (y_pred und die erwarteten Ergebnisse y_true). Das System hat gelernt aus den Eingaben korrekte Ergebnisse zu generieren.
Der schwierige vierte Teil
Wie soll man die Gewichte des Modells optimieren? Sollen die Werte erhöht, oder erniedrigt werden und wenn ja, um welche Beträge? Es gibt eine Methode, die uns hilft.
Gradienten - Abstiegsverfahren
Dies ist eine Optimierungstechnik, die in allen modernen neuronalen Netzwerken eingesetzt wird. Alle Funktionen in unserem Modell transformieren die Eingaben in einer vorsichtigen und kontinuierlichen Art und Weise. Sehen wir uns zum Beispiel z = x + y an. Eine kleine Änderung in y, bewirkt auch eine kleine Änderung in z. Und wenn man die Richtung der Änderung in y kennen, kann man daraus auf die Richtung der Änderung in z schließen. Mathematisch kann man sagen, die Funktion ist differenzierbar. Wenn man diese Funktion zu größeren Funktionen verkettet, dann ist diese Funktion noch immer differenzierbar. Es gilt: Eine kleine Änderung in den Koeffizienten des Modells resultiert in in einer kleinen vorhersehbaren Änderung im Fehlerwert. Dies ermöglicht es uns, einen mathematischen Operator, den man Gradient nennt, zu verwenden. Dies beschreibt, in welche Richtung der Fehler bei Änderung der Gewichte läuft. Damit können wir alle Gewichte ändern und nicht nur eines und zwar in eine Richtung, in der der Fehler minimiert wird. Was Differenzieren bedeutet kennen wir aus der Mathematik. Analytisch kann man das Minimum einer differenzierbaren Funktion dort finden, wo die erste Ableitung 0 ist. Es ist also die Aufgabe, alle Punkte zu finden, bei denen die erste Ableitung 0 ist, dies mit den Fehlern zu vergleichen und den besten Punkt zu finden, bei dem die Fehler am kleinsten sind. Für unser Netz bedeutet dies, dass wir die Gewichte je nach Steigung des Gradienten ein wenig in die Gegenrichtung ändern. Der Fehler wird immer ein wenig kleiner werden.
Die Vorgangsweise beim neuronalen Netz
1 Verwende ein Batch von Trainingssamples, x, mit den dazugehörigen Zielen, y_true
2 Lass das Modell laufen, um Vorhersagen zu treffen, y_pred (forward pass)
3 Berechne die Fehler des Batches, eine Messung der Differenz zwischen y_pred und y_true.
4 Berechne den Gradienten des Fehlers unter Betrachtung der Modellparemeter (forward pass)
5 Ändere die Parameter ein wenig in die Gegenrichtung des Gradienten, dies reduziert den Fehler ein wenig. Die Lernrate (learning_rate) ist ein skalarer Faktor, der die Änderungsgeschwindigkeit in die jeweilige Richtung vorgibt.
Das ist doch relativ einfach, bis auf eine Kleinigkeit. Es ist wichtig, den richtigen Wert für den Lernfaktor zu finden. Ist dieser zu klein gewählt, wird es viele Iterationen geben, ist er zu groß, werden die Updates irgendwo auf der Kurve liegen. In Keras gibt es von diesem Prinzip viele Varianten, aber ein grundsätzliches Verständnis haben wir nun. Diese Varianten werden auch als Optimierungsmethoden bezeichnet. Eine Basis davon ist das Momentum, um ein globales Minimum zu erreichen. Man kann sich vorstellen dass eine Kugel ein wenig angestossen wird und damit aus einem lokalen Minimum herauskommt und irgendwann, bei richtiger Dosierung im globalen Minimum landet.
Ableitungen verketten
In den vorherigen Algorithmen sind wir davon ausgegangen, dass wir den Gradienten leicht berechnen können, wenn die Funktion differenzierbar ist. Aber ist das so? Wie können wir bei einer komplexen Funktion den Gradienten berechnen? In einem 2 Ebenenmodell haben wir damit begonnen zu untersuchen, wie wir den Gradienten bekommen unter Einbziehung der Fehler und der Betrachtung der Gewichte. Hier kommt der Backpropagation Algorithmus ins Spiel.
Die Verkettungsregel
Backpropagation ist eine Möglichkeit die Ableitung von komplexen Kombinationen aus grundlegenden Rechenoperationen zu bilden. Üblicherweise besteht ein neuronales Netzwerk aus vielen Tensoroperationen, die miteinander verkettet sind. Jede Tensoroperation hat für sich eine einfache Ableitung und diese werden miteinander verkettet. Backpropagation startet mit dem letzten Fehlerwert und arbeitet sich dann bis zur ersten Ebene durch, unter Berücksichtigung des Beitrages jedes Parameters für den Fehlerwert. Heutzutage können alle modernen Frameworks, so wie TensorFlow, automatisch differenzieren, dies war nicht immer so. Dies erleichtert die Arbeit unheimlich.
Das Gradientenband (Gradient Tape) in Tensorflow
Ein API, mit dem man die Mächtigkeit der Differenzierung noch steigern kann ist das GradientTape. Es ist im Umfang von Python enthalten und es zeichnet alle Tensoroperationen auf und das ganze als Computergrafik (tape). Dies kann man dann verwenden, um die Ausgabe und jede Variable zu beobachten. Die Gewichte eines neuronalen Netzwerkes sind immer tf.Variables Instanzen.
Nun zurück zum Beispiel - Erkennen von handgeschriebenen Zahlen
Die Ausgangsbilder werden in Numpy Tensoren als float32 mit der Form (shape) (60000, 784), gespeichert. Die Trainingsdaten und Testdaten (10000, 784)
sparse_categorical_crossentropy ist die loss-function, die als Feedback für die Gewichtstensoren in der Lernphase verwendet wird. Die exakten Regeln für die Arbeit mit den Gradienten macht der Optimierer rmsprop. Und dann noch die Trainigsschleife.
Das Modell startet die Iteration in Minibatches von 128 samples und das Ganze 5 Mal, dies wird als Epochen bezeichnet. Für jeden Batch wird der Gradient der Fehler berechnet - mit dem Backpropagation Algorithmus und mit den Verkettungsregelen. Das wichtigste kennen wir nun und im nächsten Kapitel gibt es eine vereinfachte Version des Beispiels.