Miacisでは一つの局面に対応する置換表エントリが持つ変数として、以下のようなものを保持している。(hash_table.hppから一部抜粋し見やすく改変)
struct HashEntry { int32_t sum_N; //探索回数の合計 int32_t virtual_sum_N; //バーチャルロスの合計 //以下vectorとなっているものは全てmovesのサイズと同じサイズになる std::vector<Move> moves; //行動 std::vector<Index> child_indices; //各行動を取った後の置換表エントリに対応するインデックス std::vector<int32_t> N; //各行動を探索した回数 std::vector<int32_t> virtual_N; //バーチャルロス std::vector<FloatType> nn_policy; //ニューラルネットワークの出力方策 ValueType value; //価値。漸進的に更新され、常にこのノードを根とする部分木内の価値の平均となる bool evaled; //ニューラルネットワークによる評価が行われたかを示すフラグ std::mutex mutex; //排他制御用のmutex //ハッシュ管理用の変数 int64_t hash; //局面に対応するハッシュ値 uint16_t turn_number; //手数。ハッシュ値だけでは衝突頻度が高いので uint16_t age; //置換表のageと照らし合わせて現在の情報かどうかを判断する変数 };
ここでコメントにもある通り、std::vector
の形式で保持しているものは、この局面の合法手数だけ要素を持つことになる。基本的に将棋は終盤になると(持ち駒が増えると)平均合法手数が大きくなるため、長時間の対局を行ったとき終盤でメモリが溢れそうになってしまった。
ねね将棋では上位16手のみを保存していると聞いたので、真似してそのような機能を実装した。なお、Miacisでは詰み探索を置換表に直接書き込む都合上、ルートにおいては合法手を全て展開し、それ以降の局面のみ、ニューラルネットワークのPolicy確率が大きい順に上位N手のみを残すようにした。
対局
- Miacis : 1手1秒
- YaneuraOu/Kristallweizen(4Thread, 定跡オフ) : 1手0.25秒
保持する指し手 | 対局数 | 勝数 | 引き分け数 | 敗数 | 勝率 | 相対Eloレート(±1σ) |
---|---|---|---|---|---|---|
全て | 1000 | 535 | 2 | 463 | 53.6% | 25.11±22.73 |
上位16手 | 500 | 303 | 0 | 197 | 60.6% | 74.79±33.24 |
「全て」の方の結果は少し前のものなので今の実行バイナリと完全に同じものではないとは思われるが、性能的にはほぼ変わっていないはずであり、そのときの結果よりも今回上位16手に絞った場合のほうがやや勝率が高かった。誤差の範囲内かもしれないが、性能が落ちているということはなさそうなので今後も上位16手に絞ることにする。
上位16手に絞って性能が上がる要因があるとすれば、UCB選択で合法手を全てループするのが高速化されることが考えられる。実際に10秒の探索を10回測定して平均値を出してみると、
保持する指し手 | 初期局面でのNPS | ある中盤局面でのNPS |
---|---|---|
全て | 15685.8 | 13977.5 |
上位16手 | 16262.9 | 15806.4 |
となった。ややNPSが上がるメリットと、N手以降に良い手があり見落とすデメリットのバランスを見てNを決めれば良さそうだ。