次の方法で共有


テストの実行

放射基底関数ネットワークの訓練

James McCaffrey

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

放射基底関数 (RBF) ネットワークは、データの分類と予測が可能なソフトウェア システムです。RBF ネットワークはニューラル ネットワークと表面的に類似していますが、実際は大きく異なります。RBF ネットワークでは、1 つ以上の入力数値を受け取り、1 つ以上の出力数値を生成します。出力値は、入力値に加えて、RBF の中心、RBF の幅、RBF の重み、および RBF のバイアスと呼ばれるパラメーターの各セットから算出します。

用語を簡略化するために、RBF の重みとバイアス値の組み合わせをまとめて重みと呼ぶことがあります。多くの場合は、重みという用語を使う方が意味がわかりやすくなります。詳細については、MSDN マガジン 10 月号の私の記事「プログラマのための放射基底関数ネットワーク」(msdn.microsoft.com/magazine/dn451445) を参照してください。

分類と予測に RBF ネットワークを使う際の課題は、計算した出力が既知の出力のセットに最も適合するような、中心、幅、重み、およびバイアスの値のセットを見つけることです。これは、RBF ネットワークの訓練と呼ばれます。1988 年に導入されて以来、RBF ネットワークの研究が重ねられてきましたが、RBF ネットワークの訓練の実装方法を説明する実践的な指針は多くありません。今回の記事では、デモ RBF ネットワーク全体を示しながら説明します。

図 1 のデモ プログラムをご覧ください。このプログラムでは、花のがくへんの長さと幅、花びらの長さと幅という 4 つの数値からアヤメの種 (学名 "setosa"、"versicolor"、または "virginica") を予測する RBF モデルを作成します。デモ プログラムのデータ ソースは、フィッシャーのアヤメのデータと呼ばれる有名な 150 項目のベンチマーク セットから抽出した、30 の項目で構成されています。30 のデータ項目は、事前に処理されています。4 つの x の数値は正規化されているため、0 未満の値は平均未満の長さまたは幅を意味し、0 を超える値は平均を超える長さまたは幅を意味します。種の y 値は、それぞれ setosa は (0,0,1)、versicolor は (0,1,0)、および virginica は (1,0,0) にエンコードされています。


図 1 放射基底関数ネットワーク デモ プログラム

デモでは、30 項目のデータ セットを訓練用の 24 項目のセットに分割します。結果の RBF モデルのテストや評価を目的とした予約済みの 6 項目のセットもあります。デモでは、RBF ネットワークの 1 つのインスタンスを、4 つの入力ノード (入力データ値ごとに 1 つ)、5 つの隠し処理ノード、および 3 つの出力ノード (出力データ値ごとに 1 つ) で作成します。隠しノードの最適な数を決定するには、ほとんどの場合は試行錯誤が必要です。デモの 5 という隠しノード数は、任意に選択した数です。

図 1 は、RBF ネットワークの訓練が 3 段階で構成されていることを示しています。第 1 段階では、中心を算出します。中心は、訓練データから選択した代表的な x 値と考えることができます。RBF ネットワークには隠しノードあたり 1 つの中心が必要なので、デモには 5 つの中心が必要になります。訓練アルゴリズムでは、[9]、[19]、[21]、[20]、および [4] という訓練データ項目から x 値を選択します。つまり、最初の中心は (-0.362, -2.019, 0.074, 0.112) です。

訓練の第 2 段階では、幅を算出します。幅は、中心間の距離を表す値と考えることができます。RBF ネットワークには隠しノードあたり 1 つの幅が必要です。デモでは、5 つの幅を別々に計算するのではなく、5 つすべての隠しノードに対して 3.3318 という値の共通の幅を 1 つ算出します。

訓練の第 3 段階では、RBF の重みとバイアス値を算出します。重みとバイアス値は、数値定数と考えることができます。RBF ネットワークに、NI 個の入力ノード、NH 個の隠しノード、および NO 個の出力ノードがある場合、このネットワークには (NH * NO) 個の重み値と NO 個のバイアス値が必要になります。したがって、デモ RBF ネットワークは 4-5-3 アーキテクチャなので、5 * 3 = 15 個の重みと 3 つのバイアスで計 18 個の重みとバイアス値が必要です。デモ プログラムでは、粒子群最適化 (PSO) を使って 18 個の重みとバイアスを算出します。

24 項目の訓練データ セットを使ってデモ RBF ネットワークを訓練したら、6 項目のテスト データ セットをネットワークに入力します。この例では、RBF ネットワークは 0.8333 の分類精度で 6 つのテスト項目から 5 つの種を正しく予測します。

このコラムは、読者が C# の高度なプラグラミング スキルと、放射基底関数ネットワークの入力 - 処理 - 出力メカニズムの基礎知識を持っていることを前提とします。このメカニズムについては、10 月号のコラムで説明しました。デモ プログラムのソース コードは非常に長いのですべてをこのコラム内に掲載することはできませんが、archive.msdn.microsoft.com/mag201312TestRun(英語) で完全なコードをダウンロードできます。

プログラムの全体構造

デモ プログラムを作成するには、Visual Studio 2012 を起動し、RadialNetworkTrain という名前の C# コンソール アプリケーションを作成します。このデモは .NET との大きな依存関係がないため、どのバージョンの Visual Studio でも機能します。テンプレート コードが読み込まれたら、ソリューション エクスプローラー ウィンドウで Program.cs ファイルの名前を RadialTrainProgram.cs というわかりやすい名前に変更します。この変更により、関連付けられた Program クラスの名前が Visual Studio によって自動的に変更されます。ソース コードの先頭では、System 名前空間の参照だけを残して、.NET 名前空間の不要な参照をすべて削除します。

プログラムの全体構造を図 2に示します。いくつかの WriteLine ステートメントを削除し、少し編集しています。デモには、Main メソッドを格納するプログラム クラスに加えて、RBF ネットワークの作成と訓練をカプセル化する RadialNetwork クラス、重みとバイアス値を算出する RBF 訓練アルゴリズムに使用するための、粒子オブジェクトを定義する Particle クラス、およびユーティリティ表示メソッドを含んだ Helpers クラスがあります。

図 2 RBF ネットワーク デモ プログラムの全体構造

using System;
 namespace RadialNetworkTrain
 {
   class RadialTrainProgram
   {
     static void Main(string[] args)
     {
       Console.WriteLine("Begin radial basis function (RBF) network training demo");
       double[][] allData = new double[30][];
       allData[0] = new double[] { -0.784, 1.255, -1.332, -1.306, 0, 0, 1 };
       allData[1] = new double[] { -0.995, -0.109, -1.332, -1.306, 0, 0, 1 };
       // Etc.
       allData[28] = new double[] { 0.904, -1.473, 1.047, 0.756, 1, 0, 0 };
       allData[29] = new double[] { 1.431, 1.528, 1.209, 1.659, 1, 0, 0 };
       Console.WriteLine("First four and last line of normalized, encoded input data:");
       Helpers.ShowMatrix(allData, 4, 3, true, true);
       double[][] trainData = null;
       double[][] testData = null;
       int seed = 8; // Gives a good demo
       GetTrainTest(allData, seed, out trainData, out testData);
       Helpers.ShowMatrix(trainData, trainData.Length, 3, true, false);
       Helpers.ShowMatrix(testData, testData.Length, 3, true, false);
       int numInput = 4;
       int numHidden = 5;
       int numOutput = 3;
       RadialNetwork rn = new RadialNetwork(numInput, numHidden, numOutput);
       Console.WriteLine("Beginning RBF training");
       int maxIterations = 100; // Max for PSO
       double[] bestWeights = rn.Train(trainData, maxIterations);
       Console.WriteLine("Evaluating RBF accuracy on the test data");
       rn.SetWeights(bestWeights);
       double acc = rn.Accuracy(testData);
       Console.WriteLine("Classification accuracy = " + acc.ToString("F4"));
       Console.WriteLine("End RBF network training demo");
     }
     static void GetTrainTest(double[][] allData, int seed,
       out double[][] trainData, out double[][] testData) { . . }
   }
   public class RadialNetwork
   {
     private static Random rnd = null;
     private int numInput;
     private int numHidden;
     private int numOutput;
     private double[] inputs;
     private double[][] centroids;
     private double[] widths;
     private double[][] hoWeights;
     private double[] oBiases;
     private double[] outputs;
     public RadialNetwork(int numInput, int numHidden, int numOutput) { . . }
     private static double[][] MakeMatrix(int rows, int cols) { . . }
     public void SetWeights(double[] weights) { . . }
     public double[] GetWeights() { . . }
     private double MeanSquaredError(double[][] trainData,
       double[] weights) { . . }
     public double Accuracy(double[][] testData) { . . }
     private static int MaxIndex(double[] vector) { . . }
     public double[] ComputeOutputs(double[] xValues) { . . }
     private static double[] Softmax(double[] rawOutputs) { . . }
     public double[] Train(double[][] trainData, int maxIterations) { . . }
     private void DoCentroids(double[][] trainData) { . . }
     private static double AvgAbsDist(double[] v1, double[] v2,
       int numTerms) { . . }
     private int[] DistinctIndices(int n, int range) { . . }
     private void DoWidths(double[][] centroids) { . . }
     private double[] DoWeights(double[][] trainData, int maxIterations) { . . }
     private static double EuclideanDist(double[] v1, double[] v2,
       int numTerms) { . . }
     private static void Shuffle(int[] sequence) { . . }
   }
   public class Particle
   {
     // Implementation here
   }
   public class Helpers
   {
     // Implementation here
   }
 }

RadialNetwork クラスは、ほとんどのクラス メソッドがヘルパー メソッドなので、プログラム構造から感じられるほど複雑ではありません。Train メソッドは、DoCentroids、DoWidths、および DoWeights というヘルパー メソッドを呼び出すことで、3 段階の訓練プロセスを実行します。AvgAbsDist と DistinctIndices というプライベート メソッドは、DoCentroids のヘルパー メソッドです。DoWeights メソッドは、Shuffle プライベート メソッドを使って、粒子群最適化アルゴリズムを反復するたびに異なる順序で訓練データ項目を処理します。

デモの中心部分は非常にシンプルです。まず、正規化され、エンコードされたデータを設定します。

double[][] allData = new double[30][];
 allData[0] = new double[] { -0.784, 1.255, -1.332, -1.306, 0, 0, 1 };
 allData[1] = new double[] { -0.995, -0.109, -1.332, -1.306, 0, 0, 1 };
 // Etc.
 allData[28] = new double[] { 0.904, -1.473, 1.047, 0.756, 1, 0, 0 };
 allData[29] = new double[] { 1.431, 1.528, 1.209, 1.659, 1, 0, 0 };

ここでは説明を簡単にするためにデータをハードコーディングしていますが、ほとんどのシナリオでは、データをテキスト ファイルまたは SQL テーブルに格納することになります。次は、データを訓練とテストのサブセットに分割します。

double[][] trainData = null;
 double[][] testData = null;
 int seed = 8;
 GetTrainTest(allData, seed, out trainData, out testData);

さらに、RBF ネットワークのインスタンスを作成します。

int numInput = 4;
 int numHidden = 5;
 int numOutput = 3;
 RadialNetwork rn = new RadialNetwork(numInput,
    numHidden, numOutput);

前のセクションで述べたように、隠し処理ノードの最適な数は、基本的に試行錯誤で決定する必要があります。決定したら、ネットワークを訓練します。

int maxIterations = 100;
 double[] bestWeights = rn.Train(trainData, maxIterations);

そして最後に、結果のモデルを評価します。

rn.SetWeights(bestWeights);
 double acc = rn.Accuracy(testData);
 Console.WriteLine("Classification accuracy = " +
   acc.ToString("F4"));

bestWeights 配列は、Train メソッドで算出した RBF の重みとバイアス値を保持しています。これらの重みとバイアス値を SetWeights メソッドで読み込みます。中心と幅の値については、Train メソッドで設定しているので明示的に読み込む必要がありません。

放射基底関数ネットワークの入力 - 処理 - 出力

RBF ネットワークの訓練プロセスを理解するには、RBF ネットワークの入力 - 処理 - 出力メカニズムを把握する必要があります。図 3 のダイアグラムは、デモ RBF ネットワークで、ネットワークの訓練後にテスト データ項目 [1] = (0.482, 0.709, 0.452, 0.498) の出力を計算する方法を示しています。まず、入力値 x を各隠しノードに渡します。各隠しノードで、固有の中心と幅を使ってローカル出力を計算します。


図 3 放射基底関数ネットワークのアーキテクチャ

たとえば、一番上の隠しノードの中心は (-0.362, -2.019, 0.074, 0.112) で、幅は 3.3318 です。次に、各隠しノードのローカル出力を使用して、入力の重み付き合計とバイアス値を計算することで予備出力値を算出します。たとえば、hOutput[0] が隠しノード 0 のローカル出力を表す場合、一番上の出力ノードの予備出力は (hOutput[0] * w[0][0]) + (hOutput[1] * w[1][0]) + (hOutput[2] * w[2][0]) + (hOutput[3] * w[3][0]) + (hOutput[4] * w[4][0]) + bias[0] = -12.7999 です。

3 つの予備出力値を計算したら、Softmax 関数を使ってこれらの値を最終出力値に変換します。これは、すべての最終出力値が 0.0 ~ 1.0 の範囲に収まってその合計が 1.0 になるように、予備出力値を変更するという考え方です。このようにすると、出力値を確率として緩やかに解釈できます。

図 3 の場合、最終出力は (0.2897, 0.6865, 0.0237) です。中央のノードの値が最大なのでこれを 1 と解釈し、他の 2 つの値を 0 と解釈します。この結果、推定出力は (0, 1, 0) になります。既に述べたように、テスト データは (0.482, 0.709, 0.452, 0.498, 0.000, 1.000, 0.000) で、このうち前半 4 つの値が入力、後半 3 つの値が目標値です。つまり、RBF ネットワークは種 (この場合は Iris versicolor) を正しく予測したことになります。次は、RBF ネットワークの中心、幅、重み、およびバイアスの値を求める方法について考えてみましょう。

RBF ネットワークの中心を算出する

基本的に RadialNetwork クラスの Train メソッドは、実際のあらゆる処理を実行する 3 つのヘルパー メソッドのラッパーです。

public double[] Train(double[][] trainData, int maxIterations)
 {
   DoCentroids(trainData);
   DoWidths(this.centroids);
   double[] bestWeights = DoWeights(trainData, maxIterations);
   return bestWeights;
 }

DoCentroids メソッドは、代表的な入力値 x を算出します。この処理には、多くの方法があります。一般的な方法の 1 つは、データ項目の割り当てと再割り当てを繰り返して類似データ項目をグループにまとめる、K- 平均法または K-medoids 法クラスタリング アルゴリズムを使う方法です。アルゴリズムが完了すると、各クラスターには代表的なデータ メンバーが含まれています。これらのメンバーを RBF の中心として使用できます。

別の方法は、ランダムに選択した訓練データ項目から x 値を抽出することです。これは単純ですが、偶然に不適切な中心を選択するリスクがあります。

デモ プログラムでは、以下の擬似コードに示すような、軽量クラスタリングとも呼ばれる方法を使用しています。

initialize maxDistance
 intialize bestIndices
 loop
   select numHidden random indices into train data
   compute an estimated distance between selected data items
   if estimated distance > maxDistance then
     set maxDistance = curr distance
     set bestIndices = curr indices
   end if
 end loop
 fetch the x-values in train data at bestIndices
 store x-values into RBF centroids

わかりやすいように例を使ってこの考え方を説明しましょう。訓練データが図 1 に示した 24 項目から成っているとします。さらに、処理ループの初回にランダムに選択した 4 つのインデックスが [0]、[1]、[2]、および [3] だとします。これらのインデックスは、次の値に対応します。

0: ( 1.537, -0.382,  1.317,  0.756)
   1: (-0.468,  2.346, -1.170, -1.048)
   2: ( 1.115,  0.164,  0.560,  0.370)
   3: ( 1.220,  0.436,  0.452,  0.241)

これらの値が中心候補です。目的は代表的な x 値を取得することです。つまり、似通った値は必要ありません。そのため、これらの中心候補間の距離をある程度の精度で計算します。これには、多くの方法を利用できます。デモでは、中心候補の全ペア間の平均距離を計算する代わりに、候補の隣接するペア間の平均距離を計算することで、候補の全ペア間の平均距離を推定します。この例では、候補 [0] と [1]、[1] と [2]、および [2] と [3] の間の距離を計算します。

距離を計算する一般的な方法は、値の差の 2 乗を合計した値の平方根であるユークリッド距離を使うことです (注: デモ RBF ネットワークでは、ユークリッド距離で隠しノードのローカル出力値を計算する、ガウス カーネルを使用しています)。しかし、デモ プログラムでは、絶対値の差の平均を距離とする、マンハッタン距離というバリエーションを使用しています。そのため、候補 [0] と候補 [1] の間の距離を次のように算出します。

d = abs(1.537 - (-0.468)) + . . . + abs(0.756 - (-1.048)) / 4
   = 2.256

中心候補のセットを生成して候補セットの推定平均距離を計算する処理を指定回数繰り返すと、推定平均距離が最大の候補セットが、RBF の中心のセットとして選択されます。

訓練データの目標値 (0, 1, 0 など) は必要ないか使用されないので、RBF ネットワーク中心の算出は、管理されていない訓練技術と見なせます。つまり、中心を迅速に算出できます。また、RBF ネットワークの幅、重み、およびバイアス値については、ほぼ相当するニューラル ネットワークの重みとバイアス値よりも、少なくとも理論上はずっと迅速に計算できます。このため、RBF ネットワークはニューラル ネットワークよりも優れた可能性を秘めています (ただし、これから説明するようにそれだけではありません)。

RBF ネットワークの中心を算出する処理には、候補のインデックスの算出という興味深い問題が含まれています。デモ プログラムでは、リザーバ サンプリングという賢いアルゴリズムを使用します。このアルゴリズムは、最初の可能な n 個のインデックスを選択し、1 つ目のインデックスを確率に基づいて残りの可能なインデックスに置き換えるという考え方に基づいています。

private int[] DistinctIndices(int n, int range)
 {
   // Reservoir sampling. assumes rnd exists
   int[] result = new int[n];
   for (int i = 0; i < n; ++i)
     result[i] = i;
   for (int t = n; t < range; ++t) {
     int m = rnd.Next(0, t + 1);
     if (m < n) result[m] = t;
   }
   return result;
 }

これは短いながらも巧妙な手法です。他の手法には、ランダムなインデックスを生成してから重複の有無をチェックする総当り手法などがあります。

RBF ネットワークの幅を算出する

RBF ネットワークの入力 - 処理 - 出力メカニズムでは、各隠しノードに幅の値が必要です。幅の値を算出する方法は多数あります。デモ プログラムでは、すべての隠し処理ノードで使用可能な単一の共通の幅を計算するという、最もシンプルな方法を採用しています。この分野の研究は不確かになりがちで、結論が矛盾している場合もあります。デモ プログラムでは、共通の幅を、中心の全ペア間の平均ユークリッド距離として計算します。擬似コードで表すと、次のようになります。

sumOfDists = 0.0
 for each pair of centroids
   accumulate Euclidean distance between curr pair
 end loop
 return accumulated sumOfDists / number of pairs

私の経験では、RBF ネットワークの効果は、隠しノードの幅に使用する値によって大きく左右されます。研究によれば、幅が狭すぎると、幅が訓練データに適合しすぎて分類精度が低下しやすくなります。幅が広すぎると、データに適合しにくくなりすぎてやはり分類精度が低下しやすくなります。RBF ネットワークの幅の値を手動で設定しながらデモ コードを試すと、この影響を実際に確認できます。

研究では、隠しノードの共通幅の値として中心間の平均距離 davg を使うだけでなく、(2 * davg)、(davg / sqrt(2 * numHidden)) などのさまざまな値を使うことも提言されています。また、共通の幅を使わずに、隠しノードごとに異なる幅の値を計算する方法も多数あります。私の考えでは、幅の値によって RBF ネットワークが大きく左右されることは、幅の値の最適な計算方法に関する説得力ある研究成果の不足と並ぶ、ニューラル ネットワークやサポート ベクター マシンなどの代替手段と比べた RBF ネットワークの使用の主な短所です。

RBF ネットワークの重みとバイアスを算出する

中心と幅を算出したら、RBF ネットワークの訓練の最後の手順は、重みとバイアスの値を算出することです。大まかに言えば n 個の未知の値を含む方程式が n 個あるため、理論上は RBF ネットワークの重みを簡単かつ迅速に計算できます。そのため理論上は、標準的な数値手法を使用して重みの値を解くことができます。

残念ながら実際には、標準的な手法を使うと多くの実践的な問題が発生します。たとえば、連立方程式の標準的な解法の多くでは、逆行列を使用しますが、逆行列は、さまざまな理由から失敗するおそれがあります。

デモ プログラムでは、明確ながらもぜい弱なことがある数値手法を使って RBF ネットワークの正確な重みを解く代わりに、PSO を使って最適な値を推定します。PSO は、鳥の群れや魚の群雄などの協調したグループ行動に基づくメタヒューリスティックです。PSO の粒子の位置は、潜在的な解 (今回の例では重みの値の最適なセット) を表す箇所です。各粒子には、粒子の次の位置を決める速度があります。

PSO では、粒子のセットを作成します。シミュレートした時間の単位ごとに、粒子の現在の位置と速度、その粒子でこれまでに検出された中での既知の最適位置、およびすべての粒子の中での既知の最適位置に基づいて、各粒子が新しい位置に移動します。以下に、PSO をおおよその擬似コードで示します。

set number of particles
 set maxIterations
 initialize all particles to random positions
 loop maxIterations times
   for each particle
     update curr particle's velocity
     use new velocity to compute new position
     compute error for new position
     check if new particle best position
     check if new best position for all particles
   end for
 end loop
 return best position found by any particle

PSO は、それ自体が魅力的なトピックです。詳細については、2011 年 8 月号の私の記事「粒子群最適化」(msdn.microsoft.com/magazine/hh335067) を参照してください。PSO では、(粒子の現在の位置、これまでに検出された中での最適位置、およびこれまでに検出された中でのグローバルな最適位置の相対的な影響を制御する) 質量定数など、いくつかの自由パラメーターを指定する必要があります。また、粒子の数、最大反復回数、およびアルゴリズムを早期に終了するための省略可能なエラーしきい値も指定する必要があります。これらの要因については、デモ コードを使って試すことができます。

PSO と従来の数値手法に加えて、シンプルな勾配降下、実数値遺伝的アルゴリズムなど、RBF ネットワークの重みを見極める方法は他にも多数あります。RBF ネットワークの理論は非常に幅広く研究されてきましたが、さまざまな訓練手法の有効性の比較については、説得力のある研究成果がそれほどありません。

まとめ

デモ プログラムのコードと今回の説明から、RBF ネットワークについて調べるための確かな基盤を得られたことと思います。RBF ネットワークは研究者の間ではよく知られていますが、ソフトウェア開発者の間では、ニューラル ネットワーク分類器、単純ベイズ分類器、およびロジスティック回帰ほど使用されていないようです。考えられる理由の 1 つは、実践的な実装例の不足です。もう 1 つの考えられる理由は、特に RBF ネットワークの幅の計算に関連する、RBF ネットワークの基本的要因に関する不確かさです。個人的な意見では、RBF ネットワークの効果が他の機械学習技法と比べて高いか、低いか、またはほぼ同じかという問題を解決する明確な研究成果はありません。

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

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