Bonanza4の評価関数はどのようなものか【駒割編】

■ Bonanza4の評価関数はどのようなものか【駒割編】


Bonanza4の局面の評価関数は俗に「3駒相対」と呼ばれているが、その正確なところは十分にまで理解されているとは言い難い。


そこで、今回はその仕組みについて詳しく調べていく。


評価関数のソースはevaluate.cにある。重要なのは次の3つである。


・eval_material : 駒割を計算するための関数
・evaluate : 評価関数本体
・make_list : 評価関数の下準備として、駒の位置を格納するための関数


それぞれについて解説していくが、今回解説するのは、eval_materialのみ。


■ eval_material


この関数では駒割を計算する。要するに、先手の歩であれば、先手の盤上の歩の数 + 先手の持ち駒の歩の数を足して、その合計に、歩1枚の価値を掛け算する。


そして、それぞれの駒の値を足し併せる。
先手の駒はプラス、後手の駒はマイナスとして扱う。そうすると駒割が求まる。


これがこの関数が返す値の意味である。具体的なコードを見ていこう。

  // BB_BPAWN.PopuCount() は、先手の歩の盤面上の枚数
  // HAND_B.I2HandPawn() は、先手の手駒の歩の枚数

  itemp     = PopuCount( BB_BPAWN )   + (int)I2HandPawn( HAND_B );
  itemp    -= PopuCount( BB_WPAWN )   + (int)I2HandPawn( HAND_W );
  material  = itemp * p_value[15+pawn]; // // これは歩一枚の評価値

PopuCountは、立っているビットの数を数える関数で、bitop.h / bitop.c で、次のように実装されている。

#define PopuCount(bb)            popu_count012( bb.p[0], bb.p[1], bb.p[2] )

int
popu_count012( unsigned int u0, unsigned int u1, unsigned int u2 )
{
  int counter = 0;
  while ( u0 ) { counter++;  u0 &= u0 - 1U; }
  while ( u1 ) { counter++;  u1 &= u1 - 1U; }
  while ( u2 ) { counter++;  u2 &= u2 - 1U; }
  return counter;
}

見ての通りビットの立っている箇所の回数だけループを回る実装になっており、立っているビットが多い場合は、お世辞にも高速とは言い難い。いまならSSE4のPOPCNT命令を使うべきだろう。


しかし、Bonanzaの場合、eval_materialは盤面の初期化のときに一度呼び出されるだけである。駒を打つときには、手駒が盤上の駒に変換されるだけなので、このとき、駒割の値(material)は変化しない。


駒割が変化するのは、駒を捕獲したときと駒を成ったときのみである。Bonanzaでは、このとき、その駒の分を差分計算するように実装されているので、思考中に評価値の計算のためにPopuCount関数が呼び出されることはない。ゆえに、PopuCountは、あまり高速でなくとも問題はない。


■ p_valueの配列の内訳


さきほど引用したソースでは、p_valueの配列を次のようにアクセスしていた。

  material  = itemp * p_value[15+pawn]; // // これは歩一枚の評価値

この配列は、ini.cにて次のように配列全体を0で初期化したあとに駒の価値を代入してある。

  for ( i = 0; i < 31; i++ ) { p_value[i]       = 0; }
  …

  // 駒の価値
  p_value[15+pawn]       = DPawn;
  p_value[15+lance]      = DLance;
  p_value[15+knight]     = DKnight;
  p_value[15+silver]     = DSilver;
  p_value[15+gold]       = DGold;
  p_value[15+bishop]     = DBishop;
  p_value[15+rook]       = DRook;
  p_value[15+king]       = DKing;
  p_value[15+pro_pawn]   = DProPawn;
  p_value[15+pro_lance]  = DProLance;
  p_value[15+pro_knight] = DProKnight;
  p_value[15+pro_silver] = DProSilver;
  p_value[15+horse]      = DHorse;
  p_value[15+dragon]     = DDragon;

DPawn,DLance,…,DDragonの値は、param.hで定義されており、このparam.hというファイル自体は、ボナメソで学習したときに自動的に生成される。


特に解説の必要はないと思うが、pawn = 歩 , lance = 香 , knight = 桂 , silver = 銀 , gold = 金 , bishop = 角 , rook = 飛 , king = 王 , pro = 成 , horse = 馬 , dragon = 龍 である。


これらの定数は、shogi.hで次のように定義されている。ただし、emptyという駒が存在しないことを表わす定数は、C++用の同名の関数と被って、C++のソースとしてコンパイルしたいときに困るので次のようにp_emptyと私がリネームした。

// 駒。成は+8になるようにしておく
enum { promote = 8, p_empty = 0,
       pawn, lance, knight, silver, gold, bishop, rook, king, pro_pawn,
       pro_lance, pro_knight, pro_silver, piece_null, horse, dragon };

見ての通り、pawn = 1であり、p_value[15+pawn]とは、pawn[16]のことである。
どうも、p_value[0]〜p_value[15]は使用していないように見えるが、ini.cのset_derivative_paramで次のように設定されている。

  p_value[15-pawn]          = p_value[15+pawn];
  p_value[15-lance]         = p_value[15+lance];
  p_value[15-knight]        = p_value[15+knight];
  p_value[15-silver]        = p_value[15+silver];
  p_value[15-gold]          = p_value[15+gold];
  p_value[15-bishop]        = p_value[15+bishop];
  p_value[15-rook]          = p_value[15+rook];
  p_value[15-king]          = p_value[15+king];
  p_value[15-pro_pawn]      = p_value[15+pro_pawn];
  p_value[15-pro_lance]     = p_value[15+pro_lance];
  p_value[15-pro_knight]    = p_value[15+pro_knight];
  p_value[15-pro_silver]    = p_value[15+pro_silver];
  p_value[15-horse]         = p_value[15+horse];
  p_value[15-dragon]        = p_value[15+dragon];

私には意味がよくわからないが、先手の駒を正の数、後手の駒を負の数として表現したときに、p_value[15 + koma] で、(p_value[abs(koma)]のように絶対値を計算しなくとも)その駒の価値が算出できるところにメリットがあるのかも知れない。


■ 駒割の差分評価


では、駒割の差分評価はどこでどのように行なわれるのだろうか?


駒割を保持する変数は、MATERIALというマクロで表現される。


makemove.cで指し手を進めるが、そのときに捕獲した駒が確定するので、次のようにしてp_value配列は見ずに、駒割の変化分を引き算している。CapBのBはblack(先手)用の意味であり、CapWというマクロもだいたい以下のものと同様に書かれている。

#define CapB( PIECE, piece, pro_piece )                   \
          Xor( to, BB_B ## PIECE );                       \
          HASH_KEY  ^= ( b_ ## pro_piece ## _rand )[to];  \
          HAND_W    += flag_hand_ ## piece;               \
          MATERIAL  -= MT_CAP_ ## PIECE
#define NocapProB( PIECE, PRO_PIECE, piece, pro_piece )       \
          Xor( from, BB_B ## PIECE );                         \
          Xor( to,   BB_B ## PRO_PIECE );                     \
          HASH_KEY    ^= ( b_ ## pro_piece ## _rand )[to]     \
                       ^ ( b_ ## piece     ## _rand )[from];  \
          MATERIAL    += MT_PRO_ ## PIECE;                    \
          BOARD[to] = pro_piece

ここに出てくるMT_CAP_PAWNやMT_PRO_PAWNという定数はshogi.hで次のように定義してある。

#  define MT_CAP_PAWN       ( DPawn      + DPawn )
#  define MT_CAP_LANCE      ( DLance     + DLance )
#  define MT_CAP_KNIGHT     ( DKnight    + DKnight )
#  define MT_CAP_SILVER     ( DSilver    + DSilver )
…
#  define MT_PRO_PAWN       ( DProPawn   - DPawn )
#  define MT_PRO_LANCE      ( DProLance  - DLance )
#  define MT_PRO_KNIGHT     ( DProKnight - DKnight )
#  define MT_PRO_SILVER     ( DProSilver - DSilver )
…

相手の歩を1枚捕獲すると、相手の歩が1つ減り、自分の持ち駒として歩が1枚増えるので、歩2枚分に相当する価値だけ駒割(material)の値が変化することに注意しよう。


よって、歩1枚捕獲したときの価値(MT_CAP_PAWN)は、歩1枚の価値の2倍をdefineしてある。


■ まとめ


今回は、Bonanzaの評価関数のうち、駒割の評価部分を調べた。手駒と盤上の駒とが同じ価値というのは意外だったかも知れない。


次回は評価関数の残りの部分を詳しく見ていく。