Monte Carlo Tree Search: implémentation pour Tic-Tac-Toe

Edit: Uploded la totalité du code source si vous voulez voir si vous pouvez obtenir de l'IA pour mieux performer: https://www.dropbox.com/s/ous72hidygbnqv6/MCTS_TTT.rar

Edit: l'espace de recherche est recherché et les mouvements entraînant des pertes sont trouvés. Mais les mouvements entraînant des pertes ne sont pas visités très souvent à cause de l'algorithme UCT.

pour en savoir plus sur les MCTS (Monte Carlo Tree Search) j'ai utilisé l'algorithme pour faire une IA pour le jeu classique de tic-tac-toe. J'ai mise en œuvre de l'algorithme à l'aide de la structure suivante:

MCTS stages La Politique de tree est basée sur UCT et la politique par défaut est d'effectuer des mouvements aléatoires jusqu'à la fin du jeu. Ce que j'ai observé avec mon implémentation, c'est que l'ordinateur fait parfois des mouvements errorés parce qu'il ne "voit" pas qu'un mouvement particulier entraînera une perte directe.

Par exemple: Tic Tac Toe example Notez comment l'action 6 (carré rouge) est évaluée légèrement plus haut que le bleu carré et donc l'ordinateur Marque cet endroit. Je pense que c'est parce que la Politique de jeu est basée sur des mouvements aléatoires et donc une bonne chance existe que l'humain ne mettra pas un "2" dans la boîte bleue. Et si le joueur ne met pas un 2 dans la boîte bleue, l'ordinateur est gaurented une victoire.

Mes Questions

1) s'agit-il d'un problème connu des SCTM ou est-ce le résultat d'une mise en œuvre ratée?

2) Quelles sont les solutions possibles? Je suis penser à limiter les mouvements dans la phase de sélection mais je ne suis pas sûr: -)

Le code du noyau des SCTM:

    //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);
    }
18
demandé sur MortenGR 2014-05-22 13:44:14

4 réponses

Ok, j'ai résolu le problème en ajoutant le code:

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

je pense que le problème est que l'espace de recherche était trop petite. Cela garantit que même si selection sélectionne un mouvement qui est réellement terminal, ce mouvement n'est jamais choisi et la ressource est utilisée pour explorer d'autres mouvements à la place :).

maintenant L'AI vs AI joue toujours tie et L'Ai est impossible à battre en tant qu'humain: -)

6
répondu MortenGR 2014-05-23 10:52:03

je pense que votre réponse ne devrait pas être marqué comme acceptée. Pour le Tic-Tac-Toe, l'espace de recherche est relativement petit et l'action optimale doit être trouvée dans un nombre raisonnable d'itérations.

il semble que votre fonction de mise à jour (backpropagation) ajoute la même quantité de récompense aux noeuds à différents niveaux d'arbre. Ce n'est pas correct, puisque les États joueurs actuels sont différents à différents niveaux d'arbre.

je vous suggère de jeter un coup d'oeil à la rétropropagation dans la méthode UCT à partir de cet exemple: http://mcts.ai/code/python.html

vous devez mettre à jour la récompense totale du noeud en fonction de la récompense calculée par le joueur précédent à un niveau spécifique (noeud.playerJustMoved dans l'exemple).

5
répondu krneki 2015-06-23 17:49:08

ma toute première hypothèse est, que la façon dont votre algorithme fonctionne, choisit l'étape qui mène le plus susceptible de gagner le match (a le plus de victoires en fin de match).

votre exemple qui montre L'IA 'failing', n'est donc pas un' bug', si j'ai raison. Cette façon d'évaluer les mouvements produit des mouvements aléatoires ennemis. Cette logique échoue, car il est évident pour le joueur que 1-step est à prendre pour gagner le match.

par conséquent, vous devez effacer tous les noeuds qui contiennent un prochain noeud avec victoire pour le joueur.

peut-être que j'ai tort, était juste une première supposition...

2
répondu Martin Tausch 2014-05-22 09:56:55

il est donc possible dans n'importe quelle heuristique basée au hasard que vous ne cherchiez tout simplement pas un échantillon représentatif de l'espace de jeu. Par exemple: il est théoriquement possible que vous échantillonniez au hasard exactement la même séquence 100 fois, ignorant complètement la branche voisine qui perd. Cela le distingue des algorithmes de recherche plus typiques qui tentent de trouver chaque mouvement.

cependant, il est beaucoup plus probable qu'il s'agisse d'une mise en oeuvre ratée. Le jeu de l'arbre de tick tack n'est pas très grand, environ 9! au premier mouvement, et se rétrécit rapidement, de sorte que son improbable que la recherche d'arbre ne cherche pas chaque mouvement pour un nombre raisonnable d'itérations, et donc devrait trouver un mouvement optimal.

sans votre code, je ne peux pas vraiment fournir d'autres commentaires.

Si je devais deviner, je dirais que peut-être vous choisissez des déplacements sur le plus grand nombre de victoires, plutôt que le plus grand fraction de victoires, et donc généralement de polarisation sélection vers les mouvements qui ont été recherchés la plupart du temps.

1
répondu phil_20686 2014-05-22 10:01:59