Freigeben über



Oktober 2017

Band 32, Nummer 10

Test Run: Zeitreihenregression mit einem neuronalen C#-Netzwerk

Von James McCaffrey

James McCaffreyDas Ziel bei einem Problem der Zeitreihenregression besteht darin, basierend auf historischen Zeitdaten Vorhersagen zu treffen. Wenn Sie z. B. über monatliche Umsatzzahlen (für ein oder zwei Jahre) verfügen, möchten Sie vielleicht den Umsatz für den kommenden Monat vorhersagen. Zeitreihenregression ist für gewöhnlich sehr schwierig, und es können viele verschiedene Techniken verwendet werden.

In diesem Artikel zeige ich, wie eine Analyse der Zeitreihenregression mithilfe von Schiebefensterdaten in Kombination mit einem neuronalen Netzwerk ausgeführt werden kann. Dies lässt sich am besten an einem Beispiel erklären. Sehen Sie sich das Demoprogramm in Abbildung 1 an. Das Demoprogramm analysiert die Anzahl der Flugpassagiere für jeden Monat zwischen Januar 1949 und Dezember 1960.

Demoprogramm Zeitreihenregression mit Schiebefensterdaten

Abbildung 1: Demoprogramm Zeitreihenregression mit Schiebefensterdaten

Die Demodaten stammen aus einem bekannten Benchmarkdataset, das im Internet verfügbar sowie im Download enthalten ist, der diesen Artikel begleitet. Für die Rohdaten gilt Folgendes:

"1949-01";112
"1949-02";118
"1949-03";132
"1949-04";129
"1949-05";121
"1949-06";135
"1949-07";148
"1949-08";148
...
"1960-11";390
"1960-12";432

Es sind 144 Rohdatenelemente vorhanden. Das erste Feld gibt das Jahr und den Monat an. Das zweite Feld ist die Gesamtzahl der internationalen Flugpassagiere für den Monat (in Tausend). Das Demoprogramm erstellt Trainingsdaten mithilfe eines Schiebefensters der Größe 4, damit sich 140 Trainingselemente ergeben. Die Trainingsdaten werden durch Dividieren jeder Passagierzahl durch 100 normalisiert:

[  0]   1.12   1.18   1.32   1.29   1.21
[  1]   1.18   1.32   1.29   1.21   1.35
[  2]   1.32   1.29   1.21   1.35   1.48
[  3]   1.29   1.21   1.35   1.48   1.48
...
[139]   6.06   5.08   4.61   3.90   4.32

Beachten Sie, dass die expliziten Zeitwerte aus den Daten entfernt werden. Das erste Fenster besteht aus den ersten vier Passagierzahlen (1,12, 1,18, 1,32, 1,29), die als Vorhersagewerte verwendet werden, gefolgt von der fünften Angabe (1,21), die ein vorherzusagender Wert ist. Das nächste Fenster besteht aus den zweiten bis fünften Werten (1,18, 1,32, 1,29, 1,21), die die nächste Vorhersagewertemenge darstellen, gefolgt vom sechsten Wert (1,35), der vorherzusagen ist. Kurz gesagt, wird jede Menge von vier aufeinanderfolgenden Passagierzahlen zum Vorhersagen des nächsten Werts verwendet.

Das Demoprogramm erstellt ein neuronales Netzwerk mit vier Eingabeknoten, 12 verborgenen Verarbeitungsknoten und einem einzelnen Ausgabeknoten. Die Anzahl der Eingabeknoten entspricht der Anzahl der Prädiktoren im Schiebefenster. Die Fenstergröße muss durch Versuch und Irrtum ermittelt werden. Dies ist der größte Nachteil dieses Verfahrens. Die Anzahl der verborgenen Knoten des neuronalen Netzwerks muss ebenfalls durch Versuch und Irrtum ermittelt werden. Dies gilt immer für neuronale Netzwerke. Es ist nur ein Ausgabeknoten vorhanden, weil die Zeitreihenregression die Vorhersage eine Zeiteinheit im voraus trifft.

Das neuronale Netzwerk besitzt (4 * 12) + (12 * 1) = 60 Knoten-zu-Knoten-Gewichtungen und (12 + 1) = 13 Bias, die das neuronale Netzwerkmodell in erster Linie definieren. Mithilfe der Schiebefensterdaten trainiert das Demoprogramm das Netzwerk und verwendet dabei den grundlegenden stochastischen Rückpropagierungsalgorithmus. Dabei ist die Lernrate auf 0,01 und eine feste Anzahl von Iterationen auf 10.000 festgelegt.

Während des Trainings zeigt das Demoprogramm alle 2.000 Iterationen den mittleren quadratischen Fehler zwischen den vorhergesagten Ausgabewerten und den richtigen Ausgabewerten an. Der Trainingsfehler ist schwierig zu interpretieren. Er wird hauptsächlich überwacht, um zu ermitteln, ob seltsame Dinge geschehen (dies passiert recht häufig). In diesem Fall scheint sich der Fehler nach ungefähr 4.000 Iterationen zu stabilisieren.

Nach dem Training zeigt der Democode die 73 Gewichtungen und Biaswerte an. Auch dies ist wieder in erster Linie eine Plausibilitätsprüfung. Für Probleme der Zeitreihenregression müssen Sie normalerweise eine benutzerdefinierte Genauigkeitsmetrik verwenden. Hier bedeutet eine richtige Vorhersage, dass der nicht normalisierte vorhergesagte Passagierwert plus oder minus 30 vom tatsächlichen Wert abweicht. Mit dieser Definition erzielte das Demoprogramm eine Genauigkeit von 91,43 Prozent. 128 Werte waren richtig und 12 falsch für die 140 vorhergesagten Passagierzahlen.

Das Demoprogramm wird abgeschlossen, indem das trainierte neuronale Netzwerk zum Vorhersagen der Passagierzahl für Januar 1961 verwendet wird. Dies ist der erste Zeitraum, der über den Bereich der Trainingsdaten hinausgeht. Dieser Vorgang wird als Extrapolation bezeichnet. Die Vorhersage lautet 433 Passagiere. Dieser Wert kann als Prädiktorvariable zum Vorhersagen der Werte für Februar 1961 usw. verwendet werden.

Dieser Artikel setzt voraus, dass Sie über mittlere oder umfangreiche Programmierkenntnisse verfügen und Grundkenntnisse bezüglich neuronaler Netzwerke besitzen. Es wird aber nicht davon ausgegangen, dass Sie mit Zeitreihenregression vertraut sind. Das Demoprogramm wurde mit C# programmiert, aber Sie sollten keine Probleme haben, für den Code ein Refactoring in eine andere Sprache wie Java oder Python vorzunehmen. Das Demoprogramm ist zu lang, um es im Ganzen darzustellen, aber der vollständige Quellcode ist im Dateidownload enthalten, der zu diesem Artikel gehört.

Zeitreihenregression

Probleme der Zeitreihenregression werden häufig mithilfe eines Liniendiagramms wie in Abbildung 2 dargestellt. Die blaue Linie gibt die 144 tatsächlichen, nicht normalisierten Passagierzahlen (in Tausend) von Januar 1949 bis Dezember 1960 an. Die hellrote Linie gibt die vorhergesagten Passagierwerte an, die vom Zeitreihenmodell des neuronalen Netzwerks generiert werden. Beachten Sie Folgendes: Da das Modell ein Schiebefenster mit vier Prädiktorwerten verwendet, erfolgt die erste vorhergesagte Passagierzahl erst im Monat = 5. Außerdem habe ich Vorhersagen getroffen, die neun Monate über den Bereich der Trainingsdaten hinausreichen. Diese werden durch die gestrichelte rote Linie angegeben.

Zeitreihenregressions-Liniendiagramm

Abbildung 2: Zeitreihenregressions-Liniendiagramm

Analysen der Zeitreihenregression können nicht nur für Vorhersagen verwendet werden, die über den Bereich der Trainingsdaten hinausgehen, sondern auch zum Identifizieren anomaler Datenpunkte. Dies geschieht mit den Passagierzahlendaten des Demoprogramms nicht. Sie können erkennen, dass die vorhergesagten Werte sehr dicht bei den tatsächlichen Werten liegen. Die tatsächliche Passagierzahl für den Monat t = 67 beträgt z. B. 302 (der blaue Punkt fast in der Mitte von Abbildung 2), der vorhergesagte Wert ist 272. Angenommen, der tatsächliche Wert für den Monat t = 67 wäre 400. In diesem Fall gäbe es einen auffälligen visuellen Hinweis darauf, dass der tatsächliche Wert für den Monat 67 ein Ausreißerwert ist.

Sie können auch einen programmgesteuerten Ansatz zum Ermitteln anomaler Daten mit Zeitreihenregression verwenden. Sie könnten z. B. jeden Zeitwert markieren, bei dem der tatsächliche Datenwert und der vorhergesagte Wert eine Differenz aufweisen, die einen festen Schwellenwert übersteigt, beispielsweise das Vierfache der Standardabweichung der vorhergesagten Werte im Vergleich zu den tatsächlichen Datenwerten.

Das Demoprogramm

Zum Codieren des Demoprogramms habe ich Visual Studio gestartet und eine neue C#-Konsolenanwendung erstellt, die ich „Neural-TimeSeries“ genannt habe. Ich habe Visual Studio 2015 verwendet. Die Demo weist aber keine nennenswerten .NET Framework-Abhängigkeiten auf, sodass jede halbwegs aktuelle Version funktionieren sollte.

Nach dem Laden des Vorlagencodes in das Editorfenster habe ich im Fenster des Projektmappen-Explorers mit der rechten Maustaste auf die Datei „Program.cs“ geklickt, die Datei in „NeuralTimeSeriesProgram.cs“ umbenannt und Visual Studio dann die automatische Umbenennung der Klasse „Program“ gestattet. Am Anfang des von der Vorlage generierten Codes habe ich alle überflüssigen using-Anweisungen außer der Anweisung gelöscht, die auf den Namespace „System“ der obersten Ebene verweist.

Die Gesamtprogrammstruktur wird mit einigen kleinen platzsparenden Änderungen in Abbildung 3 dargestellt.

Abbildung 3: NeuralTimeSeries-Programmstruktur

using System;
namespace NeuralTimeSeries
{
  class NeuralTimeSeriesProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin times series demo");
      Console.WriteLine("Predict airline passengers ");
      Console.WriteLine("January 1949 to December 1960 ");

      double[][] trainData = GetAirlineData();
      trainData = Normalize(trainData);
      Console.WriteLine("Normalized training data:");
      ShowMatrix(trainData, 5, 2, true);  // first 5 rows

      int numInput = 4; // Number predictors
      int numHidden = 12;
      int numOutput = 1; // Regression

      Console.WriteLine("Creating a " + numInput + "-" + numHidden +
        "-" + numOutput + " neural network");
      NeuralNetwork nn = new NeuralNetwork(numInput, numHidden,
        numOutput);

      int maxEpochs = 10000;
      double learnRate = 0.01;
      double[] weights = nn.Train(trainData, maxEpochs, learnRate);
      
      Console.WriteLine("Model weights and biases: ");
      ShowVector(weights, 2, 10, true);

      double trainAcc = nn.Accuracy(trainData, 0.30); 
      Console.WriteLine("\nModel accuracy (+/- 30) on training " +
        "data = " + trainAcc.ToString("F4"));

      double[] future = new double[] { 5.08, 4.61, 3.90, 4.32 };
      double[] predicted = nn.ComputeOutputs(future); 
      Console.WriteLine("January 1961 (t=145): ");
      Console.WriteLine((predicted[0] * 100).ToString("F0"));

      Console.WriteLine("End time series demo ");
      Console.ReadLine();
    } // Main

    static double[][] Normalize(double[][] data) { . . }

    static double[][] GetAirlineData() {. . }

    static void ShowMatrix(double[][] matrix, int numRows,
      int decimals, bool indices) { . . }

    static void ShowVector(double[] vector, int decimals,
      int lineLen, bool newLine) { . . }

  public class NeuralNetwork { . . }
} // ns

Das Demoprogramm verwendet ein einfaches neuronales Netzwerk mit einer verborgenen Schicht, das ich ganz neu implementiert habe. Alternativ können Sie auch die in diesem Artikel vorgestellten Vorgehensweisen zusammen mit einer neuronalen Netzwerkbibliothek wie z. B. Microsoft Cognitive Toolkit (CNTK) verwenden.

Das Demoprogramm beginnt mit dem Einrichten der Trainingsdaten. Abbildung 4 zeigt dies.

Abbildung 4: Einrichten der Trainingsdaten

double[][] trainData = GetAirlineData();
trainData = Normalize(trainData);
Console.WriteLine("Normalized training data:");
ShowMatrix(trainData, 5, 2, true);

Method GetAirlineData is defined as:

static double[][] GetAirlineData()
{
  double[][] airData = new double[140][];
  airData[0] = new double[] { 112, 118, 132, 129, 121 };
  airData[1] = new double[] { 118, 132, 129, 121, 135 };
...
  airData[139] = new double[] { 606, 508, 461, 390, 432 };
  return airData;
}

Hier sind die Schiebefensterdaten mit einer Fenstergröße von 4 hartcodiert. Bevor ich das Zeitreihenprogramm geschrieben habe, habe ich ein kurzes Hilfsprogramm zum Generieren der Schiebefensterdaten aus den Rohdaten erstellt. In den meisten Szenarien, die keine Demoszenarien sind, würden Sie Rohdaten aus einer Textdatei lesen und dann programmgesteuert Schiebefensterdaten generieren. Dabei würde die Fenstergröße parametrisiert, damit Sie mit verschiedenen Größen experimentieren können.

Die Normalize-Methode dividiert einfach alle Datenwerte durch die Konstante 100. Dies geschieht aus rein praktischen Gründen. Meine ersten Versuche mit nicht normalisierten Daten erbrachten sehr schlechte Ergebnisse, aber nach der Normalisierung wurden die Ergebnisse erheblich besser. Beim Arbeiten mit neuronalen Netzwerken müssen Ihre Daten in der Theorie nicht normalisiert sein, in der Praxis bedeutet die Normalisierung jedoch häufig einen großen Unterschied.

Das neuronale Netzwerk wird folgendermaßen erstellt:

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

Die Anzahl der Eingabeknoten wird auf vier festgelegt, weil jedes Schiebefenster vier Prädiktorwerte aufweist. Die Anzahl der Ausgabeknoten wurde auf einen Knoten festgelegt, weil jede Fensterwertemenge zum Treffen einer Vorhersage für den nächsten Monat verwendet wird. Die Anzahl der verborgenen Knoten wird auf 12 festgelegt. Sie wurde durch Versuch und Irrtum ermittelt.

Das neuronale Netzwerk wird mithilfe dieser Anweisungen trainiert und ausgewertet:

int maxEpochs = 10000;
double learnRate = 0.01;
double[] weights = nn.Train(trainData, maxEpochs, learnRate);
ShowVector(weights, 2, 10, true);

Die Train-Methode verwendet grundlegende Rückpropagierung. Für diese sind zahlreiche Variationen verfügbar, z. B. die Verwendung eines Trägheitsterms oder adaptiver Lernraten zum Steigern der Trainingsgeschwindigkeit sowie der Einsatz von L1- oder L2-Regularisierung oder -Dropout zum Verhindern der Überanpassung des Modells. Die Hilfsmethode „ShowVector“ zeigt einen Vektor mit echten Werten an, formatiert mit zwei Dezimalstellen. Pro Zeile werden 10 Werte verwendet.

Nachdem das Zeitreihenmodell des neuronalen Netzwerks erstellt wurde, wird seine Vorhersagegenauigkeit ausgewertet:

double trainAcc = nn.Accuracy(trainData, 0.30);
Console.WriteLine("\nModel accuracy (+/- 30) on " + 
  " training data = " + trainAcc.ToString("F4"));

Für die Zeitreihenregression hängt die Entscheidung, ob ein vorhergesagter Wert richtig oder falsch ist, von dem Problem ab, das untersucht wird. Für die Flugpassagierdaten markiert die Methode „Accuracy“ eine vorhergesagte Passagierzahl als richtig, wenn die nicht normalisierte vorhergesagte Anzahl plus oder minus 30 der tatsächlichen Rohanzahl beträgt. Für die Demodaten sind die ersten fünf Vorhersagen für t = 5 bis t = 9 richtig, die Vorhersage für t = 10 ist aber falsch:

t  actual  predicted
= = = = = = = = = = =
 5   121     129
 6   135     128
 7   148     137
 8   148     153
 9   136     140
10   119     141

Das Demoprogramm verwendet abschließend die letzten vier Passagierzahlen (t = 141 bis 144), um die Passagierzahl für den ersten Zeitraum vorherzusagen, der den Bereich der Trainingsdaten übersteigt (t = 145 = Januar 1961):

double[] predictors = new double[] { 5.08, 4.61, 3.90, 4.32 };
double[] forecast = nn.ComputeOutputs(predictors); 
Console.WriteLine("Predicted for January 1961 (t=145): ");
Console.WriteLine((forecast[0] * 100).ToString("F0"));
Console.WriteLine("End time series demo");

Beachten Sie Folgendes: Weil das Zeitreihenmodell mithilfe normalisierter Daten (dividiert durch 100) trainiert wurde, sind die Vorhersagen ebenfalls normalisiert. Das Demoprogramm zeigt daher die vorhergesagten Werte multipliziert mit 100 an.

Neuronale Netzwerke für Zeitreihenanalysen

Wenn Sie ein neuronales Netzwerk definieren, müssen Sie die Aktivierungsfunktionen angeben, die von den Knoten der verborgenen Schicht und den Knoten der Ausgabeschicht verwendet werden. Kurz gesagt, empfehle ich die Verwendung der tanh-Funktion (hyperbolische Tangente) für die Aktivierung der verborgenen Knoten und der Identitätsfunktion für die Aktivierung der Ausgabeknoten.

Wenn eine neuronale Netzwerkbibliothek oder ein System wie Microsoft CNTK oder Azure Machine Learning verwendet wird, müssen Sie die Aktivierungsfunktionen explizit angeben. Im Demoprogramm sind diese Aktivierungsfunktionen hartcodiert. Der Schlüsselcode ist in der Methode „ComputeOutputs“ enthalten. Die Werte der verborgenen Knoten werden folgendermaßen berechnet:

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

for (int i = 0; i < numHidden; ++i)  // Add biases
  hSums[i] += this.hBiases[i];

for (int i = 0; i < numHidden; ++i)   // Apply activation
  this.hNodes[i] = HyperTan(hSums[i]); // Hardcoded

Hier ist die Funktion „HyperTan“ durch das Programm definiert, um extreme Werte zu vermeiden:

private static double HyperTan(double x) {
  if (x < -20.0) return -1.0; // Correct to 30 decimals
  else if (x > 20.0) return 1.0;
  else return Math.Tanh(x);
}

Eine vertretbare (und häufig genutzte) Alternative zur Verwendung von tanh für die Aktivierung verborgener Knoten ist die Verwendung der eng verwandten logistischen Sigmoidfunktion. Beispiel:

private static double LogSig(double x) {
  if (x < -20.0) return 0.0; // Close approximation
  else if (x > 20.0) return 1.0;
  else return 1.0 / (1.0 + Math.Exp(x));
}

Da die Identitätsfunktion nur f(x) = x lautet, stellt deren Verwendung für die Aktivierung von Ausgabeknoten nur ein extravagantes Verfahren dar, um anzugeben, dass keine explizite Aktivierung verwendet werden soll. Der Democode in der Methode „ComputeOutputs“ sieht folgendermaßen aus:

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

for (int i = 0; i < numOutput; ++i)  // Add biases
  oSums[i] += oBiases[i];

Array.Copy(oSums, this.oNodes, oSums.Length);

Die Summe der Produkte für einen Ausgabeknoten wird direkt in den Ausgabeknoten kopiert, ohne dass eine explizite Aktivierung angewendet wird. Beachten Sie, dass die oNodes-Member der NeuralNetwork-Klasse ein Array mit einer Zelle und keine einzelne Variable sind.

Die Auswahl der Aktivierungsfunktionen wirkt sich auf den Code im Rückpropagierungsalgorithmus aus, der in der Methode „Train“ implementiert wird. Die Methode „Train“ verwendet die Kalkülderivative jeder Aktivierungsfunktion. Das Derivativ von y = tanh(x) ist (1 + y) * (1 - y). Im Democode:

// Hidden node signals
for (int j = 0; j < numHidden; ++j) {
  derivative = (1 + hNodes[j]) * (1 - hNodes[j]); // tanh
  double sum = 0.0; 
  for (int k = 0; k < numOutput; ++k)
    sum += oSignals[k] * hoWeights[j][k]; 
  hSignals[j] = derivative * sum;
}

Wenn Sie logistische Sigmoidaktivierung verwenden, ist y * (1 - y) das Derivativ von y = logsig(x). Für die Ausgabeaktivierung ist das Kalkülderivativ von y = x einfach die Konstante 1. Dies ist der relevante Code in der Methode „Train“:

for (int k = 0; k < numOutput; ++k) {
  errorSignal = tValues[k] - oNodes[k];
  derivative = 1.0;  // For Identity activation
  oSignals[k] = errorSignal * derivative;
}

Offensichtlich besitzt die Multiplikation mit 1 keine Auswirkungen. Ich habe die vorliegende Codierung als eine Form der Dokumentation vorgenommen.

Zusammenfassung

Es können viele verschiedene Techniken zur Ausführung von Analysen der Zeitreihenregression verwendet werden. Im Wikipedia-Artikel zu diesem Thema werden Dutzende von Techniken aufgeführt, die auf verschiedene Weise klassifiziert sind, z. B. als parametrisch im Gegensatz zu nicht parametrisch und linear im Gegensatz zu nicht linear. Meiner Meinung nach besteht der Hauptvorteil der Verwendung eines Ansatzes mit einem neuronalen Netzwerk mit Schiebefensterdaten darin, dass das sich ergebende Modell häufig (aber nicht immer) genauer als nicht neuronale Modelle ist. Der Hauptnachteil des Ansatzes mit einem neuronalen Netzwerk ist das Experimentieren mit der Lernrate, um gute Ergebnisse zu erzielen.

Die meisten Analysetechniken für Zeitreihenregression verwenden Schiebefensterdaten oder ein ähnliches Schema. Es sind jedoch auch erweiterte Techniken verfügbar, die Rohdaten ohne Schiebefenster verwenden können. Insbesondere ein relativ neuer Ansatz sei erwähnt, bei dem ein neuronales LSTM-Netzwerk (Long Short-Term Memory) verwendet wird. Mit diesem Ansatz werden häufig sehr genaue Vorhersagemodelle erstellt.


Dr. James McCaffreyist 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 jamccaff@microsoft.com.

Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: John Krumm, Chris Lee und Adith Swaminathan


Diesen Artikel im MSDN Magazine-Forum diskutieren