国际象棋AI与alpha-beta算法

我已经为我的国际象棋游戏实现了alpha-beta算法,但它需要很长时间(4步需要几分钟)才能做出一个相当愚蠢的移动。

我已经花了两天时间试图找出错误(我认为我犯了一个错误),我非常希望能得到一些关于我的代码的外部意见。

getMove函数:在根节点被调用,它会为所有子节点(可能的移动)调用alphaBeta函数,然后选择得分最高的移动。

Move AIPlayer::getMove(Board b, MoveGenerator& gen){    // 定义常量: ALPHA=-20000 和 BETA= 20000    int alpha = ALPHA;     Board bTemp(false); // 测试棋盘    Move BestMov;    int i = -1; int temp;    int len = gen.moves.getLength();  // moves是一个保存所有合法移动的链表    BoardCounter++; // AIPlayer对象的私有属性,用于统计分析的棋盘数    Move mTemp;     // mTemp用于将列表中的下一个移动应用到临时测试棋盘上    gen.mouvements.Begin();   // 将列表计数器设置到列表的第一个元素    while (++i < len && alpha < BETA){        mTemp = gen.moves.nextElement();        bTemp.cloneBoard(b);        bTemp.applyMove(mTemp);        temp = MAX(alpha, alphaBeta(bTemp, alpha, BETA, depth, MIN_NODE));        if (temp > alpha){            alpha = temp;            BestMov = mTemp;        }    }    return BestMov;}

alphaBeta函数:

int AIPlayer::alphaBeta(Board b, int alpha, int beta, char depth, bool nodeType){    Move m;    b.changeSide();    compteurBoards++;    MoveGenerator genMoves(b); // 当构造函数给定一个棋盘时,它会自动生成可能的移动    // 棋盘对象有一个玩家属性,保存当前玩家    if (genMoves.checkMate(b, b.getSide(), moves)){ // 如果当前玩家被将死        return 100000;    }    else if (genMoves.checkMate(b, ((b.getSide() == BLACK) ? BLACK : WHITE), moves)){ // 如果另一方被将死        return -100000;    }    else if (!depth){        return b.evaluateBoard(nodeType);    }    else{        int scoreMove = alpha;        int best;        genMoves.moves.Begin();        short i = -1, len = genMoves.moves.getLength();        Board bTemp(false);        if (nodeType == MAX_NODE){            best = ALPHA;            while (++i < len){                bTemp.cloneBoard(b);                if (bTemp.applyMove(genMoves.moves.nextElement())){                     scoreMove = alphaBeta(bTemp, alpha, beta, depth - 1, !nodeType);                    best = MAX(best, scoreMove);                    alpha = MAX(alpha, best);                    if (beta <= alpha){                         std::cout << "max cutoff" << std::endl;                        break;                    }                }            }            return scoreMove;        //return alpha;        }        else{            best = BETA;            while (++i < len){                bTemp.cloneBoard(b);                if (bTemp.applyMove(genMoves.moves.nextElement())){                     scoreMove = alphaBeta(bTemp, alpha, beta, depth - 1, !nodeType);                    best = MIN(best, scoreMove);                    beta = MIN(beta, best);                    if (beta <= alpha){                         std::cout << "min cutoff" << std::endl;                        break;                    }                }            }            return scoreMove;            //return beta;        }        return meilleur;    }}

编辑:我应该指出,evaluateBoard函数只评估棋子的移动性(可能的移动数量,捕获移动会根据被捕获的棋子得到更高的分数)

谢谢你。


回答:

我可以看出你正在尝试实现一个极小化-极大化算法。然而,代码中有一些让我怀疑的地方。我们将与开源的Stockfish国际象棋引擎进行比较。请参考https://github.com/mcostalba/Stockfish/blob/master/src/search.cpp中的搜索算法

1. 按值传递Board b

你的代码中有这样的内容:

alphaBeta(Board b, int alpha, int beta, char depth, bool nodeType)

我不知道“Board”具体是什么。但这在我看来不太对。我们来看Stockfish:

Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode)

在Stockfish中,位置对象是按引用传递的。如果“Board”是一个类,每次调用alpha-beta函数时,程序都需要创建一个新的副本。在国际象棋中,当我们需要评估大量节点时,这显然是不可接受的。

2. 没有哈希

在Stockfish中,哈希是这样的:

ttValue = ttHit ? value_from_tt(tte->value(), ss->ply) : VALUE_NONE;

没有哈希,你将需要一次又一次地评估同一个位置。没有实现哈希,你将寸步难行。

3. 检查将死

这可能不是最显著的减速原因,但我们永远不应该在每个节点都检查将死。在Stockfish中:

// 所有合法移动都已被搜索。一个特殊情况:如果我们处于被将死的状态// 并且没有找到合法移动,这就是将死。如果(InCheck && bestValue == -VALUE_INFINITE)    return mated_in(ss->ply); // 从根节点到将死的步数

这是在所有可能的移动都被搜索后完成的。我们这样做是因为通常非将死节点比将死节点多得多。

4. Board bTemp(false);

这看起来是一个主要的减速原因。我们来看Stockfish:

  // 第14步。执行移动  pos.do_move(move, st, ci, givesCheck);

你不应该在每个节点都创建一个临时对象(创建bTemp的对象)。机器需要分配一些堆栈空间来保存bTemp。如果bTemp不是主要变量(即,不太可能被处理器缓存),这可能会是一个严重的性能惩罚。Stockfish只是修改内部数据结构,而不创建新的数据结构。

5. bTemp.cloneBoard(b);

与4类似,甚至更糟,这是为节点中的每个移动执行的。

6. std::cout << “max cutoff” << std::endl;

可能难以置信,向终端打印比处理要慢得多。这里你可能会造成一个潜在的减速,即字符串需要被保存到IO缓冲区。该函数可能会(我不确定100%)甚至会阻塞你的程序,直到文本显示在终端上。Stockfish只在统计摘要时这样做,绝对不是每次你有高失败或低失败时都这样做。

7. 没有排序PV移动

在解决其他问题之前,可能不是你想做的事情。在Stockfish中,他们有:

std::stable_sort(RootMoves.begin() + PVIdx, RootMoves.end());

这是在迭代加深框架中的每次迭代中完成的。

Related Posts

L1-L2正则化的不同系数

我想对网络的权重同时应用L1和L2正则化。然而,我找不…

使用scikit-learn的无监督方法将列表分类成不同组别,有没有办法?

我有一系列实例,每个实例都有一份列表,代表它所遵循的不…

f1_score metric in lightgbm

我想使用自定义指标f1_score来训练一个lgb模型…

通过相关系数矩阵进行特征选择

我在测试不同的算法时,如逻辑回归、高斯朴素贝叶斯、随机…

可以将机器学习库用于流式输入和输出吗?

已关闭。此问题需要更加聚焦。目前不接受回答。 想要改进…

在TensorFlow中,queue.dequeue_up_to()方法的用途是什么?

我对这个方法感到非常困惑,特别是当我发现这个令人费解的…

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注