Splendor AIを自己対局に進めて、最初のモデル更新を通した

前回は、ルールベースの教師から policy と value を同時に学習するところまで進めた。

policy は次にどの手を選ぶか、value はその局面から最終的に勝つか、引き分けるか、負けるかを予測する。教師データ上ではどちらにも学習できている信号が出たが、その時点ではまだ実際の対局で強いかどうかは分からなかった。

次にやったのは、このモデルをゲーム木探索に接続し、そこから自己対局データを作って次のモデルを学習するところまで一度通すことだった。

今回は、教師あり学習で作った最初のモデルが search player になり、自己対局から challenger を作り、最初の promotion まで進んだ話を書く。

まずモデルを探索に接続する

ニューラルネットワーク単体では、policy が最も高い手をそのまま選ぶだけになる。

実際にゲームをプレイするときは、その予測を PUCT というゲーム木探索に入れる。PUCT は policy を「どの手を優先して読むか」の事前情報として使い、value を「読んだ先の局面がどれくらい良さそうか」の評価として使う。

今回の最初の評価では、1手を選ぶたびに32回の simulation を行った。

最初に確認したかったのは、このモデルが少なくとも探索付きのプレイヤーとして正常に動き、非常に弱い基準を越えられるかだった。

同じ初期配置を使い、席順を入れ替えながら対局した結果は次のようになった。

対局結果
ルールベース教師 vs random200勝0敗
PV + PUCT vs random200勝0敗
PV + PUCT vs ルールベース教師111勝89敗

PV は policy-value model の略で、ここでは前回の記事で学習したモデルを指す。

random には200局すべて勝ったので、自己対局を始めるための最低限の gate は通過した。

一方、ルールベース教師との対局は111勝89敗だった。席順を入れ替えた100ペア単位で見ると score は0.555で、95%区間は0.485から0.625だった。

点推定ではPV側が上だが、区間は0.5をまたいでいる。この段階では「教師より強くなった」とは扱わず、少なくとも同程度の範囲で対局できている、とした。

前回の記事では教師の手をどれくらい模倣できたかを見ていた。ここで初めて、学習したモデルを探索に入れてゲームそのものをプレイするところまでつながった。

自己対局では探索結果を次の教師にする

ここからは、人間が書いたルールや元の教師だけを学び続けるのではなく、現在のモデル自身に探索させて学習データを作る。

1局の自己対局では、各手で現在の policy-value model を使って PUCT を行う。

探索後には、各合法手が何回訪問されたかという分布が得られる。この分布を次の policy の教師信号として保存する。value については、その対局の最終的な win / draw / loss を教師信号にする。

流れは単純化すると次のようになる。

現在のモデル
PUCTで自己対局
探索後のpolicy target + 最終勝敗
学習データに追加
現在のモデルからwarm startして再学習
challenger
現在のモデルと対局
promote または reject

ここで現在採用しているモデルを incumbent、新しく学習した候補を challenger と呼んでいる。

challenger が incumbent を明確に上回ったときだけ更新する。学習 loss が下がっただけではモデルを置き換えない。

いきなり大きく回さず、先に64局だけ測った

自己対局は PUCT を毎手行うので、教師データ生成よりかなり重い。

そこで本番の generation を決める前に、32個の初期配置から席順を入れ替えた64局だけを実行した。

Apple M2 の CPU で88.02秒かかり、3,816局面が得られた。単純換算では約2,618 games/hour、約156,000 decisions/hourだった。

探索後の policy target も確認した。

指標結果
平均 game length59.6 decisions
policy target の normalized entropy0.3102
top action の平均確率0.6450
max-ply まで終わらなかったゲーム0

policy target はほぼ one-hot でも uniform でもなく、探索によって複数候補に確率が分かれていた。

この測定を使って、最初の generation は2,048局にした。

最初の generation は supervised data を残した

最初から自己対局データだけで学習するのではなく、前回作った supervised data を anchor として残した。

実際に学習へ入ったデータは、元の教師ありデータが約80%、今回の自己対局データが約20%だった。

自己対局では1,024個の初期配置から席順を入れ替えた2,048局を生成し、118,988局面が学習対象になった。全局が通常のゲーム終了まで到達し、160 ply の上限に達したゲームはなかった。

challenger は incumbent の checkpoint から warm start し、混ぜたデータを1 epochだけ学習した。更新回数は1,045 stepだった。

最初の世代なので、ここで大量に学習して最適化するより、自己対局から昇格判定までの一連のループを実際に通すことを優先した。

free Colabでは一度止まった

最初は free Colab の T4 でgenerationを回した。

しかし、長い self-play の途中で実行クライアント側の約90分の timeout に当たり、その後セッション自体も失効した。自己対局データ、challenger、arena結果のどれも回収できなかったので、この実行から結果を推測することはせず、単純に未完了として止めた。

その後、同じ generation 設定を paid Colab の L4 で再実行した。

こちらは最後まで完了し、self-play、学習、ONNX export、昇格戦、結果の保存とローカルでの再読込まで通った。

ループ本体には2時間39分ほどかかった。

興味深かったのは、L4でCUDAを使える状態だったにもかかわらず、自己対局の実測 throughput が約873 games/hourだったことだ。

Apple M2で行った小さい calibration の約2,618 games/hourより遅い。

現在の探索はニューラルネットワーク推論を十分に batch 化できておらず、GPUの計算能力より1回ずつの推論 latency に支配されている。GPUを使えばそのまま高速になるわけではないことも、この実行で分かった。

challenger が最初の promotion を通った

最後に、学習した challenger と元の incumbent を同じ32-simulation PUCTで対局させた。

100個の初期配置について席順を入れ替え、合計200局を実行した。

結果は次の通りだった。

結果局数
challenger 勝ち114
引き分け3
incumbent 勝ち83

game score rate は0.5775だった。

昇格判定は単純に勝率が50%を超えたかではなく、席順を交換したペアを単位にして不確実性を含めて判定するようにしている。

今回の one-sided lower bound は0.5166で、事前に決めていた0.5の threshold を上回った。

そのため challenger を新しい incumbent に昇格させた。

これで、

教師ありbootstrap
探索付きモデル
自己対局
challenger学習
paired arena
promotion

までが初めて一周した。

ここで分かったこと

今回の結果だけで、自己対局を繰り返せば継続的に強くなるとはまだ言えない。

確認できたのは、教師あり学習したモデルを初期 incumbent にして、探索で新しいデータを生成し、そのデータを一部混ぜて学習した challenger が、事前に決めた promotion gate を1回通ったことだ。

ただし、一世代目が通ったことと、何世代も安定して改善できることは別の問題になる。

また、処理時間の大半を占めたのは自己対局だった。現在の unbatched な探索ではGPUを使っても十分速くならないので、世代数やデータ量を増やす前に search inference の throughput を改善する余地もかなり大きい。

次は generation を複数回つないで、promotion と rejection の両方を含めてループがどう振る舞うかを見ることになる。

前回は「ニューラルネットワークに教師を学習させる」ところまでだった。今回はそこから一歩進み、「現在のモデルが探索して作ったデータで、次のモデルを作る」ところまで到達した。


この記事は、実装・実験記録をもとに、本文の大部分をLLMが執筆し、筆者が内容を確認・編集しています。