(最終更新2025年2月1日)
ポーカーを普通にプレイするの飽きてきました。ポーカーしてるときの80%の時間はボケーっとしてます。
お酒を飲みながらやったり、猪突猛進特大コールしたり、できるだけ楽みながらやってますが⋯。
なのでポーカーAIを自作してみることにしました。
始めてから10日で学んだことと、開発したことを一旦まとめてみました。
調べることや試行錯誤が多すぎて開発に途中で飽きてるかもしれません。
いつでも開発を再開できるように、やったことは一通りメモしておきます。
ポーカー歴は半年、 10日前までは強化学習の素人だったので、間違いがあっても大目に見てください。
殴り書きに近いので読みづらかったり説明が足りてない箇所があるかもしれないです。
自分が作りたいポーカーAI
ポーカー界でよく使われているツールにGTO(Game Theory Optimal)というものがあります。相手がどのような戦略を取ったとしても自分の期待値をなるべく落とさないアクションを教えてくれるツールです。
GTOはゲーム理論に基づいていて、完全な平衡(ナッシュ均衡)を目指しています。「完全な平衡」とはつまり、「各プレイヤーが自分の戦略を変更しても得をしない状態」であり、「すべてのプレイヤーが最適解に従う状態」です。
現実のポーカーにおいて、すべてのプレイヤーが最適解に従う状況なんてあるのでしょうか?
ぶっちゃけないです。人によってゲームの理解度がまちまちですし、ハンドレンジやアグレッションには個性が出ます。GTOが最適解ではない状況はいくらでも発生します。
自分が作ってみたいのは、GTOを超える「勝ちにいくAI」です。
GTOのようなナッシュ均衡戦略をベースに持ちつつ、相手の傾向を考慮してエクスプロイトするような機能までつけれたら最高です。
あと、目的は強いAIを作ることだけではないです。知的好奇心を満たして幸せホルモンを出したり、小さな成功体験を積み重ねてアドレナリンを出すことも含まれるので、強いAIを作れなくても目的は半分達成したようなもんです。
開発の難しさ
囲碁で人間より強いAIが登場したのは2015年(Alpha碁)です。
ノーリミット・テキサスホールデム・ポーカーのヘッズアップ(1対1)で人間より強いAIが登場したのは2017年(Libratus)で、3人以上のポーカーだと2019年(Pluribus)です。
囲碁の方が圧倒的に複雑ですが、ポーカーの方がAI開発が難しかった理由は、ポーカーが不完全情報ゲームだからです。囲碁ではすべてのプレイヤーがゲームの状態を完全に把握できますが、ポーカーでは相手の手札や次に出るカードを知ることができず、確率論や期待値に基づいた意思決定をする必要があります。相手の行動から隠れた情報を推測する「読み」が重要になってきます。
加えて、ポーカーはカードが52枚もあるせいで状態空間(≒パターン)が意外と大きいです。状態空間を数字で表すと、チェスは10の50乗(100,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000)、将棋は10の80乗、囲碁は10の172乗です。ポーカーは、プリフロップで10の18乗で、ショーダウンまで行くと10の160乗だそうです。ノーリミットのベット額は自由なため、細かいベット額まで考慮すると状態空間は事実上無限になります。
ポーカーAIはデータサイエンスのPhDチームが数年かけて開発するような代物です。自分はデータサイエンスが専門外で、なおかつ時間が限られているサラリーマンですし、開発に使える機器は一般的なゲーミングPCのみです。でも、できるとこまではやってみます。データサイエンスが専門ではないサラリーマンの人たちの中ではトップクラスのものを作ってみたいです。
プロトタイプの挙動を見てみる
とりあえず、強化学習のプロトタイプを作ってみたので挙動を見てみましょう。
4人プレイのルールで200万回学習させました。
学習後のデータは55GBで約1億個の状況データが蓄積されています。
4人プレイUTGのオープンレンジ表

綺麗に確率を可視化するコードを書くのが面倒くさくて、各ハンド1色にしちゃいました。赤に近いほどベットで、青に近いほどフォールドです。
200万回ごときの学習では最善の戦略に収束しないので、AcesよりQJスーテッドの方が強気でプレイしてたりツッコミどころがあります。でも全体で見ると、「ポケットは強気」「オフスーツよりスーテッドの方が強気」「半分以上のハンドは降りている」のように、ある程度プリフロは立ち回れるようになっています。
ポストフロップのアクション
ポストフロップはダメですね。アクションがランダムになってしまっています。
なぜランダムになってしまうかは、ポーカーAIの仕組みと併せて後述します。

開発に使っているツール
強化学習に使うコードは1から自作するわけではなく、GoogleのDeepMindチームが作成したOpenSpielをベースにします。
OpenSpielを選んだ1つ目の理由は、Pythonで書かれた汎用的な強化学習のアルゴリズムが用意されているからです。Pythonはその他のライブラリも豊富ですし、可読性がいいので開発がしやすいです。
2つ目の理由は、ポーカーのゲームロジックがC++で書かれているからです。強化学習ではポーカーのシミュレーションを死ぬほど繰り返します。PythonよりもC++の方が圧倒的に高速なため、学習時間の短縮になります。
強化学習の大枠のアルゴリズムはPythonで実行し、ゲームシミュレーションはC++に引き渡して動かします。
強化学習アルゴリズムの選定
不完全情報ゲームのAI開発で一番メジャーなアルゴリズムはCFR(Counterfactual Regret Minimization)です。簡単に説明すると、「実際に取った選択肢Aの報酬」と「選択肢Bを取った場合の報酬」の差分(後悔値、Regret)を計算し、損の少ない選択肢を求めていく手法です。
CFRには様々な種類があり、状態空間が大きすぎるポーカーにおいては計算負荷を下げたMCCFR(Monte Carlo CFR)が有効です。MCCFRでは、起こり得る全てのパターンを計算するのではなく、ランダムに選択した1つのサンプルのみを計算します。サンプリングを繰り返すことで、全てのパターンを計算した状態を近似的に再現します。
理論的には「後悔値が少ない」≒「ナッシュ均衡」のため、MCCFRで学習を重ねるとGTO戦略に収束していきます。つまり、MCCFRとは「簡易版GTO」です。
サンプルの取り方にも種類がありますが、Outcome Sampling MCCFRとExternal Sampling MCCFRを併用します。(理由は長くなるので省きます)
MCCFRのデータ構造
学習後のデータはDeepMindのコードだと 状況 → [[アクションごとの後悔値], [アクションごとの推奨確率]] のような辞書形式になっています。例えば、「プリフロ / 自分はUTG / 自分は9ポケを持っている」という状況を辞書で検索すると、[フォールドX%, コールX%, レイズX%] のように推奨のアクションを返してくれます。
学習中は、とんでもない量の情報が辞書に蓄積していきます。
「プリフロ / 自分はUTG / 自分は9ポケを持っている」
「プリフロ / 自分はUTG / 自分は9ポケを持っている / 自分は5BBにレイズした / BBが10BBにリレイズした」
「プリフロ / 自分はUTG / 自分は9ポケを持っている / 自分は5BBにレイズした / BBが10BBにリレイズした / 自分はコールした / フロップにAKQモノトーンが出た / BBが10BBベットした」
プレイ人数や、ベット額のバリエーションによって状況は無限に発散します。

MCCFRの弱点
MCCFRには大きな弱点があります。
それは、MCCFRの設計上、ゲームが進むにつれて戦略が適当になっていくという点です。
学習を沢山すればプリフロップは十分に学習データが揃うのですが、ゲームが進むにつれて学習していない状況に直面する可能性が高くなります。全く学習していない状況だと、取り得るアクションはフォールド33%, コール33%, オールイン33%のようにランダムになってしまいます。
また、サンプル数が中途半端に少ないと、本来推奨すべきではないアクションを推奨する可能性があります。
例えば、リバーの特定の条件下でフォールドしかサンプルが取れていない場合、フォールドの後悔値はあるけどオールインの後悔値はないため、推奨のアクションがオールインになります(フォールド後悔値>オールイン後悔値=0)。ところが、オールインのサンプルを取ってみると実はオールインの方が後悔値が高く、フォールドすべきだったという判断になるかもしれません。
学習の回数を増やしまくったとしても、今度はデータ量が手に負えなくなります。
自分の持っているゲーミングPCの場合、メモリにデータを保持するのであれば64GB以下、SSDにデータベース化するのであれば1TB以下でないといけません。無限に近い状態空間がある中で、この容量に収まる程度の学習であればターン以降はほぼ確実に未知の状況です。

弱点を補う方法
ゲーム終盤にアクションが適当になってしまうMCCFRの弱点を補う方法は、リアルタイムに学習の起点を変えながらMCCFRを計算し続けることです。

いくらでも時間を費やせる事前学習とは違い、リアルタイムの学習は数秒以内にプレイヤーのアクションを算出しないといけません。また、メモリ容量は限られているため、不要になった学習データは削ぎ落としていかないといけません。
複雑なプロセッサ管理とメモリ管理が求められますが、現状のDeepMindのコードでは無理です。プロトタイプの検証時は応急処置的な書き換えをしましたが、リアルタイムの学習を実現するには1から書き直すレベルでコードを変えないといけません。
DeepMindのコードの問題点
DeepMindのmccfr.pyとoutcome_sampling_mccfr.pyの具体的な改善案を羅列してみます。
参考までに、学習後データの辞書のサンプルデータはこんな感じです。
“[Round 0][Player: 2][Pot: 1200][Money: 19900 19800 20000 20000 20000 20000][Private: Qc5c][Public: ][Sequences: ffc]” →
[np.array([7231.86155051, -2400.08289394, 33447.69488384, 27731.86155051, -19716.13844949]), np.array([0.200001, 0.55393785, 1.39276491, 3.65330024, 0.200001])]
- プロトタイプ作成時に適応した応急処置
- チップサイズを見慣れているBB200やスタック20000から、BB2やスタック200にする。状況キーは文字列のため、文字数を減らすだけでデータサイズが減ります。こんな小さな工夫でデータ容量が約9%減りました。
- 状況キーの文字数を更に減らすためにさらに圧縮する。これでデータ量が約32%減りました。
Before: “[Round 0][Player: 2][Pot: 12][Money: 199 198 200 200 200 200][Private: Qc5c][Public: ][Sequences: ffc]”
After: “0212199 198 200 200 200 200Qc5cffc” - プレイ人数を6人から4人に減らし、選択可能なアクションを「フォールド、コール、ハーフポットベット、ポットベット、オールイン」のfchpaパラメータから「フォールド、コール、ポットベット、オールイン」のfcpaパラメータに変更する。これでデータ量が約64%減りました。実用性は下がりましたが動作確認するためのプロトタイプなのでOKです。
- 学習後のデータをSSDに保存し、後から追加学習するコードを付け足した。容量が大きいと保存に時間がかかるため、まるごとpickle、部分ごとにpickle、joblib、圧縮の有無など色々試した結果、部分ごとにpickleが一番良かった。
- 根本的な改善案
- 後悔値と推奨確率のデータタイプをfloat64からfloat32に変えてデータ量を抑える。ただし、ゲーム開始付近の後悔値は足し算なので学習を重ねる度に大きくなる。float32の桁数から漏れる端数は足し算ができなくなる可能性があるため、毎回0.99を掛ける後悔値減衰の手法を取り入れた方がよさそう。
- 後悔値と推奨確率はnp.arrayで計算が早いというメリットはあるものの、0であっても999であっても同じメモリを食う。999よりも0の方が容量が少ないデータタイプに変更すれば、マイナスの後悔値を0とするRegret Matching Plusの手法と相性が良く、データ容量を下げられそう。処理速度とデータ容量を天秤にかけてみる。
- 学習データの構造を辞書ではなく、プレフィックス木に変える。そうすれば状況キーの冗長性を解消できる。ただし、Pythonの辞書はハッシュ値で検索が早いので、こちらも処理速度とデータ容量を天秤にかける。
- 並列処理ができるようにする。multiprocessingなどでCPU並列を試したが、Pythonのグローバルインタプリタロックのせいか、現状のコードは並列にしないほうが早い。せっかくGPUがあるので、いずれはGPUで並列処理ができるようにしたい。
- C++のゲームロジックから、急なオールインの選択肢をなくす。オールインはリレイズを繰り返して辿り着くものであり、急にオールインするものではない。トーナメントやオンラインポーカーでは必要かもしれないが、リングゲームではなくてもいい。
- 律儀に52枚のカードは使わずにハンドを抽象化する。用意されているパラメータでは、カードをA〜7の8枚に減らしたり、4つのマークを2つに減らしたりできるが、ストレートやフラッシュが出やすくなって実現的な学習データではなくなってしまう。なので、 27オフと28オフを同じようなハンドとみなすなど、抽象化にも大きな工夫が必要そう。
- 1回目の学習データを第1世代とし、第2世代を学習させる時の序盤の相手は第1世代にする。そうすれば、学習の序盤に不安定なランダムデータが溜まらなくて済み、より少ないデータ容量でナッシュ均衡に近づきやすくなる。
- 色々な工夫の末、Pythonの速度に限界を感じたら、強化学習アルゴリズムをC++にするか検討する。
開発ロードマップ
- (完) 強化学習の基礎知識、ポーカーAIの過去研究を学ぶ。
- (完) 開発環境を構築する。
- (完) プロトタイプ作成
- 事前学習external sampling MCCFRのみで2百万回学習(55GB、約1億個の状況データ)
- プロトタイプは4人プレイ、ハーフポットベットなし。
初号機からは、より実現的な戦略にするために6人プレイ、ハーフポットベットありにする。
- (完) 全体ロードマップを描く。
- (今ここ) MCCFRのロジックを改善して、処理速度向上とデータ圧縮をする。
- 初号機作成
- プリフロップ:事前学習outcome sampling MCCFR
- ポストフロップ:リアルタイムoutcome sampling MCCFR
- 初号機(改)作成
- プリフロップ:全プレイヤー同じスタックの事前学習データ(初号機)に加え、ポジションごとに自分だけショートorディープスタックの追加12パターンの事前学習データを用意する。ゲーム開始時に任意のデータをメモリに素早く乗せる必要があるため、PythonのPickleではなくC++(mmapとmemcpy)の並列処理を活用する。
- フロップ:実際のスタックサイズに切り替えてリアルタイムoutcome sampling MCCFRをする。スタックサイズが重要になってくるフロップ以降で、事前学習のスタックサイズを使い続けるのはナンセンス。事前学習データからの過学習は抑えないといけないため、学習初期は後悔値の減衰を強める。
- ターン:リアルタイム学習をoutcome sampling MCCFR からexternal sampling MCCFRに切り替える。outcome samplingは計算は速いですが学習初期は分散が多い。ターンくらいまで行くと考慮すべきパターンがかなり減っているので、より分散の少ないexternal samplingの方が適してる気がする。
- リバー:EVまたはナッシュ均衡の実数値を計算する。MCCFRでサンプリングする必要がないくらい分岐が少ないはず。3人以上残っていたら引き続きMCCFRにする。
- 簡単に二号機を操作できる仕組みを作る
- 外出から家のサーバーにアクセスできるようにする。Tailscaleが無難か。
- 直感で素早く操作できるUIが必要。パソコンアプリの方が開発が楽かもしれないが、携帯性と操作性を考えるとスマホアプリの方がいいかも。
- UIからの入力があったら即座に学習経路を変更できるようにする。例えば、フォールドしたプレイヤーがいたら、フォールドしていない場合の計算はもうせず、不要になったデータもメモリから削除する。
- 二号機作成
- 二号機を「後悔値最小化」の仕組みから「報酬最大化」の仕組みに変えてみる。「後悔値最小化」は長時間プレイすれば平均的プレイヤーには勝てるはずだが、相手のミスをエクスプロイトして利益を最大化する行動は取れない。
- 三号機作成
- 後悔値データを溜めまくるMCCFRではなく、ニューラルネットワークを用いたDeep CFRやPPO(Proximal Policy Optimization)のような強化学習アルゴリズムを試してみる。最近話題のDeepSeekが出している論文の中に、unified paradigmというなかなか面白いコンセプトがあったので試してみたい。
