CS188-Project 2

Minimax

算法简述

Minimax算法又名极小化极大算法。该算法基于一种假设,即对手也会进行最优操作并总是会采取最损人利己的策略,最后找出失败的最大可能性中的最小值的结果。

Minimax算法常用于棋类等由两方较量的游戏和程序,这类程序由两个游戏者轮流,每次执行一个步骤。我们众所周知的五子棋、象棋等都属于这类程序,所以说Minimax算法是基于搜索的博弈算法的基础。

该算法是一种零和算法,即一方要在可选的选项中选择将其优势最大化的选择,而另一方则选择令对手优势最小化的方法。

游戏过程

而在吃豆人游戏中,假设吃豆人初始分数为10,在吃到豆子之前每一步会扣一分,吃到豆子时游戏会达到终端状态并结束。

一个状态的值定义为一个agent从该状态出发能得到的最佳输出(效益)。可以简单地认为一个agent的效益就是其得到的分数。一个终端状态的值,称为终端效益。

在游戏中,有一个敌方幽灵想阻止吃豆人吃到豆子,这个幽灵会改变吃豆人原本认为最优的行动,而新的最优行动由minimax算法确定。Minimax算法只通过最大化吃豆人控制节点的子节点并同时将幽灵控制节点的子节点最小化,而不是将树的每一层的子节点的效益最大化。有的时候吃豆人想要到达终端效益,但是通过minimax吃豆人“知道”一个会采取最优行动的幽灵不会让他成功。为了采取最优行动,吃豆人必须止损,并且反直觉地远离豆子来让他的损失最小化。

代码实现

class MinimaxAgent(MultiAgentSearchAgent):
    """
    Your minimax agent (question 2)
    """

    def getAction(self, gameState):
        """
        Returns the minimax action from the current gameState using self.depth
        and self.evaluationFunction.

        Here are some method calls that might be useful when implementing minimax.

        gameState.getLegalActions(agentIndex):
        Returns a list of legal actions for an agent
        agentIndex=0 means Pacman, ghosts are >= 1

        gameState.generateSuccessor(agentIndex, action):
        Returns the successor game state after an agent takes an action

        gameState.getNumAgents():
        Returns the total number of agents in the game

        gameState.isWin():
        Returns whether or not the game state is a winning state

        gameState.isLose():
        Returns whether or not the game state is a losing state
        """
        "*** YOUR CODE HERE ***"
       #将最大值初始定义为负无穷大
        maxVal = -float('inf')
        bestAction = None
        for action in gameState.getLegalActions(0):
            # 求出接下来的所有MIN值,并和maxVal比较,求出MAX值
            value = self._getMin(gameState.generateSuccessor(0, action))
            # 满足条件则更新maxVal值,并记下bestAction
            if value is not None and value > maxVal:
                maxVal = value
                bestAction = action
        return bestAction
        
    def _getMax(self, gameState, depth = 0, agentIndex = 0):
        # 获得下一步
        legalActions = gameState.getLegalActions(agentIndex)
        # 如果遍历到根节点或无可继续节点,则返回评价值
        if depth == self.depth or len(legalActions)==0:
            return self.evaluationFunction(gameState)
        maxVal = -float('inf')
        # 对吃豆人下一步可行的操作进行遍历
        for action in legalActions:
            # 从第一个鬼怪开始MIN遍历
            value = self._getMin(gameState.generateSuccessor(agentIndex, action), depth, 1)
            if value is not None and value > maxVal:
                maxVal = value
        return maxVal
    
    def _getMin(self, gameState, depth = 0, agentIndex = 1):
        # 获得鬼怪的下一步操作
        legalActions = gameState.getLegalActions(agentIndex)
        # 同样,如果遍历到根节点或无可继续节点,则返回评价值
        if depth == self.depth or len(legalActions)==0:
            return self.evaluationFunction(gameState)
        minVal = float('inf')
        # 对当前鬼怪的可行下一步进行遍历,其中要递归调用以计算其他鬼怪的行动
        for action in legalActions:
            # 如果当前已经是最后一只鬼怪,那么下一轮就该是计算吃豆人的行为了,即调用MAX函数
            if agentIndex == gameState.getNumAgents() - 1:
             
                value = self._getMax(gameState.generateSuccessor(agentIndex, action), depth+1, 0)
            else:
                value = self._getMin(gameState.generateSuccessor(agentIndex, action), depth, agentIndex+1)
            if value is not None and value < minVal:
                minVal = value
        return minVal

结果展示

Alpha-Beta Pruning

算法简述

alpha-beta剪枝算法是基于极大极小搜索算法的。极大极小搜索策略是考虑双方对弈若干步之后,从可能的步中选一步相对好的走法来走,在有限的搜索范围内进行求解,可以理解为规定一个有限的搜索深度。

定义极大层的下界为alpha,极小层的上界为beta,alpha-beta剪枝规则描述如下:

(1)alpha剪枝。若任一极小值层结点的beta值不大于它任一前驱极大值层结点的alpha值,即alpha(前驱层) >= beta(后继层),则可终止该极小值层中这个MIN结点以下的搜索过程。这个MIN结点最终的倒推值就确定为这个beta值。

(2)beta剪枝。若任一极大值层结点的alpha值不小于它任一前驱极小值层结点的beta值,即alpha(后继层) >= beta(前驱层),则可以终止该极大值层中这个MAX结点以下的搜索过程,这个MAX结点最终倒推值就确定为这个alpha值。

游戏过程

前文提到的Minimax已经能够较好的找到最佳的解决方案。但它的执行与DFS相似,时间复杂度较大,达到了O(b^m)。为了改进这一问题,需对minimax进行优化采用α-β剪枝的方式,为吃豆人寻找最优解决策略,降低时间复杂度。

代码实现

class AlphaBetaAgent(MultiAgentSearchAgent):
    """
    Your minimax agent with alpha-beta pruning (question 3)
    """

    def getAction(self, gameState):
        """
        Returns the minimax action using self.depth and self.evaluationFunction
        """
        "*** YOUR CODE HERE ***"
        # 从根节点开始展开,求MAX值
        return self._getMax(gameState)[1]
        
    def _getMax(self, gameState, depth = 0, agentIndex = 0, alpha = -float('inf'),
               beta = float('inf')):
        # 如果到达叶子节点,或者无法继续展开,则返回当前状态的评价值
        legalActions = gameState.getLegalActions(agentIndex)
        if depth == self.depth or len(legalActions)==0:
            return self.evaluationFunction(gameState), None
        # 否则,就继续往下遍历吃豆人可能的下一步
        maxVal = None
        bestAction = None
        for action in legalActions:
            # 考虑只有一个吃豆人的情况,直接求其MIN分支的评价值,agentIndex从1开始遍历所有鬼怪
            value = self._getMin(gameState.generateSuccessor(agentIndex, action), depth, 1, alpha, beta)[0]
            if value is not None and (maxVal == None or value > maxVal):
                maxVal = value
                bestAction = action
            # 按照α-β剪枝算法,如果v>β,则直接返回v
            if value is not None and value > beta:
                return value, action
            # 按照α-β剪枝算法,这里还需要更新α的值
            if value is not None and value > alpha:
                alpha = value
        return maxVal, bestAction
    
    def _getMin(self, gameState, depth = 0, agentIndex = 0, alpha = -float('inf'),
               beta = float('inf')):
        # 如果到达叶子节点,或者无法继续展开,则返回当前状态的评价值
        legalActions = gameState.getLegalActions(agentIndex)
        if depth == self.depth or len(legalActions)==0:
            return self.evaluationFunction(gameState), None
        # 否则,就继续往下遍历当前鬼怪可能的下一步
        minVal = None
        bestAction = None
        for action in legalActions:
            # 如果当前是最后一个鬼怪,那么下一轮就该调用MAX函数
            if agentIndex >= gameState.getNumAgents() - 1:
                value = self._getMax(gameState.generateSuccessor(agentIndex, action), depth+1, 0, alpha, beta)[0]
            else:
                # 如果不是最后一个鬼怪,则继续遍历下一个鬼怪,即agentIndex+1
                value = self._getMin(gameState.generateSuccessor(agentIndex, action), depth, agentIndex+1, alpha, beta)[0]
            if value is not None and (minVal == None or value < minVal):
                minVal = value
                bestAction = action
            # 按照α-β剪枝算法,如果v<α,则直接返回v
            if value is not None and value < alpha:
                return value, action
            # 按照α-β剪枝算法,这里还需要更新β的值
            if value is not None and value < beta:
                beta = value
        return minVal, bestAction

结果展示

在这里插入图片描述

Expectimax

算法简述

然而,在学习minimax后,不难发现由于minimax假设它面对的是一个最优的对手,在对手不一定会做出最优行动的情况下,minimax就不再适用。

故考虑minimax的一种随机化的方式,从而将算法更新为
expectimax。Expectimax在游戏树中加入了机会节点,与考虑最坏情况的最小化节点不同,机会节点会考虑平均情况。更准确的说,最小化节点仅仅计算子节点的最小效益,而机会节点计算期望效益或期望值。

游戏过程

相比之前的Minimax的Agent,ExpectimaxAgent会以一定的概率去拼一拼,采用Expectimax的Agent,吃豆人会以一定的概率选择往有幽灵的方向走,因为假设往下走可能吃完所有的豆豆而取得更好的分数,所以游戏的失败率就降低了。

代码实现

class ExpectimaxAgent(MultiAgentSearchAgent):
    """
      Your expectimax agent (question 4)
    """

    def getAction(self, gameState):
        """
        Returns the expectimax action using self.depth and self.evaluationFunction

        All ghosts should be modeled as choosing uniformly at random from their
        legal moves.
        """
        "*** YOUR CODE HERE ***"
        return self._getMax(gameState)
        
    def _getMax(self, gameState, depth = 0, agentIndex = 0):
        # 获得吃豆人所有下一步行动
        legalActions = gameState.getLegalActions(agentIndex)
        # 如果到达根节点或者没有可行的行动,则返回评价函数值
        if depth == self.depth or len(legalActions)==0:
            return self.evaluationFunction(gameState)
        # 否则初始化,并对合法的下一步进行遍历
        maxVal = None
        bestAction = None
        for action in legalActions:
            # 从第一个鬼怪开始,进行Expectimax操作
            value = self._getExpectation(gameState.generateSuccessor(agentIndex, action), depth, 1)
            if value is not None and (maxVal == None or value > maxVal):
                maxVal = value
                bestAction = action 
        if depth is 0 and agentIndex is 0:
            return bestAction
        else:
            return maxVal
    
    def _getExpectation(self, gameState, depth = 0, agentIndex = 0):
        legalActions = gameState.getLegalActions(agentIndex)
        # 如果到达根节点,或者没有下一步了,则返回评价函数值
        if depth == self.depth or len(legalActions)==0:
            return self.evaluationFunction(gameState) 
        # 初始化效用值总计
        totalUtil = 0
        numActions = len(legalActions)
        # 轮询当前鬼怪所有可行的下一步
        for action in legalActions:
            # 同样,如果是最后一个鬼怪,那么接下来要去算吃豆人的MAX值
            if agentIndex >= gameState.getNumAgents() - 1:
                totalUtil += self._getMax(gameState.generateSuccessor(agentIndex, action), depth+1, 0)
            # 否则,挨个遍历各个鬼怪,计算Expectation值,并计入效用总计
            else:
                totalUtil += self._getExpectation(gameState.generateSuccessor(agentIndex, action), depth, agentIndex+1)
        # 最后需要把所有可能的下一步的效用值求平均,并返回
        return totalUtil / float(numActions)

结果展示

在这里插入图片描述

Evaluation Function

算法简述

估计函数在深度限制minimax中广泛应用,即将最大可解深度处的非叶子结点都视作叶子节点,然后用仔细挑选的估计函数给其赋虚值。由于估计函数只能用于估计非叶子结点的效益,这使得minimax的最优性不再得到保证。

游戏过程

虽然α-β剪枝能有效增加minimax的搜索深度,但是对大多数游戏来说这离到达搜索树的底部仍然还差得远。故使用估计函数(evaluation functions),这种函数输入状态并输出该节点的minimax估计值。简单直接的解释为:一个好的估计函数能给更好的状态赋更高的值。

代码实现

def betterEvaluationFunction(currentGameState):
    """
    Your extreme ghost-hunting, pellet-nabbing, food-gobbling, unstoppable
    evaluation function (question 5).
    DESCRIPTION: <write something here so we know what you did>
    """
    "*** YOUR CODE HERE ***"
    # 获得计算需要的初始信息,包括吃豆人位置、食物、鬼怪以及鬼怪为惊吓状态的剩余时间
    pacmanPos = currentGameState.getPacmanPosition()
    foods = currentGameState.getFood().asList()
    ghostStates = currentGameState.getGhostStates()
    scaredTime = [ghost.scaredTimer for ghost in ghostStates]
    
    # 先计算最近的食物对吃豆人的影响
    if len(foods)>0:
        Foods = [manhattanDistance(food, pacmanPos) for food in foods]
        # 求最近的豆豆的距离
        nearestFood = min(Foods)
        # 考虑估计函数,要将较好的结果最大化
        foodHeuristic = 0
    else:
        foodHeuristic = 0
        
    # 通过鬼怪和当前pacman的位置计算危险值
    if len(ghostStates)>0:
        # 将所有鬼怪离当前的位置全部计算出来
        Ghosts = [manhattanDistance(ghost.configuration.pos, pacmanPos) for ghost in ghostStates]
        # 求最近的鬼怪的距离,其他的鬼怪可以不计
        nearestGhost = min(Ghosts)
        dangousScore = -1000 if nearestGhost<2 else 0

    # 尽量让鬼怪保持惊吓状态,因为这种状态下的鬼怪可以被吃豆人吃掉
    totalScaredTimes = sum(scaredTime)
    
    # 最后把下一个状态的得分也计入评价值结果
    return  currentGameState.getScore() + foodHeuristic + dangousScore + totalScaredTimes

结果展示

不难看出q5由于采用了Evaluation Function函数,导致最终只能获得估计的非叶子结点的效益,这使得minimax的最优性不再得到保证,导致部分实验Score产生负值。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

Total result

在这里插入图片描述

效果对比&感悟

一、算法效率
不难发现,从算法效率来看,Minimax函数由于本质上是进行全搜索的过程,min和max过程将所有的可能性进行搜索,然后再从端点的估计值倒推计算,这样的效率非常低下,时空复杂度过高。

而在采用α-β剪枝算法后,部分不可能被选择的节点不会再被遍历,使算法的时空复杂度得到了有效的降低。虽然α-β剪枝算法还是一个深度遍历搜索算法,但它对一些非必要的估计值进行舍弃,效率还是得到了很大的提升。

二、算法准确度
我们可以看到,通过Minimax算法得到的平均分数是84分,而通过α-β剪枝算法得到的平均分数也是84分。从而也验证了α-β剪枝算法只是对原本的Minimax算法进行了时空复杂度上的优化和改进,对最终的结果并没有造成影响。

同时,我们可以通过Minimax算法的假设中发现该算法的一些不足,即Minimax算法认为对手一定会做出最优解,但当对手不做出最优行动的情况下,Minimax算法便显得不适用。此时,expectimax算法便显示其独特性,与考虑最坏情况的最小化节点的Minimax算法不同,expectimax算法会考虑平均情况,从而模拟一个更贴近实际情况的过程。

收获

一、算法的更换以及替代并不是随便得出的,是循序渐进的。我们通过发现Minimax算法本质是深度遍历搜索算法,故能够得到Minimax算法的时空复杂度高的结论。据此,采用剪枝的方法优化算法速度。同时,我们通过发现Minimax算法将对手理想化,这样的假设是存在缺陷的,故我们采用expectimax算法来解决该问题。

二、在学习过程中,搜索算法算得上是老朋友了。搜索算法实际上是根据初始条件和扩展规则构造一棵“解答树”并寻找符合目标状态的节点的过程。无论是数据结构课程学到的深度搜索算法,广度搜索算法,哈希函数,在到现在拓展学习的A*,Minimax算法,Alpha-Beta Pruning,Expectimax等算法。不同算法的流程和时间复杂度都各不相同,适用条件也有所不同,需要根据不同的情况选择合适的搜索算法。

三、理清了Minimax算法,Alpha-Beta Pruning,Expectimax,Evaluation Function四类的区别和适用情况。例如当解空间较大时,则Alpha-Beta Pruning较Minimax算法更合适;expectimax较Minimax算法考虑得更全面。

  • 5
    点赞
  • 33
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值