Kuhn Poker 上的反事实后悔最小化 (CFR)

这将《反事实后悔最小化》(CFR)应用于库恩扑克。

Kuhn Poker 是一款双人三张牌的下注游戏。玩家从 A、K 和 Queen 中各获得一张牌(无花色)。包中只有三张牌,所以遗漏了一张牌。Ace 击败 King 和 Queen,King 击败 Queen ——就像普通卡牌排名一样。

两个玩家都押注筹码(盲注筹码)。看完牌后,第一个玩家可以传球或下注筹码。如果第一个玩家通过,则拥有较高牌的玩家赢得底池。如果第一个玩家下注,第二个玩家可以下注(即跟注)筹码或过关(即盖牌)。如果第二个玩家下注,而拥有较高牌的玩家赢得底池。如果第二个玩家通过(即弃牌),则第一个玩家获得底池。这个游戏是反复玩的,一个好的策略将针对长期效用(或获利)进行优化。

以下是一些示例游戏:

  • KAp -玩家 1 获得 K 玩家 2 获得 A. 玩家 1 传球。玩家 2 没有下注机会,玩家 2 赢得筹码底池。
  • QKbp -玩家 1 得到 Q。玩家 2 得到 K。玩家 1 下注一个筹码。玩家2传球(弃牌)。玩家 1 拿到底池是因为玩家 2 盖牌。
  • QAbb -玩家 1 得到 Q。玩家 2 得到 A. 玩家 1 下注一个筹码。玩家2也下注(跟注)。玩家 2 赢得底池

__init__.py 用Kuhn Poker的具体内容扩展了中定义的InfoSet History 类别和等级。

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

库恩扑克的动作是 pass (p ) 或 bet (b )

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

游戏中的三张牌分别是 Ace、King 和 Queen

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

有两个玩家

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}%'

历史

这定义了游戏何时结束,计算效用和抽样机会事件(发牌)。

历史记录存储在字符串中:

  • 前两个角色是发给玩家 1 和玩家 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])

第二个玩家通过了

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()

前两个事件是卡牌交易;即机会事件

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

设置库恩扑克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()