非完全信息博弈中的虚拟遗憾最小化(CFR)算法(附实现代码)

一,简介

       前几年,AlphaGo兴起了机器学习的热潮。在围棋这种完全信息的零和博弈中,作为算法需要解决的仅仅只是如何搜索大规模博弈树的问题,但是在德州扑克这种非完全信息博弈的问题中往往还藏有欺骗诈唬等等手段。而且对比围棋每次下棋有限的策略(19*19个交叉点)德州扑克的策略几乎是无限的,专业选手往往押注在1000到十几万甚至百万美元,中间押注10000$和10500$也存在差距。所以由于德州扑克的种种特性,在AlphaGo 4:1战胜李世石的时候,同期的德州扑克还只能解决两人有限注的问题。

        解决此类非完全信息博弈,如果解决的思路是从寻找纳什均衡出发,那么解决手段的基础就是CFR(虚拟遗憾最小化算法)。此方法在2007年被提出后来被改进为CFR+此后的德州扑克算法,比如前几年击败职业选手的冷扑大师,和今年的Pluribus都经过了CFR过程。

算法介绍

        下面介绍CFR算法。首先我们先介绍遗憾最小化算法,以石头剪刀布为例,我们假设赢一局赢一块钱,平局不扣钱,输一局亏一块钱。首先明确,我们的策略集是[石头,剪刀,布]三种策略,初始化后悔值数组r=[0,0,0]代表三种策略的后悔值。在第一次博弈中我们随机出拳。譬如我出了石头,对方除了布,我输了一块。此时开始计算假如我采用了别的会如何?在这一局,假如我出剪刀会赢一块,出布是平局,所以我对于没有出剪刀的后悔值为1-(-1)=2对于布是0-(-1)=1.更新后悔值数组r=[0,2,1]。那么下一次出拳的概率是P(石头)=0,P(剪刀)=2/3,P(布)=1/3.

        但是在诸如德州扑克这种博弈中,计算如果不能遍历计算所有节点的遗憾值,那么可以采用虚拟遗憾最小化算法来进行模拟计算。参考浙江大学 吴飞老师的PPT。

游戏介绍

        下面我们介绍一种游戏kuhn扑克。Kuhn 扑克(Kuhn Poker)是最简单的限注扑克游戏。虽然最原始的规则仅仅需要两个玩家,但 2010 年 Abou Risk 和 Szafron创建了三人的版本,现在规则已经可以适用于 n 个玩家。Kuhn Poker 整付牌有 n+1 张,牌值为 1,2,...,n+1,1 最小,n+1 的牌值最大。每个玩家手持一张底牌和将一定底注投入彩(盲注)。Kuhn Poker 只有一轮且要在翻牌前进行押注,押注筹码是固定的,游戏不允许加注。游戏开始玩家可以选择过牌或者押注,如果一旦有人押注,则其他玩家(包括顺位在押注玩家之后的玩家,和押注玩家之前过牌的玩家)则只能选择弃牌或者跟注。游戏没有公共牌,摊牌阶段比较未弃牌的玩家中底牌牌值大小,底牌牌值最大的玩家即为获胜者。下图展示了 3 人 Kuhn 扑克扩展式博弈树的子博弈树。

 

        图中显示为3个玩家手牌为1,3,4的情况。K ,b ,f , c分别代表,过牌,押注,弃牌,跟注。图中盲注大小为1,押注大小也为1。两人kuhn扑克博弈收益如下图。

代码实现

game.py

import numpy as np

# 牌
poker = [1, 2, 3]
# 存储状态
in_game_state = ['1', '1b', '1p', '1pb', '2', '2b', '2p', '2pb', '3', '3b', '3p', '3pb']

states = {'1': [0.5, 0.5], '1b': [0.5, 0.5], '1p': [0.5, 0.5], '1pb': [0.5, 0.5], '2': [0.5, 0.5], '2b': [0.5, 0.5],
          '2p': [0.5, 0.5], '2pb': [0.5, 0.5], '3': [0.5, 0.5], '3b': [0.5, 0.5], '3p': [0.5, 0.5], '3pb': [0.5, 0.5]}

end = ['pp', 'pbp', 'pbb', 'bp', 'bb']

CFR_r = {'1': [0, 0], '1b': [0, 0], '1p': [0, 0], '1pb': [0, 0], '2': [0, 0], '2b': [0, 0],
         '2p': [0, 0], '2pb': [0, 0], '3': [0, 0], '3b': [0, 0], '3p': [0, 0], '3pb': [0, 0]}

pass_bet = {'p': 0, 'b': 1}

action = ['p', 'b']

# CFR中间状态
CFR_s = {'1': [0, 0], '1b': [0, 0], '1p': [0, 0], '1pb': [0, 0], '2': [0, 0], '2b': [0, 0],
         '2p': [0, 0], '2pb': [0, 0], '3': [0, 0], '3b': [0, 0], '3p': [0, 0], '3pb': [0, 0]}
CFR_s2 = {'1': [0, 0], '1b': [0, 0], '1p': [0, 0], '1pb': [0, 0], '2': [0, 0], '2b': [0, 0],
          '2p': [0, 0], '2pb': [0, 0], '3': [0, 0], '3b': [0, 0], '3p': [0, 0], '3pb': [0, 0]}

# 已经计算好的策略
AI_state = {'1': [0.8574384003665548, 0.14256159963344525], '1b': [0.999985067494923, 1.4932505077051726e-05],
            '1p': [0.660594597675478, 0.33940540232452193], '1pb': [0.9999911732218897, 8.826778110280645e-06],
            '2': [0.9997037628675545, 0.00029623713244554913], '2b': [0.6618884775698471, 0.3381115224301529],
            '2p': [0.9999699762812622, 3.0023718737802863e-05], '2pb': [0.5291090066614956, 0.4708909933385044],
            '3': [0.5781543746887431, 0.42184562531125686], '3b': [3.0112318949682314e-05, 0.9999698876810503],
            '3p': [1.5056159474841157e-05, 0.9999849438405252], '3pb': [1.2844892512449064e-05, 0.9999871551074876]}

AI_state2 = {'1': [0.9005897997728478, 0.0994102002271522], '1b': [0.9999998499623105, 1.5003768946759426e-07],
             '1p': [0.6660743153746168, 0.3339256846253833], '1pb': [0.999999916728828, 8.327117200450158e-08],
             '2': [0.9999943092803633, 5.6907196367392786e-06], '2b': [0.6667338506314542, 0.33326614936854576],
             '2p': [0.9999989492266308, 1.050773369199731e-06], '2pb': [0.5656811999441647, 0.43431880005583534],
             '3': [0.7008390070018635, 0.2991609929981364], '3b': [1.498520660404049e-07, 0.9999998501479339],
             '3p': [1.498520660404049e-07, 0.9999998501479339], '3pb': [1.0708730371989468e-07, 0.9999998929126963]}

AI_state3 = {'1': [0.8273490325307964, 0.17265096746920355], '1b': [0.999998499111474, 1.5008885260073963e-06],
             '1p': [0.6667986655056403, 0.3332013344943597], '1pb': [0.9999990919856274, 9.080143725515324e-07],
             '2': [0.9999405784977654, 5.94215022346119e-05], '2b': [0.6643358318791153, 0.3356641681208847],
             '2p': [0.9999801511805928, 1.98488194072019e-05], '2pb': [0.4941674200495377, 0.5058325799504624],
             '3': [0.4823765523726862, 0.5176234476273138], '3b': [1.5010912933702802e-06, 0.9999984989087066],
             '3p': [1.5010912933702802e-06, 0.9999984989087066], '3pb': [1.555232740630912e-06, 0.9999984447672594]}

AI_state4 = {'1': [0.834303037809882, 0.1656969621901179], '1b': [0.9999984955709609, 1.5044290390910841e-06],
             '1p': [0.6659521147790602, 0.33404788522093976], '1pb': [0.9999991023431777, 8.976568223041392e-07],
             '2': [0.9999750183896345, 2.4981610365392336e-05], '2b': [0.662709856181752, 0.33729014381824796],
             '2p': [0.9999927129173443, 7.287082655696929e-06], '2pb': [0.5039222434922419, 0.4960777565077581],
             '3': [0.5037166880570191, 0.49628331194298103], '3b': [1.5008164441456152e-06, 0.9999984991835559],
             '3p': [1.5008164441456152e-06, 0.9999984991835559], '3pb': [1.4874700238643287e-06, 0.9999985125299762]}


class Player(object):
    def __init__(self, is_human, hand, decision):
        self.is_human = is_human # 是否人类玩家
        self.hand = hand # 手牌
        self.information_set = str(hand) #所处信息集
        self.decision = decision # 策略集上的概率分布

    # 选择出牌
    def choice(self, h, display):
        if self.is_human == 0:
            print('your hand is:', self.hand)
            return self.human_choice()
        else:
            return self.AI_choice(h, display)

    def human_choice(self):
        a = input('p or b?:')
        print('Human choice:', a)
        self.information_set += a
        return a

    def AI_choice(self, h, display):
        a = np.random.choice(['p', 'b'], p=self.decision[str(self.hand) + h])
        if display == 1:
            print('AI distribution:', self.decision[str(self.hand) + h])
            print('AI choice:', a)
        self.information_set += a
        return a


class Game(object):
    def __init__(self, p1, p2):
        self.p1 = p1
        self.p2 = p2
        self.end = 0
        self.result = []

    # 游戏流程
    def flow(self, h, display):
        if h in end:
            self.end = 1
            self.result = self.judge(h)
            return
        if len(h) % 2 == 0:
            a = self.p1.choice(h, display=display)
        else:
            a = self.p2.choice(h, display=display)
        self.flow(h + a, display)

    # 判断胜负
    def judge(self, h):
        if h == 'pp':
            if self.p1.hand > self.p2.hand:
                return [1, -1]
            else:
                return [-1, 1]

        if h == 'pbp':
            return [-1, 1]

        if h == 'pbb' or h == 'bb':
            if self.p1.hand > self.p2.hand:
                return [2, -2]
            else:
                return [-2, 2]

        if h == 'bp':
            return [1, -1]

    # 显示结果
    def show_result(self):
        print('result:', self.result)
        print('p1 hand:', self.p1.hand)
        print('p2 hand:', self.p2.hand)

    # CFR流程
    def CFR_algorithm(self, h, pai1, pai2):
        if h in end:
            tmp = self.judge(h)
            return tmp

        if len(h) % 2 == 0:
            tmp_h = str(self.p1.hand) + h
        else:
            tmp_h = str(self.p2.hand) + h
        va = [0, 0]
        for a in action:
            # 玩家1
            if len(h) % 2 == 0:
                tmp_va = self.CFR_algorithm(h + a, pai1 * states[tmp_h][pass_bet[a]], pai2)
                va[pass_bet[a]] = tmp_va[0]
            else:
                tmp_va = self.CFR_algorithm(h + a, pai1, pai2 * states[tmp_h][pass_bet[a]])
                va[pass_bet[a]] = tmp_va[1]
        # 平均虚拟效用
        ave_va = states[tmp_h][0] * va[0] + states[tmp_h][1] * va[1]

        if len(h) % 2 == 0:
            oppo_pai = pai2
            self_pai = pai1
        else:
            oppo_pai = pai1
            self_pai = pai2

        CFR_r[tmp_h][0] = CFR_r[tmp_h][0] + oppo_pai * (va[0] - ave_va)
        CFR_r[tmp_h][1] = CFR_r[tmp_h][1] + oppo_pai * (va[1] - ave_va)
        CFR_s[tmp_h][0] = CFR_s[tmp_h][0] + self_pai * states[tmp_h][0]
        CFR_s[tmp_h][1] = CFR_s[tmp_h][1] + self_pai * states[tmp_h][1]
        CFR_s2[tmp_h][0] = CFR_s[tmp_h][0] / (CFR_s[tmp_h][0] + CFR_s[tmp_h][1])
        CFR_s2[tmp_h][1] = CFR_s[tmp_h][1] / (CFR_s[tmp_h][0] + CFR_s[tmp_h][1])

        if len(h) % 2 == 0:
            self.change_states(h, 1)
            return [ave_va, -ave_va]
        else:
            self.change_states(h, 2)
            return [-ave_va, ave_va]

    def change_states(self, h, p):
        if p == 1:
            tmp_h = str(self.p1.hand) + h
        else:
            tmp_h = str(self.p2.hand) + h

        p = max([CFR_r[tmp_h][0], 0])
        b = max([CFR_r[tmp_h][1], 0])
        if p == 0 and b == 0:
            states[tmp_h] = [0.5, 0.5]
        else:
            states[tmp_h] = [p / (p + b), b / (p + b)]

 

play.py

import numpy as np
import game

if __name__ == '__main__':
    np.random.seed(1)
    # 训练好的代码进行人机游戏
    np.random.shuffle(game.poker)
    # p1是人 p2代表电脑
    p1 = game.Player(0, game.poker[0], game.AI_state)
    p2 = game.Player(2, game.poker[1], game.AI_state)
    game1 = game.Game(p1, p2)
    game1.flow('', 1)
    game1.show_result()

    # 自己进行CFR训练
    '''
    p1 = game.Player(2, game.poker[0], game.states)
    p2 = game.Player(2, game.poker[1], game.states)
    game1 = game.Game(p1, p2)

    for i in range(100000):
        np.random.shuffle(game.poker)
        p1.hand = game.poker[0]
        p2.hand = game.poker[1]
        game1.CFR_algorithm('', 1, 1)

    print(game.CFR_s2)
    '''
  • 22
    点赞
  • 72
    收藏
    觉得还不错? 一键收藏
  • 8
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值