(最終更新:2026年5月2日)
約1年前、趣味の延長としてポーカーAIの開発を始めました。当初は強化学習もC++も初心者でしたが、チャッピー(当時は4o)と対話しながら、試行錯誤を重ねて少しずつ形にしていく過程がとても楽しかったです。
しかし、ありがたいことに仕事やプライベートで変化があり、開発にかける時間が少しずつ減っていきました。その結果、このプロジェクトは半年ほど手つかずのままになっています。
このまま完全に記憶の彼方へ消えてしまうのは少しもったいないので、せっかくなら他の人でも動かせる状態に整えて公開し、「やりたいことリスト」の断捨離の一環として一区切りつけたいと思います。
個人の趣味で作ったものなので説明はやや雑ですが、一応、下記の順番通りに進めれば誰でも動かせるはずです。GTOの裏側の仕組みやLinuxに興味のある方は、ぜひ触ってみてください。
僕が書いた強化学習のロジックに関する質問はお受けしますが、初期設定まわりの初歩的な質問はチャッピーに聞いてください。
開発の難しさ
囲碁で人間を上回るAIが登場したのは、2015年のAlpha碁です。
一方、ノーリミット・テキサスホールデム・ポーカーのヘッズアップ(1対1)で人間を上回るAIが登場したのは2017年のLibratusで、3人以上のマルチプレイヤーポーカーでは2019年のPluribusです。
盤面の状態数という意味では、囲碁の方が圧倒的に複雑です。それにもかかわらず、ポーカーAIの開発が難しかった理由は、ポーカーが不完全情報ゲームだからです。
囲碁では、すべてのプレイヤーが現在のゲーム状態を完全に把握できます。しかしポーカーでは、相手の手札や今後配られるカードを知ることができません。そのため、確率論や期待値に基づいて意思決定を行う必要があります。さらに、相手の行動から隠れた情報を推測する「読み」も重要になります。
加えて、ポーカーはカードが52枚あるため、状態空間、つまり考え得るパターンが意外と大きいゲームです。状態空間を数字で表すと、チェスは10の50乗、将棋は10の80乗、囲碁は10の172乗とされています。ポーカーはプリフロップの時点で10の18乗、ショーダウンまで含めると10の160乗にもなるそうです。さらに、ノーリミットではベット額を自由に選べるため、細かいベット額まで考慮すると、状態空間は事実上無限になります。
ポーカーAIは、本来であればデータサイエンスやAI研究を専門とするPhDチームが、数年かけて開発するようなものです。また、チャッピーなどの生成AIは、ポーカーAIに関する教師データが不足しているせいか、「どのようなロジックを組めばよいか」について、もっともらしい嘘を返すことがあります。さらに、コード変更の妥当性を検証するには、ある程度の学習データを蓄積してから挙動を観察する必要があります。
ポーカーAIの開発はかなりの沼です。足を踏み入れる場合は、十分注意してください。
強化学習アルゴリズムの選定
不完全情報ゲームのAI開発で最もメジャーなアルゴリズムの一つが、CFR(Counterfactual Regret Minimization)です。
簡単に説明すると、CFRは「ある選択肢を選んだ場合の報酬」と「別の選択肢を選んでいた場合の報酬」の差分、つまり後悔値(Regret)を計算し、その後悔が小さくなるように戦略を更新していく手法です。
CFRにはさまざまな種類があります。ポーカーのように状態空間が非常に大きいゲームでは、計算負荷を下げたMCCFR(Monte Carlo CFR)が有効です。
MCCFRでは、起こり得るすべてのパターンを毎回計算するのではなく、ランダムに選んだ一部のサンプルだけを計算します。このサンプリングを繰り返すことで、全パターンを計算した場合に近い結果を、近似的に得ることができます。
理論的には、平均後悔値が小さくなるほど戦略はナッシュ均衡に近づいていきます。そのため、MCCFRで学習を重ねることで、GTO戦略に近い戦略を求めることができます。つまり、MCCFRは「GTO戦略を近似的に求めるための手法」と言えます。
サンプルの取り方にもいくつか種類がありますが、自分はExternal Sampling MCCFRにQRE と DCFR の考え方を組み合わせています。
MCCFRのデータ構造
学習後のデータは 状況 → [[アクションごとの後悔値], [アクションごとの推奨確率]] のような辞書形式になっています。例えば、「プリフロ / 自分は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の弱点を補う方法は、リアルタイムに学習の起点を変えながらMCCFRを計算し続けることです。

いくらでも時間を費やせる事前学習とは違い、リアルタイムの学習は数秒以内にプレイヤーのアクションを算出しないといけません。また、メモリ容量は限られているため、不要になった学習データは削ぎ落としていかないといけません。
プログラム構成
強化学習を触り始めたころは、Google DeepMindが提供しているOpenSpielというライブラリを活用して、基礎を学んでいました。OpenSpielは、ポーカーを含むさまざまなゲームに対応しており、強化学習のロジックも多数用意されています。そのため、学習用の教材や実験環境としては非常に便利です。
一方で、今回のようにポーカーAIの開発に用途を絞る場合には、やや扱いにくい面もあります。OpenSpielは汎用的に設計されている分、不要な依存関係が多く、細かくコードを改修したり、処理速度を追求したりするには限界がありました。
特にポーカーの強化学習では、データ量と読み書き速度がボトルネックになりやすいため、本プロジェクトではメインとなるコードを自分で書き、必要最低限の構成で動作するように新たに構築しています。これにより、OpenSpielを使う場合と比べて、処理速度を大幅に向上させることができました。
- src/
- 強化学習の主要な学習ロジックは自分で実装
- External Sampling MCCFR に QRE と DCFR の考え方を組み合わせています
- external/acpc_server/
- NLHのディーラー役(ゲーム進行、アクション処理、状態管理)はオープンソースのAnnual Computer Poker Competitionを利用
- ここからコードを取得しました
- external/OMPEval/
- ショーダウン時のハンド評価として使用
- Git submodule として追加
- external/oneTBB/
- RAM 上でステート情報を管理する際に、並列アクセス可能なデータ構造として使用
- Git submodule として追加
- RocksDB
- SSDで学習後のステート情報を保持するために使用
- Ubuntu の apt 経由でインストール
- FLTK
- GUIの作成に使用
- Ubuntu の apt 経由でインストール
どこまで開発したか
ポーカーAIの仕組みには、大きく分けて「事前学習」と「リアルタイム学習」があると言いましたが、本記事では、事前学習のコードと、その学習結果を確認するためのGUIを公開しています。
リアルタイム学習については、内部データを観察しながら仮説を検証できる段階までは開発しましたが、他の人が直感的に扱えるGUIとして整理できていないため、今回は公開対象には含めていません。
公開したコードを使えば、アクションやベットサイズのパラメータを変更し、プリフロップの戦略を観察することができます。プログラミングにある程度自信のある人は、相手のアクション分布に「フラッシュドローを好む人」や「アグレッションの高い人」のような傾向を反映させ、CFRの戦略がどのように変化するか見てみると面白いです。
実用的なAIを目指したい人は、「リアルタイム学習」にも挑戦してみてください。自分は、ベイズ推定を使って相手のアクションからレンジを絞り込み、探索するノードを限定していく仕組みにしました。本来であれば、フロップ以降はハンドとボードの抽象化を解除したほうが、ボードの細かなニュアンスを捉えやすくなりますが、自分はそこまで開発する時間がありませんでした。
必要なもの
開発環境はLinux(Ubuntu Server)で、Windowsから遠隔操作しています。Windowsでも、WSL(Windows Subsystem for Linux)を使えば直接実行できると思います。ただ、ポーカーの強化学習はメモリを大量に使うため、メモリオーバーヘッドの少ないUbuntu Server上で起動し、WindowsからSSHで接続する方がよいと思います。
- サーバーとなるPC:
自分はミニPCを使用しています。Ubuntu ServerというOSをインストールするので、Windows等が入っているのであれば別途SSDを買った方がいいです(なのでSSDを差し替えられないノートパソコンはUbuntu Serverのインストール非推奨です)。メモリは32GB以上は欲しいです。メモリが32GB未満のPCではテストしていないので、動作は保証できません。SSH接続できるようになるまでモニターが必要です。 - 普段使いのノートパソコン:
OSとスペックは何でもいいです。この記事はWindowsでの手順になっていますが、Macでも動作確認できてます。
初期設定
サーバーの初期設定
- サーバーとなるPCにUbuntu Serverをインストールする。
- Ubuntu Serverにログインできるようになったらアップデートを適応する。
sudo apt update && sudo apt upgrade -y - 任意のIPアドレスに固定する。
- SSHをインストールする。
sudo apt install -y openssh-server - サーバーを再起動する。
sudo reboot - ノートパソコンからサーバーにSSH接続できることを確認する。WindowsであればPowerShell、Macであればターミナルを使います。今後はSSH接続のみで作業できるので、サーバーのモニターはもう不要です。
ssh ユーザー名@固定したIPアドレス - サーバーにRocksDB、FLTK、X11をインストールする。
sudo apt install -y librocksdb-dev rocksdb-tools lld libfltk1.3-dev fluid xauth x11-apps - サーバーにビルド関連をインストールする。
sudo apt install -y cmake build-essential - サーバーのスワップ処理を無効化する。無効化しないとSSDを仮想メモリとして使ってしまい、処理速度が極端に落ちます。
- サーバーのリソース制限を引き上げる。RocksDBのデータベースは複数のファイルとして保存されるため、長時間学習を実行していると、同時に開けるファイル数の上限に達することがあります。
- サーバーへのSSH接続を一旦切る。
exit
GUIの初期設定
- ノートパソコンにVcXsrvをインストールする。(Macの場合はxquartz)
- インストールされたxlaunch.exeを起動する。設定は基本標準のままでいいですが、初回はDisable access controlにチェックを入れて起動する。タスクバーに起動されたことを確認する。
- PowerShellで環境変数を設定する。
$env:DISPLAY="localhost:0.0" - -Yオプションをつけて、もう一度サーバーにSSH接続する。
ssh -Y ユーザー名@固定したIPアドレス - SSH接続で下記コマンドを実行して、ノートパソコン側に時計の画面が出てきたらGUIの初期設定は完了してます。
xclock &
ソースコード取得&ビルド
- リポジトリをクローンする。
git clone git@github.com:guregu321/poker-agent.git cd acpcでディレクトリを移動し、git submodule update --init --recursiveでサブブモジュールをダウンロードする。- makeでビルド
パラメータ設定
プレイヤー人数、スタックサイズ、コールドコールの有無、ベットサイズ、QREの有無など、各種ファイルで調整できます。設定を変更したら、make clean && makeで再度ビルドしてください。
- src/holdem.nolimit.6p.game
- プレイヤー人数やスタックサイズなど、ゲームのルールを変更できます。
- ハンドやボードの枚数を変え、NLH以外のルールで学習することも可能です。
- src/acpc_settings.hpp
- ベットサイズやオールインの閾値など、取り得るアクションの抽象化設定を変更できます。
- src/training_settings.hpp
- QREの有無やRAMの上限など、学習にまつわる設定を変更できます。
- src/es_mccfr.cpp
- パラメータではありませんが、相手のアクションに特定の傾向やバイアスを持たせたい場合は、ここのサンプリング方法を変更します。
強化学習の実行方法
学習はtraining_loop.shで走らせます。
- 学習用のセッションを開始する。(ノートパソコンの電源を切っても裏で学習が回るようにするため)
tmux new -s training - 学習を開始する。最初の数字が総学習回数です。次の数字が、累積リグレットと累積戦略を減衰させるインターバルです。
chmod +x training_loop.sh./training_loop.sh 1000000000 10000000 db 1 off - 学習用のセッションを出る場合はCtrl+Bを押して、Dを押す。
- CPUやメモリの使用状況はhtopで確認できます。
- 学習用のセッションに戻る場合は
tmux attach -t training - 学習を中断する場合はCtrl+C
- 学習を再開する場合は再開箇所を指定してください。
./training_loop.sh 100000000 10000000 db 5 off
4スレッドしかないRyzen 4300UとDDR4 RAMのポンコツPCでも、初期段階のメモリ上で完結する学習であれば、1秒あたり3万2千回のゲームシミュレーションを実行できています。ちなみに、このゲームシミュレーションでは、自分側はすべてのノードで全アクション(Fold、Call、Raise)を取り、対戦相手側は各ノードで最善のアクションを1つだけ取るようにしています。そのため、実際のゲームよりも探索ノード数は多くなっています。

サンプル画像では、1セット100万回のシミュレーション後に、累積リグレットと累積戦略へ50%の減衰をかけたうえで、2回目のセットに入っています。2回目のセットでは、SSDの学習データを参照しながら、メモリに新しいデータを蓄積していきます。遅いタイプの2.5インチSSDであっても、1秒あたり1万7千回のゲームシミュレーションができてます。M.2 SSDであればさらに高速ですが、その分、発熱は大きくなります。減衰はセットを重ねるごとに、徐々に弱まるようにしています。

学習済みデータの確認
学習済みデータはdata/に溜まっていきます。GUIで可視化する場合は、./bin/range_viewerを実行します。累積リグレットと累積戦略を減衰させるタイミングで、DBが新しく作成されます。
画面右上からDBを選択できるようにしているため、学習の過程で戦略がどのように変化したかを確認できます。下の画像では、左が100万回学習後、右が1000万回学習後のUTGのオープンレンジです。プリフロップのレンジを十分に収束させるには、何十億回もの学習が必要になります。

