五子棋AI优化:α-β剪枝

·  阅读 975

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第7篇文章,点击查看活动详情

本文首发于我的个人博客:xeblog.cn/articles/76

上篇:五子棋AI进阶:极大极小值搜索

前言

回顾上文,我们介绍了 极大极小值搜索 算法的实现原理,使得我们的 AI 可以进行更深层次的思考,棋力突飞猛进。但此时,我们又发现了一个新的问题,由于 博弈树 的分支太多,计算量太过庞大,从而使得 AI 的响应时间变长,原本思考一步的 AI 在毫秒之间就能计算出最佳落子点,现在思考两步的 AI 需要花费好几秒的时间才能找出最佳走步。棋力虽然是提升了很多,但面对这么长的耗时,我们能否做一些优化呢?

有些人可能就会说了,你特么标题不都写了吗?还问我?

所以,本文将介绍一种优化算法:α-β剪枝

Alpha-beta剪枝是一种搜索算法,用以减少极小化极大算法(Minimax算法)搜索树的节点数。这是一种对抗性搜索算法,主要应用于机器游玩的二人游戏(如井字棋、象棋、围棋)。当算法评估出某策略的后续走法比之前策略的还差时,就会停止计算该策略的后续发展。该算法和极小化极大算法所得结论相同,但剪去了不影响最终决定的分枝。—— 百度百科

α-β剪枝

算法实现原理

这个算法的实现原理并不难,甚至还特别简单。在讲解算法之前,我们先搞清楚一个问题:下图中,博弈树 A1 分支的深度优先遍历(后序遍历)的过程是怎样的?

image.png

只要搞清楚这个问题,后面讲解算法的时候就不会被弄糊涂啦。

image.png

上图描绘出了 A1 访问左子树 B1 的过程。标了数字的节点表示的是处理的顺序,优先处理最深层的节点,然后将深层节点的处理结果返回给上一层的节点,如果这一层还有节点,则继续处理,直至所有节点都处理完毕。

我们回顾一下之前 A1 分支 极大极小值搜索 的过程:

image.png

B5 ~ B12 节点是 AI和对手交替落子第四步后所形成的局面,局面对应的评分都标在了节点的下方。我们都知道,B5 ~ B12 的分支都是 Min 分支,Min 分支会将得分最低的节点返回给上层的 Max 分支,所以 A3 ~ A6 分支的得分都是对手给的最低分。接下来,轮到 A3 ~ A6 分支做选择,Max 分支会将得分最高的节点返回给上层的 Min 分支,也就是说 B1 ~ B2 分支的得分是从 A3 ~ A6 分支里挑选出来的最高分,然后 B1 ~ B2 分支又会将最小的得分返回给 A1

极大极小值搜索 的过程中,其实有些分支是可以不用访问的,比如上图中 A4 不用访问 B8 分支,B2 不用访问 A6 分支,为什么呢?

我们下面利用 α-β剪枝 算法来解释为什么有些节点不用访问。

定义变量:Max 分支挑选出来的最高分记为 αMin 分支挑选出来的最低分记为 β。初始时, α 值为无穷小 -∞β 值为无穷大 +∞

A1 分支开始,将 αβ 变量向下传递,如图所示

image.png

我们先从 Max 分支 A3 看起,A3 下一步会访问 Min 分支的 B5 节点,并且将 αβ 变量向下传递到 B5,如图所示

image.png

此时,B5 计算出了局面评分为 4,将这个评分和 β 进行比较,如果评分 < β 就更新 β 的值为当前评分。因为 4 < +∞ ,所以当前的 β 值被更新为了 4,如图所示

image.png

继续访问 B6 节点。此时,B6 节点评分为 2,与当前 β 值进行比较, 因为 2 < 4 满足条件,所以更新 β 值为 2,如图所示

image.png

B5 ~ B6 节点都访问完毕后,要将当前的 β 的值返回给上一层的 Max 分支 A3A3 此时需要做一步操作:如果 β > α,则更新 α 的值为 β,如图所示

image.png

此时,A3分支的 α 值为 2β 值为 +∞A3 分支结束,进入到 A4 分支,并将 αβ 变量传递,如图所示

image.png

A4 下一步会访问 Min 分支的 B7 节点,并且将 αβ 变量向下传递到 B7,如图所示

image.png

此时,B7 局面评分为 1,因为 1 < +∞,所以当前 β 更新为 1。此时,α 值为 2β 值为 1

image.png

这时候,我们要做一步操作:如果 α >= β 则结束当前层次的遍历。这就叫做 剪枝

image.png

因为 α(2) >= β(1) ,所以我们可以跳过B8 节点的访问。

这是为什么呢?

原因很简单,因为 α 储存着当前最大分值,β 储存着当前最小分值,在 Min 分支的时候,一直都在搜索小于 β 值的节点,找到之后会更新 β 值,并返回给 Max 分支,如果 α 此时的值就比 β 要大,那你返回给我的 β 值肯定也是会小于我当前的 α 值的,你给我我也不会要,因为我要的是比 α 大的值才对。这里说的可能会有点难以理解,我们可以这么理解:

抢劫犯已经从张三的口袋中拿到了1000元现金,张三此时却想要拿《Java从入门到入土》这本破书给他换,你说抢劫犯他能同意吗?

就是这个意思,我手中已经有好东西了,你却想拿一个不值钱的东西给我换?我肯定是不答应的。B5 ~ B6 分支给到 A3 的分数为 2B7 ~ B8 分支给到 A4 的分数为 1A4 的分数小于 A3,已经没有选择他的必要了,这个结果在 B7 节点的时候就已经能确定了,所以 B7 之后的节点都不用再去访问了,我们可以剪掉那些不用访问的分支。最后 A4 节点将当前的 α 值返回给了 B1 分支,如图所示

image.png

我们再来看 B2 分支,B2 分支将当前的 αβ 值向下传递到 B9,如图所示

image.png

B9 评分为 4,由于 4 > 2,不满足 β 的更新条件,接着访问 B10,同样也不满足更新条件,最后 β 值还是 2,将该值返回给 A5,因为 2 > -∞,所以需要将当前的 α 值更新为 2,此时 α(2) >= β(2) 条件成立,又可以跳过后面的节点了,A6 分支可以剪掉。

image.png

最后,A5 将当前的 α 返回给 B2B2 将当前的 β 返回给 A1,所以A1 最终的得分为 2

image.png

有一个地方大家可能还会有疑问:为什么 Min 分支返回给 Max 分支 β 时,Max 分支不是更新 β 而是更新 α

回看一下,我们上文对 αβ 的定义:

Max 分支挑选出来的最高分记为 αMin 分支挑选出来的最低分记为 β。初始时, α 值为无穷小 -∞β 值为无穷大 +∞

Min 分支返回给 Max 分支的最小分值其实就是当前 Max 分支的分值,所以需要更新的是 α 值,同样的,Max 分支会选择最大的一个 α 值返回给 Min 分支,这个 α 值也就是当前 Min 分支的分值,所以需要更新的是 β 值。我们只需要记住一点:α 值的来源是 ββ 值的来源是 α(除了叶子结点,因为叶子结点的值来源于评估函数)。

代码实现

我们再接着上次的代码,稍微修改下之前写的 minimax 方法,新增两个输入参数:alphabeta ,代码改动其实不是很大。

   /**
     * 极大极小值搜索、AlphaBeta剪枝
     *
     * @param type  当前走棋方 0.根节点表示AI走棋 1.AI 2.玩家
     * @param depth 搜索深度
     * @param alpha 极大值
     * @param beta  极小值
     * @return
     */
    private int minimax(int type, int depth, int alpha, int beta) {
        // 是否是根节点
        boolean isRoot = type == 0;
        if (isRoot) {
            // 根节点是AI走棋
            type = this.ai;
        }

        // 当前是否是AI走棋
        boolean isAI = type == this.ai;

        // 到达叶子结点
        if (depth == 0) {
            /**
             * 评估每棵博弈树的叶子结点的局势
             * 比如:depth=2时,表示从AI开始走两步棋之后的局势评估,AI(走第一步) -> 玩家(走第二步),然后对局势进行评估
             * 注意:局势评估是以AI角度进行的,分值越大对AI越有利,对玩家越不利
             */
            return evaluateAll();
        }

        for (int i = 0; i < this.cols; i++) {
            if (alpha >= beta) {
                /*
                 AlphaBeta剪枝

                 解释:
                 AI当前最大分数为:alpha 搜索区间 (alpha, +∞]
                 对手当前最小分数为:beta 搜索区间 [-∞, beta)

                 因为对手要选择分数小于beta的分支,AI要从对手给的分支里面选最大的分支,这个最大的分支要和当前的分支(alpha)做比较,
                 现在alpha都比beta大了,下面搜索给出的分支也都是小于alpha的,所以搜索下去没有意义,剪掉提高搜索效率。
                 */
                break;
            }

            for (int j = 0; j < this.rows; j++) {
                if (this.chessData[i][j] != 0) {
                    // 该处已有棋子,跳过
                    continue;
                }

                /* 模拟 AI -> 玩家 交替落子 */
                Point p = new Point(i, j, type);
                // 落子
                putChess(p);
                // 递归生成博弈树,并评估叶子结点的局势
                int score = minimax(3 - type, depth - 1, alpha, beta);
                // 撤销落子
                revokeChess(p);

                if (isAI) {
                    // AI要选对自己最有利的节点(分最高的)
                    if (score > alpha) {
                        // 最高值被刷新,更新alpha值
                        alpha = score;
                        if (isRoot) {
                            // 根节点处更新AI最好的棋位
                            this.bestPoint = p;
                        }
                    }
                } else {
                    // 对手要选对AI最不利的节点(分最低的)
                    if (score < beta) {
                        // 最低值被刷新,更新beta值
                        beta = score;
                    }
                }

                if (alpha >= beta) {
                    // 剪枝
                    break;
                }
            }
        }

        return isAI ? alpha : beta;
    }
复制代码

调整入口方法 getPointminimax 方法传入 αβα 值初始为 -INFINITYβ 值初始为 INFINITY

    @Override
    public Point getPoint(int[][] chessData, Point point, boolean started) {
        initChessData(chessData);
        this.ai = 3 - point.type;
        this.bestPoint = null;
        this.attack = 2;

        if (started) {
            // AI先下,首子天元
            int centerX = this.cols / 2;
            int centerY = this.rows / 2;
            return new Point(centerX, centerY, this.ai);
        }

        // 基于极大极小值搜索获取最佳棋位
        minimax(0, 2, -INFINITY, INFINITY);

        return this.bestPoint;
    }
复制代码

经过优化后,思考两步棋的 AI 耗时基本控制在 1s 内。

image.png

α-β剪枝 算法对节点价值的顺序要求很高,如果价值高的节点都排在后面,那这个算法的优化效果不会太好,所以后续还需要对节点进行价值排序,以便将 α-β剪枝的效果发挥到极致。

源码:github.com/anlingyi/xe…

分类:
后端
收藏成功!
已添加到「」, 点击更改