2026-06-28¶
デッキ内カードIDと静的特徴量の埋め込みを分離¶
Set Transformer deck context のカード表現を見直した。
これまでは deck card id と CardStaticFeatures 由来の feature token を同じ embedding
table に寄せていたため、「カード個体の identity」と「Pokemon / HP / attack_count /
retreat などの汎化特徴」が混ざりやすかった。
変更後:
card_id_embedding- raw card id を直接 index として引く。
- そのカード自体の identity を覚える。
card_feature_embeddingCardStaticFeaturesを token 化した feature token を平均 pool する。- カード種別や HP などの汎化しやすい特徴を覚える。
card_feature_encoderconcat(card_id_vec, feature_vec)をLinear(2d→d) → ReLU → LayerNormで deck card vector に戻す。
互換性:
- 既存 checkpoint からの warm-start では、shape が一致する state/action/policy/value などの重みだけを読む。
- deck card branch の新規 weight と shape が変わった
card_feature_encoderは初期化から始まる。 load_state_dict(strict=False)だけでは shape mismatch で落ちるため、runtime loader もload_compatible_state_dictを使うようにした。
進展の少ない教師方策の重みを下げる¶
v12 の teacher search は AlphaGo Zero 風に NN + ISMCTS を使う方針だが、teacher が弱い局面の policy target をそのまま imitation すると、value では負けを学んでも policy prior が悪い手を模倣してしまう。
そのため、学習時の policy loss に限って低品質 teacher の重みを下げるオプションを追加した。
追加:
--low-progress-policy-weightconfigs/v12/train-policy.yamlではlow-progress-policy-weight: 0.3
低品質 teacher の判定:
- record の
your_index側がサイドを 1 枚も取れていない - かつ以下のいずれか
- 攻撃回数が 0
- その player が勝っていない
- no-progress END がある
重要な点:
- value target は下げない。
- 負けた/停滞した局面から「これは悪い」を学ぶ信号は残す。
- policy target だけを下げる。
- 弱い search policy を強く模倣しないため。
- サイドを取れている負けデータは低品質扱いしない。
- 負けていても、攻撃やサイド取得へ進んだ手順は学習価値があるため。
- 古い JSONL は
game_prizes_takenが無いので低品質判定しない。 - 後方互換を優先する。
実装:
src/pca/training/dataset.pyis_low_progress_recordrecord_player_statrecord_policy_weight(..., low_progress_policy_weight=...)src/pca/training/selfplay.py- self-play record meta に以下を保存
game_attack_ready_countsgame_prizes_takengame_final_prize_countsgame_no_progress_pass_counts
src/pca/training/train.py- CLI / checkpoint config に
low_progress_policy_weightを追加 tests/test_training.py- 低品質 teacher policy だけ下がることをテスト
- サイド取得済みの負け record は下げないことをテスト
想定コマンド:
PYTHONPATH=src uv run python -m pca.training.train \
--config configs/v12/train-policy.yaml
たね切れ・ベンチ不在への対策¶
ルールベース AI との対戦で、Active card must be the ID of a Pokémon card. と reason=3
の種切れ負けが目立った。
修正:
- Belief hidden active sampling で Pokemon card id だけを active 候補にする。
Basic Energyを Pokemon と誤判定しないようCardStaticFeatures.is_pokemonを修正。- ISMCTS candidate pruning で、空ベンチ時にベンチへ出せる Pokemon 候補を落とさない。
v12_prize_racevalue に empty-bench seed-out risk と小さい bench safety bonus を追加。reason=3の種切れ負けは、value target には負けとして残し、policy imitation は low-progress 扱いで弱める。- evaluation summary に
pokemon_out_wins/losses/finishesを追加し、normal win から除外する。 - self-play log に
reason=pokemon_outと parent total のpokemon_outsを出すようにした。
確認:
PYTHONPATH=src python -m unittest tests.test_encoder tests.test_training
ドキュメントの更新¶
現行仕様を 1 ファイルにまとめた。
追加:
docs/architecture/current-method.md
更新:
docs/documentation-guide.mddocs/architecture/belief-guided-neural-ismcts.mddocs/research/2026-06-24-current-training-strategy.mddocs/research/2026-06-21-belief-guided-neural-ismcts.mddocs/research/2026-06-22-ismcts-distillation-results.mddocs/PLAN.md
現在の本線として明示したこと:
- NN-only ではなく、Policy/Value Net と ISMCTS を組み合わせる。
- rollout は使わず、leaf で NN value + progress value を使う。
- Belief Model は hidden state sampler の prior。
- oracle policy target は実験用で、v12 本線では使わない。
- deck context / Set Transformer は実装済みで v7 系では使っていた。v10/v11 では teacher search 安定化のため一時的に無効化したが、現在のデッキを明示的に理解させるため v12 学習 default で再度有効化する。
Policy/Value モデル構造のレビューメモ¶
current-method.md を読み物として整理する過程で、ActionConditionedPolicyValueNet
(src/pca/models/policy_value.py) と features encoder
(src/pca/features/encoder.py) を改めて精読した。設計は AlphaGo
Zero 系 + action-conditioned
policy としてオーソドックスで、致命傷はないが、後で詰まりそうな箇所をメモしておく。優先度順:
1. Hash bucket TOKEN_BUCKETS = 8192 が小さい疑い¶
- トークン化は
blake2b("prefix:value") % 8191 + 1の feature hashing (encoder.py:27-29)。 card_id:*だけで数百〜1000、それ以外にcard_*静的特徴・zone・player・log_type・phase・turn・turn_action_count・select_*等の prefix が数十あるため、ユニーク文字列数は数千〜1 万と推定される。- 8192 buckets だと衝突確率が無視できないオーダー。Embedding は衝突したカード ID と無関係 prefix を「無理やり共有」する形になる。
- TODO:
set([token(p, v) for ...])で実衝突率を測る 1 行スクリプトを書き、必要ならTOKEN_BUCKETS = 32768 〜 65536への引き上げを検討。
2. State encoder に positional encoding が無い¶
state_encoderはEmbedding → TransformerEncoder → mean poolのみで positional embedding を足していない (policy_value.py:165-171)。- これは
token("zone", ...)/token("player", ...)などの prefix トークンを delimiter とし、state を bag-of-tokens として扱う設計。 - ベンチ位置自体には効果がないので順序不変で問題ないが、「ベンチが何体いるか」のような離散カウントは self-attention 経由でしか復元できない。
- 改善余地:
bench_count:Nのような summary token を追加する案。 - TODO: 「state は順序不変な集合として設計している」旨を
current-method.mdの Policy/Value Model 節に 1 行明記する。
3. Action path が極端に asymmetric¶
- State path は Transformer × num_layers なのに対し、Action path は
Embedding → mean pool → Linear → ReLU → LayerNormのみ (policy_value.py:104-108, 237-238)。 - forward が action 数 A に比例して増えるのを避ける compute トレードオフとしては合理的。
- ただし
option_type / target / card_id / ...を mean pool で潰すため、「カード A を target B にプレイ」「カード B を target A にプレイ」が同じベクトルになる可能性がある。 - 改善余地: 重要組み合わせを prefix 設計で 1 トークン化 (例
play_target:A→B)。実測コストは小さい。
4. State-Action 相互作用が浅い (concat + 2 層 MLP)¶
policy_head = Linear(2d→d) → ReLU → Linear(d→1)のみで state と action を混ぜている (policy_value.py:130-134, 240)。- AlphaZero 系として標準だが、「同じ手でも盤面で価値が大きく変わる」ポケカ的状況では表現力不足になりやすい。
- 改善余地: 軽量な順に (a) bilinear 項
state^T W action追加、(b) policy_head を 3-4 層化、(c) action → state の cross-attention。
5. Deck context fusion の mode 間 asymmetry¶
meanモード:state_vec = context_fusion(concat(state_vec, deck_vec))(置き換え)set_transformerモード:state_vec = state_vec + deck_gate * context_fusion(...)(residual + 学習可能ゲート, gate 初期 0)- (policy_value.py:191-199)
- 意図的 (v7 系から v12 への移行で壊れないよう set_transformer 側だけ gate を入れた) だが、コメントが無いので将来読む人が混乱する可能性。
- TODO: コード側に意図コメントを 1 行入れるか、
mean側も residual + gate に揃える。
6. set_transformer fusion の残差後に LayerNorm が無い¶
state_vec + deck_gate * fusionの直後に norm が無いため、deck_gate が大きくなると分散が安定しない可能性 (policy_value.py:199)。- 実害は小さいが、
self.context_norm(state_vec + ...)1 行で予防できる。
7. State encoder の集約が素朴な mean pool¶
- Set Transformer 側は学習可能
pool_token+ MultiheadAttention pooling なのに、state 側は masked mean pool。 - State の方が情報密度が高いので、こちらに attention pool /
[CLS]token を入れる方が筋が良い可能性。
8. デフォルト d_model=128, nhead=4, num_layers=2¶
- 軽量 baseline 設定。NN forward が self-play 最大のボトルネックである事情とは整合するが、表現力の天井としては低め。
d_model=256, num_layers=3-4への引き上げは v12 以降で要検討。
検証順序の提案¶
- (1) hash 衝突実測 → 数行のスクリプトで結論が出る。最小コストで一番大きい改善余地。
- (2)(5) ドキュメント追記 → 後続実装者の事故を減らす目的でゼロコスト。
- (3) action token 構造化 → encoder 側の prefix 設計変更で済む。retrain 必要。
- (4) bilinear 追加 / (6) LayerNorm 追加 / (7) attention pool → 順にモデル改造。1 つずつ A/B して効果測定。
- (8) モデルサイズアップ → forward コスト増のため、cache / batch / remote 推論の改善と組で検討。
状態・行動・履歴からカードを参照する分岐¶
前の整理では、deck context だけが card_id_embedding + card_feature_embedding の構造化 card
vector を使い、state / action / history は flat
token 経路で処理されていた。これだと、盤面や合法手に含まれるカードの identity と静的特徴を deck と同じ粒度で扱えない。
修正:
EncodedObservationと self-play JSONL に以下を追加。state_card_idsaction_card_idshistory_card_idsActionConditionedPolicyValueNetにcard_ref_context_modeを追加。auto / none / card_ref- training default は
auto - card-reference branch は deck と同じ card vector 化を使う。
- raw card id:
card_id_embedding - 静的特徴:
card_feature_embedding - 合成:
card_feature_encoder - state は Set Transformer、history/action は card id list の mean pool で読み、各 branch に residual gate で合流する。
state_card_gatehistory_card_gateaction_card_gate- gate 初期値は
--init-card-ref-gateで指定し、標準は 0.0。 - 旧 checkpoint から warm-start した直後は既存挙動を壊さない。
意図:
- deck だけでなく、盤面・履歴・合法手に出ているカードも card id と静的特徴を同じ方法で扱えるようにする。
- flat token 経路は残す。
- zone / option type / select context / log type など、カードがどの文脈で現れたかを表すため。
TokenVocabによる card id token 共有も残す。card,option.cardId,log.cardIdの同じ card id は同じ token id に解決される。
確認予定:
PYTHONPATH=src python -m unittest tests.test_training tests.test_encoder
Structured object context への置き換え¶
card-reference branch は「カード id と静的特徴」を state / action /
history に入れられるが、state_card_ids
のような集合表現だけでは「どの Pokemon にどの Energy が付いているか」「どの option がどの target を指しているか」が弱い。
そのため、A/B 用の追加 branch ではなく、v12 本線として structured object 表現を追加し、structured
data がある場合は unstructured card_ref_context_mode を無効化するようにした。
追加した object:
state_objects- Pokemon / attached energy / attached tool / pre-evolution / hand / discard / stadium など。
- attached energy / tool / pre-evolution には
parent_zone/parent_slotを持たせる。 action_objects- option の cardId、action type、target area / index、select context。
history_objects- log の cardId、fromArea、toArea、event type。
object row の構造:
[card_id, owner, zone, role, slot, parent_zone, parent_slot,
hp_bucket, damage_bucket, energy_count, status, aux]
モデル側:
structured_context_mode = none | objects | autoautoは JSONL にsearch.*_objectsがあればobjects。- raw card id は
card_id_embedding。 - static feature は
card_feature_embedding。 - context fields は shared embedding。
structured_object_encoder(concat(card_vec, context_vec))で object vector にする。- state は Set Transformer、history / action は object mean で合流。
init_structured_gatedefault は 0.05。- warm-start を大きく壊さない範囲で、structured object branch に最初から勾配を流すため。
後方互換:
- 古い
state_card_ids/action_card_ids/history_card_idsは残す。 - ただし structured objects がある場合、
resolve_model_configはcard_ref_context_mode=noneにする。
確認:
PYTHONPATH=src python -m unittest tests.test_training tests.test_encoder tests.test_cli_config tests.test_submission tests.test_vocab
P2-2: 決定論的 TokenVocab の実装と統合¶
refactor plan の Phase 2-2 を完了。
前提: P1-1 (../research/2026-06-28-token-collision-report.md) で「8192 buckets では 6768 文字列のうち 3818 (56%) が衝突」を確認。card_id が 1000+ ある以上、bucket を増やすだけでは衝突ゼロにならない。
実装¶
- src/pca/features/vocab.py — TokenVocab クラス。card_id は
int_as_offset(実値 = offset) で新カード追加時に既存 ID が動かない構造。card/log.cardId/option.cardIdを 1 つの card_id block で共有。enum 系は 4 倍 headroom、末尾 1024 を未知 prefix 用 fallback hash に確保。 - src/pca/features/encoder.py —
token()を process-wide active vocab 経由に変更。set_active_vocab(vocab)を entrypoint で 1 回呼ぶだけで 14 箇所の encode_observation 呼び出しが自動連動する設計 (call site の plumbing を回避)。 - src/pca/cabt/card_db.py —
build_card_feature_token_table(card_db, vocab=None)で vocab パススルー。 - Entrypoint 統合: selfplay (main + spawn
worker)、train.resolve_model_config、belief_train、tournament、submission/main の 5 箇所で active
vocab を install。checkpoint load 時は
verify_vocab_digestで mismatch を警告。 - 既存 checkpoint は
load_compatible_state_dictの shape-skip で吸収 (vocab 関連 Embedding は random init から再学習が必要)。
数字¶
| 旧 hash | 新 vocab | |
|---|---|---|
| Embedding 行数 | 8192 | 18631 |
| Embedding メモリ (d=128) | 4.0 MB | 9.1 MB |
| real collision string | 3818 (56%) | 0 |
| 利用率 | 56% (hash 運次第) | 23.4% (= 4 倍の content drop を吸収する予約) |
card_id block: 5000 枠予約 / 現状 1268 使用。card_id 5000 までの新カードを vocab 拡張なしで吸収可能。
テスト¶
- tests/test_vocab.py (19 件) — block 操作 / JSON roundtrip / digest 安定性 / 追加成長 / card_id 共有 / active vocab context / 全 card db に対する real collision = 0 検証。
- tests/test_encoder.py — vocab 経由の encode_observation 動作確認を 1 件追加。
- 全 187 テスト pass。
次の運用¶
- v12 self-play の次サイクルから vocab encoding に切り替わる。既存 v11 checkpoint からは shape mismatch で Embedding 関連が random init になるので、最初の数 epoch で warm-up 必要。
- 新カード追加時は CSV を更新するだけ。vocab.size が自動的に拡張対応。card_id が 5000 を超える場合のみ
TokenVocab.from_card_db(card_db, card_id_capacity=...)で再調整。 - 計測:
PYTHONPATH=src uv run python scripts/measure_token_collisions.pyで hash + vocab 両方の collision stats と vocab レイアウトレポートが docs/research/2026-06-28-token-collision-report.md に再生成される。
メモ¶
# ismcts-simulations-per-determinizationを128で試した場合
[selfplay][game] id=8 result=p0_win reason=1 steps=93 turns=13 avg_step_turn=7.15
board taken=6-0 remain=0-6 deck=44-16 hand=7-7 atk=6-0 ready=9-12 no_pass=0-0 mill=0-0
perf elapsed=4592.0s speed=0.8g/h records/s=0.0
search begin=47616 step=206787 end=93 obs_conv=93 full_obs=93 t=23.1/69.1s
nn calls=46071 cache=2529/43447 total=4090.2s fwd=4032.5s deck=0.9s enc=44.6s tensor=10.0s
ismcts dec=93 sim=47616 hidden=372/372 time=4589.1s tree_enc=206787/323.6s nodes=45978/160809 cand=5.9/10.5
depth avg=4.34 max=5+ leaf=4.38/45883 term=3.32/1733 hist=0/2037/5046/9353/10827/20353+
reach turn_advance=0.67 attack_reached=0.26 prize_reached=0.21
root n=93 actions=4.7 atk_ready=21/93 atk_opts=1.2 atk_top prior/visit/sel=0.05/0.06/0.06 end_sel=0.06 agree=0.65 top_share prior/visit=0.44/0.52 H prior/visit=1.28/1.10
root-actions sel play=0.24 attach=0.05 evolve=0.04 ability=0.05 retreat=0.03 discard=0.00 choice=0.45 other=0.00
stops leaf=45883 term=1733 no_action=0 no_select=0 fail=0 max_depth=0
prize-delta events=10048 taken=9278-2534 value=3034.80
turns [6,2,9,16,3,5,5,10,2,17,4,12,2]
[selfplay][worker] id=2 done=1/4 global_game=8 result=p0_win reason=1 steps=93 records=93 finished=1/1
board taken=6-0 remain=0-6 deck=44-16 atk=6-0 ready=9-12 no_pass=0-0 mill=?-?
perf elapsed=4592.1s speed=0.8g/h progress=1/4(25.0%) remaining=3 eta=3h49m records/s=0.0
search begin=47616 step=206787 end=93 obs_conv=93 full_obs=93 t=23.1/69.1s
nn calls=46071 cache=2529/43447 total=4090.2s fwd=4032.5s deck=0.9s enc=44.6s tensor=10.0s
ismcts dec=93 sim=47616 hidden=372/372 time=4589.1s tree_enc=206787/323.6s nodes=45978/160809 cand=5.9/10.5
depth avg=4.34 max=5+ leaf=4.38/45883 term=3.32/1733 hist=0/2037/5046/9353/10827/20353+
reach turn_advance=0.67 attack_reached=0.26 prize_reached=0.21
root n=93 actions=4.7 atk_ready=21/93 atk_opts=1.2 atk_top prior/visit/sel=0.05/0.06/0.06 end_sel=0.06 agree=0.65 top_share prior/visit=0.44/0.52 H prior/visit=1.28/1.10
root-actions sel play=0.24 attach=0.05 evolve=0.04 ability=0.05 retreat=0.03 discard=0.00 choice=0.45 other=0.00
stops leaf=45883 term=1733 no_action=0 no_select=0 fail=0 max_depth=0
prize-delta events=10048 taken=9278-2534 value=3034.80
decks own=clefairy-ogerpon.csv opp=dragapult-blaziken.csv