前回の続き。
本論に入る前に、ここで解説したAdaBoostのアルゴリズムをもうちょい補足しておきます。
- 学習データを用意し、それぞれのデータの重みを均等にしておく。
- 学習データを識別器へ入力し、エラー率を求める。ただし、エラー率には学習データの重みを反映させる。
- 一番エラー率の低かった識別器を選択し、そのエラー率を記録しておく。
- 3で正解した学習データの重みは低く、不正解だったデータの重みを重くする。
- 更新した重みを元に2〜4の処理を複数回繰り返す。
- 最終的に選択した識別器に、エラー率に基づいた重みを割り振り(エラー率が低ければ重く、逆なら軽く)、その線形和を強識別器とする。
もちっと詳しく知りたい人は、この論文の4ページ目の囲いを読んでください。
で、ここから本題。
前回も解説したとおり、OpenCVに実装されているオブジェクト検出"cvHaarDetectObjects()"関数は、CvBoostクラスを使用しておらず、独自にAdaBoostを実装しています。
その関数のソースは、"apps/HaarTraining/src/cvboost.cpp"に実装されていて、実際に学習で使用するのは以下の3つの関数:
- icvBoostStartTraining()
- icvBoostNextWeakClassifier()
- icvBoostEndTraining()
というわけで、いきなりソースをさらすと
tmBoost.h
#include#include #include "ml.h" // Viola & Johnsのアルゴリズムで用いたAdaBoostの実装 class tmBoost { public: tmBoost(void); public: ~tmBoost(void); private: CvDTree* weakClassifier; int num_classifier; float* alpha; int* index; int weak_num; public: int train(CvMat* data, CvMat* response, int num_select); // 学習関数 float predict(const CvMat* inputMat); // 予測関数 protected: CvMat* preCalcEvalVals(CvMat *data); };
tmBoost.cpp
/******************qsort用******************/ struct qsort_data{ float data; int ref; }; int compare_data(const void *a, const void *b) { if(((struct qsort_data *)a)->data > *1; evalVals->data.fl[num_classifier * j + i] = weakClassifier[i].predict(sample)->value; } } cvReleaseMat(&sample); return evalVals; } /********* Discreet AdaBoostによるトレーニング *********/ // data:学習データ // res:教師データ // num_select: 繰り返し計算の数 int tmBoost::train(CvMat* data, CvMat* res, int num_select) { assert(data != NULL); assert(res != NULL); assert(data->rows == res->rows); assert(CV_MAT_TYPE(data->type)==CV_32FC1); assert(CV_MAT_TYPE(res->type)==CV_32FC1 || CV_MAT_TYPE(res->type)==CV_32SC1); /*** 弱識別器の学習 ここから ***/ CvMat* res2 = cvCreateMat(res->rows, res->cols, CV_32FC1); if(CV_MAT_TYPE(res->type)==CV_32SC1){ cvConvert(res,res2); } else{ cvCopy(res, res2); } releaseClassifier(); int sample_num = data->rows; // サンプル数 num_classifier = data->cols; // データ要素数=識別器の数 // 弱識別器の割り当て weakClassifier = new CvDTree[num_classifier]; CvDTreeParams param = CvDTreeParams( 1, // max depth 2, // min sample count 0, // regression accuracy: N/A here false, // compute surrogate split, as we have missing data 2, // max number of categories (use sub-optimal algorithm for larger numbers) 0, // the number of cross-validation folds false, // use 1SE rule => smaller tree false, // throw away the pruned tree branches 0 // the array of priors, the bigger p_weight, the more attention ); int i; CvMat* var_idx = cvCreateMat(1,1,CV_32SC1); CvMat* var_type = cvCreateMat( data->cols + 1, 1, CV_8U ); cvSet( var_type, cvScalarAll(CV_VAR_NUMERICAL) ); // 入力データは連続値 var_type->data.ptr[num_classifier] = CV_VAR_CATEGORICAL; // 教師データは離散値(ラベル) /*** 弱識別器の学習 ***/ for(i=0;idata.i[0] = i; // 学習に使用する要素 weakClassifier[i].train(data, CV_ROW_SAMPLE, res2, var_idx, 0, var_type, 0, param); } cvReleaseMat(&var_idx); cvReleaseMat(&var_type); /*** 弱識別器の学習 ここまで ***/ CvMat* weakEvalVals = cvCreateMat(sample_num, 1, CV_32FC1); // 弱識別器の評価データを格納 CvMat* weakTrainVals = cvCreateMat(sample_num,1, CV_32FC1); // 教師データが変換されて格納される CvMat* weights = cvCreateMat(sample_num, 1, CV_32FC1); // 入力データへの重み /* positiveデータとnegativeデータの数をカウント */ int p_num = 0; int n_num = 0; for(i=0;i data.i[i] > 0) ? p_num++ : n_num++; } /* データに重みを均等に割り当て */ float p_val = 0.5 / p_num; float n_val = 0.5 / n_num; for(i=0;i data.fl[i] = (res2->data.i[i] > 0) ? p_val : n_val; } // Boostingに必要な情報を取得 CvBoostTrainer *trainer = icvBoostStartTraining(res2, weakTrainVals, weights, NULL, CV_DABCLASS); float* tmp_alpha = (float*)malloc(num_select * sizeof(float)); int* tmp_index = (int*)malloc(num_select*sizeof(int)); CvMat *tmpEvalVals = preCalcEvalVals(data); weak_num = num_select; // Boosting for(i=0;i 0){ weak_num++; qdatas[i].data /= sum_alpha; } } alpha = (float*)malloc(weak_num * sizeof(float)); index = (int*)malloc(weak_num * sizeof(int)); for(i=0;i value; suma += alpha[i]; } return (val >= suma/2) ? 1 : 0; }
と、こんな感じ。
train関数の引数dataはN行M列の行列で、一行一行が浮動小数点ベクトルであらわすトレーニングサンプルとなっています。ここでは、N個のトレーニングサンプルから、浮動小数点ベクトルの個々の要素を用いてM個の弱識別器を作ることを想定しています。
resは応答データ(教師信号)で、N行1列の行列です。
resはdataのトレーニングサンプル一行一行に対応しており、たとえばdataの1行目のトレーニングサンプルが「正解」データなら、resの1行目は"1"に、dataの4行目のトレーニングサンプルが「非正解」データなら、resの4行目は"0"になります。(イメージとしては、ここで用いたサンプルと同じ)
predict関数の引数sampleは、1行M列のデータです。
以下、トレーニングの流れを簡単に解説
- 各N個の入力サンプルを元に、M個の弱識別器を二分木(CvDTree)でトレーニング
- icvBoostStartTraining()でAdaBoostのために必要なパラメータを取得
- 各入力サンプルをM個の識別器へ入力し、それぞれの誤差を求める。(preCalcEvalVas())
- 誤差の重みつき総和を求め、もっとも小さい総和を持つ識別器を選択する。(calcWeakEvalVals())
- 選択した識別器とその誤差に基づき、icvBoostNextWeakClassifier()で、重みの更新。その返り値をα(その識別器の重み)として保存。
- 4,5を指定した回数(num_select)繰り返す。
- icvBoostEndTraining()で終了。
以上、かなりマニアックな解説でした。
*1:struct qsort_data *)b)->data) return 1;
else return -1;
}
/**** Boosting事前計算 ****/
// もっともエラーが低い識別器を選択
int calcWeakEvalVals(CvMat *evalVals, CvMat* res, CvMat *weights, CvMat *weakEvalVals)
{
int sample_num = evalVals->rows;
int num_classifier = evalVals->cols;
float min = (float)sample_num;
float val, val2, val3;
CvMat* sample = cvCreateMat(1,num_classifier,CV_32FC1);
int i;
int ret_i = -1;
for(i=0;i