März 2016

Band 31, Nummer 3

Testlauf – Regression mit neuralen Netzwerken

Von James McCaffrey

James McCaffreyZiel eines Regressionsproblems ist das Vorhersagen des Werts einer numerischen Variablen (auch abhängige Variable genannt) basierend auf den Werten einer oder mehrerer Prädiktorvariablen (den unabhängigen Variablen), die entweder numerisch oder kategorisch sein können. Angenommen, Sie möchten das Jahreseinkommen einer Person basierend auf Alter, Geschlecht (männlich oder weiblich) und Bildungsgrad vorhersagen.

Die einfachste Form der Regression ist die lineare Regression (LR). Eine LR-Vorhersagegleichung kann wie folgt aussehen: Einkommen = 17,53 + (5,11 * Alter) + (-2,02 x männlich) + (-1,32 * weiblich) + (6,09 * Bildungsgrad). Wenngleich die lineare Regression für verschiedene Problemstellungen nützlich ist, eignet sie sich in vielen Fällen nicht sonderlich. Doch es gibt andere gängige Arten von Regression: polynominale Regression, Regression gemäß dem allgemeinen linearen Modell und die Regression mit neuralen Netzwerken. Diese Art der Regression ist fraglos eine der leistungsfähigsten Formen von Regression.

Die gängigste Art von neuralem Netzwerk (NN) ist diejenige, die eine kategorische Variable vorhersagt. Angenommen, Sie möchten die politische Neigung einer Person (konservativ, liberal, sozialdemokratisch) basierend auf Faktoren wie Alter, Einkommen und Geschlecht vorhersagen. Eine NN-Klassifizierung hat „n“ Ausgabeknoten, wobei „n“ die Anzahl der Werte ist, die die abhängige Variable verwenden kann. Die Werte der „n“ Ausgabeknoten haben die Summe 1,0 und können lose als Wahrscheinlichkeiten interpretiert werden. Demnach hat eine NN-Klassifizierung für das Vorhersagen der politischen Neigung drei Ausgabeknoten. Wenn die Ausgabeknotenwerte (0,24, 0,61, 0,15) lauten, sagt die NN-Klassifizierung „Liberal“ voraus, da der mittlere Knoten die höchste Wahrscheinlichkeit hat.

Bei der NN-Regression hat das neuronale Netzwerk einen einzigen Ausgabeknoten, der den vorausgesagten Wert der abhängigen numerischen Variablen enthält. Beim Beispiel zum Vorhersagen des Jahreseinkommens gibt es beispielsweise drei Eingabeknoten (einen für das Alter, einen für das Geschlecht [männlich = -1 und weiblich = +1] und einen für den Bildungsgrad) und einen Ausgabeknoten (Jahreseinkommen).

Sehen Sie sich das Demoprogramm in Abbildung 1 an, um ein Gefühl dafür zu bekommen, was die NN-Regression ist und worauf ich in diesem Artikel hinaus will. Anstatt ein realistisches Problem anzugehen, ist das Ziel der Demo das Erstellen eines NN-Modells, mit dessen Hilfe der Wert der Sinusfunktion vorhergesagt werden kann, um die Konzepte der NN-Regression so verständlich wie möglich darzulegen. Für den Fall, dass Ihre Trigonometriekenntnisse ein wenig eingestaubt sind, zeigt Abbildung 2 den Graphen der Sinusfunktion. Die Sinusfunktion akzeptiert einen einzelnen realen Eingabewert von minus unendlich bis plus unendlich und gibt einen Wert im Bereich von -1,0 bis +1,0 zurück. Die Sinusfunktion gibt 0 zurück, wenn x = 0,0, x = Pi (~3,14), x = 2 * Pi, x = 3 * Pi usw. Die Sinusfunktion lässt sich überraschenderweise nur schwierig modellieren.

Demo der Regression mit neuralen Netzwerken
Abbildung 1: Demo der Regression mit neuralen Netzwerken

Die Sin(x)-Funktion
Abbildung 2: Die Sin(x)-Funktion

Die Demo beginnt, indem 80 Datenelemente programmgesteuert generiert werden, die für das Training des NN-Modells verwendet werden. Die 80 Trainingselemente haben einen nach dem Zufallsprinzip gewählten Eingabewert x von 0 bis 6,4 (ein wenig größer als 2 * Pi) und einen entsprechenden Wert y, der sin(x) ist.

Die Demo erstellt ein neuronales Netzwerk des Typs 1-12-1 mit einem Eingabeknoten (für x), 12 verdeckten Verarbeitungsknoten (die die Vorhersagegleichung tatsächlich definieren) und einem Ausgabeknoten (den voraussichtlichen Sinus von x). Beim Arbeiten mit neuronalen Netzwerken ist stets Experimentieren gefragt. Die Anzahl der verdeckten Knoten wurde durch systematisches Ausprobieren ermittelt.

NN-Klassifizierungen haben zwei Aktivierungsfunktionen, eine für die verdeckten Knoten und eine für die Ausgabeknoten. Die Aktivierungsfunktion für den Ausgabeknoten für eine Klassifizierung ist fast immer die Softmax-Funktion, da sie Werte erzeugt, deren Summe 1,0 ist. Die Aktivierungsfunktion für die verdeckten Knoten für eine Klassifizierung ist meist entweder die logistische Sigmoidfunktion oder die hyperbolische Tangensfunktion (kurz tanh). Doch bei der NN-Regression gibt es eine Aktivierungsfunktion für die verdeckten Knoten, aber keine Aktivierungsfunktion für den Ausgabeknoten. Das Demo-NN verwendet die „tanh“-Funktion zur Aktivierung verdeckter Knoten.

Die Ausgabe eines neuralen Netzwerks wird durch seine Eingabewerte und eine Gruppe von Konstanten bestimmt, die als Gewichte und Verzerrungen bezeichnet werden. Da Verzerrungen eigentlich bloß besondere Arten von Gewichten sind, wird mitunter der Begriff „Gewichte“ für beides verwendet. Ein neuronales Netzwerk mit „i“ Eingabeknoten, „j“ verdeckten Knoten und „k“ Ausgabeknoten hat insgesamt (i * j) + j + (j * k) + k Gewichte und Verzerrungen. Das Demo-NN 1-12-1 hat demnach (1 * 12) + 12 + (12 * 1) + 1 = 37 Gewichte und Verzerrungen.

Der Prozess des Bestimmens der Werte für die Gewichte und Verzerrungen wird als Trainieren des Modells bezeichnet. Die Idee ist, Gewichte und Verzerrungen mit verschiedenen Werten auszuprobieren, um zu bestimmen, wo die berechneten Ausgabewerte des neuronalen Netzwerks am ehesten mit den bekannten richtigen Ausgabewerten der Trainingsdaten übereinstimmen.

Es gibt mehrere Algorithmen, mit denen ein neuronales Netzwerk trainiert werden kann. Der vielleicht gängigste Ansatz ist das Verwenden des Rückpropagierungsalgorithmus. Rückpropagierung ist ein iterativer Prozess, bei dem sich Werte der Gewichte und Verzerrungen langsam ändern, sodass das neuronale Netzwerk zumeist genauere Ausgabewerte berechnet.

Für die Rückpropagierung werden zwei erforderliche Parameter (maximale Anzahl von Iterationen und Lernrate) und ein optionaler Parameter (Momentumrate) verwendet. Der „maxEpochs“-Parameter legt einen Grenzwert für die Anzahl der Algorithmusiterationen fest. Der „learnRate“-Parameter steuert, wie stark sich die Werte von Gewichten und Verzerrungen bei jeder Iteration ändern können. Der „momentum“-Parameter beschleunigt das Training und dient zum Verhindern, dass der Rückpropagierungsalgorithmus bei einer schlechten Lösung hängen bleibt. In der Demo ist der Wert von „maxEpochs“ auf 10.000, der Wert von „learnRate“ auf 0,005 und der Wert von „momentum“ auf 0,001 festgelegt. Diese Werte wurden durch systematisches Ausprobieren ermittelt.

Bei Verwenden des Rückpropagierungsalgorithmus für das NN-Training können drei Variationen verwendet werden. Bei der Batchrückpropagierung werden alle Trainingselemente zunächst untersucht. Anschließend werden alle Werte der Gewichte und Verzerrungen angepasst. Bei der stochastischen Rückpropagierung (auch Onlinerückpropagierung genannt) werden nach der Untersuchung der einzelnen Trainingselemente alle Werte der Gewichte und Verzerrungen angepasst. Bei der Mini-Batchrückpropagierung werden alle Werte der Gewichte und Verzerrungen angepasst, nachdem ein bestimmter Bruchteil der Trainingselemente untersucht wurde. Das Demoprogramm nutzt die gängigste Variante, die stochastische Rückpropagierung.

Das Demoprogramm zeigt alle 1.000 Trainingszyklen eine Fehlermessung an. Beachten Sie, dass die Fehlerwerte etwas sprunghaft sind. Nach Abschluss des Trainings zeigte die Demo die Werte der 37 Gewichte und Verzerrungen, die das NN-Modell definieren. Die Werte der NN-Gewichte und -Verzerrungen lassen keine offensichtliche Interpretation zu. Es ist jedoch wichtig, die Werte auf fehlerhafte Ergebnisse zu untersuchen, z. B. wenn ein Gewicht einen überaus hohen Wert hat, während alle anderen Gewichte nahe null sind.

Das Demoprogramm schließt mit einer Bewertung des NN-Modells. Die vorhergesagten NN-Werte von sin(x) für x = Pi, Pi / 2 und 3 * Pi / 2 weichen alle um bis zu 0,02 von den korrekten Werten ab. Der für sin(6 * Pi) vorhergesagte Wert ist weit vom korrekten Wert entfernt. Dieses Ergebnis wurde allerdings erwartet, da das neurale Netzwerk nur zum Vorhersagen der Werte von sin(x) für x-Werte von 0 bis 2 * Pi trainiert wurde.

Dieser Artikel geht davon aus, dass Sie über mindestens fortgeschrittene Programmierkenntnisse verfügen, ohne aber anzunehmen, dass Sie irgendetwas Regression mit neuralen Netzwerken verstehen. Das Demoprogramm wurde mit C# programmiert. Sie sollten den Code jedoch relativ leicht in eine andere Sprache, zum Beispiel Visual Basic oder Perl, umgestalten können. Das Demoprogramm ist zu lang, um es in diesem Artikel im Ganzen darzustellen. Der vollständige Quellcode ist im begleitenden Download enthalten. Aus der Demo wurden alle üblichen Fehlerprüfungen entfernt, um die wichtigsten Konzepte so klar wie möglich und die Größe des Codes gering zu halten.

Struktur des Demoprogramms

Um das Demoprogramm zu erstellen, habe ich Visual Studio gestartet und über die Menüaktion „Datei | Neu | Projekt“ die C#-Vorlage „Konsolenanwendung“ ausgewählt. Ich habe Visual Studio 2015 verwendet. Das Programm hat aber keine nennenswerten .NET-Abhängigkeiten, sodass jede Version von Visual Studio funktionieren sollte. Das Projekt habe ich „NeuralRegression“ genannt.

Nachdem der Vorlagencode im Fenster „Projektmappen-Explorer“ in das Editor-Fenster geladen wurde, habe ich mit der rechten Maustaste auf die Datei „Program.cs“ geklickt und sie in „NeuralRegressionProgram.cs“ umbenannt. Ich habe Visual Studio automatisch die „Program“-Klasse umbenennen lassen. Am Anfang des Editor-Codes habe ich alle Verweise auf nicht verwendete Namespaces gelöscht und nur den Verweis auf den obersten Namespace „System“ übrig gelassen.

Die Gesamtstruktur des Demoprogramms (mit ein paar kleinen Änderungen, um Platz zu sparen) ist in Abbildung 3 zu sehen. Alle Steuerungsanweisungen sind in der „Main“-Methode enthalten. Die gesamte Funktionalität für die NN-Regression ist in einer vom Programm definierten Klasse namens „NeuralNetwork“ enthalten.

Abbildung 3: Programmstruktur für die Regression mit neuralen Netzwerken

using System;
namespace NeuralRegression
{
  class NeuralRegressionProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin NN regression demo");
      Console.WriteLine("Goal is to predict sin(x)");
      // Create training data
      // Create neural network
      // Train neural network
      // Evaluate neural network
      Console.WriteLine("End demo");
      Console.ReadLine();
    }
    public static void ShowVector(double[] vector,
      int decimals, int lineLen, bool newLine) { . . }
    public static void ShowMatrix(double[][] matrix,
      int numRows, int decimals, bool indices) { . . }
  }
  public class NeuralNetwork
  {
    private int numInput; // Number input nodes
    private int numHidden;
    private int numOutput;
    private double[] inputs; // Input nodes
    private double[] hiddens;
    private double[] outputs;
    private double[][] ihWeights; // Input-hidden
    private double[] hBiases;
    private double[][] hoWeights; // Hidden-output
    private double[] oBiases;
    private Random rnd;
    public NeuralNetwork(int numInput, int numHidden,
      int numOutput, int seed) { . . }
    // Misc. private helper methods
    public void SetWeights(double[] weights) { . . }
    public double[] GetWeights() { . . }
    public double[] ComputeOutputs(double[] xValues) { . . }
    public double[] Train(double[][] trainData,
      int maxEpochs, double learnRate,
      double momentum) { . . }
  } // class NeuralNetwork
} // ns

In der „Main“-Methode werden die Trainingsdaten von diesen Anweisungen erstellt:

int numItems = 80;
double[][] trainData = new double[numItems][];
Random rnd = new Random(1);
for (int i = 0; i < numItems; ++i) {
  double x = 6.4 * rnd.NextDouble();
  double sx = Math.Sin(x);
  trainData[i] = new double[] { x, sx };
}

Beim Arbeiten mit neuronalen Netzwerken gilt grundsätzlich: je mehr Trainingsdaten, desto besser. Für die Modellierung der Sinusfunktion für x-Werte von 0 bis 2 * Pi habe ich mindestens 80 Elemente benötigt, um gute Ergebnisse zu erzielen. Die Wahl des Startwerts 1 für das Zufallszahlobjekt erfolgte willkürlich. Die Trainingsdaten werden in der Art eines „Arrays von Arrays“ gespeichert. In praktischen Szenarien werden Trainingsdaten wahrscheinlich aus einer Textdatei eingelesen.

Das neuronale Netzwerk wird mithilfe dieser Anweisungen erstellt:

int numInput = 1;
int numHidden = 12;
int numOutput = 1;
int rndSeed = 0;
NeuralNetwork nn = new NeuralNetwork(numInput,
  numHidden, numOutput, rndSeed);

Es gibt nur einen Eingabeknoten, da die Sinuszielfunktion nur einen einzelnen Wert akzeptiert. Bei den meisten Problemstellungen mit NN-Regression gibt es mehrere Eingabeknoten, je einen für die vom Prädiktor unabhängigen Variablen. Bei den meisten Problemstellungen mit NN-Regression gibt es nur einen einzelnen Ausgabeknoten, wobei es jedoch möglich ist, zwei oder mehr numerische Werte vorherzusagen.

Ein neuronales Netzwerk benötigt ein Zufallsobjekt zum Initialisieren der Gewichtswerte und Durcheinanderbringen der Reihenfolge, in der die Trainingselemente verarbeitet werden. Der „NeuralNetwork“-Konstruktor in der Demo akzeptiert einen Startwert für das interne Zufallsobjekt. Der verwendete Wert 0 wurde willkürlich gewählt.

Das neuronale Netzwerk wird mithilfe dieser Anweisungen trainiert:

int maxEpochs = 10000;
double learnRate = 0.005;
double momentum  = 0.001;
double[] weights = nn.Train(trainData, maxEpochs,
  learnRate, momentum);
ShowVector(weights, 4, 8, true);

Ein neuronales Netzwerk ist überaus empfindlich, was die Trainingsparameter angeht. Selbst eine sehr kleine Änderung kann zu einem drastisch anderen Ergebnis führen.

Das Demoprogramm bewertet die Qualität des resultierenden NN-Modells, indem sin(x) für drei Standardwerte vorhergesagt wird. Die Anweisungen (mit geringfügigen Änderungen) lauten:

double[] y = nn.ComputeOutputs(new double[] { Math.PI });
Console.WriteLine("Predicted =  " + y[0]);
y = nn.ComputeOutputs(new double[] { Math.PI / 2 });
Console.WriteLine("Predicted =  " + y[0]);
y = nn.ComputeOutputs(new double[] { 3 * Math.PI / 2.0 });
Console.WriteLine("Predicted = " + y[0]);

Beachten Sie, dass das Demo-NN seine Ausgaben in einem Array von Ausgabeknoten speichert, auch wenn es bei diesem Beispiel nur einen einzigen Ausgabewert gibt. Die Rückgabe eines Arrays ermöglicht das Vorhersagen mehrerer Werte, ohne den Quellcode zu ändern.

Die Demo schließt mit der Vorhersage von sin(x) für einen x-Wert, der sich weit außerhalb des Bereichs der Trainingsdaten befindet:

y = nn.ComputeOutputs(new double[] { 6 * Math.PI });
Console.WriteLine("Predicted =  " + y[0]);
Console.WriteLine("End demo");

In den meisten NN-Klassifizierungsszenarien rufen Sie eine Methode auf, die die Klassifizierungsgenauigkeit berechnet, d. h. die Anzahl richtiger Vorhersagen dividiert durch die Gesamtanzahl der Vorhersagen. Dies ist möglich, da ein kategorischer Ausgabewert entweder richtig oder falsch ist. Doch beim Arbeiten mit NN-Regression gibt es keine Standardmöglichkeit zum Definieren von Genauigkeit. Wenn Sie ein bestimmtes Genauigkeitsmaß berechnen möchten, ist dieses problemabhängig. Zum Vorhersagen von sin(x) können Sie beispielsweise willkürlich eine richtige Vorhersage als Wert mit einer Abweichung von 0,01 vom richtigen Wert definieren.

Berechnen von Ausgabewerten

Die wesentlichen Unterschiede zwischen einem neuronalen Netzwerk für die Klassifizierung und einem für die Regression machen die Methoden zum Berechnen der Ausgabe und Trainieren des Modells aus. Die Definition der „NeuralNetwork“-Klassenmethode „ComputeOutputs“ beginnt wie folgt:

public double[] ComputeOutputs(double[] xValues)
{
  double[] hSums = new double[numHidden];
  double[] oSums = new double[numOutput];
...

Die Methode akzeptiert ein Array mit Werten der vom Prädiktor unabhängigen Variablen. Die lokalen Variablen „hSums“ und „oSums“ sind Arrays mit vorläufigen Werten (vor der Aktivierung) der verdeckten und Ausgabeknoten. Als Nächstes werden die Werte der unabhängigen Variablen in die Eingabeknoten des neuronalen Netzwerks kopiert:

for (int i = 0; i < numInput; ++i)
  this.inputs[i] = xValues[i];

Anschließend werden die vorläufigen Werte der verdeckten Knoten berechnet, indem jeder Eingabewert mit seinem entsprechenden Eingabe-zu-Verdeckt-Gewicht multipliziert und kumuliert wird:

for (int j = 0; j < numHidden; ++j)
  for (int i = 0; i < numInput; ++i)
    hSums[j] += this.inputs[i] * this.ihWeights[i][j];

Danach werden die Verzerrungswerte der verdeckten Knoten hinzugefügt:

for (int j = 0; j < numHidden; ++j)
  hSums[j] += this.hBiases[j];

Die Werte der verdeckten Knoten werden bestimmt, indem die Aktivierungsfunktion für verdeckte Knoten auf jede vorläufige Summe angewendet wird:

for (int j = 0; j < numHidden; ++j)
  this.hiddens[j] = HyperTan(hSums[j]);

Anschließend werden die vorläufigen Werte der Ausgabeknoten berechnet, indem jeder Wert eines verdeckten Knotens mit seinem entsprechenden Verdeckt-zu-Ausgabe-Gewicht multipliziert und kumuliert wird:

for (int k = 0; k < numOutput; ++k)
  for (int j = 0; j < numHidden; ++j)
    oSums[k] += hiddens[j] * hoWeights[j][k];

Danach werden die Verzerrungswerte der Ausgabeknoten hinzugefügt:

for (int k = 0; k < numOutput; ++k)
  oSums[k] += oBiases[k];

Bis zu diesem Punkt entspricht das Berechnen von Ausgabeknotenwerten für ein Regressionsnetzwerk exakt dem Berechnen von Ausgabeknotenwerten für ein Klassifizierungsnetzwerk. Doch bei einer Klassifizierung werden die endgültigen Ausgabeknotenwerte berechnet, indem die Softmax-Aktivierungsfunktion auf jede kumulierte Summe angewendet wird. Bei einem Regressionsnetzwerk wird keine Aktivierungsfunktion angewendet. Deshalb schließt die „ComputeOutputs“-Methode mit dem direkten Kopieren der Werte im „oSums“-Array in die Ausgabeknoten:

...
  Array.Copy(oSums, this.outputs, outputs.Length);
  double[] retResult = new double[numOutput]; // Could define a GetOutputs
  Array.Copy(this.outputs, retResult, retResult.Length);
  return retResult;
}

Der Einfachheit halber werden die Werte in den Ausgabeknoten auch in ein lokales Rückgabearray kopiert, damit auf sie einfach zugegriffen werden kann, ohne eine „GetOutputs“-Methode o. ä. aufzurufen.

Beim Trainieren einer NN-Klassifizierung mithilfe des Rückpropagierungsalgorithmus werden die Differentialableitungen der beiden Aktivierungsfunktionen genutzt. Für die verdeckten Knoten sieht der Code so aus:

for (int j = 0; j < numHidden; ++j) {
  double sum = 0.0; // sums of output signals
  for (int k = 0; k < numOutput; ++k)
    sum += oSignals[k] * hoWeights[j][k];
  double derivative = (1 + hiddens[j]) * (1 - hiddens[j]);
  hSignals[j] = sum * derivative;
}

Der Wert der lokalen Variablen namens „derivative“ ist die Differentialableitung der „tanh“-Funktion, die auf einer recht komplexen Theorie beruht. Bei einer NN-Klassifizierung erfolgt die Berechnung unter Einbeziehung der Aktivierungsfunktion des Ausgabeknotens wie folgt:

for (int k = 0; k < numOutput; ++k) {
  double derivative = (1 - outputs[k]) * outputs[k];
  oSignals[k] = (tValues[k] - outputs[k]) * derivative;
}

Hier ist Wert der lokalen Variablen namens „derivative“ die Differentialableitung der Softmax-Funktion. Da die NN-Regression jedoch keine Aktivierungsfunktion für Ausgabeknoten verwendet, ist der Code wie folgt:

for (int k = 0; k < numOutput; ++k) {
  double derivative = 1.0;
  oSignals[k] = (tValues[k] - outputs[k]) * derivative;
}

Weil die Multiplikation mit 1,0 keine Auswirkung hat, können Sie die Ableitungsbedingung einfach löschen. Eine andere Möglichkeit, sich dies vorzustellen, ist, dass bei der NN-Regression die Aktivierungsfunktion für Ausgabeknoten die „Identity“-Funktion f(x) = x ist. Die Differentialableitung der „Identity“-Funktion ist die Konstante 1,0.

Zusammenfassung

Der Democode und die Erläuterung in diesem Artikel sollten ausreichen, um Ihnen den Einstieg in die Untersuchung der Regression mit neuronalen Netzwerken mit einer oder mehreren Prädiktorvariablen zu erleichtern. Wenn Sie eine Prädiktorvariable haben, die kategorisch ist, müssen Sie die Variable codieren. Bei einer kategorischen Prädiktorvariablen, die einen von zwei möglichen Werten verwenden kann, wie z. B. Geschlecht (männlich, weiblich), codieren Sie einen Wert mit -1 und den anderen mit +1.

Bei einer kategorischen Prädiktorvariablen, die drei oder mehr mögliche Werte verwenden kann, wählen Sie eine Codierung vom Typ 1-von-(N-1). Wenn „Farbe“ z. B. eine Prädiktorvariable ist, für die einer von vier Werten (rot, blau, grün, gelb) möglich ist, wird rot mit (1, 0, 0), blau mit (0, 1, 0), grün mit (0, 0, 1) und gelb mit (-1, -1, -1) codiert.


Dr. James McCaffrey* ist in Redmond (Washington) für Microsoft Research tätig. Er hat an verschiedenen Microsoft-Produkten mitgearbeitet, unter anderem an Internet Explorer und Bing. Dr. McCaffrey erreichen Sie unter jammc@microsoft.com.*

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Gaz Iqbal und Umesh Madan