蒙特卡罗树搜索:用于井字棋的实现

18

编辑:上传了完整的源代码,如果您想查看是否能让AI表现更好:https://www.dropbox.com/s/ous72hidygbnqv6/MCTS_TTT.rar

编辑:搜索空间被搜索,导致失败的移动被发现。但是由于UCT算法,会很少访问导致失败的移动。

为了学习MCTS(蒙特卡洛树搜索),我使用该算法制作了经典游戏井字棋的人工智能。我使用以下设计实现了该算法:

MCTS stages 树策略基于UCT算法,而默认策略则是执行随机移动,直到游戏结束。我所观察到的是,有时计算机会因为无法“看到”某个特定移动将直接导致失败,而做出错误的移动。

例如: Tic Tac Toe example 请注意,动作6(红色方块)的价值略高于蓝色方块,因此计算机标记了这个位置。我认为这是因为游戏策略基于随机移动,因此存在很大的机会,人类不会在蓝色框中放置“2”。如果玩家没有在蓝色框中放置2,则计算机将保证获胜。

我的问题

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

我不理解为什么要在UCB公式中添加C * Math.Sqrt((2.0 * Math.Log((double)current.visits)) / (double)child.visits)。这个术语是用来干什么的?如果删除这部分会发生什么? - phil_20686
这是根据以下编码:http://www.cameronius.com/cv/mcts-survey-master.pdf(第9页)- BestChild编写的。如果我将其删除,AI仍然会执行“愚蠢”的移动。 - MortenGR
该论文提到该算法适用于“深度有限极小化搜索”。在极小化中,您为自己的走法和对手的走法应用相同的得分启发式方法。我从未听说过一种人工智能会假设它正在与进行随机移动的对手对局。 - vgru
1
Groo: 如果我理解得正确的话,Monte Carlo Tree Search不使用启发式算法(它可以用于诸如围棋等难以指定领域知识的游戏)。在展开阶段,会使用特定策略来模拟游戏,而这通常是随机移动(再次,如果我理解算法正确的话)。 - MortenGR
这个在 Github 上有吗? - mcintyre321
不好意思,但您可以在以下链接下载:https://drive.google.com/file/d/0B6Fm7aj1SzBlWGI4bXRzZXBJNTA/view?usp=sharing - MortenGR
4个回答

7
我认为您的答案不应被标记为接受的。对于Tic-Tac-Toe,搜索空间相对较小,可以在合理的迭代次数内找到最佳动作。
看起来您的更新函数(反向传播)将相同数量的奖励添加到不同树层级的节点中。这是不正确的,因为在不同的树层级上,当前玩家的状态是不同的。
我建议您查看UCT方法中的反向传播示例: http://mcts.ai/code/python.html 您应该根据先前玩家在特定层级(例如node.playerJustMoved)计算的奖励来更新节点的总奖励。

6

好的,我通过添加代码解决了这个问题:

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

我认为问题在于搜索空间太小了。这会确保即使选择一个实际上是终止动作的移动,也不会被选中,并且资源将用于探索其他移动 :)。

现在,人工智能VS人工智能总是打平局,人类无法战胜AI :-)


这个页面顶部的链接已经失效了。你能否将整个项目上传到某个地方并分享新链接?我计划学习你的示例,然后扩展它以创建一款纸牌游戏的人工智能。 - dotNET
你可以在这里下载:https://drive.google.com/file/d/0B6Fm7aj1SzBlWGI4bXRzZXBJNTA/view?usp=sharing(之前已从DropBox切换到Google Drive)即使我让AI工作了,我也不确定它是否完全按照MCTS的要求工作。如果你在AI方面取得进展,我会很感激如果你能分享一下(或指出我的错误:-)) - MortenGR
AI似乎表现不佳。请考虑以下棋盘: 1-0-2 2-0-0 1-0-0 现在,“计算P1”应该在右下角单元格中得到“1”,但实际上它建议在中间右侧单元格中得到“1”。这是你已经知道的吗?还是由于没有正确实现你所建议的MCTS而导致的结果? - dotNET
我自从2014年以来就没有碰过这段代码了。很抱歉,我记不得了,也没有时间去看它。但是如果你创建了更好的人工智能,请告诉我,我会更新这篇文章的内容。 - MortenGR

2
我的第一个猜测是,你的算法选择最有可能赢得比赛的步骤(在终节点中具有最多的胜利)。
如果我理解正确的话,你展示AI“失败”的例子并不是“bug”。这种评估移动方式基于敌方随机移动。这种逻辑会失败,因为玩家很明显知道该采取哪个1步来赢得比赛。
因此,你应该删除所有包含下一个节点为玩家获胜的节点。
也许我错了,这只是我的第一个猜测...

感谢回复。如果我理解正确的话,您的解决方案是删除所有可能导致在下一轮中(对玩家而言)输掉的移动。我也考虑过这个方法,但我想要更有技巧性的东西 :-) - MortenGR
我通常不是那个太理论化的人,但我会考虑一下 :) 这是一个非常有趣的问题! - Martin Tausch

1
在任意基于随机的启发式算法中,有可能你并没有搜索游戏空间的代表样本。例如,理论上可能会出现这样的情况,你随机抽取了完全相同的序列100次,完全忽略了输掉的相邻分支。这使得它与更典型的搜索算法有所区别,后者试图找到每一步。
然而,更有可能的是这是一个失败的实现。井字棋的游戏树并不是很大,第一步大约为9!,并且迅速缩小,因此树搜索应该对合理数量的迭代搜索每个移动,并找到最佳移动。
如果没有您的代码,我无法提供进一步的评论。
如果我要猜测,我会说也许您是根据获胜次数最多的移动来选择移动,而不是获胜比例最大的移动,因此通常会偏向于搜索最多次的移动。

谢谢回复。如果您想查看代码,我已将其添加到帖子中。搜索空间(以及可能导致损失的移动)在树中被识别出来,但由于UCT算法的选择,它们并不经常被访问。使用前面的示例,请参见此扩展树: https://www.dropbox.com/s/muwew62f7edaszw/ttt2.png。执行操作3可能会导致人类选择操作2,从而导致价值为0。但它也可以导致操作5、6或8,从而产生更多的价值。请注意,操作2仅被访问了10次。 - MortenGR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接