Splendor AIのモデル構造と特徴量設計
具体的な学習結果や対局評価に入る前に、現在稼働しているニューラルネットワークがゲーム盤面からどのような情報を受け取り、何を予測しているのか、そのアーキテクチャと特徴量設計を整理しておく。
Splendor(宝石の煌き)は、宝石トークンを集めて市場のカードを購入し、獲得したカードのボーナスを以降の購入割引として活用しながら勝利点を積み上げていくボードゲームである。場にはレベル1〜3のカードが公開されており、手番ではカードを購入するだけでなく、山札や場からキープ(予約)して手元に確保することもできる。最終的には誰かが15点以上に到達したラウンドでゲームが終了する。
このようなゲーム特性上、盤面状態は囲碁や将棋のような固定サイズの2次元画像グリッドとして表現するよりも、プレイヤー、カード、貴族タイル、トークン在庫、そして選択可能な合法手の集合(オブジェクトの集合)としてモデル化する方が圧倒的に自然である。
入力構成:「盤面エンティティ」と「候補手特徴量」
ネットワークに入力される特徴量は、大きく以下の要素群に分かれている。
global state + public deck membership(全体情報と山札状況)bank tokens(銀行のトークン残数)players x 2(各プレイヤーの状態)market cards x 12(場に公開されたカード)nobles x 3(貴族タイルの獲得条件)reserved cards x 6(予約されたカード)legal actions x K(合法手の候補群)は局面ごとに変動する合法手の総数である。
各プレイヤー情報には、現在の得点、所持トークン数、購入済みカードによる恒久ボーナス、予約カード枚数などを格納する。カード情報には、属性色、勝利点、基本コストに加えて「手元ボーナスを差し引いた実質コスト」も明示的に算出して渡している。
さらに、各合法手に対しても47次元の特徴量ベクトルを付与している。トークン獲得、カード購入、カード予約といった着手の種別や支払い内容、対象カードの情報に加え、その手を実際に盤面に適用した直後の状態(afterstate)における所持トークン、ボーナス、得点、銀行在庫の変化分まで含めている。
すなわちモデルには、単に「カードAを購入する手」というラベルだけでなく、「このカードを購入した場合に盤面がどう変化するか」という遷移後の状態まで直接提示している。
25個のオブジェクトをSelf-Attentionで相互作用させる
盤面状態は、最大25個のオブジェクトトークンとして表現される。
1 global1 bank2 players12 market cards3 nobles6 reserved cards各オブジェクトを128次元のベクトルへと射影し、4ヘッドのSelf-Attentionブロックを2層適用する。その後、エンティティの種類ごとにmean/maxプーリングを行い、256次元の盤面状態ベクトル(state vector)へと集約した上で、2ブロックのResidual MLPを通す。
25 objects ↓2 x Self-Attentionブロック ↓種類別のmean/maxプーリング ↓256次元 盤面ベクトル ↓2 x Residual MLPブロック開発初期は各オブジェクトを独立にエンコードして即座にプーリングしていた。しかし Splendor においては、「相手が次の手番で購入可能なカード」「自分の手元ボーナスと市場カードのコストとの差分」「購入カードの色と貴族タイルの獲得条件」といった、オブジェクト間の相対的な関係性こそが局面評価の急所となる。
早い段階でプーリングして集約してしまうと、そうした重要な相関関係を後段のMLPで復元することが難しくなる。そこで現行の設計では、まずオブジェクト同士にSelf-Attentionを介した相互参照を行わせてから情報を圧縮するパイプラインを採用している。
方策(policy)と局面評価(value)を同一の盤面表現から出力
方策ヘッド(policy head)は、固定数の着手クラス分類を行うのではなく、その局面で有効な各合法手に対して1つずつロジットを出力する。
候補手の特徴量を96次元にエンコードし、対象となる市場カード、予約カード、貴族タイルが存在する場合は、Attention適用後のオブジェクト表現も参照ベクトルとして結合する。これを256次元の盤面ベクトルと連結し、多層パーセプトロンによって候補手のスコアを算出する。
state(盤面): 256次元candidate(候補手): 96次元referenced object(対象オブジェクト): 96次元 ↓448 -> 256 -> 128 -> 1次元(着手ロジット)一方の局面評価ヘッド(value head)は、同一の盤面ベクトルから敗北・引き分け・勝利(Loss / Draw / Win)の3クラスのロジットを出力する。ゲーム木探索時には、ソフトマックス関数を通した確率から P(win) - P(loss) を計算し、局面の期待勝率として用いる。
完全な巨大Transformer構成を採用しなかった理由
候補手まで含めたすべてのアクション空間を単一の巨大なTransformerで一括処理する設計をとれば、表現力そのものはさらに高まる可能性がある。
しかし、このニューラルネットワークは静的な単発推論を行うだけでなく、PUCT探索のシミュレーションループから毎手何十回、何百回と呼び出される。モデルの計算負荷を重くしすぎると、同一の持ち時間の中で展開できる探索シミュレーション回数が大幅に削られてしまう。
そこで現行のアーキテクチャでは、盤面内の複雑な相互作用はAttentionでリッチに捉えつつ、候補手側のスコアリングは軽量なMLPに委ねることで、推論速度と表現力のバランスを最適化し、探索の深さを活かせる設計に落とし込んでいる。
特徴量生成エンジンをC++側に集約した背景
ニューラルネットワークの学習コード自体はPyTorch(Python)で実装しているが、ゲームルールに密結合した特徴量抽出パイプラインはすべてC++で実装している。
特に候補手の着手後状態(afterstate)を算出するには、その手を仮想盤面に正確に適用する必要がある。これをPython側でも別個に実装してしまうと、合法手生成、トークン支払い、予約処理、終了判定などの複雑なゲームルールがPythonとC++の間で二重管理になってしまう。
そこで、C++側の公式なゲーム遷移エンジンを直接呼び出して特徴量を生成する設計とした。
const TransitionResult after = apply_generated_candidate(state, config, catalog, candidate);システム全体の責務分担は明確に分離している。
C++側 ゲームルールの執行 合法手の列挙 特徴量ベクトルの抽出 木探索(PUCT) 本番推論実行(ONNX Runtime)
Python / PyTorch側 ニューラルネットワークモデルの定義 損失関数の設計 オプティマイザによる学習 ONNX形式へのエクスポート学習済みのPyTorchモデルはONNX形式でエクスポートされ、実対局や探索シミュレーション時にはC++環境からONNX Runtimeを通じて高速に実行される。この設計により、ルールやモデル構造の二重実装リスクを排除しつつ、高い実行性能を維持している。
この記事は、実装・実験記録をもとに、本文の大部分をLLMが執筆し、筆者が内容を確認・編集しています。