テストの実行

C# を使用した勾配降下トレーニング

James McCaffrey

コード サンプルのダウンロード

James McCaffrey個人的で非公式の定義ですが、機械学習 (ML) とはデータを使用して予測を行うシステムです。ML について研究を始めると、すぐに「勾配降下」というややミステリアスな言葉に出会います。今月は、この勾配降下法の概要を説明し、これをロジスティック回帰分類システムのトレーニングに使用する方法のデモを行います。

今回の考え方を把握するために、まずはデモ プログラムをご覧ください (図 1 参照)。デモでは、まず、10,000 個の合成データ項目を生成します。実際のデータではなく架空のデータを使用することで、データの特性をコントロールすることができるため、多くの場合、ML の調査に有効です。各データ項目には、8 個の予測変数 (ML 用語ではフィーチャー) 値と 1 つの従属変数 (0 または 1) が続きます。データは、追加の定数 (-5.02) と 8 個のランダムな重みの値 (-7.78, -0.65, ... -7.97) を使用して生成します。

勾配降下法を使用したロジスティック回帰分類のトレーニング
図 1 勾配降下法を使用したロジスティック回帰分類のトレーニング

合成データは、年齢、年収、信用度などの 8 個のフィーチャーに基づいて性別 (男性 = 0、女性 = 1) を予測する問題に対応すると考えてください。8 個のフィーチャー値は、既にスケール変換が行われ、-10.0 ~ +10.0 の範囲に収まっているものとします。

10,000 個のデータ項目を生成後、デモではこのデータをランダムに分割して、分類のトレーニングに使用する 8,000 個の項目セットと、出力されたモデルの予測精度の推定に使用する 2,000 個の項目セットに分けます。次に、ロジスティック回帰バイナリ分類を作成し、maxEpochs 変数 (1,000) と学習率 (0.01) を設定して、勾配降下トレーニングを準備します。勾配降下法は反復プロセスなので、maxEpochs 変数を使って反復回数を制限します。後ほど学習率パラメーターについて説明しますが、それまでは、学習率を、ロジスティック回帰分類モデルのトレーニングの各反復で発生する変化量を制御する値と考えてください。

デモでは分類をトレーニングして、100 回反復するたびにトレーニング データのモデルの誤差を表示します。ロジスティック回帰分類のトレーニングに勾配降下法を使用する場合、2 つの異なる方法があります。よく使われる 1 つ目のアプローチは、「確率」、「オンライン」、「インクリメンタル」(ML 用語ではカオス) と呼ばれます。2 つ目のアプローチは、「バッチ」または「オフライン」と呼ばれます。どちらのアプローチも後ほど説明しますが、今回は確率的勾配降下トレーニング法を使用します。

トレーニングが完了すると、見つかった最適な重み (-9.84, -14.88, ... -15.09) を表示します。モデルの重みはすべて、ランダム データの生成に使用した重みの約 2 倍になっているのがわかります。このデモ プログラムは、出力モデルのトレーニング データでの精度 (99.88%、8,000 個のうち 7,990 個が正解) と、テスト データでの精度 (99.80%、2,000 個のうち 1,996 個が正解) を計算します。

ロジスティック回帰分類

ロジスティック回帰分類については、具体的な例を使用して説明するのが最もわかりやすいでしょう。年齢 (x1)、年収 (x2)、教育レベル (x3) を基に性別 (男性 = 0、女性 = 1) を予測するとします。Y を予測値とすると、この問題のロジスティック回帰モデルは次の式で求められます。

Z = b0 + b1(x1) + b2(x2) + b3(x3)
Y = 1.0 / (1.0 + e^-Z)

ここで b0、b1、b2、b3 は重みで、ある決められた数値にすぎません。つまり、入力値に bi の重みを乗算し、その合計に定数 b0 を加算した中間値 Z を計算し、その Z 値を自然対数の底 e を使用する方程式に渡します。この方程式を、ロジスティック シグモイド関数と呼びます。各入力変数 (xi) には重み (bi) が関連付けられ、入力には関連付けられていない追加の重み (b0) があることがわかります。

Y は必ず 0 ~ 1 の間に収まることがわかります。Y が 0.5 未満 (0 に近い) 場合予測結果を 0 とし、Y が 0.5 以上の場合予測結果を 1 とします。n 個のフィーチャーがあるとすると、b の重みは n+1 個あることになります。ロジスティック回帰を使用してすべてのデータをモデル化できるわけではありませんが、最もシンプルな分類手法の 1 つなので、手始めとして適しています。

スケール変換した年齢 x1 = +0.80 (平均よりも高年齢)、年収 x2 = -0.50 (平均よりもやや低収入)、教育レベル x3 = -1.00 (平均よりも高学歴) という特徴を有する人がいるとします。また、b0 = 3.0、b1 = -2.0、b2 = 2.0、b3 = 1.5 とします。この場合、Z = 3.0 + (-2.0)(0.80) + (2.0)(-0.50) + (1.5)(-1.00) = -1.10 となり、Y = 1.0 / (1.0 + e^-(-1.10)) = 0.25 となります。Y は、1 よりも 0 に近い (0.5 未満の) ため、男性だと予測します。

ロジスティック回帰の出力計算を実装するデモ コードは以下のとおりです。

public double ComputeOutput(double[] dataItem, double[] weights)
{
  double z = 0.0;
  z += weights[0]; // Add b0 constant
  for (int i = 0; i < weights.Length - 1; ++i)
    z += (weights[i + 1] * dataItem[i]); // Skip b0
  return 1.0 / (1.0 + Math.Exp(-z)); // Logistic sigmoid
}

疑問になるのは 重み b の決め方です。この重み b の値を決定するプロセスをモデルのトレーニングと呼びます。入力値と出力値が判明しているトレーニング データを使用して、計算後の出力値 (目標値または目的値) と既知の正しい出力値との誤差が最小になる値のセットが見つかるまで、さまざまな重み b を試すというのがトレーニングの考え方です。

誤差が最小になる重みの値を見つけるのが難しく、そのために使用する数値最適化アルゴリズムがたくさんあります。そして各アルゴリズムに長所と短所があります。最もよく使われる最適化アルゴリズムは、シンプレックス最適化、L-BFGS 最適化、粒子群最適化、反復型ニュートン ラフソンなど、10 個以上あります。最も基本的な最適化アルゴリズムが、勾配降下法です。

勾配降下法とは

ソフトウェア開発者の観点から勾配降下法を考えてみましょう。説明や用語を勝手に変えて、できる限り考え方が明瞭になるようにします。図 2 のグラフをご覧ください。グラフは、重みの関数として誤差をプロットしています。重みが変化すると、ロジスティック回帰分類から出力される誤差が変化します。目標は、誤差が最小になる重みを見つけることです。図 2 の場合、誤差が最小になる重みは w = 5.0 です。ここでは、w で任意の b の重みを示します。図 2 に示すようなグラフは、重みごとに 1 つずつ異なることに注意してください。

偏導関数と勾配降下法
図 2 偏導関数と勾配降下法

誤差グラフの形状がすべてわかっていれば、重みの決定は簡単ですが、残念ながら、誤差グラフの形状は 1 つもわかりません。可能性のあるすべての重みを単純に試してみて、出力の誤差を計算することを考えたくなりますが、可能性のある重みの数は無数です。

関数のある点の微積分の導関数は、その点の接線の傾きです。傾きには、接線の方向を示す符号 (+ または -) と、接線の傾き度合いを示す数値があります。図 2 では、たとえば、w = 7.0 の場合、接線の傾き (つまり、導関数) は +2.15 (右上から左下への急勾配) です。w = -5.0 の場合、導関数は -0.90 (左上から右下への緩やかな傾斜) です。

重みごとに導関数があるため、個別の導関数を偏導関数と呼びます。勾配とは、すべての偏導関数の集まりです。「重み b2 に関する誤差関数の偏導関数」よりも「勾配」という言葉の方が簡単です。そのため、一般的には勾配と偏導関数は同じ意味で使われます。偏導関数は、多くの場合、6 を反転させたような特別な数学記号で示されます。

ポイントはどこにあるでしょう。図 2 を注意深く見ると、偏導関数を使用すれば、特定の重みから誤差が最小になる重みへと移動できることがわかります。偏導関数の符号は移動方向を示し、偏導関数の数値は移動距離のヒントになります。数値が大きくなると、移動距離が長くなります。この手法は、最小値に誤差関数を降下させていくことから、勾配降下法と呼ばれます。

ここまでは問題ありませんか。ここからは、この考え方をコードに置き換えてみましょう。つまり、ロジスティック回帰の重みを更新する擬似コードを考えます。インターネット上には、重みの更新ルールを導くためのかなり洗練された微積分について説明するリソースが数多くあります。結論だけを拾えば以下のようになります。

wj = wj + a * (target - computed) * xj

言葉にすると、「j 番目の重み (新しい重み) は、定数 "a" と、トレーニング データの目標値と計算値の差と、j 番目の重みに関連付けられているフィーチャー (入力) とを掛け合わせた値に古い重みを加算した値」です。更新ルールは、多くの場合、重みにギリシャ文字のシータ (θ) を、定数にアルファ (α) を用いて表します。定数 "a" を学習率と呼びます。

j = 2、つまり b2 の重みを求めるとします。このとき、b2 の現在値が 0.50 だとします。あるトレーニング データ項目で、既知の目標値を 1、(すべての x 値を使用して) 計算した出力値を 0.80、x2 (重み b2 に対応する入力値) の値を 3.0、学習率を 0.10 とすると、新しい重み b2 は以下のようになります。

b2 = 0.50 + 0.10 * (1 - 0.80) * 3.0
b2 = 0.50 + 0.06
b2 = 0.56

更新ルールは、停止条件に達するまで繰り返し適用されます。実に簡単です。計算出力 (0.80) が目標値 (1.0) に比べて小さすぎるので、重みの更新ルールによって重みの値が増えたことがわかります。このルールは、トレーニングの次回の反復で計算出力値が増加する効果があることになります。計算出力値が目標値に比べて大きすぎる場合、重みの更新ルールによって重みが減ります。とても簡単です。

重みの更新ルールを導く方法は主に 2 つあります。インターネット上で見つかる資料で確認できる最も一般的なアプローチは、まず、特定の重みのセットでトレーニング データのセットが得られる確率を定義した後、最尤推定法と呼ばれるかなり複雑な計算手法を使用して、観察するデータの確率が最大となるパラメーターの値を見つける方法です。

ほかにも、2 つの一般的な誤差定義 (誤差の偏差の平方和またはクロス エントロピ誤差) のいずれかを使用して、誤差の意味を定義することから始める方法があります。このアプローチでは、その後微積分を使用して、誤差が最小になる重みのセットを見つけます。クロス エントロピ誤差から始める場合、出力される重みの更新ルールは、確率を最大にすることによって生成されるルールと同じです。誤差の偏差の平方和から始める場合、出力される更新ルールに以下のように 2 つの項が追加されます。

wj = wj + a * (target - computed) * xj * computed * (1 - computed)

こちらの更新ルールの場合、計算した項は常に 0 ~ 1 の間に収まるため、計算出力値と (1 - 計算出力値) の積は常に 0 ~ 0.25 の間に収まります。つまり、この更新ルールを使用して重みを更新すると、シンプルな形式よりもさらに手順が少なくなります。実際には、どちらの更新ルールも同様の結果が得られるため、ほとんどの場合、シンプルな方を使用します。さいわい、ロジスティック回帰分類をトレーニングするために、重みの更新ルールを導く方法について知る必要はありません。

確率導出アプローチは、勾配を上るほど確率が最大値に向かうため、勾配上昇法と呼ばれます。誤差導出アプローチは、勾配を下がるほど誤差が最小値に向かうため、勾配降下法と呼ばれます。勾配を使用するロジスティック回帰分類のトレーニングは、勾配降下法と呼ばれたり、勾配上昇法と呼ばれたりするところがポイントです。どちら用語も同じ重みの更新ルールをテーマにしています。

デモ プログラムの構造

スペースを節約するために少し編集したデモ プログラムの構造を図 3 に示します。デモを作成するには、Visual Studio を起動して、C# コンソール アプリケーション テンプレートを選択します。プロジェクトには「LogisticGradient」という名前を付けます。このデモは .NET との大きな依存関係がないため、どのバージョンの Visual Studio でも機能します。デモ コードは長すぎてコラムにすべて掲載することはできませんが、全ソース コードは、このコラム付属のコード ダウンロードから入手できます。中心となる考え方をできるだけ明確にするために、通常行うエラー チェックはすべて削除しています。

図 3 デモ プログラムの構造

using System;
namespace LogisticGradient
{
  class LogisticGradientProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin classification demo");
      Console.WriteLine("Demonstrating gradient descent");
      ...
      Console.WriteLine("End demo");
      Console.ReadLine();
    }
    static double[][] MakeAllData(int numFeatures,
      int numRows, int seed) { . . }
    static void MakeTrainTest(double[][] allData, int seed,
      out double[][] trainData, out double[][] testData) { . . }
    static void ShowData(double[][] data, int numRows,
      int decimals, bool indices) { . . }
    static void ShowVector(double[] vector,
      int decimals, bool newLine) { . . }
  }
  public class LogisticClassifier
  {
    private int numFeatures;
    private double[] weights;
    private Random rnd;
    public LogisticClassifier(int numFeatures) { . . }
    public double[] Train(double[][] trainData,
      int maxEpochs, double alpha) { . . }
    private void Shuffle(int[] sequence) { . . }
    private double Error(double[][] trainData,
      double[] weights) { . . }
    private double ComputeOutput(double[] dataItem,
      double[] weights) { . . }
    private int ComputeDependent(double[] dataItem,
      double[] weights) { . . }
    public double Accuracy(double[][] trainData,
      double[] weights) { . . }
  }
}

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

LogisticGradientProgram クラスには、合成データを作成して表示するヘルパー メソッド MakeAllData、MakeTrainTest、ShowData、および ShowVector があります。すべての分類ロジックは、LogisticClassifier というプログラム定義のクラスに含めています。Main メソッドでは、以下のステートメントを使用して合成データを作成します。

int numFeatures = 8;
int numRows = 10000;
int seed = 1; // Arbitrary
double[][] allData = MakeAllData(numFeatures, numRows, seed);

MakeAllData メソッドが基本的なロジスティック回帰分類を実行します。このメソッドは、ランダムな重みを生成後、ランダムな入力値を繰り返し生成し、ロジスティック シグモイド関数を使用して重みと入力値を組み合わせて、対応する出力値を計算します。このメソッドは、データにランダムなノイズを追加しません。つまり、理論的には 100% の予測精度を実現します。合成データは、以下のようにトレーニング セットとテスト セットに分割します。

double[][] trainData;
double[][] testData;
MakeTrainTest(allData, 0, out trainData, out testData);
ShowData(trainData, 3, 2, true);
ShowData(testData, 3, 2, true);

MakeTrainTest メソッドは、80% 対 20% にハードコードされたトレーニング セット分割率を使用します。この分割率はパラメーターとして渡してもかまいません。ロジスティック回帰分類は、以下のコードを使用して作成およびトレーニングします。

LogisticClassifier lc = new LogisticClassifier(numFeatures);
int maxEpochs = 1000;
double alpha = 0.01; // Learning rate
double[] weights = lc.Train(trainData, maxEpochs, alpha);
ShowVector(weights, 4, true);

トレーニング パラメーター maxEpochs と alpha (学習率) の値は、試行錯誤で決めました。大部分の ML トレーニング メソッドを調整する場合は、通常、何らかの実験を行って優れた予測精度を得る必要があります。トレーニング済みのモデルの品質は、以下のようにして評価します。

double trainAcc = lc.Accuracy(trainData, weights);
Console.WriteLine(trainAcc.ToString("F4"));
double testAcc = lc.Accuracy(testData, weights);
Console.WriteLine(testAcc.ToString("F4"));

2 つの精度値のうちテスト データでのモデルの精度の方が関連性が高くなります。テスト データでのモデルの精度によって、出力値がわからない新しいデータが渡された場合のモデルのおおまかな精度がわかります。

勾配降下トレーニングの実装

Train メソッドの定義は以下のように始めます。

public double[] Train(double[][] trainData, int 
  maxEpochs, double alpha)
{
  int epoch = 0;
  int[] sequence = new int[trainData.Length];
  for (int i = 0; i < sequence.Length; ++i)
    sequence[i] = i;
...

epoch 変数は、トレーニング ループのカウンター変数です。sequence という配列は、トレーニング データのインデックスで初期化します。ここでの考えでは、毎回の反復ではトレーニング データを処理する順番をランダムに変えています。次に、メインの重み更新ループを始めます。

while (epoch < maxEpochs)
{
  ++epoch;
  if (epoch % 100 == 0 && epoch != maxEpochs)
  {
    double mse = Error(trainData, weights);
    Console.Write("epoch = " + epoch);
    Console.WriteLine(" error = " + mse.ToString("F4"));
  }
  Shuffle(sequence); // Process data in random order
...

誤差の計測は、100 回反復するたびに計算して表示します。Error メソッドは、計算値と目標出力値の差の 2 乗を合計した平均として、平均 2 乗誤差を返します。これは、勾配降下重み更新ルールの基礎である誤差定義と少し異なっていることに注意してください。勾配降下トレーニングを使用する場合、誤差は暗黙のうちに使用され、直接使用されることはありません。特に粒子群最適化など、誤差を明示的に使用するトレーニング手法もあります。Shuffle メソッドは、Fisher-Yates アルゴリズムを使用して、sequence 配列に含まれるトレーニング データのインデックスをシャッフルします。

勾配降下トレーニングの中核となるのは、以下の短いコードです。

for (int ti = 0; ti < trainData.Length; ++ti)
{
  int i = sequence[ti];
  double computed = ComputeOutput(trainData[i], weights);
  int targetIndex = trainData[i].Length - 1;
  double target = trainData[i][targetIndex];
  weights[0] += alpha * (target - computed) * 1;
  for (int j = 1; j < weights.Length; ++j)
    weights[j] += alpha * (target - computed) * trainData[i][j - 1];
}

まず、ランダムな順序での次回のトレーニング項目を、シャッフルした sequence 配列を使って特定します。ComputeOutput メソッドは、現在の重みを使用して、0.0 ~ 1.0 の間の値に収まる重みセットの現在出力値を計算します。目標値 0 または 1 は、現在のトレーニング項目から求めます。b0 の重みを最初に更新します。前述のとおり、重みの更新ルールでは、変更する重みに関連付けられた入力値を使用します。ただし、重み b0 は実際の入力に関連付けられていません。そこで、ロジスティック回帰の重み b0 には、常に 1.0 となるダミーの入力値があると考えます。デモ コードでは、何も影響を与えないことが明白な 1.0 を乗算して、b0 の更新と別の b の重みの更新を同じように扱っています。b0 以外の重み b の更新は簡単ですが、インデックスの使い方には少し注意が必要です。Train メソッドは以下のように終了します。

...
} // While loop
  return this.weights;
} // Train

このメソッドは、LogisticClassifier の weights 配列に含まれる実際の重みへの参照を返します。安全性を考え、results 配列を作成した後、重みを results 配列にコピーして、配列への参照を返してもかまいません。

前述のとおり、デモでは確率的勾配降下法を使用しています。各トレーニング項目を検出したら、そのトレーニング項目の勾配を計算し、すべての重みの更新に使用します。これとは対照的に、バッチ勾配降下法では、まず、すべてのトレーニング項目の毎回の反復の勾配を蓄積後、重みを更新します。バッチ トレーニングを使用する場合、Train メソッドの中心部分は図 4 に示すコードになります。

図 4 バッチ トレーニングを使用する場合の Train メソッド

double[] accumulatedGradients = new double[weights.Length];
for (int i = 0; i < trainData.Length; ++i)  // Accumulate
{
  double computed = ComputeOutput(trainData[i], weights);
  int targetIndex = trainData[i].Length - 1;
  double target = trainData[i][targetIndex];
  accumulatedGradients[0] += (target - computed) * 1; // For b0
  for (int j = 1; j < weights.Length; ++j)
    accumulatedGradients[j] += (target - computed) * trainData[i][j - 1];
}
for (int j = 0; j < weights.Length; ++j) // Update all wts
  weights[j] += alpha * accumulatedGradients[j];

バッチ トレーニングを使用する場合、全トレーニング項目を処理してから重みを更新するため、トレーニング データをランダムな順序で処理するメリットはありません。

勾配降下トレーニングが最初に考案されたとき、バッチ アプローチは利用可能なすべての情報を使用して重みの勾配を見つけるため、バッチ アプローチの手法の方が理論的に優れていると考えられていました。しかし、ML の専門家は、単一のトレーニング項目の勾配を全体勾配の大まかな推定に使用することで、トレーニング速度を高速化できることにすぐ気付きました。つまり、確率的 (ランダムに決定される) 勾配降下法では、1 つの勾配を使用して全体の勾配を推定します。

まとめ

ロジスティック回帰分類でよく使用される基本的な勾配降下法の 2 つの関連するバリエーションは、BFGS および L-BFGS と呼ばれます。このような 2 つのアルゴリズムは、複雑さがかなり増しますが、基本的な勾配降下法を強化するための試みです。

勾配降下法は、ロジスティック回帰分類だけでなく、別の複数の ML 手法にも使用できます。勾配降下法は、特にニューラル ネットワークのトレーニングに使用できます。勾配降下法をニューラル ネットワークに使用する場合は、バックプロパゲーションと呼ばれます。


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

この記事のレビューに協力してくれた技術スタッフの Richard Hughes (Microsoft Research) に心より感謝いたします。