Unified Token Policy / Value / Belief Model¶
更新日: 2026-07-03
このドキュメントは v13 で追加した UnifiedTokenPolicyValueNet の入力層と出力 head の設計をまとめる。既存の ActionConditionedPolicyValueNet は legacy path として残し、旧 checkpoint / evaluation / submission は引き続き利用できる。v13 の目的は、カード固有の静的情報と、試合中に変わる動的状態、さらに「どこにあるか・誰のものか・何番目か」という位置情報を 1 つの構造付き token にまとめることで、Transformer が盤面上の関係を学びやすくすることである。
2026-07-02 時点では、同じ shared Transformer trunk から次の head を同時に出す。
- policy head: CABT が返した合法 option ごとの logit。
- value head: 現局面の勝敗期待値。
- aux prize heads: このターンのサイド取得枚数と、残りサイドに対する将来取得率。
- integrated belief heads: 相手 hand / deck / prize、次の脅威カード、knockout threat。
standalone BeliefNet は削除しない。v13 checkpoint に belief heads がある場合は --belief-source policy でそれを hidden-state prior に使える。--belief-source auto では、従来の --belief-checkpoint があればそちらを優先し、無ければ policy checkpoint の integrated belief heads を使う。
中心概念¶
CardData / AttackData は「カードの定義」であり、試合中には変化しない。盤面上のカードは、その定義から作られた「いまそこにある実体」である。したがって 1 枚の盤面 token は、次の 3 種類を結合して作る。
board object token =
static_card_embedding(card_id, CardData, AttackData)
+ dynamic_state(HP, damage, energy_count, status, parent, ...)
+ position(area, owner, slot, role)
同じリザードンexでも、自分のバトル場にいて HP が 180 まで減っている場合と、相手のトラッシュにある場合では、同じ static embedding を参照しながら異なる dynamic / position を持つ別 token になる。
入力の全体像¶
flowchart TD
subgraph OBS["CABT observation / public state"]
ST["state_tokens<br/>global summary tokens"]
SO["state_objects<br/>visible board / hand / discard / stadium objects"]
HT["history_tokens<br/>recent event summaries"]
HO["history_objects<br/>cards attached to recent events"]
DT["deck_tokens<br/>own deck card ids"]
end
subgraph STATIC["Static embeddings"]
CID["card_id_embedding"]
CFE["CardStaticFeatures tokens<br/>from JSONL card_metadata"]
AFE["AttackStaticFeatures tokens<br/>from JSONL action_metadata"]
CSE["static_card_embedding"]
ASE["static_attack_embedding"]
CID --> CSE
CFE --> CSE
AFE --> ASE
end
subgraph TOKENIZE["Tokenization"]
GT["global token<br/>from state_tokens"]
BO["board object tokens<br/>static + dynamic + position"]
HE["history event tokens<br/>event + object + time"]
DV["deck summary token<br/>Set Transformer pooled"]
CLS["CLS token"]
end
subgraph ENCODER["Shared Transformer state encoder"]
SEQ["[CLS] + global + deck + board + history"]
ENC["TransformerEncoder"]
STATE["encoded state<br/>CLS representation"]
end
subgraph ACTIONS["Legal action options"]
AT["action_tokens<br/>option type / select context"]
AO["action_objects<br/>action / actor / target / attack"]
AOA["action object attention"]
SAA["state-action cross attention"]
AV["action option vectors"]
end
subgraph HEADS["Output heads"]
VH["value head"]
PH["action-conditioned policy head"]
AH["aux prize heads"]
BH["integrated belief heads"]
V["value v in [-1, 1]"]
PI["policy logits<br/>one logit per legal option"]
AUX["this-turn prize logits<br/>own/opp completion logits"]
BEL["opponent hand/deck/prize logits<br/>next threat + knockout threat"]
end
ST --> GT
CSE --> BO
SO --> BO
HT --> HE
CSE --> HE
HO --> HE
DT --> DV
CLS --> SEQ
GT --> SEQ
DV --> SEQ
BO --> SEQ
HE --> SEQ
SEQ --> ENC --> STATE
STATE --> VH --> V
STATE --> AH --> AUX
STATE --> BH --> BEL
AT --> AV
CSE --> AOA
ASE --> AOA
AO --> AOA
AOA --> AV
ENC --> SAA
AV --> SAA --> AV
STATE --> PH
AV --> PH --> PI
実装は src/pca/models/policy_value_unified/ に分けている。
card_static.py: card id と静的カード特徴をstatic_card_embeddingにする。tokens.py: global / object / history / action token の vector 化を行う。heads.py: value head、action-conditioned policy head、aux prize heads、integrated belief heads。model.py:UnifiedTokenPolicyValueNet本体。factory.py: checkpoint のmodel_classから legacy / unified を振り分ける。
実装上の入力と設計上の意味¶
UnifiedTokenPolicyValueNet.forward() は legacy model と同じ呼び出し形を保つため、既存 batch をそのまま受け取れる。設計上の入力との対応は次の通りである。
| forward 引数 | 形の目安 | 設計上の意味 | 主な使い道 |
|---|---|---|---|
state_tokens |
[B, state_len] |
global state summary | global token を作る。サイド枚数、山札枚数、手札枚数、トラッシュ枚数、turn flags、select context などの summary |
state_objects |
[B, object_count, W] |
board objects | 場、手札、トラッシュ、スタジアム、見えているサイド等のカード実体 |
history_tokens |
[B, H, history_len] |
history event summary | 直近ログの event type / action summary / 時系列情報 |
history_objects |
[B, H, object_count, W] |
history cards | ログに紐づくカード。公開サーチ、移動、トラッシュなど |
deck_tokens |
[B, deck_len] |
own deck card ids | default では Set Transformer で deck_vec に圧縮 |
action_tokens |
[B, A, action_len] |
legal option summary | CABT が返した合法手ごとの option type / select context |
action_objects |
[B, A, object_count, W] |
legal option structure | action 自体、actor、対象カード、付け先、進化先、攻撃対象、attack id など |
deck_vector |
[B, d_model] |
cached deck summary | self-play / eval の高速化用。deck_integration=pooled の時だけ使う |
出力は固定クラス分類ではなく、合法手数 A に合わせた action-conditioned output である。
| 出力 | 形 | 意味 |
| ----------------------------- | ------------------------: | -------------------------------------------------------- | ---------- |
| policy_logits | [B, A] | 各合法 option の logit。softmax すると pi(a | s) になる |
| value | [B] | 現局面の勝敗期待値。-1 が負け寄り、+1 が勝ち寄り |
| this_turn_prize_gain_logits | [B, 7] | 実教師軌跡で、このターンに自分が取ったサイド枚数 0..6 |
| own_prize_completion_logit | [B] | future_own_gain / current_own_remaining_prize の logit |
| opp_prize_completion_logit | [B] | future_opp_gain / current_opp_remaining_prize の logit |
| opponent_hand_logits | [B, card_id_vocab_size] | 相手手札にあるカードの multi-label logits |
| opponent_deck_logits | [B, card_id_vocab_size] | 相手山札にあるカードの multi-label logits |
| opponent_prize_logits | [B, card_id_vocab_size] | 相手サイドにあるカードの multi-label logits |
| next_threat_logits | [B, card_id_vocab_size] | 次に脅威になりうる公開カードの multi-label logits |
| knockout_threat | [B] | KO threat の確率スカラー |
このため、合法手が 3 個の局面では policy 出力は 3 個、合法手が 20 個の局面では 20 個になる。巨大な固定アクション空間を持たず、CABT が返す option をそのまま比較する。
State Encoder と Policy Head の関係¶
flowchart LR
subgraph STATE_IN["State-side inputs"]
CLS2["CLS"]
G2["global token"]
D2["deck summary token"]
B2["board object tokens"]
H2["history event tokens"]
end
STATE_SEQ["state token sequence"]
TR["shared Transformer encoder"]
SV["state_vec = encoded CLS"]
VALUE["value head<br/>MLP"]
AUXH["aux prize heads"]
BELH["belief heads"]
V2["v"]
AUX2["prize aux outputs"]
BEL2["belief outputs"]
subgraph ACTION_IN["Action-side inputs"]
A1["option 1 token"]
A2["option 2 token"]
A3["option N token"]
end
SCORE["policy head<br/>score(state_vec, action_vec_i)"]
L1["logit 1"]
L2["logit 2"]
L3["logit N"]
CLS2 --> STATE_SEQ
G2 --> STATE_SEQ
D2 --> STATE_SEQ
B2 --> STATE_SEQ
H2 --> STATE_SEQ
STATE_SEQ --> TR --> SV
SV --> VALUE --> V2
SV --> AUXH --> AUX2
SV --> BELH --> BEL2
SV --> SCORE
A1 --> SCORE --> L1
A2 --> SCORE --> L2
A3 --> SCORE --> L3
value head は state sequence の [CLS] 表現だけを見る。policy head は同じ state representation と、各合法手の action token を照合して option ごとの logit を出す。
Static Card Embedding¶
最初の v13 実装では、effect text encoder 本体はまだ入れない。代わりに CardStaticFeatures と CABT の攻撃定義から作った static feature token を使う。v13 の training は、これらの static feature table も self-play JSONL から復元する。つまり学習時に別途 EN_Card_Data.csv や attack_data.json を読む必要はない。
card_id_embedding(card_id)
card_feature_embedding(CardStaticFeatures tokens from search.card_metadata)
concat -> Linear/ReLU/LayerNorm -> static_card_embedding
ここで card_id_embedding は既知カード固有の性質を学ぶための経路であり、card_feature_embedding は HP、カード種別、進化段階、攻撃数、逃げエネなどの静的特徴を与える経路である。
CardStaticFeatures は self-play 収集時にカードDBから作り、各 decision record の search.card_metadata に埋め込む。training は JSONL 内の feature_tokens から card_feature_token_table を復元する。デッキ文脈を再構成できるように、record には見えているカードだけでなく、その record の自分側デッキに含まれるカードの metadata も入れる。
攻撃は card id とは別の attackId を持つため、別途 static_attack_embedding を作る。
attack_id_embedding(attackId)
attack_feature_embedding(AttackStaticFeatures tokens)
concat -> Linear/ReLU/LayerNorm -> static_attack_embedding
AttackStaticFeatures は self-play 収集時に CABT の cg.api.all_attack() から読み、各 decision record の search.action_metadata に埋め込む。training は simulator API も別の attack_data.json も読まず、JSONL だけから attack feature table を復元する。JSONL に card / attack metadata が無い場合は v13 training を停止するため、静的特徴が効いていない状態で静かに学習が進むことはない。将来的に attack text / skill text の事前埋め込みを入れる場合は、この static embedding の材料として追加する。
新しい self-play JSONL に入る metadata の例:
{
"search": {
"card_metadata": [
{
"card_id": 1001,
"name": "Mega Lucario ex",
"hp": 340,
"category": "pokemon",
"stage_or_kind": "mega",
"attack_count": 2,
"feature_tokens": [12, 301, 418, 9001]
}
],
"action_metadata": [
{
"option_index": 0,
"option_type": 12,
"card_id": 0,
"attack_id": 983,
"name": "Mega Brave",
"damage": 270,
"energies": ["Fighting", "Colorless"],
"text": "...",
"feature_tokens": [31, 520, 9102]
}
]
}
}
古い JSONL には search.card_metadata / search.action_metadata が無いので、v13 の JSONL-only training には新しく self-play を収集する必要がある。後処理で metadata を付与することも技術的には可能だが、その場合は変換時だけカードDB / CABT attack API が必要になる。
Board Object Token¶
盤面・手札・トラッシュ・スタジアム・見えているサイドなど、公開または自分視点で見えているカードは object token になる。
flowchart TD
CARD_ID["card_id<br/>例: Charizard ex"]
CARD_DEF["CardData / AttackData<br/>HP, type, stage, attack cost, damage"]
STATIC["static_card_embedding<br/>何のカードか / 何ができるカードか"]
DYN["dynamic state<br/>damage, HP proxy, energy_count,<br/>status, parent info, misc flags"]
POS["position context<br/>area, owner, slot, role"]
CONCAT["concat / fuse"]
OBJ["board object token<br/>このカード実体の表現"]
CARD_ID --> STATIC
CARD_DEF --> STATIC
STATIC --> CONCAT
DYN --> CONCAT
POS --> CONCAT
CONCAT --> OBJ
object row =
card_id
area
owner
slot
role
parent_area
parent_slot
hp_or_damage
energy_count
status
misc flags ...
この row の card_id で static_card_embedding を引き、残りの context token を position / dynamic 情報として埋め込む。
現実装では、area / owner / slot / role / dynamic fields は context token 群として埋め込み、まず平均 pool してから static_card_embedding と融合する。つまり設計意図としては「カードの中身 + どこにあるか + 誰のものか + 状態」を 1 token に持たせているが、各 context field を個別の typed embedding として concat する最終形ではない。将来、必要なら area_embedding / owner_embedding / slot_embedding / dynamic scalar projection を分けて concat することで、位置情報の順序と役割をより明示できる。
例:
自分の手札のモンスターボール
static_card_embedding[Monster Ball]
+ area=HAND
+ owner=SELF
+ slot=hand[3]
相手トラッシュのDrakloak
static_card_embedding[Drakloak]
+ area=DISCARD
+ owner=OPPONENT
+ slot=discard[2]
自分バトル場のリザードンex
static_card_embedding[Charizard ex]
+ area=ACTIVE
+ owner=SELF
+ slot=0
+ damage / energy_count / status
エネルギーや道具、進化前カードのように、別のポケモンに付属するカードは parent_area / parent_slot を持つ。これにより「この炎エネルギーはバトル場のリザードンexについている」「この進化前はベンチ 2 番目のポケモンの下にある」といった関係を token 側から渡す。
同じカード ID でも、position と dynamic が異なるため、Transformer には別の token として渡る。
flowchart LR
S1["static_card_embedding<br/>Monster Ball"]
H1["area=HAND<br/>owner=SELF<br/>slot=3"]
T1["hand object token"]
S2["static_card_embedding<br/>Monster Ball"]
D1["area=DISCARD<br/>owner=SELF<br/>slot=7"]
T2["discard object token"]
S1 --> T1
H1 --> T1
S2 --> T2
D1 --> T2
Global State Token¶
サイド枚数、山札枚数、手札枚数、トラッシュ枚数、turn、supporterPlayed、energyAttached、select context のような、カード 1 枚には紐づかない全体情報は global token にまとめる。
global token =
prize_count_self / opponent
deck_count_self / opponent
hand_count_self / opponent
discard_count_self / opponent
turn / step / current_player
supporter_played / energy_attached
select_context
これにより、「サイドが残り 1 枚なので攻撃の価値が高い」「山札が少ないので deck-out 負けが近い」といった全体スカラーを、各 object token と attention で結びつけられる。
History Event Tokens¶
直近ログは event token として入れる。event type、関連カード、from/to area、owner、時系列位置を持つ。
history event token =
event_type
card static embedding (if any)
from_area / to_area
owner
time_position
現実装では history_tokens と history_objects を受け、イベントごとに flat event vector と object mean を融合する。相手の公開サーチ、手札に戻したカード、トラッシュされたカードなどは PublicKnowledgeTracker と Belief の制約にも使われるが、Policy/Value 側でも history token として参照できる。
Action Option Tokens¶
Policy head は固定アクション分類ではない。CABT が返した合法 option をそれぞれ token 化し、encoded state と照合して logit を出す。
action token =
option_type
select_context
action objects =
action header # option type / select context
action actor # どのカードや場の実体が行動するか
action card # 手札などから使うカードがある場合
action target # 付け先 / 進化先 / 攻撃対象 / 移動先
action attack # attackId と static_attack_embedding
action_objects は平均 pool ではなく、小さな attention pooling で 1 つの action vector にまとめる。さらに action vector を query として encoded state sequence に cross-attention させる。これにより、「この技はどのバトルポケモンが使うのか」「この attach はどのポケモンに付くのか」「この attackId のコスト/ダメージは盤面のエネルギーや相手 HP と噛み合うのか」を policy head が参照しやすくなる。
これにより、局面によって合法手の数が違っても、その数だけ action token を作ればよい。policy output は pi(a | s) の合法手分布になる。
Aux Prize Heads¶
v13 の aux prize heads は、勝敗 value だけでは表しにくい「サイド取得に近い局面か」を補助的に学習する。学習済み checkpoint では、v13_aux_prize_race search value profile を使うことで、ISMCTS leaf value に小さい重みで混ぜられる。
leaf での使い方は次の通りである。aux は勝敗 value を置き換えず、サイド取得に近い局面を探索内でわずかに優先するための tie-break として扱う。
aux_prize_value =
aux_prize_completion_weight * (own_prize_completion - opp_prize_completion)
+ aux_this_turn_prize_weight * E[this_turn_prize_gain] / 6
leaf_value =
progress_value
+ model_value_weight * NN win value
+ aux_prize_value
ISMCTS の leaf が相手手番側の observation になっている場合、own/opp はその observation の current player 視点なので、root player 視点に合わせて符号を反転する。
保存される JSONL schema は SelfPlayRecord.aux である。
{
"aux": {
"this_turn_prize_gain": 2,
"this_turn_prize_gain_mask": true,
"own_prize_completion": 0.5,
"opp_prize_completion": 0.25,
"prize_completion_mask": true,
"current_own_remaining_prize": 4,
"current_opp_remaining_prize": 4,
"future_own_prize_gain": 2,
"future_opp_prize_gain": 1
}
}
ラベルは oracle 最大値ではなく、実際に self-play / rule teacher が辿った教師軌跡から作る。
this_turn_prize_gain: decision 時点からそのターン終了までに、自分が実際に取ったサイド枚数。値は0..6。own_prize_completion:future_own_prize_gain / current_own_remaining_prize。opp_prize_completion:future_opp_prize_gain / current_opp_remaining_prize。future_own_prize_gain: decision 時点以降、ゲーム終了までに自分が実際に取ったサイド枚数。future_opp_prize_gain: decision 時点以降、ゲーム終了までに相手が実際に取ったサイド枚数。
mask 方針:
- 既存 JSONL のように
auxが無い record は、すべて mask 0 として扱う。 - unfinished game の completion target は mask 0。未完了の最終ターンは
this_turn_prize_gain_maskも 0。 - deck-out / pokemon-out でゲームが終わった場合も、サイド取得枚数そのものは実際に取得した物理枚数を使う。勝敗ルールは main value の target として扱い、aux prize target とは分離する。
学習 profile は次の名前で切り替える。
--aux-prize-target-profile none|completion_v1
--this-turn-prize-loss-weight
--own-prize-completion-loss-weight
--opp-prize-completion-loss-weight
completion_v1 は this_turn_prize_gain_logits に cross entropy、own/opp completion logits に BCE-with-logits を使う。
Integrated Belief Heads¶
v13 unified model には standalone BeliefNet と同等の belief heads を載せている。入力は public observation だけであり、推論時に相手手札やサイドの正解を見ない。--full-observation-targets 付き self-play で保存した BeliefTrainingTarget を教師ラベルとして使う。
出力:
opponent_hand_logits : 相手手札カードの multi-label logits
opponent_deck_logits : 相手山札カードの multi-label logits
opponent_prize_logits : 相手サイドカードの multi-label logits
next_threat_logits : 次に脅威になりうるカードの multi-label logits
knockout_threat : KO threat probability
forward_state(...) は action tokens なしで shared state encoder と aux / belief heads だけを呼ぶための API である。ISMCTS で hidden-state prior だけ必要な場合、合法手の policy logits を作らず belief heads を取り出せる。
belief-source の挙動:
| 値 | 挙動 |
|---|---|
auto |
--belief-checkpoint があれば standalone BeliefNet を使う。無ければ v13 policy checkpoint の integrated belief heads を使う。 |
separate |
standalone BeliefNet だけを使う。--belief-checkpoint 必須。 |
policy |
v13 policy checkpoint の integrated belief heads だけを使う。 |
学習 profile:
--integrated-belief-profile none|belief_v1
--belief-hand-loss-weight
--belief-deck-loss-weight
--belief-prize-loss-weight
--belief-threat-loss-weight
--belief-ko-loss-weight
belief_v1 は hand/deck/prize/threat に multi-label BCE、knockout threat に BCE を使う。belief が無い record は mask 0 になるので、既存 JSONL でも学習自体は通る。ただし integrated belief を実際に学習するには --full-observation-targets 付きで収集した JSONL が必要である。
Deck Integration¶
自分が使うデッキ情報は標準入力である。初期 v13 では 60 枚を shared Transformer に直接入れず、Set Transformer で deck_vec に圧縮して deck_summary token として入れる。
deck_integration=pooled
deck_tokens -> Set Transformer -> deck_vec -> deck_summary token
deck_integration=tokens
deck_tokens -> card object tokens
future experiment only
pooled を default にする理由は、60 枚すべてを state sequence に足すと token 数と forward cost が増えやすいためである。将来的に「自分のデッキ 60 枚をすべて shared Transformer に入れる」実験をするために tokens の config 値は予約している。
deck_integration=tokens は「deck vector を渡さず、deck_tokens をそのまま token 列化する」モードである。誤って cached deck_vector が渡った場合は、pooled 実験と tokens 実験が混ざらないように model 側でエラーにする。
入れない情報¶
Policy/Value model は提出時に見えない情報を直接見ない。
- 相手手札の正解
- 相手山札の順番
- サイド落ちの正解
- 自分山札の順番
これらは training label や Belief target には使えるが、Policy/Value の観測入力には入れない。実戦時は public observation と Belief-guided ISMCTS で hidden state を扱う。
Legacy との互換¶
legacy checkpoint は model_class が未指定でも ActionConditionedPolicyValueNet としてロードする。新 checkpoint は model_class=UnifiedTokenPolicyValueNet を保存する。
ロード経路:
model_class missing / legacy / ActionConditionedPolicyValueNet
-> ActionConditionedPolicyValueNet
model_class unified / UnifiedTokenPolicyValueNet
-> UnifiedTokenPolicyValueNet
train.py、selfplay.py、tournament.py、submission/main.py、decision/batched_policy.py は factory 経由でロードするため、旧新の checkpoint を同じ CLI から扱える。
selfplay / eval / submission では、checkpoint に model_class=UnifiedTokenPolicyValueNet が保存されていれば CLI 側で --model-class unified を毎回指定する必要はない。model_class が無い古い checkpoint は legacy model として扱う。
v13 の初期設定¶
model-class: unified
structured-input-mode: unified
deck-integration: pooled
deck-context-mode: set_transformer
structured-context-mode: objects
history-context-mode: event_transformer
card-ref-context-mode: none
aux-prize-target-profile: completion_v1
integrated-belief-profile: belief_v1
card-ref-context-mode を無効にするのは、Unified では card id と静的特徴が object token に直接入るため、旧モデルの補助 card-ref branch と役割が重なるためである。
今後の拡張¶
- effect text / attack text embedding を
static_card_embeddingに追加する。 deck_integration=tokensで自分のデッキ 60 枚を shared Transformer に直接入れる実験を行う。- action option token の構造を強化し、攻撃 ID、対象ポケモン、進化元、付け先、選択 context をより明示的に分ける。
- aux prize heads を ISMCTS leaf value に小さい重みで混ぜるかを、calibration と評価ログを見て判断する。
- token 数と forward 時間を smoke log に出し、legacy と unified の速度差を測る。