【问题标题】:Pacman AI - Minimax Application - Avoiding repeated game tree statesPacman AI - Minimax 应用程序 - 避免重复的游戏树状态
【发布时间】:2020-02-26 16:55:04
【问题描述】:

在一个项目的上下文中,在 UC Berkley pacman ai 项目(它的第二部分)之后,我想在没有 alpha-beta 剪枝的情况下实现 minimax 算法,用于布局足够小以至于递归不是递归的对抗代理一个问题。

将问题定义为 2 人(我们假设只有 1 个幽灵)、回合制、具有完美信息的零和游戏后,应用递归将非常简单。然而,由于许多不同的策略最终可能处于相同的游戏状态(定义为 pacman 的位置、ghost 的位置、食物的位置和当前正在玩的玩家的元组),我想找到一种方法来避免重新计算所有这些状态。

我搜索并阅读了一些关于转置表的内容。但是,我不确定如何使用这种方法,我认为我应该实现的是以下内容: 每次扩展尚未访问的状态时,将其添加到“已访问”集合中。如果状态已经扩展,那么如果轮到最大玩家 (pacman),则返回 +inf 值(通常不会被最小玩家选择),如果轮到最小玩家,则相应地返回 -inf。

我认为这个想法的问题,以及它适用于某些布局但不适用于其他布局的原因是,当我点击一个节点时,其所有子节点都已展开,我必须选择的唯一值from 是 +/- 无穷大。这导致无限值向上传播并被选中,而实际上这种游戏状态有可能导致损失。我想,我已经理解了这个问题,但我似乎无法找到解决它的方法。

还有其他方法可以用来避免计算重复的游戏状态吗?有没有我不知道的标准方法?

这是一些伪代码:

 def maxPLayer(currentState, visitedSet):
     if not isTerminalState
         for nextState, action in currentState.generateMaxSuccessors()
             if nextState not in visitedSet
                mark nextState as visited
                scores = scores + [minPlayer(nextState, visitedSet)]
         if scores is not empty
            return bestScore = max(scores)
         else
            return +inf              #The problem is HERE!
     else
         return evalFnc(currentState)
 end MaxPlayer

def minPlayer(currenstState, visitedSet):
    if not isTerminal
        for nextState, action in generateMinSuccessors()
            if nextState not in visitedSet 
                mark nextState as visited
                scores = scores + [maxPLayer(nextState, visitedSet)]
        if scores is not empty
            return bestScore = min(scores)
        else
            return -inf            #The problem is also HERE!
    else
        return evalFnc(currentState)
   end MinPlayer

请注意,第一个玩的玩家是最大的,我选择得分最高的动作。无论我是否考虑无限值,都不会发生任何变化,仍然存在代理失败或无限循环的游戏实例。

【问题讨论】:

    标签: algorithm artificial-intelligence memoization minimax pacman


    【解决方案1】:

    我遇到的问题最终与'game state' 的定义以及必须如何处理'repeated states' 有关。

    事实上,考虑一个游戏状态树和一个特定的游戏状态x,它由以下标识:

    • pacman 的位置。
    • 网格上食物颗粒的数量和位置。
    • 幽灵的位置和方向(方向被考虑在内,因为幽灵被认为无法转半圈。

    现在假设您开始沿着树的某个分支向下走,并且在某个时刻您访问了节点 x。假设它之前没有被访问过并且它不是游戏的最终状态,则应该将此节点添加到访问过的节点集合中。

    现在假设一旦你完成了树的这个特定分支,你就开始探索另一个分支。经过一定数量的未确定步骤后,您再次到达标识为x 的节点。这就是问题中代码的问题所在。

    事实上,虽然定义的游戏状态完全相同,但到达该状态所遵循的路径却不同(因为我们目前处于与原始分支不同的新分支上)。显然,考虑状态为已访问或使用最后一个分支计算的效用是错误的。它会产生意想不到的结果。

    解决这个问题的方法很简单,就是为树的每个分支设置一组单独的已访问节点。这样就避免了上述情况。从那时起,可以考虑两种策略:

    1. 第一个包括考虑循环遍历已经访问过的状态作为 pacman 的最坏情况和 ghost 的最佳策略(这显然不是严格正确的)。考虑到这一点,树的同一分支中的重复状态被视为一种“终端”状态,它们将 -inf 作为实用程序返回。
    2. 第二种方法包括使用转置表。然而,这并不容易实现:如果一个节点不在字典中,则将其初始化为无穷大,以表明它当前正在被计算,如果稍后访问,则不应重新计算。当到达终端状态时,在所有节点上递归时,将当前节点与相应终端状态之间的游戏得分差异存储在字典中。如果在遍历一个分支时您访问了一个已经在字典中的节点,则返回当前游戏分数(这取决于您到达该节点的路径,并且可以从一个分支更改为另一个分支)加上字典中的值(这是从该节点到终端状态的分数形式的增益(或损失),并且始终相同)。

    在更实际的情况下,第一种方法实现起来非常简单,每次将它作为参数传递给下一个玩家时复制集合就足够了(这样不同分支中的值不会相互影响)。这将使算法显着变慢,即使对于非常小、简单的迷宫(1 个食物颗粒和可能 7x7 的迷宫)也应该应用 alpha beta。在任何其他情况下,python 都会抱怨递归或解决问题的时间太长(超过几分钟)。然而它是正确的。

    第二种方法更复杂。我没有正式的正确性证明,尽管直觉上它似乎有效。它的速度明显更快,并且还与 alpha beta 剪枝兼容。

    对应的伪代码很容易从解释中推导出来。

    【讨论】:

      【解决方案2】:

      我认为你的方法的主要缺点是你认为已经访问过的状态是对手移动到的不受欢迎的目标。您应该检索在第一次访问该状态时计算的值,而不是返回无穷大值。

      实际上这意味着您应该使用(状态->值)映射而不是(状态)集合。

      只有在尚未计算第一次访问的值的情况下(因为递归调用导致访问 祖先 状态),您才需要使用保留值。但是让那个值是 undefined/null/None,这样它就不会被视为其他数值结果,而是会被排除在可能的路径之外,即使在回溯时也是如此。

      作为旁注,我将在函数的 start 处(在当前状态上)执行状态查找和标记,而不是在相邻状态的循环内。 p>

      以下是这两个函数之一的外观:

      def maxPLayer(currentState, evaluatedMap):
          if currentState in evaluatedMap
              return evaluatedMap.get(currentState)
      
          evaluatedMap.set(currentState, undefined)
      
          if not isTerminalState
              bestScore = undefined
              for nextState in currentState.generateMaxSuccessors()
                  value = minPlayer(nextState, evaluatedMap)
                  if value != undefined
                      scores.append(value)
              if scores is not empty
                  bestScore = max(scores)
          else
              bestScore = evalFnc(currentState)
      
          evaluatedMap.set(currentState, bestScore)
          return bestScore
      end MaxPlayer
      

      undefined 将在访问状态期间使用,但它的值尚未确定(因为挂起的递归调用)。如果一个状态是当前玩家没有有效的移动(被“卡住”),那么该状态将永久获得值undefined,在其他情况下,值undefined最终将被真实分数替换。

      【讨论】:

      • 我想根据你所描述的你的意思是:if currentState in evaluatedMap...。如果你想编辑它。如果不是这正是我想要的,只需要实际测试它。
      • 这种方法似乎不起作用......在 python 中使用字典实现后,我在到达终端状态之前达到了 resursion 限制。这意味着在开始填充字典之前会重复状态。我想。
      • 我不能早点回来。你说的对;当再次访问当前递归分支中的祖先状态时,这将卡住。您可以在其最终分数尚未确定时为该状态保留一个特殊值(不是数字),并将其从任何路径中排除。查看更新以回答。
      • 这是另一个有趣的想法,感谢您的尝试。然而,似乎仍然存在 score 为空的情况,在这种情况下 bestScore 未定义。未定义的值最终会传播回根,此时我抛出异常。再次,我试图尽可能接近你的想法,我希望不是我的实现有问题。
      • 很可能在某个时刻,唯一可用的移动是已经访问过的状态,由一个有利于来回行走的评估函数驱动。如果您可以生成一个简单的案例,包含您的实际评估函数和其他未知数,以便我们重现它,我们可能会提供更多帮助。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-12
      • 2017-11-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多