Kuhn ポーカーにおける反事実に基づく後悔最小化 (CFR)

これは反事実に基づく後悔最小化 (CFR) をクーンポーカーにも適用します。

Kuhn Pokerは、2人用の3カードベッティングゲームです。プレイヤーには、エース、キング、クイーンのカードがそれぞれ1枚ずつ配られます(スーツなし)。パックにはカードが3枚しかないので、1枚のカードは残ります。通常のカードランキングと同じように、エースがキングとクイーン、キングがクイーンを倒します

どちらのプレイヤーもアンティチップ(盲目的にチップをベットする)。カードを見た後、最初のプレーヤーはパスまたはチップをベットできます。最初のプレーヤーがパスした場合、高いカードを持っているプレーヤーがポットを獲得します。最初のプレーヤーがベットした場合、 2番目のプレーはチップまたはパス(つまりフォールド)をベット(つまりコール)できます。2人目のプレイヤーがベットし、高い方のカードを持っているプレイヤーがポットを獲得した場合。2人目のプレイヤーがパス (つまりフォールド) すると、最初のプレイヤーがポットを獲得します。このゲームは繰り返しプレイされるため、優れた戦略を立てることで長期的な効用(または賞金)を狙うことができます

ゲームの例をいくつか示します。

  • KAp -プレイヤー 1 は K、プレイヤー 2 は A プレイヤー 1 のパス。プレイヤー 2 にはベットチャンスがなく、プレイヤー 2 はポットのチップを獲得します
  • QKbp -プレイヤー1はQ、プレイヤー2はK、プレイヤー1はチップをベットします。プレイヤー 2 パス (フォールド)。プレイヤー2がフォールドしたため、プレイヤー1はポットを獲得します
  • QAbb -プレイヤー 1 は Q、プレイヤー 2 は A プレイヤー 1 がチップをベットします。プレイヤー 2 もベット (コール) します。プレイヤー2がポットを獲得します

そこで、InfoSet History クラスとクラスを Kuhn __init__.py Pokerの仕様で拡張しています。

Open In Colab

37from typing import List, cast, Dict
38
39import numpy as np
40
41from labml import experiment
42from labml.configs import option
43from labml_nn.cfr import History as _History, InfoSet as _InfoSet, Action, Player, CFRConfigs
44from labml_nn.cfr.infoset_saver import InfoSetSaver

Kuhn ポーカーのアクションはパス (p ) またはベット (b )

47ACTIONS = cast(List[Action], ['p', 'b'])

場に出ている3枚のカードは、エース、キング、クイーンの3枚です。

49CHANCES = cast(List[Action], ['A', 'K', 'Q'])

プレイヤーは2人います

51PLAYERS = cast(List[Player], [0, 1])
54class InfoSet(_InfoSet):

保存/読み込みはサポートしていません

59    @staticmethod
60    def from_dict(data: Dict[str, any]) -> 'InfoSet':
62        pass

アクションのリストを返します。History 端末の状態はクラスごとに処理されます。

64    def actions(self) -> List[Action]:
68        return ACTIONS

人間が読める文字列表現-ベッティングの確率を教えてくれます

70    def __repr__(self):
74        total = sum(self.cumulative_strategy.values())
75        total = max(total, 1e-6)
76        bet = self.cumulative_strategy[cast(Action, 'b')] / total
77        return f'{bet * 100: .1f}%'

歴史

これにより、ゲームが終了するタイミングを定義し、ユーティリティを計算し、チャンスイベント(ディーリングカード)をサンプリングします。

履歴は文字列で保存されます。

  • 最初の 2 文字は、プレイヤー 1 とプレイヤー 2 に配られるカードです。
  • 3番目のキャラクターは最初のプレイヤーのアクションです
  • 4番目のキャラクターは2人目のプレイヤーのアクションです
80class History(_History):

歴史

94    history: str

与えられた履歴文字列で初期化

96    def __init__(self, history: str = ''):
100        self.history = history

履歴が終端か (ゲームオーバー) か。

102    def is_terminal(self):

プレイヤーはまだ行動を起こしていません

107        if len(self.history) <= 2:
108            return False

最後にプレイしたプレイヤーが合格しました (ゲームオーバー)

110        elif self.history[-1] == 'p':
111            return True

両方のプレーヤーがコール(ベット)(ゲームオーバー)

113        elif self.history[-2:] == 'bb':
114            return True

その他の組み合わせ

116        else:
117            return False

プレイヤーのターミナルユーティリティを計算し、

119    def _terminal_utility_p1(self) -> float:

プレイヤー 1 の方が良いカードを持っているか、そうでなければ

124        winner = -1 + 2 * (self.history[0] < self.history[1])

2 人目のプレーヤーが合格

127        if self.history[-2:] == 'bp':
128            return 1

両方のプレイヤーがコールし、良いカードを持っているプレイヤーがチップを獲得します

130        elif self.history[-2:] == 'bb':
131            return winner * 2

最初のプレーヤーがパスし、良いカードを持っているプレーヤーがチップを獲得します

133        elif self.history[-1] == 'p':
134            return winner

歴史は終わらない

136        else:
137            raise RuntimeError()

プレイヤー用のターミナルユーティリティを入手

139    def terminal_utility(self, i: Player) -> float:

プレイヤー 1 の場合

144        if i == PLAYERS[0]:
145            return self._terminal_utility_p1()

それ以外の場合は、

147        else:
148            return -1 * self._terminal_utility_p1()

最初の 2 つのイベントはカードディール、つまりチャンスイベントです。

150    def is_chance(self) -> bool:
154        return len(self.history) < 2

履歴にアクションを追加して新しい履歴を返す

156    def __add__(self, other: Action):
160        return History(self.history + other)

現在のプレイヤー

162    def player(self) -> Player:
166        return cast(Player, len(self.history) % 2)

チャンスアクションを試してみよう

168    def sample_chance(self) -> Action:
172        while True:

カードをランダムに選ぶ

174            r = np.random.randint(len(CHANCES))
175            chance = CHANCES[r]

カードが以前に配られたかどうか確認する

177            for c in self.history:
178                if c == chance:
179                    chance = None
180                    break

以前に配られていない場合はカードを返却してください

183            if chance is not None:
184                return cast(Action, chance)

人間が読める表現

186    def __repr__(self):
190        return repr(self.history)

現在の履歴の情報セットキー。これは、現在のプレイヤーにのみ表示される一連のアクションです。

192    def info_set_key(self) -> str:

現在のプレイヤーを取得

198        i = self.player()

現在のプレイヤーは自分のカードとベットアクションを見る

200        return self.history[i] + self.history[2:]
202    def new_info_set(self) -> InfoSet:

新しい情報セットオブジェクトを作成する

204        return InfoSet(self.info_set_key())

空の履歴オブジェクトを作成する関数

207def create_new_history():
209    return History()

構成は CFR 構成クラスを拡張します

212class Configs(CFRConfigs):
216    pass

Kuhn create_new_history ポーカーのメソッドを設定

219@option(Configs.create_new_history)
220def _cnh():
224    return create_new_history

実験を実行する

227def main():

実験を行います。追跡情報を書き込むのは、sqlite 処理をスピードアップするためだけです。アルゴリズムは反復処理が速く、反復のたびにデータを追跡するため、Tensorboard などの他の宛先への書き込みには比較的時間がかかります。私たちの分析にはSQLiteで十分です

236    experiment.create(name='kuhn_poker', writers={'sqlite'})

構成を初期化

238    conf = Configs()

設定をロード

240    experiment.configs(conf)

保存するモデルを設定

242    experiment.add_model_savers({'info_sets': InfoSetSaver(conf.cfr.info_sets)})

実験を始める

244    with experiment.start():

イテレーションを始める

246        conf.cfr.iterate()

250if __name__ == '__main__':
251    main()