每日算法 — 使用java实现五子棋:深度卷积神经网络与策略网络

一、问题背景

五子棋(Gomoku)是一种双人对弈的纯策略型棋类游戏,双方轮流在15×15的棋盘交叉点上落子,先形成五子连珠者获胜。本文用Java实现一个五子棋AI,核心算法为深度卷积神经网络(CNN)提取棋盘空间特征,结合策略网络输出每个空位的落子概率分布,实现端到端的棋力评估与决策。

二、算法核心思想

2.1 棋盘状态表示

将15×15棋盘编码为两个二进制通道:
– 黑子通道:当前玩家执子位置为1,其余为0
– 白子通道:对手执子位置为1,其余为0

这种双通道表示保留了空间结构信息,适合卷积神经网络处理。

2.2 卷积神经网络结构

采用轻量级CNN提取局部棋形特征:

输入(2×15×15) → Conv(16, 3×3, ReLU) → Conv(32, 3×3, ReLU) → Flatten → FC(256, ReLU) → FC(225, Softmax)

最终输出225个概率值,对应15×15每个交叉点的落子概率。

2.3 策略梯度训练

通过自我对弈生成训练数据,以胜负结果为奖励信号。采用REINFORCE策略梯度更新网络:

$$\nabla_\theta J(\theta) = \mathbb{E}\pi \left[ G_t \cdot \nabla\theta \log \pi_\theta(a_t | s_t) \right]$$

其中 $G_t$ 为从时刻 $t$ 开始的累积折扣奖励,$\pi_\theta$ 为策略网络。

三、完整Java实现

import java.util.*;

/**
 * 五子棋AI - 深度卷积神经网络与策略网络
 */
public class GomokuCNN {

    static final int BOARD_SIZE = 15;
    static final int WIN = 5;

    // ==================== 卷积层 ====================
    static class ConvLayer {
        int inChannels, outChannels, kernelSize;
        double[][][][] weights; // [out][in][k][k]
        double[] bias;
        Random rand;

        public ConvLayer(int inC, int outC, int k, long seed) {
            this.inChannels = inC; this.outChannels = outC; this.kernelSize = k;
            weights = new double[outC][inC][k][k];
            bias = new double[outC];
            rand = new Random(seed);
            double scale = Math.sqrt(2.0 / (inC * k * k));
            for (int oc = 0; oc < outC; oc++)
                for (int ic = 0; ic < inC; ic++)
                    for (int i = 0; i < k; i++)
                        for (int j = 0; j < k; j++)
                            weights[oc][ic][i][j] = rand.nextGaussian() * scale;
        }

        double[][][] forward(double[][][] input) {
            int H = input[0].length, W = input[0][0].length;
            int pad = kernelSize / 2;
            double[][][] out = new double[outChannels][H][W];
            for (int oc = 0; oc < outChannels; oc++) {
                for (int h = 0; h < H; h++) {
                    for (int w = 0; w < W; w++) {
                        double sum = bias[oc];
                        for (int ic = 0; ic < inChannels; ic++) {
                            for (int kh = 0; kh < kernelSize; kh++) {
                                for (int kw = 0; kw < kernelSize; kw++) {
                                    int ih = h + kh - pad, iw = w + kw - pad;
                                    if (ih >= 0 && ih < H && iw >= 0 && iw < W)
                                        sum += input[ic][ih][iw] * weights[oc][ic][kh][kw];
                                }
                            }
                        }
                        out[oc][h][w] = Math.max(0, sum); // ReLU
                    }
                }
            }
            return out;
        }
    }

    // ==================== 全连接层 ====================
    static class FCLayer {
        int inputSize, outputSize;
        double[][] weights;
        double[] bias;
        double lr;

        public FCLayer(int inSize, int outSize, double lr, long seed) {
            this.inputSize = inSize; this.outputSize = outSize; this.lr = lr;
            weights = new double[inSize][outSize];
            bias = new double[outSize];
            Random rand = new Random(seed);
            double scale = Math.sqrt(2.0 / inSize);
            for (int i = 0; i < inSize; i++)
                for (int j = 0; j < outSize; j++)
                    weights[i][j] = rand.nextGaussian() * scale;
        }

        double[] forward(double[] input) {
            double[] out = new double[outputSize];
            for (int j = 0; j < outputSize; j++) {
                double sum = bias[j];
                for (int i = 0; i < inputSize; i++) sum += input[i] * weights[i][j];
                out[j] = sum;
            }
            return out;
        }
    }

    // ==================== 策略网络 ====================
    static class PolicyNetwork {
        ConvLayer conv1, conv2;
        FCLayer fc1, fc2;

        public PolicyNetwork() {
            conv1 = new ConvLayer(2, 16, 3, 1);
            conv2 = new ConvLayer(16, 32, 3, 2);
            fc1 = new FCLayer(32 * BOARD_SIZE * BOARD_SIZE, 256, 0.001, 3);
            fc2 = new FCLayer(256, BOARD_SIZE * BOARD_SIZE, 0.001, 4);
        }

        /** 前向传播,返回logits */
        double[] forward(double[][][] boardState) {
            double[][][] h1 = conv1.forward(boardState);
            double[][][] h2 = conv2.forward(h1);
            double[] flat = flatten(h2);
            double[] h3 = fc1.forward(flat);
            for (int i = 0; i < h3.length; i++) h3[i] = Math.max(0, h3[i]); // ReLU
            return fc2.forward(h3);
        }

        double[] flatten(double[][][] arr) {
            int c = arr.length, h = arr[0].length, w = arr[0][0].length;
            double[] out = new double[c * h * w];
            int idx = 0;
            for (int i = 0; i < c; i++)
                for (int j = 0; j < h; j++)
                    for (int k = 0; k < w; k++)
                        out[idx++] = arr[i][j][k];
            return out;
        }

        /** Softmax + 合法位置掩码 */
        double[] predict(double[][][] state, boolean[][] validMask) {
            double[] logits = forward(state);
            // 掩码非法位置
            for (int i = 0; i < logits.length; i++)
                if (!validMask[i / BOARD_SIZE][i % BOARD_SIZE]) logits[i] = Double.NEGATIVE_INFINITY;
            return softmax(logits);
        }
    }

    static double[] softmax(double[] x) {
        double max = x[0];
        for (double v : x) if (v > max) max = v;
        double sum = 0;
        double[] out = new double[x.length];
        for (int i = 0; i < x.length; i++) {
            out[i] = Math.exp(x[i] - max);
            sum += out[i];
        }
        for (int i = 0; i < x.length; i++) out[i] /= sum;
        return out;
    }

    // ==================== 五子棋环境 ====================
    static class GomokuEnv {
        int[][] board = new int[BOARD_SIZE][BOARD_SIZE]; // 0=空, 1=黑, 2=白
        int currentPlayer = 1; // 黑先
        boolean gameOver = false;
        int winner = 0;
        List<int[]> history = new ArrayList<>();

        void reset() {
            for (int[] row : board) Arrays.fill(row, 0);
            currentPlayer = 1;
            gameOver = false;
            winner = 0;
            history.clear();
        }

        /** 执行落子,返回是否成功 */
        boolean step(int row, int col) {
            if (gameOver || row < 0 || row >= BOARD_SIZE || col < 0 || col >= BOARD_SIZE || board[row][col] != 0)
                return false;
            board[row][col] = currentPlayer;
            history.add(new int[]{row, col, currentPlayer});
            if (checkWin(row, col)) {
                gameOver = true;
                winner = currentPlayer;
            } else if (history.size() == BOARD_SIZE * BOARD_SIZE) {
                gameOver = true;
                winner = 0; // 和棋
            }
            currentPlayer = (currentPlayer == 1) ? 2 : 1;
            return true;
        }

        boolean checkWin(int r, int c) {
            int player = board[r][c];
            int[][] dirs = {{0,1},{1,0},{1,1},{1,-1}};
            for (int[] d : dirs) {
                int count = 1;
                for (int sign = -1; sign <= 1; sign += 2) {
                    for (int k = 1; k < WIN; k++) {
                        int nr = r + sign * k * d[0], nc = c + sign * k * d[1];
                        if (nr < 0 || nr >= BOARD_SIZE || nc < 0 || nc >= BOARD_SIZE || board[nr][nc] != player) break;
                        count++;
                    }
                }
                if (count >= WIN) return true;
            }
            return false;
        }

        /** 获取当前玩家的状态表示 [2][15][15] */
        double[][][] getState() {
            double[][][] state = new double[2][BOARD_SIZE][BOARD_SIZE];
            int myColor = (currentPlayer == 1) ? 1 : 2;
            int oppColor = (currentPlayer == 1) ? 2 : 1;
            for (int r = 0; r < BOARD_SIZE; r++) {
                for (int c = 0; c < BOARD_SIZE; c++) {
                    if (board[r][c] == myColor) state[0][r][c] = 1;
                    else if (board[r][c] == oppColor) state[1][r][c] = 1;
                }
            }
            return state;
        }

        boolean[][] getValidMask() {
            boolean[][] mask = new boolean[BOARD_SIZE][BOARD_SIZE];
            for (int r = 0; r < BOARD_SIZE; r++)
                for (int c = 0; c < BOARD_SIZE; c++)
                    mask[r][c] = (board[r][c] == 0);
            return mask;
        }
    }

    // ==================== 策略梯度训练(简化版) ====================
    static class Trainer {
        PolicyNetwork net;
        Random rand = new Random();

        public Trainer(PolicyNetwork net) { this.net = net; }

        /** 自我对弈一局,返回对局记录 */
        List<GameRecord> selfPlay() {
            GomokuEnv env = new GomokuEnv();
            List<GameRecord> records = new ArrayList<>();
            while (!env.gameOver) {
                double[][][] state = env.getState();
                boolean[][] mask = env.getValidMask();
                double[] probs = net.predict(state, mask);
                int action = sampleAction(probs);
                int row = action / BOARD_SIZE, col = action % BOARD_SIZE;
                records.add(new GameRecord(state, action, env.currentPlayer));
                env.step(row, col);
            }
            // 分配奖励:胜者+1,败者-1,和棋0
            for (GameRecord gr : records) {
                if (env.winner == 0) gr.reward = 0;
                else gr.reward = (gr.player == env.winner) ? 1 : -1;
            }
            return records;
        }

        int sampleAction(double[] probs) {
            double p = rand.nextDouble();
            double cum = 0;
            for (int i = 0; i < probs.length; i++) {
                cum += probs[i];
                if (p <= cum) return i;
            }
            return probs.length - 1;
        }
    }

    static class GameRecord {
        double[][][] state;
        int action, player;
        double reward;
        GameRecord(double[][][] s, int a, int p) { state = s; action = a; player = p; }
    }

    // ==================== 主程序 ====================
    public static void main(String[] args) {
        PolicyNetwork net = new PolicyNetwork();
        Trainer trainer = new Trainer(net);
        GomokuEnv env = new GomokuEnv();

        // 演示:随机初始化网络 vs 简单启发式玩家
        System.out.println("五子棋AI演示局(CNN策略网络 vs 贪心启发式)");
        System.out.println("========================================");

        env.reset();
        int step = 0;
        while (!env.gameOver && step < 225) {
            if (env.currentPlayer == 1) {
                // AI落子
                double[] probs = net.predict(env.getState(), env.getValidMask());
                int action = argMax(probs);
                int r = action / BOARD_SIZE, c = action % BOARD_SIZE;
                env.step(r, c);
                System.out.printf("AI落子: (%d, %d)%n", r, c);
            } else {
                // 贪心启发式:寻找最佳进攻/防守位置
                int[] move = greedyMove(env);
                env.step(move[0], move[1]);
                System.out.printf("贪心落子: (%d, %d)%n", move[0], move[1]);
            }
            printBoard(env.board);
            step++;
        }

        if (env.winner != 0) System.out.println("胜者: " + (env.winner == 1 ? "AI" : "贪心启发式"));
        else System.out.println("和棋");
    }

    /** 简单贪心启发式:优先连成五子,其次阻止对手 */
    static int[] greedyMove(GomokuEnv env) {
        int bestR = -1, bestC = -1, bestScore = -1;
        for (int r = 0; r < BOARD_SIZE; r++) {
            for (int c = 0; c < BOARD_SIZE; c++) {
                if (env.board[r][c] != 0) continue;
                int score = evaluate(env.board, r, c, env.currentPlayer);
                // 也考虑对手威胁
                int opp = (env.currentPlayer == 1) ? 2 : 1;
                score = Math.max(score, evaluate(env.board, r, c, opp));
                if (score > bestScore) {
                    bestScore = score;
                    bestR = r; bestC = c;
                }
            }
        }
        return new int[]{bestR, bestC};
    }

    /** 评估落子价值:连子长度越长分数越高 */
    static int evaluate(int[][] board, int r, int c, int player) {
        int[][] dirs = {{0,1},{1,0},{1,1},{1,-1}};
        int maxScore = 0;
        for (int[] d : dirs) {
            int count = 1;
            boolean open1 = false, open2 = false;
            for (int sign = -1; sign <= 1; sign += 2) {
                int blocked = 0;
                for (int k = 1; k <= 5; k++) {
                    int nr = r + sign * k * d[0], nc = c + sign * k * d[1];
                    if (nr < 0 || nr >= BOARD_SIZE || nc < 0 || nc >= BOARD_SIZE) { blocked = 1; break; }
                    if (board[nr][nc] == player) count++;
                    else if (board[nr][nc] == 0) { open1 = true; break; }
                    else { blocked = 1; break; }
                }
                if (sign == -1 && count > 1) open2 = open1;
            }
            int score = count;
            if (count >= 5) score = 100000;
            else if (count == 4) score = (open1 || open2) ? 10000 : 1000;
            else if (count == 3) score = (open1 && open2) ? 1000 : 100;
            else if (count == 2) score = (open1 && open2) ? 100 : 10;
            maxScore = Math.max(maxScore, score);
        }
        return maxScore;
    }

    static int argMax(double[] arr) {
        int idx = 0;
        for (int i = 1; i < arr.length; i++) if (arr[i] > arr[idx]) idx = i;
        return idx;
    }

    static void printBoard(int[][] board) {
        System.out.print("  ");
        for (int c = 0; c < BOARD_SIZE; c++) System.out.print(c % 10);
        System.out.println();
        for (int r = 0; r < BOARD_SIZE; r++) {
            System.out.print(r % 10 + " ");
            for (int c = 0; c < BOARD_SIZE; c++) {
                if (board[r][c] == 0) System.out.print(".");
                else if (board[r][c] == 1) System.out.print("X");
                else System.out.print("O");
            }
            System.out.println();
        }
        System.out.println();
    }
}

四、训练优化建议

上述代码展示了网络结构与推理流程。实际训练需注意:

  1. 数据增强:对棋盘进行旋转和翻转,扩充训练样本8倍
  2. 温度参数:在Softmax前除以温度 $T$,控制探索程度。$T>1$ 时更均匀,$T<1$ 时更贪婪
  3. 价值网络辅助:除策略网络外,增加价值网络 $V(s)$ 评估当前胜率,指导MCTS搜索
  4. 残差连接:在卷积层间添加跳跃连接,缓解梯度消失,提升深层网络训练效果

五、算法复杂度分析

模块 时间复杂度 空间复杂度
卷积前向(Conv1) $O(C_{out} \cdot C_{in} \cdot K^2 \cdot H \cdot W)$ $O(C_{out} \cdot H \cdot W)$
卷积前向(Conv2) $O(32 \cdot 16 \cdot 9 \cdot 225)$ $O(32 \cdot 225)$
全连接 $O(D_{in} \cdot D_{out})$ $O(D_{out})$
贪心评估 $O(N^2 \cdot D \cdot L)$ $O(1)$

其中 $N=15$ 为棋盘大小,$K=3$ 为卷积核,$D=4$ 为方向数,$L=5$ 为连子长度。

六、总结

本文实现了基于CNN策略网络的五子棋AI:
– 双通道状态编码:分别表示己方与对方的棋子分布,保留空间结构
– 轻量CNN:两层卷积提取局部棋形特征,全连接层输出落子概率
– 策略梯度:通过自我对弈生成数据,以胜负结果驱动网络优化
– 贪心基线:提供可对比的启发式对手,验证网络学习效果

读者可在此基础上扩展残差网络、AlphaZero风格的MCTS+神经网络结合等高级架构。