Lernen aus Erfahrung

Neuronale Netze
Kommentare

Das menschliche Gehirn ist imstande, ohne explizite Anweisungen und rein aus Erfahrung zu lernen. Seit die Informatik als wissenschaftliche Disziplin existiert, ist es ein Ziel und ein Traum, diesen Prozess mit elektronischen Hilfsmitteln nachzuahmen und zu simulieren. Neuronale Netze fungieren dabei als beliebtes Werkzeug, da sie dem Vorbild des menschlichen Gehirns und dessen Neuronen (Nervenzellen) rein schematisch am ehesten entsprechen. 

Um sich in der Thematik künstlicher neuronaler Netze („künstlich“ in Abgrenzung zu natürlichen neuronalen Netzen genutzt) besser zurechtzufinden, ist ein kurzer Abstecher in die Biologie und die Funktionsweise natürlicher Neuronen hilfreich. Wie wir sehen werden, lässt sich das Prinzip, das hinter der Funktionsweise von Nervenzellen steckt, relativ einfach auf ein Softwaremodell übertragen. Ein natürliches Neuron besteht, vereinfacht ausgedrückt, aus drei Komponenten (Abb. 1):

  • Zellkörper: Er ist für unsere Betrachtungen jedoch nicht weiter relevant.
  • Dendriten (gr. Dendron, Baum): Sie fungieren als Inputelemente der Nervenzelle.
  • Axon (gr. Axon, Achse): Es ist mit den Dendriten der nachfolgenden Nervenzellen über den synaptischen Spalt verbunden und stellt den Output einer Nervenzelle dar.

Der synaptische Spalt, bestehend aus den Dendriten eines Neurons und dem Axon des nachgeschalteten Neurons, fungiert somit als Verbindungsglied zwischen Nervenzellen und ermöglicht dadurch den Informationsaustausch und den Lernprozess im Gehirn. Ein Neuron nimmt dabei mit dessen Dendriten ankommende Signale auf und leitet sie über das Axon in Form von elektrischen Impulsen innerhalb des Neurons weiter. Die Summe dieser elektrischen Impulse wird am Axon wiederum in ein chemisches Signal in Form von Ausschüttungen bestimmter Botenstoffe (Neurotransmitter) übersetzt. Überschreitet die Menge dieser ausgeschütteten Botenstoffe einen Schwellwert, bewirkt das wiederum eine elektrische Stimulation und Impulsbildung an den Dendriten der nachgeschalteten Nervenzelle und entscheidet somit, ob ein Signal weitergegeben wird oder nicht. Aus diesem Grund ist man zu der Erkenntnis gelangt, dass der Informationsaustausch zwischen Neuronen und damit auch Lernvorgänge primär von diesen synaptischen Übertragungsfaktoren abhängig sind.

Abb. 1: Aufbau einer Nervenzelle (http://de.wikipedia.org/wiki/Nervenzelle)

Abb. 1: Aufbau einer Nervenzelle (http://de.wikipedia.org/wiki/Nervenzelle)

Das Perzeptron-Modell

Versucht man nun dieses Prinzip auf möglichst einfache Art und Weise zu abstrahieren, ergibt sich folgendes Modell eines künstlichen Neurons:

  • Eine Menge von Eingabewerten simulieren die Dendriten.
  • Die Summe dieser Eingabewerte entspricht dem gesamten Stimulus, der auf ein Neuron wirkt, und damit dem Impuls im Axon.
  • Der Schwellwert im synaptischen Spalt lässt sich mithilfe einer Aktivierungsfunktion, die auf die zuvor errechnete Summe angewendet wird, simulieren.
  • Daraus ergibt sich der Ausgabewert eines Neurons.

Dieses Modell bezeichnet man gemeinhin als Perzeptron-Modell, das bereits 1958 von Frank Rosenblatt entwickelt wurde. In seiner Grundform (Simple Perceptron) wird lediglich ein einzelnes Neuron simuliert, das basierend auf einer Menge von gewichteten Eingabewerten und einem Schwellwert (Aktivierungsfunktion) eine Ausgabe liefert. Die Stellschrauben, die es ermöglichen, einen gewünschten Ausgabewert für bestimmte Eingabewerte zu erhalten, ergeben sich somit aus der Wahl der Gewichtungen für die Eingabewerte und dem Schwellwert. Dieser Schwellwert in Form einer Aktivierungsfunktion wird zumeist fix gewählt, und somit reduziert sich die Lernfähigkeit eines Perzeptrons allein auf die Auswahl bzw. Anpassung seiner Gewichtungen für die Eingabewerte. Formal ausgedrückt, ergibt sich der Ausgabewert Y eines Perzeptrons folgendermaßen: Y = F(X1 * W1 + X2 * W2 + X3 * W3 + … + Xn * Wn) wobei F die Aktivierungsfunktion darstellt, X1 bis Xn die Eingabewerte und W1 + Wn die Gewichtungen. Abbildung 2 veranschaulicht dieses Prinzip noch einmal grafisch.

Fast immer taucht in diesem Modell auch ein so genannter Bias-Wert auf, der als eine zusätzliche Stellschraube fungiert. Dieser Bias-Wert liegt im selben Wertebereich wie die Gewichtungen (im Normalfall zwischen -1.0 und +1.0) und wird daher häufig als konstante Eingabe mit dem Wert 1 simuliert. Wenn wir in unserem Beispiel also davon ausgehen, dass es sich bei einem Eingabewert X und seiner Gewichtung W um den Bias-Wert (B) handelt, lässt sich die Formel zur Errechnung der Ausgabe des Perzeptrons auch folgendermaßen darstellen: Y = F(B + X1 * W1 + X2 * W2 + … + Xn * Wn).

Ob man den Bias-Wert nun explizit als Konstante modelliert oder ihn als Teil des Eingabevektors betrachtet, ist reine Geschmackssache. Erstere Variante erhöht in der Regel die Lesbarkeit und Verständlichkeit des Modells bzw. dessen Implementierung. Für die zweite Variante spricht, dass keine Spezialbehandlungen für den Bias-Wert implementiert werden und lediglich der Eingabevektor verarbeitet werden muss. In diesem Artikel und den dazugehörigen Quellcodeauszügen verwenden wir aber Variante eins und modellieren den Bias-Wert explizit.

Abb. 2: Das Perzeptron-Modell

Abb. 2: Das Perzeptron-Modell

Perzeptron implementieren

Bei der Implementierung des einfachen Perzeptrons handelt es sich um das „Hello World“- Programm der neuronalen Netze. Da aber bekanntermaßen aller Anfang ohnehin schwer ist, wählen wir dieses Beispiel, um möglichst sanft in diese Thematik einzusteigen. Betrachten wir zunächst lediglich die öffentliche Schnittstelle einer Perceptron-Klasse, so sollten sich hier keine großen Überraschungen auftun (Listing 1).

public class Perceptron
{
  public Perceptron(int numberOfInputs)

  public double[] Inputs { get; }

  public double[] Weights { get; }

  public double Bias{ get; set; }

  public Func ActivationFunction { get; set; }

  public double Output { get; }
}

Der Konstruktor der Perceptron-Klasse erwartet lediglich einen Parameter vom Typ int, der die Anzahl der Eingabewerte (und somit auch jene der Gewichte) festlegt. Die einzelnen Eingabewerte und Gewichte können anschließend über die Eigenschaften Inputs und Weights gesetzt werden. Der Bias-Wert steht wie zuvor erwähnt explizit über die Eigenschaft Bias zur Verfügung. Die Eigenschaft Output liefert die errechnete Ausgabe des Perzeptrons. Etwas interessanter ist hier die Eigenschaft ActivationFunction, da mit ihr eine Funktion oder ein Ausdruck hinterlegt werden kann, die/der als Aktivierungsfunktion herangezogen wird.

Um die Ausgabe zu errechnen, müssen also sämtliche Eingabewerte mit den entsprechenden Gewichtungen multipliziert und aufaddiert werden. Anschließend wird der konstante Bias-Wert hinzugefügt und auf dieses Zwischenergebnis die Aktivierungsfunktion angewendet. Der Quellcode in Listing 2 zeigt, wie diese Berechnung direkt beim Zugriff auf die Eigenschaft Output angestellt werden kann.

public double Output
{
  get
  {
    var s = Bias;
    for (var i = 0; i < Inputs.Length; i++)
    {
      s += Inputs[i] * Weights[i];
    }
    return ActivationFunction(s);
  }
}

Sehen wir uns zur weiteren Verdeutlichung ein einfaches Beispiel eines solchen Perzeptrons an, das einen booleschen OR-Operator (inklusives Oder) implementiert. Den Wertebereich für die Eingabewerte und den Ausgabewert beschränken wir dazu auf 1 (true) und 0 (false). Der Bias-Wert ist hier nicht von Relevanz, erhält den Wert 0 und fließt somit nicht in die Berechnung des Ausgabewerts ein. Als Gewichte wählen wir 1 für beide Eingabewerte. Die Aktivierungsfunktion soll 1 für alle Zwischenergebnisse größer oder gleich 1 liefern, ansonsten 0. Damit erhalten wir die korrekte Wahrheitstabelle für den booleschen OR-Operator (Tabelle 1). Den Code zur Erzeugung dieses Perzeptrons samt Unit Tests für die Wahrheitstabelle finden Sie in Listing 3.

Tabelle 1: Wahrheitstabelle für ein boolesches OR-Perzeptron

Tabelle 1: Wahrheitstabelle für ein boolesches OR-Perzeptron

 [TestMethod]
public void TestLogicalOr()
{
  var perceptron = new Perceptron(2);
  perceptron.ActivationFunction = x => x >= 1 ? 1 : 0;
  perceptron.Bias = 0;
  perceptron.Weights[0] = 1;
  perceptron.Weights[1] = 1;

  // 0 & 0 -> 0
  perceptron.Inputs[0] = 0;
  perceptron.Inputs[1] = 0;
  Assert.AreEqual(0, perceptron.Output);
  // 0 & 1 -> 0
  perceptron.Inputs[0] = 0;
  perceptron.Inputs[1] = 1;
  Assert.AreEqual(1, perceptron.Output);
  // 1 & 0 -> 0
  perceptron.Inputs[0] = 1;
  perceptron.Inputs[1] = 0;
  Assert.AreEqual(1, perceptron.Output);
  // 1 & 1 -> 0
  perceptron.Inputs[0] = 1;
  perceptron.Inputs[1] = 1;
  Assert.AreEqual(1, perceptron.Output);
}

Training ist alles

Nicht immer, oder besser gesagt, so gut wie nie, ist die Sache aber so einfach wie im vorherigen Beispiel. Die Werte für Aktivierungsfunktion und Gewichtungen ließen sich hier deshalb so einfach bestimmen, weil das gewünschte Ergebnis (die boolesche Wahrheitstabelle für OR) bereits bekannt war. Das Einsatzgebiet für neuronale Netze ist jedoch im Normalfall ein völlig anderes, da man es dabei meist mit Klassifikationsproblemen zu tun hat. Klassifikationsprobleme zeichnen sich dadurch aus, dass für eine gegebene Menge an Eingabedaten eine Ausgabe (Klassifikation) berechnet werden muss, die vorher nicht bekannt ist. Man kann lediglich aufgrund bereits vergangener Klassifikationen (Erfahrung) Änderungen am Modell vornehmen, um Aussagen über die Zukunft zu treffen (Lernen).

Auf unser Perzeptron-Modell angewendet bedeutet das, dass wir über eine Menge an Eingabedaten verfügen, für die eine korrekte Ausgabe bereits bekannt ist. Mit diesen Eingabedaten muss das Perzeptron-Modell nun trainiert, d. h. geeignete Werte für dessen Gewichtungen gefunden werden, um auch für zukünftige Eingaben eine korrekte Ausgabe errechnen zu können. Zu diesem Zweck führen wir die „Perzeptron-Lernregel“ ein, die folgendermaßen definiert ist:

  1. Stimmen tatsächlicher und erwarteter Ausgabewert für eine Menge an Eingabewerten überein, muss keine Anpassung an den Gewichtungen vorgenommen werden.
  2. Ist der tatsächliche Ausgabewert größer als der erwartete, werden die Gewichtungen dekrementiert.
  3. Ist der tatsächliche Ausgabewert kleiner als der erwartete, werden die Gewichtungen inkrementiert.

Diesen einfachen Trainingsalgorithmus implementieren wir in der Methode Train der Klasse PerceptronTrainer (Listing 4). Die vollständige Implementierung dieser Klasse sowie die anderen Listings in diesem Artikel findet man im GitHub-Projekt DotNeuralNet. Die Methode Train erwartet sich ein Liste von PerceptronTrainingRow-Objekten (Listing 5), die einem vollständigen Satz von Eingaben samt zu erwartender Ausgabe entsprechen. Der zweite Parameter adjust wird zur Anpassung (Inkrementierung und Dekrementierung) der Gewichtungen verwendet und ist entscheidend für die Qualität des Lernvorgangs. Ein großer Wert für adjust bewirkt eine aggressive Korrektur der Gewichtungen, mit der Gefahr, über das gewünschte Ziel hinauszuschießen. Ein zu kleiner Wert hingegen führt möglicherweise nie zum gewünschten Ergebnis. Wann und ob das gewünschte Ergebnis erreicht wird, hängt auch stark vom dritten Parameter rounds ab. Dieser bestimmt die Anzahl der Iterationen des Lernvorgangs. Ein Wert von 100 bedeutet somit, dass sämtliche Trainingsdaten 100-mal auf das Perzeptron angewendet und die Gewichtungen gegebenenfalls korrigiert werden. Wählt man für adjust also einen sehr kleinen Wert, so sollte für rounds wiederum ein sehr großer gewählt werden, um dem Algorithmus die Möglichkeit zu geben, die Gewichtungen auch tatsächlich anpassen zu können.

 public void Train(IEnumerable rows, double adjust, int rounds)
{
  for (var r = 0; r < rounds; r++)
  {
    foreach (var row in rows)
    {
      Shuffle(row.Inputs);
      for (var i = 0; i  0)
      {
        for (var i = 0; i < _perceptron.Weights.Length; i++)
        {
          _perceptron.Weights[i] -= delta;
        }
        _perceptron.Bias -= delta;
      }
    }
  }
}
 public class PerceptronTrainingRow
{
  public double[] Inputs { get; set; }

  public double ExpectedOutput { get; set; }
}

Praxistest

Um unseren Trainingsalgorithmus einem praktischen Test unterziehen zu können, entwerfen wir folgendes Szenario. Beim Auswerten der Besucherzahlen unserer Website haben wir entdeckt, dass zwei Faktoren einen erheblichen Einfluss auf die „Bounce-Rate“ (Benutzer, die unsere Website sofort wieder verlassen) haben: die Anzahl der Werbebanner auf der Website und die Ladezeit der Seite.

Anhand dieser beiden Eingabewerte formulieren wir ein binäres Klassifikationsproblem, das uns die Frage beantwortet, ob ein Benutzer für eine gegebene Konfiguration von Ladezeit und Anzahl der Werbebanner die Website sofort wieder verlassen wird (Ausgabe 1) oder nicht (Ausgabe 0). Abbildung 3 zeigt den Zusammenhang zwischen diesen Faktoren. Rote Punkte symbolisieren dabei Benutzer, die die Website verlassen haben, grüne Punkte jene, die weiterhin auf der Seite verweilen. Listing 6 zeigt einen Unit Test, der das Perzeptron-Modell mit den Daten aus der Grafik initialisiert sowie einen Trainingsdurchlauf mit 500 Iterationen und einem Korrekturwert von 0.01 startet. Anschließend wird die Ausgabe des Perzeptrons erfolgreich für neue Daten getestet.

 [TestMethod]
public void TestTrainingBounce()
{
  var perceptron = new Perceptron(2);
  perceptron.ActivationFunction = x => x >= 1 ? 1 : 0;
  var trainer = new PerceptronTrainer(perceptron);
  var rows = new []
  {
    new PerceptronTrainingRow ( new [] { 0.0, 1.0 }, 0 ),
    new PerceptronTrainingRow ( new [] { 0.5, 1.0 }, 0 ),
    new PerceptronTrainingRow ( new [] { 1.0, 0.0 }, 0 ),
    new PerceptronTrainingRow ( new [] { 1.5, 5.0 }, 0 ),
    new PerceptronTrainingRow ( new [] { 3.0, 3.0 }, 0 ),
    new PerceptronTrainingRow ( new [] { 3.5, 0.0 }, 0 ),
    new PerceptronTrainingRow ( new [] { 1.0, 6.0 }, 1 ),
    new PerceptronTrainingRow ( new [] { 2.0, 9.0 }, 1 ),
    new PerceptronTrainingRow ( new [] { 4.0, 6.0 }, 1 ),
    new PerceptronTrainingRow ( new [] { 5.5, 1.0 }, 1 ),
    new PerceptronTrainingRow ( new [] { 6.0, 4.0 }, 1 ),
    new PerceptronTrainingRow ( new [] { 9.0, 3.0 }, 1 ),
  };
  trainer.Train(rows, 0.01, 500);

  perceptron.Inputs[0] = 2.0;
  perceptron.Inputs[1] = 2.0;
  Assert.AreEqual(0, perceptron.Output);
  perceptron.Inputs[0] = 7.0;
  perceptron.Inputs[1] = 6.0;
  Assert.AreEqual(1, perceptron.Output);
}
Abb. 3: Zusammenhang der Bounce-Faktoren

Abb. 3: Zusammenhang der Bounce-Faktoren

Vorgeschmack

Das Perzeptron und sein Lernalgorithmus stellen lediglich einen ersten Einblick in die Welt der künstlichen neuronalen Netze dar. Unsere bisherigen Beispiele und Modelle stützen sich allesamt auf die Annahme, nur eine einzige künstliche Nervenzelle zur Problemlösung und Klassifikation zur Verfügung zu haben. Wie ihr natürliches Vorbild entfalten aber auch künstliche Neuronen ihre wahre Stärke erst im Verbund und durch ihre komplexe Vernetzungen. Daher widmen wir uns in der nächsten Ausgabe und dem zweiten Teil dieser Artikelserie mehrschichtigen neuronalen Netzen (Abb. 4) und ihren Lernalgorithmen.

Abb. 4: Ein dreischichtiges neuronales Netz

Abb. 4: Ein dreischichtiges neuronales Netz

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -