Neuronales Netz – Erklärung des Beispielcodes

Edit: Sorry, der Link für das Beispiel von joelgrus war kaputt, ich habe ihn wieder aktualisiert. 

Neuronale Netze sind wirklich nicht einfach zu verstehen. Bereits die Theorie ist schwierig und vielfältig, und dann soll man das Ganze noch in der Python Implementation verstehen? Ich selber habe schon stundenlang den Code studiert und bin doch immer wieder verwirrt.

In diesem Beitrag werde ich das Beispiel aus dem Buch Einführung in Data Science erklären. Ich werde den Code auseinandernehmen, alles Unwichtige löschen, deutsche Kommentare hinzufügen und mich auf das Wesentliche beschränken.

So hoffe ich, kann ich den Code herunterbrechen, als dass auch Nicht-Albert-Einsteins eine Chance haben, diesen zu verstehen.

Das Beispiel ist öffentlich verfügbar unter dieser Adresse:
https://github.com/joelgrus/data-science-from-scratch/blob/master/first-edition/code-python3/neural_networks.py

Mein angepasster Code ist ganz unten in diesem Beitrag zu finden und kann direkt etwa in PyCharm laufen gelassen werden.

Was für ein neuronales Netzwerk baut das Beispiel?

Der Beispielcode baut ein neuronales Netzwerk, um die Zahlen zwischen 0 und 9 zu erkennen – Im Buch ist quasi die Idee, bei einem Captcha die Zahlen herauszufinden.

Wie sehen die Zahlen 0-9 als Eingabe aus?

Konkret hat man jede der 10 Zahlen in einem Vektor mit jeweils 25 Zeichen. Na, das kann man sich ja jetzt super vorstellen.

Eine Null sieht etwa so aus:

"""11111
   1...1
   1...1
   1...1
   11111""",

Und eine Vier so:

"""1...1
   1...1
   11111
   ....1
   ....1"""

Und so weiter. Wenn man das neuronale Netz trainiert, werden die Punkte in Nullen umgewandelt und das Ganze ist dann ein langer Vektor.

Für das Training des Netzwerkes wird aus der Null also dann sowas:

[1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1]

Das Ziel ist, dass man am Ende dem Netz einen Vektor geben kann, und wenn dieser ähnlich wie dieser Nullvektor aussieht (er muss nicht genau übereinstimmen), sagt das Netz korrekt eine Null vorher.

Wie sieht denn die Ausgabe aus?

Die Eingabe ist also ein 25-stelliger Vektor für jede Zahl 0 bis 9. Die Ausgabe ist ein 10-stelliger Vektor mit lauter Nullen ausser jeweils bei der Zahl, die er voraussagen will.

Eine Null hat also diesen Eintrag:

[1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Und eine 4 hat diesen Eintrag:

[0, 0, 0, 0, 4, 0, 0, 0, 0, 0]

Wie üblich beginnt ein Vektor mit der Position 0, nicht bei 1.

Wie wird die Ausgabe eines Neurons berechnet?

Wie in meinem Beitrag Neuronale Netze Tutorial – Übersicht der Konzepte oder ML: Was ist der Perzeptron Lernalgorithmus? beschrieben, wird auch hier für jedes Neuron zuerst die Nettoeingabe berechnet (= Skalarprodukt) und dann alles in die Sigmoid Funktion gehauen, was einen Wert zwischen 0 und 1 für die folgende Schicht ergibt.

Kurze Repetition Skalarprodukt: Jedes Neuron hat ja etliche, sagen wir beispielsweise 26 Verknüpfungen, welche von der vorherigen Schicht in dieses Neuron hineinführen – das sind die x-Werte. Und jede Verknüpfung hat ein Gewicht, was ja eben die Gewichte sind, die wir mit dem Netz trainieren wollen – Das sind die w-Werte.

Und das Skalarprodukt ist = x1 * w1 + x2 * w2 + x3 * w3 etc.

Das ergibt also eine Zahl, die man in die Sigmoid-Funktion gibt und etwas zwischen 0 und 1 herausbekommt.

Im Code werden die Gewichte weights genannt und die x-Werte einfach inputs.

def dot(v,w):
 """ Skalarprodukt zweier Vektoren = Summe der Produkte 
 der Komponenten
 v_1 * w_1 + v_2 * w_2 + ... + v_n * w_n """
 return sum(v_i * w_i
 for v_i, w_i in zip(v,w))

def sigmoid(t):
 return 1 / (1 + math.exp(-t))

def neuron_output(weights, inputs):
 """Hier passiert das Multiplizieren der Gewichte 
 w * Inputs i und das Anwenden der Sigmoid Funktion.
 Die Gewichte sind um 1 länger als die Inputs
 weil das Bias noch am Ende angehängt wird"""
 return sigmoid(dot(weights, inputs))

Moment mal, wieviele Schichten und Neuronen haben wir?

Bei der Initialisierungphase des Codes sehen wir, wie das neuronale Netz aufgebaut wird:

random.seed(0) # Setze seed damit jeder Durchlauf gleich ist
 input_size = 25 # Jede Eingave ist ein Vektor mit Länge 25
 num_hidden = 5 # Wir haben 5 Neuronen in der versteckten Schicht
 output_size = 10 # Wir haben 10 Neuronen in der Ausgabeschicht

Wie man hier sieht, haben wir drei Schichten:

  • Die Eingabeschicht sind die oben erwähnten 25-langen Vektoren.

Also moment mal. Das bedeutet: EIN 25-langer Vektor ist auch nur EINE Zahl, die man „gleichzeitig“ trainiert. Anders gesagt: Man trainiert zuerst die 0, dann die 1, bis zur 9 und fängt dann wieder bei 0 an.

  • Die versteckte Schicht hat 5 Neuronen.

Warum genau 5? Hmm keine Ahnung – Aber auf jeden Fall kann man diesen Wert später auch ändern. Grundsätzlich sollte man so programmieren, dass man immer dynamisch auf den verwendeten Wert reagiert. Egal ob es jetzt 5 oder 10 Neuronen in dieser Schicht sind, der Code sollte deswegen nicht abstürzen.

  • Die Ausgabeschicht hat wie bereits erwähnt 10 Ausgabeneuronen, nämlich genau die 10 Zahlen, die wir voraussagen wollen.

Wie wird der Eingabe- und Ausgabevektor initialisiert?

Für die Eingabewerte, also diese komischen 25-stelligen Vektoren, wird die Python Funktion map verwendet. Diese führt für jede der raw_digits die Funktion make_digit aus, und die wiederum ändert jeden Punkt zu einer 0.

# map function: Fuehre die Funktion make_digit mit jedem
 # raw_digits Teil aus und gib es zurück
 # Auf Deutsch: Mach aus jedem Punkt der raw_digits eine 0
 inputs = list(map(make_digit, raw_digits))

Der Ausgabevektor beziehungsweise Zielvektor braucht die bereits erwähnten 10 Einträge der Zahlen 0-9 in dieser speziellen Vektorform, bei der die jeweilige Zahl eine 1 ist und alle andern eine 0.

# Die richtigen Ausgaben = 10 Zeilen
 # vier ist etwa
 # 0 1 2 3 4 5 6 7 8 9
 # [0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
 targets = [[1 if i == j else 0 for i in range(10)]
 for j in range(10)]

inputs und targets können dann für das Trainieren des Netzes verwendet werden.

Wie wird das neuronale Netz gebaut und initialisiert?

Eine einzige Variable namens network vereint alle Informationen zum Netzwerk. Als oberste Stufe ist es eine Liste mit zwei Einträgen, nämlich den 0-er Index für die versteckte Schicht und den 1-er Index für die Ausgabeschicht.

network

Im Screenshot sieht man schön die zwei obersten Einträge für die Schichten, dann jeweils 5 beziehungsweise 10 Einträge für die Neuronen.

Ich bin unzählige Male durcheinandergekommen, welcher Eintrag jetzt warum wieviele Untereinträge hat. Darum hier ein Screenshot vom Netz der versteckten Schicht. Man sieht, dass jedes Neuron 25 Gewichte trägt.

network-input-weights

Und hier ein Screenshot von der Ausgabeschicht. Jedes Neuron trägt die Gewichte der 5 vorherigen Neuronen der versteckten Schicht.

network-output-weights

Für die Gewichte werden übrigens zufällige Werte genommen:

# initialisiere hidden und output weights mit random Werten
 # ---------------------------------------------------------
 # jedes versteckte Neuron hat ein Gewicht pro Eingabe plus 
 # ein Bias Gewicht
 hidden_layer = [[random.random() for __ in range(input_size + 1)]
 for __ in range(num_hidden)]

# jedes Ausgabe-Neuron hat ein Gewicht pro Eingabe plus 
# ein Bias Gewicht
 output_layer = [[random.random() for __ in range(num_hidden + 1)]
 for __ in range(output_size)]

# das Netzwerk startet mit zufälligen Gewichten
 network = [hidden_layer, output_layer]

Bei beiden Schichten wird noch ein Bias hinzugefügt, damit die Werte nicht durch den Nullpunkt gehen (wenn man diesem https://www.youtube.com/watch?v=biLLUSkohiY Video glauben mag. Das muss ich später noch einmal untersuchen).

Zwischen der Eingabe- und der versteckten Schicht haben wir dann also

26 * 5 = 130 Gewichte

Zwischen der versteckten und der Ausgabe-Schicht haben wir

6 * 10 = 60 Gewichte

So, der einfache Teil ist somit gemacht. Jetzt kommen nur noch zwei schwere Teile und das neuronale Netz ist fertig.

Wie wird das Netz trainiert und später verwendet?

Das Trainieren wird mit der Funktion backpropagate gemacht. Dieser übergibt man das Netzwerk, den Eingabe- und Ausgabe-Vektor.

# Trainiere das neuronale Netz
 # 10,000 Iterationen sind genug um zu konvergieren
 for __ in range(10000):
   for input_vector, target_vector in zip(inputs, targets):
     backpropagate(network, input_vector, target_vector)

Hier sieht man auch den Code, den ich schon erwähnt hatte: Für jede Zahl im Eingabevektor (also die Zahlen von 0-9) wird die Funktion backpropagate aufgerufen – aber stets einzeln, da eine Zahl ja ein 25-stelliger Vektor von 0en und 1en ist. Und das ganze 10’000 Mal, um die Gewichte zu trainieren.

Das Vorhersagen oder Bestimmen von Zahlen wird mit der Funktion predict gemacht. Dieser übergibt man einen 25-stelligen Vektor und erhält dann die Antwort, um welche Zahl es sich gemäss Netz handelt.

def predict(input):
 # Das Resultat der Ausgabe ist im letzten Index, also im -1
 return feed_forward(network, input)[-1]

Total easy oder! Haha… Ja, dieser Code ist noch easy, der komplizierte Teil ist aber in diesen beiden Funktionen, und die schauen wir uns jetzt an.

Die Funktion feed_forward

Wir beginnen mit dieser Funktion, da sie dann von der Funktion backpropagate verwendet wird (und auch einfacher zu verstehen ist, die schwerste Funktion kommt quasi als „Endboss“ am Schluss).

Ich habe dem Code tonnenweise Kommentare hinzugefügt. Das mache ich normalerweise natürlich nicht, sondern nur, wenn ich etwas nicht verstehe.
(Also mache ich es eigentlich fast immer… nein nein kleiner Scherz 😉 )

Jetzt muss ich nochmals einen wichtiger Punkt erwähnen: Die Variable neural_network in dieser Funktion beinhaltet die gesamte Struktur des Netzes – und das sind drei Ebenen: Zuerst die zwei Schichten (versteckte und Ausgabeschicht), dann für jede Schicht die entsprechenden Neuronen und dann noch für jedes Neuron die Gewichte aller Verknüpfungen, die in dieses Neuron führen.

3 Ebenen: Schichten -> Neuronen -> Gewichte

Darum wird in der Funktion feed_forward zuerst über die zwei Schichten iteriert. Dann wird mit der Super-Sigmoid-Funktion neuron_output der Ausgabewert jedes Neurons berechnet, wie am Anfang des Posts erklärt.

Und wie schon gesagt bedeutet das, für jedes Neuron werden seine Gewichte mit den Eingabewerten multipliziert und addiert (Skalarprodukt) und das Resultat in die Sigmoid-Funktion geprügelt.

Die Gewichte haben ja bereits das Bias erhalten, deshalb muss jetzt der Eingabevektor auch ein Bias erhalten, damit die Vektoren die gleiche Länge haben und skalarproduktet werden können.

def feed_forward(neural_network, input_vector):
 """Nimmt ein Neuronales Netzwerk entgegen - Darin sind 
 die Gewichte gespeichert!
 Es ist eine list (Schichten) of lists (Neuronen) of lists (weights)
 Gehe durch jeden Layer und dann durch jeden Neuron.
 Für jeden Neuron, berechne das Skalarprodukt und 
 dann den Sigmoid Wert"""

  outputs = []

# gehe durch die beiden Layers Hidden + Output
 for layer in neural_network:

# Fuege wieder das Bias hinzu
   input_with_bias = input_vector + [1]

 # Gehe durch jedes Neuron und aktualisiere die Gewichte 
 # mit Skalarprodukt
 # dann rechne noch für jedes Neuron den Sigmoid Wert aus
 # Für den Hidden Layer gibt das nur noch 5 Werte, weil 
 # es 5 Neuronen sind
 # (Einmal die Sigmoid Funktion pro Neuron)
 # Für den Output Layer gibt es 10 Werte
   output = [neuron_output(neuron, input_with_bias)
            for neuron in layer]

 # Speichere die zwei Output-Resultate (eines pro Layer)
   outputs.append(output)

 # Der Input zum nächsten Layer ist der Output von diesem Layer
 # So werden die Ausgaben vom Hidden Layer gleich an 
 # den Output Layer weitergereicht
   input_vector = output

return outputs

Ein ganz ganz wichtiger Punkt, den man hier im Code sieht, ist in der zweitletzten Zeile. Für die Sigmoidifizierung der versteckten-Schicht-Neuronen werden nämlich die Zahlen aus dem 25-stelligen Eingabevektor verwendet (was einer Zahl, etwa der 0, entspricht). Die Ausgabe der Neuronen der versteckten Schicht wird dann als Eingabe für die Berechnung der Sigmoid-Werte der Ausgabeschicht verwendet.

Wir erinnern uns: Jedes Neuron der Ausgabeschicht hat 6 Verknüpfungen von der vorherigen Schicht (5 Neuronen + Bias) und benötigt daher auch 6 x-Werte und Gewichte für die Sigmoid-Berechnung.

Und wie sieht outputs am Ende der Funktion aus? Nun wir haben die 5 berechneten Werte für die Neuronen der versteckten Schicht und 10 Werte für die Neuronen der Ausgabeschicht.

berechnete_outputs

Man beachte, dass die Funktion feed_forward den Target-Vektor nicht benötigt. Stattdessen nimmt es einen Eingabevektor, rechnet das Netz durch alle Schichten gemäss den Gewichten und am Ende erhält man in outputs[-1] die Vorhersage der Zahl 0 bis 9 (vorausgesetzt natürlich, das Netz wurde trainiert).

Die Funktion backpropagate

Bei dieser Funktion hatte ich am meisten Mühe, sie zu verstehen. Darum habe ich sie auch regelrecht mit Kommentaren vollgemüllt.

Ich werde versuchen, den Code Schritt für Schritt zu erklären – Ich bin nämlich fest überzeugt, dass die Schritte „theoretisch“ nicht sooo kompliziert sind.

Was macht die Funktion grundsätzlich? Es rechnet einmal durch das gesamte Netz, indem es die Funktion feed_forward aufruft. Die berechneten Werte verwendet sie dann, um die Fehler oder Deltas der aktuellen Gewichte herauszufinden und diese zu korrigieren.

Okay, ganz langsam… tief Luft holen…

Die Inputparameter der Funktion kennen wir immer noch: Es sind das neuronale Netz, dann der aktuelle Zahl (etwa der 25-stellige Vektor der Zahl 0) und den 10-stelligen Zielvektor der aktuellen Zahl.

Wenn also der Eingabevektor die 25 Zahlen für die 0 sind, dann steht in target folgendes:

input_vector = [1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1]
target = [1, 0, 0, 0, 0, 0, 0, 0, 0, 0]

Zuerst wird also die Funktion feed_forward aufgerufen und die berechneten Ausgabewerte der beiden Schichten entnommen.

Dann werden die Gewichte für die Ausgabe- und die versteckte Schicht aktualisiert.

Für die versteckte Schicht und die Ausgabeschicht sind es jeweils die gleichen zwei Schritte, die gemacht werden:
1. Zuerst wird der Fehler berechnet, den die Gewichte momentan noch haben.
2. Dann aktualisiere jedes Gewicht um den berechneten Fehler

Die Gewichte der Ausgabeschicht

Das Berechnen der Fehler, oder Deltas, wie sie im Code genannt werden, geschieht mit den von der feed_forward berechneten Ausgabewerte und dem Zielwert.

Einfach gesagt macht man folgendes:

Vorhandener Fehler = Berechneter Wert minus Zielwert

Wäre der berechnete Wert nämlich bereits der gewünschte Wert, würde die Rechnung Null ergeben und somit wäre auch der Fehler Null.

Was jetzt hier noch dazukommt, ist die Ableitung der Sigmoidfunktion. Das hat mit dem Gradientenverfahren zu tun. Das habe ich in meinem Artikel Neuronale Netze Tutorial – Übersicht der Konzepte unter dem Punkt 9. Oh Shit – der schwere Teil erkärt. Wer es aber wirklich im Detail wissen will, dem sei mein Lieblingsbuch Neuronale Netze selbst programmieren empfohlen. In diesem Buch Einführung in Data Science, von dem ich das Beispiel habe, wird einfach nur gezeigt, wie man es macht, aber nicht im Detail erklärt.

# Berechne nun die Fehler der berechneten Werte
# Wegen dem Gradientenverfahren wird aber nicht 
# einfach "output - target"
# gemacht sondern die Ableitung der Sigmoid Funktion mit einbezogen

# Die Formel "output * (1 - output)" ergibt sich aus der Ableitung
# der Sigmoid Funktion

# output_deltas sind dann die 10 Zahlen, die den Fehler 
# repräsentieren
 output_deltas = [output * (1 - output) * (output - target[i])
                  for i, output in enumerate(outputs)]

Man sieht im Code auch schön, wie durch die 10 Zahlen des Outputs sowie auch durch das target iteriert wird – Jede Zahl wird also einzeln auf einen Fehler durchgerechnet.

Die output_deltas sehen nach dem ersten Durchlauf so aus:

output-deltas

Hat man die Fehlerdeltas berechnet, kann man die Gewichte anhand derer aktualisieren:

# Aktualisiere die Gewichte vom Output Layer (network[-1])
# Gehe durch jedes Output Neuron (und dessen 5 Gewichte)
# Für jedes Output Neuron gehe durch die 
# berechneten Hidden Outputs
 for i, output_neuron in enumerate(network[-1]):
   for j, hidden_output in enumerate(hidden_outputs + [1]):
# Aktuelles Gewicht = Aktuelles Gewicht - berechneten Fehler 
# dieser Ausgabe
# * (mal) Berechnete Sigmoid Ausgabe dieses Neurons 
# (für die Gewichtung, wenn
# die Sigmoid Ausgabe klein war wird dieses Neuron weniger gewichtet)
     output_neuron[j] -= output_deltas[i] * hidden_output

Man geht durch jedes Neuron, und für jedes Neuron geht man durch die Gewichte, die in dieses Neuron führen. So wird jedes Gewicht gemäss dem Fehler nach oben oder unten korrigiert.

PS: output_neuron steht für ein einzelnes Neuron, welches 6 Gewichte hat (5 + 1 Bias). Man verwendet extra enumerate, damit man einen Index j hat, mit dem man durch die Gewichte iterieren kann.

output-neuron

Man beachte das kleine Minuszeichen vor -= welches bedeutet, dass man den rechten Term von output_neuron[j] abzieht.

Das sieht also eigentlich so aus:

output_neuron[j] = output_neuron[j] – output_deltas[i] * hidden_output

Debuggt man mit PyCharm durch den Teil sieht man schön die einzelnen Werte vor und nach der Berechnung:

output-neuron-debug

Die Gewichte der versteckten Schicht

Nun werden noch einmal diesselben zwei Schritte für die versteckte Schicht gemacht mit kleinen Unterschieden.

Die versteckte Schicht hat bekannterweise 5 Neuronen und erhält daher auch 5 Error-Werte.

# Die Formel für die Hidden deltas ist:
# error_hidden = error_output * gewichte_von_hidden_zu_output_layer

# Das erste Neuron ist ja mit der Ausgabeschicht durch 
# 10 Gewichte verbunden
# Der Fehler e für den ersten Hidden Knoten etwa berechnet sich so:
# e_hidden_1 = e_output_1 * Gewicht_zu_diesem_Knoten_1
# + e_output_2 * Gewicht_zu_diesem_Knoten_2 etc.
# darum kommt hier das dot(ausgabefehler, ausgabegewichte)
 hidden_deltas = [hidden_output * (1 - hidden_output) *
                dot(output_deltas, [n[i] for n in network[-1]])
                for i, hidden_output in enumerate(hidden_outputs)]

Um die Fehler herauszufinden, macht man jetzt quasi eine umgekehrte Nettoeingabe beziehungsweise Skalarprodukt, daher braucht man hier auch die dot Funktion. Statt den Eingabewerten und Gewichten verwendet man die Output-Werte der Ausgabeschicht und die… hmm ja gut die Gewichte werden auch verwendet. Also rechnet man quasi einfach das Skalarprodukt, aber statt von vorne nach hinten durch das Netz zu gehen kommt man vom Ende und rechnet sich nach vorne zur versteckten Schicht. Ich denke, so ist es am Einfachsten, sich das vorzustellen.

Das effektive Aktualisieren der Gewichte geschieht ähnlich wie vorhin.

# Aktualisiere die Gewichte der versteckten Schicht ( = network[0])
 for i, hidden_neuron in enumerate(network[0]):
# jeder Hidden Neuron hat 26 Gewichte, die von den 
# Eingabesignalen zu ihm führen
# darum kann er schön mit dem Eingabevektor von 26 Werten 
# multipliziert werden
   for j, input in enumerate(input_vector + [1]):
# Aktuelles Gewicht = Aktuelles Gewicht - Berechneten Fehler 
# * Neuron_Sigmoid_Gewichtung
# in diesem Fall nimmt man den Eingabevektor, also für die 0 
# etwa diesen:
# [1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 
#     1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1]
# und 1 sagt dann "dieser Wert soll korrigiert werden"
# und 0 "dieser Wert soll nicht korrigiert werden"
     hidden_neuron[j] -= hidden_deltas[i] * input

Zusammenfassung der Backpropagation

Wie ich sagte, ist die Fehlerrückführung theoretisch gar nicht so kompliziert – Wenn man es aber aus dem Code herauslesen muss, können einen die vielen Vektoren und Formeln schon verwirren. Ich werde demnächst noch einen Beitrag schreiben, der das Ganze einfach erklärt.

Bis dahin seien hier nochmals die vier Formeln zusammengefasst. Wie beschrieben, sind es zwei mal die gleichen zwei Schritte:

1. Das Herausfinden des Fehlers der aktuellen Schicht
2. Das Aktualisieren der Gewichte der aktuellen Schicht

Fehler der Ausgabeschicht
error_output = Soll-Wert minus Ist-Wert

Aktualisieren der Gewichte zwischen versteckter und Ausgabeschicht
output_weights = output_weights – error_output * hidden_output

Fehler der versteckten Schicht
error_hidden = error_output und output_weights

Aktualisieren der Gewichte zwischen Eingabe- und versteckter Schicht
hidden_weights = hidden_weights – error_hidden * input

Eine Zahl zwischen 0 und 9 vorhersagen

Hat man nun das Netz trainiert (in 10’000 Iterationen), sind die Gewichte entsprechend angepasst worden. Dann kann mit der Funktion predict beziehungsweise feed_forward ein Wert vorausgesagt werden. Als Input braucht es wieder einen 25-stelligen Vektor.

Im Codebeispiel ganz unten werden etwa die 3 und die 8 vorhergesagt.

# round Funktion = Runde die Ausgabe auf zwei Nachkommastellen
# Ergibt so etwas: 
# 0 [0.96, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.02, 0.03, 0.0]
 print([round(x, 2) for x in
 predict( [0,1,1,1,0,    # .@@@.
           0,0,0,1,1,    # ...@@
           0,0,1,1,0,    # ..@@.
           0,0,0,0,0,    # ...@@
           0,1,1,1,0])]) # .@@@.

Dies ist nicht genau die trainierte 3, die sieht nämlich so aus:

"""11111
   ....1
   11111
   ....1
   11111""",

Und dennoch kann das Netz mit 90% Sicherheit sagen, dass es eine 3 ist.

 0      1     2     3
[0.0, 0.0, 0.0, 0.9, 0.0, 0.0, 0.0, 0.01, 0.0, 0.19]

Ich hoffe, dieser Beitrag hilft, die Funktionsweise eines neuronales Netzwerk besser zu verstehen, so dass wir alle ruhiger schlafen können.

Der Code in seiner ganzen Pracht

Hmm gibt es in WordPress eine anständige Code-Funktion?

Wem es lieber ist, der möge den Code von meinem Github-Account Downloaden: https://github.com/CorDharel/machinelearning/blob/master/18NeuronaleNetzwerkeMinimal.py

import math, random

def dot(v,w):
    """ Skalarprodukt zweier Vektoren = Summe der Produkte der Komponenten
    v_1 * w_1 + v_2 * w_2 + ... + v_n * w_n """
    return sum(v_i * w_i
              for v_i, w_i in zip(v,w))

def sigmoid(t):
    return 1 / (1 + math.exp(-t))

def neuron_output(weights, inputs):
    """Hier passiert das Multiplizieren der Gewichte w * Inputs i und das
    Anwenden der Sigmoid Funktion.
    Die Gewichte sind um 1 länger als die Inputs
    weil das Bias noch am Ende angehängt wird"""
    return sigmoid(dot(weights, inputs))


def feed_forward(neural_network, input_vector):
    """Nimmt ein Neuronales Netzwerk entgegen - Darin sind die Gewichte gespeichert!
    Es ist eine list (Schichten) of lists (Neuronen) of lists (weights)

    Gehe durch jeden Layer und dann durch jeden Neuron.
    Für jeden Neuron, berechne das Skalarprodukt und dann den Sigmoid Wert"""

    outputs = []

    # gehe durch die beiden Layers Hidden + Output
    for layer in neural_network:

        # Fuege wieder das Bias hinzu
        input_with_bias = input_vector + [1]

        # Gehe durch jedes Neuron und aktualisiere die Gewichte mit Skalarprodukt
        # dann rechne noch für jedes Neuron den Sigmoid Wert aus
        # Für den Hidden Layer gibt das nur noch 5 Werte, weil es 5 Neuronen sind
        # (Einmal die Sigmoid Funktion pro Neuron)
        # Für den Output Layer gibt es 10 Werte
        output = [neuron_output(neuron, input_with_bias)
                  for neuron in layer]

        # Speichere die zwei Output-Resultate (eines pro Layer)
        outputs.append(output)

        # Der Input zum nächsten Layer ist der Output von diesem Layer
        # So werden die Ausgaben vom Hidden Layer gleich an den Output Layer weitergereicht
        input_vector = output

    return outputs


def backpropagate(network, input_vector, target):

    # network hat nur zwei Einträge:
    # 0 = Gewichte vom Hidden Layer, 1 = Gewichte vom Output Layer
    # Werden so im Code genannt: output_neuron, hidden_neuron

    hidden_outputs, outputs = feed_forward(network, input_vector)

    # hidden_outputs sind die 5 berechnete Ausgaben des Hidden Layers
    # outputs sind die 10 berechneten Ausgaben des Output Layers

    # Berechne nun die Fehler der berechneten Werte
    # Wegen dem Gradientenverfahren wird aber nicht einfach "output - target"
    # gemacht sondern die Ableitung der Sigmoid Funktion mit einbezogen

    # Die Formel "output * (1 - output)" ergibt sich aus der Ableitung
    # der Sigmoid Funktion

    # output_deltas sind dann die 10 Zahlen, die den Fehler repräsentieren
    output_deltas = [output * (1 - output) * (output - target[i])
                     for i, output in enumerate(outputs)]

    # Aktualisiere die Gewichte vom Output Layer (network[-1])
    # Gehe durch jedes Output Neuron (und dessen 5 Gewichte)
    # Für jedes Output Neuron gehe durch die berechneten Hidden Outputs
    for i, output_neuron in enumerate(network[-1]):
        for j, hidden_output in enumerate(hidden_outputs + [1]):
            # Aktuelles Gewicht = Aktuelles Gewicht - berechneten Fehler dieser Ausgabe
            # * (mal) Berechnete Sigmoid Ausgabe dieses Neurons (für die Gewichtung, wenn
            # die Sigmoid Ausgabe klein war wird dieses Neuron weniger gewichtet)
            output_neuron[j] -= output_deltas[i] * hidden_output


    # back-propagate die Fehler zum Hidden Layer
    # gibt 5 Fehler- bzw. Delta-Werte für die Hidden-Nodes

    # Die Formel für die Hidden deltas ist:
    # error_hidden = error_output * gewichte_von_hidden_zu_output_layer

    # Das erste Neuron ist ja mit der Ausgabeschicht durch 10 Gewichte verbunden
    # Der Fehler e für den ersten Hidden Knoten etwa berechnet sich so:
    # e_hidden_1 = e_output_1 * Gewicht_zu_diesem_Knoten_1
    #            + e_output_2 * Gewicht_zu_diesem_Knoten_2 etc.
    # darum kommt hier das dot(ausgabefehler, ausgabegewichte)
    hidden_deltas = [hidden_output * (1 - hidden_output) *
                      dot(output_deltas, [n[i] for n in network[-1]])
                     for i, hidden_output in enumerate(hidden_outputs)]

    # Aktualisiere die Gewichte der versteckten Schicht ( = network[0])
    for i, hidden_neuron in enumerate(network[0]):
        # jeder Hidden Neuron hat 26 Gewichte, die von den Eingabesignalen zu ihm führen
        # darum kann er schön mit dem Eingabevektor von 26 Werten multipliziert werden
        for j, input in enumerate(input_vector + [1]):
            # Aktuelles Gewicht = Aktuelles Gewicht - Berechneten Fehler * Neuron_Sigmoid_Gewichtung
            # in diesem Fall nimmt man den Eingabevektor, also für die 0 etwa diesen:
            # [1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 0, 0, 0, 1, 1, 1, 1, 1, 1]
            # und 1 sagt dann "dieser Wert soll korrigiert werden"
            # und 0 "dieser Wert soll nicht korrigiert werden"
            hidden_neuron[j] -= hidden_deltas[i] * input


# Lass das Programm nur laufen, wenn es direkt aufgerufen und
# nicht importiert wurde
if __name__ == "__main__":

    raw_digits = [
          """11111
             1...1
             1...1
             1...1
             11111""",

          """..1..
             ..1..
             ..1..
             ..1..
             ..1..""",

          """11111
             ....1
             11111
             1....
             11111""",

          """11111
             ....1
             11111
             ....1
             11111""",

          """1...1
             1...1
             11111
             ....1
             ....1""",

          """11111
             1....
             11111
             ....1
             11111""",

          """11111
             1....
             11111
             1...1
             11111""",

          """11111
             ....1
             ....1
             ....1
             ....1""",

          """11111
             1...1
             11111
             1...1
             11111""",

          """11111
             1...1
             11111
             ....1
             11111"""]

    # ersetze die Punkte in den Zahlen durch 0en
    def make_digit(raw_digit):
        return [1 if c == '1' else 0
                for row in raw_digit.split("\n")
                for c in row.strip()]

    # map function: Fuehre die Funktion make_digit mit jedem
    # raw_digits Teil aus und gib es zurück
    # Auf Deutsch: Mach aus jedem Punkt der raw_digits eine 0
    inputs = list(map(make_digit, raw_digits))

    # Die richtigen Ausgaben = 10 Zeilen
    # vier ist etwa
    #  0  1  2  3  4  5  6  7  8  9
    # [0, 0, 0, 0, 1, 0, 0, 0, 0, 0]
    targets = [[1 if i == j else 0 for i in range(10)]
               for j in range(10)]

    random.seed(0)   # Setze seed damit jeder Durchlauf gleich ist
    input_size = 25  # Jede Eingave ist ein Vektor mit Länge 25
    num_hidden = 5   # Wir haben 5 Neuronen in der versteckten Schicht
    output_size = 10 # Wir haben 10 Neuronen in der Ausgabeschicht

    # initialisiere hidden und output weights mit random Werten
    # ---------------------------------------------------------
    # jedes versteckte Neuron hat ein Gewicht pro Eingabe plus ein Bias Gewicht
    hidden_layer = [[random.random() for __ in range(input_size + 1)]
                    for __ in range(num_hidden)]

    # jedes Ausgabe-Neuron hat ein Gewicht pro Eingabe plus ein Bias Gewicht
    output_layer = [[random.random() for __ in range(num_hidden + 1)]
                    for __ in range(output_size)]

    # Das Netz startet mit zufälligen Werten
    network = [hidden_layer, output_layer]

    # Trainiere das neuronale Netz
    # 10,000 Iterationen sind genug um zu konvergieren
    for __ in range(10000):
        for input_vector, target_vector in zip(inputs, targets):
            backpropagate(network, input_vector, target_vector)

    def predict(input):
        # Das Resultat der Ausgabe ist im letzten Index, also im -1
        return feed_forward(network, input)[-1]

    # drucke eine Liste der Vorhersagen von 0 - 9
    for i, input in enumerate(inputs):
        outputs = predict(input)
        print(i, [round(p,2) for p in outputs])

    print()
    print(""".@@@.
...@@
..@@.
...@@
.@@@.""")
    # round Funktion = Runde die Ausgabe auf zwei Nachkommastellen
    # Ergibt so etwas: 0 [0.96, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.02, 0.03, 0.0]
    print([round(x, 2) for x in
          predict(  [0,1,1,1,0,    # .@@@.
                     0,0,0,1,1,    # ...@@
                     0,0,1,1,0,    # ..@@.
                     0,0,0,0,0,    # ...@@
                     0,1,1,1,0])]) # .@@@.
    print()

    print(""".@@@.
@..@@
.@@@.
@..@@
.@@@.""")
    print([round(x, 2) for x in
          predict(  [0,1,1,1,0,    # .@@@.
                     1,0,0,1,1,    # @..@@
                     0,1,1,1,0,    # .@@@.
                     1,0,0,1,1,    # @..@@
                     0,1,1,1,0])]) # .@@@.


Ein Gedanke zu „Neuronales Netz – Erklärung des Beispielcodes

  1. stefan

    Hallo! Muss es oben „Wie sieht die Ausgabe aus? am Beispiel „4“ nicht [0,0,0,0,1,0,0,0,0,0] heißen? Du hast da eine 4 reingeschrieben. Oder? Danke für deine guten Erklärungen!

    Like

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit deinem WordPress.com-Konto. Abmelden /  Ändern )

Facebook-Foto

Du kommentierst mit deinem Facebook-Konto. Abmelden /  Ändern )

Verbinde mit %s