强化学习简单入门——训练井字棋AI

MDP马尔科夫决策过程

这个说来抽象,其实蛮简单,这里直接摘抄一下WIKI的定义
马尔可夫决策过程是一个4 元组SAPaRa(S,A,P_ {a},R_ {a}) ,且
S是一组有限的状态,A是一组有限的动作(或者,AsA_ {s}如 是状态可提供的有限操作集s,Pass= Prst+1=sst=sat=aP_ {a}(s,s')= \ Pr(s_ {t + 1} = s'\mid s_ {t} = s,a_ {t} = a)是状态s会经过动作a转变到状态 s’ 的概率,RassR_ {a}(s,s')是从状态转换后获得的奖励(或预期的奖励)
也就是状态间可以通过特定的动作进行转换,而且动作有可能对应着奖励。
在这里插入图片描述
我们的强化学习将基于MDP的假设进行。

值迭代算法

基于MDP假设的强化学习中,我们会用一个状态奖励(V表)来记录在某个状态下执行某个动作的预期奖赏。如果MDP的环境已知,即我们知道了states的总数和每个state对应的可能的actions,就可以用值迭代算法通过N次迭代计算出每个状态对应的状态值函数V(x)。迭代公式是
V(x)=maxaAxXPxxa(Rxxa+γV(x)) V'(x) = max_{a \in A} \sum_{x' \in X} P_{x \rightarrow x'}^{a}(R_{x \rightarrow x'}^{a}+\gamma V(x'))
即我们每次迭代对所有状态按照上式进行一次更新,并且在每个状态的前瞻动作-状态对中,贪心地选择最好的reward值来更新当前的V值。最终我们会得到一个收敛的V函数,在决策时,只需要在每一步的x时,选择让下一步的V值V(x)V(x')最大的动作a(xx)a(x\rightarrow x')即可。

Q-learning

但是事实往往并不如所愿,想要完全获悉一个MDP过程的全部state信息和action信息在正常任务中一般是不可行的。事实上我们常常发现,一个MDP中的状态数会随着独立的unit数目指数级别增长,即使是斗地主这种只有50几张牌,3个玩家的游戏,所对应的全部状态也几乎无法计算。而状态对应的动作就更为具有挑战性了,毕竟状态数已经很多,如果每种状态对应的动作很多,很多时候我们只有在获知状态后才能得知下一步的动作。
为此我们有一种不需要得知MDP的环境信息也能进行学习的算法,Q-learning。因为现在我们不能得到状态数,也就无法建立V表,但我们可以建立一种基于状态-动作对的估值函数Q(x,a),也就是Q表。为了代替上面的算法,这种算法允许agent通过在环境中不断采样来更新Q值,并在多次更新求平均的过程中达到收敛。
Q(x,a)=(1α)Q(x,a)+α(R(x,a)+γQ(x,a)) Q(x,a) = (1-\alpha)Q(x,a)+\alpha (R(x',a') + \gamma Q(x',a'))
其中alpha是学习率,表示Q值的更新速度;gamma是奖赏discount。其中x’的选取是依赖当前学习到的策略Q的,有时我们也会为了探索而附加一些噪音。这种学习方法允许在线学习,而且可以帮助我们避开很多无效的状态。这种让agent亲自在环境中探索并学习的方式也更易于应用。

学习井字棋

当我们要把上面的算法应用到实际场景中也会面临一些难题。用一个最简单的井字棋对弈问题为例,这个问题的状态空间总数是39=196833^9=19683,似乎并不是太多。但是其中有着相当多的无效状态,因为我们的对弈是每个玩家轮流下子,而且一旦一方胜利游戏就会结束,再并且每一步的action都是不同的,都需要独立计算。这就让值迭代算法变得不可行。
而如果我们要用Q-learning来学习,又有其他问题。这个问题中我们让agent在环境中采样,就只能让两个agent互相下棋,而agent每下一步所获得的reward都是无法获知的。我们需要自己设计其他的评估策略来从游戏的胜负平中得到奖赏信息。

  1. 井字棋游戏是双方平等的游戏。每次一方落子后换边下棋,只需要把黑白子反转,就能用完全相同的评估策略来评估局势。评估局势的过程和action的关系并不大,在当前的状态x采取某状态变换到下一个状态x’,局势的好坏只取决于x’.因此我们的学习目标是一个值函数V(x)。
  2. 奖赏的设计。我们在游戏结束后会判一方胜利或两方平局,很显然胜利的一方的所有决策都对这个胜利有帮助,我们会为所有操作奖赏1,并用Q-learning为所有状态提供衰减过后的奖赏。同理,失败的一方会受到-1的惩罚奖励。如果两者平局,我们的经验告诉我们,后手方是井字棋游戏中明显处于劣势的一方,我们也会给先手方一个小的惩罚-0.1,而后手方则会得到小的奖励0.1。

具体实现

想让AI学会玩游戏我们至少要先做好一个游戏。首先设计井字棋游戏的基本逻辑和V表,我们的棋盘会用一个长度9的字符串表示,对己方、对方和空格分别用O X _代指。

# 首先设计井字棋游戏的基本逻辑和V表

values = {}
#我们的棋盘会用一个长度9的字符串表示,对己方、对方和空格分别用O X _代指

def exchange(board):
    '''
    输入:board,string,棋盘字符串
    输出:交换OX后的棋盘字符串
    '''
    board = board.replace('O','0')
    board = board.replace('X','x')
    board = board.replace('0','X')
    board = board.replace('x','O')
    return board

def EnumAllStates(size = 9):
    '''
    输出:所有可能的state字符串
    用于初始化values表
    '''
    prefix = ["O","X","_"]
    if size==1:
        return prefix
    postfix = EnumAllStates(size-1)
    ret = []
    for pre in prefix:
        for post in postfix:
            ret.append(pre+post)
    return ret


states = EnumAllStates()
for state in states:
    values[state] = 0

另外,我们会枚举所有状态来初始化V表。
然后我们实现井字棋游戏的逻辑判断,包括基础的胜负和平局。为了让agent能够探索井字棋世界,我们会定义探索"任意落子后的下一个状态"的函数,并定义一个策略,让agent通过查V表来选择最优的落子方案。

import numpy as np
import random

def WinCheck(board):
    '''
    输入:board,string,棋盘字符串
    输出:string,胜负判断
    '''
    board = list(board)
    board = np.array(board).reshape(3,3)
    if (np.diag(board)=='O').all():
        return "win"
    elif (np.diag(np.flipud(board))=='O').all():
        return "win"
    elif (board[0]=='O').all() or (board[1]=='O').all() or (board[2]=='O').all():
        return "win"
    elif (board[:,0]=='O').all() or (board[:,1]=='O').all() or (board[:,2]=='O').all():
        return "win"
    elif (np.diag(board)=='X').all():
        return "lose"
    elif (np.diag(np.flipud(board))=='X').all():
        return "lose"
    elif (board[0]=='X').all() or (board[1]=='X').all() or (board[2]=='X').all():
        return "lose"
    elif (board[:,0]=='X').all() or (board[:,1]=='X').all() or (board[:,2]=='X').all():
        return "lose"
    elif '_' not in board.squeeze():
        return "draw"
    else:
        return "unknown"


def successors(board):
    '''
    得到board的所有落子选项对应的状态
    并返回含有所有后续状态的列表
    '''
    ret = []
    for i in range(9):
        if board[i]=='_':
            ret.append(board[:i]+'O'+board[i+1:])
    return ret

def policy(board, values, noise = 0):
    '''
    根据values做出决策,noise可以得到一些
    非最优选择
    '''
    states = successors(board)
    if random.random()<noise:
        return random.choice(states)
    state = max(states,key = lambda key:values[key])
    return state

然后就是定义超参和训练了,我这里定义了10%的探索噪声,0.8的折扣和0.2的学习率,为胜利提供1的奖赏(失败-1),为后手平局提供0.1的奖赏(先手平局-0.1).

def train(values, lr = 0.2, noise = 0.1, iters = 100000,
         discount = 0.8, win_reward = 1, draw_reward = 0.1):
    '''
    通过对抗方式训练values表
    '''
    Awin = 0
    Bwin = 0
    draw = 0
    
    
    for t in range(iters):
        pathA = []  # 先手的状态路径
        pathB = []  # 后手的状态路径
        player = -1
        path_dict = {-1:pathA,1:pathB}
        board = "_________"
        winner = 0  #0表示平局,1为B获胜,-1为A获胜
        while(1):
            # 得到下一步的落子策略
            state = policy(board, values, noise = noise)
            path_dict[player].append(state)
            # 胜负检测
            info = WinCheck(state)
            if info=="win":
                winner = player
            elif info=="lose":
                winner = -player
            elif info=="draw":
                winner = 0
            if info!="unknown":
                break
            board = exchange(state) #交换O与X
            player = -player  #交换玩家
        # 更新values
        if winner==-1:
        # 奖励A,惩罚B
            reward = win_reward
            Awin += 1
        elif winner==1:
            # 奖励B,惩罚A
            reward = -win_reward
            Bwin += 1
        elif winner==0:
            # 轻微惩罚A,轻微奖励B
            reward = -draw_reward
            draw += 1
        values[pathA[-1]] = (1-lr)*values[pathA[-1]]+lr*reward
        for i in range(len(pathA)-2,-1,-1):
            values[pathA[i]] = (1-lr)*values[pathA[i]]+\
            lr*(reward+values[pathA[i+1]]*discount)
        values[pathB[-1]] = (1-lr)*values[pathB[-1]]+lr*(-reward)
        for i in range(len(pathB)-2,-1,-1):
            values[pathB[i]] = (1-lr)*values[pathB[i]]+\
            lr*((-reward)+values[pathB[i+1]]*discount)
        if (t+1)%500==0:
            print("Iteration: %d"%(t+1))
            print("A win times: %d"%(Awin))
            print("B win times: %d"%(Bwin))
            print("Draw times: %d"%(draw))
            print("**---------------------**")
        
    return values

训练十万次,观察胜利、失败和平局次数变化。可以观察到从A胜B的多一些,到两者经常平局。我们认为算法已经收敛。

# 我们可以用学习完毕的values来建立一个下棋bot
# 让它和人类对弈来检验算法正确性

def PrintBoard(board):
    for i in range(3):
        for j in range(3):
            print(board[i*3+j],end = ' ')
        print()


def agent(values):
    player = input("您想要先手还是后手?(enter o or d)")
    player = -1 if player=='o' else 1
    board = "_________"
    PrintBoard(board)
    while(1):
        if player == -1:
            #人类下棋
            n = input("请输入落子位置(1~9)")
            n = int(n)-1
            board = board[:n]+'O'+board[n+1:]
            PrintBoard(board)
            
        else:
            # 机器下棋
            print("Agent正在思考...")
            board = policy(board, values)
            board = exchange(board)
            PrintBoard(board)
            board = exchange(board)
        
        # 胜负检测
        info = WinCheck(board)
        if info!="unknown":
            break
        
        player = -player
        board = exchange(board)
        
    print("Game over!")

agent(values)

我们还可以让机器和人对弈;可以看见,想赢下现在的agent已经并不简单了。

您想要先手还是后手?(enter o or d)o
_ _ _ 
_ _ _ 
_ _ _ 
请输入落子位置(1~9)5
_ _ _ 
_ O _ 
_ _ _ 
Agent正在思考...
_ _ X 
_ O _ 
_ _ _ 
请输入落子位置(1~9)6
_ _ X 
_ O O 
_ _ _ 
Agent正在思考...
_ _ X 
X O O 
_ _ _ 
请输入落子位置(1~9)9
_ _ X 
X O O 
_ _ O 
Agent正在思考...
X _ X 
X O O 
_ _ O 
请输入落子位置(1~9)8
X _ X 
X O O 
_ O O 
Agent正在思考...
X X X 
X O O 
_ O O 
Game over!


您想要先手还是后手?(enter o or d)d
_ _ _ 
_ _ _ 
_ _ _ 
Agent正在思考...
_ _ _ 
X _ _ 
_ _ _ 
请输入落子位置(1~9)5
_ _ _ 
X O _ 
_ _ _ 
Agent正在思考...
_ _ X 
X O _ 
_ _ _ 
请输入落子位置(1~9)1
O _ X 
X O _ 
_ _ _ 
Agent正在思考...
O _ X 
X O _ 
_ _ X 
请输入落子位置(1~9)6
O _ X 
X O O 
_ _ X 
Agent正在思考...
O _ X 
X O O 
_ X X 
请输入落子位置(1~9)7
O _ X 
X O O 
O X X 
Agent正在思考...
O X X 
X O O 
O X X 
Game over!

总结

这个强化学习的例子虽然简单,我这里实现只用了一个半小时,但是它包含了基础的强化学习思想和算法。希望你能够通过这个简单的例子,学会使用Q-learning来解决一些简单的问题。
当然如果你有兴趣也可以深入学习,强化学习还有很多更先进的知识。像是用神经网络代替Q表的DQL,启发式的方法,值函数近似和梯度方法等。
以DQL为例,如果我们要实现一个棋盘为11x11的五子棋AI,则状态空间到达31213^121那么大,用Q表进行Q-learning是不现实的。为此我们可以设计一个输入是1x11x11的卷积神经网络,并用这个神经网络学习一个Q值的回归任务。我们知道卷积神经网络有着很强的特征提取能力,只要设计合适的卷积核大小(比如对五子棋的五子连珠使用5x5的卷积核,或两层3x3的卷积核提取特征),就能从当前的棋盘中提取到合适的pattern,并把这些pattern通过全连接层拟合成能表征Q值的连续函数。神经网络回归比起用Q表死记硬背要来得更为高效和节约。前几年大热的AlphaGo就是用类似的技术实现的。当然,这个技术还可以用于很多其他领域。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章