Neuronale Netze II

Ein neuronales Netz besteht aus autonomen Zellen (Neuronen), die Zahlen empfangen und nach der Multiplikation mit Faktoren (Wichtungen) addieren. Ist die Summe größer als ein Schwellenwert, wird eine neue Zahl erzeugt und an nachfolgenden Neuronen übermittelt. Die Neuronen sind dabei in Ebenen angeordnet, die Zahlen laufen von einer Ebenen zur nächste Ebene. Die letzte Ebene liefert das Ergebnis.

Man wird im letzten Kapitel feststellen, dass der Lernprozess aufgrund des Zufallsanteils bei den Wichtungsänderungen nicht sehr zielgerichtet abläuft und dadurch mehr Lernschritte als erforderlich benötigt. Allerdings dürfte dies Verfahren den Lernprozessen in der Natur noch am nächsten kommen.
Ein anderes Verfahren ist, die Wichtungsänderung direkt über die Abweichungen von vorgegebenen und berechneten Ausgangswerten durchzuführen. Verfahren dieser Art sind die Delta-Regel und der Backpropagation-Algorithmus.

Die Idee

Für jedes Ausgangsneuron kann die Abweichung oder der Fehler von vorgegebenem und berechnetem Ausgangswert bestimmt werden unter Berücksichtigung des Vorzeichens. Die Eingangswichtungen des Neurons werden um diesen Wert, multipliziert mit einer Lernrate, geändert. War ein Eingangswert 0, so trägt er unabhängig von der Wichtung nicht zum Fehler bei. In diesem Fall ist eine Änderung der Wichtung nicht sinnvoll. Daher wird der Fehler nicht nur mit der Lernrate, sondern auch mit dem entsprechenden Eingangswert multipliziert.

Eine Ebene tiefer kann man den Fehler durchkoppeln oder geeignet anpassen: Da die Eingangswerte mit Wichtungen multipliziert werden, muss bei einer kleinen Wichtung der Fehler des Neurons, das den Eingangwert liefert, entsprechend groß sein, um einen Effekt auf den Fehler des betrachteten Ausgangsneurons zu haben. Ist hingegen die Wichtung groß, so kann bereits ein kleiner Fehler des Vorgängerneurons eine große Wirkung auf das Ausgangsneuron haben. Damit erscheint es sinnvoll, nicht für alle Neuronen den gleichen Fehler zu verwerten, sondern ihn von Ebene zu Ebene anzupassen, beginnend mit der Ausgangsebene.

Das bedeutet, dass der Lernprozess nicht von der Eingangsebene bis zur Ausgangsebene abgewickelt wird, sondern in umgekehrter Richtung. Außerdem sollte jedes Neuron "wissen", mit welchen Neuronen seine Eingänge verbunden sind. Technisch ist es günstiger, die Neuronen in einer Liste abzulegen und zu jedem Neuron eine Liste anzufertigen, die seinen Eingängen entspricht und die Neuronen enthält, die die Eingangswerte liefern.
Um Rekursionen nicht unnötig kompliziert zu machen, ist es günstig, die vorgegebenen Eingangswerte in "Dummy-Neuronen" zu kopieren, die lediglich die Vorgaben ausgeben, aber keine Eingänge besitzen und auch keine Berechnungen ausführen. Die Unterscheidung "Dummy-Neuron" und "normales Neuron" geschieht über das Flag inf (true = Dummy-Neuron).

Dazu werden die Klassen neuron und einneur definiert, letztere zur Behandlung der einzelnen Neuroneneingänge. Der Lernprozess gliedert sich auf in entsprechende Methoden für diese Klassen.

Das Programm

Klasse einneur ohne Kontruktor:
neuron ne=null;
double we=0,fk=1;
eineur nxt=null;
 
public double wert() {
  we=ne.wert();
  return we*fk;
}

void lernen(double d) {
  ne.lernen(d*fk);
  fk=fk-100*d*we;
}

Zeiger auf Vorgängerneuron, das einen Wert an den Dendriten liefert
Wert vom Vorgängerneuron, Wichtungsfaktor
Zeiger awf Nachfolgedendriten
 
 
Wert vom Vorgängerneuron
Rückgabe des gewichteten Wertes
 
 
Lernschritt bei Fehler d (Differenz berechneter und vorgegebener Ausgangswert)
Lernaufruf für das Vorgängerneuron mit gewichtetem Fehler d*fk
Anpassung der Wichtung um Lernrate 100 * Fehler * Wert

Klasse neuron ohne Konstruktor und Anlegen der Eingangsliste vom Typ einneur:
eineur anfli=null;
neuron nxt=null;
double dfa=100,gr=0;
double we=0;
boolean inf=false;
 
double wert() {
  if(inf) return we;
  else {
    double su=gr;
    for(eineur zwli=anfli;zwli!=null;zwli=zwli.nxt)
      su=su+zwli.wert();
    su=Math.max(su,-1000);
    return 1/(Math.exp(-su)+1);
  }
}

void lernen(double d) {
  for(eineur zw=anfli;zw!=null;zw=zw.nxt) zw.lernen(d);
  gr=gr+100*d;
}

Anfang der Liste der Eingangsneuronen / Dendritenliste
Zeiger auf Nachfolger in der Neuronenliste
Fehlerwert, Schaltschwelle
Ausgangswert
Flag Dummy-Neuron, normales Neuron
 
 
wenn Dummy-Neuron, dann Rückgabe des gespeicherten Wertes we
sonst (normales Neuron):
Eingangssumme mit Schaltschwelle initialisiert
Durchlaufen der Dendritenliste, dabei
  Summieren der Eingangswerte
Berechung und Rückgabe des Ausgangswertes
 
 
 
 
 
Anpassung der Eingangswichtungen
Anpassung der Schaltschwelle mit Lernrate 100

Der Lernprozess des gesamten Netzes erfolgt in gleicher Weise wie im letzten Kapitel:
In einer Schleife werden Lernschritte durchgeführt, bis die Gesamtabweichung der berechneten und vorgegebenen Ausgänge 0,02 unterschreitet oder 1000 Lernschritte durchgeführt worden sind. Dabei wird zunächst für jedes Eingangsmuster das Ausgangsmuster durch das Netzes berechnet und dieses Muster mit der Vorgabe verglichen. Danach erfolgt der Aufruf der lernen-Methode zunächst für die Ausgangsneuronen und rekursiv für die restlichen Neuronen.

Das vollständige Programm kann unter neudel.java geladen und für eigene Experimente genutzt werden: Nach dem Lernprozess kann man weitere Muster eingeben und testen, welche Ausgangsmuster dann erzeugt werden.

Hinweis: Damit die durch den Fehler d verursachten Änderungen nicht zu groß werden und dadurch die Stabilisierung der Wichtungen verhindern, kann man den Fehler d durch den Term d/(a + Math.abs(d)) ersetzen, wobei a eine positive Zahl ist, z.B. 1 .