C++のTemplateを用いた評価関数の評価・学習ルーチンの実装
選手権では「選手権後にパラメータ学習を始めたい」って人が何人かいて、じゃぁ評価関数の実装をどうしよう? って話をしていました。
んで、「それならGPS将棋なりBonanzaなりKnightCapなりのソース読めば良いんじゃね?」だと芸がないので、GA将では色々試した結果、現在はこういう実装です、って話をしたいと思います。
まず一番最初に思い付くのは、評価と学習を別の関数にするやり方。例えばこんな感じ*1。
// 局面positionの、先手にとっての評価値を返す関数。 double evaluate( Position *const position ) { double score = 0.0; for( 盤上の全ての駒 ) { const double s = 先手の駒か ? 1.0 : -1.0; score += s * 駒の価値; }// for(...) for( 先手と後手 ) { const double s = 先手か ? 1.0 : -1.0; for( 全ての持ち駒 ) { score += s * 持ち駒の価値; }// for(...) }// for(...) return score; }// evaluate(...) // 局面positionの、先手にとっての評価値をtargetに近付ける double learn( Position *const position, const double target ) { // 評価値の誤差 const double error = target - evalute( position ); // 誤差を元にパラメータ調整の係数を決める double f = error * 学習率とか色々 for( 盤上の全ての駒 ) { const double s = 先手の駒か ? 1.0 : -1.0; 駒の価値 += f * s; }// for(...) for( 先手と後手 ) { const double s = 先手か ? 1.0 : -1.0; for( 全ての持ち駒 ) { 持ち駒駒の価値 += f * s; }// for(...) }// for(...) }// learn(...)
ただ、これは2つ悪い点があります。
- 同じ制御構造を2つ書かないといけない。
- 美しくない。
特に1は致命的で、評価項目が増えるに従いメンテもデバッグも大変になっていきます。私は"+="を"*="と書いてしまって頭を抱えた事があります。
ところで、evaluate()とlearn()はループ内の処理が異なるだけで、制御構造は全く同じです*2。
それならと、evaluate()とlearn()を一つの関数にまとめて、仮想関数を使うなりモード切替のパラメータを与えるなりして処理を切り替えた事もあります。
が、仮想関数は関数呼び出しのオーバーヘッドが大きくて遅くなりましたし、モード切替は書くのもメンテも面倒です。
という訳で、最終的にはテンプレートを使う事にしました。ちょっと長くなりますが、動作が速くてメンテも楽。現在はこの方式です。
// 局面評価用クラス class EvaluateProcessor { public: // 評価値 double score; EvaluateProcessor() { this->score = 0.0; }// EvaluateProcessor() void process( パラメータ *const param, const double featureQuantity ) { this->score += paramの値 * featureQuantity; }// process(...) }// EvaluateProcessor // 学習用クラス class LearnProcessor { public: // 学習時に特徴量にかける係数 const double factor; LearnProcessor( const double factor_in ) : factor( factor_in ) { /* NOP */ }// LearnProcessor() void process( パラメータ *const param, const double featureQuantity ) { paramの値 += this->factor * featureQuantity; }// process(...) }// LearnProcessor template<class PROCESSOR> void forEach( Position *const position, PROCESSOR *const p ) { for( 盤上の全ての駒 ) { const double s = 先手の駒か ? 1.0 : -1.0; p->process( &駒の価値, s ); }// for(...) for( 先手と後手 ) { const double s = 先手か ? 1.0 : -1.0; for( 全ての持ち駒 ) { p->process( &持ち駒の価値, s ); }// for(...) }// for(...) }// forEach(...) // 局面positionの、先手にとっての評価値を返す関数。 double evaluate( Position *const position ) { EvaluateProcessor ep; // 局面評価 forEach( position, &ep ); return ep.score; }// evaluate(...) // 局面positionの、先手にとっての評価値をtargetに近付ける double learn( Position *const position, const double target ) { // 評価値の誤差 const double error = target - evalute( position ); // 誤差を元にパラメータ調整の係数を決めて、プロセッサに渡す LearnProcessor lp( error * 学習率とか色々 ); // 学習 forEach( position, &lp ); }// learn(...)
多分どっかのソース読めばこれと同じ(か、多分もっと良い)方法があるのですが、とりあえず私の現状はこんな感じです。
願わくば、この記事が少しでも参考になります様に。