sbinpack v2
概要
sbinpack(Shogi binpack)は、チェスの訓練データ形式である binpack を将棋向けに適応したバイナリフォーマットです。
オリジナルの binpack は Stockfish の NNUE 訓練データ用に設計され、実戦譜から生成された 連続局面を効率的に圧縮できます。 sbinpack は同じ設計を将棋へ適用し、開始局面と連続する指し手列を保存します。
v2 の仕様として、EncodedMove のオーダリング、評価値差分、per-chain メタデータ拡張枠を定義します。
方針
sbinpackは、評価値を伴う連続局面を保存する用途に使います。
参考資料
- オリジナル binpack 仕様: Stockfish binpack.md
- binpack 圧縮実装: nnue_data_compress
設計目標
- 仕様の安定性: バージョンで順序が揺れないルールベースのオーダリングを採用する。
- 圧縮効率: 小さいインデックスを可変長整数で短く保存できる。
- 説明可能性: 将棋の自然な分類(打ち/非打ち、成り/非成り、駒種、座標)で決まる。
- binpack 互換の構造: Chunk / Chain / Stem / MoveText の階層構造を踏襲する。
Python bindings (rsshogi)
record.to_sbinpack(
stem_score: int = 0,
include_main: bool = True,
include_variations: bool = False,
metadata: bytes | None = None,
) -> bytes
Record.from_sbinpack(data: bytes) -> Record
Record.from_sbinpack_with_metadata(data: bytes) -> tuple[Record, bytes]
rsshogi.record.decode_sbinpack(data: bytes) -> list[tuple[Record, bytes]]
rsshogi.record.decode_sbinpack_file(path) -> list[tuple[Record, bytes]]
-
include_variations=Trueの場合、metadata は出力される各 variation chain にコピーされるため、同じ bytes が 1 ファイル内に複数回現れ得ます。 -
本手順/分岐のすべての指し手に評価値が必要です(欠損時は
ValueError)。 -
include_variations=Trueの場合、分岐ごとに独立した chain を追加します(学習用)。 -
metadataは 0..=127 bytes の per-chain opaque bytes です。未指定の場合はMetadataLen=0の 1 byte だけを出力します。 -
from_sbinpackはRecordだけを返します。opaque metadata も必要な場合はfrom_sbinpack_with_metadataを使います。 -
from_sbinpackは 単一 chain のみ対応します(複数 chain はValueError)。
Rust の逐次デコード
SbinpackDecoder は、入力全体を SbinpackFile に展開せず、chunk、chain、指し手適用前の局面を 1 件ずつ通知します。
入力には借用した &[u8] または所有する Vec<u8> を渡せます。
use rsshogi::records::formats::sbinpack::{
SbinpackDecodeEvent, SbinpackDecoder, SbinpackError,
};
fn visit(bytes: Vec<u8>) -> Result<(), SbinpackError> {
let mut decoder = SbinpackDecoder::new(bytes);
while let Some(result) = decoder.decode_next_with(|event| {
if let SbinpackDecodeEvent::PositionBeforeMove { position, eval, .. } = event {
// position は指し手適用前の局面。必要な所有値はここで取り出す。
let _ = (position.turn(), eval);
}
}) {
result?;
}
Ok(())
}
decode_next_controlled_with で ChainStart から SkipChain を返すと、PackedSfen と指し手インデックスの合法性を検証せず、その chain の可変長 payload を読み飛ばせます。
結果種別や opaque metadata だけで chain を除外する consumer 向けの高速経路です。
通常の decode_next_with は全 chain を replay して検証します。
EncodedMove の前提
LegalAllで合法手を列挙し、その並びの インデックスをEncodedMoveとする。- インデックスは 0 起点で、
u16に収まる合法手列を指します。 - インデックスの並びは 固定ルールで決める(統計依存の再学習はしない)。
ファイルフォーマット(v2)
オリジナルの binpack 形式(Chunk / Chain / Stem / MoveText の構造)を踏襲しつつ、 将棋固有の要素(PackedSfen、mv、合法手オーダリング)を組み込んでいます。
binpack との主な違い
| 項目 | binpack (Chess) | sbinpack (Shogi) |
|---|---|---|
| マジック | BINP | SBN2 |
| 局面表現 | 24 bytes | PackedSfen (32 bytes) |
| 指し手 | 2 bytes | mv |
| Stem サイズ | 32 bytes | 38 bytes |
| エンディアン | Big-endian | Little-endian |
| 可変長整数 | 4bit+拡張ビット | ULEB128 |
バイトオーダー
- すべて リトルエンディアン。
- 可変長整数は ULEB128。
- v2 は
SBN2マジックで識別します。
ルート構造
File = Chunk*
Chunk = ChunkHeader + Chain*
ChunkHeader = "SBN2" + chunk_size(u32 LE)
Chain(局面単位エントリ)
Chain = Stem + MetadataLen + Metadata + Count + MoveText
Stem = SbinpackStem(38 bytes, LE)
MetadataLen = u8 byte_length(Metadata), 0..=127
Metadata = opaque user bytes
Count = u16 LE
MoveText = (EncodedMove, EncodedScore) * Count
-
MetadataLenは 1 byte 固定です。v2 の metadata 上限は 127 bytes/chain なので、0x80..=0xffは不正値です。 -
MetadataLen=0の場合、Metadata は空です。 -
Metadata は opaque bytes です。rating、game id、engine name などの内部構造は利用側で定義できます。
-
MetadataLenはchunk_end内、かつ後続のCount(u16)を読める範囲に収まる必要があります。 -
metadata は小さな sideband 用です。v2 の実装上限は 127 bytes/chain で、これを超える場合は不正データとして扱います。
SbinpackStem(38 bytes)
SbinpackStem は sbinpack 固有の構造であり、40 bytes の
PackedSfenValue とは別形式です。
packed_sfen[32] # PackedSfen (互換)
score_i16 # Stemの評価値(cp, i16)
best_move_u16 # mv
ply_result_u16 # result(6bit) + ply(10bit, 局面のply)
EncodedMove(v2)
EncodedMove = ULEB128(legal_move_index)
legal_move_indexは v1.0.0 オーダリングのインデックス。- 0 起点です。
v2 でも EncodedMove のオーダリングは v1.0.0 と同じです。
Metadata(v2)
Metadata は sbinpack core から見て opaque な byte 列です。未使用時は MetadataLen=0、使用時は
MetadataLen で byte 数を宣言してから、そのまま Metadata を格納します。
固定 tag は v2 では予約しません。将来、rsshogi 標準 metadata schema を定義する場合も、この opaque bytes の上に別仕様として載せます。
EncodedScore(v2)
評価値は差分 + ZigZag + ULEB128 で可変長化する。
norm_i = move_eval_i # i が偶数
norm_i = -move_eval_i # i が奇数
delta_0 = norm_0 - stem_score
delta_n = norm_n - norm_{n-1} # n >= 1
EncodedScore = ULEB128(ZigZag(delta))
move_evalとstem_scoreは、対応する局面の手番側から見た cp 値です。moves[i].evalはその手を指す前の局面に対応します。- wire 形式に評価値の視点を示すフラグはないため、外部 producer と consumer もこの手番側視点の契約に従う必要があります。
- 差分を取る前に、各
move_evalを stem 側の視点へ正規化します。moves[0]は stem と同じ「指す前の局面」に対応するため反転せず、以後は 1 手ごとに符号を反転します。 - 正規化は wire 上の差分計算だけに使います。decode 後の
move_evalとstem_scoreは手番側視点のままです。 - 評価値は ZigZag + ULEB128 で符号化された
i32として扱います。-32000..=32000外の値も特殊符号化せず、そのまま差分に乗せます。
ZigZag 定義
zigzag(i32) = (i32 << 1) ^ (i32 >> 31)
unzigzag(u32) = (u32 >> 1) ^ -(u32 & 1)
ply_result の定義(v2)
bit0..9: ply (0..1023)
bit10..15: result (GameResult, 0..63)
resultはGameResultを 6bit で保持(現在の最大値は21)。plyは 10bit で保持(0..1023)。1023超は 1023 に丸める。
v1.0.0 オーダリング(確定)
以下のキーを この順番で比較し、昇順が小さいインデックスになります。
- 非打ち → 打ち
- quiet(非捕獲) → capture(捕獲)
- 非成り → 成り
- 駒種優先順位(下記)
- 目的地(to)の段(前進優先)
- 目的地(to)の筋(中央優先)
- 移動元(from)の段(前進優先)
- 移動元(from)の筋(中央優先)
- mv(タイブレーク)
駒種優先順位(通常手)
P > L > N > S > G > B > R > K > +P > +L > +N > +S > +B > +R
- 「素直な将棋の駒順」として説明可能性を優先。
- 成駒は基礎駒の後にまとめる。
駒種優先順位(打ち駒)
P > L > N > S > G > B > R
段/筋の比較ルール
- 手番視点で正規化してから比較する。
- 先手: そのまま
- 後手: 盤を 180 度回転(
inv)
- 段は前進優先(正規化後に
1 → 9)。 - 筋は中央優先(
5,4,6,3,7,2,8,1,9)。
オーダリングの根拠
- 安定性: 統計で順序が揺れると互換性が崩れるため、ルールベースを採用。
- 自然な分類: 打ち/非打ち、捕獲/非捕獲、成り/非成り、駒種、座標は将棋の標準的な分類。
- 圧縮効率: 順序の大枠は頻度に沿う傾向があり、可変長符号化で十分な圧縮が得られる。