Монте-Карло Дерево Поиск Tic-Tac-Toe Game

Дерево Монте-Карло Поиск Tic-Tac-Toe Game

1. обзор

В этой статье мы собираемся изучитьMonte Carlo Tree Search (MCTS) algorithm и его приложения.

Подробно рассмотрим его фазы черезimplementing the game of Tic-Tac-Toe in Java. Мы разработаем общее решение, которое можно будет использовать во многих других практических приложениях с минимальными изменениями.

2. Вступление

Проще говоря, поиск по дереву Монте-Карло является вероятностным алгоритмом поиска. Это уникальный алгоритм принятия решений благодаря его эффективности в открытых средах с огромным количеством возможностей.

Если вы уже знакомы с алгоритмами теории игр, такими какMinimax, для этого требуется функция для оценки текущего состояния, и он должен вычислить множество уровней в дереве игры, чтобы найти оптимальный ход.

К сожалению, это невозможно сделать в такой игре, какGo, в которой есть высокий коэффициент ветвления (что приводит к миллионам возможностей по мере увеличения высоты дерева), и трудно написать хорошую функцию оценки для вычислить, насколько хорошо текущее состояние.

Поиск по дереву Монте-Карло применяетMonte Carlo method к поиску по дереву игр. Поскольку он основан на случайной выборке игровых состояний, ему не нужно грубой силой выходить из каждой возможности. Кроме того, это не обязательно требует от нас написания оценочных или хороших эвристических функций.

И, короткое примечание - это произвело революцию в мире компьютерного Go. С марта 2016 года он стал распространенной темой исследований, посколькуAlphaGo от Google (построенный с помощью MCTS и нейронной сети) превосходитLee Sedol (чемпион мира по Go).

3. Алгоритм поиска по дереву Монте-Карло

Теперь давайте посмотрим, как работает алгоритм. Сначала мы построим дерево упреждения (дерево игры) с корневым узлом, а затем продолжим его расширять с помощью случайных развертываний. При этом мы будем вести подсчет посещений и выигрышей для каждого узла.

В конце мы собираемся выбрать узел с наиболее многообещающей статистикой.

Алгоритм состоит из четырех этапов; давайте рассмотрим их все подробно.

3.1. выбор

На этом начальном этапе алгоритм начинается с корневого узла и выбирает дочерний узел так, что он выбирает узел с максимальной вероятностью выигрыша. Мы также хотим удостовериться, что каждому узлу предоставлен равный шанс.

The idea is to keep selecting optimal child nodes until we reach the leaf node of the tree. Хороший способ выбрать такой дочерний узел - использовать формулу UCT (верхняя граница достоверности, применяемая к деревьям):image, в которой

  • wi = количество выигрышей послеi-го хода

  • ni = количество симуляций послеi-го хода

  • c = параметр разведки (теоретически равный √2)

  • t = общее количество симуляций для родительского узла

Формула гарантирует, что ни одно государство не станет жертвой голода, и она также играет многообещающие ветви чаще, чем их коллеги.

3.2. расширение

Когда он больше не может применять UCT для поиска узла-преемника, он расширяет игровое дерево, добавляя все возможные состояния из конечного узла.

3.3. моделирование

После расширения алгоритм произвольно выбирает дочерний узел и имитирует рандомизированную игру из выбранного узла, пока не достигнет конечного состояния игры. Если узлы выбираются случайно или почти случайно во время воспроизведения, это называется легким воспроизведением. Вы также можете выбрать интенсивную игру, написав качественные эвристические или оценочные функции.

3.4. Обратное распространение

Это также называется фазой обновления. Как только алгоритм достигает конца игры, он оценивает состояние, чтобы выяснить, какой игрок выиграл. Он перемещается вверх к корню и увеличивает оценку посещений для всех посещенных узлов. Он также обновляет счет выигрыша для каждого узла, если игрок на этой позиции выиграл плей-офф.

MCTS продолжает повторять эти четыре фазы до определенного фиксированного количества итераций или определенного фиксированного количества времени.

В этом подходе мы оцениваем выигрышную оценку для каждого узла на основе случайных ходов. Чем выше число итераций, тем достовернее становится оценка. Оценки алгоритма будут менее точными в начале поиска и продолжат улучшаться по прошествии достаточного количества времени. Опять же, это зависит исключительно от типа проблемы.

4. Пробный прогон

imageimage

Здесь узлы содержат статистику в виде общего числа посещений / выигрышей

5. Реализация

Теперь давайте реализуем игру в крестики-нолики, используя алгоритм поиска по дереву Монте-Карло.

Мы разработаем обобщенное решение для MCTS, которое также можно будет использовать для многих других настольных игр. Мы рассмотрим большую часть кода в самой статье.

Хотя для ясности объяснения нам, возможно, придется пропустить некоторые второстепенные детали (не относящиеся к MCTS), но вы всегда можете найти полную реализациюhere.

Прежде всего, нам нужна базовая реализация для классовTree иNode, чтобы иметь возможность поиска по дереву:

public class Node {
    State state;
    Node parent;
    List childArray;
    // setters and getters
}
public class Tree {
    Node root;
}

Поскольку каждый узел будет иметь определенное состояние проблемы, давайте также реализуем классState:

public class State {
    Board board;
    int playerNo;
    int visitCount;
    double winScore;

    // copy constructor, getters, and setters

    public List 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 */
    }
}

Теперь давайте реализуем классMonteCarloTreeSearch, который будет отвечать за поиск следующего лучшего хода из данной игровой позиции:

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();
    }
}

Здесь мы продолжаем итерацию по всем четырем фазам до заданного времени, и в конце мы получаем дерево с надежной статистикой для принятия разумного решения.

Теперь давайте реализуем методы для всех этапов.

We will start with the selection phase, который также требует реализации UCT:

private Node selectPromisingNode(Node rootNode) {
    Node node = rootNode;
    while (node.getChildArray().size() != 0) {
        node = UCT.findBestNodeWithUCT(node);
    }
    return node;
}
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(),
          Comparator.comparing(c -> uctValue(parentVisit,
            c.getState().getWinScore(), c.getState().getVisitCount())));
    }
}

На этом этапе рекомендуется листовой узел, который следует расширить на этапе расширения:

private void expandNode(Node node) {
    List 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);
    });
}

Next, we write code to pick a random node and simulate a random play out from it. Также у нас будет функцияupdate для распространения оценки и количества посещений, начиная с листа до корня:

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;
}

Теперь мы закончили с внедрением MCTS. Все, что нам нужно, это конкретная реализация классаBoard для Tic-Tac-Toe. Обратите внимание, чтобы играть в другие игры с нашей реализацией; Нам просто нужно изменить классBoard.

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 getEmptyPositions() {
        int size = this.boardValues.length;
        List 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;
    }
}

Мы только что реализовали ИИ, который нельзя победить в крестики-нолики. Давайте напишем единичный пример, демонстрирующий, что AI vs. ИИ всегда приводит к ничьей:

@Test
public 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);
}

6. преимущества

  • Это не обязательно требует каких-либо тактических знаний об игре

  • Общая реализация MCTS может быть повторно использована для любого количества игр с небольшими изменениями

  • Ориентирован на узлы с более высокими шансами на победу в игре

  • Подходит для задач с высоким коэффициентом ветвления, поскольку не тратит вычисления на все возможные ветви

  • Алгоритм очень прост в реализации

  • Выполнение может быть остановлено в любой момент времени, и оно по-прежнему будет предлагать следующее лучшее состояние, вычисленное до сих пор.

7. недостаток

Если MCTS используется в своей базовой форме без каких-либо улучшений, она может не предлагать разумных действий. Это может произойти, если узлы не посещаются должным образом, что приводит к неточным оценкам.

Однако MCTS можно улучшить с помощью некоторых методов. Он включает в себя как предметно-зависимые, так и независимые от предметной области методы.

В предметно-ориентированных методах стадия симуляции дает более реалистичные выходы, чем стохастические симуляции. Хотя это требует знания игровых техник и правил.

8. Резюме

На первый взгляд трудно поверить, что алгоритм, основанный на случайном выборе, может привести к интеллектуальному ИИ. Однако вдумчивая реализация MCTS действительно может предоставить нам решение, которое можно использовать во многих играх, а также в задачах принятия решений.

Как всегда, полный код алгоритма можно найтиover on GitHub.