[人工智能] alpha-beta剪枝算法及实践

alpha-beta剪枝算法及实践

  • 算法原理
  • 算法伪码
  • 中国象棋AI实践

算法原理

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

为此要定义一个静态估计函数f,以便对棋局的势态做出优劣的估计,这个函数可根据棋局的优劣势态的特征来定义。这里规定,MAX代表程序方,MIN代表对手方,P代表一个棋局(即一个状态)。有利于MAX的势态,f(p)取正值,有利于MIN的势态,f(p)去负值,势态均衡,f(p)取零。极大极小搜索的基本思想是:
(1)当轮到MIN走步的节点时,MAX应考虑最坏的情况(因此,f(p)取极小值)。
(2)当轮到MAX走步的节点时,MAX应考虑最好的情况(因此,f(p)取极大值)。
(3)当评价往回倒退的时候,相应于两位棋手的对抗策略,不同层上交替地使用(1)、(2)两种方法向上传递倒推值。所以这种搜索方法称为极大极小过程。实际上,这种算法是假定在模拟过程中双方都走出最好的一步,对MAX方来说,MIN方的最好一步是最坏的情况,MAX在不断地最大化自己的利益。

极大极小搜索策略在一些棋盘AI中非常常见,但是它有个致命的弱点,就是非常暴力地搜索导致效率不高,特别是当讲搜索的深度加大时会有明显的延迟,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值。

算法伪码

先看极大极小搜索算法:

//node记录当前player,depth记录搜索深度
function minimax(node, depth) 
   // 如果能得到确定的结果或者深度为零,使用评估函数返回局面得分
   if node is a terminal node or depth = 0
       return the heuristic value of node
   // 如果轮到对手走棋,是极小节点,选择一个得分最小的走法
   if the adversary is to play at node
       let α := +∞
       for each child of node
           α := min(α, minimax(child, depth-1))
   // 如果轮到我们走棋,是极大节点,选择一个得分最大的走法
   else {we are to play at node}
       let α := -∞
       foreach child of node
           α := max(α, minimax(child, depth-1))
   return α;

alpha-beta剪枝就在极大极小搜索算法上优化:

function alphabeta(node, depth, α, β, Player)
    //达到最深搜索深度或胜负已分         
    if  depth = 0 or node is a terminal node
        return the heuristic value of node
    if  Player = MaxPlayer // 极大节点
        for each child of node // 子节点是极小节点
            α := max(α, alphabeta(child, depth-1, α, β, not(Player) ))   
            if β ≤ α 
            // 该极大节点的值>=α>=β,该极大节点后面的搜索到的值肯定会大于β,因此不会被其上层的极小节点所选用了。对于根节点,β为正无穷
                 break //beta剪枝                        
        return α
    else // 极小节点
        for each child of node //子节点是极大节点
            β := min(β, alphabeta(child, depth-1, α, β, not(Player) )) // 极小节点
            if β ≤ α // 该极大节点的值<=β<=α,该极小节点后面的搜索到的值肯定会小于α,因此不会被其上层的极大节点所选用了。对于根节点,α为负无穷
                break //alpha剪枝
        return β 

可以看到alpha-beta剪枝每次跟踪两个变量alpha和beta,对于MAX方,beta是父节点MIN的一个上界,当前搜索到alpha父节点的上界时,没有必要继续搜索了,因为已经达到了父节点的上界;对于MIN方,alpha是父节点MAX的一个下界,当前搜索到alpha父节点的下界时,没有必要继续搜索了,因为已经达到了父节点的下界,最搜索下去只是徒劳。

中国象棋AI实践

alpha-beta剪枝算法仅仅是AI的核心,做一个中国象棋AI还涉及到其他很多的方面的准备。其中最为重要的就是静态局面评估函数,它决定了象棋AI的聪明程度,通常来说是根据人的经验来进行的评估的,不同的人有不同的版本。此外还有涉及到中国象棋的诸多规则,因此细节方面的事情不少。

状态表示

struct states{//一个走棋动作
    pair<int,int>from;//源头
    pair<int,int>to;//目的
    bool player;
    int source;//源头棋子
    int target;//目的棋子
    states(){
        from = to = pair<int,int>(0,0);
        player = false;
        source = target = 0;
    }
    states(int fx,int fy,int tx,int ty,int tar){
        from = pair<int,int>(fx,fy);
        to = pair<int,int>(tx,ty);
        target = tar;
        player = source = 0;
    }
};

struct ChessInfo{//存储每个中国象棋状态
    pair<int,int>index;//位置
    int value;
    bool alive;
    int type;
};

象棋类

class ChessBoard{
private:
    int fx,fy,tx,ty;//移棋的源和目标
    bool player;//当前玩家在哪一方
    bool isBegin;
    stack<states>chessRecord;//移棋记录,用于悔棋
    pair<int,int>redGen;//红将位置
    pair<int,int>blackGen;//黑将位置
    std::map<int,ChessInfo>chesses;//32 chesses
    int search_depth;
    states aiAction;

public:
    ChessBoard();
    ~ChessBoard();
    void AiTestFunc();
    bool CouldMove(int record[10][9],int fy,int fx,int ty,int tx);//合法走棋判断
    //could move or not
    bool CouldGeneral(int record[10][9],int &fy,int &fx,int &ty,int &tx);//帅 or 将
    bool CouldChariot(int record[10][9],int &fy,int &fx,int &ty,int &tx);//车
    bool CouldHorse(int record[10][9],int &fy,int &fx,int &ty,int &tx);//马
    bool CouldCannon(int record[10][9],int &fy,int &fx,int &ty,int &tx);//炮
    bool CouldAdvisor(int record[10][9],int &fy,int &fx,int &ty,int &tx);//士
    bool CouldMinister(int record[10][9],int &fy,int &fx,int &ty,int &tx);//相、象
    bool CouldSoldider(int record[10][9],int &fy,int &fx,int &ty,int &tx);//兵卒

    bool WinJudgement(int record[10][9],pair<int,int>redGeneral,pair<int,int>blackGeneral);//胜负判断

    //Ai part
    int ValueEstimation(map<int,ChessInfo>& target,bool red);//静态局面评估
    //AI核心算法
    int AlphaBeta(int record[10][9],map<int,ChessInfo>target,int depth,int alpha,int beta);
    //获取所有可能的走法
    bool MakeNextMove(int record[10][9],vector<states>& moves,ChessInfo &obj,bool red);
    //悔棋,用于回溯
    bool UnMakeMove(int record[10][9],map<int,ChessInfo>& target,states& move);
    //移动一步
    void MoveIt(int record[10][9],map<int,ChessInfo>& target,states &obj);
};

核心函数

int ChessBoard::AlphaBeta(int record[10][9],map<int, ChessInfo> target,
                          int depth, int alpha, int beta)
{
    if(!depth){//到达搜索深度
        if(WinJudgement(record,target[17].index,target[1].index)){//胜负已出
            cout << "win!" << endl;
            return MATE;
        }
        else return ValueEstimation(target,!search_depth%2);//返回局面评估
    }
    int value = 0;
    int x = (depth%2)?1:17;//根据深度判断当前玩家
    int up = x + 16;
    for(;x < up;x ++){//对于当前方的每一个棋子,共16个棋子
        if(!target[x].alive)continue;//若棋子已死,跳过
        vector<states> moves;//记录当前棋子的走法
        MakeNextMove(record,moves,target[x],x>=17);//获取当前棋子的所有走法
        if(moves.empty())continue;//若为空跳过
        for(int y = 0;y < moves.size();y ++){//对于每一步走法
            MoveIt(record,target,moves[y]);//走这步棋子
            value = -AlphaBeta(record,target,depth-1,-beta,-alpha);//递归调用,获取这步走法的局面评估
            UnMakeMove(record,target,moves[y]);//回溯这步棋子
            if(search_depth == depth && value > alpha){
                aiAction = moves[y];//若此时时最顶层,则记录最佳走法,贪心策略
            }
            alpha = (value > alpha)?value:alpha;//极大搜索
            if(alpha >= beta)return alpha;//剪枝
        }
    }
    return alpha;
}

局面评估

评估用简单的棋力相加(摊手0.0)

int ChessBoard::ValueEstimation(map<int,ChessInfo>& target,bool red)
{
    int x = (red)?17:1;
    int up = x + 16;
    int ret = 0;
    for(;x < up;x ++){
        if(target[x].alive){
            int index = (red)?(INDEX(target[x].index.first,CHESSBOARD_ROW-1-target[x].index.second)):
                              (INDEX(target[x].index.first,target[x].index.second));
            ret += CHESSMAN_VALUE[target[x].type-1][index];
        }
    }
    return ret;
}

最终做出来,还是感觉AI水平太弱了(逃),评估函数不行,中国象棋千变万化,高水平AI哪有那么容易。

这里写图片描述

这里写图片描述

参考资料:《人工智能基础教程(第二版》作者:朱福喜

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页