基于博弈树的开源五子棋AI教程[7 多线程搜索]

文章目录

引子

多线程加快搜索速度这一认知是经受住实践考验的。博弈树搜索的并行搜索方式有很多种,例如叶子并行,根并行,树分裂等算法。笔者给出一种实现起来比较简单的根并行算法。

在是实现时需要注意两点,第一,怎么安全的剪枝;第二,如何进行线程间的通信。对于AB剪枝有三点发现可以指导我们设计多线程的并行算法:

  1. 当某一节点搜索完成,其分数才能安全的更新父亲节点的AB值。
  2. 一个节点的AB值可以安全的更新其所有子孙节点的AB值。
  3. 如果一个节点alpha >= beta, 这个节点可以安全的被剪枝

这样一来,就可以知道一个节点搜索完成后,如何更新博弈树所有节点的AB值,如何剪枝。通信方式使用的全局变量+读写锁控制的,全局变量保存所有节点状态的AB值。当搜索开始,从根节点沿着搜索路径开始更新沿路的所有节点AB值,然后从全局变量中读取该节点的AB值。搜索完成后,更新父亲节点AB值。

定义

clike 复制代码
struct parallelNABSearchNode{
    int alpha, beta;
    parallelNABSearchNode() : alpha(-INT_MAX), beta(INT_MAX){}
    parallelNABSearchNode(int aalpha, int abeta) : alpha(aalpha), beta(abeta){}
    QString str();
    //返回值:true已经更新,false表示没更新
    bool getAlphaBeta(int &aalpha, int &abeta);

    bool updateLeaf2RootAlphaBeta(int score);

    //返回值:true已经更新,false表示没更新
    bool updateRoot2LeafAlphaBeta(int aalpha, int abeta);
};
clike 复制代码
    //并行化搜索技术
    static QReadWriteLock parallelSearchTableLock;
    static QHash<quint64, parallelNABSearchNode> parallelSearchTable;

函数实现三个方法,一个getAlphaBeta(int &aalpha, int &abeta)是从全局变量中获取AB值,一个updateLeaf2RootAlphaBeta是从更新该节点的父亲的AB值,还有一个updateRoot2LeafAlphaBeta是更新儿子节点的AB值。

clike 复制代码
bool parallelNABSearchNode::getAlphaBeta(int &aalpha, int &abeta){
    if(!globalParam::utilGameSetting.IsOpenParallelSearch) return false;
    if(aalpha >= alpha && beta >= abeta) return false;
    if(aalpha < alpha){
        aalpha = alpha;
    }
    if(beta < abeta){
        abeta = beta;
    }
    return true;
}
clike 复制代码
bool parallelNABSearchNode::updateLeaf2RootAlphaBeta(int score){
    if(!globalParam::utilGameSetting.IsOpenParallelSearch) return false;
    if(score > alpha){
        alpha = score;
        return true;
    }
    return false;
}
clike 复制代码
bool parallelNABSearchNode::updateRoot2LeafAlphaBeta(int aalpha, int abeta){
    if(!globalParam::utilGameSetting.IsOpenParallelSearch) return false;
    if(alpha >= aalpha && abeta >= beta) return false;
    if(alpha < aalpha){
        alpha = aalpha;
    }
    if(abeta < beta){
        beta = abeta;
    }
    return true;
}

实现

现在已经实现了线程间通信的工具,只需要在搜索时调用这些利器就可以了,总体的实现思路和常规负极大搜索如出一撤。为了能后续兼容树分裂的算法,这里给出了并行化搜索指定深度的接口。

clike 复制代码
//fail-soft negMax Alpha-Beta pruning search
int GameAI::NABParallelSearch(int depth, int alpha, int beta, bool maximizingPlayer, quint8 searchSpaceType)
{
    int score = -INT_MAX;
    QWriteLocker writeLock(&globalParam::parallelSearchTableLock);
    // 更新根节点->当前节点搜索路径上AB值
    for(int pid = 0;pid < parallelSsearchHistoryPlayersHash.size() - 1; ++pid){
        //表项不存在会自动调用默认构造函数
        parallelNABSearchNode *curNode = &globalParam::parallelSearchTable[parallelSsearchHistoryPlayersHash[pid]];
        parallelNABSearchNode *sontNode = &globalParam::parallelSearchTable[parallelSsearchHistoryPlayersHash[pid + 1]];
        //更新下一层的AB值
        sontNode->updateRoot2LeafAlphaBeta(- curNode->beta, - curNode->alpha);
    }
    // 获取当前AB值
    globalParam::parallelSearchTable[zobristSearchHash.hash()].getAlphaBeta(alpha, beta);
//    // 更新AB值后可能引发剪枝
//    if(alpha >= beta){   // AB剪枝
//        aiCalInfo.cutTreeTimesCurrentTurn ++;
//        return beta;
//    }
    writeLock.unlock();

    //探查置换表中值
    if(zobristSearchHash.getNABTranspositionTable(score, depth, alpha, beta)) {
        return score;
    }

    // ??或 分数过大过小
//    if (qAbs(score) > globalParam::utilGameSetting.MaxScore){
//        //保存置换表
//        return score;
//    }

    int evalPlayer = globalParam::AIPlayer;
    MPlayerType searchPlayer = maximizingPlayer ? evalPlayer : UtilReservePlayer(evalPlayer);

    // 达到搜索深度
    if (depth == 0 || checkSearchBoardWiner() != PLAYER_NONE){
        //保存置换表
        score = evaluateBoard(evalPlayer);//负极大搜索中评估必须searchPlayer
        if(!maximizingPlayer) score *= -1;

//        //VCF
//        QList<MPoint> vcf, vcfpath;
//        if(VCXSearch(globalParam::utilGameSetting.MaxVctSearchDepth, maximizingPlayer, VCT_SEARCH, vcf, vcfpath)){
//            qDebug() << "NABsearch : find vct";
//            if(maximizingPlayer) return globalParam::utilGameSetting.MaxScore;
//            else return -globalParam::utilGameSetting.MaxScore;
//        }
        return score;
    }

    // 着法生成
    QVector<MPoint> searchPoints;
    getSortedSearchSpace(searchPoints, evalPlayer, searchPlayer, searchSpaceType);

    int scoreBest = -INT_MAX;
    int hashf = hashfUperBound;
    MPoint moveBest(InvalidMPoint);
    quint16 savedSearchBoardPatternDirection[boardSize][boardSize];

    for (const auto &curPoint : searchPoints) {
        if (!searchBoardHasPiece(curPoint)) {
            setSearchBoard(curPoint, searchPlayer, savedSearchBoardPatternDirection);// searchPlayer落子
            score = -NABParallelSearch(depth - 1, -beta, -alpha, !maximizingPlayer,searchSpaceType);
            setSearchBoard(curPoint, PLAYER_NONE, savedSearchBoardPatternDirection);// 撤销落子

            if (score > scoreBest) {
                scoreBest = score;
                moveBest = curPoint;
                if (score >= beta) {
                    hashf = hashfLowerBound;
                    appendSearchKillerTable(curPoint, depth, hashf);
                    aiCalInfo.cutTreeTimesCurrentTurn ++;
                    break; // Alpha-Beta 剪枝
                }
                if (score > alpha) {
                    alpha = score;
                    hashf = hashfExact;

                    //更新当前层的AB值
                    writeLock.relock();
                    parallelNABSearchNode *curNode = &globalParam::parallelSearchTable[zobristSearchHash.hash()];
                    curNode->alpha = scoreBest;
                    writeLock.unlock();
                }
            }
        }
    }

//    writeLock.relock();
//    //更新当前层的AB值
//    parallelNABSearchNode *curNode = &globalParam::parallelSearchTable[zobristSearchHash.hash()];
//    curNode->alpha = scoreBest;
//    writeLock.unlock();
    writeLock.relock();
    //更新上一层的AB值:只有当前所有节点搜索完成后,得到的值才是可靠的,才能用来更新父亲节点的AB值
    if(parallelSsearchHistoryPlayersHash.size() >= 2){
        const quint64& fatherHash = parallelSsearchHistoryPlayersHash[parallelSsearchHistoryPlayersHash.size()-2];
        parallelNABSearchNode *fatherNode = &globalParam::parallelSearchTable[fatherHash];
        fatherNode->updateLeaf2RootAlphaBeta(-scoreBest);
    }
    writeLock.unlock();

    //更新历史表
    appendSearchHistoryTable(moveBest, depth, hashf);
    // 更新置换表
    zobristSearchHash.appendNABTranspositionTable(depth, scoreBest, hashf, moveBest, UtilReservePlayer(searchPlayer));

    return scoreBest;
}

结果

这里实现的并行化搜索效果并不出众,只能说是有一定效果。在深度为6搜索情况下,线程数为4的并行化搜索能加速2~3倍。这一点也是可以理解的,因为负极大搜索的节点如果排序较好,搜索量主要集中在PV路径的搜索上。简单的分裂根节点能提升的速度是可预见,只有动态的分裂树,把算力平摊到PV路径搜索,加速PV路径产生能提高博弈树搜索的瓶颈。

尾记

这里实现并行化搜索还存在一些值得思考的问题,如何能提高搜索的稳定性,在发生截断返回时,仍能正确的搜索到PV路径,而不是会因为提前的不安全的剪枝与PV路径失之交臂。后面也希望有时间继续研究下如何高效的分裂树,而不是盲目的根分裂。

相关推荐
2601_957780841 天前
Claude 4.6 对阵 GPT-5.4:2026 开发者大模型 API 选型深度解析
人工智能·python·gpt·ai·claude
2601_957780841 天前
GPT-5.5 深度解析:2026年4月OpenAI旗舰模型的技术跨越与商业决策指南
大数据·人工智能·python·gpt·openai
zhangfeng11331 天前
利用WorkBuddy 国产小龙虾 制作视频 1 Remotion 方案 2 备选:moviepy 方案渲染视频
人工智能
冬奇Lab1 天前
RAG 系列(十四):Self-RAG——让模型决定要不要检索
人工智能·llm
chatexcel1 天前
AI工具里的知识库是什么?定义、原理、场景与ChatExcel示例解析
人工智能
冬奇Lab1 天前
一天一个开源项目(第99篇):AiToEarn - 用 AI 把内容变成收入的一站式平台
人工智能·开源·资讯
千叶风行1 天前
Text-to-SQL 技术设计与注意事项
前端·人工智能·后端
夜郎king1 天前
Spring AI 对接大模型开发易错点总结与实战解决办法
java·人工智能·spring
从孑开始1 天前
manyspeech-cli 语音识别命令行工具
人工智能·语音识别·工具·asr
hans汉斯1 天前
计算机科学与应用|基于大模型深度语义理解的智能内容纠错系统
人工智能·计算机视觉·视觉检测·数据·病虫害检测