TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

Aperyでやねうら王のPackedSfenValueを読み込む

世界コンピュータ将棋選手権のアピール文章にも書いたが、マルチGPUで動かす場合、GPUごとに異なるモデルをロードすることで、モデルごとに誤る確率が独立とすると複数モデルが同時に誤る確率は、単一のモデルを使用する場合より低くなるため精度の向上が期待できる。
世界コンピュータ将棋選手権では、教師ありで学習した3つのモデルを使用したが、どれもelmoで生成したデータを使用しており、学習局面数の差しかなくあまり効果がなかったかもしれない。
elmoとは違う系統の教師局面で学習したモデルを使えば、モデルごとの独立性が高まる。

3駒関係の評価関数でも、Aperyとやねうら王の評価関数で重み付き平均をとるということが行われており、効果を上げている。

そこで、やねうら王で生成した教師局面からもモデルの学習を行えるようにしたい。
計算リソースが足りないため、自己対局による強化学習はゼロからではなく事前学習したモデルから開始する予定だが、その際にも2つの系統のモデルうまく使えないかと考えている。

dlshogiはベースにAperyから派生したelmo_for_learnを使用しているので、やねうら王で生成した教師局面の読み込みには対応していない。
そこで、一部ソースを追加してやねうら王で生成した教師局面を読み込めるようにした。

実装とテスト方法について、メモを残しておく。

elmoの教師局面

elmoで使用されている教師局面は、HuffmanCodedPosAndEval(hcpe)という形式で保存されている。
hcpeは、局面をハフマン符号で圧縮したHuffmanCodedPos(hcp)と評価値、指し手、勝敗から構成されている。
hcpは、手番と王の座標、盤面の各座標の駒と持ち駒から構成されている。
各座標の駒と持ち駒は、ハフマン符号で圧縮されて1~8bitで表現されている。

やねうら王の教師局面

やねうら王の教師局面は、PackedSfenValueという形式で保存されている。
PackedSfenValueは、局面をハフマン符号で圧縮したPackedSfenと評価値、指し手、手数、勝敗から構成されている。
PackedSfenは、hcpと同じ情報から構成されているが、ハフマン符号がelmo(Apery)とは異なる。

つまり、ハフマン符号を変更すれば、ほぼ同じコードでPackedSfenValueを読み込むことができる。

elmo(Apery)のハフマン符号

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コマンドの局面を変更することで異なる局面を生成できる。
評価値、指し手、手数、勝敗は、ソースに直書きしているので、必要に応じて修正する。