库恩扑克上的反事实遗憾最小化(CFR)

这适用于库恩扑克的反事实后悔最小化(CFR)

库恩扑克是一款双人三张牌博彩游戏。玩家将从Ace、King和Queen中各获得一张牌(没有花色)。包中只有三张牌,因此遗漏了一张牌。Ace 击败 King and 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 用库恩扑克的InfoSet 细节扩展了中定义的History 班级和班级。

Open In ColabView Run

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

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

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

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

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

有两个玩家

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

不支持保存/加载

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

返回操作列表。终端状态由History 类处理。

65    def actions(self) -> List[Action]:
69        return ACTIONS

人类可读的字符串表示——它给出了下注概率

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

历史

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

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

  • 前两个角色是发给玩家 1 和玩家 2 的牌
  • 第三个角色是第一个玩家的动作
  • 第四个角色是第二个玩家的动作
81class History(_History):

历史

95    history: str

使用给定的历史字符串进行初始化

97    def __init__(self, history: str = ''):
101        self.history = history

历史记录是否终止(游戏结束)。

103    def is_terminal(self):

玩家尚未采取行动

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

最后一个玩过的玩家通过(游戏结束)

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

两位玩家都叫(下注)(游戏结束)

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

任何其他组合

117        else:
118            return False

计算玩家的终端实用程序

120    def _terminal_utility_p1(self) -> float:

如果玩家 1 有更好的牌或者其他

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

第二个玩家通过了

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

两位玩家都被召唤,持有更好牌的玩家将赢得筹码

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

第一个玩家通过,持有更好牌的玩家将赢得筹码

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

历史不是终结的

137        else:
138            raise RuntimeError()

获取玩家的终端实用程序

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

如果是玩家 1

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

否则,

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

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

151    def is_chance(self) -> bool:
155        return len(self.history) < 2

在历史记录中添加操作并返回新的历史记录

157    def __add__(self, other: Action):
161        return History(self.history + other)

当前玩家

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

采样一个机会动作

169    def sample_chance(self) -> Action:
173        while True:

随机挑选一张牌

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

看看之前有没有发过牌

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

如果之前没有发过牌,则归还该牌

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

人类可读的表示

187    def __repr__(self):
191        return repr(self.history)

当前历史记录的信息集关键字。这是一串仅对当前玩家可见的动作。

193    def info_set_key(self) -> str:

获取当前玩家

199        i = self.player()

当前玩家看到她的牌和投注动作

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

创建新的信息集对象

205        return InfoSet(self.info_set_key())

用于创建空历史对象的函数

208def create_new_history():
210    return History()

配置扩展了 CFR 配置类

213class Configs(CFRConfigs):
217    pass

设置库恩扑克create_new_history 的方法

220@option(Configs.create_new_history)
221def _cnh():
225    return create_new_history

运行实验

228def main():

创建一个实验,我们只写跟踪信息sqlite 来加快速度。由于算法的迭代速度很快,而且我们在每次迭代时都会跟踪数据,因此写入其他目标(如 Tensorboard)可能相对耗时。SQLite 对于我们的分析来说已经足够了。

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

初始化配置

239    conf = Configs()

装载配置

241    experiment.configs(conf)

设置要保存的模型

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

开始实验

245    with experiment.start():

开始迭代

247        conf.cfr.iterate()

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