CS188 Project 2: Multi-Agent Search
Question 2 (5 points): Minimax
原理
Minimax算法即极小化极大算法,是一种找出失败的最大可能性中的最小值的算法。Minimax算法常用于棋类等由两方较量的游戏和程序,这类程序由两个游戏者轮流,每次执行一个步骤。Minimax算法是基于搜索的博弈算法的基础。该算法是一种零总和算法,即一方要在可选的选项中选择将其优势最大化的选择,而另一方则选择令对手优势最小化的方法。
Minimax是一种悲观算法,即假设对手每一步都会将我方引入从当前看理论上价值最小的格局方向,即对手具有完美决策能力。因此我方的策略应该是选择那些对方所能达到的让我方最差情况中最好的,也就是让对方在完美决策下所对我造成的损失最小。Minimax不找理论最优解,因为理论最优解往往依赖于对手是否足够愚蠢,Minimax中我方完全掌握主动,如果对方每一步决策都是完美的,则我方可以达到预计的最小损失格局,如果对方没有走出完美决策,则我方可能达到比预计的最悲观情况更好的结局。总之我方就是要在最坏情况中选择最好的。
方法
Minimax算法要定义一个静态估计函数f,对当前局势做出判断,这个函数根据棋局的优劣势态的特征来定义。MAX代表我方,MIN代表对手方,P代表一个状态。对有利于MAX的局势,f§取正值,有利于MIN的局势,f§取负值,局势均衡,f(p)取零。极大极小搜索的基本思想是:
(1)当轮到MIN走步的节点时,MAX应考虑最坏的情况(因此,f§取极小值)。
(2)当轮到MAX走步的节点时,MAX应考虑最好的情况(因此,f§取极大值)。
(3)当评价回溯的时候,相应于两位棋手的对抗策略,不同层上交替地使用(1)、(2)两种方法向上传递倒推值。所以这种搜索方法称为极大极小过程。实际上,这种算法是假定在模拟过程中双方都走出最好的一步,对MAX方来说,MIN方的最好一步是最坏的情况,MAX在不断地最大化自己的利益。
该算法的时间复杂度为O(b^m),空间复杂度为O(b*m),其中m是游戏树的最大深度,在每个节点存在b个有效走法。它是一种简单有效的对抗搜索算法,但如果搜素树很大,则无法在有效时间内返回结果。
代码
对于Pacman游戏,将在multiAgents.py的MinimaxAgent类中编写代码,在该环境中的幽灵是随机行动的,但本算法将从最坏的情况的情况出发考虑,即Pacman可能会去主动接近幽灵。我维护一个数组res,该数组记录了下一层所有节点的评价值,并且遍历了当前操作对象的合法actions,然后通过递归向res里添加下一层的值。
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.getNextState(agentIndex, action):
Returns the child 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 ***"
GhostIndex = [i for i in range(1, gameState.getNumAgents())]
## 对当前幽灵的下一步动作进行遍历,要递归调用以计算其他幽灵的行动
def term(state, d):
return state.isWin() or state.isLose() or d == self.depth
def min_value(state, d, ghost):
if term(state, d):
return self.evaluationFunction(state)
v = 10000000000000000
for action in state.getLegalActions(ghost):
#min_value函数会反复调用获得幽灵的下一步操作
if ghost == GhostIndex[-1]:
v = min(v, max_value(state.getNextState(ghost, action), d + 1))
#如果当前幽灵已经是最后一个,那么应该计算Pacman的行为,调用max_value函数
else:
v = min(v, min_value(state.getNextState(ghost, action), d, ghost + 1))
return v
def max_value(state, d):
if term(state, d):
return self.evaluationFunction(state)
v = -10000000000000000
for action in state.getLegalActions(0):
#获得Pacman下一步所有的合法动作
v = max(v, min_value(state.getNextState(0, action), d, 1))
return v
res = [(action, min_value(gameState.getNextState(0, action), 0, 1)) for action in
gameState.getLegalActions(0)]
res.sort(key=lambda k: k[1])
return res[-1][0]
util.raiseNotDefined()
结果
Question 3 (5 points): Alpha-Beta Pruning
原理
Alpha-Beta剪枝算法是基于Minimax算法的。极大极小搜索策略是在双方对弈若干步之后,从可能的步中选一步相对好的走法来走,在有限的搜索范围内进行求解,可以理解为规定一个有限的搜索深度。但Minimax算法有个致命的弱点,就是非常暴力地搜索导致效率不高,特别是当搜索的深度加大时效率极低,Alpha-Beta剪枝算法在此基础上进行了优化。由于MIN、MAX不断的倒推过程存在着联系,当它们满足某种关系时后续的搜索是多余的,Alpha-Beta剪枝算法把生成后继和倒推值估计结合起来,及时减掉一些无用的分支,以此来提高算法的效率。
方法
定义极大层的下界为alpha,极小层的上界为beta,alpha-beta剪枝规则如下:
(1)alpha剪枝。若任一极小值层结点的beta值不大于它任一前驱极大值层结点的alpha值,即alpha(前驱层) >= beta(后继层),则可终止该极小值层中这个MIN结点以下的搜索过程。这个MIN结点最终的倒推值就确定为这个beta值。
(2)beta剪枝。若任一极大值层结点的alpha值不小于它任一前驱极小值层结点的beta值,即alpha(后继层) >= beta(前驱层),则可以终止该极大值层中这个MAX结点以下的搜索过程,这个MAX结点最终倒推值就确定为这个alpha值。
代码
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 ***"
now_value = -1e10
alpha = -1e10
beta = 1e10
next_PacmanAction = Directions.STOP
legal_actions = gameState.getLegalActions(0).copy()
for next_action in legal_actions:
nextState = gameState.getNextState(0, next_action)
next_value = self.get_node_value(nextState, 0, 1, alpha, beta)
# 只有一个Pacman的情况,直接求其评价值,开始遍历所有幽灵
#如果不是最后一个幽灵,则继续遍历下一个幽灵
if next_value > now_value:
now_value, next_PacmanAction = next_value, next_action
alpha = max(alpha, now_value)
return next_PacmanAction
util.raiseNotDefined()
def get_node_value(self, gameState, cur_depth=0, agent_index=0, alpha=-1e10, beta=1e10):
"""
Using self-defined function, alpha_value(), beta_value() to choose the most appropriate action
Only when it's the final state, can we get the value of each node, using the self.evaluationFunction(gameState)
Otherwise we just get the alpha/beta value we defined here.
"""
max_party = [0, ]
min_party = list(range(1, gameState.getNumAgents()))
# 如果当前是最后一个鬼怪,那么下一轮就是计算Pacman的行为
if cur_depth == self.depth or gameState.isLose() or gameState.isWin():
return self.evaluationFunction(gameState)
# 如果深度超限,或者无法继续展开,则返回当前状态的评价值
# 否则,就继续往下遍历Pacman可能的下一步
elif agent_index in max_party:
return self.alpha_value(gameState, cur_depth, agent_index, alpha, beta)
elif agent_index in min_party:
return self.beta_value(gameState, cur_depth, agent_index, alpha, beta)
else:
print('Errors occur in your party division !!! ')
def alpha_value(self, gameState, cur_depth, agent_index, alpha=-1e10, beta=1e10):
v = -1e10
legal_actions = gameState.getLegalActions(agent_index)
for index, action in enumerate(legal_actions):
next_v = self.get_node_value(gameState.getNextState(agent_index, action),
cur_depth, agent_index + 1, alpha, beta)
v = max(v, next_v)
if v > beta: # 按照α-β剪枝算法,如果v>β,则直接返回v
return v
alpha = max(alpha, v) # 按照α-β剪枝算法,这里还需要更新α的值
return v
def beta_value(self, gameState, cur_depth, agent_index, alpha=-1e10, beta=1e10):
"""
min_party, search for minimums
"""
v = 1e10
legal_actions = gameState.getLegalActions(agent_index)
for index, action in enumerate(legal_actions):
if agent_index == gameState.getNumAgents() - 1:
next_v = self.get_node_value(gameState.getNextState(agent_index, action),
cur_depth + 1, 0, alpha, beta)
v = min(v, next_v) # 开始下一深度
if v < alpha:
return v
else:
next_v = self.get_node_value(gameState.getNextState(agent_index, action),
cur_depth, agent_index + 1, alpha, beta)
v = min(v, next_v) # 在下一深度处开始
if v < alpha:
return v
beta = min(beta, v)
return v
结果
Question 4 (5 points): Expectimax
原理
Minimax存在约束,因为Minimax在对手不一定会做出最优行动的情况下,总是认为它面对的是一个最优的对手,这种情况可能具有随机性,又或者这个对手的行动是随机的。
Minimax是Expectimax的一种特例。Expectimax在游戏树中加入了机会节点,与考虑最坏情况的最小化节点不同,机会节点会考虑平均情况。即最小化节点仅仅计算子节点的最小效用,而机会节点计算期望效用。Expectimax的伪代码与Minimax很相似,就是把最小效益换成了期望效益,这是因为最小化节点被替换成了机会节点。
Expectimax可以得到有关对手在任何状态下如何行动的概率模型,即假定对于任何状态,Expectimax算法都具有对于对手行动的概率分布。最小化节点就是认为值最小的子节点概率为1而其他子节点概率为0的机会节点。
方法
def value(s)
if s is a max node return maxVualue(s)
if s is an exp node return expValue(s)
if s is a terminal node return evaluation(s)
def maxValue(s)
values=[values(s') for s' in successors(s)]
return max(values)
def expValue(s)
values=[value(s') for s' in successor(s)]
weights=[probability(s,s') for s' in successor(s)]
return expectation(values,weights)
代码
Pacman对幽灵下一步如何行动有一个置信度,Pacman使用四层搜索,它的评估函数原来避免被吃,而幽灵使用两层搜索,它的评估函数原来寻找Pacman。
class ExpectimaxAgent(MultiAgentSearchAgent):
"""
Your expectimax agent (question 4)
"""
INF = 100000.0
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 ***"
maxValue = -self.INF
maxAction = Directions.STOP
for action in gameState.getLegalActions(agentIndex=0):
sucState = gameState.getNextState(action=action, agentIndex=0)
sucValue = self.expNode(sucState, currentDepth=0, agentIndex=1)
if sucValue > maxValue:
maxValue = sucValue
maxAction = action
return maxAction
def maxNode(self, gameState, currentDepth):
if currentDepth == self.depth or gameState.isLose() or gameState.isWin():
# 如果达到搜索深度,则返回当前状态的评价值
return self.evaluationFunction(gameState)
maxValue = -self.INF
for action in gameState.getLegalActions(agentIndex=0):
sucState = gameState.getNextState(action=action, agentIndex=0)
sucValue = self.expNode(sucState, currentDepth=currentDepth, agentIndex=1)
if sucValue > maxValue:
maxValue = sucValue
return maxValue
def expNode(self, gameState, currentDepth, agentIndex):
if currentDepth == self.depth or gameState.isLose() or gameState.isWin():
# 如果达到搜索深度,则返回当前状态的评价值
return self.evaluationFunction(gameState)
numAction = len(gameState.getLegalActions(agentIndex=agentIndex))
# 如果接下来Pacman已经没有可行的行动,要终止迭代
totalValue = 0.0
numAgent = gameState.getNumAgents()
for action in gameState.getLegalActions(agentIndex=agentIndex):
sucState = gameState.getNextState(agentIndex=agentIndex, action=action)
if agentIndex == numAgent - 1:
sucValue = self.maxNode(sucState, currentDepth=currentDepth + 1)
else:
sucValue = self.expNode(sucState, currentDepth=currentDepth, agentIndex=agentIndex + 1)
# 接下来的动作是计算幽灵的行动影响
totalValue += sucValue
return totalValue / numAction
# 将totalValue除以所有可行的动作数,求得平均值,并返回
结果
Question 5 (6 points): Evaluation Function
原理
虽然α-β剪枝能有效增加Minimax的搜索深度,但是对大多数游戏来说离到达搜索树的底部仍然差得远。于是,可以使用估计函数,该函数可以输出该节点的Minimax估计值。一个好的估计函数能给更好的状态赋更高的值。估计函数在具有深度限制的Minimax中应用广泛,即将最大可解深度处的非叶子结点都视作叶子节点,然后用估计函数给其赋虚值。由于估计函数只能用于估计非叶子结点的效益,这使得Minimax可能不再最优。
在设计一个用于Minimax的agent时,选择估计函数很重要,好的估计函数能让agent的行动更接近最优策略。此外,在使用估计函数之前访问的层数越深,结果也会更好。使用估计函数的层越接近叶子节点,得到的估值就越接近真实值,最终得到的策略也越接近于最优策略
方法
估计函数的设计不一定是线性函数。而且估计函数会尽可能频繁地为更好的位置给出更高的分数。这需要对各种不同特征和权重的agent进行微调。
代码
def betterEvaluationFunction(currentGameState):
"""
Your extreme ghost-hunting, pellet-nabbing, food-gobbling, unstoppable
evaluation function (question 5).
"""
# 获得计算需要的初始信息,包括Pacman的位置、食物、幽灵以及幽灵为惊吓状态的剩余时间
newPos = currentGameState.getPacmanPosition()
newFood = currentGameState.getFood()
newGhostStates = currentGameState.getGhostStates()
#常量
INF = 100000000.0 # 无穷大值
WEIGHT_FOOD = 10.0 # 食物基数值
WEIGHT_GHOST = -10.0 # 幽灵基数值
WEIGHT_SCARED_GHOST = 100.0 # 惊吓幽灵的基数值
score = currentGameState.getScore()
# 求最近的食物距离
distancesToFoodList = [util.manhattanDistance(newPos, foodPos) for foodPos in newFood.asList()]
if len(distancesToFoodList) > 0: # 先计算最近的食物对Pacman的影响
score += WEIGHT_FOOD / min(distancesToFoodList)
else:
score += WEIGHT_FOOD
# 求幽灵的距离
for ghost in newGhostStates:
distance = manhattanDistance(newPos, ghost.getPosition())
if distance > 0:
if ghost.scaredTimer > 0: # 如果幽灵处于惊吓状态,加分
score += WEIGHT_SCARED_GHOST / distance
else: # 如果幽灵不处于惊吓状态,减分
score += WEIGHT_GHOST / distance
else:
return -INF # Pacman死了
return score
# Abbreviation
better = betterEvaluationFunction
结果
数据及效果对比
Minimax
Alpha-Beta Pruning
Expectimax
对于以上三种算法,相比于Minimax,Expectimax会以一定的概率去拼一拼,当幽灵靠近Pacman时,Minimax会认为Pacman会被吃掉,所以快速自杀以取得高分;但是如果采用Expectimax,这时候Pacman会以一定的概率选择继续行动,因为继续行动可能吃完所有的食物而取得更好的分数,所以游戏的失败率就降低了,因此Expectimax的成功率会比Minimax高。
而对于Alpha-Beta剪枝算法来说,是一种找到最佳Minimax步骤的算法,同时可以避免搜索不可能被选择的步骤的子树,搜索过程中会传递两个边界参数,这些边界基于已经看到的搜索树部分来限制候选步骤,降低了Minimax算法的时间复杂度,因此Alpha-Beta剪枝算法提高了Minimax算法的效率。
收获
在本周,我们从之前的简单搜索问题,转移到考虑对抗搜索问题。在课堂上学习到了三种基础算法:
Minimax:当对手采取最优行动时使用,可以通过α-β剪枝算法对其进行优化。相比Expectimax,Minimax采取的行动更加保守,所以在不知道对手会如何行动的情况下,Minimax会倾向于放弃原本的最优结果。它的执行与DFS相似,而且时间复杂度也很大,达到了 O(b^m) 。
Alpha-Beta Pruning:为了降低Minimax算法的时间复杂度,提出了Alpha-Beta 剪枝算法,它是Minimax算法的优化算法,它们产生的结果是完全相同的,只不过运行效率不一样。这种方法的前提假设与Minimax也是一样的:
(1)双方都按自己认为的最佳方法行动。
(2)对给定的局势用一个分值来评估,这个评估值永远是从一方来评价的,己方有利时给一个正数,对方有利时给一个负数。
(3)从Max来看,分值大的数表示对己方有利,而对于对方Min来说,它会选择分值小的行动方法。
Expectimax:当对手采取次优行动时使用,通过对手行动的概率分布来计算各个状态的期望值。大多数情况下,在游戏树中将以上算法一路跑到底会耗费过多的计算资源,于是引入估计函数来提高算法效率。考虑了如何定义agent的效用函数从而做出理性决策。通过选择合适的函数,能进一步让agent寻求风险、规避风险和风险中立。
在本次实习中,着重训练了这三种算法的实践,通过Pacman游戏来对这三种算法进行分析比较。巩固了课堂上学习的对抗搜索算法,加强了对对抗搜索算法的应用能力,同时也认真学习吸取了别人优秀的题解方法,锻炼了我的算法思维,巩固了以往所学的知识,查漏补缺,夯实基础,同时也学习了许多新的、高效的、实用的知识,提高了算法设计能力。通过这次实习,我更加深刻地掌握了Minimax、Alpha-Beta Pruning、Expectimax这三种经典算法,也能够较为熟练地将其运用到应用中去。