Ich habe mir ein Neuronales Netz mit Forward Propagation in Calc zusammengebaut, das geht gar nicht schwer. Zuerst stelle ich das allgemeine Tabellendokument vor, aber das kann man eigentlich gleich überspringen und gleich beim Video unten weitermachen, in dem ich das hübsch angeordnet habe.
Vorwissen, vorauszusetzen
Ein Neuronales Netz besteht aus Schichten mit Knoten; im allgemeinsten Fall ist jeder Knoten mit jedem Knoten der Vorgängerschicht verbunden, das sind im Bild die schwarzen Pfeile:
Das Blaue ist die Eingangsschicht; sie enthält die nur wenig bearbeiteten Anfangsdaten. Das Rote ist die versteckte Schicht, bei der jeder Knoten von jedem Knoten blauen Schicht Werte empfängt und mit einer Gewichtung verrechnet. Das Grüne ist die Ausgabeschicht mit dem Ergebnis, das an den Knoten abzulesen ist. Auch hier gibt es wieder Gewichtungen für die Knoten der roten Schicht.
Im Beispiel unten gibt es aber nicht drei, sondern 784 Knoten in der Input-Schicht, nicht vier, sondern 30 Knoten in der versteckten Schicht, und nicht zwei, sondern 10 Knoten in der Ausgabe-Schicht. Eine Zeichnung wäre unübersichtlich. Demnach gibt es nicht 3×4 plus 3×2 Pfeile, sondern 784×30 plus 30×10, jeder mit 1 Gewichtung. Zu jedem der 30 rosa Knoten gehören also 784 Gewichtungen und 1 Biaswert, zu jedem der 10 grünen Knoten 30 Gewichtungen und 1 Biaswert. Diese Werte sind die trainierten Muskeln eines Neuronalen Netzes, alles andere ist das Skelett, das vor dem Training festgelegt und nicht mehr geändert wird.
Ich habe dafür ein Tabellendokument mit mehreren Tabellenblättern angelegt, für jede Schicht eines. Das macht das System leicht erweiterbar auf alle möglichen Größen. Später habe ich das dann für einen Spezialfall, die MNIST-Ziffernerkennung mit 30 Knoten in der versteckten Schicht, in einem einzigen Tabellenblatt zusammengelegt; unten ist ein Video dazu, das das Prinzip erklärt.
Tabellenblatt 1: Data
Ich habe mit dem verbreiteten MNIST-Datensatz gearbeitet und beispielhaft mit einer Ziffer davon. Deren Bild (links, 28×28 Pixel) wird im ersten Tabellenblatt „Data“ als Tabelle (rechts, 28×28 Zellen) eingetragen, wobei die Zahlen für die Grauwerte stehen:


Tabellenblatt 2: InputLayer
Im Tabellenblatt „InputLayer“ werden diese 28×28 Zahlen dann untereinander in eine Spalte geschrieben und außerdem die ursprünglichen Werte zwischen 0 bis 255 auf Werte zwischen 0 und 1 normalisiert. Das geschieht deshalb, weil das echte Neuronale Netz, mit dem ich als Vorlage gearbeitet habe, das auch so macht. Eine solche Vorlage brauche ich, weil ich mit meinem Tabellendokument ein fertiges Netz zwar nachbauen, aber nicht trainieren kann, und nur mit einem trainierten Netz kann man vorzeigbar arbeiten.
Das sind 784 Knoten oder Zeilen.
Tabellenblatt 3: HiddenLayer
Im Tabellenblatt „HiddenLayer“ gibt es dann 30 Zeilen, für jeden Knoten der versteckten Schicht eine. Jede Zeile enthält in den Spalten mit blauem Inhalt die 784 Gewichtungen der Verbindungen zu den 784 Knoten des InputLayers, also zu den Eingangswerten. Dazu kommt noch 1 blaue Spalte für den Biaswert.

In den Spalten mit grünen Text wird das dann verrechnet:
- Spalte G enthält die gewichtete Summe. Das heißt, es ist die Summe aller 784 Ausgangswerte der vorhergehenden Schicht (die dort untereinander stehen), jeweils multipliziert mit dem entsprechenden Wert der Gewichtung der Verbindung; das sind die blauen Werte (nebeneinander in Spalte I bis ADL) in der dem Knoten entsprechenden Zeile in diesem Tabellenblatt.
- Davon wird in Spalte F der Bias-Wert aus Spalte H abgezogen. (Eigentlich müsste es addiert werden und dafür ein umgekehrtes Vorzeichen haben; mein Fehler – aus historischen Gründen habe ich mit dem Schwellwert Theta gerechnet statt mit dem Bias. Das ändert aber kaum etwas.)
- Darauf wiederum wird in Spalte C die Aktivierungsfunktion angewendete, hier die klassische logistische Sigmoid-Funktion. (In den versteckten Spalten D und E könnte man auch eine andere wählen.) In der Spalte C stehen also untereinander die Ausgabewerte der Neuronen dieser Schicht.
Alles gar nicht schwer, auch die Formeln sind nur wenig knifflig. Für die Spalte G mit der gewichteten Summe kann man entweder SUMMENPRODUKT verwenden oder MMULT (Matrixmultiplikation). Ich habe das erste genommen, weil es dann nichts ausmacht, wenn Zellen leer sind, beim zweiten gibt es dann einen Fehler.
Tabellenblatt 4: OutputLayer
Die letzte Schicht enthält 10 Zeilen, entsprechend den 10 Knoten der Ausgabeschicht, wobei jeder Knoten für ein mögliches Ergebnis steht: 0 bis 9. Jetzt hat jeder Knoten gewichtete Verbindungen zu allen 30 Knoten der Vorgängerschicht, das und das Bias sind die blauen Werte.

- Ausgerechnet wird daraus auf die gleiche Art wie in der Vorgängerschicht der Output, der wieder in Spalte C steht. Spalten D und E sind hir nicht ausgeblendet, spielen aber keine Rolle.
- Der höchste Wert beim Output steht bei der Zeile, die für den Knoten steht, der der Ziffer 7 entspricht: und richtig, der Input ist ja auch als 7 zu identifizieren. Wenn wir an am Input im ersten Tabellenblatt etwas ändern und eine andere Ziffer zeichnen, wird hier in Spalte C eine andere Zeile den höchsten Wert erhalten.
Das funktioniert alles durch die Magie der blauen Zahlenwerte, alles andere, also die grünen Zahlen, sind ja nur nach Formeln daraus errechnet. Diese blauen Werte bleiben auch immer gleich und unverändert, ich muss sie nicht noch irgendwie anpassen, um die Aufgabe erledigen zu können. Die blauen Werte enthalten irgendwie und irgendwo die Unterschiede zwischen den Ziffern. Nach dem Training wurden 98,1% der Ziffern aus der Testmenge richtig erkannt.
Das Geheimnis ist also, wie man auf diese blauen Werte kommt. Das geschieht durch das Training des Netzes, und darum geht es hier nicht, und das lässt sich in der Tabellenkalkulaton auch nur schwer abbilden.
Video
Hier mit einer aufgehübschten Calc-Datei und einem einzigen Tabellenblatt als Filmchen:
(Link zu Youtube, falls das Anschauen im Blog Probleme macht.)
Exkurs: Softmax – muss niemand mehr lesen
Der ganze Blogeintrag ist nur wegen eines Problems entstanden, über das ich noch gar nicht geschrieben habe. Spalte C im Output-Layer hat das eigentliche Ergebnis, hier sind die Werte für die 10 Knoten:

Darauf habe ich in Spalte B die sogenannte Softmax- oder normalisierte Exponentialfunktion angewendet. Das macht man gerne mal am Output von Neuronalen Netzen. Danach sehen die Werte so aus:

Man sieht: Die qualitativen Unterschiede zwischen den Werten sind gleich geblieben: Was vorher hoch war (7, 2, 9) ist es jetzt in der gleichen Reihenfolge danach auch noch, was vorher klein war, ist das weiterhin. Aber die quantitativen Unterschiede zwischen den hohen und niedrigen Werten sind geringer geworden. Außerdem summieren sich die Werte von 0 bis 9 jetzt auf 1, so dass man die Werte jetzt auch schön als Prozentangaben lesen kann.
Warum macht man das?
Dazu müsste man jemand fragen, der sich auskennt. Ich kann das nur wiedergeben: Es hat sich, so heißt es, beim Verarbeiten in den nächsten Schichten oft von Vorteil gezeigt, mit den normalisierten und weniger gespreizten Werten weiterzuarbeiten.
Wie macht man das?
Meine Rohwerte für die Knoten 0 bis 9 stehen in den Zellen C3 bis C12. Um das Softmax für den Knoten 0 berechnen, berechne ich nicht etwa die Summe von C3 bis C12 (und dann den Anteil von C3 daran), sondern die Summe der Potenzen von eC3 bis eC12 (und dann den Anteil von eC3 daran). In Calc:
= exp(C3) / ( exp(C3) + exp(C4
)
+ exp(C5)
+ exp(C6)
+ exp(C7)
+ exp(C8)
+ exp(C9)
+ exp(C10)
+ exp(C11)
+ exp(C12) )
Natürlich möchte ich diese lange Formel aber nicht für 30 oder mehr Spalten abtippen, wie ich das im HiddenLayer müsste. Außerdem reizt es, da eine kompaktere Form zu finden. Die gibt es in den fertigen Formeln aber nicht. Auf Mastodon erfuhr ich dafür von den Geheimnissen der Matrixformeln.
Die Lösung als Matrixformel
Was ich möchte: Nimm alle Werte von … bis …, führe eine Operation auf ihnen aus (nämlich e hoch) und führe ein weitere Operation mit allen Ergebnissen aus (nämlich Addition). Das geht! Dazu tippe ich erst einmal folgende semantisch fragwürdige Formel in ein Feld:
=SUMME(EXP(C3:C12))
und schließe nicht mit Enter ab, sondern mit Strg+Shift+Enter, worauf der Zelleninhalt umgewandelt wird zu:
{=SUMME(EXP(C3:C12))
}
Das ist dann eine Matrixformel und das Ergebnis ist eine Matrix; in diesem Fall eigentlich ein Skalar, es könnte auf andere Zellbereiche angewendet und mit anderen Funktionen auch ein Vektor oder eine tatsächliche Matrix sein. Intern zählt das so oder so als Matrix und wird beim Kopieren in andere Zellen deshalb etwas anders behandelt.
Ich bin aber noch nicht fertig mit dem Softmax, muss erst noch C3 durch das Ergebnis teilen, um das Ergebnis für Knoten 0 zu erhalten:
{=EXP(C3)/SUMME(EXP(C3:C12))
}
In Excel funktioniert das auch, in Calc bin ich auf eine Schwierigkeit gestoßen. Einzeln kopieren kann ich die Formel da zwar auch, aber wenn ich sie mit Drag and Drop auf andere Zellen anwende, wird leider die exakt gleiche Formel kopiert, das heißt: es werden keine relativen Zellbezüge angepasst – nach dem Kopieren durch Aufziehen steht immer C3 in der Formel – dabei möchte ich für die Knoten 1 bis 9 ja als Dividend ja C4 bis C12 stehen haben.
Mit der Lösung, die ich für mich gefunden habe, muss ich schreiben:
=EXP(INDIREKT(ADRESSE(ZEILE();3)))/SUMME(EXP(C3:C12))
und das dann wieder per Strg+Shift+Enter zur Matrixformel machen (mit den geschweiften Klammern). Dann klappt das auch mit dem Aufziehen.
Schreibe einen Kommentar