Подтвердить что ты не робот

Поиск дерева Монте-Карло: реализация для Tic-Tac-Toe

Изменить: Усложненный полный исходный код, если вы хотите узнать, можете ли вы улучшить работу ИИ: https://www.dropbox.com/s/ous72hidygbnqv6/MCTS_TTT.rar

Изменить: выполняется поиск в поисковом пространстве и происходит перемещение в результате потери. Но действия, приводящие к потерям, часто не посещаются из-за алгоритма UCT.

Чтобы узнать о MCTS (Поиск по дереву Монте-Карло), я использовал алгоритм для создания ИИ для классической игры tic-tac-toe. Я реализовал алгоритм, используя следующий дизайн:

MCTS stages Политика дерева основана на UCT, а политика по умолчанию - выполнять произвольные ходы до окончания игры. То, что я наблюдал при моей реализации, заключается в том, что компьютер иногда совершает ошибочные шаги, потому что он не может "видеть", что конкретный ход приведет к потере напрямую.

Например: Tic Tac Toe example Обратите внимание, что действие 6 (красный квадрат) оценивается немного выше, чем синий квадрат, и поэтому компьютер отмечает это пятно. Я думаю, это связано с тем, что игровая политика основана на случайных ходах, и поэтому существует хорошая вероятность, что человек не поместит "2" в синюю коробку. И если игрок не помещает 2 в синюю коробку, компьютер gaurenteed выигрывает.

Мои вопросы

1) Является ли это известной проблемой с MCTS или это результат неудачной реализации?

2) Какие могут быть возможные решения? Я думаю об ограничении ходов на этапе выбора, но я не уверен: -)

Код для основного MCTS:

    //THE EXECUTING FUNCTION
    public unsafe byte GetBestMove(Game game, int player, TreeView tv)
    {

        //Setup root and initial variables
        Node root = new Node(null, 0, Opponent(player));
        int startPlayer = player;

        helper.CopyBytes(root.state, game.board);

        //four phases: descent, roll-out, update and growth done iteratively X times
        //-----------------------------------------------------------------------------------------------------
        for (int iteration = 0; iteration < 1000; iteration++)
        {
            Node current = Selection(root, game);
            int value = Rollout(current, game, startPlayer);
            Update(current, value);
        }

        //Restore game state and return move with highest value
        helper.CopyBytes(game.board, root.state);

        //Draw tree
        DrawTree(tv, root);

        //return root.children.Aggregate((i1, i2) => i1.visits > i2.visits ? i1 : i2).action;
        return BestChildUCB(root, 0).action;
    }

    //#1. Select a node if 1: we have more valid feasible moves or 2: it is terminal 
    public Node Selection(Node current, Game game)
    {
        while (!game.IsTerminal(current.state))
        {
            List<byte> validMoves = game.GetValidMoves(current.state);

            if (validMoves.Count > current.children.Count)
                return Expand(current, game);
            else
                current = BestChildUCB(current, 1.44);
        }

        return current;
    }

    //#1. Helper
    public Node BestChildUCB(Node current, double C)
    {
        Node bestChild = null;
        double best = double.NegativeInfinity;

        foreach (Node child in current.children)
        {
            double UCB1 = ((double)child.value / (double)child.visits) + C * Math.Sqrt((2.0 * Math.Log((double)current.visits)) / (double)child.visits);

            if (UCB1 > best)
            {
                bestChild = child;
                best = UCB1;
            }
        }

        return bestChild;
    }

    //#2. Expand a node by creating a new move and returning the node
    public Node Expand(Node current, Game game)
    {
        //Copy current state to the game
        helper.CopyBytes(game.board, current.state);

        List<byte> validMoves = game.GetValidMoves(current.state);

        for (int i = 0; i < validMoves.Count; i++)
        {
            //We already have evaluated this move
            if (current.children.Exists(a => a.action == validMoves[i]))
                continue;

            int playerActing = Opponent(current.PlayerTookAction);

            Node node = new Node(current, validMoves[i], playerActing);
            current.children.Add(node);

            //Do the move in the game and save it to the child node
            game.Mark(playerActing, validMoves[i]);
            helper.CopyBytes(node.state, game.board);

            //Return to the previous game state
            helper.CopyBytes(game.board, current.state);

            return node;
        }

        throw new Exception("Error");
    }

    //#3. Roll-out. Simulate a game with a given policy and return the value
    public int Rollout(Node current, Game game, int startPlayer)
    {
        Random r = new Random(1337);
        helper.CopyBytes(game.board, current.state);
        int player = Opponent(current.PlayerTookAction);

        //Do the policy until a winner is found for the first (change?) node added
        while (game.GetWinner() == 0)
        {
            //Random
            List<byte> moves = game.GetValidMoves();
            byte move = moves[r.Next(0, moves.Count)];
            game.Mark(player, move);
            player = Opponent(player);
        }

        if (game.GetWinner() == startPlayer)
            return 1;

        return 0;
    }

    //#4. Update
    public unsafe void Update(Node current, int value)
    {
        do
        {
            current.visits++;
            current.value += value;
            current = current.parent;
        }
        while (current != null);
    }
4b9b3361

Ответ 1

Хорошо, я решил проблему, добавив код:

        //If this move is terminal and the opponent wins, this means we have 
        //previously made a move where the opponent can always find a move to win.. not good
        if (game.GetWinner() == Opponent(startPlayer))
        {
            current.parent.value = int.MinValue;
            return 0;
        }

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

Теперь AI против AI всегда играет на галстуке, а Ai невозможно бить как человек: -)

Ответ 2

Я думаю, что ваш ответ не должен быть отмечен как принятый. Для Tic-Tac-Toe пространство поиска относительно невелико, и оптимальное действие должно быть найдено в пределах разумного числа итераций.

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

Я предлагаю вам взглянуть на backpropagation в методе UCT из этого примера: http://mcts.ai/code/python.html

Вы должны обновить итоговое вознаграждение node на основе вознаграждения, рассчитанного предыдущим игроком на определенном уровне (node.playerJustMoved в примере).

Ответ 3

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

Ваш пример, который показывает AI "failing", поэтому не является "ошибкой", если я прав. Этот способ оценивания движений исходит из вражеских случайных ходов. Эта логика терпит неудачу, потому что это очевидно для игрока, который должен сделать 1 шаг, чтобы выиграть матч.

Поэтому вы должны удалить все узлы, которые содержат следующий node с выигрышем для игрока.

Может быть, я ошибаюсь, было только первое предположение...

Ответ 4

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

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

Без вашего кода я не могу представить дальнейшие комментарии.

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