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

sazpack フォーマット仕様(SAZ2 v4)

sazpack(マジック SAZ2、現行バージョン 4)は、AlphaZero 系のモンテカルロ木探索(MCTS)自己対局から得られる教師データを対局単位のチャンクとして保存するバイナリフォーマットである。 各対局の開始局面と終局情報に加え、各手番における探索後の root WDL、ネットワークの raw 出力(raw WDL、raw prior、raw mate、raw moves-left)、証明詰み手数、および合法手ごとの訪問回数と勝敗上下限(疎な policy 分布)を格納する。

reader は version 4 のチャンクだけを受理する。 異なる版のデータを利用する際は、ヘッダとペイロードの仕様を確認して変換するか、生成元から再生成する。 終局理由の意味が生成元で失われている場合、版番号を変更するだけでは復元できない。

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

sazpack チャンクは、10 バイトの固定長チャンクヘッダと、連続配置された対局レコード列(SazSelfplayGame*)で構成される。 固定幅のマルチバイト整数はすべてリトルエンディアン(LE)であり、要素数と訪問回数には可変長整数 ULEB128(u32 上限)を使用する。

Chunk = ChunkHeader (10 bytes) + SazSelfplayGame*

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

オフセットサイズエンコーディングフィールド名内容と検証規則
0x004B[u8; 4]magicb"SAZ2" 固定。不一致時は SazSelfplayError::InvalidMagic。
0x041Bu8version4 固定。4 以外の値は SazSelfplayError::UnsupportedVersion(version)。v3 以前のチャンクは読み替えずに拒否する。
0x051Bu8flags0 固定。0 以外の値は SazSelfplayError::UnsupportedFlags(flags)。
0x064Bu32 LEpayload_lenヘッダ直後に続くペイロード全体のバイト長。ファイル長が 10 + payload_len より短い場合は Truncated、長い場合は TrailingBytes。

対局レコードのワイヤーレイアウト(SazSelfplayGame / SazGame)

チャンクペイロード内では、以下の順序で 1 局分のデータが直列化される。

順序サイズエンコーディングフィールド名内容と有効値
132B[u8; 32]stem_packed_sfen対局開始局面の 32 バイト PackedSfen。codec はバイト列を保持し、局面としての妥当性は検証しない。
21Bu8game_result対局結果 GameResult の数値表現(未定義値は InvalidGameResult)。
31Bu8termination_reason終局理由 SazTerminationReason(0:合法手なし、1:千日手引分、2:連続王手の千日手勝ち、3:連続王手の千日手負け、4:優等局面、5:劣等局面、6:入玉宣言、7:最大手数到達、8:投了、9:裁定)。10 以上は InvalidTerminationReason。
8 と 9 の終局局面には合法手が残る。8 は敗者側が投了した終局であり、評価値の閾値で手番側(最後に記録された局面の手番)の負けを確定させる自己対局の打ち切りを含む。9 は投了を伴わない宣言であり、引き分けを宣言した場合の game_result は DrawByAdjudication になる。
41Bu8entering_king_rule入玉宣言ルール EnteringKingRule(0:None、1:Point24、2:Point24Handicap、3:Point27、4:Point27Handicap、5:TryRule、6:Unset)。7 以上は InvalidEnteringKingRule。
51..=5 BULEB128 (u32)position_countこの対局に含まれる局面レコードの個数 P。
6可変長SazSelfplayPosition * PpositionsP 個の局面レコードを順に連続格納する。

局面レコードのワイヤーレイアウト(SazSelfplayPosition / SazPosition)

各局面レコードは、34 バイト(mate が Some の場合は 36 バイト)の固定順序ヘッダに続けて、ULEB128 の policy_count と可変長の policy エントリ列を配置する。

順序サイズエンコーディングフィールド名内容と量子化スケール
12Bu16 LEplayedこの局面で実際に選択された 16-bit 指し手(Move::raw())。
26Bu16 LE × 3root_wdlMCTS 探索後の root 集約 WDL(win, draw, loss の順)。3 要素の合計は厳密に 65535。
36Bu16 LE × 3outcome_wdl終局結果から確定した教師 WDL(win, draw, loss の順、手番視点)。合計は厳密に 65535。
46Bu16 LE × 3raw_wdl探索を通す前にネットワークが出力した WDL(win, draw, loss の順)。合計は厳密に 65535。
52Bu16 LEraw_mateネットワークの mate head が出力した詰み確率(u16 / 65535 量子化)。
62Bu16 LEraw_moves_leftネットワークの moves-left head の予測残り手数(手数 * 32 の固定小数点数、分解能 1/32 手)。
72Bu16 LEplies_left実際の対局終了までの残り手数。
84Bu32 LErequested_visitsこの局面で要求された探索シミュレーション回数。
92Bu16 LEtarget_weight_milli学習時のサンプル重み(千分率、1000 = 重み 1.0)。
101Bu8exploration_flags探索設定やノイズ有無を表すビットフラグ。
111Bu8mate_tag証明詰み手数 mate の有無タグ(0:None、1:Some)。0 と 1 以外は InvalidMateTag。
120B または 2Bi16 LE(条件付き)mate_valuemate_tag == 1 のときのみ存在する 2 バイト符号付き詰み手数。mate_tag == 0 のときは 0 バイト。
131..=5 BULEB128 (u32)policy_count後続する policy エントリの個数 M。
14可変長SazSelfplayPolicyEntry * MpolicyM 個の候補手 policy エントリを連続格納する。

Policy エントリのワイヤーレイアウト(SazSelfplayPolicyEntry / SazPolicyEntry)

各候補手(最小 9 バイト)は以下の順序で直列化される。

順序サイズエンコーディングフィールド名内容と検証規則
12Bu16 LEmv16-bit 指し手(Move::raw())。学習時には手番と合わせて CompactMoveLabel(0..1496)または MoveLabel(0..2187)へ変換して利用できる。
22Bu16 LEprior探索が実際に使用した事前確率(Dirichlet ノイズ等適用後、量子化値 0..=65535)。
32Bu16 LEraw_priorネットワークが出力したそのままの事前確率(量子化値 0..=65535)。
41..=5 BULEB128 (u32)visits探索終了時に root がこの候補手へ保持していた累積訪問回数(ツリー再利用による引き継ぎ分を含む)。
51Bu8lower探索で証明された勝敗下限 SazOutcomeBound(0:Loss、1:Draw、2:Win)。3 以上は InvalidOutcomeBound。
61Bu8upper探索で証明された勝敗上限 SazOutcomeBound(0:Loss、1:Draw、2:Win)。3 以上は InvalidOutcomeBound。

Note

メモリ上や下流の学習テンソル構築において、候補手のラベルと勝敗上下限を 16-bit 整数 packed_label_bounds: u16 にパックして扱う場合は、下位 11 ビット(bits 0..=10)に CompactMoveLabel(0..1496)、bits 11..=12 に lower(SazOutcomeBound)、bits 13..=14 に upper(SazOutcomeBound)、bit 15 に 0 を割り当てる設計と対応付けられる。SAZ2 v4 チャンクのワイヤー上では、局面再生なしで直接読み書きできるよう mv: u16 LE(2B)と lower: u8(1B)、upper: u8(1B)として記録される。

2. 検証不変条件(Validation Invariants)

serialize_chunk(書き出し)と deserialize_chunk(読み込み)の双方で、以下の不変条件が厳密に検証される。

  1. WDL 分布の総和:各局面の root_wdl、outcome_wdl、raw_wdl は、いずれも u32::from(win) + u32::from(draw) + u32::from(loss) == 65535(SazWdl::TOTAL)を満たさなければならない。不一致の場合は SazSelfplayError::InvalidDistributionSum を返す。
  2. Prior 分布の総和:policy が空でない各局面について、全エントリの prior の合計および raw_prior の合計がそれぞれ厳密に 65535 に一致しなければならない(桁溢れを防ぐため残余予算 65535 からの減算 checked_sub で検証される)。なお、policy が 0 件の終端局面等では合計が 65535 に達しないため、policy は少なくとも 1 手以上のエントリを持ち合計 65535 を満たす必要がある。
  3. 列挙値とタグの境界:未知の GameResult、SazTerminationReason(> 9)、EnteringKingRule(> 6)、SazOutcomeBound(> 2)、および mate_tag(0 または 1 以外)はすべてデコード時に拒否される。

局面の妥当性、着手の合法性、候補手の重複、lower <= upper、終局結果と終局理由の整合性は codec の検証対象に含まれない。 例えば DrawByAdjudication と Resignation の組み合わせも読み書きできるため、生成側で整合性を確認する。 終局理由の表にある条件はデータの意味を定める契約であり、codec が盤面を再生して判定するものではない。

3. コード例

Python

Python では rsshogi.sazpack サブモジュールで SazWdl、SazPolicyEntry、SazPosition、SazGame と読み書き関数を公開している。

from rsshogi.core import Board
from rsshogi.record import GameResult
from rsshogi.sazpack import (
    SazGame,
    SazPolicyEntry,
    SazPosition,
    SazWdl,
    decode_sazpack,
    decode_sazpack_file,
    write_sazpack,
    write_sazpack_file,
)

board = Board()
wdl = SazWdl(win=30000, draw=5535, loss=30000)  # 合計 65535
outcome = SazWdl(win=65535, draw=0, loss=0)

pos = SazPosition(
    played="7g7f",
    root_wdl=wdl,
    outcome_wdl=outcome,
    raw_wdl=wdl,
    raw_mate=0,
    raw_moves_left=3200,  # 100.0 手 (100 * 32)
    plies_left=95,
    requested_visits=800,
    target_weight_milli=1000,
    exploration_flags=1,
    policy=[
        SazPolicyEntry(mv="7g7f", prior=40000, raw_prior=40000, visits=500, lower=0, upper=2),
        SazPolicyEntry(mv="2g2f", prior=25535, raw_prior=25535, visits=300, lower=0, upper=2),
    ],
    mate=None,
)

game = SazGame(
    stem=board.to_sfen(),
    game_result=GameResult.BLACK_WIN,
    termination_reason=0,  # NoLegalMoves
    entering_king_rule=3,  # Point27
    positions=[pos],
)

# バイト列およびファイルへの書き出しとデコード
chunk_bytes = write_sazpack([game])
decoded_games = decode_sazpack(chunk_bytes)

write_sazpack_file("selfplay.sazpack", [game])
loaded_games = decode_sazpack_file("selfplay.sazpack")

Rust

Rust では serialize_chunk / deserialize_chunk に加え、チャンク内における各対局のバイト範囲(Range<usize>)を同時に取得できる deserialize_chunk_indexed を提供している。

#![allow(unused)]
fn main() {
use rsshogi::records::formats::sazpack::{
    IndexedSazSelfplayGame, SazGame, SazOutcomeBound, SazPolicyEntry, SazPosition,
    SazTerminationReason, SazWdl, deserialize_chunk, deserialize_chunk_indexed, serialize_chunk,
};

let bytes = serialize_chunk(&games)?;
let decoded: Vec<SazGame> = deserialize_chunk(&bytes)?;

// チャンク内の各対局が占める payload バイト範囲を取得する
let indexed: Vec<IndexedSazSelfplayGame> = deserialize_chunk_indexed(&bytes)?;
for item in &indexed {
    let slice = &bytes[item.payload_range.clone()];
    println!("game positions={}, byte_len={}", item.game.positions.len(), slice.len());
}
}

関連項目