次の方法で共有



March 2016

Volume 31 Number 3

テストの実行 - ニューラル ネットワーク回帰

James McCaffrey

James McCaffrey回帰問題は、1 つ以上の予測変数 (独立変数) に基づいて、数値変数 (従属変数) の値を予測するのが目標です。予測変数は、数値またはカテゴリのいずれかになります。たとえば、年齢、性別 (男性または女性)、学歴に基づいて年収を予測するといった考え方です。

回帰の最も単純な形式を、線形回帰 (LR) と呼びます。LR 予測の方程式は、「年収 = 17.53 + (5.11 * 年齢) + (-2.02 * 男性) + (-1.32 * 女性) + (6.09 * 学歴)」というようになります。LR が有効な問題もありますが、多くの状況では効果的とはいえません。ですが、一般的な種類の回帰は他にも、多項式回帰、一般線形モデル回帰、ニューラル ネットワーク回帰 (NNR) などがあります。中でも、おそらく、ニューラル ネットワーク回帰が最も強力な回帰の形式です。

最も一般的な種類のニューラル ネットワーク (NN) は、カテゴリ変数を予測するものです。たとえば、年齢、収入、性別などの要素を基に、政治的傾向 (保守派、穏健派、リベラル派) を予測するような考え方です。NN 分類子は n 個の出力ノードを持ちます。この場合の n は、従属変数が取り得る値の数です。n 個の出力ノードの値の合計は 1.0 になり、大きく解釈すれば確率です。したがって、政治的傾向を予測する場合、NN 分類子は 3 個の出力ノードを持つことになります。出力ノードの値が (0.24, 0.61, 0.15) の場合、中央のノードの確率が最大なので、NN 分類子は「穏健派」と予測します。

NN 回帰の場合、NN は 従属数値変数の予測値を保持する出力ノードを 1 つ持ちます。したがって、年収を予測するの場合、3 つの入力ノード、年齢、性別 (male = -1、female = +1)、学歴から、1 つの出力ノード、年収を求めます。

NN 回帰や今回の主旨を理解するには、図 1 のデモ プログラムを見るのが一番です。NN 回帰の考え方ができるだけ明瞭になるように、このデモでは、現実にある問題に取り組むのではなく、サイン関数の値を予測する NN モデルの作成を目標にします。三角関数の知識がやや不安だという方は、図 2 に示すサイン関数のグラフをご覧ください。サイン関数は、負の無限大から正の無限大までの実数入力値を 1 つ受け取り、-1.0 ~ +1.0 の値を返します。サイン関数は、x = 0.0、x = pi (~3.14)、x = 2 * pi、x= 3 * pi などの場合は 0 を返します。サイン関数は、モデル化が実に難しい関数です。

ニューラル ネットワーク回帰のデモ
図 1 ニューラル ネットワーク回帰のデモ

Sin(x) 関数
図 2 Sin(x) 関数

デモでは、まず、プログラムを使用して、NN モデルのトレーニングに使用する 80 個のデータ項目を生成します。80 個のトレーニング項目には、0 ~ 6.4 (2 * pi より少し大きい値) のランダム入力値 x と、対応する y 値 (sin(x) の値) を含めます。

デモでは、1-12-1 NN を作成します。つまり、NN には、(x に対する) 入力ノードが 1 つ、(事実上予測方程式を定義する) 隠し処理ノードが 12、(x の予測サイン値になる) 出力ノードが 1 つあります。NN に取り組む場合、必ずテストが必要です。隠しノードの数は試行錯誤で決定します。

NN 分類子には、隠しノードと出力ノード用に 2 つの活性化関数があります。分類子出力ノードの活性化関数はほぼ常にソフトマックス関数になります。それは、合計が 1.0 になる値をソフトマックスが生成するためです。分類子隠しノードの活性化関数は、通常、ロジスティック シグモイド関数またはハイパーボリック タンジェント関数 (略称は tanh) のいずれかになります。ただし、NN 回帰の場合、隠しノードの活性化関数はありますが、出力ノードの活性化関数はありません。デモ NN では、隠しノードの活性化に tanh 関数を使用します。

NN の出力は、入力値 (重み) と一連の定数 (バイアス) によって決まります。バイアスも特殊な重みにすぎないため、どちらも「重み」と表現される場合もあります。i 個の入力ノード、j 個の隠しノード、および k 個の出力ノードを持つニューラル ネットワークには、合計 (i * j) + j + (j * k) + k 個の重みとバイアスがあります。したがって、1-12-1 デモ NN には、(1 * 12) + 12 + (12 * 1) + 1 = 37 個の重みとバイアスがあります。

重みとバイアスの値を決めるプロセスをモデルのトレーニングと呼びます。さまざまな重みとバイアスの値を試して、NN で計算した出力値が、トレーニング データの正しい出力値にほぼ一致するものを特定します。

NN のトレーニングには、複数の異なるアルゴリズムを使用できます。群を抜いてよく使われるアプローチが、逆伝播アルゴリズムです。逆伝播は、重みとバイアスの値をゆっくりと変えていく反復プロセスで、NN では通常、正確性の高い出力値が計算されます。

逆伝播は、2 つの必須パラメーター (最大反復回数と学習率) と、1 つのオプション パラメーター (運動率) を使用します。maxEpochs パラメーターは、アルゴリズムの反復回数に制限を設けます。learnRate パラメーターは、重みとバイアスの値の毎回の反復時の変化を制御します。momentum パラメーターは、トレーニングの速度を上げ、逆伝播アルゴリズムが不適切な解にこだわらないようにします。今回のデモでは、maxEpochs の値を 10,000、learnRate の値を 0.005、momentum の値を 0.001 に設定しています。これらの値は、試行錯誤の結果決めたものです。

NN トレーニングで逆伝播アルゴリズムを使用する場合、使用できるバリエーションが 3 つあります。バッチ逆伝播では、最初にすべてのトレーニング項目をテストしてから、重みとバイアスの値をすべて調整します。確率論的逆伝播 (オンライン逆伝播) では、毎回トレーニング項目をテストした後に、重みとバイアスの値をすべて調整します。ミニバッチ逆伝播では、トレーニング項目の指定部分をテストした後に、重みとバイアスの値をすべて調整します。今回のデモ プログラムでは、最もよく使われるバリエーションとして確率論的逆伝播を使用します。

デモ プログラムでは、1,000 トレーニング エポックごとに、測定誤差を表示します。誤差の値がやや大きく変化しているのがわかります。トレーニングの完了後、デモは、NN モデルを定義する 37 個の重みとバイアスの値を表示します。NN の重みとバイアスの値には明確な解釈はありませんが、値をテストして、1 つの重みが非常に大きく、他すべてが 0 に近いといった、不適切な結果をチェックしておくことは重要です。

デモ プログラムは、最後に NN モデルを評価して終了します。x = pi、pi / 2、および 3 * pi / 2 に対する sin(x) の NN 予測値はすべて、正しい値から 0.02 の範囲内に収まっています。sin(6 * pi) の予測値は、正しい値とは大きく異なっています。ただし、NN は、0 から 2 * pi までの x 値の sin(x) 値を予測することだけを目的としてトレーニングしているため、これは想定どおりの結果です。

今回は、中級以上のプログラミング スキルがあることを前提にしていますが、ニューラル ネットワーク回帰についての知識は問いません。デモ プログラムは C# を使用してコーディングしていますが、コードを Visual Basic や Perl などの別の言語にリファクタリングしても大きな問題は起きません。デモ プログラムは長すぎてここにすべて掲載することはできませんが、完全なソース コードは本稿付属のコード ダウンロードから入手できます。コードのサイズを小さくして重要なアイデアを明確にするために、デモ コードでは通常のエラー チェックをすべて省略しています。

デモ プログラムの構造

デモ プログラムを作成するには、Visual Studio を起動し、[ファイル] メニューの [新規作成] をポイントし、[プロジェクト] をクリックして、C# コンソール アプリケーション テンプレートを選択します。今回は Visual Studio 2015 を使用しましたが、このデモは .NET との大きな依存関係がないため、どのバージョンの Visual Studio でも機能します。プロジェクトには「NeuralRegression」という名前を付けます。

テンプレート コードがエディター ウィンドウに読み込まれたら、ソリューション エクスプローラー ウィンドウで Program.cs ファイルを選択し、右クリックして、名前を「NeuralRegressionProgram.cs」というわかりやすい名前に変更します。これにより Program クラスの名前が Visual Studio によって自動的に変更されます。エディターのコードの先頭で、使用していない名前空間への参照をすべて削除し、最上位レベルの System 名前空間への参照のみを残します。

スペースを節約するために少し編集したデモ プログラムの全体構造を図 3 に示します。制御ステートメントはすべて Main メソッドに含めています。ニューラル ネットワーク回帰の機能はすべて NeuralNetwork というプログラム定義のクラスに含めています。

図 3 ニューラル ネットワーク回帰プログラムの構造

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

Main メソッドでは、次のステートメントを使用してトレーニング データを作成します。

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

ニューラル ネットワークを処理する場合の一般的なルールとして、トレーニング データの数が多いほど適切な結果が得られます。0 ~ 2 * pi の x 値のサイン関数をモデル化する場合、適切な結果を得るには少なくとも 80 個のトレーニング項目が必要です。乱数オブジェクトのシード値 1 は、任意に選択した値です。トレーニング データは、配列の配列形式の行列に格納します。実際のシナリオでは、テキスト ファイルからトレーニング データを読み取ってもかまいません。

以下のステートメントを使用してニューラル ネットワークを作成します。

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

今回ターゲットにするサイン関数は値を 1 つしか受け取らないので、入力ノードは 1 つだけです。多くのニューラル ネットワーク回帰の問題では、予測独立変数ごとに 1 つずつ、複数の入力ノードを使用します。また多くは、出力ノードを 1 つだけ使用しますが、2 つ以上の数値を予測することも可能です。

NN では、重み値を初期化し、トレーニング項目の処理順序をランダムにするための、ランダム オブジェクトが必要です。デモ NeuralNetwork コンストラクターは、内部ランダム オブジェクト用にシード値を受け取ります。使用している値 0 は任意です。

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

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

NN は、トレーニング パラメーターの値によって大きく左右されます。ほんの少しの変化で、結果が大きく異なります。

デモ プログラムでは、3 つの標準値で sin(x) を予測することによって、NN モデルの結果の品質を評価しています。少し編集したステートメントを以下に示します。

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

今回の例には出力値が 1 つしかないのに、デモ NN では、出力を出力ノードの配列に格納しています。配列を返しておけば、ソース コードを変更することなく複数の値を予測できると考えました。

デモは、トレーニング データの範囲を大きくはずれた x 値の sin(x) を予測して終了します。

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

多くの NN 分類子のシナリオでは、分類の精度を計算するメソッドを呼び出します。つまり、正しい予測数を合計予測数で除算します。カテゴリ出力値は正解か不正解のいずれかになるので、分類の精度を計算することは可能です。ただし、NN 回帰に取り組む場合、精度を定義する標準の方法はありません。何らかの尺度で精度を計算する場合、その尺度は問題によって異なります。たとえば、sin(x) を予測する場合には、正しい値から 0.01 の範囲内に収まる値を適切な予測として定義することができます。

出力値の計算

分類用に設計した NN と、回帰用に設計した NN の主な違いの多くは、出力を計算してモデルをトレーニングするメソッドにあります。NeuralNetwork クラスの ComputeOutputs メソッドの定義は、以下のコードで始まります。

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

このメソッドは、予測独立変数の値を保持する配列を受け取ります。hSums と oSums というローカル変数は、隠しノードと出力ノードの (活性化前の) 暫定値を保持するスクラッチ配列です。次に、独立変数値をニューラル ネットワークの入力ノードにコピーします。

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

続いて、各入力値に対応する入力ノードと隠しノードの重みを乗算し、結果を累計することで、隠しノードの暫定値を計算します。

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

これに隠しノードのバイアス値を加算します。

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

隠しノードの値は、隠しノードの活性化関数を各暫定合計値に適用することによって決定します。

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

次に、各隠しノードの値を対応する隠しノードと出力ノードの重みに乗算し、結果を累計することで、出力ノードの暫定値を計算します。

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

これに出力ノードのバイアス値を加算します。

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

ここまでの回帰ネットワークの出力ノード値の計算は、分類子ネットワークの出力ノード値の計算とまったく同じです。ただし、分類子では、ソフトマックス活性化関数を各累積合計に適用することによって、最終的な出力ノード値を計算します。回帰ネットワークでは、活性化関数を適用しません。そのため、ComputeOutputs メソッドは、単に oSums スクラッチ配列の値を出力ノードに直接コピーして終了します。

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

便宜上、出力ノードの値もローカルの戻り値の配列にコピーし、GetOutputs メソッドの類を呼び出さなくても簡単にアクセスできるようにしています。

逆伝播アルゴリズムを使用して NN 分類子をトレーニングする場合は、2 つの活性化関数の微積分の導関数を使用します。隠しノードの場合は、コードが以下のようになります。

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

derivative というローカル変数の値は、tanh 関数の微積分の導関数で、非常に複雑な理論から得られます。NN 分類子では、この計算に出力ノードの活性化関数の導関数が関与します。

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

ここでは、ローカル変数の導関数値は、ソフトマックス関数の微積分の導関数です。ただし、NN 回帰では、出力ノードの活性化関数を使用しないため、コードは以下のようになります。

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

当然、1.0 を乗算しても何も影響がないため、derivative の項を削除してもかまいません。別の考え方をすると、NN 回帰の出力ノードの活性化関数は、恒等関数 f(x) = x です。恒等関数の微積分の導関数は定数 1.0 です。

まとめ

1 つ以上の数値予測変数を使用するニューラル ネットワーク回帰について調べる場合、今回のデモ コードと説明を確認するだけですぐに作業を開始できるようになります。カテゴリ予測変数を使用する場合は、変数をエンコードする必要があります。性別 (男性、女性) といった、2 値のいずれかを取るカテゴリ予測変数の場合は、一方の値を -1、もう一方の値を +1 としてエンコーディングします。

3 つ以上の値を受け取るカテゴリ予測変数では、1-of-(N-1) エンコーディングを使用します。たとえば、予測変数が、4 つの値 (赤、青、緑、黄) のいずれかの色になる場合、赤を (1, 0, 0)、青を (0, 1, 0)、緑を (0, 0, 1)、黄色を (-1, -1, -1) としてエンコーディングします。


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

この記事のレビューに協力してくれたマイクロソフト技術スタッフの Gaz Iqbal および Umesh Madan に心より感謝いたします。