Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ビット圧縮(PackedSfen / HuffmanCodedPos)

ディープラーニング将棋の自己対局データや数億局面の教師データでは、1 局面あたりのデータサイズが入出力帯域とストレージ容量のボトルネックとなる。 rsshogi は、position-serialization Cargo feature のもとで、任意の合法局面を 正確に 256 ビット(32 バイト) の固定長バイト配列 [u8; 32] に可逆圧縮する PackedSfen(YaneuraOu 形式)と HuffmanCodedPos(Apery / dlshogi 形式)の 2 種類を実装している。


先後反転した局面の出力

Position::to_sfen_flipped() は、盤面を 180 度回転し、駒色、持ち駒、手番を入れ替えた SFEN を返す。 Position::to_packed_sfen_flipped() は同じ反転規則を使って PackedSfen を返す。 表現不能な局面をエラーとして扱う場合は try_to_packed_sfen_flipped() を使う。 元の局面と履歴は変更しない。

PackedSfen の反転出力は SFEN 文字列の生成と再解析を経由しない。 出力用の局面には盤面、持ち駒、手番と玉位置だけを設定し、bitboard や戦術キャッシュの再計算を行わない。 Python では Board.to_sfen_flipped() と Board.to_packed_sfen_flipped(out=None) を使う。


1. ビットストリーム I/O(BitReader / BitWriter)

ハフマン符号化では、1 ビットのフラグから 7 ビットの座標、2〜8 ビットの可変長符号までがバイト境界(8 ビット単位)を跨いで隙間なく詰め込まれる。 crates/rsshogi/src/board/position/bit_io.rs の BitReader と BitWriter は、[u8; 32] バッファに対する LSB-first(各バイトの最下位ビット Bit 0 から最上位ビット Bit 7 の順) のビット読み書きを担う。

#![allow(unused)]
fn main() {
// BitWriter::write_one_bit のビット位置計算 (cursor: 0..256)
self.buf[self.cursor >> 3] |= (b & 1) << (self.cursor & 7);
self.cursor += 1;
}
  • cursor >> 3 で書き込み先のバイトインデックス(0..32)を特定し、cursor & 7 でそのバイト内のビットオフセット(0..8)を決定する。
  • 複数ビットの読み書きでは、コード定義側で「LSB から順に送出するビット列(Pre-reversed bit pattern)」を保持しておくことで、1 ビットずつシフトしながらそのままストリームへ流し込める設計となっている。

2. 将棋の駒数不変条件と 256 ビット固定長への収束原理

可変長符号(ハフマン符号)を用いているにもかかわらず、なぜどんな局面でも必ず 256 ビット(32 バイト)以内に収まるのか。 その理由は、「盤上の駒が空きマスに変わって盤面ビット数が減った分だけ、持ち駒が増えて持ち駒ビット数が増える(逆もまた然り)」という将棋の総駒数(40 枚)不変条件 にある。

  • 盤上の空きマス:1 ビット(0)で表現される。
  • 盤上の駒(玉を除く):移動元、先後、成フラグを含め、駒種に応じたビット数が割り当てられる。
  • 持ち駒:同一駒種の盤上駒よりも常に 2 ビット少ない符号長(先後 1 ビットと成フラグ 1 ビットが不要なため、あるいは空きマス 0 の 1 ビット分との差し引きで盤上配置と同等の合計ビット数)が割り当てられている。

具体的には、ある駒が盤上から捕獲されて持ち駒に移ると、盤面側ではそのマスが「駒(N ビット)」から「空マス(1 ビット)」に変わって N - 1 ビット減少し、持ち駒側では N - 2 ビット増加するため、局面全体の合計ビット数はむしろ 1 ビット減少 する。 その結果、全 38 枚の非玉駒がすべて盤上に存在し、かつ成駒が混じる最悪ケースでも、合計ビット数は必ず 最大 256 ビット以下 に収まることが数学的に保証される。


3. PackedSfen(YaneuraOu 形式)のビット構造と符号表

PackedSfen(crates/rsshogi/src/board/position/packed_sfen.rs)は、以下の 4 セクションを先頭(Bit 0)から順番に書き込む。

Bit:   0      1..=7 (7b)     8..=14 (7b)    15.. (可変長)                 ..255
     +------+--------------+--------------+-----------------------------+------------------+
     | Turn | Black King   | White King   | 81 Squares Board Encoding   | Hand Pieces      |
     | (1b) | Square (0..80| Square (0..80| (sq = 0..=80, King = Empty) | (R->B->G->S->N->L|
     +------+--------------+--------------+-----------------------------+------------------+
  1. 手番(1 ビット):先手(BLACK)なら 0、後手(WHITE)なら 1。
  2. 先手玉と後手玉の座標(7 ビット × 2 = 14 ビット):先手玉の Square(0..=80、玉不在時は 81)と後手玉の Square をそれぞれ 7 ビット整数で書き込む。
  3. 盤面 81 マスの符号化(sq = 0..=80):先手玉および後手玉がいるマスも含めて全 81 マスを走査するが、玉がいるマスはすでに先頭 14 ビットで記録済みであるため、盤面ストリーム上では「空きマス(0、1 ビット)」として出力する。それ以外の駒があるマスは、駒種プレフィックスに続けて 先後(1b) と 成フラグ(金以外のとき 1b、後置) を書き込む。
  4. 持ち駒の符号化:先手 → 後手の順、かつ飛 → 角 → 金 → 銀 → 桂 → 香 → 歩の順で、所持枚数分だけ持ち駒符号を繰り返し書き込む(玉が 2 枚とも盤上にある通常の局面では、最初の 2 枚の玉スロットをスキップして残りのビット領域に詰め込む)。

PackedSfen の符号定義表(LSB-first 送出順)

対象プレフィックス(ビット幅)付加ビット(送出順)盤上合計ビット数持ち駒の符号(プレフィックス + 先後 1b)持ち駒合計ビット数
空きマス(または玉)0(1b)なし1 bitなしなし
歩(PAWN)01(2b, 値 0x01)先後 1b + 成 1b4 bits0(1b, 値 0x00)+ 先後 1b2 bits
香(LANCE)0011(4b, 値 0x03)先後 1b + 成 1b6 bits001(3b, 値 0x01)+ 先後 1b4 bits
桂(KNIGHT)1011(4b, 値 0x0b)先後 1b + 成 1b6 bits101(3b, 値 0x05)+ 先後 1b4 bits
銀(SILVER)0111(4b, 値 0x07)先後 1b + 成 1b6 bits011(3b, 値 0x03)+ 先後 1b4 bits
金(GOLD)1111(4b, 値 0x0f)先後 1b(成なし)5 bits111(3b, 値 0x07)+ 先後 1b4 bits
角(BISHOP)011111(6b, 値 0x1f)先後 1b + 成 1b8 bits01111(5b, 値 0x0f)+ 先後 1b6 bits
飛(ROOK)111111(6b, 値 0x3f)先後 1b + 成 1b8 bits11111(5b, 値 0x1f)+ 先後 1b6 bits

4. HuffmanCodedPos(Apery / dlshogi 形式)のビット構造と PackedSfen との違い

HuffmanCodedPos(crates/rsshogi/src/board/position/huffman_coded_pos.rs)も同じく 256 ビットのハフマン符号化形式であるが、YaneuraOu の PackedSfen とは以下の 3 つの重要な非互換点 があるため、両者のバイト列を直接混用することはできない。

相違点PackedSfen(YaneuraOu 形式)HuffmanCodedPos(Apery / dlshogi 形式)
1. 玉が存在するマスの扱い81 マスすべてを走査し、玉のマスにも空きマスビット 0(1b × 2 = 2b)を出力する。玉が存在する 2 マスを盤面ループから完全にスキップ し、残りの 79 マスのみ を符号化する(2 ビット節約)。
2. 成フラグと先後ビットの配置すべての成れる駒で [プレフィックス] -> [先後 1b] -> [成 1b] の順で固定。桂、銀、金は [プレフィックス 3b] -> [成 1b(金は 0 固定)] -> [先後 1b] の順で、成フラグが先後ビットより前に置かれる。
3. プレフィックスのビット値香=0x03、桂=0x0b、銀=0x07、金=0x0f 等。香=0x03、桂=0x03(ビット幅で区別)、銀=0x05、金=0x0f 等。

HuffmanCodedPos の符号定義表(盤上 79 マスと持ち駒)

駒種盤上符号(合計ビット数、カッコ内は BitWriter 送出値)持ち駒符号(合計ビット数、カッコ内は BitWriter 送出値)
空きマス1 bit(0x00)なし
歩(PAWN)4 bits(プレフィックス 0x01 [2b] + 成 1b + 先後 1b)2 bits(プレフィックス 0x00 [1b] + 先後 1b)
香(LANCE)6 bits(プレフィックス 0x03 [4b] + 成 1b + 先後 1b)4 bits(プレフィックス 0x01 [3b] + 先後 1b)
桂(KNIGHT)6 bits(プレフィックス 0x0b [4b] + 成 1b + 先後 1b)4 bits(プレフィックス 0x05 [3b] + 先後 1b)
銀(SILVER)6 bits(プレフィックス 0x07 [4b] + 成 1b + 先後 1b)4 bits(プレフィックス 0x03 [3b] + 先後 1b)
金(GOLD)6 bits(プレフィックス 0x0f [5b, 成ビット相当が 0 固定] + 先後 1b)4 bits(プレフィックス 0x07 [3b] + 先後 1b)
角(BISHOP)8 bits(プレフィックス 0x1f [6b] + 成 1b + 先後 1b)6 bits(プレフィックス 0x0f [5b] + 先後 1b)
飛(ROOK)8 bits(プレフィックス 0x3f [6b] + 成 1b + 先後 1b)6 bits(プレフィックス 0x1f [5b] + 先後 1b)

PackedSfen では盤上の金が 5 ビットであるのに対し、HuffmanCodedPos では金も成ビット位置に 0 を持つ 6 ビット符号となっており、代わりに玉のいる 2 マス分の空きマスビット(2 ビット)を完全にスキップすることで、全駒配置時の最大ビット数がちょうど 256 ビットに収まるよう設計されている。 rsshogi は両フォーマットのデコーダとエンコーダを独立して備えているため、YaneuraOu 系データセットと Apery / dlshogi 系データセットをロスレスに相互変換できる。