浅谈德州扑克AI核心算法:CFR

2,455 阅读11分钟

本文首发于行者AI

引言

自2017年AlphaGo打败世界围棋冠军柯洁后,人工智能彻底进入大众视野,一时间棋牌类的AI在人工智能界掀起了一股大风。其实早在AlphaGo之前,人们就对棋牌类的人工智能发起了挑战,从简单的跳棋、五子棋,到更加复杂的中国象棋、国际象棋,以及最近非常热门的围棋和德州扑克,数十年间也是硕果累累。而相对于跳棋、象棋等完全信息游戏,德州扑克不仅要根据不完全信息进行复杂决策,还要应付对手的虚张声势、故意示弱等招数,其对应的博弈树无论是广度还是深度都十分庞大,它也一直都是科学家们想要攻克的高山。而在AlphaGO打败柯洁的同年,德扑AI DeepStack和Libratus也先后在 “一对一无限注德州扑克” 上击败了职业扑克玩家,在不完全信息博弈中做出了里程碑式的突破,而他们所采用的的核心算法就是Counterfactual Regret Minimization(CFR)。

1. Regret Matching

1.1算法原理

CFR算法的前身是regret matching算法,在此算法中,智能体的动作是随机选择的,其概率分布与 positive regret呈正比, positive regret表示一个人因为过去没有选择该行动而受到的相对损失程度。

这里对Regret Matching算法中的符号做出若干定义:

  • N={1,2,...,n}N=\left\{1,2,...,n\right\} 表示博弈玩家的有限集合。玩家ii 所采用的的策略为σi\sigma_i

  • 对于每个信息集Iiξi,σi(Ii):A(Ii)[0,1]I_i∈\xi_i,\sigma_i(I_i):A(I_i)→[0,1],是在动作集A(Ii)A(I_i)上的概率分布函数。玩家ii的策略空间用Σi\Sigma_i表示 。

  • 一个策略组包含所有玩家策略,用σ=(σ1,σ2,...,σn)\sigma=(\sigma_1,\sigma_2,...,\sigma_n).

  • 在博弈对决中,不同玩家在不同时刻会采取相应策略以及行动。策略下对应的动作序列发生概率表示为πσ(h)\pi^\sigma(h),且πσ(h)=iNπiσ(h)\pi^\sigma(h)=\prod_{i∈N}\pi_i^\sigma(h)

    这里的πiσ(h)\pi^\sigma_i(h)表示玩家ii使用策略σi\sigma_i促使行动序列hh发生的概率,除了玩家ii以外,其他玩家通过各自策略促使行动序列hh发生的概率为:πiσ(h)=iN/iπjσ(h)\pi^\sigma_{-i}(h)=\prod_{i∈N/{i}}\pi_j^\sigma(h)

  • 对于每个玩家iN,ui:ZRi∈N,u_i:Z→R,表示玩家的收益函数。

  • 计算玩家在给定策略下所能得到的期望收益:ui(σ)=ΣhZui(h)πσ(h)u_i(\sigma)=\Sigma_{h∈Z}u_i(h)\pi^\sigma(h)

  • 纳什均衡:策略组σ=(σ1,σ2,...,σn)\sigma=(\sigma^*_1,\sigma^*_2,...,\sigma^*_n)是纳什平衡当且仅当对每个玩家iNi∈N,满足条件:ui(σ)maxσi(σ1,σ2,...,σn)u_i(\sigma)\geq max_{\sigma_i^`}(\sigma^*_1,\sigma^*_2,...,\sigma^*_n)

  • 遗憾值:玩家在第T次采取策略的遗憾值为:

    RiT(a)=ΣT=1T(μi(a,σit)μi(σit,σit))R_i^T(a)=\Sigma_{T=1}^T(\mu_i(a,\sigma_{-i}^t)-\mu_i(\sigma_i^t,\sigma_{-i}^t))
  • 策略:根据遗憾值更新策略

    σiT+1(a)=RiT(a)ΣbAiRiT(b)\sigma_i^{T+1}(a)=\frac{R_i^T(a)}{\Sigma_{b∈A_i}R_i^T(b)}
  • 平均遗憾值:假设博弈能够重复进行,令第次的博弈时的策略组为,若博弈已经进行了次,则这次博弈对于玩家的平均遗憾值定义为:

    RegretiM=1MmaxσiΣi=1M(ui(σi,σtt)ui(σt)){Regret_i^M}=\frac{1}{M}max_{\sigma_i^*∈\Sigma_{i=1}^M}(u_i(\sigma_i^*,\sigma_{-t}^t)-u_i(\sigma^t))

Regret matching算法流程为:

  • 对于每一位玩家,初始化所有累积遗憾为0。

  • for from 1 to T(T:迭代次数):

    ​ a)使用当前策略与对手博弈

    ​ b)根据博弈结果计算动作收益,利用收益计算后悔值

    ​ c)历史后悔值累加

    ​ d)根据后悔值结果更新策略

  • 返回平均策略(累积后悔值/迭代次数)

1.2实例

石头剪子布是最为简单的零和博弈游戏,是典型的正则式博弈,其payoff table如下:

图1·石头剪刀布收益图

Regret matching算法流程在本例中为:

​ a)首次迭代,player1和player2都以13\frac{1}{3}概率随机选择动作,假设player1选择布,player2选择剪刀。

​ b)以player1视角,首次博弈结果收益为:u(r,s)=1u(p,s)=1u(s,s)=0u_{(r,s)}=1、 u_{(p,s)}=-1、 u_{(s,s)}=0

​ c)根据结果收益计算后悔值,

regretr=u(r,s)u(p,s)=1(1)=2,regretp=u(p,s)u(p,s)=1(1)=0,regrets=u(s,s)u(p,s)=0(1)=1regret_r= u_{(r,s) }-u_{(p,s) }=1-(-1)=2, regret_p= u_{(p,s)}- u_{(p,s)}=-1-(-1)=0, regret_s= u_{(s,s) }-u_{(p,s) }=0-(-1)=1

​ d)进行归一化处理更新player1的行动策略:(23R,0P,13S)(\frac{2}{3}R,0P,\frac{1}{3} S).

​ e)根据更新后的策略选择动作进行多次博弈,直至达到纳什平衡

​ f)返回平均策略

核心代码如下(具体代码戳这儿):

​ 1)获得策略方法:1.清除遗憾值小于零的策略并重置策略为0;2.正则化策略,保证策略总和为13.在某种情况下,策略的遗憾值总和为0,此时重置策略为初始策略。

def get_strategy(self):
       """
       Get current mixed strategy through regret-matching
       :return:
       """
       normalizing_sum = 0
 
       # add this overcome initial best response opponent
       # if min(self.strategy) < 0:
       #     min_val = min(self.strategy)
       #     self.strategy = [_ - min_val for _ in self.strategy]
 
       for i in range(NUM_ACTIONS):
           self.strategy[i] = max(self.regret_sum[i], 0)
           normalizing_sum += self.strategy[i]
 
       # normalize
       for i in range(NUM_ACTIONS):
           if normalizing_sum > 0:
               self.strategy[i] /= normalizing_sum
           else:
               self.strategy[i] = 1 / NUM_ACTIONS
           self.strategy_sum[i] += self.strategy[i]
 
       return self.strategy

​ 2)训练方法:1.玩选择策略进行博弈,根据博弈结果计算动作效益;2.根据动作效益计算后悔值。

def train(self, iterations: int):
    action_utility = [0] * NUM_ACTIONS
 
    opponent_stats = [0] * NUM_ACTIONS
    cum_utility = 0
    for i in range(iterations):
        # Get regret-matched mixed-strategy actions
        strategy = self.get_strategy()
        # strategy = self.get_average_strategy()
 
        my_action = self.get_action(strategy)
 
        other_action = self.get_action(self.opp_strategy)
 
        opponent_stats[other_action] += 1
 
        # Compute action utilities
        action_utility[other_action] = 0
        action_utility[(other_action + 1) % NUM_ACTIONS] = 1
        action_utility[other_action - 1] = -1
 
        # Accumulate action regrets
        for a in range(NUM_ACTIONS):
            self.regret_sum[a] += action_utility[a] - action_utility[my_action]
 
            # self.regret_sum[a] = opponent_stats
 
        cum_utility += action_utility[my_action]
 
    print(f'opponent_stats: {opponent_stats}, {[_ - min(opponent_stats) for _ in opponent_stats]}')
    print(f'cum utility: {cum_utility}')

实验结果:

​ 1)当固定对手策略为{0.4, 0.3, 0.3}时

图2·固定对手策略,玩家策略

​ 2)当玩家和对手都采用Regret Matching更新策略时

图3·玩家和对手策略

2. Counterfactual Regret Minimization

2.1算法原理

石头剪子布是典型的“一次性”博弈,玩家做出动作即得到结果。而生活中显然许多的博弈属于序列化博弈,博弈由一系列的动作组成,上一步的动作可能会导致下一步的动作选择变更,最终的动作组合形成博弈结果。这种序列游戏我们不再使用payoff table表示,而是使用博弈树的形式。博弈树由多种状态组成,边表示从一个状态到另一个状态的转换。状态可以是机会节点或决策节点。机会节点的功能是分配一个机会事件的结果,因此每个边代表该机会事件的一个可能结果以及事件发生的概率。在决策节点上,边代表行动和后续状态,这些状态是玩家采取这些行动的结果。

同样地,对CFR算法中的符号进行若干定义:

  • 每个信息集II发射部分的概率πσ(I)=ΣhIπσ(h)\pi^\sigma(I)=\Sigma_{h∈I}\pi^\sigma(h),表示所有能到达信息集的行动序列的概率累加。

  • 当采取策略σ\sigma下,施加博弈行动序列hh后达到最终局势zz的概率为:πσ(h,z)\pi^\sigma(h,z)

  • 当采用策略σ\sigma时,其所对应的行动策略的虚拟收益:ui(σ,h)=ΣzZπσ(h,z)ui(z)u_i(\sigma,h)=\Sigma_{z∈Z}\pi^\sigma(h,z)u_i(z)

  • 玩家ii采取行动aa所得到的的虚拟后悔值:r(h,a)=vi(σIa,h)vi(σ,h)r(h,a)=v_i(\sigma_{I→a},h)-v_i(\sigma,h)

  • 行动序列hh所对应的信息集II后悔值为:r(Ia)=Σr(h,a)r(I,a)=\Sigma r(h,a)

  • 玩家ii在第TT轮次采取行动aa的后悔值为:RegrettT(I,a)=Σt=1Trit(I,a)Regret_t^T(I,a)=\Sigma_{t=1}^Tr_i^t(I,a)

  • 同样地,对于后悔值为负数不予考虑,记:RegrettT,+(I,a)=max(RiT(I,a),0)Regret_t^{T,+}(I,a)=max(R_i^T(I,a),0)

  • T+1T+1轮次,玩家ii选择行动aa的概率计算如下:

    σiT+1(I,a)={RegretiT,+(I,a)ΣaA(I)RegretiT,+(I,a)if ΣaA(I)RegretiT,+(I,a)>01A(I),otherwise\sigma_i^{T+1}(I,a)= \begin{cases}\frac{Regret_i^{T,+}(I,a)}{\Sigma a∈A(I)Regret_i^{T,+}(I,a)} &\text{if $\Sigma_{a∈A(I)}Regret_i^{T,+}(I,a)>0$}\\ \frac{1}{A(I)},&\text{otherwise} \end{cases}

算法流程:

  • 针对每个信息集,初始化后悔值和策略
  • 使用当前策略与对手博弈
  • 计算在本次博弈中所访问到的每个信息集的收益和后悔值
  • 通过Regret Matching算法更新策略
  • 多次迭代,直到纳什平衡
  • 返回平均策略(累积后悔值/迭代次数)

2.2实例

库恩扑克(Kunh’s pocker)是最简单的限注扑克游戏,由两名玩家进行游戏博弈,牌值只有1,2和3三种情况。每轮每位玩家各持一张手牌,根据各自判断来决定加定额赌注过牌(P)还是加注(B)。具体游戏规则如下:

图4·库恩扑克规则

以玩家α视角构建库恩扑克博弈树:

图5·先手玩家博弈树

CFR算法流程在本例中为:

​ a)初始策略为随机策略,假设玩家α抽到的牌值为:3

​ b)第一轮迭代时,节点选择动作P的虚拟收益计算方法为:uAσ(3,P)=Σi=13πBσ(3P)πσ(zi3P)uA(zi)u_A^\sigma(3,P)=\Sigma_{i=1}^3\pi_B^\sigma(3P)\pi^\sigma(z_i|3P)u_A(z_i)。结合博弈树求解得到:πBσ(3,P)=1\pi_B^\sigma(3,P)=1πσ(z13P)=0.5\pi^\sigma(z_1|3P)=0.5πσ(z23P)=0.25\pi^\sigma(z_2|3P)=0.25πσ(z33P)=0.25\pi^\sigma(z_3|3P)=0.25uA(z1)=1u_A(z_1)=1uA(z2)=1u_A(z_2)=1 uA(z3)=2u_A(z_3)=2 uA(3,P)=1.25。∴u_A(3,P)=1.25。同理,计算节点选择动作B的虚拟收益为:uA(3,B)=0.5u_A(3,B)=0.5

​ c)利用虚拟收益更新后悔值:R1(3,P)=R0(3,P)+uAσ(3,P)(σ(3,B)uAσ(3,B)+σ(3,P)uAσ(3,P))=0+1.25(0.5×0.5+0.5×1.25)=0.375R^1(3,P)=R^0(3,P)+u_A^\sigma(3,P)-(\sigma(3,B)u_A^\sigma(3,B)+\sigma(3,P)u_A^\sigma(3,P))=0+1.25-(0.5\times 0.5+0.5\times 1.25)=0.375

​ d)利用后悔值更新策略:σA1(3,P)=0.375÷(0.375+0)=1\sigma_A^1(3,P)=0.375\div (0.375+0)=1σA1(3B)=1÷A(3)=0.5\sigma_A^1(3,B)=1\div |A(3)|=0.5

​ e)归一化策略:σA1(3,P)=23\sigma_A^1(3,P)=\frac{2}{3}σA1(3,B)=13\sigma_A^1(3,B)=\frac{1}{3}

​ f)多次迭代,直至达到纳什平衡

核心代码实现:

class KuhnTrainer:
    """
    P = pass
    B = bet
 
    payoff table:
 
    P   P       higher player +1
    P   B   P   player2 +1
    P   B   B   higher player +2
    B   P       player1 +1
    B   B       higher player +2
    """
 
    def __init__(self):
        self.node_map: Dict[str, Node] = {}
 
    def train(self, iterations):
        """
        Train Kuhn poker
        :param iterations:
        :return:
        """
        cards = [1, 2, 3]
        util = 0
        for i in range(iterations):
            # shuffle cards
            self.shuffle_cards(cards)
            cur_util = self.cfr(cards, "", 1, 1)
            util += cur_util
        print("Average game value: ", util / iterations)
        for k, v in self.node_map.items():
            print(v)
 
    def shuffle_cards(self, cards):
        random.shuffle(cards)
 
    def cfr(self, cards, history: str, p0, p1):
        """
        Counterfactual regret minimization iteration
 
        P   P       higher player +1
        P   B   P   player2 +1
        P   B   B   higher player +2
        B   P       player1 +1
        B   B       higher player +2
 
        :param cards:
        :param history:
        :param p0:
        :param p1:
        :return:
        """
        plays = len(history)
        player = plays % 2
        opponent = 1 - player
 
        # Return payoff for terminal states
        if plays > 1:
            terminal_pass = history[plays - 1] == 'p'
            double_bet = history[plays - 2: plays] == 'bb'
            is_player_card_higher = cards[player] > cards[opponent]
            if terminal_pass:
                if history == 'pp':
                    return 1 if is_player_card_higher else -1
                else:
                    return 1
            elif double_bet:
                return 2 if is_player_card_higher else -2
 
        info_set = str(cards[player]) + history
 
        # Get information set node or create it if nonexistant
        node = self.node_map.get(info_set, None)
        if node is None:
            node = Node()
            node.info_set = info_set
            self.node_map[info_set] = node
 
        # For each action, recursively call cfr with additional history and probability
        strategy = node.get_strategy(p0 if player == 0 else p1)
        util = [0] * NUM_ACTIONS
        node_util = 0
        for i in range(NUM_ACTIONS):
            next_history = history + ("p" if i == 0 else "b")
            util[i] = -self.cfr(cards, next_history, p0 * strategy[i], p1) if player == 0 else \
                -self.cfr(cards, next_history, p0, p1 * strategy[i])
 
            node_util += strategy[i] * util[i]
 
        # For each action, compute and accumulate counterfactual regret
        for i in range(NUM_ACTIONS):
            regret = util[i] - node_util
            node.regret_sum[i] += regret * (p1 if player == 0 else p0)
 
        return node_util

实验结果:

图6·库恩扑克,玩家和对手策略

3.引申

CFR算法出现时就已经能够解决德州扑克,但面对52张底牌、加注、过牌、河牌等复杂多变的情况使得德扑的博弈树无论是深度还是广度都十分的庞大,对计算资源和储存资源上的开销过于巨大,使得仅仅靠CFR算法攻克德扑十分困难。而CFR后续的研究者们都在费尽心力优化CFR算法的效率,致力于提高计算速度和压缩存储空间。在此,笔者简单介绍几种CFR变种算法,仅做了解。

3.1 CFR+:

与CFR算法不同的是,CFR+算法对累计平均策略做折减,对迭代的策略进行平均时,给近期迭代的策略赋予更高的权重;直观上,越到后期,策略表现越好,因此在都策略做平均时,给近期策略更高的权重更有助于收敛。

在CFR+算法中,counterfactual utility被定义为以下形式:

upσ(I,a)=ΣzZ(I,a)πpσ(z)πσ(h,z)up(z)u_p^\sigma(I,a)=\Sigma_{z∈Z(I,a)}\pi_{-p}^\sigma(z)\pi^\sigma(h,z)u_p(z)

在的基础上,CFR+算法定义了一个regretlikevalueregretlike value,此时CFR+算法中的regretregret为一个累加值,而CFR算法定义RegretRegret的为平均值,因此CFR+算法中的regret计算方式为:

Qt(a)=(Qt1(a)+Rt(a))+,Rt(a)=RT(a)RT1(a)Q^t(a)=(Q^{t-1}(a)+△R^t(a))^+, △R^t(a)=R^T(a)-R^{T-1}(a)

另外,在CFR+算法中,最后输出的平均策略为一下形式:

σpT=2Σt=1TtσptT2+T\sigma_p^T=\frac{2\Sigma_{t=1}^Tt\sigma_p^t}{T^2+T}

3.2 MCCFR:

MCCFR(Monte Carlo Counterfactual Regret Minimization)是蒙特卡洛算法和CFR算法的结合,其核心在于:在避免每轮迭代整棵博弈树的同时,依然能够保证 immediate counterfactual regretimmediate\space counterfactual\space regret 的期望值保持不变。将叶子节点分割为不同的blockQ={Q1,Q2,...,Qn}block:Q=\{Q_1,Q_2,...,Q_n\},且保证覆盖所有的叶子结点。

定义QjQ_j是在当前迭代中选择QiQ_i的概率:Σj=1rQj=1\Sigma_{j=1}^rQ_j=1

定义Q(z)Q(z)表示在当前迭代中采样到叶子节点的概率:Q(z)=Σj:zQjQjQ(z)=\Sigma_{j:z∈Q_j}Q_j

那么在选择QjQ_j迭代时,得到的采样虚拟值为:vi(σ,I/j)=ΣzQjZi1Q(z)ui(z)πiσ(z[I],z)v_i(\sigma,I/j)=\Sigma_{z∈Q_j}∩Z_i\frac{1}{Q(z)}u_i(z)\pi^\sigma_{-i}(z[I],z)

通过一定的概率选择不同的block,得到一个基于采样的CFR算法。

3.3结语

除了上述介绍的两个算法外,CFR算法的优化数不胜数,有提高计算速度的Discount-CFR、Warm Start、Total RBP,也有压缩存储空间的CFR-D、Continue-Resolving、Safe and Nested Subgame Solving等。

机器博弈是人工智能领域的重要研究方向。非完备信息博弈是机器博弈的子领域。非完备信息博弈中存在隐藏信息和信息不对称的特点,和完备信息博弈相比,非完备信息博弈更加贴近现实生活中。例如,竞标、拍卖、股票交易等现实问题中都存在隐藏信息和信息不对称。因此,研究非完备信息博弈问题更有现实意义。德州扑克博弈包含了隐藏信息、信息不对称和随机事件等重要特性,它是典型的非完备信息博弈。对其的研究具有非常重大的意义,感兴趣的读者可深入了解。

4.参考文献

[1] Brown, N., Kroer, C. and Sandholm, T.: Dynamic Thresholding and Pruning for Regret Minimization, AAAI Conference on Artificial Intelligence (2017).

[2] Lanctot, M., Waugh, K., Zinkevich, M. and Bowling, M.: Monte Carlo Sampling for Regret Minimization in Extensive Games, Advances in Neural Information Processing Systems 22 (Bengio, Y., Schuurmans, D., Lafferty, J. D., Williams, C. K. I. and Culotta, A., eds.), Curran Associates, Inc., pp. 1078–1086 (2009).

[3] Gibson, R. 2014. Regret Minimization in Games and the Development of Champion Multiplayer Computer PokerPlaying Agents. Ph.D. Dissertation, University of Alberta. Gilpin, A., and Sandholm, T. 2007. Lossless abstraction of imperfect information games. Journal of the ACM 54(5).

我们是行者AI,我们在“AI+游戏”中不断前行。

前往公众号 【行者AI】,回复【虎步龙行】,即可领取限量款红包封面。快来和我们一起探讨技术问题吧!