top of page
  • AutorenbildSimon Schwaiger

Wanderung in Richtung des geringsten Fehlers: Wie Programme lernen

Es haben sich bereits viele unserer AIAV Use Cases damit befasst, wie verschiedene Modelle lernen können, eine Aufgabe zu lösen. Beispiele für solche von uns eingesetzten Modelle sind neuronale Netze (siehe AIAV Use Case Traue keinem neuronalen Netz: wissenschaftliche Bildanalysen), naive Bayes (siehe AIAV Use Case Einsatz des Naive Bayes zur Filterung von Spam) oder sogar einfache Tabellen (siehe AIAV Use Case Bis zum High-Score! Wie ein Roboter durch Ausprobieren lernt). Auch wenn sich diese Modelle in ihrer Struktur und mathematischen Beschreibung stark unterscheiden, folgt das Vorgehen beim Einsatz immer einem ähnlichen Prinzip: Ein Modell wird initialisiert, es wird mittels Trainingsdaten trainiert und anschließend in seiner trainierten Form für eine Aufgabe eingesetzt.


Dabei stellt sich die Frage, was während des Trainingsvorgangs passiert, um den Modellen das Lernen zu ermöglichen.


Um aufzuarbeiten, wie Modelle lernen, müssen wir uns zunächst bewusst sein, anhand welcher Information gelernt wird. Dabei unterscheiden wir zwischen drei großen Feldern im maschinellen Lernen: Supervised Learning, Unsupervised Learning und Reinforcement Learning. Beim Supervised Learning lernt ein Modell aus händisch beschrifteten Trainingsdaten (siehe AIAV Video Supervised Learning). Wenn Modelle eigenständig Strukturen in Daten finden, spricht man von Unsupervised Learning (siehe AIAV Video Unsupervised Learning), während beim Reinforcement Learning Modelle aus Beobachtungen der Umwelt sowie einem Feedback-Signal lernen (siehe AIAV Video Reinforcement Learning).


Während die Theorie hinter Lernen auf alle drei Gruppen anwendbar ist, werden wir im Zuge diese Use Cases Neuronale Netze (siehe AIAV Video Klassische Neuronale Netze) mittels Supervised Learning trainieren, da die Theorie bei diesem Vorgehen sehr gut veranschaulicht werden kann. Neuronale Netze werden in der Fachliteratur in der Regel als Funktion beschrieben, welche aus Schichten besteht. Dabei besteht jede Schicht wiederum aus linearen Regressionsmodellen, sowie einer nichtlinearen Aktivierungsfunktion. Das Ganze klingt auf den ersten Blick etwas furchteinflößend - schlüsseln wir die Theorie also etwas auf.


Ein neuronales Netz nähert eine Funktion an. Das heißt, es liest eine Liste an Eingangsgrößen und gibt eine Liste an Ausgangsgrößen aus. Das Ziel des Netzes ist es also, Eingang und Ausgang so zu verknüpfen, dass bestimmte Werte am Eingang bestimmte Werte am Ausgang auslösen. Diese Verknüpfung ist in Schichten organisiert. Eingangsgrößen werden der Ersten Schicht übergeben, deren Ausgang wiederum der zweiten Schicht übergeben wird. Diese Kette wird fortgesetzt, bis wir von der letzten Schicht die Ausgangsgrößen des Netzwerks erhalten (siehe Abbildung 1).


Abbildung 1: Ein neuronales Netz ist eine Funktion, welche eine Liste an Eingangsgrößen zu Ausgangsgrößen verarbeitet. Dafür gliedert man neuronale Netze in Schichten, welche aus sogenannten Neuronen bestehen. Jedes Neuron hat dabei jeden Ausgang der vorhergehenden Schicht als Eingang.


Jede Schicht besteht dabei aus mehreren sogenannten Neuronen. Diese spiegeln die Inspiration, die Neuronale Netze vom menschlichen Hirn nehmen, wieder. In seiner einfachsten Form ist Neuron dabei mit jedem der Ausgänge der vorherigen Schicht verbunden und summiert diese auf. Diese Summe wird dann im Neuron einer linearen Transformation unterzogen.


Ein Exkurs in die Lineare Algebra

Um zu erklären, was genau im Neuron passiert, machen wir einen Exkurs zur linearen Algebra. Stellen Sie sich eine lineare Funktion vor. Bei dieser wird jeder Punkt auf der Funktion f(x) abhängig vom Eingangswert x durch folgende Gleichung beschrieben:

Eine Visualisierung der Gleichung ergibt eine Gerade mit Steigung k, die die Y-Achse im Punkt (0,d) schneidet. Abbildung 2 zeigt dabei, wie Parameter k und d das Aussehen der resultierenden Linie einstellen.


Abbildung 2: Eine lineare Funktion hat zwei einstellbare Parameter: k beschreibt die Steigung, während d die Gerade entlang der abhängigen Achse verschiebt.


Stellen Sie sich jetzt eine zweidimensionale Punktwolke, also eine Ansammlung an Punkten auf einer Ebene, vor. Diese Punktwolke können wir nun mithilfe unserer linearen Funktion nun modellieren, indem wir k und d so einstellen, dass die Gerade die Punktwolke so gut wie möglich darstellt.


Aber was heißt in diesem Kontext so gut wie möglich?


Um diese Frage zu beantworten, definieren wir einen Fehler. Dieser gibt uns eine numerische Einschätzung, wie weit unsere Soll- (aus dem Datensatz) und Ist-Werte (Voraussagen unseres Modells) voneinander entfernt sind. Diese Einschätzung wird von der sogenannten Loss-Funktion bestimmt (siehe Kapitel 1.1.5 in diesem Buch). Wir können die Loss-Funktion auf Abweichungen zwischen Punkten in der Punktwolke und den entsprechenden Punkten in unserem Modell anwenden, um Aussage darüber zu treffen, wie gut die Gerade die Punktwolke widerspiegelt.


Da wir nun einschätzen können, wie sehr unsere Gerade die Punktwolke, also unseren Datensatz, widerspiegelt, müssen wir uns noch überlegen, wie wir die Parameter k und d setzen, sodass unser gesamter Fehler so klein wie möglich ist. Dazu nutzen wir ein Vorgehen namens Gradient Descent. Die Idee dahinter ist einfach: Wir initialisieren zunächst beide Parameter k und d mit zufälligen Werten. Anschließend betrachten wir jeden Eintrag in unserem Datensatz und ermitteln für jeden unserer Parameter die Richtung, in welcher der Loss für diesen Eintrag kleiner wird; für unsere lineare Funktion müssen wir also für jeden Datenpunkt zwei Richtungen, eine für k und eine für d, berechnen. Wir optimieren dann die Funktion, indem wir jeden Parameter leicht in die vorgegebene Richtung verändern. Die Schritte von Berechnung der Richtung und Optimierung werden dann für jeden Punkt in der Punktwolke in mehreren Durchgängen (sogenannten Epochen) nacheinander durchgeführt, um den Loss über die gesamten Trainingsdaten zu minimieren. Diesen Vorgang nennt man Training oder Fitting. Abbildung 3 zeigt diesen für mehrere pseudo-zufällig generierte Punktwolken.

Abbildung 3: Die Parameter k und d von zufällig initialisierten Geraden werden mittels Gradient Descent so gesetzt, dass der Loss minimiert wird.


Die Richtung, in welche die Parameter verändert werden sollen, wird dabei auf mithilfe eines einfachen Tricks ermittelt. Die Loss-Funktion gibt an, wie nahe die Gerade und die Punktwolke zueinander sind. Da wir die Loss-Funktion einfach mathematisch beschreiben können, können wir diese auch ableiten. Die Ableitung stellt dann direkt die Richtung dar. Dabei fällt Ihnen sicher auf, dass der Loss nur eine Richtung vorgibt, wir aber zwei Parameter, k und d, setzen können. Hier bedienen wir uns eines weiteren Tricks: wir teilen den vom Loss vorgegebenen Fehler auf. d wird dabei direkt in die vom Loss vorgegebene Richtung optimiert, während k noch mit der Eingangsgröße multipliziert wird. Mathematisch können wir den Unterschied zwischen den beiden Parametern einfach formulieren:

Mithilfe des Fehlers können wir nun die Parameter in kleinen Schritten in Richtung des Fehlers anpassen. Wenn wie diese Anpassung Stück für Stück über die vorhandenen Trainingsdaten mehrmals durchführen, bewegen sich die Parameter immer weiter in Richtung des geringsten Fehlers!


Die in diesem Abschnitt besprochene Modellart nennt man Lineare Regression. In neuronalen Netzen stellen sie ein einzelnes Neuron dar; wir setzen also Gruppen von diesen einfachen linearen Modellen parallel ein, um Schichten zu formen, die wir wiederum hintereinander anordnen. Die angewandten Konzepte bleiben dabei gleich; in der Fachliteratur bezeichnet man hier k als Weight und d als Bias.


Vom Neuron zum Netz mittels Nichtlinearitäten

Leider könnte ein neuronales Netz, welches nur aus solchen linearen Bausteinen besteht, keine komplexen Funktionen darstellen; eine Kombination aus linearen Modellen ergibt nämlich wiederum nur ein lineares Modell. Deshalb fügen wir nach jeder Schicht noch eine nicht-lineare Aktivierungs-Funktion ein. Diese Funktion wird auf den Ausgang jeder Schicht angewandt und erlaubt dem neuronalen Netz, nichtlineare Funktionen abzubilden.


Unser Vorgehen beim Optimieren durch Gradient Descent kann grundsätzlich wie oben beschrieben angewandt werden, obwohl wir jetzt mehrere Neuronen sowie nicht-lineare Funktionen vorliegen haben. Wichtig ist jetzt nur die Reihenfolge, in der wir die Parameter verändern: Der Loss beschreibt die Diskrepanz zwischen Modell-Ausgang und Trainingsdaten. Das heißt, er gibt uns lediglich Auskunft über die Richtung zum Überarbeiten der Parameter der letzten Schicht. Diese können wir auch wie oben beschrieben direkt verändern. Damit wir dennoch die Parameter der übrigen Schichten optimieren können, müssen wir abschätzen, welcher Loss am Ausgang der vorderen Schichten vorhanden ist. Diese Schätzung erzielen wir, indem wir uns nach vorne hanteln und den Fehler am Eingang der hinteren Schicht dem Fehler am Ausgang der vorderen Schicht gleichsetzen. Da wir sowohl die Aktivierungsfunktion als auch die hintere Schicht mathematisch beschreiben und leicht ableiten können, nehmen wir sie quasi in unseren Loss mit auf und optimieren so die Parameter der vorderen Schicht. Dieses Vorgehen ist im Code relativ einfach umzusetzen und skaliert auf beliebig viele Schichten.


Abbildung 4 zeigt unser resultierendes Verständnis vom neuronalen Netz mit den diskutierten Komponenten.


Abbildung 4: Mit unserem mathematischen Verständnis von neuronalen Netzen wissen wir nun, dass Schichten aus linearen Regressionsmodellen (also Geraden) bestehen. Nach jeder versteckten Schicht fügen wir noch eine nicht-lineare Aktivierungsfunktion hinzu, um nicht-lineare Funktionen annähern zu können (gezeichnet basierend auf Abbildung 1.6 in diesem Buch).


Praktische Implementierung

Für die praktische Implementierung wollen wir mithilfe eines neuronalen Netzwerks handgeschriebene Ziffern im MNIST Datensatz erkennen. Dieser Datensatz bietet einen einfachen Weg, um Implementierungen von Modellen anschaulich zu evaluieren. Er besteht aus 28 mal 28 Pixel großen Graustufenbildern, die handgeschriebene Ziffern von null bis neun darstellen. Die 70.000 beschrifteten Bilder im Datensatz sind dabei frei zugänglich und können direkt mithilfe der Keras API heruntergeladen werden. Nach dem Herunterladen müssen wir die Daten vorverarbeiten, damit sie in einer Form sind, die unsere Implementierung des neuronalen Netzes versteht.


Anders als bei typischen Aufgaben in der Bildverarbeitung setzen wir kein Convolutional Neural Network (CNN, siehe AIAV Video CNN Classifier) ein, da wir ausschließlich die oben diskutierte Theorie umsetzen wollen. Schichten, welche aus den oben beschriebenen Neuronen bestehen, nennt man voll-vernetzt oder fully connected Das resultierende Netzwerk fällt in eine der einfachsten Kategorien an neuronalen Netzen, den sogenannten Feed-Forward Neural Networks bei denen Information direkt vom Eingang zum Ausgang, also immer von vorne nach hinten, fließt. Andere Netzwerkarten, wie z.B. CNNs oder die sogenannten Recurrent Neural Networks (siehe AIAV Video Recurrent Neural Networks Part 1) setzen bei der Optimierung ähnliche Prinzipien ein, weisen aber Eigenheiten im Aufbau und der darunterliegenden Mathematik auf.


Während wir die Bilder aus dem Datensatz direkt in ein CNN geben könnten, müssen wir für unser neuronales Netz die Bilder zunächst als eindimensionalen Vektor formatieren. Hier können wir die Werte aus jedem Bild einfach direkt übernehmen, aber anstatt in einer 28 mal 28 großen Matrix speichern wir sie in einer Liste mit 784 (= 28 mal 28) Einträgen. Diese Liste können wir dem neuronalen Netz direkt übergeben.


Anschließend müssen wir noch die Beschriftungen der Bilder formatieren. Diese liegen im Datensatz als ganzzahlige Beschriftung vor. Da das neuronale Netz aber für jede mögliche Ziffer einen Ausgang hat, müssen wir die Beschriftung zu einem Vektor der Länge zehn, also einem Eintrag pro Ziffer, formatieren. Die Keras Funktion to_categorical übernimmt das automatisch. Abschließend teilen wir den Datensatz noch in einen Trainings- und einen Testteil auf. Das ist notwendig, um die Leistung des trainierten Netzes anhand von Bildern zu evaluieren, die dem Netz unbekannt sind.


Für die Implementierung des Trainings verwenden wir die mittlere quadratische Abweichung als Loss und die tanh-Funktion als Aktivierungsfunktion. Beide Funktionen basieren auf der Vektor- und Matrix Bibliothek NumPy. Dabei müssen wir beachten, dass wir nicht nur die Funktionen selbst darstellen müssen, sondern für die Optimierung auch die erste Ableitung beider Funktionen benötigen.


Nun ist die Vorbereitung erledigt und wir müssen lediglich die Trainingsschleife implementieren. In dieser gehen wir wie folgt vor:

  1. Zufälliges Einlesen von mehreren Einträgen (= Bilder und Beschriftung) im Datensatz

  2. Netz trifft Voraussage über jeden Eintrag

  3. Ermittlung der Ableitung des Losses von Beschriftung-Voraussage

  4. Iteration von hinten nach vorne über alle Schichten und Anpassung der Parameter


Abbildung 5 visualisiert die Voraussagen der resultierenden Implementierung. Die hier gezeigten Bilder sind Teil des Test-Datensatzes, das heißt, dass das trainierte Netzwerk diese noch nie gesehen hat und nicht "aus dem Gedächtnis" arbeiten kann. Über den gesamten Testdatensatz erzielt unser Neuronales Netz eine Genauigkeit von 93,64%.

Abbildung 5: Die direkte Umsetzung der voll vernetzten Schichten und des oben beschriebenen Trainings resultiert in einem neuronalen Netz, welches handgeschriebene Ziffern erkennt.


Fazit

Neuronale Netze erlauben es uns, nicht-lineare Funktionen anzunähern. Sie verwenden intern lineare Modelle mit einstellbaren Parametern, sowie nicht-lineare Aktivierungsfunktionen. Das Training passiert dabei vom Ausgang des Netzes ausgehend nach vorne anhand einer ableitbaren Fehlerfunktion. Die dabei resultierende Implementierung ist leicht verständlich und weist, unter Mithilfe von bestehenden Bibliotheken wie NumPy, einen relativ kurzen Code auf.


Wir haben uns in diesem Use Case mit neuronalen Netzen beschäftigt, welche nur aus voll Vernetzten Schichten bestehen. In der Praxis werden aber auch Netze mit anderen Schichtarten, wie z.B. Convolutional Neural Networks (CNN, siehe AIAV Video CNN Classifier) eingesetzt. Zusätzlich dazu ist unsere Trainingsfunktion vergleichsweise einfach gestaltet; in der Praxis werden komplexere Optimierungsverfahren, wie z.B. der Adam Optimiser für das Training von neuronalen Netzen eingesetzt. Diese beiden Faktoren limitieren die Genauigkeit, die mit unserem trainierten Netz erzielt werden kann. Nach dem Training erreicht unser neuronales Netz eine Genauigkeit von 93,64% beim Klassifizieren von Ziffern im MNIST Datensatz.


Weiterführende AIAV Inhalte

Der Beitrag Auf der Suche nach dem "besten" AI-Algorithmus, oder auch nicht erklärt das No-Free-Lunch Theorem und zeigt, warum Modelle nur lernen können, was wir ihnen auch zeigen.


Der Beitrag AI und Computerhardware zeigt Ihnen, mithilfe welcher Arten von Computern Modelle, wie sie in diesem Use Case vorgestellt wurden, trainiert werden können und welche Vorteile die richtige Hardware für AI bietet.



141 Ansichten

Aktuelle Beiträge

Alle ansehen
bottom of page