dlshogiは今までAperyから派生したelmo_for_learnのソースを使用していましたが、最新のAperyで修正されたバグの修正を取り込むため使用ライブラリをAperyに変更しました。
入玉宣言に修正が入っていたので、そこだけ取り込むつもりでしたが、ついでにすべてのソースを最新にしました。
また、今までLEARNを有効にしてビルドしていたので、枝狩りが少ない状態になっており長手数の詰みが見つけられない局面があったため、対局用ではLEARNをオフにしてビルドすることにしました。
dlshogiは今までAperyから派生したelmo_for_learnのソースを使用していましたが、最新のAperyで修正されたバグの修正を取り込むため使用ライブラリをAperyに変更しました。
入玉宣言に修正が入っていたので、そこだけ取り込むつもりでしたが、ついでにすべてのソースを最新にしました。
また、今までLEARNを有効にしてビルドしていたので、枝狩りが少ない状態になっており長手数の詰みが見つけられない局面があったため、対局用ではLEARNをオフにしてビルドすることにしました。
世界コンピュータ将棋選手権のアピール文章にも書いたが、マルチGPUで動かす場合、GPUごとに異なるモデルをロードすることで、モデルごとに誤る確率が独立とすると複数モデルが同時に誤る確率は、単一のモデルを使用する場合より低くなるため精度の向上が期待できる。
世界コンピュータ将棋選手権では、教師ありで学習した3つのモデルを使用したが、どれもelmoで生成したデータを使用しており、学習局面数の差しかなくあまり効果がなかったかもしれない。
elmoとは違う系統の教師局面で学習したモデルを使えば、モデルごとの独立性が高まる。
3駒関係の評価関数でも、Aperyとやねうら王の評価関数で重み付き平均をとるということが行われており、効果を上げている。
そこで、やねうら王で生成した教師局面からもモデルの学習を行えるようにしたい。
計算リソースが足りないため、自己対局による強化学習はゼロからではなく事前学習したモデルから開始する予定だが、その際にも2つの系統のモデルうまく使えないかと考えている。
dlshogiはベースにAperyから派生したelmo_for_learnを使用しているので、やねうら王で生成した教師局面の読み込みには対応していない。
そこで、一部ソースを追加してやねうら王で生成した教師局面を読み込めるようにした。
実装とテスト方法について、メモを残しておく。
elmoで使用されている教師局面は、HuffmanCodedPosAndEval(hcpe)という形式で保存されている。
hcpeは、局面をハフマン符号で圧縮したHuffmanCodedPos(hcp)と評価値、指し手、勝敗から構成されている。
hcpは、手番と王の座標、盤面の各座標の駒と持ち駒から構成されている。
各座標の駒と持ち駒は、ハフマン符号で圧縮されて1~8bitで表現されている。
やねうら王の教師局面は、PackedSfenValueという形式で保存されている。
PackedSfenValueは、局面をハフマン符号で圧縮したPackedSfenと評価値、指し手、手数、勝敗から構成されている。
PackedSfenは、hcpと同じ情報から構成されているが、ハフマン符号がelmo(Apery)とは異なる。
つまり、ハフマン符号を変更すれば、ほぼ同じコードでPackedSfenValueを読み込むことができる。
elmo(Apery)では、ハフマン符号は、position.cppで以下のように定義されている。
const HuffmanCode HuffmanCodedPos::boardCodeTable[PieceNone] = { {Binary< 0>::value, 1}, // Empty {Binary< 1>::value, 4}, // BPawn {Binary< 11>::value, 6}, // BLance {Binary< 111>::value, 6}, // BKnight {Binary< 1011>::value, 6}, // BSilver {Binary< 11111>::value, 8}, // BBishop {Binary< 111111>::value, 8}, // BRook {Binary< 1111>::value, 6}, // BGold {Binary< 0>::value, 0}, // BKing 玉の位置は別途、位置を符号化する。使用しないので numOfBit を 0 にしておく。 {Binary< 1001>::value, 4}, // BProPawn {Binary< 100011>::value, 6}, // BProLance {Binary< 100111>::value, 6}, // BProKnight {Binary< 101011>::value, 6}, // BProSilver {Binary< 10011111>::value, 8}, // BHorse {Binary< 10111111>::value, 8}, // BDragona {Binary< 0>::value, 0}, // 使用しないので numOfBit を 0 にしておく。 {Binary< 0>::value, 0}, // 使用しないので numOfBit を 0 にしておく。 {Binary< 101>::value, 4}, // WPawn {Binary< 10011>::value, 6}, // WLance {Binary< 10111>::value, 6}, // WKnight {Binary< 11011>::value, 6}, // WSilver {Binary< 1011111>::value, 8}, // WBishop {Binary< 1111111>::value, 8}, // WRook {Binary< 101111>::value, 6}, // WGold {Binary< 0>::value, 0}, // WKing 玉の位置は別途、位置を符号化する。 {Binary< 1101>::value, 4}, // WProPawn {Binary< 110011>::value, 6}, // WProLance {Binary< 110111>::value, 6}, // WProKnight {Binary< 111011>::value, 6}, // WProSilver {Binary< 11011111>::value, 8}, // WHorse {Binary< 11111111>::value, 8}, // WDragon }; // 盤上の bit 数 - 1 で表現出来るようにする。持ち駒があると、盤上には Empty の 1 bit が増えるので、 // これで局面の bit 数が固定化される。 const HuffmanCode HuffmanCodedPos::handCodeTable[HandPieceNum][ColorNum] = { {{Binary< 0>::value, 3}, {Binary< 100>::value, 3}}, // HPawn {{Binary< 1>::value, 5}, {Binary< 10001>::value, 5}}, // HLance {{Binary< 11>::value, 5}, {Binary< 10011>::value, 5}}, // HKnight {{Binary< 101>::value, 5}, {Binary< 10101>::value, 5}}, // HSilver {{Binary< 111>::value, 5}, {Binary< 10111>::value, 5}}, // HGold {{Binary< 11111>::value, 7}, {Binary< 1011111>::value, 7}}, // HBishop {{Binary< 111111>::value, 7}, {Binary< 1111111>::value, 7}}, // HRook };
やねうら王ではハフマン符号は、extra/sfen_packer.cppにコメントで記載されている。コメントに合わせて、Aperyのposition.cppに以下の通りハフマン符号を定義する。
const HuffmanCode PackedSfen::boardCodeTable[PieceNone] = { { Binary< 0>::value, 1 }, // Empty { Binary< 1>::value, 4 }, // BPawn { Binary< 11>::value, 6 }, // BLance { Binary< 1011>::value, 6 }, // BKnight { Binary< 111>::value, 6 }, // BSilver { Binary< 11111>::value, 8 }, // BBishop { Binary< 111111>::value, 8 }, // BRook { Binary< 1111>::value, 6 }, // BGold { Binary< 0>::value, 0 }, // BKing 玉の位置は別途、位置を符号化する。使用しないので numOfBit を 0 にしておく。 { Binary< 101>::value, 4 }, // BProPawn { Binary< 10011>::value, 6 }, // BProLance { Binary< 11011>::value, 6 }, // BProKnight { Binary< 10111>::value, 6 }, // BProSilver { Binary< 1011111>::value, 8 }, // BHorse { Binary< 1111111>::value, 8 }, // BDragona { Binary< 0>::value, 0 }, // 使用しないので numOfBit を 0 にしておく。 { Binary< 0>::value, 0 }, // 使用しないので numOfBit を 0 にしておく。 { Binary< 1001>::value, 4 }, // WPawn { Binary< 100011>::value, 6 }, // WLance { Binary< 101011>::value, 6 }, // WKnight { Binary< 100111>::value, 6 }, // WSilver { Binary< 10011111>::value, 8 }, // WBishop { Binary< 10111111>::value, 8 }, // WRook { Binary< 101111>::value, 6 }, // WGold { Binary< 0>::value, 0 }, // WKing 玉の位置は別途、位置を符号化する。 { Binary< 1101>::value, 4 }, // WProPawn { Binary< 110011>::value, 6 }, // WProLance { Binary< 111011>::value, 6 }, // WProKnight { Binary< 110111>::value, 6 }, // WProSilver { Binary< 11011111>::value, 8 }, // WHorse { Binary< 11111111>::value, 8 }, // WDragon }; const HuffmanCode PackedSfen::handCodeTable[HandPieceNum][ColorNum] = { { { Binary< 0>::value, 3 },{ Binary< 100>::value, 3 } }, // HPawn { { Binary< 1>::value, 5 },{ Binary< 10001>::value, 5 } }, // HLance { { Binary< 101>::value, 5 },{ Binary< 10101>::value, 5 } }, // HKnight { { Binary< 11>::value, 5 },{ Binary< 10011>::value, 5 } }, // HSilver { { Binary< 111>::value, 5 },{ Binary< 10111>::value, 5 } }, // HGold { { Binary< 1111>::value, 7 },{ Binary< 1001111>::value, 7 } }, // HBishop { { Binary< 11111>::value, 7 },{ Binary< 1011111>::value, 7 } }, // HRook };
AperyのPositionクラスに、PackedSfenから局面を読み込めるメソッドを追加する。
bool set(const PackedSfen& sfen, Thread* th);
setメソッドは、HuffmanCodedPosから局面を読み込むメソッドをほぼ流用して実装した。
これで、PackedSfenValueの読み込みが可能になる。
指し手は、形式が異なるため以下のようにしてApery形式に変換を行う。
// 指し手 bit0..6 = 移動先のSquare、bit7..13 = 移動元のSquare(駒打ちのときは駒種)、bit14..駒打ちか、bit15..成りか u16 bestMove16 = psv->move & 0x7f; // 移動先 if ((psv->move & 0x4000) == 0) { // 移動 bestMove16 |= psv->move & 0x3f80; } else { // 駒打ち bestMove16 |= (((psv->move >> 7) & 0x7f) + SquareNum - 1) << 7; } // 成り if ((psv->move & 0x8000) != 0) bestMove16 |= 0x4000; const Move move = Move(bestMove16);
やねうら王を使ってテスト用の教師局面を作成した。
やねうら王のビルドは、Visual Studio 2017が必要だが、2015しかインストールしていないので、MSYS2のg++でビルドした。
やねうら王のMakefileで、「COMPILER = g++」を有効にして、MSYS2 MinGW 64-bitで、makeを実行する。
makeのターゲットはavx2とする。
make -j8 avx2
やねうら王では、ユーザ定義の処理をsource\engine\user-engine\user-search.cppに定義できるようになっているので、ここにテスト用データを生成する処理を記述する。
void user_test(Position& pos_, istringstream& is) { std::cout << pos_.sfen() << std::endl; Learner::PackedSfenValue psv; psv.gamePly = 1; pos_.sfen_pack(psv.sfen); psv.move = move_from_usi("2g2f"); psv.score = 10; psv.game_result = s8(1); std::fstream fs; fs.open("test.psv", ios::out | ios::binary); fs.write((char*)&psv, sizeof(Learner::PackedSfenValue)); fs.close(); }
コマンドラインで、YaneuraOu-by-gcc.exeを実行して、
isready position startpos user
と入力すると初期局面のテストデータをtest.psvというファイルに出力できる。
positionコマンドの局面を変更することで異なる局面を生成できる。
評価値、指し手、手数、勝敗は、ソースに直書きしているので、必要に応じて修正する。
前回、ねね将棋が世界コンピュータ将棋選手権で高い探索速度を出していたので、バリューの計算中に末端ノードから新しく探索を行う方法で簡易な実装をして実験を行った。
しかし、末端ノードから新しく探索を始めると、新しく始めた探索のバリューの計算されるまで、元の探索のバックアップが行われないため、正しくゲーム木が成長しない状態となっていた。
そこで、ねね将棋と同じように経路を保存して、バッチで評価した後にバックアップを行う実装に変更して検証をやり直した。
ねね将棋では、シングルスレッドで探索を行い、GPUの計算をマルチプロセスで行う実装になっているが、GPUの計算中も探索を行いたいため、以下のような方式にした。
探索とGPU計算は同一のスレッドで直列に実行し、GPU計算の完了のためにスレッドの同期を不要にした。探索はバッチサイズ分実行し、GPU計算はバッチ処理を行う。
この探索スレッドを2つ並列で実行し、GPUの計算は競合するため排他制御を行う。
そうすることで、一方のスレッドでGPUの計算中にも探索を行うことができる。
GPUが複数枚ある場合は、GPUごとに2つの探索スレッドを割り当てる。
GPU2枚のPCで探索速度の比較を行った。
スレッド数 | 探索速度(シミュレーション/秒) | |
変更前 | 168×2 | 12457 |
変更後 | 2×2(バッチサイズ192) | 14158 |
※初期局面で10万回シミュレーション
探索速度が向上することが確かめられた。
前回の記事で、探索速度を上げるとモンテカルロ木探索の特性としてゲーム木が幅方向に広がるため、強さに結びつかないという考察を行った。
幅方向に広がるのを抑えるため、バッチサイズ分探索を繰り返す際、同じ経路を探索した場合は、その探索は破棄して、バッチサイズを減らす実装にした。
そうすることで、バリューの計算を待つ処理を疑似的に再現できる。
今回の実装方法で、探索の深さを確認した。
探索の深さ | |
変更前 | 36 |
変更後 | 35 |
今回の実装では、探索の深さはバリューを待つ実装と同等になった。
GPSfishと対局して、強さの確認を行った。
1手3秒50回対局で勝率は72%となり、変更前とほぼ同じ勝率となった。
GPUあたり2スレッドで探索を行うことの効果を確認した。
スレッド数 | 探索速度(シミュレーション/秒) |
1 | 13154 |
2 | 14158 |
3 | 14057 |
スレッド数を2にすると1の場合よりも探索速度が向上している。3にしてもGPU計算が競合するため意味がない。
以前の実装はスレッドの同期を行うため、Linuxで性能がでなかった。
今回の実装はGPUの計算を待つ処理を直列で実装しているため、スレッドの同期が不要となっている。
そのため、Linuxでも探索速度が上がることが期待できる。
そこで、探索速度をLinuxで測定を行った。
スレッド数 | 探索速度(シミュレーション/秒) | |
変更前 | 168×2 | 2258 |
変更後 | 2×2(バッチサイズ192) | 11388 |
探索速度が5倍に向上した。
しかし、Windowsの8割程度なので、まだ改善の余地はありそうである。
GPUの枚数をさらに増やした場合に、探索速度が向上するかAWSの4GPUのインスタンス(p3.8xlarge)で確認を行った。
GPU枚数 | 探索速度(シミュレーション/秒) | 探索の深さ | 探索時間(秒) |
1 | 9416 | 30 | 9.512 |
2 | 18529 | 35 | 4.533 |
3 | 27475 | 35 | 2.632 |
4 | 35701 | 33 | 2 |
※バッチサイズ192
※初期局面で10万回シミュレーション
探索速度がほぼ線形に伸びている。
10万回シミュレーションで測定しているため、探索の深さは深くなっていないが、同じ時間の探索ではより多くのシミュレーションが行えるため、深さも深くできる可能性がある。
そこでGPU枚数4枚でシミュレーション回数を40万回にして測定を行った。
シミュレーション回数 | 探索速度(シミュレーション/秒) | 探索の深さ | 探索時間(秒) |
10万 | 35701 | 33 | 2 |
40万 | 36298 | 37 | 9.077 |
探索時間は、GPU1枚のときと同じだが、探索の深さはより深く探索できている。
探索速度の向上が、強さに結びつくことが予想できる。
dlshogiでは、MCTSの末端ノードでバリューを計算し、その値をバックアップしているが、GPUでバリューの計算が終わるまで待機している。
バリューの計算が終わる前に次の探索を始めると、ノードにバーチャルロスのみが反映された状態で、勝敗の推定値が反映されておらず、その状態で探索すると精度が落ちるためである。
そのため、GPUを増やしても探索速度(シミュレーション/秒)が上げられない点が課題となっている。
第28回世界コンピュータ将棋選手権で、ねね将棋が、CPUを1スレッドにして、バリューの計算中もシミュレーションを繰り返すことで、高い探索速度を達成していた。
select766.hatenablog.com
キューに追加する際に、経路も保存する実装になっており、実装は複雑になっている。
dlshogiはスタックの巻き戻しの際にバックアップしているので、同じような実装を試すには大幅な変更になるので、バリューの計算中に末端ノードから新しく探索を行うことで、経路の保存はスタックに残すことで実装してみた。
デフォルトのスタックサイズではオーバーフローするため、リンカオプションでスタックサイズを十分に大きくした。
同じノードに到達した際にポリシーの計算を待つ処理はそのままとした。
※2018/5/9 追記
この実装方法には問題があり、下記の検証はあまり意味のない結果でしたので、後日記事を書き直します。
// valueの計算中は、別の探索を行う while (uct_node[child_index].evaled == 0) { // 探索回数を1回増やす atomic_fetch_add(&po_info.count, 1); // 盤面のコピー unique_ptr<Position> pos(new Position(*pos_root)); // 1回プレイアウトする UctSearch(pos.get(), current_root, 0); // 探索を打ち切るか確認 bool interruption = InterruptionCheck(); // ハッシュに余裕があるか確認 bool enough_size = uct_hash->CheckRemainingHashSize(); if (!pondering && GetSpendTime(begin_time) > time_limit) break; if (!(po_info.count < po_info.halt && !interruption && enough_size)) break; }
GPU2枚のPCで探索速度を比較した結果、以下の通りとなった。
※初期局面を1秒間探索
※スレッド数は探索速度が最大になるように調整
スレッド数 | 探索速度(シミュレーション/秒) | |
---|---|---|
バリューを待つ実装 | 144×2 | 10121 |
バリューを待たない実装 | 8×2 | 15239 |
バリューを待たない実装の方が、探索速度が向上することが確かめられた。
探索速度が上がったことで、強くなっているか確認するために、GPSfishと対局させてみた。
結果、0勝10敗と、明らかに弱くなってしまった。
バリューの計算中に探索を行うと、探索が幅方向に広がるため、それが原因で探索の精度が落ちていると考えた。
そのことを確認するために、探索の深さを調べた。
探索終了後に、ゲーム木を訪問回数が最大のノードをたどったときの探索の深さは以下のようになっていた。
探索の深さ | |
---|---|
バリューを待つ実装 | 24 |
バリューを待たない実装 | 10 |
仮説の通り、バリューの計算を待たない場合、探索の深さが浅くなっていた。
次に、探索の幅が広がらないように、PUCTの定数を大きくして、再度測定してみた。
PUCTの定数 | 探索の深さ | 探索速度(シミュレーション/秒) |
---|---|---|
1.0 | 10 | 15239 |
2.0 | 17 | 12760 |
3.0 | 19 | 11087 |
4.0 | 21 | 10068 |
5.0 | 23 | 8847 |
PUCTの定数を増やすと深さが大きくなることが確かめられた。しかし、反比例して探索速度が落ちている。
バリューを待つ実装の方が、同じ探索の深さでは探索速度が高くなっている。
単純に探索速度を上げるだけでは、強さには結びつかないため、幅と深さと速度のバランスをとることが必要そうだ。
ねね将棋がブログで解説を書かれる予定ということなので、この辺の分析もしてもらえると嬉しい。
dlshogiは、バランスは悪くないようにも思えるが、現在の実装ではLinuxで性能がでない課題があるので、どうするか継続検討したい。
第28回世界コンピュータ将棋選手権に参加してきました。
dlshogiは、一次予選に7位で通過しましたが、二次予選では1勝8敗で24チーム中最下位という結果でした。
dlshogiは、今回注目されていたCrazy Shogiと同じくモンテカルロ木探索とディープラーニングを組み合わせたソフトです。
今回ディープラーニングを使用したチームは複数チームありましたが、一次予選通過できたのはdlshogiだけでした。
dlshogiの学習方法、探索の実装がある程度通用することが示せてよかったと思います。
大会では、参加者の方と情報交換できて、新たに試したいこともでてきました。
YSSの山下さんとPonanza Chainerの大渡さんに技術的な話を聞くことができて参加した甲斐があった思います。
ねね将棋とbroadenもモンテカルロ木探索とディープラーニングを試されており、有益な情報交換をすることができました。
来年は、Crazy Shogiも優勝を目指してくるということなので、こちらも負けないように頑張るつもりです。
dlshogiの第28回世界コンピュータ将棋選手権バージョンのビルド済みファイルを公開しました。
第5回将棋電王トーナメントバージョンは、Chainerの環境構築が必要でしたが、USIエンジンの実行のみであれば不要になっています。
CUDA、cuDNNはライセンス上の問題が起きないように同梱していませんので、別途インストールが必要です。下記のページの説明を参照してください。
Release 第28回世界コンピュータ将棋選手権バージョン · TadaoYamaoka/DeepLearningShogi · GitHub
以前にAWSのWindowsサーバで、GPUを増やしても探索速度を上げられなかったということを書きましたが、後からバグがあって1個のGPUしか使っていなかったがわかりました。
バグを修正したバージョンで、p3.8xlargeで4GPUを使うと自宅のGPU2枚のPCより少し探索速度を上げることができました。
p3.16xlargeだと、なぜか探索速度が落ちてしまいました。
ということで世界コンピュータ選手権はp3.8xlargeを使うことにします。
ハードスペック | スレッド数 | シミュレーション/秒 |
Corei9 10コア+GPU2枚(TitanV+1080Ti) | 255+255 | 10779 |
Xeon 32コア+GPU4枚(Tesla V100) | 168+168+144+96 | 12224 |
スペックは2倍以上ですが、探索速度の向上は13%です。
スレッド数は探索速度が最大になるように調整した結果です。
dlshogiはLinuxでも実行できるようにしていますが、探索速度はWindowsの方が高いので、AWSのOSはWindowsにする予定です。
USIエンジンをAWSで実行するには、SSHでリモートで実行して標準入出力でやり取りすれば可能です。
将棋所は、実行ファイルに引数を渡すことができませんが、バッチファイルにSSHコマンド記述して、バッチファイルを登録すれば問題ありません。
WindowsのAMIは、デフォルトではSSHで接続できないため、自分でSSHサーバを構築する必要があります。
WidnowsでのSSHサーバの構築方法は、別の記事にしました。
AWSのWindows AMIにsshで接続する - TadaoYamaokaの開発日記
上記の記事を参考に、AWSのWindowsサーバにSSHサーバを構築して、公開鍵で認証する設定を行います。
将棋所を実行するクライアントからsshコマンドで接続できることが確認できたら、以下のようなバッチファイルを作成します。
@echo off C:\OpenSSH-Win64\ssh xxxx@xxxx.us-west-2.compute.amazonaws.com C:\Users\xxxx\Documents\dlshogi\usi
※ユーザ名とホスト名とUSIエンジンのパスは書き換えてください。
これを将棋所に登録します。
将棋所のエンジン選択は、デフォルトで拡張子が「.exe」のファイルしか表示されませんが、ファイル名に「*」を入力してEnterキーを押せば、バッチファイルも表示されるので、登録できます。
これで、ローカルで動くUSIエンジンと同様にエンジン設定と実行が可能です。
当然ですが、エンジン設定に設定するファイルパスはサーバ上でのパスです。
以上です。