なんだか雲行きの怪しい雑記帖

ぐだぐだ日記とメモと,あと不定期更新

週アレ(11) C++で始めるLibSVM

週に一回アレしてアレするか迷っていた記事:十一回目

前回GLSLについての話で,続きは次回と書いておきましたが,今回はちょっと横道にそれてLibSVMの話をしようと思います.
たまには自分が書きやすい記事でもいいじゃないか,と.
あと備忘録.


最近,パターン認識とか機械学習とかいうワードが,「ビッグデータ」という流行語に乗っかって流行りだしてきてるんじゃないかと思っています
パターン認識と機械学習(Pattern Recognition and Machine Learning:PRML)というキーワード自体も有名だと思いますし,私としても一回くらいこれについて触れておいてもいいかな,と思ってこの記事を書こうと思いました

なお,SVMの基礎知識についてはあまり触れないので,ご容赦ください
C++からLibSVMを使う例として見て頂ければ幸いです

また,OpenCV使いながらも可視化を行っているので,「このカーネルでこのパラメータだとこうなるのか」の確認とかなどもいいかもしれません

LibSVM

過去の自分の記事引用:

公式ページ:LIBSVM -- A Library for Support Vector Machines
サポートベクトルマシン(SupportVectorMachine)を学習(したり評価したり)するためのライブラリ
サポートベクトルマシンの説明は省きますが、LibSVMには
  • カーネルトリックを用いた非線形SVMの学習(自分で設計したカーネルも使用可能)

  • マルチクラス分類のSVMの学習(one vs oneとか※)

  • グリッドサーチなど学習するための便利(必須)なツールが標準装備(Pythonが必要)

  • 拡張やツールなどもそこそこ豊潤(LIBSVM Tools)

といった特徴があります

私の中で単純にSVMを使う場合のライブラリとして,選択肢は大きくこのLibSVMとSVM light(公式ページ:SVM-Light Support Vector Machine)の二つが存在すると思います

SVM lightの方は
  • ライブラリ自体は極めてシンプル

  • SVM structやSVM rankといった派生ライブラリが存在する(シンプルなので拡張しやすいと思われる)

という特徴があると思います

どちらも研究に使う場合は十分なライセンスで,実際アカデミックな分野での使用が多いです
どちらが良いかと言われると難しいです,というのもどちらもそれなりに実績あるしそれなりに使いやすいです


サポートベクトルマシン

SVMの説明は省くと言いましたが,ぐぐって最初の方にでてくる有用なサイトぐらいはリファしておこうと思いました

Wiki:サポートベクターマシン
宇都宮大学 足立・平田研究室:Support Vector machine
静岡理工科大学 金久保教授:サポートベクターマシン(SVM)
日本大学 計算知能研究室:Support Vector Machine

結局,SVMというのは(線形)識別器の一つで,多次元空間において二つのベクトル集合を区切る超平面を求める手法の一つです
SVMのアイディア自体はシンプルですが,高い精度を誇ることが今までの実験で確認されています


C++でLibSVMを使いたい

そんなSVMですが,やっぱり自分のプログラムに組み込んで使いたいって欲求があったりします
というわけで今回は,SVMを学習&予測に使えるLibSVMをC++から使ってみようと思います

早速,LibSVMを本家サイトからダウンロードしてきましょう
私がこの記事を書いている時点では最新バージョンは3.17でした

インクルードするヘッダ

LibSVMをダウンロードしてくるとわかりますが,そもそもヘッダは「svm.h」の一つしかありません
    // libsvm
#include "svm.h"

ヘッダがひとつだと楽ですよね

学習に必要なもの

二つあります
学習データと,学習するSVMのパラメータです

学習データ

LibSVMでは学習データは「svm_problem」構造体で表現されます
struct svm_problem
{
int l;// 学習データに含まれるノード(ベクトル)の数
double *y;// 学習ノードそれぞれのラベル(カテゴリを表す値)配列:y[0]-y[l-1]まで必要
struct svm_node **x;// 学習ノードの配列(なぜポインタのポインタなのかは後述)
};

メンバ変数が少なく,どれも割りとそのままなので簡単に説明を書いておきました
ノードの数「l」と,学習ノードのラベル配列「y」については特にこれ以上の説明は要らないと思います

さて,学習ノードについてですが,まずその参照先である「svm_node」について見てましょう
struct svm_node
{
int index;// 次元の添字
double value;// indexの次元の値
};

コメントを書いてしまったのでお気づきの方もおられるかもしれませんが,実は「svm_node」はベクトルの一要素(ベクトル内の次元の一つ)に対応します
というのも,学習ノードというのは「ベクトルの配列」であり,この「ベクトル」自体が「値の配列」なため,ポインタのポインタということになります

通常はベクトルを表現するとき,値の配列を渡せばよいのですが,その「値」を表す「svm_node」には次元を表す「index」がメンバにあります
実はLibSVMではスパースなベクトル(大半の次元が0であるようなベクトル)もメモリ的に効率よく扱えるように,値が0でない次元とその値だけを持つように工夫しています

例えば,N次元の[ 1, 0, 0, ..., 0, 1 ]^Tというベクトルがあったとすると,「svm_node」の配列を用いると
    svm_node elem[2];

elem[0].index = 1;// 次元の番号は1から始まる
elem[0].value = 1;

elem[1].index = N;
elem[1].value = 1;

と表現できます

単純な値の配列として表現した場合は
    double elem[N];
elem[0] = 1;
elem[N-1] = 1;

となりますが,doubleとして確保した要素のうち,1~N-2番目の要素が必要ないですね

ただし,実際のLibSVMではこのままだとそれぞれのベクトルのデータがどこまで続いているか分からなくなってしまうため,”終端”を表す「svm_node」を最後に追加する必要があります
”終端”を表す「svm_node」はその「index」を-1に設定することになっています
先ほどの例では,実際には次のようなコードにしなければなりません
    svm_node elem[3];

elem[0].index = 1;// 次元の番号は1から始まる
elem[0].value = 1;

elem[1].index = N;
elem[1].value = 1;

elem[2].index = -1;// valueはなんでもいい


学習するSVMのパラメータ

LibSVMで学習するSVMのパラメータは「svm_parameter」構造体で指定します,分かりやすい名前ですね
struct svm_parameter
{
// !!SVMの種類:C_SVC,NU_SVC,ONE_CLASS,EPSILON_SVR,NU_SVRの中から選ぶ!!
int svm_type;

// !!使用するカーネルの種類:LINEAR,POLY,RBF,SIGMOID,PRECOMPUTEDの中から選ぶ!!
int kernel_type;

// 多項式カーネルで用いるパラメータ:次数
int degree; /* for poly */

// !!多項式,RBF,シグモイドカーネルで使うパラメータ:カーネルによって意味合いが異なる!!
// RBFでは放射基底関数のsigmaになる
double gamma; /* for poly/rbf/sigmoid */

// 多項式,シグモイドカーネルで使うパラメータ
double coef0; /* for poly/sigmoid */


/* these are for training only */

// キャッシュサイズ
double cache_size; /* in MB */

// イテレーションの停止基準
double eps; /* stopping criteria */

// !!SVMにおけるコストパラメータ!!
double C; /* for C_SVC, EPSILON_SVR and NU_SVR */

// svm_typeに関連したパラメータ
int nr_weight; /* for C_SVC */
int *weight_label; /* for C_SVC */
double* weight; /* for C_SVC */
double nu; /* for NU_SVC, ONE_CLASS, and NU_SVR */
double p; /* for EPSILON_SVR */

// その他
int shrinking; /* use the shrinking heuristics */
int probability; /* do probability estimates */
};

特に重要だと思われるところは「!!~~!!」で示しておきました
識別器としてSVMを使う場合は「svm_type」,「kernel_type」,「gamma」,「C」だけ見とけばとりあえず大丈夫だと思います

LibSVMに学習してもらう

説明した「svm_problem」と「svm_parameter」を用意できれば,LibSVMに識別器のモデルを学習してもらうのは非常に簡単です
    // 学習!
svm_model* model = svm_train( &prob, &param );

//
// ~何か識別器を使った処理~
//

// 後処理
svm_free_and_destroy_model( &model );


なお,学習したモデルのセーブ/ロードは
int svm_save_model(const char *model_file_name, const struct svm_model *model);
struct svm_model *svm_load_model(const char *model_file_name);

と,既にLibSVM側に用意されていますので,この関数を呼べばOK

LibSVMを使って識別する

必要なのはモデル「svm_model」と,識別したい対象のベクトル「svm_node*」(特徴値の配列)です
double svm_predict(const struct svm_model *model, const struct svm_node *x);

返ってくるのはモデルを生成する際に使った学習データのラベル値です

サンプル

長いコードだけどどん

二クラスの学習データを正規分布から生成し,それからSVMによる識別器を学習します
学習データには二次元のベクトルにしているので,OpenCV使って可視化してます
@20140108
コードがちょろっと間違っていたので修正,あとOpenCV使うか否かのマクロ追加

    // OpenCV使えない場合はコメントアウト
#define ENABLE_OPENCV

// libsvm
#include <libsvm/svm.h>

#ifdef ENABLE_OPENCV
// opencv
#include <opencv2/opencv.hpp>
#endif// ENABLE_OPENCV

// その他STLとか
#include <ctime>
#include <iostream>
#include <vector>
#include <memory>
#include <functional>
#include <random>
#include <utility>


// サンプルデータを表すクラス
// 二次元ベクトル:ラベル(ノードが属するクラスのカテゴリ)を持つ
class node
{
public :
node( int label, double x, double y )
: label_( label ), x_( x ), y_( y )
{
}
// member variables
int label_;
double x_, y_;
};


int main( int, char*[] )
{
using namespace std;

// サンプルデータを適当に作る
// 今回は2クラスで,いずれも正規分布に従います
std::vector< node > samples;

// それぞれのクラスで作るサンプルデータの数です
const auto kClass1NodeNum = 100;
const auto kClass2NodeNum = 100;

// それぞれのクラスのラベルです,今回はクラス1は「1」,クラス2は「-1」としました
const auto kClass1Label = 1;
const auto kClass2Label = -1;

// それぞれのクラスのx,y軸のMeanとSigmaを入れて,その正規分布に従う乱数生成器を作ります
auto class1_x_generator = std::normal_distribution<double>( 0.3, 0.2 );
auto class1_y_generator = std::normal_distribution<double>( 0.3, 0.2 );

auto class2_x_generator = std::normal_distribution<double>( 0.7, 0.2 );
auto class2_y_generator = std::normal_distribution<double>( 0.7, 0.2 );

// 実際にサンプルデータを作ります
std::mt19937 eng( static_cast<unsigned int>( time( nullptr ) ) );
for( std::size_t i=0; i<kClass1NodeNum; ++i )
{
samples.push_back( node( kClass1Label, class1_x_generator( eng ), class1_y_generator( eng ) ) );
}
for( std::size_t i=0; i<kClass2NodeNum; ++i )
{
samples.push_back( node( kClass2Label, class2_x_generator( eng ), class2_y_generator( eng ) ) );
}


// libsvmに学習してもらいます
// libsvm用の学習データを作ります
svm_problem prob;
svm_node* prob_vec;
// 学習データの数
prob.l = samples.size();
// 各学習データのラベル
prob.y = new double[ prob.l ];
for( std::size_t i=0; i<samples.size(); ++i )
{
prob.y[i] = samples[i].label_;
}
// 各学習データのベクトル
/*
libsvmではスパースな特徴量も効率的に扱えるよう,一つの学習データ(ベクトル)はsvm_nodeの配列で表されます
svm_nodeはベクトル内の一つの軸の値を保持するもので,そのベクトルの軸の番号「index」(1から始まる)と値「value」をメンバに持ちます
二次元ベクトルの場合,軸はXとYで二つなので,二次元ベクトルを表現するのにsvm_node[2]という配列が必要です
しかし,これだとベクトルの終端がどこだか分からないため(libsvmではスパースなベクトルも扱えるため),
実際には最後に”終端”を表すsvm_nodeが必要です(この”終端”を表すsvm_nodeは,indexを「-1」とすることで処理されます)
よって,二次元ベクトル一つを表現するのに必要なのはsvm_node[3]の配列です
*/
prob_vec = new svm_node[ prob.l *(2+1) ];// 全てのベクトルが2次元,かつベクトルの句切れに1個必要なので,学習データ*3のsvm_nodeが必要
prob.x = new svm_node*[ prob.l ];
for( std::size_t i=0; i<samples.size(); ++i )
{
prob.x[i] = prob_vec+i*3;
prob.x[i][0].index = 1;
prob.x[i][0].value = samples[i].x_;
prob.x[i][1].index = 2;
prob.x[i][1].value = samples[i].y_;
prob.x[i][2].index = -1;
}
// 学習する識別器のパラメータ
svm_parameter param;
param.svm_type = C_SVC;// SVCとかSVRとか
param.kernel_type = LINEAR;// RBF(放射基底関数)カーネルとかLINEAR(線形)カーネルとかPOLY(多項式)カーネルとか
param.C = 8096;// SVMにおけるコスト:大きいほどハードマージン
param.gamma = 0.1;// カーネルとかで使われるパラメータ

// その他
param.coef0 = 0;
param.cache_size = 100;
param.eps = 1e-3;
param.shrinking = 1;
param.probability = 0;

// その他状況(svm_typeやカーネル)に応じて必要なもの
param.degree = 3;
param.nu = 0.5;
param.p = 0.1;
param.nr_weight = 0;
param.weight_label = nullptr;
param.weight = nullptr;

// 学習!
cout << "Ready to train ..." << endl;
svm_model* model = svm_train( &prob, &param );
cout << "Finished ..." << endl;


// predit training samples
int correct_count =0, wrong_count =0;
cout << "predict training samples ..." << endl;
for( std::size_t i=0; i<samples.size(); ++i )
{
svm_node test[3];
test[0].index = 1;
test[0].value = samples[i].x_;
test[1].index = 2;
test[1].value = samples[i].y_;
test[2].index = -1;
// libsvmにpredictしてもらう
const auto kPredictedLabel = static_cast<int>( svm_predict( model, test ) );
// 結果カウント
if ( kPredictedLabel == samples[i].label_ )
{
++correct_count;
}
else
{
++wrong_count;
}
}
cout << "done" << endl;
cout << "RESULT : correct=" << correct_count << " : wrong=" << wrong_count << endl;
cout << "Accuracy[%]=" << ( static_cast<double>(correct_count)/static_cast<double>(correct_count+wrong_count)*100.0 ) << endl;


#ifdef ENABLE_OPENCV
// 表示
// 表示するサイズ
const auto kHeight = 640;
const auto kWidth = 640;
// 真っ白なイメージを作る
cv::Mat img( kHeight, kWidth, CV_8UC3 );
img = cv::Scalar( 255, 255, 255 );
// 全てのピクセルで判定する
cout << "Rendering result ..." << endl;
for( std::size_t y=0; y<kHeight; ++y ) for( std::size_t x=0; x<kWidth; ++x )
{
// 現在のピクセルに対応するベクトル作る
svm_node test[3];
test[0].index = 1;
test[0].value = static_cast<double>(x) / static_cast<double>( kWidth );
test[1].index = 2;
test[1].value = static_cast<double>(y) / static_cast<double>( kHeight );
test[2].index = -1;
// libsvmにpredictしてもらう
const auto kPredictedLabel = static_cast<int>( svm_predict( model, test ) );
// 結果に依って色分けする
if ( kPredictedLabel == kClass1Label )
{
img.at<cv::Vec3b>( y, x ) = cv::Vec3b( 128, 128, 255 );
}
else
{
img.at<cv::Vec3b>( y, x ) = cv::Vec3b( 255, 128, 128 );
}
}
cout << "Rendering background done ..." << endl;
// サンプルデータを描画する
const auto kClass1Color = cv::Scalar( 0, 0, 255 );
const auto kClass2Color = cv::Scalar( 255, 0, 0 );
for( std::size_t i=0; i<samples.size(); ++i )
{
const auto kColor = ( samples[i].label_==kClass1Label ? kClass1Color : kClass2Color );
cv::circle( img, cv::Point(samples[i].x_*kWidth,samples[i].y_*kHeight), 2, kColor, -1 );
}
cout << "Rendering sample data done ..." << endl;
// サポートベクトルを描画する : ちょっと大きめの円で描画
const auto kSupportVectorNum = svm_get_nr_sv( model );
int* sv_indices = new int[ kSupportVectorNum ];
svm_get_sv_indices( model, sv_indices );
for( int i=0; i<kSupportVectorNum; ++i )
{
const auto kSampleIdx = sv_indices[i] -1;// 1から始まりらしい
const auto kColor = ( samples[kSampleIdx].label_==kClass1Label ? kClass1Color : kClass2Color );
cv::circle( img, cv::Point(samples[kSampleIdx].x_*kWidth,samples[kSampleIdx].y_*kHeight), 4, kColor, -1 );
}
cout << "Rendering support vectors done ..." << endl;

// 表示してまつ
cv::imshow( "result", img );
cv::waitKey();
#endif// ENABLE_OPENCV


// 後始末
svm_free_and_destroy_model( &model );
delete[] prob.y;
delete[] prob.x;
delete[] prob_vec;

return 0;
}


学習データを作るのに標準入りした「std::normal_distribution」とか惜しみなく使っております

コンパイル確認:
 MSVS2013,MSVS2012
 clang++3.3
 g++-4.6

さて,上記のプログラムを実行するとこんな図が得られます

svm_linear_chard.png

赤い点がカテゴリ1,青い点がカテゴリ2に対応する学習データです
点が大きいものはサポートベクトルです

上記のプログラムそのままだと線形カーネルでハードマージンよりの結果が得られます
識別器周辺の学習データのみがサポートベクトルとして選ばれていることに注意してください

じゃあ,もっとソフトなマージンにした場合は結果はどうなるかというと,
コストパラメータCを「0.1」として学習した時の図です
svm_linear_csoft.png

コストパラメータが大きい時に比べて,より多くの学習データがサポートベクトルとして選ばれていることに注意してください
(私見なんですが,コストパラメータを小さくするとスパースな解を生成するSVMと言えど学習データ全体を鑑みた結果になり,ちょうど学習データ全体を使った解をだすFisherのLDAと似ているような結果になりやすいです)

とりあえず,コストパラメータを下げると「境界付近のより広い範囲の学習データを考慮する」ことになるようです


さて,次は非常によく使われるRBFカーネルを使ってみましょう
「param.svm_type」に「LINEAR」ではなく「RBF」を指定すればRBFカーネルを使った識別器を生成します

RBFカーネルを使った際に重要なパラメータはコスト「C」とRBFカーネルのsigmaに対応する「gamma」です
とりあえずどちらも大きな値として8096を設定してみました
svm_rbf_toohard.png
恐ろしくフクザツなモデルになりました
しかし,学習データ全てに対して正しい識別ができているように見えます
(いわゆる過学習:複雑すぎるモデルを用いた時,学習データに過度に適合したモデルになってしまうこと)

RBFカーネルにおいてCとgammaを大きくすると複雑すぎるモデルが得られましたが,他のパラメータについても試してみましょう

Cは大きく,gammaを小さくしたとき
svm_rbf_chard_gsoft.png
ちょっと線形に似てきましたね

逆に.Cは小さくgammaを大きいままにしたとき
svm_rbf_csoft_ghard.png
最初に見たような,非常に複雑なモデルが得られました

C,gamma共に小さくしたとき
svm_rbf_csoft_gsoft.png
かなり,線形カーネルで学習したときに近い形となりました


上述Cとgammaそれぞれについて,4つのパラメータ設定をみてきました
そこからなんとなく,RBFにおけるgammaパラメータはモデル自体の複雑さに関連するパラメータと言えそうです
gammaが小さければ線形カーネルに近づき,gammaが大きければ非線形な識別器が得られるようです

今回は二次元ベクトルという,ベクトルの中でもかなり特殊な部類の次元についての実験でしたが,おおよそのパラメータに関する予想みたいなのは得ることができたようです


パラメータ調整について

さて,ここまで見てきておそらく大半の人が思ったでしょうが,SVMにおいて精度を高めたい場合
パラメータ調整は必須
です

コストパラメータとgammaパラメータに関して,いくつかの組み合わせを試して,一番よい精度を持ったパラメータを最終的な評価に使うというのが一般的です

「え? じゃあ学習データとは別にテストデータも用意しなくちゃいけないの?」
「でも具体的にどれくらいの組み合わせを探索する必要があるの…?」

その辺に興味を持った人は「グリッドサーチ」とかの単語で調べればでてくると思います
また,(前回の記事でも貼らせていただいたんですが)SlideShare -SVM実践ガイド (A Practical Guide to Support Vector Classification)-なども参考になると思います

上記二つに簡単に答えておくと,
 パラメータはログスケールで探索し,libsvmのグリッドサーチツールではコストは「2^-5~2^15」,gammaは「2^-15~2^3」程度の空間をサーチしています
 指数の値はステップ2(2ずつ値を増やしたり減らしたり)で進めるので,組み合わせ総数的には10*9=90程度です

 それぞれのパラメータの精度評価には,学習データをk個に分割し,k-1個を学習データ,1個をテストデータとして平均識別精度を使うk-fold cross validation(k分割交差検定)とかが簡単でよく使われます
 LibSVMではデフォルトは5分割の5-fold cross validation


おわりに

なんか,フツーにC++でLibSVM触って終わってしまいました,すいません
でも,実際に動いているのを見ると面白かったりしますし,これはこれでよいのではないかと勝手に思っています(
なお,更に弁明しておきますと,私は理論系で生きていけるレベルの人間ではないので,「SVMについて」の詳細な議論は全くできません
でも使う分には特に問題ないから世の中不思議だ

コードとしては紹介しませんでしたが,ここから画像用意してOpenCVで適当な特徴量抽出してLibSVMにぶっ込めば,簡単な画像認識が行えます
C++で画像認識がこんなに簡単に出来るような時代もなかなかだと思ってるので,興味がある人はちゃちゃっとやってみると面白いと思いますよ!

ちょっとしたことでLibSVMに悩んでいるユーザのお役に立てたらこれ幸い
それでは

…もっと短く終わる予定だったのに,書いてみたら案外長くて疲れた…時間の節約とは…


おまけ1:unique_ptrとか使ったC++コード

std::unique_ptrとかちゃんと使って書いてた時のC++のソースコード
std::make_uniqueとかがMSVS2012とかだと入ってなくて,素直に「new」と「delete」使って書き直す前のバージョンです
あとrange-based forとか
C++好きな人はこっちの方がまだしっくりくるんじゃないかな…
@20140108
コードがちょろっと間違っていたので修正,あとOpenCV使うか否かのマクロ追加
coefを可視化するコードも入ってるがあってるかは不明

    // OpenCV使えない場合はコメントアウト
#define ENABLE_OPENCV

// libsvm
#include <libsvm/svm.h>

#ifdef ENABLE_OPENCV
// opencv
#include <opencv2/opencv.hpp>
#endif// ENABLE_OPENCV

// その他STL
#include <iostream>
#include <vector>
#include <memory>
#include <functional>
#include <random>
#include <utility>


// サンプルデータを表すクラス
// 二次元ベクトル:ラベル(ノードが属するクラスのカテゴリ)を持つ
class node
{
public :
node( int label, double x, double y )
: label_( label ), x_( x ), y_( y )
{
}
// member variables
int label_;
double x_, y_;
};


int main( int, char*[] )
{
using namespace std;

// サンプルデータを適当に作る
// 今回は2クラスで,いずれも正規分布に従います
std::vector< node > samples;

// それぞれのクラスで作るサンプルデータの数です
const auto kClass1NodeNum = 100;
const auto kClass2NodeNum = 100;

// それぞれのクラスのラベルです,今回はクラス1は「1」,クラス2は「-1」としました
const auto kClass1Label = 1;
const auto kClass2Label = -1;

// それぞれのクラスのx,y軸のMeanとSigmaを入れて,その正規分布に従う乱数生成器を作ります
auto class1_x_generator = std::normal_distribution<double>( 0.3, 0.2 );
auto class1_y_generator = std::normal_distribution<double>( 0.3, 0.2 );

auto class2_x_generator = std::normal_distribution<double>( 0.7, 0.2 );
auto class2_y_generator = std::normal_distribution<double>( 0.7, 0.2 );

// 実際にサンプルデータを作ります
std::random_device rd;
std::mt19937 eng( rd() );
for( std::size_t i=0; i<kClass1NodeNum; ++i )
{
samples.push_back( node( kClass1Label, class1_x_generator( eng ), class1_y_generator( eng ) ) );
}
for( std::size_t i=0; i<kClass2NodeNum; ++i )
{
samples.push_back( node( kClass2Label, class2_x_generator( eng ), class2_y_generator( eng ) ) );
}


// libsvmに学習してもらいます
// libsvm用の学習データを作ります
svm_problem prob;
std::unique_ptr<double[]> prob_y;
std::unique_ptr<svm_node*[]> prob_node;
std::unique_ptr<svm_node[]> prob_vec;
// 学習データの数
prob.l = samples.size();
// 各学習データのラベル
prob_y = std::make_unique<double[]>( prob.l );
prob.y = prob_y.get();
for( std::size_t i=0; i<samples.size(); ++i )
{
prob.y[i] = samples[i].label_;
}
// 各学習データのベクトル
/*
libsvmではスパースな特徴量も効率的に扱えるよう,一つの学習データ(ベクトル)はsvm_nodeの配列で表されます
svm_nodeはベクトル内の一つの軸の値を保持するもので,そのベクトルの軸の番号「index」(1から始まる)と値「value」をメンバに持ちます
二次元ベクトルの場合,軸はXとYで二つなので,二次元ベクトルを表現するのにsvm_node[2]という配列が必要です
しかし,これだとベクトルの終端がどこだか分からないため(libsvmではスパースなベクトルも扱えるため),
実際には最後に”終端”を表すsvm_nodeが必要です(この”終端”を表すsvm_nodeは,indexを「-1」とすることで処理されます)
よって,二次元ベクトル一つを表現するのに必要なのはsvm_node[3]の配列です
*/
prob_node = std::make_unique<svm_node*[]>( prob.l );
prob_vec = std::make_unique<svm_node[]>( prob.l *(2+1) );// 全てのベクトルが2次元,かつベクトルの句切れに1個必要なので,学習データ*3のsvm_nodeが必要
prob.x = prob_node.get();
for( std::size_t i=0; i<samples.size(); ++i )
{
prob.x[i] = prob_vec.get()+i*3;
prob.x[i][0].index = 1;
prob.x[i][0].value = samples[i].x_;
prob.x[i][1].index = 2;
prob.x[i][1].value = samples[i].y_;
prob.x[i][2].index = -1;
}
// 学習する識別器のパラメータ
svm_parameter param;
param.svm_type = C_SVC;// SVCとかSVRとか
param.kernel_type = LINEAR;// RBF(放射基底関数)カーネルとかLINEAR(線形)カーネルとかPOLY(多項式)カーネルとか
param.C = 8096;// SVMにおけるコスト
param.gamma = 0.01;// カーネルとかで使われるパラメータ

// その他
param.coef0 = 0;
param.cache_size = 100;
param.eps = 1e-3;
param.shrinking = 1;
param.probability = 0;

// その他状況(svm_typeやカーネル)に応じて必要なもの
param.degree = 3;
param.nu = 0.5;
param.p = 0.1;
param.nr_weight = 0;
param.weight_label = nullptr;
param.weight = nullptr;

// 学習!
cout << "Ready to train ..." << endl;
std::unique_ptr<svm_model,std::function<void(svm_model*)>> model(
svm_train( &prob, &param ),
[]( svm_model* p ){ if (p){ svm_free_and_destroy_model(&p); } }
);
cout << "Finished ..." << endl;

// predit training samples
int correct_count =0, wrong_count =0;
cout << "predict training samples ..." << endl;
for( const auto& it : samples )
{
svm_node test[3];
test[0].index = 1;
test[0].value = it.x_;
test[1].index = 2;
test[1].value = it.y_;
test[2].index = -1;
// libsvmにpredictしてもらう
const auto kPredictedLabel = static_cast<int>( svm_predict( model.get(), test ) );
// 結果カウント
if ( kPredictedLabel == it.label_ )
{
++correct_count;
}
else
{
++wrong_count;
}
}
cout << "done" << endl;
cout << "RESULT : correct=" << correct_count << " : wrong=" << wrong_count << endl;
cout << "Accuracy[%]=" << ( static_cast<double>(correct_count)/static_cast<double>(correct_count+wrong_count)*100.0 ) << endl;


#ifdef ENABLE_OPENCV
// 表示
// 表示するサイズ
const auto kHeight = 640;
const auto kWidth = 640;
// 真っ白なイメージを作る
cv::Mat img( kHeight, kWidth, CV_8UC3 );
img = cv::Scalar( 255, 255, 255 );
// 全てのピクセルで判定する
cout << "Rendering result ..." << endl;
for( std::size_t y=0; y<kHeight; ++y ) for( std::size_t x=0; x<kWidth; ++x )
{
// 現在のピクセルに対応するベクトル作る
svm_node test[3];
test[0].index = 1;
test[0].value = static_cast<double>(x) / static_cast<double>( kWidth );
test[1].index = 2;
test[1].value = static_cast<double>(y) / static_cast<double>( kHeight );
test[2].index = -1;
// libsvmにpredictしてもらう
const auto kPredictedLabel = static_cast<int>( svm_predict( model.get(), test ) );
// 結果に依って色分けする
if ( kPredictedLabel == kClass1Label )
{
img.at<cv::Vec3b>( y, x ) = cv::Vec3b( 128, 128, 255 );
}
else
{
img.at<cv::Vec3b>( y, x ) = cv::Vec3b( 255, 128, 128 );
}
}
cout << "Rendering background done ..." << endl;
auto dimg = img.clone();
// サンプルデータを描画する
const auto kClass1Color = cv::Scalar( 0, 0, 255 );
const auto kClass2Color = cv::Scalar( 255, 0, 0 );
for( std::size_t i=0; i<samples.size(); ++i )
{
const auto kColor = ( samples[i].label_==kClass1Label ? kClass1Color : kClass2Color );
cv::circle( img, cv::Point(samples[i].x_*kWidth,samples[i].y_*kHeight), 2, kColor, -1 );
}
cout << "Rendering sample data done ..." << endl;
// サポートベクトルを描画する : ちょっと大きめの円で描画
const auto kSupportVectorNum = svm_get_nr_sv( model.get() );
auto sv_indices = std::make_unique<int[]>( kSupportVectorNum );
svm_get_sv_indices( model.get(), sv_indices.get() );
for( int i=0; i<kSupportVectorNum; ++i )
{
const auto kSampleIdx = sv_indices[i] -1;// 1から始まりらしい
const auto kColor = ( samples[kSampleIdx].label_==kClass1Label ? kClass1Color : kClass2Color );
cv::circle( img, cv::Point(samples[kSampleIdx].x_*kWidth,samples[kSampleIdx].y_*kHeight), 4, kColor, -1 );
}
cout << "Rendering support vectors done ..." << endl;

// 適当にcoef,各クラス毎の識別器の各SVのalphaっぽい,ホントか?
cout << "Rendering coef ..." << endl;
for( std::size_t i=0; i<kSupportVectorNum; ++i )
{
const auto kSampleIdx = sv_indices[i] -1;// 1から始まりらしい
const auto kColor = ( samples[kSampleIdx].label_==kClass1Label ? kClass1Color : kClass2Color );
const auto kW = std::abs( model->sv_coef[0][i] ) / param.C;
const auto kRadius = static_cast<int>( kW*4 ) +1;
cv::circle( dimg, cv::Point(samples[kSampleIdx].x_*kWidth,samples[kSampleIdx].y_*kHeight), kRadius, kColor, -1 );
}
cout << "Rendering coef done ..." << endl;

// 表示してまつ
cv::imshow( "result", img );
cv::imshow( "coef", dimg );
cv::waitKey();
#endif// ENABLE_OPENCV


return 0;
}



おまけ2:線形カーネルで二値分類の時,その射影軸ベクトルを求める

やってから,LIBSVMの特徴量の重みを見る - LIBSVMのモデルの読み方さんとネタ被りしていることに気付いた

線形カーネルで二値分類の時は,SVMの理論から簡単にその射影軸の基底ベクトルを求めることができます
意味的には各軸の重さを表すベクトルで,その向きは各クラス(カテゴリ)を識別するために使いますね

各軸の重み求めただけだとホントにネタ被りになるので,折角二次元のデータをランダムに作って学習させているので,実際の求めた射影軸ベクトルを使って予測を行うコード等も追加しています
また,libsvmのAPIを使う場合(LINEARカーネルではあるが,その計算はナイーブにサポートベクトルを用いている)と,射影軸ベクトルを使った予測とで処理時間的なのも比較しています

特に説明する気がないのでコードだけどん
 (面倒臭かったので時間計測にWindowsAPI使ってます,Win以外の人は時間計測あたりを書き換えるといいと動くと思います)
    // OpenCV使えない場合はコメントアウト
#define ENABLE_OPENCV

// 二値分類問題の線形の場合に限り,射影軸の単位ベクトルを求める処理のフラグ
#define EXTRACT_LINEAR_PROJECTION_WEIGHT
// ↑のフラグが有効な時のみ,LINEARカーネルでもサポートベクトルを用いた計算と線形で計算した時の処理時間を計測
// 面倒くさかったのでWin限定です
#define ENABLE_COMPARE_PREDICT_CALCULATION

// libsvm
#include <libsvm/svm.h>

#ifdef ENABLE_OPENCV
// opencv
#include <opencv2/opencv.hpp>
#endif// ENABLE_OPENCV

// その他STLとか
#include <ctime>
#include <iostream>
#include <vector>
#include <memory>
#include <functional>
#include <random>
#include <utility>

#ifdef ENABLE_COMPARE_PREDICT_CALCULATION
#include <Windows.h>
#include <mmsystem.h>

// 時間計測用のちらっとしたクラス
class elapsed_clock
{
public :
elapsed_clock()
{
QueryPerformanceFrequency( &freq_ );
QueryPerformanceCounter( &start_ );
}
double elapsed_msec() const
{
LARGE_INTEGER current;
QueryPerformanceCounter( &current );
return ( static_cast<double>( current.QuadPart-start_.QuadPart ) / static_cast<double>( freq_.QuadPart ) * 1000.0 );
}
private :
LARGE_INTEGER freq_, start_;
};
#endif// ENABLE_COMPARE_PREDICT_CALCULATION


// サンプルデータを表すクラス
// 二次元ベクトル:ラベル(ノードが属するクラスのカテゴリ)を持つ
class node
{
public :
node( int label, double x, double y )
: label_( label ), x_( x ), y_( y )
{
}
// member variables
int label_;
double x_, y_;
};


int main( int, char*[] )
{
using namespace std;

// サンプルデータを適当に作る
// 今回は2クラスで,いずれも正規分布に従います
std::vector< node > samples;

// それぞれのクラスで作るサンプルデータの数です
const auto kClass1NodeNum = 100;
const auto kClass2NodeNum = 100;

// それぞれのクラスのラベルです,今回はクラス1は「1」,クラス2は「-1」としました
const auto kClass1Label = 1;
const auto kClass2Label = -1;

// それぞれのクラスのx,y軸のMeanとSigmaを入れて,その正規分布に従う乱数生成器を作ります
auto class1_x_generator = std::normal_distribution<double>( 0.3, 0.2 );
auto class1_y_generator = std::normal_distribution<double>( 0.3, 0.2 );

auto class2_x_generator = std::normal_distribution<double>( 0.7, 0.2 );
auto class2_y_generator = std::normal_distribution<double>( 0.7, 0.2 );

// 実際にサンプルデータを作ります
std::mt19937 eng( static_cast<unsigned int>( time( nullptr ) ) );
for( std::size_t i=0; i<kClass1NodeNum; ++i )
{
samples.push_back( node( kClass1Label, class1_x_generator( eng ), class1_y_generator( eng ) ) );
}
for( std::size_t i=0; i<kClass2NodeNum; ++i )
{
samples.push_back( node( kClass2Label, class2_x_generator( eng ), class2_y_generator( eng ) ) );
}


// libsvmに学習してもらいます
// libsvm用の学習データを作ります
svm_problem prob;
svm_node* prob_vec;
// 学習データの数
prob.l = samples.size();
// 各学習データのラベル
prob.y = new double[ prob.l ];
for( std::size_t i=0; i<samples.size(); ++i )
{
prob.y[i] = samples[i].label_;
}
// 各学習データのベクトル
/*
libsvmではスパースな特徴量も効率的に扱えるよう,一つの学習データ(ベクトル)はsvm_nodeの配列で表されます
svm_nodeはベクトル内の一つの軸の値を保持するもので,そのベクトルの軸の番号「index」(1から始まる)と値「value」をメンバに持ちます
二次元ベクトルの場合,軸はXとYで二つなので,二次元ベクトルを表現するのにsvm_node[2]という配列が必要です
しかし,これだとベクトルの終端がどこだか分からないため(libsvmではスパースなベクトルも扱えるため),
実際には最後に”終端”を表すsvm_nodeが必要です(この”終端”を表すsvm_nodeは,indexを「-1」とすることで処理されます)
よって,二次元ベクトル一つを表現するのに必要なのはsvm_node[3]の配列です
*/
prob_vec = new svm_node[ prob.l *(2+1) ];// 全てのベクトルが2次元,かつベクトルの句切れに1個必要なので,学習データ*3のsvm_nodeが必要
prob.x = new svm_node*[ prob.l ];
for( std::size_t i=0; i<samples.size(); ++i )
{
prob.x[i] = prob_vec+i*3;
prob.x[i][0].index = 1;
prob.x[i][0].value = samples[i].x_;
prob.x[i][1].index = 2;
prob.x[i][1].value = samples[i].y_;
prob.x[i][2].index = -1;
}
// 学習する識別器のパラメータ
svm_parameter param;
param.svm_type = C_SVC;// SVCとかSVRとか
param.kernel_type = LINEAR;// RBF(放射基底関数)カーネルとかLINEAR(線形)カーネルとかPOLY(多項式)カーネルとか
param.C = 1;// SVMにおけるコスト:大きいほどハードマージン
param.gamma = 0.1;// カーネルとかで使われるパラメータ

// その他
param.coef0 = 0;
param.cache_size = 100;
param.eps = 1e-3;
param.shrinking = 1;
param.probability = 0;

// その他状況(svm_typeやカーネル)に応じて必要なもの
param.degree = 3;
param.nu = 0.5;
param.p = 0.1;
param.nr_weight = 0;
param.weight_label = nullptr;
param.weight = nullptr;

// 学習!
cout << "Ready to train ..." << endl;
svm_model* model = svm_train( &prob, &param );
cout << "Finished ..." << endl;


// predit training samples
int correct_count =0, wrong_count =0;
cout << "predict training samples ..." << endl;
for( std::size_t i=0; i<samples.size(); ++i )
{
svm_node test[3];
test[0].index = 1;
test[0].value = samples[i].x_;
test[1].index = 2;
test[1].value = samples[i].y_;
test[2].index = -1;
// libsvmにpredictしてもらう
const auto kPredictedLabel = static_cast<int>( svm_predict( model, test ) );
// 結果カウント
if ( kPredictedLabel == samples[i].label_ )
{
++correct_count;
}
else
{
++wrong_count;
}
}
cout << "done" << endl;
cout << "RESULT : correct=" << correct_count << " : wrong=" << wrong_count << endl;
cout << "Accuracy[%]=" << ( static_cast<double>(correct_count)/static_cast<double>(correct_count+wrong_count)*100.0 ) << endl;


#ifdef ENABLE_OPENCV
// 表示
// 表示するサイズ
const auto kHeight = 640;
const auto kWidth = 640;
// 真っ白なイメージを作る
cv::Mat img( kHeight, kWidth, CV_8UC3 );
img = cv::Scalar( 255, 255, 255 );
// 全てのピクセルで判定する
cout << "Rendering result ..." << endl;
for( std::size_t y=0; y<kHeight; ++y ) for( std::size_t x=0; x<kWidth; ++x )
{
// 現在のピクセルに対応するベクトル作る
svm_node test[3];
test[0].index = 1;
test[0].value = static_cast<double>(x) / static_cast<double>( kWidth );
test[1].index = 2;
test[1].value = static_cast<double>(y) / static_cast<double>( kHeight );
test[2].index = -1;
// libsvmにpredictしてもらう
const auto kPredictedLabel = static_cast<int>( svm_predict( model, test ) );
// 結果に依って色分けする
if ( kPredictedLabel == kClass1Label )
{
img.at<cv::Vec3b>( y, x ) = cv::Vec3b( 128, 128, 255 );
}
else
{
img.at<cv::Vec3b>( y, x ) = cv::Vec3b( 255, 128, 128 );
}
}
cout << "Rendering background done ..." << endl;
// サンプルデータを描画する
const auto kClass1Color = cv::Scalar( 0, 0, 255 );
const auto kClass2Color = cv::Scalar( 255, 0, 0 );
for( std::size_t i=0; i<samples.size(); ++i )
{
const auto kColor = ( samples[i].label_==kClass1Label ? kClass1Color : kClass2Color );
cv::circle( img, cv::Point(samples[i].x_*kWidth,samples[i].y_*kHeight), 2, kColor, -1 );
}
cout << "Rendering sample data done ..." << endl;
// サポートベクトルを描画する : ちょっと大きめの円で描画
const auto kSupportVectorNum = svm_get_nr_sv( model );
int* sv_indices = new int[ kSupportVectorNum ];
svm_get_sv_indices( model, sv_indices );
for( int i=0; i<kSupportVectorNum; ++i )
{
const auto kSampleIdx = sv_indices[i] -1;// 1から始まりらしい
const auto kColor = ( samples[kSampleIdx].label_==kClass1Label ? kClass1Color : kClass2Color );
cv::circle( img, cv::Point(samples[kSampleIdx].x_*kWidth,samples[kSampleIdx].y_*kHeight), 4, kColor, -1 );
}
cout << "Rendering support vectors done ..." << endl;

// 表示してまつ
cv::imshow( "result", img );
cv::waitKey();
#endif// ENABLE_OPENCV


#ifdef EXTRACT_LINEAR_PROJECTION_WEIGHT
if ( model->nr_class==2 && model->param.kernel_type==LINEAR )
{
cout << "EXTRACT_LINEAR_PROJECTION_WEIGHT : begin" << endl;

const auto kSupportVectorNum = svm_get_nr_sv( model );
int* sv_indices = new int[ kSupportVectorNum ];
svm_get_sv_indices( model, sv_indices );

// 射影軸のベクトル : クラス1が射影後正となるようにしています
std::vector<double> w(2,0);
for( int i=0; i<kSupportVectorNum; ++i )
{
const auto kClass1 = ( i < model->nSV[0] );
const auto kMul = ( kClass1 ? 1.0 : -1.0 );
const auto kCoef = std::abs( model->sv_coef[0][i] );
w[0] += kMul * kCoef * model->SV[i][0].value;
w[1] += kMul * kCoef * model->SV[i][1].value;
}
// normalize
{
double sum =0;
for( std::size_t i=0; i<w.size(); ++i )
{
sum += std::abs( w[i] );
}
if ( sum > 1e-10 )// biasは適当,div_by_zeroしなければよいので
{
for( std::size_t i=0; i<w.size(); ++i )
{
w[i] /= sum;
}
}
}
// 境界上にある点への位置ベクトルをもとめる
std::vector<double> b(2,0);
for( int i=0; i<kSupportVectorNum; ++i )
{
const auto kCoef = std::abs( model->sv_coef[0][i] );
b[0] += kCoef * model->SV[i][0].value;
b[1] += kCoef * model->SV[i][1].value;
}
// normalize
{
double sum =0;
for( std::size_t i=0; i<kSupportVectorNum; ++i )
{
sum += std::abs( model->sv_coef[0][i] );
}
if ( sum > 1e-10 )// biasは適当,div_by_zeroしなければよいので
{
for( std::size_t i=0; i<b.size(); ++i )
{
b[i] /= sum;
}
}
}
// 射影後の境界バイアス : は射影軸の単位ベクトル*境界上の点ベクトルですね
const auto kBias = w[0]*b[0] + w[1]*b[1];

cout << "w : (x,y)=(" << w[0] << "," << w[1] << ")" << endl;
cout << "b : (x,y)=(" << b[0] << "," << b[1] << ")" << endl;
cout << "Bias : "<< kBias << endl;

cout << "EXTRACT_LINEAR_PROJECTION_WEIGHT : end " << endl;

#ifdef ENABLE_OPENCV
{
// 表示
// 表示するサイズ
const auto kHeight = 640;
const auto kWidth = 640;
// 真っ白なイメージを作る
cv::Mat img( kHeight, kWidth, CV_8UC3 );
img = cv::Scalar( 255, 255, 255 );
// 全てのピクセルで判定する
cout << "Rendering result ..." << endl;
for( std::size_t y=0; y<kHeight; ++y ) for( std::size_t x=0; x<kWidth; ++x )
{
// 現在のピクセルに対応するベクトル作る
const auto kX = static_cast<double>(x) / static_cast<double>( kWidth );
const auto kY = static_cast<double>(y) / static_cast<double>( kHeight );
// 自分でpredictする
const auto kProjected = w[0]*kX + w[1]*kY;// これはdot : それがLINEARカーネルでしたね
const auto kPredicted = ( kProjected >= kBias );// 閾値による判定 :境界バイアスは,原点から超平面へおろした垂線の長さに一致します
// 結果に依って色分けする
if ( kPredicted )
{
img.at<cv::Vec3b>( y, x ) = cv::Vec3b( 128, 128, 255 );
}
else
{
img.at<cv::Vec3b>( y, x ) = cv::Vec3b( 255, 128, 128 );
}
}
cout << "Rendering background done ..." << endl;
// サンプルデータを描画する
const auto kClass1Color = cv::Scalar( 0, 0, 255 );
const auto kClass2Color = cv::Scalar( 255, 0, 0 );
for( std::size_t i=0; i<samples.size(); ++i )
{
const auto kColor = ( samples[i].label_==kClass1Label ? kClass1Color : kClass2Color );
cv::circle( img, cv::Point(samples[i].x_*kWidth,samples[i].y_*kHeight), 2, kColor, -1 );
}
cout << "Rendering sample data done ..." << endl;
// サポートベクトルを描画する : ちょっと大きめの円で描画
const auto kSupportVectorNum = svm_get_nr_sv( model );
int* sv_indices = new int[ kSupportVectorNum ];
svm_get_sv_indices( model, sv_indices );
for( int i=0; i<kSupportVectorNum; ++i )
{
const auto kSampleIdx = sv_indices[i] -1;// 1から始まりらしい
const auto kColor = ( samples[kSampleIdx].label_==kClass1Label ? kClass1Color : kClass2Color );
cv::circle( img, cv::Point(samples[kSampleIdx].x_*kWidth,samples[kSampleIdx].y_*kHeight), 4, kColor, -1 );
}
cout << "Rendering support vectors done ..." << endl;
// 境界バイアスの元になった点ベクトルを黄色で表示
cv::circle( img, cv::Point(b[0]*kWidth,b[1]*kHeight), 4, cv::Scalar(0,255,255), -1 );

// 表示してまつ
cv::imshow( "ex_linear_result", img );
cv::waitKey();
}
#endif// ENABLE_OPENCV

#ifdef ENABLE_COMPARE_PREDICT_CALCULATION
{
const auto kTestNum = 1000000;
cout << "compare calculation cost : test_num=" << kTestNum << " ..." << endl;

cout << "making random tests ..." << endl;
std::vector<node> tests;
auto test_x_generator = std::normal_distribution<double>( 0.5, 0.5 );
auto test_y_generator = std::normal_distribution<double>( 0.5, 0.5 );

svm_node* nodes = new svm_node[kTestNum*3 +1];
for( int i=0; i<kTestNum; ++i )
{
nodes[i*3 +0].index = 1;
nodes[i*3 +0].value = test_x_generator(eng);
nodes[i*3 +1].index = 2;
nodes[i*3 +1].value = test_y_generator(eng);
nodes[i*3 +2].index = -1;
}
// for result
std::vector<int> prediction_libsvm(kTestNum), prediction_linear(kTestNum);

// test
cout << "TEST : libsvm_model" << endl;
{
elapsed_clock eclock;
for( int i=0; i<kTestNum; ++i )
{
prediction_libsvm[i] = static_cast<int>( svm_predict( model, nodes +i*3 ) );
}
const auto kElapsed = eclock.elapsed_msec();
cout << "ELAPSED : " << kElapsed << "msec" << endl;
}
cout << "TEST : linear_model" << endl;
{
elapsed_clock eclock;
for( int i=0; i<kTestNum; ++i )
{
// なんか単純にやり過ぎるとフェアじゃない気がしたので,svm_node形式でアクセス
// これでも判定部分がないし,判定による分岐予測ミスのペナルティとかがつかないので,
// こっちのが早くなっちゃうんですが,多分そのレベル以上にlibsvm_modelが遅いでいいと思います
prediction_linear[i] = ( nodes[i*3+0].value*w[0] + nodes[i*3+1].value*w[1] ) >= kBias;
}
const auto kElapsed = eclock.elapsed_msec();
cout << "ELAPSED : " << kElapsed << "msec" << endl;
}

cout << "compare prediction result ..." << endl;
{
int same =0;
for( int i=0; i<kTestNum; ++i )
{
if ( ( prediction_libsvm[i]==kClass1Label ) == prediction_linear[i] )
{
++same;
}
}
cout << "total predicted vectors (test num) : " << kTestNum << endl;
cout << "same prediction : " << same << endl;
}

// release
delete[] nodes;
}
#endif// ENABLE_COMPARE_PREDICT_CALCULATION
}
else
{
cout << "EXTRACT_LINEAR_PROJECTION_WEIGHT : model is not linear binary classifier" << endl;
}
#endif// EXTRACT_LINEAR_PROJECTION_WEIGHT


// 後始末
svm_free_and_destroy_model( &model );
delete[] prob.y;
delete[] prob.x;
delete[] prob_vec;

return 0;
}


OpenCVによる可視化結果はこんな感じ
libsvm_ex_linear_result.jpg
左図は既に今までのコードで示した,libsvmのモデルによる予測結果&サポートベクトル等の可視化です.
右図はサポートベクトルの可視化は同じですが,薄い赤と青でしめされる空間全体の予測結果が異なります.
また,黄色い点はサポートベクトルの平均を表しています(これは判別境界上の点となります)

実は微妙に結果が異なるのですが,多分浮動小数点の計算誤差だということにしておきます
 (ホントはdouble程度の精度だと見えるレベルの計算誤差はでないハズなんですが…)

また,予測の処理時間については,手元の環境だと
 ・libsvmのAPIを用いた場合 : 13453.1msec
 ・自分で射影ベクトル使った場合 : 10.3msec
っていう結果になりました

手元の環境っていうのは次
 ・Win7x64 with IntelCore i5 1.8GHz CPU
 ・Compiler : MSVC2013pro, -O2
 ・テストサンプル数 : 1,000,000
まぁ,何回か回して平均とったわけではないので,安定した結果じゃないとは思うけど,参考程度に

線形カーネル使う人は,上記みたいにいちいち射影ベクトルとかバイアス計算するの面倒だからLIBLINEAR使う方をオススメしておきます,個人的には
暇があったらこのブログにLIBLINEARを使ったコードでも追記するかもしれない

関連記事
スポンサーサイト



コメント

コメントの投稿


管理者にだけ表示を許可する

トラックバック

トラックバック URL
http://fe0km.blog.fc2.com/tb.php/95-5b2f62a9
この記事にトラックバックする(FC2ブログユーザー)