0

我正在尝试实现一种类似于 td-gammon 的双陆棋算法,如此处所述

如论文中所述,td-gammon 的初始版本仅在特征空间中使用原始棋盘编码,这创建了一个良好的游戏代理,但要获得世界级的代理,您需要添加一些与良好相关的预先计算的特征玩。最重要的特征之一是印迹曝光。

印迹暴露在定义为:

对于给定的污点,掷出 36 的数量将允许对手击中污点。总的污点暴露量是 36 次滚动的数量,这将使对手能够击中任何污点。印迹暴露取决于: (a) 印迹前所有敌方人员的位置;(b) 污点和敌方人员之间阻挡点的数量和位置,以及 (c) 横杆上的敌方人数,以及允许他们重新进入棋盘的掷骰,因为横杆上的人必须重新-在印迹可以被击中之前进入。

我尝试了各种方法来有效地计算此功能,但我的计算仍然太慢,我不知道如何加快它。

请记住,td-gammon 方法会评估给定掷骰子的每个可能的棋盘位置,因此对于每个玩家掷骰子的每一轮,您都需要为每个可能的棋盘位置计算此特征。

一些粗略的数字:假设每回合大约有 30 个棋盘位置,平均游戏持续 50 回合,我们得出运行 1,000,000 次游戏模拟需要:(x * 30 * 50 * 1,000,000) / (1000 * 60 * 60 * 24) 天其中 x 是计算特征的毫秒数。让 x = 0.7 我们有大约 12 天的时间来模拟 1,000,000 场比赛。

我真的不知道这是否是合理的时机,但我觉得必须有一个明显更快的方法。

所以这就是我尝试过的:

方法1(掷骰子)

对于 21 个可能的骰子掷骰中的每一个,递归检查是否发生命中。这是此过程的主要工作:

private bool HitBlot(int[] dieValues, Checker.Color checkerColor, ref int depth)
    {
        Moves legalMovesOfDie = new Moves();

        if (depth < dieValues.Length)
        {
            legalMovesOfDie = LegalMovesOfDie(dieValues[depth], checkerColor);
        }

        if (depth == dieValues.Length || legalMovesOfDie.Count == 0)
        {
            return false;
        }

        bool hitBlot = false;

        foreach (Move m in legalMovesOfDie.List)
        {
            if (m.HitChecker == true)
            {
                return true;
            }

            board.ApplyMove(m);
            depth++;
            hitBlot = HitBlot(dieValues, checkerColor, ref depth);
            board.UnapplyMove(m);
            depth--;

            if (hitBlot == true)
            {
                break;
            }
        }

        return hitBlot;
    }

该函数的作用是将一个骰子值数组作为输入(即,如果玩家掷出 1,1,则该数组将为 [1,1,1,1]。然后该函数递归检查是否有命中以及是否命中所以以 true 退出。函数 LegalMovesOfDie 计算该特定骰子值的合法移动。

方法 2(通过印迹)

使用这种方法,我首先找到所有的印迹,然后对于每个印迹,我循环遍历每个可能的骰子值并查看是否发生命中。该功能经过优化,因此一旦骰子值记录了命中,我就不会再将它用于下一个印迹。它还经过优化,仅考虑印迹前面的移动。我的代码:

public int BlotExposure2(Checker.Color checkerColor)
    {
        if (DegreeOfContact() == 0 || CountBlots(checkerColor) == 0)
        {
            return 0;
        }

        List<Dice> unusedDice = Dice.GetAllDice();

        List<int> blotPositions = BlotPositions(checkerColor);

        int count = 0;

        for(int i =0;i<blotPositions.Count;i++)
        {
            int blotPosition = blotPositions[i];

            for (int j =unusedDice.Count-1; j>= 0;j--) 
            {
                Dice dice = unusedDice[j];

                Transitions transitions = new Transitions(this, dice);

                bool hitBlot = transitions.HitBlot2(checkerColor, blotPosition);

                if(hitBlot==true)
                {
                    unusedDice.Remove(dice);

                    if (dice.ValuesEqual())
                    {
                        count = count + 1;
                    }
                    else
                    {
                        count = count + 2;
                    }
                } 
            }
        }


        return count;
    }

方法transitions.HitBlot2 采用一个blotPosition 参数,确保只考虑那些在印迹前面的移动。

这两种实现都很慢,当我使用分析器时,我发现递归是原因,所以我尝试重构它们如下:

  1. 使用 for 循环而不是递归(丑陋的代码,但它更快)
  2. 要使用parallel.foreach,而不是一次检查1个骰子值,我会并行检查这些值。

这是我运行 50000 次功能计算的平均计时结果(请注意,每种方法的计时都是使用相同的数据完成的):

  1. 使用递归的方法 1:每次计算 2.28 毫秒
  2. 使用递归的方法 2:每次计算 1.1 ms
  3. 使用 for 循环的方法 1:每次计算 1.02 毫秒
  4. 使用 for 循环的方法 2:每次计算 0.57 毫秒
  5. 使用 parallel.foreach 的方法 1:每次计算 0.75 毫秒 6 使用 parallel.foreach 的方法 2:每次计算 0.75 毫秒

我发现时间非常不稳定(可能取决于神经网络权重的随机初始化),但大约 0.7 毫秒似乎是可以实现的,如果你回想一下,这会导致 1,000,000 场比赛的 12 天训练。

我的问题是:有谁知道这是否合理?有没有我不知道的更快的算法可以减少训练?

最后一条信息:我正在一台相当新的机器上运行。英特尔 Cote (TM) i7-5500U CPU @2.40 GHz。

需要更多信息,请告诉我,我会提供。

谢谢, 奥菲尔

4

1 回答 1

0

是的,计算这些特征会产生非常复杂的代码。查看 GNU 双陆棋代码。找到eval.c并查看 1008 到 1267 的行。是的,它有 260 行代码。该代码计算击中至少一个棋子的掷骰数,以及击中至少 2 个棋子的掷骰数。如您所见,代码很繁琐。

如果您找到更好的计算方法,请发布您的结果。为了改进,我认为您必须查看董事会代表。你能以不同的方式代表董事会,使这个计算更快吗?

于 2016-10-09T15:24:00.157 回答