Zurück

Neuronale Netze mit Python und TensorFlow

 

Im Gegensatz zum vorherigen Artikel über TensorFlow, handelt sich hier eher um ein Tutorial, wie man TensorFlow verwendet um ein neuronales Netz zu erstellen und zu trainieren. Der Artikel geht davon aus, dass man über Python-Kentnisse verfügt und TensorFlow und Scikit-Learn bereits installiert hat. (Wie installiert man TensorFlow)

Wir fangen an mit einem Vergleich zwischen echten und künstlichen neuronalen Netzen:

Echte Neuronale Netzwerke

Nervenzelle; Autor: Quasar Jarosz; Licenz: CC BY-SA 3.0

Gezeigt ist ein Neuron. Im menschlichen Gehirn gibt es etwa 86 Milliarden davon. Die wichtigsten Strukturen sind:

  • Dendrite - erhalten Signale von anderen Nervenzellen, wobei ein Neuron sehr viele Dendrite haben kann.
  • Zellkörper - summiert die Signale um die Ausgabe zu generieren
  • Axon - wenn die Summe einen Schwellwert erreicht, wird ein Signal über den Axon übertragen. Nervenzelle haben immer nur einen Axon
  • Axonterminale (Synapse) - Der Verbindungspunkt zwischen Axon und Dendriten. Die Stärke der Verbindung entspricht der Stärke des übertragten Signal (synaptische Gewichte)

Künstliche Neuronale Netz

Perzeptronen und Sigmoidneurone sind die Hauptbestandteile eines neuronalen Netzes.

Bild von Perzeptronen. Einer mit, der andere ohne Bias

Ähnlich wie Neuronen, haben Perzeptronen mehrere Inputs (x) mit entsprechenden Gewichtungen (w). Die Outputs sind entweder 0 oder 1. Jeder Input wird mit einer entsprechenden Gewichtung multipliziert. Am Ende summiert man alle Ergebnisse und addiert noch die Bias (b oder Theta), welche dem Schwellwert entspricht. Angenommen, man hat N Inputs, sieht die Formel folgendermaßen aus:

Ist die Gesamtsumme größer 0, ist der Output 1; ist die Summe kleiner 0, ist der Output entsprechend 0.

Perzeptronen sind jedoch oft zu primiv. Nehmen wir an, wir wollen eine Gewichtung um einen kleinen Betrag ändern, so dass wir die Ausgabe des Netzweks entsprechend ändern. Bei Perzeptronen kann eine kleine Änderung der Gewichtungen dazu führen, dass der Output eines Neurons z.B plötzlich von 0 auf 1 umschlägt. Dies könnte das Verhalten des gesamten Netzwerks in einer unerwarteten Weise verändern. Um damit umzugehen, verwenden wir Sigmoidneuronen in neuronalen Netzen. Diese Art von Neuronen haben auch eine Aktivierungsfunktion, die als Sigmoidfunktion bezeichnet wird.

Diese Funktion erlaubt, dass der Output der Sigmoidneuronen auch Zahlen zwischen 0 und 1 beinhaltet, statt nur 0 oder 1 wie im Perzeptron. Der große Vorteil ist, dass somit kleine Anpassungen an den Gewichtungen und Biasen nur in kleinen Abweichungen in der Ausgabe resultieren.

Neuronales Netz von Glosser.ca, lizensiert unter CC BY-SA 3.0

Ein neuronales Netz besteht aus mehrere Schichten, wobei jede Schicht aus mehreren Neuronen besteht. In der Regel hat jedes Neuron aus einer Schicht Verbindungen zu allen anderen Neuronen aus der nächsten Schicht. Die erste Schicht ist die Inputschicht, wo der Datensatz eingeht. Dahinter können beliebig viele versteckte Schichten liegen, aber die letzte ist die Outputschicht, mit 1 oder mehreren Neuronen (Die Anzahl hängt vom Problem ab, dass man lösen möchte ab).

Funktionsweise von TensorFlow

Die Hauptdatenstruktur in TensorFlow ist ein Tensor. Hierbei handelt es sich um ein Array von primitiven Datentypen, die in eine beliebige Anzahl von Dimensionen geformt sind. Der Rang eines Tensoren ist die Anzahl seiner Dimensionen.

# Tensor Beispiele

3 # Rang 0
[1, 2, 3] # Rang 1
[[1,2,3], [1,2,3]] # Rang 2
[[[1,2,3], [1,2,3]]] # Rang 3

Einfache Arten von Tensoren sind Konstanten und Unbekannten (Constant, Variable). Konstanten sind Tensoren, deren Wert nicht verändert werden kann, sprich während der Ausführung des gesamten Programms behalten sie den gleichen Wert. Im Gegensatz dazu können die Werte von Variablen geändert werden, weshalb sie als Gewichte und Bias für die verschiedenen Schichten der Modelle verwendet werden.

Es ist auch wichtig, die Tensoren zu erwähnen, die das Ergebnis einer Operation mit anderen Tensoren enthalten. Beispiele sind Addition, Multiplikation oder die Anwendung einer komplexeren Funktion.

All dies bildet eine Art Graph, der aus allen Inputs, Outputs und Operationen dazwischen besteht. Die Knoten im Graphen können 0 oder mehr Tensoren aufnehmen und haben einen Tensor als Output. Um die gleiche Analogie wie die offizielle Seite zu verwenden, kann man einen Graphen als eine Quellcode-Detei beschreiben. Diese enthält Definitioen, die Datentypen von den Ein- und Ausgaben jeses Tensors, und die Reihenfolge der Operationen. Die Objekte im Graphen können jedoch nicht ausgewertet werden und müssen zuerst "kompiliert" werden.

import tensorflow as tf

a = tf.constant(2.0, name="const_2", dtype=tf.float32)
b = tf.constant(3.0, name="const_3", dtype=tf.float32)
c = a + b

# Das zeigt uns nur, dass dieser Tensor die Operation Addition asuführt
print(c)
 
Tensor("add_1:0", shape=(), dtype=float32)
 

Es ist zu sehen, dass print(c) nur eine Beschreibung des Tensors liefert. Um sie tatsächlich auszuwerten, benötigen wir ein sogenanntes Session-Objekt. Das Session-Objekt ist wie eine ausführbare Datei. Es kapselt die Umgebug, in der die Graph-Objekte ausgeführt und ausgewertet werden, durch Zuweisung von Ressourcen und ggf. Verteilung der Ausführung an verschiedene Geräte.

Dieser Ansatz bietet die Möglichkeit, das Modell separat und parallel auf mehreren CPUs und GPUs laufen zu lassen, was zu schnellere Trainingszeiten führt. Außerdem hat man die Möglichkeit Teile des Graphen für andere Modelle wiederzuverwenden, oder den ganzen Graphen mittels TensorBoard zu visualisieren.

# mit einer Session können wir die Berechnung ausführen und das Ergebnis ausgeben
sess = tf.Session()
sess.run(c)
5.0
 

TensorBoard Visualisierung des Graphen des oben definierten Programm

 

Implementierung eines neuronalen Netzes

Nun werden wir ein einfaches neuronales Netz mit TensorFlow implementieren. Zusätzlich brauchen wir aber einige Funktionen aus scikit-learn. Wir werden den Iris-Datensatz benutzen und versuchen mit dem neuronalen Netz die Blumen richtig zu klassifizieren.

Als erstes wollen wir TensorFlow und einige Funktionen aus scikit-learn importieren. Dann laden wir den Irisdatensatz. Wichtig ist reshape((-1, 1)) auf den Blumenklassen auszuführen, da sie im Moment in einem eindimensionalen Array liegen, aber die OneHotEncoder.fit_transform() Funktion, die wir später benötigen, erfordert ein zweidimensionales Array als Eingabe.

import tensorflow as tf
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split

# Datensatz
data = load_iris()
features = data.data
labels = data.target.reshape((-1, 1))

# Klassen von Blumen zeigen
labels
array([[0],
       [0],
       [0],
       ...
       [0],
       [1],
       ...
       [1],
       [2],
       ...
       [2]])
 

One-Hot Encoding

Wie man hier sieht, sind die Blummenklassen mit 0, 1 oder 2 bezeichnet. In der Regel, wenn man neuronale Netze für Klassifizierungsprobleme benutzen will, hat man so viele Neuronen in der letzten Schicht wie Klassen. Wir erwarten für jede Stichprobe eine Ausgabe, die so Aussieht [0.15, 0.70, 0.15]. In diesem Beispiel ist die Blume der Klasse 1, weil die zweite Zahl im Array die größte ist.

Wir wollen aber die Vorhersagen des neuronalen Netzes mit den echten Klassen vergleichen, deswegen wandeln wir die numerische Darstellung der Klassen in ein sogenanntes One-hot encoding um. So hat man ein 3-Array als Bezeichner für jede Klasse.

Beispiel:

Klasse 0 -> [1, 0, 0]
Klasse 1 -> [0, 1, 0]
Klasse 2 -> [0, 0, 1]

Wir verwenden dafür die OneHotEncoder von scikit-learn

from sklearn.preprocessing import OneHotEncoder

enc = OneHotEncoder(sparse=False)
enc.fit_transform(labels)
array([[1., 0., 0.],
       [1., 0., 0.],
       ...
       [1., 0., 0.],
       [0., 1., 0.],
       [0., 1., 0.],
       ...
       [0., 1., 0.],
       [0., 0., 1.],
       ...
       [0., 0., 1.]])
 

Trainings- und Testdaten

Um das Modell gut validieren zu können, müssen wir Daten verwenden, mit denen nie trainiert worden ist. Deswegen benutzen wir die train_test_split Funktion, um 20 Prozent der Daten als Testdaten zu nehmen. Dann merken wir uns die Anzahl von Merkmale bzw. von Klassen. In diesem Fall ist x_size = 4 und y_size = 3

# Trainings- und Testdaten erzeugen
train_x, test_x, train_y, test_y = train_test_split(
                                features, 
                                enc.transform(labels) )

x_size = train_x.shape[1]
y_size = train_y.shape[1]
 

Placeholder (Platzhalter) sind Tensors, die für die Dateneingabe sorgen. Man muss nur die Dimensionen spezifizieren. Zum Beispiel sind hier die Dimensionen für X [None, x_size], weil wir eine unbestimmte Zahl von Stichproben haben mit jeweils x_size Merkmalen.

In X geben wir die Merkmale und in Y die richtigen Klassen ein, damit wir das Modell trainieren können.

X = tf.placeholder(tf.float32, shape=[None, x_size])
Y = tf.placeholder(tf.float32, shape=[None, y_size])
 

Für das Modell des neuronales Netzes benutzen wir 2 versteckte Schichten, mit jeweils 128 Neuronen. Für die versteckten Schichten haben wir die sigmoid-Akitivierungsfunktion ausgewählt. Eine Akitvierungfunktion für die Ausgabenschicht wird hier jedoch nicht definiert, da die Konstenfunktion diese später übernimmt.

Die softmax Funktion funktioniert nicht so gut für die Zwischenschichten, deswegen haben wir die sigmoid Aktivierungsfunktion ausgewählt.

TensorFlow bietet viele Werkzeuge an, mit denen man ein Modell erstellen kann. Wir schauen uns hier zwei Möglichkeiten an. Zunächst definieren wir die Schichten mit TensorFlow Core. Man braucht mehr Codezeilen, dafür aber hat man mehr Kontrolle über das Program. Alternativ verwenden wir die API tf.layers, womit man schnell neue Schichten definieren kann.

TensorFlow Core

Um schneller das Modell erstellen zu können, definieren wir eine eigene Funktion hidden_layer, mit den folgenden Parametern.

  • input - Der Eingabetensor oder auch die vorige Schicht.
  • shape - Ein 2-Array mit den Dimensionen der Schicht. Das erste Element ist die Neuronenanzahl der vorigen Schicht und das zweite - die Neuronenanzahl der aktuellen Schicht.
  • activation - Aktivierungsfunktion. Da wir für verschiedene Schichten, unterschiedliche Aktivierungsfunktionen verwenden wollen, brauchen wir diese als Parameter.

Mit random_normal(shape) generieren wir zufällige Werte mit bestimmten Dimensionen für Gewichtungen und Biases. Danach bilden wir Unbekannten, deren Werte man mit einem Optimierer anpassen kann.

Die algebraische Operationen, die im neuronalen Netz stattfinden, sind folgenderweise definiert:

  • man multipliziert das Input mit den Gewichtungen
  • man addiert die Biases dazu
  • man wendet die Aktivierungsfunktion an das Ergebnis an

Die Dimensionen der Gewichtungen und der Biases in den versteckten Schichten unterscheiden sich. In der ersten Schicht hat der Input die Dimensionen 1x4, weil jede Stichprobe 4 Merkmale hat. Die Gewichtungen haben die Dimension 4x128, weil wir 128 Neuronen haben und in jedem sollen wir eine Gewichtung pro Merkmal haben. Nachdem wir tf.matmul() ausführen, kommt ein Ergebnis heraus mit den Dimensionen 1x128. Um die Biases dazu zu addieren, müssen sie dieselbe Dimensionen haben. Bei einer Stichprobe x, Gewichtungen für jedes Neuron W, und Bias b, sieht die Formel für die erste versteckte Schicht folgendermaßen aus:

Die tf.layers API

Mit tf.layers ist es sehr einfach ein neuronales Netz zu definieren. tf.layers.dense bietet uns das typische Schichtenmodell (Fully Connected Layer) und behandelt automatisch die Addition von Biases und die Berechnung von Dimensionen. Wir geben nur den Eingabetensor, die Neuronenanzahl und die Aktivierungsfunktion ein.

Es gibt auch andere Parameter, die man anpassen kann, die aber für uns im Moment nicht relevant sind.

# NN Modell mit TF Core
def hidden_layer(inputs, shape, activation=None):
    weights = tf.Variable(tf.random_normal(shape))
    biases = tf.Variable(tf.random_normal([1, shape[1]]))
    
    if activation:
        return activation(tf.add(tf.matmul(inputs, weights), biases))
    else:
        return tf.add(tf.matmul(inputs, weights), biases)

core_layer1 = hidden_layer(X, [x_size, 128], tf.nn.sigmoid)
core_layer2 = hidden_layer(core_layer1, [128, 128], tf.nn.sigmoid)
core_y_hat = hidden_layer(core_layer1, [128, y_size], None)
# MM Modell mit tf.layers
# wir wollen die Variablen nicht überschreiben
h_layer1 = tf.layers.dense(X, 128, activation=tf.nn.sigmoid)
h_layer2 = tf.layers.dense(h_layer1, 128, activation=tf.nn.sigmoid)
y_hat = tf.layers.dense(h_layer2, y_size, activation=None)
 

Kostenfunktion, Optimierer und Session

Wir brauchen jetzt eine Kostenfunktion um den Fehler zwischen den Vorhersagen und den echten Klassen zu berechnen. Die TensorFlow Funktion softmax_cross_entropy_with_logits_v2() ist dem quadratischen Fehler ähnlich, mit dem Unterschied, dass sie die Eingaben als eine Wahrscheinlichkeitsverteilung interpretiert. Zuerst wird eine softmax Funktion auf den Eingaben ausgeführ. Diese erlaub uns eine Wahrscheinlichkeit für die Ausgabenklassen zu definieren, da die Summe aller Elemente aus der Ausgabeliste wegen der Aktivierungsfunktion gleich 1 ist.

Beispiel für Ausgabe: [0.10, 0,78, 0,12].

Das heißt, dass das Modell zu 78% sicher ist, dass die aktuelle Stichprobe der Klasse 1 entspricht. Die Vorhersage wird mit der realen Klasse verglichen und der Fehler berehnet. Da die Eingaben der Kostenfunktion als eine Wahrscheilichkeitsverteilung interpretert werden, ist die Funktion eher für Klassifikationsprobleme geeignet.

Die Rückgabewert dieser Funktion ist eine Liste mit den Fehlern der Vorhersage. Wir verwenden dann die Funktion reduce_mean(), um den Mittelwert dieser Fehler zu berechnen, so dass wir mit nur einer Zahl arbeiten können.

Danach brauchen wir einen Optimierer, damit wir die Gewichtungen in den vorigen Schichten anpassen können. Der GradientDescentOptimizer kann hier gut eingesetzt werden und hat zum Ziel, die Kosten zu minimieren. Als Argument gibt man die Lernrate ein. Diese gibt an, wie stark sich die Gewichtungen bei jeder Iteration des Optimierers ändern können. Man sollte darauf achten, dass eine kleine Lernrate das Training zwar zuverlässiger machen kann, aber die Optimierung länger dauern wird. Eine hohe Lernrate hingegen könnte zu einem ungenaeun Modell führen. Durch die großen Änderungen der Gewichtungen kann der Optimierer die besseren WErte überschreiten und somit die Kosten verschlechtern.

Bevor wir mit der Ausführung des Programms starten, müssen wir die Unbekannten initializieren. Diese sind die Gewichtungen und Biases in den Neuronenschichten, die der Optimierer anpasst, um die Kosten zu minimieren. Die Unbekannten haben am Anfang keine Werte, deshalb müssen wir den global_variables_initializer() ausführen, um ihnen zufällige Werte zuzuweisen.

# Diese Variable modifizieren um die zwei Modelle zu testen
# (y_hat ODER core_y_hat)
y_pred = y_hat

# Kosten- und Optimierungsfunktion
loss = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(
            labels=Y, logits=y_pred))

train_step = tf.train.GradientDescentOptimizer(0.0005).minimize(loss)

# Unbekannten initialisieren und Session erstellen
sess = tf.Session()
sess.run(tf.global_variables_initializer())
 

Trainingsschleife

Die erste Schleife ist für die Anzahl der Epochen. In diesem Fall wollen wir 300 mal alle Stichproben dem neuronalen Netz eingeben und zwar einer nach dem anderen (die zweite Schleife).

Zum ersten führen wir session.run() mit train_step aus, also man berechnet die Kostenfunktion und den Optimierer. Deswegen geben wir die Trainingsdaten ein und alle 10 Epochen wollen wir die Genauigkeit evaluieren.

Dafür verwenden wir tf.argmax um die Stelle der größten Zahl bei der Vorhersage und bei den echten Klassen zu vergleichen. Das gibt uns eine Liste von booleschen Werten (True oder False). Mithilfe von cast(tf.float32), wandeln wir die boolesche Werte in 1 oder 0 um. Schließlich berechnen wir den Mittelwert dieser Liste. So bekommen wir eine Genauigkeit als Prozent.

# Trainingschleife 
for epoch in range(300):
    for i in range(train_x.shape[0]):
        sess.run(train_step, feed_dict={X: train_x[i:i+1], Y: train_y[i: i+1]})                                                 
        
    if (epoch % 10 == 0):
        correct_prediction = tf.equal(tf.argmax(y_pred, 1), tf.argmax(Y, 1))                                                      
        accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))                                                       
        print("Epoch %d: Accuraccy = %f, Loss = %f" % (                                                                          
                          epoch,                                                                                                   
                          sess.run(accuracy, feed_dict={X: test_x, Y: test_y}),                                                    
                          sess.run(loss, feed_dict={X: train_x, Y: train_y})))
    
 
Epoch 0: Accuraccy = 0.315789, Loss = 1.159480
Epoch 10: Accuraccy = 0.263158, Loss = 1.083721
Epoch 20: Accuraccy = 0.263158, Loss = 1.072558
Epoch 30: Accuraccy = 0.289474, Loss = 1.061280
Epoch 40: Accuraccy = 0.631579, Loss = 1.049726
Epoch 50: Accuraccy = 0.684211, Loss = 1.037734
Epoch 60: Accuraccy = 0.684211, Loss = 1.025150
Epoch 70: Accuraccy = 0.684211, Loss = 1.011829
Epoch 80: Accuraccy = 0.684211, Loss = 0.997632
Epoch 90: Accuraccy = 0.684211, Loss = 0.982428
Epoch 100: Accuraccy = 0.684211, Loss = 0.966103
Epoch 110: Accuraccy = 0.684211, Loss = 0.948561
Epoch 120: Accuraccy = 0.684211, Loss = 0.929738
Epoch 130: Accuraccy = 0.684211, Loss = 0.909607
Epoch 140: Accuraccy = 0.684211, Loss = 0.888190
Epoch 150: Accuraccy = 0.684211, Loss = 0.865571
Epoch 160: Accuraccy = 0.684211, Loss = 0.841893
Epoch 170: Accuraccy = 0.684211, Loss = 0.817365
Epoch 180: Accuraccy = 0.684211, Loss = 0.792254
Epoch 190: Accuraccy = 0.736842, Loss = 0.766866
Epoch 200: Accuraccy = 0.736842, Loss = 0.741530
Epoch 210: Accuraccy = 0.763158, Loss = 0.716568
Epoch 220: Accuraccy = 0.763158, Loss = 0.692276
Epoch 230: Accuraccy = 0.763158, Loss = 0.668899
Epoch 240: Accuraccy = 0.789474, Loss = 0.646622
Epoch 250: Accuraccy = 0.789474, Loss = 0.625565
Epoch 260: Accuraccy = 0.789474, Loss = 0.605784
Epoch 270: Accuraccy = 0.789474, Loss = 0.587283
Epoch 280: Accuraccy = 0.815789, Loss = 0.570026
Epoch 290: Accuraccy = 0.842105, Loss = 0.553942
 

Man sieht, dass die Genauigkeit steigt und die Kosten sinken. Man kann auch die Lernrate und die Anzahl von Neuronen in einer Schicht anpassen um die Änderungen in der Genauigkeit zu veranschaulichen.

Was kann man verbessern?

  • Man kann immer die Anzahl von Schichten und Neuronen anpassen
  • Die Lernrate kann auch angepasst werden. Noch besser ist es, die Lernrate dynamisch einzustellen, inde man mit einem höheren Wert beginnt und ihn während des Trainings verringert.
  • Um bessere Modelle zu bekommen, sollte man die Daten bei jeder Epoche mischen.
  • Für bessere Laufzeit, sollte man die Stichproben nicht eine nach den anderen dem neuronalen Netzes eingeben, sondern die in Stapel zusammenfassen. Die größe der Stapel hängt von der Anzahl von Merkmalen und dem vorhandenen Haupt- oder Videospeicherplatz ab.

Ressoursen

  • Git Repository mit dem kompletten Code - Link
  • Wie wählt man eine optimale Anzahl von Zwischenschichten und deren Nuronen? (Enghlisch) - Link
Weiter
Kommentare
Trackback-URL:

Noch keine Kommentare. Seien Sie der Erste.

Ancud IT-Beratung GmbH
Glockenhofstraße 47 
90478 Nürnberg 

Tel.: +49 911 2525 68-0