多クラス ロジスティック回帰分類
ロジスティック回帰 (LR) 分類は、機械学習 (ML) における "Hello, world!" のようなものだと私は考えています。標準の LR 分類は、2 つのカテゴリ値のうちいずれかのカテゴリ値だけを受け取る変数の値を予測するのが目標です。たとえば、身長と年収に基づいて、性別 (男性または女性) を予測することができます。
多クラス LR 分類は標準の LR を拡張したもので、予測対象の変数に 3 種類以上の値を格納できるようになっています。たとえば、年齢、年収などの予測変数に基づいて、政治的傾向 (保守派、穏健派、またはリベラル派) を予測することができます。今回は、多クラス LR のしくみを説明し、C# を使用して多クラス LR を実装する方法を示します。
今回の目標を確認するには、図 1 のデモ プログラムを見るのが一番です。デモでは、まず、1,000 行の合成データを生成します。各データ行には 4 個の予測変数 (フィーチャーと呼ばれます) があり、予測対象の変数は 3 つの値のうちいずれかを受け取ることができます。たとえば、ある生成データの行は次のようになります。
5.33 -4.89 0.15 -6.67 0.00 1.00 0.00
図 1 実行時の多クラス ロジスティック回帰
最初の 4 つの値は、現実のデータを表す正規化された予測値です。予測値は、0.0 の値がフィーチャーの平均ちょうどを表し、0.0 を超える値がフィーチャーの平均を超えていることを表し、0.0 未満の値がフィーチャーの平均未満を表すように正規化されています。最後の 3 つの値は、予測対象の変数に 1-of-N エンコーディングを適用した値です。たとえば、政治的傾向を予測する場合、(1 0 0) は保守派、(0 1 0) は穏健派、(0 0 1) はリベラル派を表します。
合成データの生成後、この合成データをランダムに分割して、トレーニング セット (データの 80%、つまり 800 行) とテスト セット (残りの 200 行) に分けます。トレーニング データは予測モデルの作成に使用します。テスト データは、予測対象の値が不明な新しいデータでの、モデルの予測精度を推定するために使用します。
f 個のフィーチャーと c 個のクラスがある多クラス LR モデルには、(f * c) 個の重みと c 個のバイアスがあります。これらの値は、決められた数値定数です。このデモでは、4 * 3 = 12 個の重みと 3 個のバイアスがあります。トレーニングとは、重みとバイアスの値を推定するプロセスです。トレーニングは反復プロセスです。デモでは、トレーニング反復 (多くの場合、ML 文献でエポックと呼ばれます) の最大回数を 100 に設定します。多クラス LR 分類のトレーニングに使用する手法は、バッチ勾配降下と呼ばれます。バッチ勾配降下手法では、学習率と重みの減衰率という 2 つのパラメーターの値が必要です。この 2 つの値は、多くの場合、試行錯誤の末に見出されます。デモでは、0.01 と 0.10 という値をそれぞれに割り当てています。
トレーニング中、デモでは 10 エポックごとに進捗メッセージを表示します。図 1 のメッセージを確認すると、トレーニングがすぐに収束していて、最初の 20 エポック以降は精度が向上していないことがわかります。
トレーニングが完了すると、12 個の重みと 3 個のバイアスについて見つかった最適な値を表示します。これらの 15 個の値で多クラス LR モデルを定義します。15 個の値を使用して、デモでは、モデルのトレーニング データでの予測精度 (92.63%、800 個のうち 741 個が正解) と、テスト データでの予測精度 (90.00%、200 個のうち 180 個が正解) を計算します。
今回は、中級レベルまたは高度なプログラミング スキルがあることを前提としますが、多クラス ロジスティック回帰の知識は問いません。今回のデモ プログラムは C# を使ってコーディングしていますが、他のプログラミング言語へのコードのリファクタリングもそれほど難しくありません。
多クラス ロジスティック回帰について理解する
年齢 (x0)、年収 (x1)、身長 (x3)、および教育レベル (x4) を基に政治的傾向 (保守派、穏健派、またはリベラル派) を予測するとします。(y0, y1, y2) のように 3 つの値で政治的傾向をエンコーディングします。保守派は (1, 0, 0)、穏健派は (0, 1, 0)、リベラル派は (0, 0, 1) です。この問題の多クラス LR モデルは次の式で求められます。
z0 = (w00)(x0) + (w01)(x1) + (w02)(x2) + b0
y0 = 1.0 / (1.0 + e^-z0)
z1 = (w10)(x0) + (w11)(x1) + (w12)(x2) + b1
y1 = 1.0 / (1.0 + e^-z1)
z2 = (w20)(x0) + (w21)(x1) + (w22)(x2) + b2
y2 = 1.0 / (1.0 + e^-z2)
この式で、wij はフィーチャー変数 i とクラス変数 j に関連付けられている重み値であり、bj はクラス変数 j に関連付けられているバイアス値です。
多クラス LR の例を図 2 に示します。あるトレーニング データ項目には、4 つの予測値 (5.10, -5.20, 5.30, -5.40) に続けて 3 つの出力値 (0, 0, 1) が含まれています。予測値は任意の値ですが、4 つの予測値が表す人物として、年齢が平均を超えていて年収が平均未満、身長が平均を超えていて教育レベルが平均未満で、政治的傾向がリベラル派の人物を想像できます。
図 2 多クラス ロジスティック回帰のデータ構造
weights 行列の 3 列はそれぞれ、3 つのクラス値のいずれかに対応しています。各列の 4 つの値は、4 つの予測値 x に対応しています。biases 配列は、各クラスに 1 つある、予測に関連付けられていない追加の特別な重みを保持します。
biases 配列を weights 行列に追加行として格納することも可能な点に注意してください。数学方程式が単純化されるので、この手法は研究論文でよく使用されます。ただし、デモ実装が目的の場合、個人的な意見としては weights 行列と biases 配列を分けたままにした方が少しわかりやすいと考えています。
多クラス LR では、クラスごとに出力値を計算します。図 2 の場合、計算した出力値は (0.32, 0.33, 0.35) です。出力値は合計すると 1.0 になり、確率として解釈できます。3 つの出力値の中で最後の出力値が (ほんのわずかな差ですが) 最大のため、出力が (0, 0, 1) に相当するという結論になります。この例の場合、計算した出力がトレーニング データ項目の 3 つの出力に一致しているため、モデルによる予測が正しかったことになります。
出力値を計算するには、まず、各入力値を対応する重みの値と乗算し、得られた積を合計してから、対応するバイアス値を加算します。このような積の合計は、z 値と呼ばれる場合がよくあります。z 値を、ロジスティック シグモイド関数と呼ばれる関数 1.0 / (1.0 + e^-z) に渡します。この関数の e は数学定数であり、^ は累乗を意味しています。図 2 ではわかりにくいですが、ロジスティック シグモイド関数の出力は常に 0.0 ~ 1.0 です。
各ロジスティック シグモイド値は、最終出力値の計算に使用します。ロジスティック シグモイド値を合計して、除数として使用します。このプロセスは、ソフトマックス関数と呼ばれます。LR に関するこのような概念が初耳の場合、最初はとてもややこしく感じられるかもしれません。しかし、図 2 の例を何回か確認すれば、最終的には LR が見掛けほど複雑でないとわかります。
ここで疑問になるのは、重み値とバイアス値の決め方です。重み値とバイアス値を決定するプロセスをモデルのトレーニングと呼びます。入力値と出力値が判明しているトレーニング データを使用して、トレーニング データでの計算後の出力値と正しい出力値 (目標値) との誤差が最小になる値のセットが見つかるまで、さまざまな重みとバイアスを試すというのがトレーニングの考え方です。
重みやバイアスの厳密な値を計算することは現実的ではないので、重みとバイアスを推定する必要があります。重みやバイアスの推定に使用できる、いわゆる数値最適化と呼ばれる手法はいくつもあります。一般的な手法としては、L-BFGS アルゴリズム、反復再重み付け最小二乗法、および粒子群最適化が挙げられます。デモ プログラムでは、紛らわしい呼び方ですが、勾配降下法 (計算した出力値と既知の出力値との誤差を最小化する方法) とも勾配上昇法 (重みとバイアスが最適になる確率を最大化する方法) とも呼ばれる手法を使用します。
デモ プログラムの構造
スペースを節約するために少し編集したデモ プログラムの構造を図 3 に示します。デモ プログラムを作成するには、Visual Studio を起動して、C# コンソール アプリケーション テンプレートを選択します。プロジェクトには「LogisticMultiClassGradient」という名前を付けます。このデモは .NET との大きな依存関係がないため、どのバージョンの Visual Studio でも機能します。デモ コードは長すぎてコラムにすべて掲載することはできませんが、全ソース コードは、このコラム付属のコード ダウンロードから入手できます。中心となる考え方をできるだけ明確にするために、通常行うエラー チェックはすべて削除しています。
図 3 デモ プログラムの構造
using System;
namespace LogisticMultiClassGradient
{
class LogisticMultiProgram
{
static void Main(string[] args)
{
Console.WriteLine("Begin classification demo");
...
Console.WriteLine("End demo");
Console.ReadLine();
}
public static void ShowData(double[][] data,
int numRows, int decimals, bool indices) { . . }
public static void ShowVector(double[] vector,
int decimals, bool newLine) { . . }
static double[][] MakeDummyData(int numFeatures,
int numClasses, int numRows, int seed) { . . }
static void SplitTrainTest(double[][] allData,
double trainPct, int seed, out double[][] trainData,
out double[][] testData) { . . }
}
public class LogisticMulti
{
private int numFeatures;
private int numClasses;
private double[][] weights; // [feature][class]
private double[] biases; // [class]
public LogisticMulti(int numFeatures,
int numClasses) { . . }
private double[][] MakeMatrix(int rows,
int cols) { . . }
public void SetWeights(double[][] wts,
double[] b) { . . }
public double[][] GetWeights() { . . }
public double[] GetBiases() { . . }
private double[] ComputeOutputs(double[] dataItem) { . . }
public void Train(double[][] trainData, int maxEpochs,
double learnRate, double decay) { . . }
public double Error(double[][] trainData) { . . }
public double Accuracy(double[][] trainData) { . . }
private static int MaxIndex(double[] vector) { . . }
private static int MaxIndex(int[] vector) { . . }
private int[] ComputeDependents(double[] dataItem) { . . }
}
}
テンプレート コードが読み込まれたら、ソリューション エクスプローラー ウィンドウで Program.cs ファイルを右クリックし、名前を「LogisticMultiProgram.cs」というわかりやすい名前に変更します。これにより、Visual Studio が自動的に Program クラスの名前を変更します。エディター ウィンドウのソース コードの先頭にある using ステートメントを、最上位レベルの System 名前空間を指定するステートメントを除いてすべて削除します。
LogisticMultiProgram クラスには、MakeDummyData、SplitTrainTest、ShowData、および ShowVector というヘルパー メソッドがあります。これらのメソッドで、合成データを作成して表示します。すべての分類ロジックは、LogisticMulti というプログラム定義のクラスに含めています。
Main メソッドでは、以下のステートメントを使用して合成データを作成します。
int numFeatures = 4;
int numClasses = 3;
int numRows = 1000;
double[][] data = MakeDummyData(numFeatures,
numClasses, numRows, 0);
MakeDummyData メソッドでは、一連のランダムな重みとバイアスを生成後、データ行ごとに、ランダムな入力値を生成し、重み値、バイアス値、および入力値を組み合わせて、対応する 1-of-N エンコーディングした出力値を計算します。合成データは、以下のように 80% をトレーニング セットに分割し、20% をテスト セットに分割します。
double[][] trainData;
double[][] testData;
SplitTrainTest(data, 0.80, 7, out trainData, out testData);
ShowData(trainData, 3, 2, true);
ShowData(testData, 3, 2, true);
値が 7 の引数はランダム シードで、単にデモの見栄えをよくするために使用しています。多クラス LR 分類は、以下のステートメントを使用して作成およびトレーニングします。
LogisticMulti lc = new LogisticMulti(numFeatures, numClasses);
int maxEpochs = 100;
double learnRate = 0.01;
double decay = 0.10;
lc.Train(trainData, maxEpochs, learnRate, decay);
トレーニング パラメーター maxEpochs (100)、学習率 (0.01)、および重みの減衰率 (0.10) の値は、試行錯誤で決めました。大部分の ML トレーニング メソッドを調整する場合は、通常、何らかの実験を行って優れた予測精度を得る必要があります。
トレーニングが完了したら、最適な重み値とバイアス値を LogisticMulti オブジェクトに格納します。次のようにして、最適な重み値とバイアス値を取得して表示します。
double[][] bestWts = lc.GetWeights();
double[] bestBiases = lc.GetBiases();
ShowData(bestWts, bestWts.Length, 3, true);
ShowVector(bestBiases, 3, true);
void Train メソッドを Get メソッドと組み合わせて使用する代わりに、Train メソッドをリファクタリングして、出力パラメーターや結合した配列の値として、最適な重みの行列と最適なバイアスの配列を返すこともできます。トレーニング済みのモデルの品質は、以下のようにして評価します。
double trainAcc = lc.Accuracy(trainData, weights);
Console.WriteLine(trainAcc.ToString("F4"));
double testAcc = lc.Accuracy(testData, weights);
Console.WriteLine(testAcc.ToString("F4"));
2 つの精度値のうち、テスト データでのモデルの精度の方が重要です。テスト データでのモデルの精度によって、出力値がわからない新しいデータが渡された場合のモデルのおおまかな精度がわかります。
多クラス LR トレーニングを実装する
LogisticMulti クラスのコンストラクターは、次のように定義します。
public LogisticMulti(int numFeatures, int numClasses)
{
this.numFeatures = numFeatures;
this.numClasses = numClasses;
this.weights = MakeMatrix(numFeatures, numClasses);
this.biases = new double[numClasses];
}
MakeMatrix メソッドは、配列の配列形式の行列用にメモリを割り当てるヘルパー メソッドです。weights 行列と biases 配列はすべて、0.0 の値へと暗黙的に初期化されます。一部の研究者が好んでいる別の方法では、重みとバイアスを小さい (通常は 0.001 ~ 0.01) ランダム値へと、明示的に初期化します。
ComputeOutputs メソッドの定義を図 4 に示します。ComputeOutputs メソッドは、各クラスに 1 つ値が用意されている値の配列を返します (各値は 0.0 ~ 1.0 の間であり、値を合計すると 1.0 になります)。
図 4 ComputeOutputs メソッド
private double[] ComputeOutputs(double[] dataItem)
{
double[] result = new double[numClasses];
for (int j = 0; j < numClasses; ++j) // compute z
{
for (int i = 0; i < numFeatures; ++i)
result[j] += dataItem[i] * weights[i][j];
result[j] += biases[j];
}
for (int j = 0; j < numClasses; ++j) // 1 / 1 + e^-z
result[j] = 1.0 / (1.0 + Math.Exp(-result[j]));
double sum = 0.0; // softmax scaling
for (int j = 0; j < numClasses; ++j)
sum += result[j];
for (int j = 0; j < numClasses; ++j)
result[j] = result[j] / sum;
return result;
}
クラスの定義には、ComputeDependents という同様のメソッドも含まれています。
private int[] ComputeDependents(double[] dataItem)
{
double[] outputs = ComputeOutputs(dataItem); // 0.0 to 1.0
int maxIndex = MaxIndex(outputs);
int[] result = new int[numClasses];
result[maxIndex] = 1;
return result;
}
ComputeDependents メソッドは、ある値が 1 でその他の値が 0 になっている、整数配列を返します。このような計算出力値をトレーニング データの既知の目標出力値と比較すると、モデルが正しく予測したかどうかを判断できます。そのため、予測精度の計算にも使用できます。
Train メソッドを非常に大まかな擬似コードで表すと、次のようになります。
loop maxEpochs times
compute all weight gradients
compute all bias gradients
use weight gradients to update all weights
use bias gradients to update all biases
end-loop
それぞれの重み値とバイアス値には、関連付けられた勾配値があります。大まかに言えば、勾配は、目標出力値と比べて計算出力値がどちらの方向 (正または負) へどれほど離れているかを示す値です。たとえば、ある重みについて、他のすべての重みとバイアスの値が一定に保たれ、計算出力値が 0.7 で、目標出力値が 1.0 であると仮定します。計算値が小さすぎるため、勾配の値はおよそ 0.3 です (この値は重みに追加されます)。重みの値が増加すると、計算出力値も増加します。詳細は省きますが、基本的な考えは非常にシンプルです。
勾配トレーニングの基盤となる数学は微積分を使用していて非常に複雑ですが、さいわい、この数学を完全には理解していしなくてもコードを実装できます。Train メソッドの定義は以下のように始めます。
public void Train(double[][] trainData, int maxEpochs,
double learnRate, double decay)
{
double[] targets = new double[numClasses];
int msgInterval = maxEpochs / 10;
int epoch = 0;
while (epoch < maxEpochs)
{
++epoch;
...
targets 配列は、トレーニング データ項目に格納されている正しい出力値を保持します。msgInterval 変数は、進捗メッセージの表示回数を制御します。続いて、進捗メッセージを表示します。
if (epoch % msgInterval == 0 && epoch != maxEpochs)
{
double mse = Error(trainData);
Console.Write("epoch = " + epoch);
Console.Write(" error = " + mse.ToString("F4"));
double acc = Accuracy(trainData);
Console.WriteLine(" accuracy = " + acc.ToString("F4"));
}
ML トレーニングには、通常、試行錯誤が必要なため、進捗メッセージを表示すると非常に便利です。次に、重みの勾配とバイアスの勾配用に格納先を割り当てます。
double[][] weightGrads = MakeMatrix(numFeatures, numClasses);
double[] biasGrads = new double[numClasses];
このような格納先の割り当てをメインの while ループ内で実行していることに注意してください。C# では配列を 0.0 に初期化するので、すべての勾配が初期化されます。別の方法として、while ループの外側で領域を割り当ててから ZeroMatrix や ZeroArray という名前のヘルパー メソッドを呼び出すこともできます。次に、すべての重みの勾配を計算します。
for (int j = 0; j < numClasses; ++j) {
for (int i = 0; i < numFeatures; ++i) {
for (int r = 0; r < trainData.Length; ++r) {
double[] outputs = ComputeOutputs(trainData[r]);
for (int k = 0; k < numClasses; ++k)
targets[k] = trainData[r][numFeatures + k];
double input = trainData[r][i];
weightGrads[i][j] += (targets[j] - outputs[j]) * input;
}
}
}
このコードが多クラス LR の核心です。重みの勾配はそれぞれ、基本的に目標出力値と計算出力値の誤差です。入力が負の値になる (つまり、正の方向に重みを調整する必要がある) 可能性があるという点を考慮して、関連付けられた入力値をこの誤差に乗算します。
個人的によく使用している、興味深い別の方法を紹介しましょう。この方法では、入力値の大きさを無視し、その符号だけを使用します。
double input = trainData[r][i];
int sign = (input > 0.0) ? 1 : -1;
weightGrads[i][j] += (targets[j] - outputs[j]) * sign;
私の経験では、この手法を使用するとモデルの精度が向上します。次に、すべてのバイアスの勾配を計算します。
for (int j = 0; j < numClasses; ++j) {
for (int i = 0; i < numFeatures; ++i) {
for (int r = 0; r < trainData.Length; ++r) {
double[] outputs = ComputeOutputs(trainData[r]);
for (int k = 0; k < numClasses; ++k)
targets[k] = trainData[r][numFeatures + k];
double input = 1; // 1 is a dummy input
biasGrads[j] += (targets[j] - outputs[j]) * input;
}
}
}
コードを調べると、バイアスの勾配計算を、重みの勾配を計算している for ループ内で実行できることがわかります。ここでは、パフォーマンスは低下しますが、わかりやすいように 2 つの勾配計算を分けました。また、暗黙的な入力値 1 での乗算は省略できます。この処理の追加も、わかりやすさを目的としています。次に、重みを更新します。
for (int i = 0; i < numFeatures; ++i) {
for (int j = 0; j < numClasses; ++j) {
weights[i][j] += learnRate * weightGrads[i][j];
weights[i][j] *= (1 - decay); // wt decay
}
}
重み勾配の学習率部分に基づいて重みを増減した後で、重みの減衰率を使用して重みの値を減らします。たとえば、デモでは一般的な重みの減衰値 0.10 を使用しているので、(1 - 0.10) での乗算は 0.90 での乗算、つまり 10% の減少に当たります。重みの減衰は、正規化とも呼ばれています。この手法は、重みの値が制御不能になることを防いでいます。バイアス値を更新して、Train メソッドを終了します。
...
for (int j = 0; j < numClasses; ++j) {
biases[j] += learnRate * biasGrads[j];
biases[j] *= (1 - decay);
}
} // While
} // Train
このトレーニング手法では、クラス メンバーの既存の weights 行列と biases 配列を更新します。これらの値は多クラス LR モデルを定義し、Get メソッドで取得可能です。
まとめ
勾配トレーニングには、バッチ勾配トレーニングと確率的勾配トレーニングという 2 つの主なバリエーションがあります。今回紹介した方法は、バッチ勾配トレーニングです。バッチ手法では、すべてのトレーニング項目について計算出力と目標出力との誤差を合計することで、勾配を計算します。確率的勾配トレーニングの場合は、単に個別のトレーニング データ項目を使用して勾配を推定します。いくつかの実験によると、多クラス LR に適用する手法としてはバッチ トレーニングの方がモデルの精度が高まりますが、確率的トレーニングよりも時間がかかります。ニューラル ネットワークではバッチ トレーニングよりも確率的トレーニングの方が優れている場合が一般的なため、この実験結果はかなり意外です。
Dr. James McCaffrey* は、ワシントン州レドモンドにある Microsoft Research に勤務しています。これまでに、Internet Explorer、Bing などの複数のマイクロソフト製品にも携わってきました。McCaffrey 博士の連絡先は、jammc@microsoft.com (英語のみ) です。*
この記事のレビューに協力してくれた技術スタッフの Todd Bello (マイクロソフト) と Alisson Sol (マイクロソフト) に心より感謝いたします。