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 と同じ動機で、コード側に最小限のコメントを残す。
対象:
- policy_value.py の
_deck_contextに mean / set_transformer 分岐の意図 ("v7 互換 / v12 安全側 residual gate") を 1 行コメント。
所要: 5 分。
Phase 2: トークン化の改修 (retrain 必要、最大の改善余地)¶
P1-1 の実測結果を見てから着手。
P2-1: TOKEN_BUCKETS 拡張 (緊急退避策)¶
目的: 衝突が深刻 (P1-1 で衝突 bucket > 100) の場合、まず単純拡張で被害を小さくする。
実装:
- encoder.py:12 を
TOKEN_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.py—TokenVocabクラス、VocabBlock、set_active_vocab/get_active_vocab/active_vocab(context manager) /verify_vocab_digestを実装。card_idblock はint_as_offsetで実値をそのまま offset 化 (新カードが既存 ID を動かさない設計)。card,log.cardId,option.cardIdは 1 つの 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.py—token(prefix, value, vocab=None)を改修し、explicit vocab 引数 → process-wide active vocab → 旧 hashing の順に解決。src/pca/cabt/card_db.py—build_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.py—resolve_model_configでmodel_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.py—load_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 だけで浅い。
実装:
- policy_value.py:130-134 の
policy_headを以下に変更:
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 の分散が安定しない可能性。
実装:
- policy_value.py:199 を以下に変更:
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_takenplayer*_attack_reached_rateplayer*_prize_reached_ratedeck_out_losses,pokemon_out_losses- モデル健全性:
- policy loss / value loss の収束
- 学習中の
deck_gateの値推移 (Phase 3-2 / 3-4 で特に)
次のアクション¶
- P1-1 (衝突率実測) を最優先で実行。所要 30 分。結果次第で P2-1 vs P2-2 の選択が決まる。
- P1-2, P1-3 (ドキュメント / コメント追記) は並行でいつでも入れて OK。
- P1-1 の数字を見て、Phase 2 の進め方を確定する。