GMU:Neuronale Netze - eine Einführung für Künstler/Präsentation/Neuronale Netze in Processing: Difference between revisions

From Medien Wiki
Line 118: Line 118:
</source>
</source>


Bei <code>f(float)</code> handelt es sich um die [http://en.wikipedia.org/wiki/Artificial_neuron#Types_of_transfer_functions Aktiviersungsfunktion] (transfer function), hier eine Sigmoid Funktion.
Die resultierende Ausgabe ist das Result der [http://en.wikipedia.org/wiki/Artificial_neuron#Types_of_transfer_functions Aktiviersungsfunktion] (transfer function), hier eine Sigmoid Funktion.


<source lang="java">
<source lang="java">
Line 143: Line 143:
=== Der Lernprozess - Backpropagation ===
=== Der Lernprozess - Backpropagation ===


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


<source lang="java">
<source lang="java">
Line 184: Line 184:
</source>
</source>


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


<source lang="java">
<source lang="java">

Revision as of 00:41, 12 July 2010

Processing

Processing IDE

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 (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 Wiki-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 unteren Beispiel handelt es sich um eine leicht modifizierte Version eines neuronalen Netzes von Daniel Shiffman [1]. Mittels eines 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 nie gleich 0 oder 1. 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 im Folgenden als mögliches Antwortpaar für das Training via Backpropagation [0.2, 0.8] gewählt (siehe angepasste Wahrheitstabelle links).

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 in der globalen Scope zur Verfügung stehen.

ArrayList inputs = new ArrayList();

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.

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); 
    float known = 1.0;
    // Choose the appropriate answer for the training's input.
    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 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) {
    float[] inp = (float[]) inputs.get(i); 
    float known = 0.8;
    if ((inp[0] > 0.5 && inp[1] > 0.5) || (inp[0] < 0.5 && inp[1] < 0.5)) known = 0.2;
    float result = nn.feedForward(inp);
    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).
      if (to == this) { sum += from.getOutput() * c.getWeight(); }
    }
    output = f(sum);
  }
}

Die resultierende Ausgabe ist das Result 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. 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 berechnet, 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();
}
</sourceu>

Daraufhin werden zuerst die äußeren Gewichte, vom Output zum Hidden Layer, angepasst.

<source lang="java">
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);
  c.adjustWeight(deltaOutput * c.getFrom().getOutput() * ETA);
}

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

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);
    if (c.getFrom() == hidden[i]) { sum += c.getWeight() * deltaOutput; }
  }
  for (int j = 0; j < connections.size(); j++) {
    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;

Referenzen