コンテンツにスキップ

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.csvattack_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_idstatic_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 でも、positiondynamic が異なるため、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_tokenshistory_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_v1this_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.pyselfplay.pytournament.pysubmission/main.pydecision/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 の速度差を測る。