次の方法で共有



October 2017

Volume 32 Number 10

テストの実行 - C# ニューラル ネットワークを使用する時系列回帰

James McCaffrey

James McCaffrey時系列回帰問題の目標は、履歴データに基づいて予測を行うことです。たとえば、(1 年または 2 年分の) 月次売上データを基に翌月の売上を予測するのが目標です。時系列回帰は通常とても難しく、使用できるテクニックは多種多様でその数も豊富です。

今回は、ローリング ウィンドウとニューラル ネットワークを組み合わせて使用して時系列回帰分析を実行する方法を紹介します。わかりやすいように例を使って説明します。まずは、図 1 のデモ プログラムをご覧ください。デモ プログラムでは、1949 年 1 月~ 1960 年 12 月の旅客機の月間乗客数を分析します。

ローリング ウィンドウを使用する時系列回帰のデモ

図 1 ローリング ウィンドウを使用する時系列回帰のデモ

このデモ データは、インターネット上の多くのサイトで入手できる著名なベンチマーク データセットで、本稿付属のサンプル コードに含めています。次のように、このデータをそのまま示します。

"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

144 件のデータ項目があります。最初のフィールドは、年と月です。次のフィールドには、国際線旅客機の月間合計乗客数が千人単位で示されます。デモでは、ローリング ウィンドウのサイズが 4 で 140 件以上のトレーニング項目を含むトレーニング データを作成します。トレーニング データは、各乗客数を 100 で除算することで正規化します。

[  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

データからは明示的な時間値を削除しています。1 つ目のウィンドウは、最初の 4 件の乗客数 (1.12、1.18、1.32、1.29) から成り、これらの値を予測値として使用します。その後に、5 件目の乗客数 (1.21) が続きます。これが予測対象値です。次のウィンドウは 2 件目から 5 件目の乗客数 (1.18, 1.32, 1.29, 1.21) から成ります。これらの値は次のセットの予測値です。この後に、予測対象値の 6 件目の乗客数 (1.35) が続きます。つまり、4 つの連続した乗客数から成る各セットを、次の乗客数を予測するのに使用します。

デモでは、4 個の入力ノード、12 個の隠し処理ノード、1 個の出力ノードで構成されるニューラル ネットワークを作成します。入力ノードの数は、ローリング ウィンドウの予測値の数に対応しています。ウィンドウのサイズは試行錯誤によって決める必要があり、このテクニック最大の欠点です。ニューラル ネットワークの隠しノードの数も試行錯誤によって決定する必要がありますが、これはどのニューラル ネットワークでも同じです。時系列回帰では、次に予測する時間単位が 1 個なので、出力ノードは 1 つです。

ニューラル ネットワークには、ノード間に (4 * 12) + (12 * 1) = 60 個の重みと、(12 + 1) = 13 個のバイアスがあり、これが事実上ニューラル ネットワーク モデルを定義します。デモ プログラムでは、学習率を 0.01 に、イテレーションの固定数を 10,000 に設定した基本的な確率論的バックプロパゲーション (誤差逆伝搬) アルゴリズムとローリング ウィンドウ データを一緒に使用してネットワークをトレーニングします。

トレーニングの間、デモ プログラムでは 2,000 回のイテレーションごとに予測出力値と正しい出力値との 2 乗誤差を表示します。トレーニングの誤差を解釈するのは難しく、たいていの場合は、非常に奇妙なことが起こっていないか (よく起こります) を確認するために監視します。今回の場合は、約 4,000 回のイテレーション後に、誤差が安定するようです。

トレーニング後、デモ コードには 73 個の重み値とバイアス値をサニティ チェックとして表示します。時系列回帰問題では通常、独自の正確性メトリックを使用しなければなりません。ここでは、正規化していない予測乗客数が実際値のプラスマイナス 30 の間に収まっている場合に、正確な予測とします。この定義に基づくと、デモ プログラムでは 140 件の予測乗客数のうち 128 件が正確で、12 件が不正確なため、正確性は 91.43% になります。

デモは、トレーニング後のニューラル ネットワークを使用し、トレーニング データの範囲を超える最初の月 1961 年 1 月の乗客数を予測して終了します。これは、外挿と呼ばれます。予測乗客数は 433 です。この値は、1961 年 2 月以降の乗客数を予測するための予測変数として使用できます。

今回は、中級または高度なプログラミング スキルおよびニューラル ネットワークの基本知識があることを前提としますが、時系列回帰の知識は問いません。デモ プログラムは C# を使用してコーディングしていますが、コードを JavaScript や Python などの別の言語にリファクタリングしても大きな問題は起きません。デモ プログラムは長すぎてここにすべて掲載することはできませんが、完全なソース コードは本稿付属のコード ダウンロードから入手できます。

時系列回帰

時系列回帰問題は通常、図 2 に示すように、折れ線グラフを使用して表示します。青い線は、1949 年 1 月~ 1960 年 12 月の正規化していない実際の 144 件の乗客数 (千人単位) を示します。赤い線は、ニューラル ネットワークを使用する時系列モデルによって生成された予測乗客数を示します。このモデルでは 4 つの予測値を含むローリング ウィンドウを使用しているため、最初の予測乗客数は、month (月) が 5 になるまで求められません。また、トレーニング データの範囲を超える 9 か月の予測を行っています。その結果を赤い点線で示しています。

時系列回帰の折れ線グラフ

図 2 時系列回帰の折れ線グラフ

時系列回帰分析は、トレーニングの範囲を超える時間の予測に加え、異常なデータ ポイントを特定するために使用することもできます。デモ プログラムの乗客数データでは、異常なデータ ポイントは発生していません。ご覧のとおり予測数が実際数と非常に近い値になっています。month t = 67 の実際の乗客数は 302 (図 2 の中心にある青い点) で、予測乗客数は 272 です。しかし、month t = 67 の実際の乗客数を 400 と仮定します。すると、month 67 の実際の数が外れ値であることが明確かつ視覚的に示されます。

また、プログラム用いて、時系列回帰分析で異常データを見つけることもできます。たとえば、予測データ値と実データ値の標準偏差が 4 倍であるなど、実データ値と予測値が固定しきい値以上に外れた時間値にフラグを設定できます。

デモ プログラム

デモ プログラムをコーディングするには、Visual Studio を起動して、新しい C# コンソール アプリケーションを作成し、「Neural-TimeSeries」という名前を付けます。今回のデモでは Visual Studio 2015 を使用していますが、このデモ プログラムは、Microsoft .NET Framework との大きな依存関係がないので、比較的新しいバージョンの Visual Studio であれば適切に動作します。

テンプレート コードがエディター ウィンドウに読み込まれたら、ソリューション エクスプローラー ウィンドウで Program.cs ファイルを右クリックし、ファイルの名前を「NeuralTimeSeriesProgram.cs」に変更します。Program クラスの名前が Visual Studio によって自動的に変更されます。テンプレートによって生成されたコードの上部で、不要な using ステートメントを、最上位レベルの System 名前空間を参照するステートメントを除いてすべて削除します。

スペースを節約するために少し編集したプログラムの全体構造を図 3 に示します。

図 3 NeuralTimeSeries プログラムの構造

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

デモでは、ゼロから実装した、1 つの隠し層を持つ簡単なニューラル ネットワークを使用します。または、Microsoft Cognitive Toolkit (CNTK) などのニューラル ネットワーク ライブラリを利用して、本稿で紹介したテクニックを使用することもできます。

デモでは、まず図 4 に示すように、トレーニング データを設定します。

図 4 トレーニング データの設定

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;
}

ローリング ウィンドウデータのウィンドウ サイズを 4 にハードコーディングしています。時系列のプログラムを作成する前に、短いユーティリティ プログラムを作成して、未加工のデータからローリング ウィンドウ データを生成します。デモ以外の大半のシナリオでは、テキスト ファイルからデータを読み込み、プログラムでローリング ウィンドウ データを生成します。このウィンドウ サイズはパラメーター化しているため、さまざまなサイズで実験できます。

Normalize メソッドは、すべてのデータ値を定数 100 で除算するだけです。このようにしたのは、純粋に実用的な理由からです。最初に正規化していないデータで予測を試みましたが、結果は極めて不正確でした。しかし、正規化を行うと、結果が大きく改善されました。ニューラル ネットワークを使用する場合、理論的にはデータを正規化する必要はありません。しかし実際には、正規化を行うことで大きな差が出ることはよくあります。

次のようにニューラル ネットワークを作成します。

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

入力ノードの数は、各ローリング ウィンドウに 4 個の予測値があるため、4 に設定します。出力ノードの数は、1 に設定します。これは、各セットのウィンドウの値を次月の予測を行うために使用するためです。隠しノードの数は、12 に設定します。この数は、試行錯誤によって決めました。

以下のステートメントを使用してニューラル ネットワークのトレーニングと評価を行います。

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

Train メソッドでは、基本的なバックプロパゲーションを使用します。トレーニング速度を上げるためにモーメンタムまたはアダプティブな学習率を使用したり、モデルのオーバーフィットを減らすために L1 正規化、L2 正規化、ドロップアウトを使用するものなど、さまざまなバリエーションがあります。ヘルパー メソッド ShowVector は、小数点以下 2 桁に設定した実際値を 1 行あたり 10 個含むベクトルを表示します。

ニューラル ネットワークを使用する時系列モデルを作成したら、予測の正確性を評価します。

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

時系列回帰では、予測値が正確か不正確かの判断は、調査中の問題によって異なります。旅客機の乗客データでは、正規化していない予測数と実際のデータの誤差がプラスマイナス 30 以内に収まる場合に、メソッド Accuracy によって、予測乗客数が正確だと判断されます。デモ データの場合、最初の 5 つの予測、つまり t = 5 から t = 9 までは正確ですが、t = 10 の予測は正確ではありません。

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

デモ プログラムは最後に、最後の 4 件の乗客数 (t = 141 ~ 144) を使用して、トレーニング データの範囲を超える最初の月 (t = 145 = 1961 年 1 月) の乗客数を予測します。

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");

時系列モデルは正規化した (100 で除算した) データを使用してトレーニングしているため、予測値もまた正規化されています。そのため、デモで表示される予測値は 100 倍して考えます。

時系列回帰分析に使用するニューラル ネットワーク

ニューラル ネットワークを定義する際、隠し層ノードと出力層ノードで使用する活性化関数を指定する必要があります。簡単に言うと、隠し層ノードの活性化にハイパーボリック タンジェント (tanh) 関数を使用し、出力ノードの活性化に ID 関数を使用するのがお勧めです。

ニューラル ネットワーク ライブラリ、または Microsoft CNTK や Azure Machine Learning などのシステムを使用する際は、明示的に活性化関数を指定する必要があります。デモ プログラムではこの活性化関数をハードコーティングします。主なコードは、ComputeOutputs メソッドにあります。隠しノード値は、以下のように計算します。

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

HyperTan 関数は、極端な値を避けるために、プログラムで定義しています。

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);
}

隠しノードの活性化に使用する tanh の合理的かつ一般的な代替となるのは、密接に関連するロジスティック シグモイド関数です。以下に例を示します。

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));
}

ID 関数は f(x) = x であるため、出力ノードの活性化にこの関数を使用しているのは、明示的な活性化関数を使用していないことを示すためだけです。ComputeOutputs メソッドのデモ コードを以下に示します。

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);

出力ノードの積の合計は、明示的な活性化関数を使用することなく、直接出力ノードにコピーします。NeuralNetwork クラスの oNodes メンバーは、単一の変数ではなく、1 つのセルを持つ配列であることに注意してください。

どの活性化関数を選択するかは、Train メソッドで実装する逆伝播アルゴリズムのコードに影響を与えます。Train メソッドは、各活性化関数の微積分の導関数を使用します。y = tanh(x) の導関数は、(1 + y) * (1 - y) です。デモ コードは、以下のようになります。

// 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;
}

ロジスティック シグモイド活性化関数を使用する場合、y = logsig(x) の導関数は、y * (1 - y) です。出力ノードの活性化では、y = x の微積分の導関数は定数 1 です。Train メソッドの関連コードを以下に示します。

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

もちろん、1 を掛けても効果はありません。ドキュメントの形式で機能するようにコーディングしています。

まとめ

時系列回帰分析の実行に使用できるテクニックは数多くあり、種類も豊富です。このトピックに関する Wikipedia では、パラメトリックとノンパラメトリック、同期と非同期などのように多くの方法で分類される多数のテクニックが一覧されています。私見ですが、ローリング ウィンドウ データと共にニューラル ネットワーク アプローチを使用する主なメリットは、生成されるモデルが、ニューラル モデル以外のモデルよりも正確である場合が多いように思います (常にそうとは限りません)。ニューラル ネットワーク アプローチの主なデメリットは、良い結果を得るために学習率を試行錯誤しなければならないことです。

たいていの時系列回帰分析テクニックでは、ローリング ウィンドウ データやそれに似た手法を使用します。ウィンドウに分割しないでデータをそのまま使用する高度なテクニックもあります。特に、比較的新しいものとしては、ロングショート ターム メモリ ニューラル ネットワークと呼ばれるネットワークが用いられます。この手法では、非常に正確な予測モデルが生成されることが多くなります。


Dr.James McCaffrey は、ワシントン州レドモンドの Microsoft Research に勤務しています。これまでに、Internet Explorer、Bing などの複数のマイクロソフト製品にも携わってきました。Dr.McCaffrey の連絡先は jammc@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれたマイクロソフト技術スタッフの John Krumm、Chris Lee、および Adith Swaminathan に心より感謝いたします。


この記事について MSDN マガジン フォーラムで議論する