機械学習界隈で大活躍のランダムフォレストはR、scikit-learnでの実装が一般的に使われてるようですが、以下の理由によりOpenCV3で使ってみることにしました。
・ C++上で他モジュールと連携させたい
・ 画像データの学習であり、OpenCVで取り扱っている
・ 他のC++ライブラリもあるけど、環境導入が面倒臭い
しかし、OpenCV3版ランダムフォレスト RandomTreesに関する日本語情報がほとんどない上にリファレンスもなんか実装と違う(RTrees::Paramが存在しない?)こともあり、いろいろ苦労したのでまとめた覚書になります。
#データ準備(ml::TrainData)
先ずデータの用意でつまづきました。データクラスとして使用するTrainDataは、RandomTreesだけでなく他のmlモジュールにも使えるものとなってます。
##csvファイルからの読み込み
csvファイルから学習データをTrainDataクラスに読み込みます。csvファイルは以下のように、行ごとにサンプルの特徴量ベクトルが並ぶ形式となります。ここでは、先頭列をラベルとしていますが、任意の列とすることも可能です。
以下のloadFromCSVを用いて読み込みます。
Ptr<TrainData> loadFromCSV(const String& filename, int headerLineCount, int responseStartIdx=-1, int responseEndIdx=-1, const String& varTypeSpec=String(), char delimiter=',', char missch='?')
- filename:csvファイルパス
- headerLineCount:ヘッダーの無視したい行数を指定。#を先頭につけとけば自動で無視される。
- responseStartIdx:ラベルのインデックスを指定。デフォルトでは最後尾をラベルとする。
- responseEndIdx:ラベルの終了インデックスを指定。デフォルトで1変数のラベルとする。
- varTypeSpec:各変数が連続値かカテゴリかを指定。
- ラベル変数をカテゴリにすると分類、連続値にすると回帰問題となる。
- デフォルトでは、ラベルが1変数の場合、整数・文字列であれば分類(カテゴリ)に、それ以外であれば回帰(連続値)となる。
- "ord"と指定するとすべて連続値として読み込まれる。
- "cat"と指定するとすべてカテゴリとして読み込まれる。
- 連続値・カテゴリを混合して指定する場合は、"ord[0,3-4],cat[1-2]"のように、各インデックスを指定する。
- delimiter:csvファイルの区切り文字。デフォルトで','で区切る。
(※半角空白があると強制的に区切られてしまうため、1変数の表記に半角空白を入れない。)
上で示したcsvデータは以下のように読み込みます。ラベルが先頭列なので、responseStartIdx=0とします。
string filename = "sample.csv";
Ptr<TrainData> data = TrainData::loadFromCSV(filename, 0, 0, -1);
##メモリからの読み込み
画像群から特徴抽出してメモリ上でデータセットを生成した場合はcreateでTrainDataを作ることができます。
Ptr<TrainData> create(InputArray samples, int layout, InputArray responses, InputArray varIdx=noArray(), InputArray sampleIdx=noArray(), InputArray sampleWeights=noArray(), InputArray varType=noArray())
- samples:入力変数ベクトル(特徴量)のfloat型データ配列。上記csvデータのラベルを除いたもの。
- layout:サンプル方向。csvデータと同じことをすると、ROW_SAMPLEと指定すれば行ごとにサンプルとして読み込む。列にしたければCOL_SAMPLE。
- responses:出力変数(ラベル)のデータ配列。ROW_SAMPLEであれば、サンプル数×1のカラムになる。float型であればデフォルトで連続値(回帰問題)となる。
- varIdx:学習に使用する変数のインデックス。全て使うなら無視。
- sampleIdx:学習に使用するサンプルのインデックス。全て使うなら無視。
- varType:各変数が連続値かカテゴリか指定。<特徴変数の数>+<ラベルの数>のuchar型カラム配列となっている。連続値の変数のインデックスに0,カテゴリには1を指定する。
データ方向と、分類か回帰かに注意すれば問題ないと思います。
###学習、テストデータ分割
setTrainTestSplitRatioを使うと、TrainData内で学習用・テスト用を振り分けて評価することができます。
void TrainData::setTrainTestSplitRatio(double ratio, bool shuffle=true)
- ratio:学習用に振り分ける割合。データ総数×ratioが学習データ数となる。
- ratioが0のときは振り分けられない。1.0ではエラーになるため注意。
- shuffle:デフォルトでランダムにシャッフルして振り分ける。
#Random Trees(ml::RTrees)
データが準備できたら学習を始めましょう。
###初期化
RTreesのcreate関数で初期化します。
Ptr<RTrees> rtrees = RTrees::create();
###パラメータ設定
学習パラメータをゴリゴリセットしていきます。
rtrees->setMaxDepth(5);
rtrees->setMinSampleCount(2);
rtrees->setCalculateVarImportance(true);
rtrees->setTermCriteria(TermCriteria(TermCriteria::MAX_ITER+TermCriteria::EPS, 100, 0.1));
- setMaxDepth:言わずもがな、木の最大の深さ
- setMinsampleCount:葉ノードの最小サンプル数
- setCalcVarImportance:説明変数の重要度を計算するかのフラグ。オンにすると変数の解析ができる。(ランダムフォレストのウリ)
- setTermCriteria:アンサンブル木の停止条件。
- MAX_ITER:木の最大個数
- EPS:OOBエラーの閾値
他にも沢山パラメータがありますが、基本的に押さえるのはここらへんかなと思います。
###学習
データ準備で用意したTrainDataを引数に食わせて学習を開始します。
rtrees->train(data);
###評価
TrainDataでsetTrainTestSplitRatioを設定していれば、学習・テストデータそれぞれ評価できます。
calcError関数にTrainDataとフラグ(学習:false,テスト:true)を入れて評価します。
printf("train error: %f\n", rtrees->calcError(data, false, noArray()));
printf("test error: %f\n\n", rtrees->calcError(data, true, noArray()));
分類問題ではエラー率(%表記)で、回帰問題ではRMSで評価されます。
###保存、読み込み
学習が終わったら、save関数で保存しましょう。
rtrees->save("rtree_model.xml");
同様にload関数で読み込めます。
Ptr<RTrees> rtrees = RTrees::load<RTrees>("rtree_model.xml");
###推定
最後に推定です。
今回の例ではROW配列に推定したい特徴ベクトルを格納してpredict関数に入れれば推定結果が返ります。
以下の例では、TrainDataのサンプルを取得して、1サンプルごとに推定処理を行っています。
Mat samples = data->getSamples();
for (int i = 0; i < samples.rows; i++) {
Mat sample = samples.row(i);
float result = rtrees->predict(sample);
cout << "index:" << i << ", predict:" << result <<endl;
}
複数のサンプルをまとめて推定もできますが、第二引数に結果を受け取る配列を指定する必要があります。
Mat results;
float result = rtrees->predict(samples, results);
ここでの戻り値は最初のサンプルのみの推定結果となるため、特に意味はありません。
###補足:学習時のエラーについて
サンプルの学習データを利用して実験していたところ、入力変数と出力変数(ラベル)をカテゴリにするとエラーが生じることがありました。
どうやら木を学習するサブセット内で、各出力変数に対して最低1サンプルないと例外を飛ばすみたいです。
つまり、ラベルがA,Bの二つだった場合、サブセット内のサンプル全てがAラベルだとエラーになります。
OpenCVの仕様なのでどうしようもないのですが、サブセットのサンプル数が少ないとそういうことも起こりやすくなる気がするので、データ量を増やすか、入力変数か出力変数どちらかを実数値にした方がよさそうです。
そもそも、画像データで学習するなら特徴量は実数になるしCVとしては問題ないということでしょうか…(それでも、入力変数にカテゴリを許してるあたり変な感じがします)
データマイニング的な利用はやはりRやscikit-learnが良いのですかね。