Published on

蒙特卡洛树搜索(Monte Carlo Tree Search)的技术演进(2): AlphaZero

Authors
  • avatar
    Name
    Yu Jiang
    Twitter

前言

2016年,DeepMind推出AlphaGo,将监督学习+深度强化学习+蒙特卡洛树搜索的组合应用在状态空间和动作空间具有一定复杂度的围棋这款棋类游戏中,击败了世界冠军李世石,论文登上《Nature》, 重燃了人类追求人工智能的热情,并为通用人工智能的探索方向指明了路线,AGI = 深度学习 + 强化学习。2017年,DeepMind推出AlphaGo Zero, 摒弃掉领域知识人类棋谱的依赖,仅仅依赖深度强化学习+蒙特卡洛树搜索的组合轻松击败AlphaGo,再次登上《Nature》,2018年,DeepMind推出AlphaGo Zero更通用的版本AlphaZero,在围棋,国际象棋,日本将棋上同时超越人类选手,登上《Science》。

因为这个系列的主角是蒙特卡洛树搜索算法,因此,在这篇文章中,我会先简要的总结相比于AlphaGo, AlphaGo Zero进行了哪些改进,并从蒙特卡洛树搜索算法为第一视角,探究AlphaGo, AlphaGo Zero在蒙特卡洛树搜索算法上的演进变化,分析如何将深度学习技术应用在该算法上,最后将重点落在更通用的版本Alpha Zero上。

1 AlphaGo Zero与AlphaGo的差异

AlphaGo主要做了这么几件事

  • 基于领域知识,将围棋的盘面信息手动加工为特征信息,进入模型
  • 构造一个独立的策略网络,基于16万局人类专家数据,进行策略网络的监督学习训练
  • 构造一个独立的价值网络,用于预测一个状态的最优价值
  • MCTS是经典的四个阶段,选择,扩展,模拟,回溯。但在这个过程中,引入了策略网络和价值网络
  • 在状态s处扩展子节点,在扩展节点的同时,使用策略策略网络的概率输出p(s, a)给没一个子节点一个先验概率
  • 选择阶段,使用pUCT(Predictive Upper Confidence Bound for Trees)进行动作的选择,就是在UCB公式的基础上加入了扩展节点时的先验概率p(s,a)
  • 模拟阶段,不再使用一个随机策略进行rollout的生成,而是使用一个(fast)策略网络,指导rollout阶段的动作选择
  • 回溯:回溯的值 = 模拟的rollout的累计回报 + 价值网络对状态s的价值评估

AlphaGo中的MCTS算法与原始的UCT算法相比,核心是把深度学习技术(策略网络和价值网络)引入了蒙特卡洛树搜索算法中,增强了她的智能性。

相比于AlphaGo,AlphaGo Zero主要做了以下几件事

  • 仅使用棋盘上的黑白棋子作为输入特征,不再基于领域知识构造人工特征,因此叫做without Human Knowledge
  • 不再依赖人类专家数据,对策略网络进行监督学习训练。仅仅依赖深度强化学习+蒙特卡洛树搜索
  • 仅使用一个神经网络结构,其中一个头作为策略网络输出动作概率,另一头作为价值网络估计状态的最优价值
  • MCTS变为如下四个阶段:选择,扩展,评估,回溯。
  • 选择,扩展阶段与alphago一致,不再进行模拟阶段的蒙特卡洛rollout进行状态评估,而是直接用价值网络对状态进行评估,然后进入回溯阶段

2 AlphaZero与AlphaGo Zero的差异

AlphaZero来源于文章《A general reinforcement learning algorithm that masters chess, shogi and Go through self-play》,相比于AlphaGo Zero, 该算法旨在解决两个问题:1是提供一个更通用目的的强化学习算法,2是提供一个更通用目的的树搜索算法。同一个卷积神经网络结构,在围棋,国际象棋,日本将棋同时超越人类选手。

  1. 提供一个更通用目的的强化学习算法
    • AlphaGo Zero是估计和优化获胜的概率,因为围棋只有输赢的二分类结果,但是其他游戏存在平局的情况,因此AlphaZero估计和优化的是结果的期望值
    • 强化学习过程中,自我博弈棋局(self-play games)的生成机制存在差异
      • In AlphaGo Zero, self-play games were generated by the best player from all previous iterations. After each iteration of training, the performance of the new player was measured against the best player; if the new player won by a margin of 55% then it replaced the best player. By contrast, AlphaZero simply maintains a single neural network that is updated continually, rather than waiting for an iteration to complete. Self-play games are always generated by using the latest parameters for this neural network.
  2. 提供一个更通用目的的树搜索算法
    • 由于AlphaGo和AlphaGo Zero都是用来解决围棋问题,因此在两个地方可以利用围棋旋转和发射具有不变性的特点。第一个点是, 每一个位置生成8个对称性从而增强人类专家的训练数据(AlphaGo使用,AlphaGo Zero不再使用),第二个点是:在蒙特卡洛树搜索算法中,棋盘上的位置在被价值网络评估之前,被随机选择的旋转或者反射转换,用于平均在评估中存在的偏差(AlphaGo Zero在使用这个特点)。由于其他棋类游戏不具有围棋的对称性特点,为了达到通用的树搜索目的,AlphaZero在MCTS过程中不再使用与具体游戏相关的特殊性质。

3 AlphaZero算法

class ValueNeuralNet():
    @classmethod
    def evaluate(self, game_state):
        return np.random.random()

class PolicyNeuralNet():
    @classmethod
    def evaluate(self, game_state, action_size=4):
        return np.random.random([action_size])

class AlphaZeroNeuralNet():
    @classmethod
    def evaluate(self, game_state):
        state_value = ValueNeuralNet.evaluate(game_state)
        action_prob = PolicyNeuralNet.evaluate(game_state)
        return action_prob, state_value

def alphazero_mcts(game_state, num_reads, mdp):
    root = UCTNode(game_state)
    for _ in range(num_reads):
        leaf = root.select_leaf()
        child_priors,value_estimate = AlphaZeroNeuralNet.evaluate(leaf.game_state)
        leaf.expand(child_priors, mdp)
        leaf.backup(value_estimate)
    return max(root.children.items(),key=lambda item: item[1].number_visits)

从细节上看,AlphaZero做了这么几件事

  1. 构造了一个统计的神经网络架构AlphaZeroNeuralNet, 可以对任意一个状态,给出该状态的动作概率(策略网络)和最优价值的评估值(价值网络)
  2. 给定一个状态game_state, 会进行一次完整的蒙特卡洛树搜索,最终的目的是得到给状态下的经过搜索后的价值最大的那个动作
  3. 蒙特卡洛树搜索的具体流程: 将给定的状态game_state创建为根节点,进行num_reads次的搜索。每次搜索,都是a先基于pUCT公式,逐层找到叶子节点leaf = root.select_leaf(),b然后基于网络结构,对叶子节点的状态进行推断,基于策略网络,给出叶子节点状态k个动作的先验概率,同时给出叶子节点状态的价值评估。c扩展,将叶子节点进行一次扩展,扩展出k个节点,每个节点赋予一个先验概率,同时基于mdp,得到每个子节点的状态。d最后将叶子节点状态的价值估计值进行回溯,更新搜索路径上的信息。

值得注意的还有两个细节。

  1. 为什么说AlphaZero是一个model-based的算法?这里的model体现在什么地方?

序列决策的语境里,model是指的环境的model,也就是环境的MDP,原始的UCT算法在两个地方使用了MDP,第一个地方是在扩展阶段,假定一个叶子节点的状态是s,该节点状态对应的动作有k个, 因此我可以一次扩展k个子节点,但我如何知道这个子节点的状态呢?这个时候就需要依赖MDP, 比如说我们想知道第一个动作对应的子节点的状态,则需要调用MDP的dynamic函数,next_state = mdp.step(s, a_1)。

  1. 在MCTS的过程中,除了依赖MDP的dynamics,还依赖了什么?

给定一个状态,策略网络会为所有可能的动作生成先验概率,但是在真实的环境中,有些状态会面临一些非合法的动作,因此,在蒙特卡洛树搜索(MCTS)中的扩展节点阶段,需要依赖MDP给出叶子节点状态对应的合法动作,然后只对合法动作(legal_actions)进行子节点的扩展。为了实现这一点,需要进行以下数据处理和转换:

  1. 过滤合法动作:从策略网络输出的所有动作概率中,筛选出合法动作。
  2. 归一化概率:对筛选出的合法动作的概率进行归一化,使其总和为1。
import numpy as n

def filter_and_normalize(probs, legal_actions):
    """
    过滤合法动作并归一化概率
    
    :param probs: 策略网络输出的所有动作的概率,形状为 (num_actions,)
    :param legal_actions: 合法动作的列表,例如 [0, 1, 3]
    :return: 归一化后的合法动作概率,形状为 (num_legal_actions,)
    """
    # 过滤合法动作的概率
    legal_probs = probs[legal_actions]
    
    # 归一化概率
    legal_probs /= np.sum(legal_probs)
    
    return legal_probs

all_probs = np.array([0.1, 0.3, 0.2, 0.4])

# 合法动作
legal_actions = [0, 1, 3]

# 过滤并归一化
normalized_legal_probs = filter_and_normalize(all_probs, legal_actions)

print("归一化后的合法动作概率:", normalized_legal_probs)

完整的代码如下

import numpy as np
import math

def filter_and_normalize(probs, legal_actions):
    """
    过滤合法动作并归一化概率
    
    :param probs: 策略网络输出的所有动作的概率,形状为 (num_actions,)
    :param legal_actions: 合法动作的列表,例如 [0, 1, 3]
    :return: 归一化后的合法动作概率,形状为 (num_legal_actions,)
             legal_actions: 合法动作的列表(用于映射)
    """
    # 过滤合法动作的概率
    legal_probs = probs[legal_actions]
    
    # 归一化概率
    legal_probs /= np.sum(legal_probs)
    
    return legal_probs, legal_actions

def map_to_original_space(legal_probs, legal_actions, num_actions):
    """
    将过滤后的概率映射回原始动作空间
    
    :param legal_probs: 过滤并归一化后的合法动作概率,形状为 (num_legal_actions,)
    :param legal_actions: 合法动作的列表
    :param num_actions: 原始动作空间的大小
    :return: 映射回原始动作空间的概率,形状为 (num_actions,)
    """
    # 初始化原始动作空间的概率数组
    original_probs = np.zeros(num_actions)
    
    # 将过滤后的概率值填充到对应的原始动作位置
    for action, prob in zip(legal_actions, legal_probs):
        original_probs[action] = prob
    
    return original_probs


class UCTNode:
    def __init__(self, state, parent=None, prior=0):
        self.state = state  # Current state in the MDP
        self.is_expanded = False
        self.parent = parent  # Parent node
        self.children = {}  # Dictionary of actions to child nodes
        self.prior = prior  # Prior probability of the node
        self.total_value = 0  # Cumulative reward from simulations
        self.number_visits = 0  # Number of times the node has been visited

    def Q(self):
        """Compute the average reward (exploitation term)."""
        return self.total_value / (1 + self.number_visits)

    def U(self):
        """Compute the exploration term."""
        if self.parent is None:
            return 0  # Root node has no exploration term
        return (math.sqrt(self.parent.number_visits) * self.prior) / (1 + self.number_visits)

    def best_child(self):
          return max(self.children.values(),
               key=lambda node: node.Q() + node.U())

    def select_leaf(self):
        current = self
        while current.is_expanded:
            current = current.best_child()
        return current
    
    def expand(self, prior_probability, mdp):
        """Expand the node by generating child nodes for all legal actions."""
        if mdp.is_terminal(self.state):  # 如果是终结状态,直接返回
            self.is_expanded = False
            return
        legal_actions = mdp.get_legal_actions(self.state)
        if not legal_actions:  # 如果没有合法动作,标记为已扩展
            self.is_expanded = False
            return
        
        normalized_legal_probs, legal_actions = filter_and_normalize(prior_probability, legal_actions)
        # 映射回原始动作空间
        num_actions = len(prior_probability)
        original_space_probs = map_to_original_space(normalized_legal_probs, legal_actions, num_actions)

        
        for action in legal_actions:
            next_state, reward = mdp.step(self.state, action)
            prior = original_space_probs[action]
            self.children[action] = UCTNode(next_state, parent=self, prior=prior)
        self.is_expanded = True

    def backpropagate(self, value_estimate):
        """Backpropagate the value estimate up the tree."""
        current = self
        while current is not None:
            current.number_visits += 1
            current.total_value += value_estimate
            current = current.parent

class ValueNeuralNet():
    @classmethod
    def evaluate(self, game_state):
        return np.random.random()

class PolicyNeuralNet():
    @classmethod
    def evaluate(self, game_state, action_size=4):
        return np.random.random([action_size])

class AlphaZeroNeuralNet():
    @classmethod
    def evaluate(self, game_state):
        state_value = ValueNeuralNet.evaluate(game_state)
        action_prob = PolicyNeuralNet.evaluate(game_state)
        return action_prob, state_value

def alphazero_mcts(game_state, num_reads, mdp):
    root = UCTNode(game_state)
    for _ in range(num_reads):
        leaf = root.select_leaf()
        child_priors,value_estimate = AlphaZeroNeuralNet.evaluate(leaf.game_state)
        leaf.expand(child_priors, mdp)
        leaf.backup(value_estimate)
    return max(root.children.items(),key=lambda item: item[1].number_visits)

后记

AlphaZero摒弃掉了需要依赖人类专家数据的监督学习,由一个更通用的纯深度强化学习算法和一个更通用的蒙特卡洛树搜索技术构成,不再依赖领域知识,将强化学习的能力边界提升到了一个新的高度,给通用人工智能指引了方向。AlphaZero告诉我们,只要你有一个环境模型(environment model, world model, MDP),你就可以在一个特定状态,基于这个环境模型进行规划,然后选择那个规划好的模型,同时策略网络直接学习那个规划后的结果,使得策略网络的输出越来越接近规划的结果,这就是learning by planning.

除了游戏,棋牌这类相对规则清晰的场景,其他在一些特定领域或者特定问题上,很难获得一个已知的环境模型用来做规划,这种问题如何解决?AlphaZero的下一篇MuZero告诉我们,没有一个已知的环境模型的情况下,我们可以基于历史轨迹数据,学习一个环境模型来做规划,从而达到更通用的目的。

参考文献

Silver, D., Huang, A., Maddison, C.J., Guez, A., Sifre, L., Van Den Driessche, G., Schrittwieser, J., Antonoglou, I., Panneershelvam, V., Lanctot, M. and Dieleman, S., 2016. Mastering the game of Go with deep neural networks and tree search. nature, 529(7587), pp.484-489.

Silver, D., Schrittwieser, J., Simonyan, K., Antonoglou, I., Huang, A., Guez, A., Hubert, T., Baker, L., Lai, M., Bolton, A. and Chen, Y., 2017. Mastering the game of go without human knowledge. nature, 550(7676), pp.354-359.

Silver, D., Hubert, T., Schrittwieser, J., Antonoglou, I., Lai, M., Guez, A., Lanctot, M., Sifre, L., Kumaran, D., Graepel, T. and Lillicrap, T., 2017. Mastering chess and shogi by self-play with a general reinforcement learning algorithm. arxiv preprint arxiv:1712.01815.