Java Neuronales Netzwerk: Beispielcode und Architekturen

Neuronale Netze sind ein Eckpfeiler der modernen künstlichen Intelligenz. Dieser Artikel bietet eine Grundlage für den Aufbau und das Training eines neuronalen Netzes in Java und beleuchtet verschiedene Architekturen und Anwendungsbereiche.

Einführung in Neuronale Netze

Das menschliche Gehirn besteht aus etwa 100 Milliarden Nervenzellen, den Neuronen, die als Bausteine der Informationsverarbeitung dienen. Künstliche neuronale Netze (KNNs) sind der Versuch, diese Funktionsweise in Programmiersprachen nachzubilden.

Ein künstliches Neuron empfängt Eingaben, verarbeitet sie mithilfe mathematischer Operationen und gibt eine Ausgabe zurück. Diese Ausgabe kann als Eingabe für andere Neuronen dienen. Obwohl KNNs nur eine grobe Vereinfachung biologischer Nervenzellen darstellen, haben sie sich als äußerst nützlich erwiesen.

Grundlegende Konzepte

Künstliches Neuron

Ein künstliches Neuron hat mehrere Eingänge (x1, x2, x3, …), die jeweils mit einem Gewicht (w1, w2, w3, …) verbunden sind. Das Neuron berechnet seine Aktivierung, indem es die Produkte aus Eingabe und Gewicht aufsummiert:

Aktivierung = (x1 * w1) + (x2 * w2) + (x3 * w3) + ...

Lesen Sie auch: Mehr über Java-Konnektivität erfahren

Zusätzlich besitzt das Neuron einen Schwellwert (s). Die Aktivierung und der Schwellwert fließen in eine mathematische Formel ein, die die Ausgabe des Neurons berechnet. Diese Ausgabe liegt typischerweise zwischen 0 und 1.

Neuronale Netze

Mehrere Neuronen bilden zusammen ein neuronales Netz. Üblicherweise werden Neuronen in Schichten gruppiert, die miteinander verbunden sind. Die Ausgabe eines Neurons wird so zur Eingabe eines anderen.

Die Gewichte und Schwellwerte eines neuronalen Netzes werden nicht manuell eingestellt, sondern durch einen Trainingsprozess optimiert. Dabei werden zufällige Zahlen erzeugt und dann hilft ein Algorithmus dabei, während des Trainingsprozesses die Gewichte und Schwellwerte der Neuronen so zu verändern, dass es am Ende möglichst gut funktioniert. Dieser Prozess ist komplex und erfordert oft große Datenmengen.

Neuron Class in Java

Um zu zeigen, dass in einem Neuron wirklich keine Magie steckt, und um eine bessere Vorstellung davon zu bekommen, wie man die grafische Abbildung des Neurons in Programmiercode umsetzt, zeigt die folgende Abbildung den Quelltext eines Neurons in der Programmiersprache Java. Selbst wer diese noch nicht kennt, kann vielleicht erahnen, dass hier keine höhere Intelligenz am Werk ist, sondern nur ein paar Zuweisungen und eine mathematische Berechnung.

class Neuron { Random random = new Random(); private Double bias = random.nextDouble(-1, 1); public Double weight1 = random.nextDouble(-1, 1); private Double weight2 = random.nextDouble(-1, 1); public double compute(double input1, double input2){ double preActivation = (this.weight1 * input1) + (this.weight2 * input2) + this.bias; double output = Util.sigmoid(preActivation); return output; }}

Diese Neuron-Klasse ist recht simpel und weist drei Mitglieder auf: bias, weight1 und weight2. Jedes dieser Mitglieder wird mit einem zufälligen Double zwischen -1 und 1 initialisiert.

Lesen Sie auch: Einführung in neuronale Netze mit Java

Um den Output des Neurons zu berechnen, folgen wir dem gezeigten Algorithmus: Wir multiplizieren jede Eingabe mit ihrer Gewichtung plus dem Bias: input1 * weight1 + input2 * weight2 + bias. So erhalten wir die unverarbeitete Berechnung (preActivation), die wir durch die Aktivierungsfunktion laufen lassen. In diesem Fall verwenden wir die Sigmoid-Aktivierungsfunktion, die Werte in einem Bereich von -1 bis 1 komprimiert.

public class Util { public static double sigmoid(double in){ return 1 / (1 + Math.exp(-in)); }}

Nachdem wir nun die Funktionsweise von Neuronen beleuchtet haben, gilt es, einige Neuronen in ein Netzwerk einzufügen. Dazu nutzen wir eine Network-Class mit einer Liste von Neuronen.

class Network { List<Neuron> neurons = Arrays.asList( new Neuron(), new Neuron(), new Neuron(), /* input nodes */ new Neuron(), new Neuron(), /* hidden nodes */ new Neuron()); /* output node */}

Obwohl die Liste der Neuronen eindimensional ist, werden wir sie während der Nutzung zu einem Netzwerk verbinden. Die ersten drei Neuronen sind Inputs, die folgenden beiden versteckt und das letzte der Output-Knoten.

Prediction mit dem Netzwerk

Nun soll es darum gehen, ein Netzwerk zu Prediction-Zwecken einzusetzen. Dazu verwenden wir einen einfachen Datensatz mit zwei ganzzahligen Inputs und einem Antwortformat von 0 bis 1. In unserem Beispiel wird eine Kombination aus Gewicht und Größe verwendet, um das Geschlecht einer Person zu erraten. Dabei wird davon ausgegangen, dass mehr Gewicht und Größe auf eine männliche Person hindeuten. Dieselbe Formel ließe sich für jede beliebige Wahrscheinlichkeitsrechnung mit zwei Faktoren und einem Output nutzen. Den Input könnte man auch als Vektor betrachten - und somit die Gesamtfunktion der Neuronen als Umwandlung eines Vektors in einem Skalarwert. Die Prediction-Phase des Netzes gestaltet sich wie folgt.

public Double predict(Integer input1, Integer input2){ return neurons.get(5).compute( neurons.get(4).compute( neurons.get(2).compute(input1, input2), neurons.get(1).compute(input1, input2) ), neurons.get(3).compute( neurons.get(1).compute(input1, input2), neurons.get(0).compute(input1, input2) ) );}

die beiden Inputs fließen an die ersten drei Neuronen. Deren Outputs werden an die Neuronen 4 und 5 weitergeleitet wird, die wiederum in das Output-Neuron einspeisen. Dieser Prozess wird als Feedforward bezeichnet. Nun könnten wir das Netz zu einer Prediction auffordern.

Lesen Sie auch: Neuronen einfach erklärt

Network network = new Network();Double prediction = network.predict(Arrays.asList(115, 66));System.out.println("prediction: " + prediction);

Das würde sicher zu Ergebnissen führen - die allerdings nur auf Zufallswerten und Bias basieren. Für eine echte Prediction ist es nötig, das Netzwerk zuvor zu trainieren.

Das Netzwerk trainieren

Das Training eines neuronalen Netzwerks folgt einem Prozess, der als Backpropagation bekannt ist. Der beinhaltet im Grunde, Änderungen rückwärts durch das Netzwerk zu “schieben”, damit sich der Output in Richtung eines gewünschten Zielwerts bewegt. Backpropagation lässt sich mit Hilfe von Funktionsdifferenzierung durchführen - für unser Beispiel werden wir allerdings einen anderen Weg gehen und jedem Neuron die Fähigkeit verleihen, zu “mutieren”.

In jeder Trainingsrunde (auch Epoch genannt) wählen wir ein anderes Neuron aus, um eine kleine, zufällige Anpassung an einer seiner Eigenschaften (weight1, weight2 oder bias) vorzunehmen und dann zu prüfen, ob sich die Ergebnisse verbessern. Ist das der Fall, behalten wir diese Änderung mit einer remember()-Methode bei. Wenn sich die Ergebnisse verschlechtert haben, machen wir sie mit einer forget()-Methode rückgängig.

Um die Änderungen zu tracken, fügen wir Class-Mitglieder hinzu (old*-Versionen von weights und bias). Im Folgenden betrachten wir die Methoden mutate(), remember() und forget().

public class Neuron() { private Double oldBias = random.nextDouble(-1, 1), bias = random.nextDouble(-1, 1); public Double oldWeight1 = random.nextDouble(-1, 1), weight1 = random.nextDouble(-1, 1); private Double oldWeight2 = random.nextDouble(-1, 1), weight2 = random.nextDouble(-1, 1); public void mutate(){ int propertyToChange = random.nextInt(0, 3); Double changeFactor = random.nextDouble(-1, 1); if (propertyToChange == 0){ this.bias += changeFactor; } else if (propertyToChange == 1){ this.weight1 += changeFactor; } else { this.weight2 += changeFactor; }; } public void forget(){ bias = oldBias; weight1 = oldWeight1; weight2 = oldWeight2; } public void remember(){ oldBias = bias; oldWeight1 = weight1; oldWeight2 = weight2; }}

Zusammengefasst:

  • Die mutate()-Methode wählt eine zufällige Eigenschaft und einen zufälligen Wert zwischen -1 und 1 aus und ändert dann die Eigenschaft.
  • Die forget()-Methode setzt diese Änderung auf den alten Wert zurück.
  • Die remember()-Methode kopiert den neuen Wert in den Puffer.

Um nun die neuen Fähigkeiten unseres Neurons zu nutzen, fügen wir Network eine train()-Methode hinzu.

public void train(List<List<Integer>> data, List<Double> answers){ Double bestEpochLoss = null; for (int epoch = 0; epoch < 1000; epoch++) { // adapt neuron Neuron epochNeuron = neurons.get(epoch % 6); List<Double> predictions = new ArrayList<>(); for (int i = 0; i < data.size(); i++) { predictions.add(i, this.predict(data.get(i).get(0), data.get(i).get(1))); } Double thisEpochLoss = Util.meanSquareLoss(answers, predictions); if (bestEpochLoss == null){ bestEpochLoss = thisEpochLoss; epochNeuron.remember(); } else { if (thisEpochLoss < bestEpochLoss) { bestEpochLoss = thisEpochLoss; epochNeuron.remember(); } else { epochNeuron.forget(); } } }}

Die train()-Methode iteriert eintausendmal über die aufgeführten data, answers und lists. Es handelt sich um gleich große Trainingsmengen: data beinhaltet Input-Werte, answers die bekannten, richtigen Antworten. Die Methode ermittelt dann einen Wert darüber, wie nahe das Ergebnis des Netzwerks den bekannten, richtigen Antworten kommt. Dann wird ein zufälliges Neuron verändert (mutiert), wobei die Änderung beibehalten wird, wenn ein neuer Test ergibt, dass sie eine bessere Vorhersage zur Folge hatte.

Die Ergebnisse lassen sich mithilfe der Mean-Squared-Error (MSE) -Formel überprüfen - einer dafür gängigen Methode.

public static Double meanSquareLoss(List<Double> correctAnswers, List<Double> predictedAnswers){ double sumSquare = 0; for (int i = 0; i < correctAnswers.size(); i++) { double error = correctAnswers.get(i) - predictedAnswers.get(i); sumSquare += (error * error); } return sumSquare / (correctAnswers.size());}

System feinabstimmen

Nun müssen wir nur noch einige Trainingsdaten in das Netz fließen lassen und es mit weiteren Predictions austesten. Im Folgenden betrachten wir, wie man Trainingsdaten bereitstellt.

List<List<Integer>> data = new ArrayList<>();data.add(Arrays.asList(115, 66));data.add(Arrays.asList(175, 78));data.add(Arrays.asList(205, 72));data.add(Arrays.asList(120, 67));List<Double> answers = Arrays.asList(1.0,0.0,0.0,1.0);Network network = new Network();network.train(data, answers);

In unseren Trainingsdaten bestehen aus einer Liste von zweidimensionalen Integer-Sets (wir könnten sie uns als Gewicht und Größe vorstellen) und einer Liste von Antworten (wobei 1.0 weiblich und 0.0 männlich ist). Wenn wir den Trainingsalgorithmus eine Logging-Funktionalität hinzufügen, erhalten wir nachfolgendes Resultat.

// Logging:if (epoch % 10 == 0) System.out.println(String.format("Epoch: %s | bestEpochLoss: %.15f | thisEpochLoss: %.15f", epoch, bestEpochLoss, thisEpochLoss));// output:Epoch: 910 | bestEpochLoss: 0.034404863820424 | thisEpochLoss: 0.034437939546120Epoch: 920 | bestEpochLoss: 0.033875954196897 | thisEpochLoss: 0.431451026477016Epoch: 930 | bestEpochLoss: 0.032509260025490 | thisEpochLoss: 0.032509260025490Epoch: 940 | bestEpochLoss: 0.003092720117159 | thisEpochLoss: 0.003098025397281Epoch: 950 | bestEpochLoss: 0.002990128276146 | thisEpochLoss: 0.431062364628853Epoch: 960 | bestEpochLoss: 0.001651762688346 | thisEpochLoss: 0.001651762688346Epoch: 970 | bestEpochLoss: 0.001637709485751 | thisEpochLoss: 0.001636810460399Epoch: 980 | bestEpochLoss: 0.001083365453009 | thisEpochLoss: 0.391527869500699Epoch: 990 | bestEpochLoss: 0.001078338540452 | thisEpochLoss: 0.001078338540452

Wie zu sehen, nimmt der “Loss” (also die Fehlerabweichung von “100 Prozent korrekt”) langsam ab. Das Modell nähert sich also immer mehr einer genauen Vorhersage an. Nun gilt es zu überprüfen, wie gut unser Modell mit echten Daten funktioniert.

System.out.println("");System.out.println(String.format(" male, 167, 73: %.10f", network.predict(167, 73)));System.out.println(String.format("female, 105, 67: %.10f", network.predict(105, 67)));System.out.println(String.format("female, 120, 72: %.10f | network1000: %.10f", network.predict(120, 72)));System.out.println(String.format(" male, 143, 67: %.10f | network1000: %.10f", network.predict(143, 67)));System.out.println(String.format(" male', 130, 66: %.10f | network: %.10f", network.predict(130, 66)));

Wie zu sehen, füttern wir unser trainiertes neuronales Netz mit Daten und geben die Vorhersagen aus. Das Resultat sieht in etwa wie folgt aus.

// Trainierte Predictionsmale, 167, 73: 0.0279697143female, 105, 67: 0.9075809407female, 120, 72: 0.9075808235male, 143, 67: 0.0305401413male', 130, 66: network: 0.9009811922

Das Netzwerk bei den meisten Wertepaaren (Vektoren) ziemlich gute Arbeit geleistet hat. Es gibt den weiblichen Datensätzen eine Schätzung um 0.907 - was ziemlich nahe an 1 ist. Zwei männliche Datensätze weisen 0.027 und 0.030 auf - und nähern sich damit der 0. Der männliche Ausreißer-Datensatz (130, 67) wird als “wahrscheinlich weiblich” angesehen, bei einem Wert von 0.900 allerdings mit geringerer Zuversicht.

Es gibt eine Reihe von Möglichkeiten, die Einstellungen an diesem System zu verändern: Die Anzahl der Epochs in einem Trainingslauf ist dabei ein wichtiger Faktor. Je mehr Epochs, desto besser wird das Modell auf die Daten abgestimmt. Das kann auch die Genauigkeit von Live-Daten verbessern, die mit den Trainingssätzen übereinstimmen. Allerdings kann es auch in einem “Overtraining” resultieren - also einem Modell, das zuversichtlich die falschen Ergebnisse für Randfälle vorhersagt.

Häufig verwendete Neuronale Netzwerkarchitekturen

Es gibt viele verschiedene Arten von neuronalen Netzen, von denen jede für bestimmte Aufgaben besser geeignet ist. Hier sind einige der häufigsten Architekturen:

Feed Forward Neural Networks (FFNN) / Fully Connected Neural Networks

Dies ist quasi das „Brot-und-Butter NN“. Dieses findet man besonders häufig als Bestandteil größerer Architekturen, oft im Übergang von einem Teil der Architektur zu einem anderen. Wie es funktioniert, es aufgebaut ist und wie es angewandt wird, haben wir in unserem vorigen Artikel im Detail ausgeführt.

Nutzt man es alleine ohne andere, komplexere Varianten, eignet es sich für einfachere Probleme. Heutzutage ist es oft wichtig, um Blöcke in komplexeren Architekturen zu verbinden. Oder es ermöglicht am Ende einer komplexen Architektur, ein Ergebnis aus der „Vorarbeit“ der spezialisierten Architekturen zu extrahieren.

Convolutional Neural Networks (CNN)

CNNs sind die Stars, wenn es um Bilderkennung und Spracherkennung geht. Sie liefern wesentlich bessere Ergebnisse als einfache Feed-Forward Netze. CNNs sind (sehr grob) inspiriert von Strukturen im visuellen Kortex von Wirbeltieren. Mathematischer ausgedrückt benutzen sie sogenannte Faltungen für Ihre Berechnungen. Elektroingenieure werden sich hier zu Hause fühlen: CNNs sind im Grunde trainierbare Filter in 1D, 2D oder 3D.

Bei einem CNN bleibt die ursprüngliche Struktur des Inputs (meist Bilder) erhalten, d.h. die Werte werden weiterhin als 2D-Anordnung von Pixeln aufgefasst. Statt einfacher Addition und Multiplikation wird eine mathematische Operation namens „Faltung“ durchgeführt.

Anders bisherige Ansätze sind sie sehr robust gegenüber Veränderungen im Bild wie Drehungen, Farb- und Helligkeitsänderungen usw. d.h. Das geschieht durch mehrere hintereinandergeschaltete Netze, die über mehrere Schritte ausgehend von einfachen (Kontrast-)Kanten und Punkten komplexere Gebilde höherer Ordnung wie z.B. Gesichter, Gebäude, Verkehrszeichen, Zahlen, Tiere usw. Breite und Höhe des Bildes sind die ersten 2 Dimensionen, die Farbinformation die dritte und strukturelle Eigenschaften des Bildes an einem Punkt (z.b. RGB-Farbwerte und damit Kontraste, Helligkeiten usw. In weiteren Schichten werden diese Matritzen weiter reduziert, z.b. Wenn die Eigenschaftsmatritzen klein genug sind (z.b. Für Videos zB.

Rekurrente Neuronale Netze (RNN)

Der bekannteste Vertreter dieser Klasse von Neuronalen Netzen ist das Long-Short-Term-Memory (LSTM). Sehr oft, wenn Daten eine nicht eindeutige, feste Länge haben (Filme, Text, Audioaufnahmen, Börsenkurse) kommen RNNs zum Einsatz. Meistens nehmen sie ein anderes Netz mit ins Boot. So kann ein CNN, das mit Bildern klarkommt z.B. zusammen mit einem RNN auf Filmen arbeiten.

LSTMs sind wesentlich komplexer als Feed-Forward-NNs. Die Eingabe in einen LSTM Block ist nicht nur der Input (grün, mit „x“ gekennzeichnet), sondern auch die Ausgabe eines LSTM Blocks aus einem vorherigen Schritt (blau). Die interne Struktur erlaubt es einem LSTM zu lernen, welche zuvor gesehenen Informationen zur Bildung des Outputs („o“, in pink) Beitragen sollen. So wird eine Art von Gedächtnis simuliert. Dabei können sowohl weit zurückliegende Informationen als auch Informationen aus direkt zuvor gemachten Eingaben verarbeitet werden. Letztere gibt es zwar auch, jedoch sind sie momentan eher eine akademische Kuriosität und funktionieren vor allem ganz anders als Rekurrente Netze.

Autoencoder

Autoencoder sind eine Klasse von Neuronalen Netzen, die keine festen Label zum Lernen brauchen, sich also vor allem für Unüberwachtes Lernen bei Neuronalen Netzen eignen. Autoencoder sind eine bestimmte Art, Neuronale Netze aufzubauen und anzuordnen. Prinzipiell kann man jede Art von Neuronalem Netz in einen Autoencoder verwandeln. Der Vorteil von Autoencodern ist, dass sie keine „Zieldaten“ brauchen, man spart also viel Arbeit bei der Datenvorverarbeitung. Der Nachteil ist, dass es ihnen viel schwerer fällt, etwas zu lernen und es auch nicht garantiert ist, dass das Gelernte nützlich ist.

Das besondere bei einer Autoencoder Struktur ist, dass die Schichten des NN in der Mitte sehr schmal werden und dann wieder breiter. Die Ausgabe ist dann wieder so groß wie die Eingabe. Dies zwingt den Autoencoder, die Eingabedaten eigenständig zu komprimieren, also unnütze Informationen zu entfernen.

Transformer

Transformer sind aus sogenannten Attention-Layern aufgebaut, die es dem Netz ermöglichen, zu lernen, welche Teile eines Inputs zueinander in Beziehung stehen. Denkt man an Sprache, bedeutet dies aufeinander Bezug nehmende Satzteile und komplexe Syntax. Hier wird hier schnell klar, wieso Transformer für das Feld des Textverstehens (und ggf. der Texterzeugung!) ein enormer Schritt sind.

Ein neuer Layer-Typ, „Attention“ (Aufmerksamkeit) genannt, erlaubt es Transformers selektiv Eingaben zueinander in Bezug zu setzen.

Auch hier ist auf den ersten Blick zu erkennen, dass die Architektur wesentlich komplexer ist, als bei einfachen NN. Das uns bereits bekannte Feed-Forward-NN ist hier nur einer von vielen Bestandteilen (in blau). Auch zeigt sich hier sehr schön, dass wir uns von biologischen Vorbildern sehr weit entfernt haben: Im Gegensatz zu z.B. CNN gibt es für Transformer keine Inspiration aus der Natur, es geht hier nur noch um Lineare Algebra, nicht mehr um „Neuronen“.

Kombination verschiedener Architekturen

Eine Architektur kommt selten allein. Die meisten modernen Neuronalen Netze kombinieren viele verschiedene Techniken in Schichten, so dass man meist von Layer-Typen statt von Netz-Typen spricht. Man kombiniert z.B. mehrere CNN-Layer, ein Fully-Connected Layer und ein LSTM-Layer. Eventuell das ganze auch noch auf eine Art und Weise, die einen Autoencoder ergibt. Wichtig hierbei - die Netze wachsen nicht. Die Struktur wird durch eine Programmiererin in Stein gemeißelt und dann trainiert. Das Netz kann nicht feststellen, dass es ein Layer nicht braucht, dieses wieder entfernen und sich quasi selbst “optimieren”.

Deep Learning

Deep Learning ist eine junge Teildisziplin des maschinellen Lernens. Deep Learning verwendet künstliche neuronale Netzwerke und sehr große Datenmengen, um ein gegebenes Ziel zu erreichen. Das Ziel ist die direkte Abbildung der Mechanismen unseres Gehirns. Auf Grundlage von bereits gesammelten Informationen kann das System das Erlernte immer wieder mit neuen Informationen verbinden und somit wiederum „lernen“. Damit ist das neuronale Netz dazu in der Lage, Prognosen oder Entscheidungen zu treffen und diese auch zu hinterfragen.

Deep Learning verbindet Big Data mit den Algorithmen, die auf künstlichen neuronalen Netzwerken angewendet werden. Die Daten aus sehr großen Datenbanken werden normalisiert und die Merkmale extrahiert. Danach werden die Ergebnisse erzeugt und wiederum in das Netzwerk gegeben, um ein noch besseres Ergebnis zu erzielen.

Deep Learning eignet sich dort besonders gut, wo große Datenmengen nach Patterns untersucht werden müssen. Aktuelle Anwendungsbeispiele sind die Gesichts- und die Spracherkennung. Besonders bei Letzterem kann man den Prozess des Deep Learnings sehr gut beobachten: Je öfter man mit dem Sprachassistenten von Apple spricht, desto besser kann Siri reagieren, teilweise sogar auf Dialekte. Die Verwendung von Algorithmen des Deep Learnings mithilfe von künstlichen neuronalen Netzwerken findet immer mehr Verbreitung und die Bandbreite der Applikationen wird immer größer - von Verkehrszeichenerkennung bis hin zu medizinischen Untersuchungen.

tags: #java #hausaufgabe #neuronen