蒙特卡洛方法-井字棋程序

| |

动态规划Dynamic Programming

如果环境的动态特性完全已知,那么可以直接求解出状态价值函数,但是求解过程较为繁琐,因此一般采用迭代法求解。
假设有一个近似的价值函数序列,$v_0,v_1,v_2,\dots,$从$S^+$映射到实数集$\Reals$,初始的近似值$v_0$除了终止状态的值必须为0外可以任意选取,然后下一轮的迭代使用$v_{\pi}$的贝尔曼方程进行更新,对应任意的$s \isin S$有:

$$ \begin{gather} v_{k+1} = E_{\pi} [ R_{t+1} + \gamma v_k (S_{t+1}) | S_t =s] \notag \\ =\sum_a \pi (a|s) \sum_{s' , r}p(s',r|s,a)[r + \gamma v_k(s')] \end{gather} $$

在保证$v_{\pi}$存在的前提下,序列{${v_k}$} 在$k \to \infty$时收敛到$v_{\pi}$
这就是迭代策略评估算法。

This algorithm is called iterative policy evaluation.

迭代策略评估算法迭代策略评估算法

网格寻路示例

现有一个$4 \times 4$的网格地图,左上角与右下角的位置是终止状态,现在要求解出某个智能体在地图上移动到终止状态的最优策略,智能体可以向上下左右四个方向移动。

网格寻路例题网格寻路例题

非终止状态集合为$S = { 1,2,\dots ,14 }$
每种状态都对应有4种可能的动作$A = { up,down,right,left }$
每个动作会导致状态转移,但若超过网格边界则状态保持不变,如:$P((6,-1)|5,right) = 1$,$P((7,-1)|7,right) = 1$,对任意的$r \isin R$,都有$P((10,r)|5,right) = 0$.
这是一个无折扣的分幕式任务。在到达终止状态之前,所有动作的收益均为-1。对于所有的状态$s,s'$和动作$a$,期望的收益函数均为$r(s,a,s') =-1$。
假设智能体采取等概率的随机策略,即:所有动作的执行概率相等,均为$0.25$.

计算过程如下图,注意图中保留了一位小数,$k=2$时$-1.7$实际上是$-1.75$
$k=2$时:
$(-1) + 0.25 * (-1)+0.25*(-1)+0.25*(-1) + 0.25*0 =-1.75$
$k=3$时:
$(-1.7) + 0.25*(-1)+0.25*(-1)+0.25*(-1) + 0.25*0 =-2.45$
以此类推,得到下图:

计算过程计算过程

第三次迭代后,所有策略都是最优的。

蒙特卡洛Monte Carlo Methods

“蒙特卡洛”泛指任何包含大量随机成分的估计方法。
经典的蒙特卡洛方法解释是在正方形中求圆形面积的示例,这里不再赘述。
《图灵的大教堂:数字宇宙开启智能时代》 [美] 乔治·戴森一书中有关于蒙特卡洛方法诞生历史的一些叙述。

在绝大多数实际场景中,我们并不清楚完整的环境知识,因此不适用动态规划的方法。蒙特卡洛方法仅需采样的经验就可以推理出最优的行为。
前面的多臂赌博机问题中,曾经提到这个通用公式:

$$ \begin{gather} 新的估计值 \gets 旧的估计值 + 步长 \times (目标 - 旧估计值) \end{gather} $$

蒙特卡洛方法的公式:

$$ \begin{gather} Q(S_{t} A_t) \gets Q(S_t,A_t)+ \alpha * (\sum_{i=t}^T R_i-Q(S_t,A_t)) \end{gather} $$

蒙特卡洛方法必须等到一幕的结尾才能确定对$Q(S_{t} A_t)$的增量。
算法:

蒙特卡洛蒙特卡洛

井字棋实例

《Reinforcement Learning: An Introduction》提供了随书代码
https://github.com/ShangtongZhang/reinforcement-learning-an-introduction
其中第一章的代码就是井字棋程序,下面的程序在某些地方参考了例程的代码,同时思路也参考了B站up主PenicillinLP的教程。

python

import numpy as np
import random

# 输入3*3数组,输出元组
def get_tuple(numpy_array):
    return tuple(map(tuple, numpy_array))

def print_board(board):
    for i in range(3):
        print('-------------')
        out = '| '
        for j in range(3):
            if board[i, j] == 1:
                token = '*'
            elif board[i, j] == -1:
                token = 'x'
            else:
                token = '0'
            out += token + ' | '
        print(out)
    print('-------------')

class Agent_my():
    # 参数:玩家索引号,玩家训练参数(Epsilon、Learning_rate)
    # 玩家需要有游戏策略,策略表,after_state,q,r
    def __init__(self,ooxx_index,Epsilon=0.1,Learning_rate=0.1):
        self.index = ooxx_index
        self.epsilon = Epsilon
        self.alpha = Learning_rate
        self.q_values = {}  # Q值表,创建字典
        self.outcomes = []
        self.isepsilon = [] # 记录该步骤是否探索了
        # 字典的键是棋盘可能的状态,值是状态对应的估计收益值

    def reset(self):
        self.outcomes =[]
        self.isepsilon = []

    # 输入当前棋盘状态
    # 选择行动,根据Q值表选择,搜索当前状态下的所有可能的键,
    # 返回下一棋局的状态,即可
    def choose_action(self, current_board):
        valid_moves=[]
        # 先计算所有的outcome
        for row in range(3):
            for col in range(3):
                outc = current_board.copy()
                if outc[row, col] == 0:
                    # 返回outcome
                    outc[row, col] = self.index
                    valid_moves.append(get_tuple(outc))

        if random.uniform(0, 1) < self.epsilon:
            self.isepsilon.append(0)
            # 随机选择动作
            next_board = random.choice(valid_moves)
        else:
            self.isepsilon.append(1)
            # 选择具有最大Q值的动作
            actions ={}
            max_q = 0
            for action in valid_moves:
                temp_q = self.q_values.setdefault(action, 0)
                actions[action] = temp_q
                if temp_q > max_q:
                    max_q = temp_q
            # 得到了最大的q值和actions列表
            # keys_with_value = [key for key, value in my_dict.items() if value == target_value]
            best_actions = [action for action, q_value in actions.items() if q_value == max_q]
            next_board = random.choice(best_actions)

        # 将选取的outcome加入列表
        self.outcomes.append(next_board)

        return np.array(next_board)


    # 更新Q值
    # 输入是reward,current_board
    # 和棋奖励是0.5,赢棋奖励是1,输棋奖励0
    def update_q_value(self, reward):
        # 先单独设置T时刻
        self.q_values[self.outcomes[-1]] = reward

        # 然后设置T-1,T-2...
        for i in reversed(range(len(self.outcomes) - 1)):
            current_t = self.outcomes[i]
            last_T = self.outcomes[i+1]

            # 下面就是程序的关键,计算公式,蒙特卡洛法的计算公式
            # 这里setdefault是为了避免某个键尚不存在
            self.q_values[current_t] = self.q_values.setdefault(current_t,0) + self.alpha * self.isepsilon[i] * (self.q_values[last_T] - self.q_values.setdefault(current_t,0))

# 返回true,false
def board_over(board,player_index):
    # Check rows, columns, and diagonals
    for i in range(3):
        # all()函数用于判断整个数组中的元素的值是否全部满足条件,如果满足条件返回True,否则返回False
        if np.all(board[i, :] == player_index) or \
            np.all(board[:, i] == player_index):
            return True
    if np.all(np.diag(board) == player_index) or \
        np.all(np.diag(np.fliplr(board)) == player_index):
        return True
    return False

class Environment_my():
    def __init__(self):
        self.board = np.zeros((3,3))
        self.winner = None
        self.r1 = 0
        self.r2 = 0
        # 记录胜率
        self.win1 =0
        self.win2 = 0
        self.win0 =0

    def reset(self):
        self.board = np.zeros((3, 3))
        self.winner = None
        self.r1 = 0
        self.r2 = 0

    # 检查游戏是否结束或是和棋
    # 返回true 结束,false未结束,返回1,1号玩家胜利,-1,2号玩家胜利
    def is_game_over(self):
        if board_over(self.board,1) == True:
            self.winner = 1
            self.r1 = 1
            self.win1 +=1
            self.r2 = 0
            return True
        if board_over(self.board,-1) == True:
            self.winner = -1
            self.win2 +=1
            self.r1 = 0
            self.r2 = 1
            return True
        if (np.all(self.board != 0) and self.winner==None):
            self.win0 +=1
            self.r1 = 0.5
            self.r2 = 0.5
            return True
        return False

# human interface
# input a number to put a chessman
# | q | w | e |
# | a | s | d |
# | z | x | c |
class HumanPlayer:
    def __init__(self, **kwargs):
        self.symbol = None
        self.keys = ['q', 'w', 'e', 'a', 's', 'd', 'z', 'x', 'c']
        self.state = None

    def reset(self):
        pass
    def choose_action(self, current_board):
        print_board(current_board)
        key = input("Input your position:")
        data = self.keys.index(key)
        i = data // 3
        j = data % 3
        next_board = current_board.copy()
        next_board[i,j] = -1
        return next_board

def human_test(ai_agent,human_player):
    env = Environment_my()
    while True:
        ai_agent.reset()
        env.reset()
        while True:
            env.board = agent1.choose_action(env.board)
            # print(env.board)
            # 检查游戏是否结束
            if env.is_game_over() == True:
                break
            env.board = human_player.choose_action(env.board)
            if env.is_game_over() == True:
                break
        if env.winner == 1:
            print("You lose!")
        elif env.winner == -1:
            print("You win!")
        else:
            print("It is a tie!")


def test_ai(agent1,agent2,testi=1000):
    agent1.epsilon =0
    agent2.epsilon =0
    env_pk =Environment_my()
    for i in range(testi):
        env_pk.reset()
        agent1.reset()
        agent2.reset()
        while True:
            env_pk.board = agent1.choose_action(env_pk.board)
            if env_pk.is_game_over() == True:
                # agent1.update_q_value(env_pk.r1)
                # agent2.update_q_value(env_pk.r2)
                break
            env_pk.board = agent2.choose_action(env_pk.board)
            if env_pk.is_game_over() == True:
                # agent1.update_q_value(env_pk.r1)
                # agent2.update_q_value(env_pk.r2)
                break
    print(f"Agent 1 Wins: {env_pk.win1 / testi}, Agent 2 Wins: {env_pk.win2 / testi}, Draws: {env_pk.win0 / testi}")


if __name__ == "__main__":
    agent1 = Agent_my(1,0.1,0.1)
    agent2 = Agent_my(-1,0.1,0.1)
    env = Environment_my()

    print("start")
    # 落子
    all_e =100000
    z_e= 1000
    for i in range(all_e):
        env.reset()
        agent1.reset()
        agent2.reset()
        while True:
            env.board = agent1.choose_action(env.board)
            # 检查游戏是否结束
            if env.is_game_over() == True:
                agent1.update_q_value(env.r1)
                agent2.update_q_value(env.r2)
                break
            env.board = agent2.choose_action(env.board)
            if env.is_game_over() == True:
                agent1.update_q_value(env.r1)
                agent2.update_q_value(env.r2)
                break
        if (i + 1) % z_e == 0:
            print(f"Agent 1 Wins: {env.win1 / i}, Agent 2 Wins: {env.win2 / i}, Draws: {env.win0 / i}")
    test_ai(agent1,agent2)


    # 训练完成后,与人类玩家竞赛,看看效果。这里设置了人类玩家后手,机器人先手。
    p2 = HumanPlayer()
    human_test(agent1,p2)

运行效果类似于下图:

井字棋程序输出井字棋程序输出

测试效果还是蛮好的,我基本上不能赢,只能和棋。

测试效果测试效果

Reference