コンテンツにスキップ

Policy / Value モデル構造の修正計画

作成日: 2026-06-28

ActionConditionedPolicyValueNet (../../src/pca/models/policy_value.py) と features encoder (../../src/pca/features/encoder.py) を精読した結果、設計の致命傷は無いが、表現力の天井を下げている懸念点が複数見つかった (../journal/2026-06-28.md#policy-value-モデル構造のレビューメモ)。本メモはその指摘をフェーズ分けの実装計画に落としたもの。

前提とスコープ

与件

  • カード ID は 1000 種類以上ある (デッキ構築・対戦ログから確認済み)。
  • 現在 TOKEN_BUCKETS = 8192 (encoder.py:12)、トークン化は blake2b("prefix:value") % 8191 + 1 の feature hashing。
  • ユニーク token 文字列数の概算: card 関連 ~2000 (card_id(1500+) + 静的特徴 prefix × 値域) + 状態系 ~1000 = 約 3000-4000
  • バースデーパラドックスより、8192 buckets では衝突確率が事実上 100%。「2 個以上同居する bucket」が概算 ~500 個。

スコープ外

  • 学習データ生成 (self-play) の探索改善は本計画外 (v12 探索改善 を参照)。
  • Belief Model / PublicKnowledgeTracker の改修は本計画外。
  • Compute インフラ (NN cache / remote 推論) の改善は前提として進行中であるものとする。

制約

  • Phase 2 以降は 既存 checkpoint と非互換になる可能性が高い。
  • 各 Phase の改修は、1 項目ずつ A/B 評価して効果を切り分ける。一括投入はしない。
  • A/B 評価は holdout deck pool との online ISMCTS 対戦で行う (configs/v12/evaluate-mac-quality.yaml を流用)。

全体ロードマップ

flowchart LR
    P11["P1-1<br/>衝突率実測"]:::diag
    P12["P1-2/3<br/>docs/comments"]:::diag
    P22["P2-2<br/>決定論的 ID"]:::tok
    P23["P2-3<br/>summary token"]:::tok
    P24["P2-4<br/>action token 構造化"]:::tok
    P31["P3-1<br/>bilinear policy_head"]:::arch
    P32["P3-2<br/>LayerNorm 追加"]:::arch
    P33["P3-3<br/>attention pool"]:::arch
    P34["P3-4<br/>fusion 統一"]:::arch
    AB["A/B 評価"]:::eval
    P4["Phase 4<br/>capacity scaling"]:::scale

    P11 --> P22
    P12 -.-> P22
    P22 --> P23
    P22 --> P24
    P23 --> P31
    P24 --> P31
    P31 --> AB
    P32 --> AB
    P33 --> AB
    P34 --> AB
    AB --> P4

    classDef diag  fill:#e3f2fd,stroke:#1976d2,color:#000
    classDef tok   fill:#fff3e0,stroke:#f57c00,color:#000
    classDef arch  fill:#f3e5f5,stroke:#7b1fa2,color:#000
    classDef eval  fill:#fce4ec,stroke:#c2185b,color:#000
    classDef scale fill:#e8f5e9,stroke:#388e3c,color:#000

Phase 1: 計測 & 文書化 (即着手、retrain 不要)

P1-1: 衝突率実測スクリプト

目的: 「8192 buckets で実際にどれだけ衝突しているか」を数字で確定させ、Phase 2 の方針 (単純拡張 vs 決定論的 ID) を決定する材料にする。

実装:

  • scripts/measure_token_collisions.py を新規作成。
  • self-play 1〜数ゲームを CABT で回し、encode_observation が生成する全 token 文字列 (token 化前の f"{prefix}:{value}" 文字列) を収集。
  • 集計指標:
  • ユニーク文字列数 |S|
  • ユニーク token id 数 |T|
  • 衝突 bucket 数 (|S| - |T|)
  • bucket あたりの文字列数の分布 (max / 上位 10)
  • TOKEN_BUCKETS を 8192 / 16384 / 32768 / 65536 と仮想変更したときの衝突数

完了条件: 上記指標を docs/research/2026-06-28-token-collision-report.md として書き出す。

所要: 30 分。

P1-2: ドキュメント追記

目的: 設計意図がコードからは読み取りにくい箇所を current-method.md に明記し、将来の事故を防ぐ。

対象:

  • state は順序不変な bag-of-tokens として設計している 旨を current-method.md の Policy/Value Model 節 に追記。
  • deck context の mean / set_transformer fusion は非対称 (mean=置き換え / set_transformer=residual + gate) を同節に追記。

所要: 10 分。

P1-3: コードコメント追加

目的: P1-2 と同じ動機で、コード側に最小限のコメントを残す。

対象:

所要: 5 分。

Phase 2: トークン化の改修 (retrain 必要、最大の改善余地)

P1-1 の実測結果を見てから着手。

P2-1: TOKEN_BUCKETS 拡張 (緊急退避策)

目的: 衝突が深刻 (P1-1 で衝突 bucket > 100) の場合、まず単純拡張で被害を小さくする。

実装:

  • encoder.py:12TOKEN_BUCKETS = 65536 に変更。
  • Embedding テーブルが 65536 × d_model に増える (d_model=128 で 8 MB、256 で 16 MB)。memory 的に問題なし。
  • 既存 checkpoint と非互換 → 新規 self-play & 学習が必要。

長所: 変更箇所 1 行。

短所: 衝突確率を下げるだけで「ゼロ保証」は得られない。長期的には P2-2 が望ましい。

所要: 1 行 + 学習。

P2-2: 決定論的 ID 割当への移行 (推奨) — ✅ 完了 (2026-06-28)

目的: 衝突を確率ではなく構造的にゼロにする。

実装内容 (実装ファイルへのリンク付き):

  • src/pca/features/vocab.pyTokenVocab クラス、VocabBlockset_active_vocab / get_active_vocab / active_vocab (context manager) / verify_vocab_digest を実装。
  • card_id block は int_as_offset で実値をそのまま offset 化 (新カードが既存 ID を動かさない設計)。
  • card, log.cardId, option.cardId1 つの card_id block を共有 (CARD_ID_SHARED_PREFIXES)。
  • enum block は enum_headroom_factor=4.0 で 4 倍予約。
  • 末尾 1024 buckets を fallback hash に確保 (CABT 側で未知 prefix が出ても crash しない安全弁)。
  • src/pca/features/encoder.pytoken(prefix, value, vocab=None) を改修し、explicit vocab 引数 → process-wide active vocab → 旧 hashing の順に解決。
  • src/pca/cabt/card_db.pybuild_card_feature_token_table(card_db, vocab=None) で vocab パススルー対応。
  • Entrypoint 統合:
  • src/pca/training/selfplay/impl.py, src/pca/training/selfplay/policies.py, src/pca/training/selfplay/policy_factory.py — メインと worker (run_selfplay_worker) の双方で set_active_vocab 呼び出し (spawn worker は active vocab を継承しないため必須)。checkpoint load 時に verify_vocab_digest で警告。
  • src/pca/training/train.pyresolve_model_configmodel_config["token_buckets"] = vocab.size / model_config["vocab_digest"] = vocab.digest() を保存。
  • src/pca/training/belief_train.py — 同様。
  • src/pca/evaluation/tournament/impl.py — tournament 実行時に active vocab を install。
  • src/pca/submission/main.pyload_card_db() で active vocab を install、verify_vocab_digest で checkpoint mismatch を警告。
  • Tests:
  • tests/test_vocab.py — 19 件 (block/JSON roundtrip/digest/append/active vocab/real-collision-zero)。
  • tests/test_encoder.py — 1 件追加 (vocab 経由で encode_observation が正しく動作)。

結果:

  • vocab.size = 18631 (旧 8192 比 2.3 倍、Embedding メモリ d=128 で 9.1 MB)。
  • real collision = 0 (../research/2026-06-28-token-collision-report.md 参照)。
  • 既存 checkpoint との互換性は load_compatible_state_dict の shape-skip で吸収 (vocab 関連 Embedding は random init から再学習が必要)。
  • 全 187 テスト pass。

Embedding メモリの目安:

d_model TOKEN_BUCKETS=8192 TokenVocab.size=18631
128 4.0 MB 9.1 MB
256 8.0 MB 18.2 MB

次の Phase 3 / 4 への影響: model の Embedding サイズが vocab 駆動になったため、capacity scaling (Phase 4) で token_buckets を別途気にする必要は無くなった (vocab を rebuild するだけで自動追従)。

P2-3: Summary token の追加

目的: bag-of-tokens 設計では「ベンチが何体いるか」のような離散カウントが self-attention 経由でしか復元できない。これを summary token で直接渡す。

実装:

  • encoder.py:118 周辺encode_player_state 系で以下を追加:
  • token("bench_count", n) (n: 0〜5)
  • token("hand_count_bucket", n) (n: 0〜10、粗いバケット)
  • token("prize_remaining", n) (n: 0〜6)
  • token("deck_remaining_bucket", n) (粗い)
  • 同じ情報が state_tokens に複数ある形にして、attention でわざわざ集計しなくて済むようにする。

所要: 半日 + 学習。

P2-4: Action token の構造化

目的: option_type / target / card_id を mean pool で潰すと、「カード A を target B にプレイ」「カード B を target A にプレイ」が同じベクトルになる。重要な組み合わせを 1 token 化することで失われている情報を回復する。

実装:

  • encoder.py:207 周辺encode_option (action_tokens を作る箇所) を見直し:
  • token("opt:type:target", f"{type}|{target}") のような複合 prefix token を追加。
  • 既存の独立 token (option_type, target 単体) は併存させる (情報は冗長化)。
  • どの組み合わせを 1 token 化するかは、CABT option の構造 (src/pca/cabt/schema.py) を見ながら決める。

所要: 1 日 + 学習。

Phase 3: アーキテクチャ改修 (retrain 必要、表現力向上)

Phase 2 のトークン側修正を入れたうえで、モデル側の表現力を 1 項目ずつ強化する。1 つずつ A/B 評価

P3-1: policy_head に bilinear 項追加

動機: state-action 相互作用が concat → 2層MLP だけで浅い。

実装:

self.policy_head = nn.Sequential(
    nn.Linear(d_model * 2, d_model),
    nn.ReLU(),
    nn.Linear(d_model, 1),
)
self.policy_bilinear = nn.Bilinear(d_model, d_model, 1)
# forward:
# logits = mlp_logits + bilinear(state_vec_expanded, action_vec)

所要: 半日 + 学習。

P3-2: set_transformer fusion 後に LayerNorm 追加

動機: state_vec + deck_gate * fusion の直後に norm が無く、deck_gate が大きくなると state_vec の分散が安定しない可能性。

実装:

self.context_norm = nn.LayerNorm(d_model)
# in _deck_context:
return self.context_norm(state_vec + self.deck_gate * deck_delta)

所要: 5 分 + 学習。

P3-3: state encoder の集約を attention pool に

動機: 現状は素朴な masked mean pool。Set Transformer 側は学習可能 pool_token + MultiheadAttention pooling なので、こちらも揃える。

実装:

  • policy_value.py:165-171_state_pool を、SetTransformerDeckEncoder の pool_token + MultiheadAttention pooling と同じ形に書き直す。あるいは [CLS] token を state_tokens の先頭に追加して encoder 後にその位置のベクトルを使う。

所要: 半日 + 学習。

P3-4: mean / set_transformer の fusion 流儀統一

動機: mode 切替で挙動差があり、将来読む人が混乱する。

実装:

  • mean モードも state_vec + deck_gate * context_fusion(...) の residual + gate 形式に揃える。
  • gate 初期値は両モード共通 (0.0)。

所要: 1 時間 + 学習。

P3 全体の進め方

  • P3-2 (LayerNorm 追加) は単独で簡単なので、Phase 2 と並行して入れて A/B しても良い。
  • P3-1 / P3-3 は効果が大きそうな順 (P3-1 を先) で 1 つずつ。
  • P3-4 はゲート挙動の整理目的が主で、勝率に直接効くものではない。Phase 3 の最後に。

Phase 4: 容量スケーリング (compute 改善とセット)

P3 までで表現力ボトルネックを洗い出した後の最終手段。

前提条件

NN forward が self-play の最大ボトルネックなので、以下が先に解消されている必要がある:

  • NN cache (LRU) のヒット率改善
  • batch 化 (現状 1 局面ずつ forward しがち)
  • remote 推論サーバの安定運用

P4-1: d_model=128 → 256

  • 表現次元 2 倍。forward FLOPs は約 4 倍 (Transformer 内部) になる。

P4-2: num_layers=2 → 3 または 4

  • encoder 深さ増。forward FLOPs は線形で増える。

所要: 学習 + 計算量増。compute 改善とのバランスで判断。

既存 checkpoint との互換性

Phase 既存 ckpt 互換 備考
Phase 1 計測 & 文書化のみ
P2-1 (buckets 拡張) Embedding テーブル shape 変更
P2-2 (決定論 ID) 同上
P2-3 (summary token) embedding はそのまま使えるが、新 token は random init → 短い fine-tune で吸収可能
P2-4 (action 構造化) 同上
P3-1 (bilinear) bilinear は新規 init、既存 head は warm-start 可能
P3-2 (LayerNorm) LayerNorm パラメータは新規 init
P3-3 (attention pool) pool 部分は新規 init
P3-4 (fusion 統一) gate を 0 から再学習
Phase 4 d_model / num_layers 変更で shape 変更

非互換 () の Phase に入るタイミングで「fresh ループ」(新規 self-play → 新規学習) を 1 回挟む。init-checkpoint: None で v12 を回せる体力 (Kaggle GPU 数時間〜数日) が現実的な律速。

検証指標

各 Phase / 各 A/B で以下を測る。

  • 直接指標: tournament win rate (vs holdout deck pool, online ISMCTS)
  • 派生指標 (teacher search の質):
  • player*_avg_prizes_taken
  • player*_attack_reached_rate
  • player*_prize_reached_rate
  • deck_out_losses, pokemon_out_losses
  • モデル健全性:
  • policy loss / value loss の収束
  • 学習中の deck_gate の値推移 (Phase 3-2 / 3-4 で特に)

次のアクション

  1. P1-1 (衝突率実測) を最優先で実行。所要 30 分。結果次第で P2-1 vs P2-2 の選択が決まる。
  2. P1-2, P1-3 (ドキュメント / コメント追記) は並行でいつでも入れて OK。
  3. P1-1 の数字を見て、Phase 2 の進め方を確定する。