Processing

Processing IDE[[wikipedia:Integrated development environment]]

Processing ist eine Programmiersprache und Entwicklungsumgebung, die 2001 am Massachusetts Institute of Technology von den Erfindern Ben Fry und Casey Reas als quelloffenes Projekt ins Leben gerufen wurde. Die Sprache wurde speziell für die Bereiche Simulation und Animation entworfen und richtet sich daher vorwiegend an Personen aus der Gestaltung und Kunst.

Das sogenannte Sketchbook verkörpert das Prinzip einer stark vereinfachten IDE[[wikipedia:Integrated development environment]] (Integrated Development Environment). Dem Nutzer steht eine simple Toolbar, ein Editor und Textfeld zur Verfügung. Über das Hauptprogramm hinaus erzeugte Klassen werden in eigenen Tabs untergebracht. Jede dieser Sketches ist tatsächlich nur eine Unterklasse der PApplet Java-Klasse, welche fast alle Features der Sprache implementiert. Alle zusätzlich erstellten Klassen werden beim Kompilieren als innere Klassen behandelt. Wenn man in Java programmieren will, muss dies explizit geschehen.

Weitere Informationen bzgl. Processing und dessen Verwendung lassen sich auf der Hauptseite und im WikiHaiwaiian for ''fast'' (not an acronym). It is the name for a hypertext system for websites where the user may not only read the content but is also able to change it instantaneously through the browser.-Eintrag zu Processing finden.


Neuronale Netze

Da man in Processing direkt in Java programmieren kann, sind einem bzgl. der Komplexität der Programme keine Grenzen gesetzt. Für die einfache Erstellung neuronaler Netze kann man zusätzliche Libraries verwenden, die man dem Sketchbook beifügt. Als Beispiel wird hier die nn-library von Daniel Shiffman verwendet.

i(1) i(2)
0.2 0.2 0.2
0.2 0.2 0.8
0.8 0.2 0.8
0.8 0.8 0.2

Bei dem unten aufgeführtem Beispiel handelt es sich um eine leicht modifizierte Version eines neuronalen Netzes von Daniel Shiffman [1]. Mittels eines wikipedia:Multilayer perceptrons wird hier versucht das nichtlinear trennbare XOR Problem zu lösen. Jede Ecke des Würfels entspricht einer 0 oder 1, wobei sich gleiche Werte je Fläche diagonal spiegeln, d.h. benachbarte Eckpunkte sind niemals beide gleichzeitig 0 oder 1, sondern immer im Wechsel. Zwischen 2 Eckpunkten wird entlang der Flächen interpoliert, sodass man für Werte zwischen 0 und 1 eine Annährung erhält. Zur Verdeutlichung wurde in den folgenden Beispielbildern als mögliches Antwortpaar für das Training via wikipedia:Backpropagation [0.2, 0.8] gewählt (siehe angepasste Wahrheitstabelle links). Damit wird dem Netz ein schnelleres Lernen ermöglicht, da die Ableitungen der wikipedia:Sigmoid function an diesen Stellen nicht allzu niedrig sind. Je weiter gen 0 und 1, desto niedriger werden die abgeleiten Werte (Anstieg der Tangenten an den jeweiligen Punkten) und desto weniger wird der Lernprozess beeinflusst, bzw. die Gewichte verändert.

XOR possible state (1)
XOR possible state (2)
XOR possible state (3)
XOR possible state (4)
XOR possible state (5)

Anlegen eines neuronalen Netzes

Multilayer Perceptron

Zunächst erzeugt man ein neues Netzwerk, für welches man vorgibt, wieviele input und hidden Neuronen enthalten sein sollen. Zusätzlich wird je Ebene intern ein BIAS Neuron angelegt (siehe Abbildung rechts). Nach Modifikation der Library wäre es ebenfalls möglich die Anzahl der Output-Neuronen zu bestimmen. Davon wurde hierbei jedoch abgesehen.

Network net = new Network(2,3);

Trainieren des neuronalen Netzes

In der setup() Methode initialisiert man einmalig die Eingabewerte (hier mit den ursprünglichen Werten 0 und 1 für das XOR Problem), welche im globalen Scope, d.h. dem programmweiten Namensraum, zur Verfügung stehen. Dies bedeutet, dass jede erstellte Methode und instanziierte Klasse auf diese Variablen Zugriff hat.

ArrayList inputs = new ArrayList();

In dem einmaligen Setup werden die Eingabewerte, welche für das Training benötigt werden, initialisiert.

void setup() {
  size(400, 400, OPENGL);
  inputs.add(new float[] { 0, 0 });
  inputs.add(new float[] { 1, 0 });
  inputs.add(new float[] { 0, 1 });
  inputs.add(new float[] { 1, 1 });
}

Das Training wird randomisiert jeden Frame 5 mal wiederholt. Die draw()-Methode wird fortlaufend nach setup() ausgeführt und bei Programmabbruch terminiert. Sie sollte daher niemals explizit aufgerufen werden.

void draw() {
  // One epoch.
  int rate = 5;
  for (int i = 0; i < rate; i++) {
    // Randomly select from training set.
    int pick = int(random(inputs.size()));
    float[] inp = (float[]) inputs.get(pick); 
    // Choose the appropriate answer for the training's input.
    float known = 1.0;
    if ((inp[0] > 0.5 && inp[1] > 0.5) || (inp[0] < 0.5 && inp[1] < 0.5)) known = 0.0;
    float result = net.train(inp, known);
  }
}

Um nach jedem Durchlauf den mittleren quadratischen Fehler zu erhalten, berechnet man zunächst die Ausgabe des gesamten Netzes und anschließend den Fehler wie folgt:

float calcError() {
  float mse = 0.0;
  for (int i = 0; i < inputs.size(); ++i) {
    // Retrieve single test input pair.
    float[] inp = (float[]) inputs.get(i);
    // Calculate appropriate answer.
    float known = 1.0;
    if ((inp[0] > 0.5 && inp[1] > 0.5) || (inp[0] < 0.5 && inp[1] < 0.5)) known = 0.0;
    // Feed forward each test input.
    float result = nn.feedForward(inp);
    // Add up mean square error from output.
    mse += (result - known) * (result - known);
  }
  // Return root mean squared error.
  return sqrt(mse / inputs.size());
}

Erläuterung der Library

Ein Neuron hat grundlegend folgende Eigenschaften:

  • Ausgabewert
  • Liste der Verbindungen
  • BIAS-Status (0 oder 1)

Für alle nicht-BIAS Neuronen ist der BIAS-Status 0. Ein Neuron ist dann ein BIAS Neuron, wenn es bei der Initialisierung einen beliebigen Integer erhält. Die Ausgabe eines Neurons wird für jedes einzeln berechnet. Dafür stellt die Basisklasse folgende Methode bereit:

public void calcOutput() {
  if (!bias) {
    float sum = 0;
    for (int i = 0; i < connections.size(); ++i) {
      Connection c = (Connection) connections.get(i);
      Neuron from = c.getFrom();
      Neuron to = c.getTo();
      // Only calculate the incoming connection's weight (input) only.
      if (to == this) { sum += from.getOutput() * c.getWeight(); }
    }
    output = f(sum);
  }
}

Die Ausgabe ist das Resultat der Aktiviersungsfunktion (transfer function), hier eine Sigmoid Funktion.

public static float f(float x) {
  return 1.0f / (1.0f + (float) Math.exp(-x));
}

Bei der initialen Erstellung des Netzes werden die einzelnen Ebenen miteinander verbunden. Eine Verbindung hält die Referenz von einem Neuron zu einem anderen, sowie das dazugehörige Gewicht, welches zunächst zufällig gewählt wird. Für den Konstruktor und die Gewichtsanpassung ergibt sich:

public Connection(Neuron a_, Neuron b_) {
  from = a_;
  to = b_;
  // A random value between -1 and 1.
  weight = (float) Math.random() * 2 - 1;
}

public void adjustWeight(float delta) {
  weight += delta;
}

Der Lernprozess - Backpropagation

Als Eingabe wird ein zufällig gewähltes Paar an Werten aus dem Trainingsset und der entsprechende Erwartungswert übergeben.

public float train(float[] inputs, float answer) {
  ...
}

Zunächst wird die derzeitige Netzausgabe via eines Feed-forward Systems berechnet - Ebene für Ebene werden die Ausgabewerte einzelner Neuronen akkumuliert, um am Ende eine Netzausgabe zu erhalten.

float result = feedForward(inputs);

public float feedForward(float[] inputVals) {
  // Feed input.
  for (int i = 0; i < inputVals.length; i++) {
    input[i].input(inputVals[i]);
  }

  // Calculate hidden layer output.
  for (int i = 0; i < hidden.length - 1; i++) {
    hidden[i].calcOutput();
  }

  // Calculate net output.
  output.calcOutput();
  return output.getOutput();
}

Daraufhin werden zuerst die äußeren Gewichte, vom Output zum Hidden Layer, angepasst. Die hier verwendete Lernrate (allgemein mit dem griechischen Buchstaben Eta bezeichnet) bestimmt zusätzlich den Lernfortschritt. Je größer dieser Wert, desto schneller lernt das Netz (mit größeren Sprüngen). Dabei kann es jedoch passieren, dass die Anpassungen zu schnell zu ungenau werden, oder man sogar zwischen Ergebnissen hin und her springt. Ein weiteres, schwerwiegendes Problem ist der Lernstillstand, der durch lokale Minima verursacht werden kann.

ArrayList connections = output.getConnections();
float deltaOutput = result * (1 - result) * (answer - result);
for (int i = 0; i < connections.size(); i++) {
  Connection c = (Connection) connections.get(i);
  // The weight is adjusted depending on the incoming input
  // and a calculated difference between the expected answer
  // and overall net result, as well as a predefined defined ETA
  c.adjustWeight(deltaOutput * c.getFrom().getOutput() * ETA);
}

Danach erst erfolgt die Anpassung der inneren Gewichte - vom Hidden Layer zum Input Layer.

// For all hidden neurons...
for (int i = 0; i < hidden.length; i++) {
  connections = hidden[i].getConnections();
  float sum  = 0;
  for (int j = 0; j < connections.size(); j++) {
    Connection c = (Connection) connections.get(j);
    // Sum up the output of all outgoing connections.
    if (c.getFrom() == hidden[i]) { sum += c.getWeight() * deltaOutput; }
  }
  for (int j = 0; j < connections.size(); j++) {
    // Adjust the weight of all incoming connections accordingly.
    Connection c = (Connection) connections.get(j);
    if (c.getTo() == hidden[i]) {
      float output = hidden[i].getOutput();
      float deltaHidden = output * (1 - output) * sum;
      c.adjustWeight(deltaHidden * c.getFrom().getOutput() * ETA);
    }
  }
}
return result;

Dies sei nur ein Beispiel bzgl. neuronaler Netze in Processing. Desweiterin existiert noch die Möglichkeit FANN Java Bindings zu benutzen. Dabei handelt es sich um eine Open Source (Fast Artificial) Neural Network Library, deren Multilayer Netzwerke in C implementiert sind und damit einen klaren Geschwindigkeitsvorteil bieten.

Unter näherer Betrachtung der hier vorgestellten Library scheint eine solche mit wenig Aufwand umsetzbar. Zumindest Konzepte wie simple Multilayer Perceptrons mit jeweils nur einer Lage Input, Hidden und Output Neuronen stellen keine Herausforderung dar.

Weitere Beispiele

Genetic Algorithm - Das Ziel dieser Simulation ist das Lernen über Generationen. Hinter jeder Ameise steckt ein neuronales Netz, das ihre Bewegungen beeinflusst. Dessen Aufgabe ist es, die roten Punkte (Nahrung) einzusammeln. Nach einem bestimmten Zeitraum wird eine neue, 'bessere' Generation an Ameisen geschaffen.

Neural Network Visualization - Hierbei handelt es sich um eine simple Visualisierung eines neuronalen Netzes, bei dem sich eine Fläche an eine Vorgabe anpasst. Die grüne Fläche stellt die Zielausrichtung und die blauen Flächen die verschiedenen Hidden Neuronenschichten dar.

Referenzen

  1. Daniel Shiffman: The Nature of Code - Neural Networks