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

sbinpack フォーマット仕様(SBN2)

sbinpack(Shogi binpack、マジック SBN2、現行バージョン 2)は、Stockfish の NNUE 訓練データ形式である binpack を将棋向けに適応した高圧縮バイナリフォーマットである。 開始局面(SbinpackStem)と、それに続く連続した指し手および評価値列(チェーン)を、合法手オーダリングインデックスとパリティ正規化差分で可変長符号化して保存する。

設計の特徴と Stockfish binpack との比較

項目Stockfish binpack(チェス)sbinpack v2(将棋)
チャンクマジックBINPSBN2
先頭局面表現24 バイト32 バイト PackedSfen
Stem サイズ32 バイト38 バイト(SbinpackStem)
エンディアンビッグエンディアンリトルエンディアン(LE)
可変長整数符号化4-bit + 拡張ビットULEB128(符号付き差分は ZigZag + ULEB128)
チェーン単位メタデータなしMetadataLen (0..=127) + opaque バイト列

1. ワイヤーレイアウト仕様

sbinpack ファイルは 1 個以上のチャンク(Chunk)を連結したストリームであり、各チャンクは 8 バイトのチャンクヘッダと 0 個以上のチェーン(Chain)で構成される。

File  = Chunk*
Chunk = ChunkHeader (8 bytes) + Chain*
Chain = SbinpackStem (38 bytes)
      + MetadataLen (1 byte, 0..=127)
      + Metadata (MetadataLen bytes)
      + Count (u16 LE, 2 bytes)
      + (EncodedMove (ULEB128) + EncodedScore (ZigZag + ULEB128)) * Count

チャンクヘッダ(8 バイト)

オフセットサイズ型フィールド名内容と検証規則
0x004B[u8; 4]magicb"SBN2" 固定。不一致時は SbinpackError::InvalidMagic。
0x044Bu32 LEchunk_sizeこのチャンクヘッダ直後に続くペイロード(Chain*)の合計バイト数。

(デコード後の SbinpackFile 構造体は version = 2 および flags = 0 を保持する。)

チェーン構造(SbinpackChain)

1 つのチェーンは開始局面から連続する手順ライン(本手順または 1 つの変化手順)を表す。

順序サイズ型フィールド名内容と検証規則
138BSbinpackStemstemチェーン先頭局面の 38 バイト固定長ヘッダ(下表参照)。
21Bu8metadata_lenmetadata のバイト長(0..=127)。128 以上の値は SbinpackError::MetadataTooLarge。
30..=127 B[u8]metadata利用側が任意に定義できるチェーン単位の opaque バイト列(metadata_len == 0 のときは 0 バイト)。
42Bu16 LEcount後続する MoveText に含まれる指し手の総数 K。
5可変長(ULEB128, ULEB128) * Kmoves1 手ごとに EncodedMove(合法手インデックスの ULEB128)と EncodedScore(評価値差分の ZigZag + ULEB128)を交互に K 回繰り返す。

先頭局面ヘッダ(SbinpackStem、38 バイト)

SbinpackStem は sbinpack 固有の 38 バイト構造体であり、40 バイトの PackedSfenValue(PSV)とはレイアウトが異なる。

オフセットサイズ型フィールド名内容
0x0032B[u8; 32]packed_sfenチェーン先頭局面の 32 バイト PackedSfen。
0x202Bi16 LEscore先頭局面の評価値(手番視点、単位:cp)。
0x222Bu16 LEbest_move_u16先頭局面の 16-bit 指し手(rsshogi::Move::raw())。
0x242Bu16 LEply_result下位 10 ビット(bits 0..=9)が先頭局面の手数 ply(0..=1023、超過時は 1023 に飽和)、上位 6 ビット(bits 10..=15)が GameResult の数値表現(0..=63)。

Record との相互変換における SbinpackStem の扱い

  • Record から sbinpack への変換時:チェーンの指し手列 moves が 1 手以上ある場合は先頭手 moves[0].mv が stem.best_move_u16 に格納され、0 手(空手順)の場合は Move::MOVE_NONE(0)が格納される。stem.score には呼び出し側が指定した stem_score(既定値 0)が格納される。
  • sbinpack から Record への変換時:stem.packed_sfen と ply_result から初期局面と対局結果が復元されるが、stem.score および stem.best_move_u16 は Record 側にはコピーされない(各手の評価値は MoveText 内の EncodedScore から復元される)。

2. 指し手の符号化(EncodedMove)と決定的オーダリング

各局面における指し手は、MoveListGen::<LegalAll> で生成されたすべての通常合法手を以下の v1.0.0 決定的オーダリング規則 でソートした際の 0 始まりインデックス(u16)を ULEB128 で可変長符号化したものである。

合法手ソートキーの優先順位(昇順)

  1. 駒打ち判定:盤上移動(0) → 駒打ち(1)。
  2. 捕獲判定:非捕獲の静かな手(0) → 駒を取る手(1)。
  3. 成り判定:不成(0) → 成り(1)。
  4. 移動前(または打つ)駒種の優先順位: 歩 (0) → 香 (1) → 桂 (2) → 銀 (3) → 金 (4) → 角 (5) → 飛 (6) → 玉 (7) → と (8) → 成香 (9) → 成桂 (10) → 成銀 (11) → 馬 (12) → 龍 (13)。
  5. 移動先(to)の段:手番視点で正規化(後手番は盤面を 180 度回転)した後の段で、敵陣に近い前方段を優先(1 段目 → 9 段目)。
  6. 移動先(to)の筋:手番視点で正規化した後の筋で、中央筋を優先(5, 4, 6, 3, 7, 2, 8, 1, 9 筋の順)。
  7. 移動元(from)の段:盤上移動について、手番視点で正規化した後の前方段を優先(1 段目 → 9 段目)。
  8. 移動元(from)の筋:盤上移動について、手番視点で正規化した後の中央筋を優先(5, 4, 6, 3, 7, 2, 8, 1, 9 筋の順)。
  9. タイブレーク:mv.to_move().raw() の昇順。

出現頻度の高い手(歩や銀の前進、中央付近の着手など)に小さいインデックスが割り当てられるため、ほとんどの合法手インデックスが 1 バイト(0..=127)の ULEB128 に収まる。

3. 評価値差分の符号化(EncodedScore)

各指し手 moves[i] の評価値 move_eval_i(i32、その手を指す直前の手番視点)は、手番の交代による符号反転を打ち消すパリティ正規化を行ったうえで、直前の値との差分を ZigZag + ULEB128 で符号化する。

norm_i         = move_eval_i   (i が偶数のとき)
               = -move_eval_i  (i が奇数のとき)

delta_0        = norm_0 - stem.score
delta_i        = norm_i - norm_{i-1}  (i >= 1)

EncodedScore_i = ULEB128(ZigZag(delta_i))

ここで 32-bit 符号付き整数 x に対する ZigZag 変換と逆変換は以下の通りである。

zigzag(x)   = ((x << 1) ^ (x >> 31)) as u32
unzigzag(u) = ((u >> 1) as i32) ^ -((u & 1) as i32)

形勢が急変しない通常の対局では、パリティ正規化後の隣接評価値差分 delta_i が 0 付近の小さな値に集中するため、高い圧縮率が得られる。

4. コード例

Python

Python では Record のメソッドに加えて、rsshogi.record モジュールで複数局やファイル単位の読み書き関数を提供している。

from rsshogi.record import EngineInfo, GameResult, MoveEntry, Record, SpecialMoveEntry
from rsshogi.record import (
    decode_sbinpack,
    decode_sbinpack_file,
    write_sbinpack,
    write_sbinpack_file,
)

# 評価値付きの Record を構築する(sbinpack 変換には全手の評価値が必須)
record = Record.from_main_line(
    "lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
    [
        MoveEntry("7g7f", engine_info=EngineInfo(eval=110)),
        MoveEntry("3c3d", engine_info=EngineInfo(eval=-115)),
    ],
    SpecialMoveEntry("RESIGN", GameResult.WHITE_WIN),
)

# 1. 単一 Record と sbinpack バイト列の相互変換
data = record.to_sbinpack(
    stem_score=100,
    include_main=True,
    include_variations=False,
    metadata=b"game-001",
)
restored = Record.from_sbinpack(data)
restored_rec, meta = Record.from_sbinpack_with_metadata(data)

# 2. 複数 Record の一括シリアライズとファイル入出力
chunk_bytes = write_sbinpack([record], stem_score=100, metadatas=[b"batch-1"])
pairs = decode_sbinpack(chunk_bytes)  # list[tuple[Record, bytes]]

write_sbinpack_file("train.sbinpack", [record], stem_score=100)
file_pairs = decode_sbinpack_file("train.sbinpack")

Rust(ゼロコピー逐次デコーダ SbinpackDecoder)

Rust の SbinpackDecoder は、ファイル全体を Vec<SbinpackChain> に展開することなく、チャンクとチェーンを先頭から走査して各指し手適用前の &Position をゼロコピーでコールバックへ貸し出す。 また、decode_next_controlled_with で ChainStart イベント時に SbinpackDecodeControl::SkipChain を返すと、PackedSfen の復元や合法手生成を行わずにそのチェーンの可変長ペイロードを高速にスキップできる。

#![allow(unused)]
fn main() {
use rsshogi::records::formats::sbinpack::{
    SbinpackDecodeControl, SbinpackDecodeEvent, SbinpackDecoder, SbinpackError,
};
use rsshogi::types::GameResult;

fn scan_sbinpack(bytes: &[u8]) -> Result<usize, SbinpackError> {
    let mut decoder = SbinpackDecoder::new(bytes);
    let mut visited_positions = 0usize;

    while let Some(step) = decoder.decode_next_controlled_with(|event| match event {
        SbinpackDecodeEvent::ChainStart { result, metadata, .. } => {
            // 例: 未確定局や特定のメタデータを持つチェーンをリプレイせずにスキップする
            if result == Some(GameResult::Invalid) || metadata.is_empty() {
                SbinpackDecodeControl::SkipChain
            } else {
                SbinpackDecodeControl::Continue
            }
        }
        SbinpackDecodeEvent::PositionBeforeMove { position, mv, eval, .. } => {
            let _ = (position.turn(), mv, eval);
            visited_positions += 1;
            SbinpackDecodeControl::Continue
        }
        _ => SbinpackDecodeControl::Continue,
    }) {
        step?;
    }
    Ok(visited_positions)
}
}

関連項目