admin管理员组

文章数量:1441241

蒙特卡洛树

1. 概述

在本文中,我们将探讨蒙特卡罗树搜索 (MCTS) 算法及其应用。

我们将通过在Java 中实现井字游戏来详细研究它的阶段。我们将设计一个通用解决方案,该解决方案可用于许多其他实际应用,只需进行最少的更改。

2. 简介

简单地说,蒙特卡罗树搜索是一种概率搜索算法。它是一种独特的决策算法,因为它在具有巨大可能性的开放式环境中的效率。

如果您已经熟悉像 Minimax 这样的博弈论算法,它需要一个函数来评估当前状态,并且它必须在游戏树中计算许多级别才能找到最佳移动。

不幸的是,在像围棋这样分支因子很高的游戏中这样做是不可行的(随着树的高度增加,导致数百万种可能性),并且很难编写一个好的评估函数来计算当前状态有多好。

蒙特卡罗树搜索将蒙特卡罗方法应用于游戏树搜索。由于它基于游戏状态的随机抽样,因此不需要暴力破解每种可能性。此外,它不一定要求我们编写评估或良好的启发式函数。

自 2016 年 3 月以来,随着谷歌的AlphaGo(由 MCTS 和神经网络构建)击败李世石(围棋世界冠军),它已成为一个流行的研究课题。

3. 蒙特卡洛树搜索算法

现在,让我们探讨一下该算法的工作原理。最初,我们将构建一个具有根节点的展望树(游戏树),然后我们将通过随机推出继续扩展它。在此过程中,我们将维护每个节点的访问计数和获胜计数。

最后,我们将选择具有最有希望的统计数据的节点。

该算法由四个阶段组成;让我们详细探讨所有这些。

3.1. 选择

在此初始阶段,算法从根节点开始,然后选择一个子节点,以便选取获胜率最高的节点。我们还希望确保每个节点都有公平的机会。

这个想法是继续选择最佳子节点,直到我们到达树的叶节点。选择此类子节点的一个好方法是使用 UCT(应用于树的置信上限)公式:

其中

Wi =第i步后获胜的次数

Ni =第i步后的模拟次数

C =勘探参数(理论上等于√2)

T =父节点的模拟总数

该公式确保没有状态会成为受害者,并且它也比同类更频繁地发挥有前途的分支。

3.2. 扩展

当它无法再应用 UCT 来查找后续节点时,它会通过附加叶节点中的所有可能状态来扩展游戏树。

3.3. 模拟

扩展后,算法任意选取一个子节点,从所选节点模拟随机博弈,直到达到博弈的结果状态。如果在播放过程中随机或半随机选择节点,则称为轻度播放。您还可以通过编写质量启发式或评估函数来选择繁重的播放。

3.4. 反向传播

这也称为更新阶段。一旦算法到达游戏结束,它就会评估状态以确定哪个玩家赢了。它向上遍历到根,并增加所有访问过的节点的访问分数。如果该位置的玩家赢得了播出,它还会更新每个节点的获胜分数。

MCTS 不断重复这四个阶段,迭代直到固定的次数或固定的时间量。

在这种方法中,我们根据随机移动来估计每个节点的获胜分数。因此,迭代次数越高,估计就越可靠。算法估计在搜索开始时不太准确,并在足够长的时间后继续改进。同样,这完全取决于问题的类型。

4. 演练

在这里,节点包含总访问/获胜分数的统计信息。

5. 实施

现在,让我们实现一个井字游戏 - 使用蒙特卡洛树搜索算法。

我们将为 MCTS 设计一个通用解决方案,该解决方案也可用于许多其他棋盘游戏。下面是大部分核心代码。

首先,我们需要一个基本的实现,让TreeNode类具有树搜索功能:

代码语言:javascript代码运行次数:0运行复制
public class Node {
    State state;
    Node parent;
    List<Node> childArray;
    // setters and getters
}
public class Tree {
    Node root;
}Copy

由于每个节点都有特定的问题状态,让我们也实现一个State类:

代码语言:javascript代码运行次数:0运行复制
public class State {
    Board board;
    int playerNo;
    int visitCount;
    double winScore;

    // copy constructor, getters, and setters

    public List<State> getAllPossibleStates() {
        // constructs a list of all possible states from current state
    }
    public void randomPlay() {
        /* get a list of all possible positions on the board and 
           play a random move */
    }
}Copy

现在,让我们实现MonteCarloTreeSearch类,该类将负责从给定的游戏位置查找下一个最佳移动:

代码语言:javascript代码运行次数:0运行复制
public class MonteCarloTreeSearch {
    static final int WIN_SCORE = 10;
    int level;
    int opponent;

    public Board findNextMove(Board board, int playerNo) {
        // define an end time which will act as a terminating condition

        opponent = 3 - playerNo;
        Tree tree = new Tree();
        Node rootNode = tree.getRoot();
        rootNode.getState().setBoard(board);
        rootNode.getState().setPlayerNo(opponent);

        while (System.currentTimeMillis() < end) {
            Node promisingNode = selectPromisingNode(rootNode);
            if (promisingNode.getState().getBoard().checkStatus() 
              == Board.IN_PROGRESS) {
                expandNode(promisingNode);
            }
            Node nodeToExplore = promisingNode;
            if (promisingNode.getChildArray().size() > 0) {
                nodeToExplore = promisingNode.getRandomChildNode();
            }
            int playoutResult = simulateRandomPlayout(nodeToExplore);
            backPropogation(nodeToExplore, playoutResult);
        }

        Node winnerNode = rootNode.getChildWithMaxScore();
        tree.setRoot(winnerNode);
        return winnerNode.getState().getBoard();
    }
}Copy

在这里,我们不断迭代所有四个阶段,直到预定义的时间,最后,我们得到一个具有可靠统计数据的树来做出明智的决策。

现在,让我们实现所有阶段的方法。

我们将从选择阶段开始,这也需要 UCT 实现:

代码语言:javascript代码运行次数:0运行复制
private Node selectPromisingNode(Node rootNode) {
    Node node = rootNode;
    while (node.getChildArray().size() != 0) {
        node = UCT.findBestNodeWithUCT(node);
    }
    return node;
}Copy
代码语言:javascript代码运行次数:0运行复制
public class UCT {
    public static double uctValue(
      int totalVisit, double nodeWinScore, int nodeVisit) {
        if (nodeVisit == 0) {
            return Integer.MAX_VALUE;
        }
        return ((double) nodeWinScore / (double) nodeVisit) 
          + 1.41 * Math.sqrt(Math.log(totalVisit) / (double) nodeVisit);
    }

    public static Node findBestNodeWithUCT(Node node) {
        int parentVisit = node.getState().getVisitCount();
        return Collections.max(
          node.getChildArray(),
          Comparatorparing(c -> uctValue(parentVisit, 
            c.getState().getWinScore(), c.getState().getVisitCount())));
    }
}Copy

此阶段建议使用应在扩展阶段进一步扩展的叶节点:

代码语言:javascript代码运行次数:0运行复制
private void expandNode(Node node) {
    List<State> possibleStates = node.getState().getAllPossibleStates();
    possibleStates.forEach(state -> {
        Node newNode = new Node(state);
        newNode.setParent(node);
        newNode.getState().setPlayerNo(node.getState().getOpponent());
        node.getChildArray().add(newNode);
    });
}Copy

接下来,我们编写代码来选择一个随机节点并模拟从中随机播放。此外,我们将有一个更新函数来传播从叶到根的分数和访问计数:

代码语言:javascript代码运行次数:0运行复制
private void backPropogation(Node nodeToExplore, int playerNo) {
    Node tempNode = nodeToExplore;
    while (tempNode != null) {
        tempNode.getState().incrementVisit();
        if (tempNode.getState().getPlayerNo() == playerNo) {
            tempNode.getState().addScore(WIN_SCORE);
        }
        tempNode = tempNode.getParent();
    }
}
private int simulateRandomPlayout(Node node) {
    Node tempNode = new Node(node);
    State tempState = tempNode.getState();
    int boardStatus = tempState.getBoard().checkStatus();
    if (boardStatus == opponent) {
        tempNode.getParent().getState().setWinScore(Integer.MIN_VALUE);
        return boardStatus;
    }
    while (boardStatus == Board.IN_PROGRESS) {
        tempState.togglePlayer();
        tempState.randomPlay();
        boardStatus = tempState.getBoard().checkStatus();
    }
    return boardStatus;
}Copy

现在我们已经完成了 MCTS 的实施。我们所需要的只是一个井字游戏特定的Board类实现。请注意,使用我们的实现玩其他游戏;我们只需要更Board类。

代码语言:javascript代码运行次数:0运行复制
public class Board {
    int[][] boardValues;
    public static final int DEFAULT_BOARD_SIZE = 3;
    public static final int IN_PROGRESS = -1;
    public static final int DRAW = 0;
    public static final int P1 = 1;
    public static final int P2 = 2;
    
    // getters and setters
    public void performMove(int player, Position p) {
        this.totalMoves++;
        boardValues[p.getX()][p.getY()] = player;
    }

    public int checkStatus() {
        /* Evaluate whether the game is won and return winner.
           If it is draw return 0 else return -1 */         
    }

    public List<Position> getEmptyPositions() {
        int size = this.boardValues.length;
        List<Position> emptyPositions = new ArrayList<>();
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (boardValues[i][j] == 0)
                    emptyPositions.add(new Position(i, j));
            }
        }
        return emptyPositions;
    }
}Copy

我们刚刚实现了一个在井字游戏中无法击败的AI。让我们写一个单位案例来证明 AI vs.AI 总是会导致平局:

代码语言:javascript代码运行次数:0运行复制
@Test
void givenEmptyBoard_whenSimulateInterAIPlay_thenGameDraw() {
    Board board = new Board();
    int player = Board.P1;
    int totalMoves = Board.DEFAULT_BOARD_SIZE * Board.DEFAULT_BOARD_SIZE;
    for (int i = 0; i < totalMoves; i++) {
        board = mcts.findNextMove(board, player);
        if (board.checkStatus() != -1) {
            break;
        }
        player = 3 - player;
    }
    int winStatus = board.checkStatus();
 
    assertEquals(winStatus, Board.DRAW);
}Copy

6. 优势

  • 它不一定需要任何关于游戏的战术知识
  • 通用的 MCTS 实现可以重用于任意数量的游戏,只需进行少量修改
  • 专注于赢得游戏机会较高的节点
  • 适用于高分支因子的问题,因为它不会浪费所有可能的分支上的计算
  • 算法非常容易实现
  • 可以在任何给定时间停止执行,它仍然会建议到目前为止计算的下一个最佳状态

7. 缺点

如果 MCTS 以基本形式使用而没有任何改进,则可能无法提出合理的举措。如果没有充分访问节点,导致估计不准确,则可能会发生这种情况。

但是,可以使用一些技术来改善 MCTS。它涉及特定于领域的技术以及独立于领域的技术。

在特定领域的技术中,模拟阶段产生更真实的播放,而不是随机模拟。虽然它需要了解游戏特定的技术和规则。

8. 总结

乍一看,很难相信依赖随机选择的算法可以带来智能人工智能。然而,深思熟虑的MCTS实施确实可以为我们提供一种解决方案,可用于许多游戏以及决策问题。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。 原始发表:2023-02-17,如有侵权请联系 cloudcommunity@tencent 删除算法游戏函数解决方案搜索

本文标签: 蒙特卡洛树