cdrnet website > NeuroBox

Einführung in die NeuroBox Bibliothek

Geschrieben von: Christoph Rüegg

« Neuronale Netze (NN), oft auch als künstliche neuronale Netze (KNN) oder artificial neural networks (ANN) bezeichnet, sind informationsverarbeitende Systeme, die aus einer grossen Anzahl einfacher Einheiten (Zellen, Neuronen) bestehen, die sich Informationen in Form der Aktivierung der Zellen über gerichtete Verbindungen (connections, links) zusenden.

Das Studium neuronale Netze ist motiviert durch ihre grobe Analogie zu den Gehirnen von Säugetieren, bei denen Informationsverarbeitung durch sehr viele Nervenzellen stattfindet, die im Verhältnis zum Gesamtsystem sehr einfach sind und die den Grad ihrer Erregung über Nervenfasern an andere Nervenzellen weiterleiten.

Neben dieser Motivation durch die teilweise Ähnlichkeit zu erfolgreichen biologischen Systemen beziehen (künstliche) neuronale Netze aber einen grossen Teil ihrer Motivation, speziell für die Naturwissenschaften, aus der Tatsache, dass es sich hierbei um massiv parallele, lernfähige Systeme handelt, die auch für sich genommen als parallele Algorithmen interessant sind. Diese Algorithmen sind in Form von Programmen, Netzwerksimulatoren oder auch in Form spezieller neuronaler Hardware in vielen Anwendungsgebieten einsetzbar. » (A. Zell, Simulation neuronaler Netze, ISBN 3-486-24350-0)

NeuroBox ist eine freie C# Bibliothek für den Aufbau, das Trainieren und Propagieren solcher neuronalen Netzwerke mit dem .NET Framework, die sich (bisher) vor allem auf einfache feedforward Netze konzentriert. Dieser Artikel soll einen Einblick in den Aufbau der Bibliothek bieten, nicht jedoch die Grundlagen neuronaler Netze behandeln.

Überblick über die Komponenten

Die NeuroBox besteht im Grunde aus zwei Bereichen. Zum einen ist das das rohe Netzwerk (NeuralNetwork) und zum anderen ein Set von aufgabenorientierten Klassen, die das Arbeiten mit der Bibliothek vereinfachen (BuildingBlocks). Üblicherweise arbeitet man nur mit einem der beiden Bereichen.

Cdrnet.NeuroBox.NeuralNetwork

Der Kern der Bibliothek, das rohe Netzwerk, spielt sich einzig in diesem Namensraum ab. In ihm sind entsprechend alle wichtigen Komponenten definiert: Netzwerke, bestehend aus Schichten, Neuronen und Synapsen, sowie die verwendeten Ereignisse und einige Elemente, die für die Konfiguration des Netzwerkes zuständig sind.

Cdrnet.NeuroBox.BuildingBlocks

Um mit dem rohen Netzwerk arbeiten zu können, ist ein technisches Verständnis der Funktionsweise und des Verhaltens neuronaler Netze notwendig. Im BuildingBlocks Namensraum stehen daher spezielle Klassen zur Verfügung, die viele technische Aspekte wie die Strukturierung des Netzwerks und ein automatisiertes Trainingssystem für typische Anwendungen wie der Mustererkennung (Pattern Matching) übernehmen. Building Blocks sind also flexible Module, die mit minimalem Aufwand in eine Anwendung eingebunden werden können. Flexibel sind sie insofern, als dass einzelne Komponenten zur Laufzeit durch benutzerdefinierte Versionen ersetzt werden können, sowie durch einen relativ hohen Grad an Konfigurierbarkeit durch spezialisierte (abgeleitete) Konfigurationsklassen.

Das neuronale Netzwerk

Die NeuroBox ist speziell auf Feedforward Netze ausgelegt. Solche Netze sind gerichtete Graphen, deren Ecken in mehreren Schichten (Layers) organisiert sind. Typisch sind Netze mit einer Eingabeschicht, einer verstecken Zwischenschicht sowie einer Ausgabeschicht.

Cdrnet.NeuroBox.NeuralNetwork.Network

Dies ist die zentrale Klasse in diesem Bereich; sie repräsentiert ein ganzes Netzwerk, hortet eine Konfigurationsinstanz und die Netzwerkschichten und ist für die meisten Fälle die Schnittstelle zwischen der Bibliothek und der Anwendung, in der sie verwendet wird. Erster Schritt einer Implementation ist entsprechend das Instanzieren dieser Klasse:

Network network = new Network();

Cdrnet.NeuroBox.NeuralNetwork.Layer

Diese Klasse repräsentiert eine Netzwerkschicht. Diese Schichten sind als doppelt verkettete Liste implementiert und referenziert daher neben einer Konfigurationsinstanz den jeweils vorherigen und nachfolgenden Layer - sofern es einen solchen gibt. Layer ist als Collection implementiert; in dieser Collection werden alle Neuronen des Layers verwaltet.

Layers werden üblicherweise via AddLayer() Methode der Network Instanz erstellt und automatisch mit der gegebenen Anzahl Neuronen initialisiert.

network.AddLayer(5);

Cdrnet.NeuroBox.NeuralNetwork.Neuron

Neuronen sind zusammen mit den Synapsen die eigentlich interessanten Objekte eines neuronalen Netzes. Neuronen haben in erster Linie eine Aktivität (Netzeingang, Fliesskommazahl) und einen daraus berechneten normalisierten Ausgang (Fliesskommazahl in einem geschlossenen Intervall), referenzieren jedoch auch eine Konfigurationsinstanz sowie eine Liste von ein- und ausgehen Synapsen.

Cdrnet.NeuroBox.NeuralNetwork.Synapse

Die aktive Verbindung zwischen Neuronen. Synapsen sind gewichtet (Fliesskommazahl) und referenzieren auf das Eingangs- und Ausgangsneuron.

Von sich aus hat ein Netzwerk keine Synapsen. Die Neuron Klasse bietet für die Verknüpfung die Methode ConnectToNeuron() an, die eine passende Synapse erstellt und korrekt bei beiden beteiligten Neuronen registriert. Um eine Synapse zu entfernen, reicht es, ihre Disconnect() Methode aufzurufen; sie entfernt automatisch ihre Registrierung bei alle beteiligten Neuronen.

Synapse s = network.LastLayer[0].ConnectToNeuron(network.LastLayer[1]);
s.Disconnect();

Aufbau eines Netzwerkes

Um ein Netzwerk aufzubauen müssen die benötigten Schichten mit ihren Neuronen erstellt werden. Im zweiten Schritt werden diese Neuronen dann beliebig untereinander mittels Synapsen verknüpft.

Der erste Schritt erfolgt am einfachsten über die AddLayer Methode, bei der als Parameter gleich die Anzahl der zu konstruierenden Neuronen angegeben werden kann. Der Inputlayer muss dabei nicht separat erstellt werden, da er bereits im Konstruktor der Network Klasse instanziert wurde. Hingegen müssen vor dem 2. Schritt die Neuronen des Inputlayers initialisiert werden. Dies erfolgt entweder durch Data Binding oder durch den Aufruf von InitUnboundInputLayer() (mehr dazu im folgenden Abschnitt "Datenaustausch - Data Binding").

Der zweite Schritt kann entweder automatisiert oder von Hand mittels ConnectToNeuron() vollzogen werden. Automatisiert stehen zwei Methoden zur Verfügung. Die erste verbindet das Feedforward Netzwerk klassisch Schichtenweise (alle Neuronen eines Layers werden mit allen Neuronen des folgenden Layers verbunden):

network.AutoLinkFeedforward();

Die zweite Variante entspricht der obigen, verbindet zusätzlich aber alle Neuronen eines Layers auch untereinander (laterale Rückkopplung):

network.AutoLinkFull();

Datenaustausch - Data Binding

Bei einem neuronalen Netz müssen vor allem drei Datenmengen ausgetauscht werden: Die Netzwerkeingabe (Input), die berechnete Ausgabe (Output) sowie die zu trainierende Zielausgabe (Training). Ersteres und letzteres in Richtung Netzwerk, zweiteres in Richtung Anwendung. Alle diese Datenmengen werden als Arrays von Fliesskommazahlen (double[]) realisiert. Die aktuelle Ausgabe kann wie folgt abgerufen werden:

double[] currentOutput = network.CollectOutput();

Für die anderen beiden Mengen wurde das Konzept der Datenbindung verwendet. Dabei werden von beiden Seiten, Anwendung und Netzwerk, zwei Arrays gehalten; eines für die aktuelle Eingabe und eines für die aktuelle Zielausgabe. Soll ein andere Muster trainiert werden, so kann die Anwendung einfach die Einträge der beiden Arrays anpassen; keine Methodenaufrufe auf Seite des Netzwerkes sind nötig. Damit dies funktioniert müssen die entsprechenden Arrays mit der korrekten Grösse an das Netzwerk gebunden werden. Achtung: Der Aufruf von BindInputLayer initialisiert die Eingabeschicht neu; diese Methode sollte daher möglichst früh aufgerufen werden, in jedem Fall bevor irgendwelche Synapsen erstellt wurden.

double[] input = new double[4];
double[] training = new double[2];
network.BindInputLayer(input);
network.BindTraining(training);

Das Databinding ist nicht in jedem Fall praktikabel, insbesondere da die Anwendung irgendwo das Array speichern können muss. Für diesen Fall ist das unbound Szenario vorgesehen. Dabei muss zu Beginn der Input Layer mittels InitUnboundInputLayer initialisiert werden, danach können die Daten jeweils mittels PushUnboundInput und PushUnboundTraining übergeben werden.

network.InitUnboundInputLayer(4);
network.PushUnboundInput(new double[] {-1,1,0,0.5});
network.PushUnboundOutput(new double[] {1,-1});

Arbeiten mit dem Netzwerk

Ist das Netzwerk fertig aufgebaut, so kann man es nun nach belieben berechnen lassen (propagieren) und trainieren (hier: backpropagieren). Wird das Netz berechnet, so kann danach mittels CollectOutput() die aktuelle Ausgabe der Outputneuronen zusammengestellt werden:

network.CalculateFeedforward();
double[] currentOutput = network.CollectOutput();

Das trainieren ist nicht viel aufwändiger. Wurde das zu trainierende Muster entweder mittels Databinding oder durch PushUnboundInput() und PushUnboundTraining() übergeben, so reicht ein Aufruf von TrainCurrentPattern() um das Netzwerk durch Anpassen der Synapsengewichte zu trainieren. Diese Methode erwartet zwei Boolean Parameter. Mit dem ersten wird festgelegt, ob das Netzwerk vor dem Trainieren erst noch berechnet werden muss oder ob dies übergangen werden kann (weil bereits berechnet), mit dem zweiten ob es nach dem Trainieren gleich neu berechnet werden soll.

network.TrainCurrentPattern(false,true);

Konfigurieren des Netzes

Über die Configuration Klasse können viele Aspekte des Netzes an die eigenen Anforderungen angepasst werden. Solche Aspekte beinhalten insbesondere die Aktivierung verschiedener Trainingsoptimierungen, deren Konfiguration sowie weitere Netzwerkparameter. Zugriff auf die Konfiguration erhält man über die Netzwerk Instanz. Von sich aus verwenden alle Objekte die Selbe Konfiguration, bei Bedarf kann aber auch beispielsweise einzelnen Schichten oder Neuronen spezielle andere Instanzen übergeben.

network.Configuration.LearnRate = 0.3;

Folgende Parameter stehen zur Verfügung:

Der Pattern Matching Building Block

Das direkte Arbeiten mit den Netzwerkklassen von oben ist auf Anwendungsseite relativ aufwändig, muss doch beispielsweise selber ein System für die Strukturierung des Netzes entworfen werden, ausserdem steht kein System zum Verwalten mehrere Patterns und deren automatisiertem Training zur Verfügung. Einen Grossteil dieser Arbeit nimmt unter anderem die GridPatternMatching Klasse des BuildingBlocks Namensraum ab. Folgender Befehl initialisiert diesen Building Block für Muster der Breite 7 und Höhe 9 und mit 10 verschiedenen Ausgaben (=10 Ausgabeneuronen).

GridPatternMatching gpm = new GridPatternMatching(7,9,10);

Auf das arbeitende Netzwerk im Hintergrund hat man (falls nötig) Zugriff über die Eigenschaft NeuralNetwork.

Patternverwaltung

Die verschiedenen zu trainierenden Muster werden in Instanzen der GridPattern Klasse verwaltet. GridPatterns speicher zwei boolsche Arrays: Das Input Array bestimmt, welche Pixel im Eingabegitter aktiv (true) sein sollen, das Training Array auf welche Output Neuronen die Eingabe trainiert werden können. Daneben speichert es auch einen Titel, die Masse des Inputgitters, die Anzahl Outputneuronen und das zu trainierende Output Neuron (aus diesem wird automatisch das zuvor erwähnte Training Array generiert).

In der Praxis erstellt man aber kaum selber Instanzen dieser Klasse, sondern nutzt die wesentlich bequemere AddPattern() Methode der GridPatternMatching Klasse. Folgender Aufruf erstellt ein Pattern, welches das Muster mit dem ersten Eingabefeld aktiv und dem zweiten deaktiv auf das zweite Ausgabeneuron (nullbasiert, daher 1) hin trainiert.

gmp.AddPattern("Muster JA/NEIN",1,new bool[] {true,false});

Sind die gewünschten Patterns geladen, so kann ein beliebiges Pattern aktiviert werden. Dabei werden seine Eingabe und das gewünschte Training automatisch in das Netzwerk übertragen.

gmp.SelectPattern(2);

Häufig möchte man die Eingaben leicht verändern ohne dafür extra ein neues Pattern zu erstellen. Für diesen Fall kann man mit der folgenden Methode ein Array (bool oder double) mit den neuen Inputdaten übergeben:

gmp.PushInput(array);

Arbeiten mit dem Netzwerk

Das berechnen des aktuellen Netzes funktioniert wie gewohnt, mit dem kleinen Zusatz, das hier als Returnwert der Methode der Index des Neurons mit der höchsten Aktivität (also der beste Kandidat für die Ausgabe) zurückgegeben wird.

int index = gmp.CalculateCurrentNetwork();

Dasselbe gilt für das Trainieren des aktuellen Patterns:

gmp.TrainCurrentNetwork();

Neu hinzugekommen ist die Möglichkeit, automatisiert alle Patterns trainieren zu lassen. Dabei wird ein (austauschbarer) INetworkTrainer verwendet:

if(gmp.AutoTrainNetwork())
MessageBox.Show("erfolgreich");
else
MessageBox.Show("fehlgeschlagen");

Ähnlich wie der NetworkTrainer kann auch die StructureFactory, die für den Aufbau des Netzwerks zuständig ist, durch benutzerdefinierte Implementationen der IStructureFactory Schnittstelle ausgetauscht werden.

Erweiterte Konfiguration

Mit der von Configuration abgeleitete GridConfiguration Klasse wurden einige für das GridPatternMatching und speziell für die Standard StructureFactory spezifische neue Parameter eingeführt: