9ボールのAIプレイヤーを作る:直線カットショットの候補生成

Reddit r/MachineLearning / 2026/5/5

💬 オピニオンDeveloper Stack & InfrastructureTools & Practical UsageModels & Research

要点

  • 著者は9ボールのAIプレイヤーを、パターンプレイ支援のために構築しており、テーブルレイアウトから勝率 p(win) を推定するトランスフォーマーベースのモデルを用いている。
  • システムは直線カットだけでなく、バンク、キック、キャロム、コンビネーションショット、さらにセーフティも含めて候補ショットを生成し、各候補の結果状態に p(win) モデルを適用して最適なショットを選定する。
  • プール物理のシミュレーションにはコストがかかるため(オープンソースの PoolTool を使用)、候補評価の効率化が重要な課題として述べられている。
  • 最適化の要点として、(オブジェクトボール位置、ポケット、速度) ごとに事前計算した「acceptance window(受け入れ範囲)」を使い、成功させるために成立するOB(オブジェクトボール)の発射角の範囲を絞り込む。
  • ナイーブなグリッド探索では計算が膨大になること、また CMA-ES などの反復最適化でも学習規模では十分に速くならないため、候補を高速にふるい分ける工夫が必要だと説明している。
Building a 9-ball AI player: Candidate generation for direct cut shots [P]

私はパターンプレイを助けるための9ボールプレイヤーを作っています。次のボールを作る方法はいくつもあり、場合によっては複数の明らかなポケットに入れられることもあります。どれを選ぶべきかは、そのショットを入れる確率だけでなく、次のショットに有利な位置にボールを残せるか、そしてその後のポジション取りもしやすいかに依存します。そこで、以下のコンポーネントを作りました:

  • テーブルレイアウトを与えると p(win) を学習する、トランスフォーマーベースのモデル。
  • 候補ショット生成器:カットショット、バンクショット、キックショット、キャロム、コンビネーションショットに加えてセーフティも含める。
  • 評価器:p(win) モデルに基づいて、各候補ショットの結果状態に対する最良のショットを選ぶ。

真値(ground truth):pooltool

プールの物理はよくモデル化されていますが高価です。私は pooltool の Python ライブラリを使っています。これは精度の高いボール‐クッション‐ポケット‐フェルト間の相互作用を備えた、堅実なオープンソースのビリヤードシミュレータです。典型的なショット評価で出てくる 1〜3 個のオブジェクトボール配置に対して、1 ショットのエンドツーエンドのシミュレーションは 1CPUスレッドで約 5〜15ms かかります。フルラック(オブジェクトボールが9個)では、追跡するペアごとの衝突が増えるため、約 20〜50ms に跳ね上がります。

速そうに聞こえますが、計算するとそうでもありません。各レイアウトで 6 ポケットすべてに向けた候補ショットを作りたいのですが、各ポケットには探索すべき 5 次元のパラメータ空間があります:速度、狙い角、キュースティックの高さ(elevation)、サイドスピン、フォロー/ドロー。

素朴なグリッドスイープでも、例えば各次元を 10 ステップで粗く区切るだけで、100K(組み合わせ数)×10ms = 1 ポケットあたり約 17 分、1 回の意思決定ごとにかかります。CMA-ES のような反復型オプティマイザなら 1 ポケットあたり約 500〜1000 回のシミュレーションにまで下がりますが、それでも 1 ポケットあたり 5〜10 秒、1 レイアウトあたり 30〜60 秒です。価値ネットワーク(value network)を何百万もの意思決定で学習するとなると、計算資源の投入は数か月規模になります。

候補の高速評価

ショット選択は、「すべての可能なショットをシミュレーションしなくても入るのか」を知る必要があります。ただし、現時点ではテーブルの最終位置がどうなるかは必要ありません。

そこで私は、ショットを「オブジェクトボールに何をさせる必要があるか」と「それを実現するためにキューボールをどう打つか」に分けて捉えることで問題に取り組みました。まずショット作成における最初のコンポーネントが Acceptance window(受け入れ窓)のルックアップです。これは (オブジェクトボール位置, ポケット, 速度) ごとにオフラインで事前計算してあります。具体的には、選択したポケットに対して異なる速度でボールを実際に落とす OB(オブジェクトボール)離脱角(departure angle)の範囲です。これは「ボールに何をさせるべきか」という仕様そのものです。ポケットのジョー(jaw)の幾何、レールに沿う効果(down-the-rail effect)など、すべてを含んでいます。

次に Shot-index(ショットインデックス)のルックアップテーブルを作成しました。望ましい OB 離脱角(キュー→OB 線からの偏向角として測定)とキュー→OB 距離が与えられたとき、あらかじめ pooltool で離散グリッドの (距離, 速度, 狙いオフセット, スピン, ドロー) 上をサンプリングして事前計算しておいたインデックスから、その幾何を作り出すショットを引き当てます(elevation はシミュレーションしない)。インデックスは OB 離脱角をキーにしています。ルックアップの結果として、望ましい角度へ OB を送る (speed, aim_offset, spin, draw) の候補タプルが返ります(距離はレイアウトによって固定です)。

これは改善でしたが、離散化のせいで穴(抜け)が残ります。そこで、連続空間への汎化のための throw model を構築しました。これは小さな MLP で、(キュー→OB 距離, 速度, 狙い角, スピン, ドロー, elevation) を入力として OB 離脱の偏差(deviation)を予測します。これは shot-index のデータを連続空間に拡張するものです。アーキテクチャはかなり素直です。特徴量は aim_offset、distance、speed、side spin、draw、elevation です。出力は、キューとオブジェクトボールの角度に対する偏差です。隠れ層は隠れ層ごとに 128 次元の全4層で、ReLU 活性を使用し、合計で約 50k パラメータです。私は 5M ショットでモデルを学習しました(生成に約 6 時間)。検証セット(約 1.1M)での Mean Angle Error は約 0.2 度でした。また、学習のために左右の対称性を用い、データを2倍使えるようにして、プレイ中にミラーリング処理を気にする必要がないようにしました。

この仕組みの良いところは、ショットインデックスを使ってショットの十分良い初期パラメータセットを得たうえで、さまざまなパラメータに小さな摂動を加え、GPU上で throw model によってそれらをバッチ評価できる点です。私のセットアップでの速度向上は、物理エンジンを通してそれらすべてのショットをシミュレートする場合と比べて約 10000 倍で、自己対戦用の十分なデータを生成するうえで大きな差になります。1000 個の候補ショットのバッチは、評価に 1 ms かかります。平均 10 ms の 1000 回のシミュレーションと比べてください。

次に、速度、スピン、ドロー周りのバケツ(bucketing)によって、意図したポケットの acceptance window 内に入ると予測されるすべてのショットをクラスタリングします。そして各クラスタから代表となるショットを選び、物理エンジンでノイズ付きシミュレーション(ショットに実行ノイズを加える)を用いて評価します。確実に実行できない「100万分の1」のようなショットを見つけたくありません。最後に、p(win) モデル(この投稿では詳細に触れていません)を使って、ショット後のテーブル状態の最大期待値を取ることで、ショット選択を行います。

候補を見つけた後は依然として物理シミュレーションを行うので、エンドツーエンドの速度向上はおよそ 50〜100 倍でした。

ショット選択の可視化

より具体化するために、キューボールがテーブル中央、8 ボールが左上方向、9 ボールが下レールにある 8〜9 ボール配置を用意しました。色は 9 ボールの位置に基づく p(win) を表しています(9 ボールはショット中に動かさないものとしてあります)。この投稿では、選択された 10 ショットを 20 回ずつシミュレートしました。結果として、10/20 のうち 6 ショットは 20/20 全て成功、3 ショットは 19/20、残り 1 ショットは 15/20 成功でした。キューボールの軌道の色は、これら 20 回のショットでの成功率を反映しています。各 10 ショットについて 20 回のうち 1 つだけノイズありシミュレーションをプロットしました。残りはかなり近いはずです。

9 ボールの周りの黒い領域は、9 ボールから 1 ボール未満の距離内にあり、キューボールが 9 ボールの空間に侵入してしまうため無効な位置を表しています。

この投稿では直接ショットのみを話しましたが、p(win) のヒートマッププロットに織り込まれている型(テンプレート)のバンクショット、キックショット、キャロム、コンビネーションショットもあります。もちろん、9 ボールのみのケースではキャロムやコンビネーションはここには適用されません。

次は何をする?

私はカリキュラム学習(curriculum learning)に取り組んでいます。9 ボールだけを使った p(win) モデルは素直です。9 ボールをポケットに入れれば勝ち(ただしスクラッチしない場合)。スクラッチしたら負けです。なぜなら、まともな相手なら手球付きで 9 ボールを入れてくるからです。外した場合の報酬は、結果として得られた状態からの (1-p(win)) です。私はフルのショット選択オプションを使って約 100k ショットをシミュレーションし、p(win) モデルに対して 4 倍の対称性を使用しました。モデルが更新されるたびに 100% 成功しないショットについてはショット選択を作り直します。モデルの更新によって、異なるショット選択/セーフティ位置につながる可能性があるためです。

単一ボールのシナリオが「解けた」ら、2 ボールのシナリオに進みます。オンボールを作ると、モデルから価値を参照する「解けた状態」になります。ミスした場合は、モデルのイテレーション間で再評価されます。私はカリキュラムを進め、 ボールのシナリオをマスターし、そこから最大 9 ボールまでの n ボール配置をすべてマスターしていきます。

うまくいかなかったことはいろいろ試しました。たとえば、(ミラーリングに基づく)ゴーストポケットの角度を特徴量として与えたとき(物理に基づいたML)、バンクモデルがかなり改善しました。興味があれば、そのどれについてでも詳細を共有できます。

提出者: /u/ArithmosDev
[リンク] [コメント]