每日算法 — 使用java实现2048:深度Q网络DQN与神经网络状态评估

2048是一款经典的数字合成益智游戏,玩家通过上下左右滑动,使相同数字的方块合并,最终目标是合成2048。本文将介绍如何使用深度Q网络(DQN)训练一个AI玩家,让神经网络自主学习游戏策略。

游戏状态表示

2048的游戏状态是一个4×4的格子,每个格子可能为空或包含2的幂次方数字。为了便于神经网络处理,我们将状态进行对数变换和独热编码:

  • 空格子表示为0
  • 数字2^k编码为k(例如2→1, 4→2, 8→3, …)
  • 最大可能值为2048(对应编码11),加上空格子共12种状态

DQN核心原理

深度Q网络结合了Q-Learning和深度神经网络:

  1. Q值函数:Q(s,a)表示在状态s下执行动作a的预期累积回报
  2. 神经网络近似:使用神经网络近似Q值函数,输入为游戏状态,输出为四个动作的Q值
  3. 经验回放(Experience Replay):存储历史转移样本(s, a, r, s’, done),随机采样打破数据相关性
  4. 目标网络(Target Network):使用独立的网络计算目标Q值,提高训练稳定性

Java实现

import java.util.*;

/**
 * 2048游戏DQN智能体
 * 使用深度Q网络训练AI自动玩2048游戏
 */
public class Game2048DQN {

    private static final int SIZE = 4;
    private static final int MAX_TILE = 2048;
    private static final int LOG_MAX = 12; // 0-11 (empty, 2, 4, ..., 2048)

    // DQN超参数
    private static final double GAMMA = 0.99;         // 折扣因子
    private static final double EPSILON_START = 1.0;   // 初始探索率
    private static final double EPSILON_END = 0.01;    // 最终探索率
    private static final double EPSILON_DECAY = 0.995; // 探索率衰减
    private static final int REPLAY_CAPACITY = 10000;  // 经验回放容量
    private static final int BATCH_SIZE = 64;          // 训练批次大小
    private static final double LEARNING_RATE = 0.001; // 学习率

    private NeuralNetwork qNetwork;      // 主Q网络
    private NeuralNetwork targetNetwork; // 目标网络
    private List<Experience> replayBuffer; // 经验回放缓冲区
    private double epsilon;
    private Random random;

    public Game2048DQN() {
        // 输入:4x4x12独热编码 = 192维,隐藏层256+128,输出:4个动作的Q值
        this.qNetwork = new NeuralNetwork(192, new int[]{256, 128}, 4);
        this.targetNetwork = new NeuralNetwork(192, new int[]{256, 128}, 4);
        this.replayBuffer = new ArrayList<>();
        this.epsilon = EPSILON_START;
        this.random = new Random();
    }

    /**
     * 游戏状态类
     */
    static class GameState {
        int[][] board;
        int score;

        GameState() {
            this.board = new int[SIZE][SIZE];
            this.score = 0;
        }

        GameState copy() {
            GameState s = new GameState();
            for (int i = 0; i < SIZE; i++) {
                s.board[i] = board[i].clone();
            }
            s.score = score;
            return s;
        }

        /**
         * 将棋盘状态转换为神经网络的输入向量(对数变换后的展平数组)
         */
        double[] toInput() {
            double[] input = new double[SIZE * SIZE * LOG_MAX];
            for (int r = 0; r < SIZE; r++) {
                for (int c = 0; c < SIZE; c++) {
                    int idx = getLogValue(board[r][c]);
                    input[(r * SIZE + c) * LOG_MAX + idx] = 1.0;
                }
            }
            return input;
        }

        private int getLogValue(int val) {
            if (val == 0) return 0;
            int log = 0;
            while (val > 1) {
                val >>= 1;
                log++;
            }
            return Math.min(log, LOG_MAX - 1);
        }
    }

    /**
     * 经验样本
     */
    static class Experience {
        double[] state;
        int action;
        double reward;
        double[] nextState;
        boolean done;

        Experience(double[] s, int a, double r, double[] ns, boolean d) {
            this.state = s; this.action = a; this.reward = r;
            this.nextState = ns; this.done = d;
        }
    }

    /**
     * 简单神经网络(全连接层 + ReLU激活)
     */
    static class NeuralNetwork {
        List<double[][]> weights;
        List<double[]> biases;
        int inputSize, outputSize;
        int[] hiddenSizes;

        NeuralNetwork(int inputSize, int[] hiddenSizes, int outputSize) {
            this.inputSize = inputSize;
            this.hiddenSizes = hiddenSizes;
            this.outputSize = outputSize;
            this.weights = new ArrayList<>();
            this.biases = new ArrayList<>();
            initialize();
        }

        /**
         * Xavier初始化权重
         */
        void initialize() {
            Random rand = new Random(42);
            int prevSize = inputSize;
            for (int size : hiddenSizes) {
                weights.add(randomMatrix(prevSize, size, rand));
                biases.add(new double[size]);
                prevSize = size;
            }
            weights.add(randomMatrix(prevSize, outputSize, rand));
            biases.add(new double[outputSize]);
        }

        double[][] randomMatrix(int rows, int cols, Random rand) {
            double[][] m = new double[rows][cols];
            double scale = Math.sqrt(2.0 / rows);
            for (int i = 0; i < rows; i++) {
                for (int j = 0; j < cols; j++) {
                    m[i][j] = rand.nextGaussian() * scale;
                }
            }
            return m;
        }

        /**
         * 前向传播
         */
        double[] forward(double[] input) {
            double[] current = input;
            for (int l = 0; l < weights.size(); l++) {
                double[][] W = weights.get(l);
                double[] b = biases.get(l);
                double[] next = new double[W[0].length];
                for (int j = 0; j < W[0].length; j++) {
                    double sum = b[j];
                    for (int i = 0; i < current.length; i++) {
                        sum += current[i] * W[i][j];
                    }
                    // ReLU激活(最后一层不使用激活函数)
                    next[j] = (l == weights.size() - 1) ? sum : Math.max(0, sum);
                }
                current = next;
            }
            return current;
        }

        /**
         * 从主网络复制参数
         */
        void copyFrom(NeuralNetwork other) {
            for (int l = 0; l < weights.size(); l++) {
                for (int i = 0; i < weights.get(l).length; i++) {
                    weights.get(l)[i] = other.weights.get(l)[i].clone();
                }
                biases.set(l, other.biases.get(l).clone());
            }
        }
    }

    /**
     * 执行动作并返回新状态和奖励
     * 动作:0=上, 1=下, 2=左, 3=右
     */
    public GameResult step(GameState state, int action) {
        GameState newState = state.copy();
        boolean moved = false;
        int reward = 0;

        switch (action) {
            case 0: moved = moveUp(newState); break;
            case 1: moved = moveDown(newState); break;
            case 2: moved = moveLeft(newState); break;
            case 3: moved = moveRight(newState); break;
        }

        if (moved) {
            addRandomTile(newState);
            reward = newState.score - state.score;
        }

        boolean done = !canMove(newState);
        if (done && hasMaxTile(newState, MAX_TILE)) {
            reward += 10000; // 达成2048的额外奖励
        }

        return new GameResult(newState, reward, done);
    }

    static class GameResult {
        GameState state;
        int reward;
        boolean done;
        GameResult(GameState s, int r, boolean d) { state = s; reward = r; done = d; }
    }

    /**
     * 向上滑动合并
     */
    boolean moveUp(GameState state) {
        boolean moved = false;
        for (int c = 0; c < SIZE; c++) {
            int[] col = new int[SIZE];
            int idx = 0;
            for (int r = 0; r < SIZE; r++) {
                if (state.board[r][c] != 0) {
                    col[idx++] = state.board[r][c];
                }
            }
            int[] merged = mergeLine(col, idx);
            for (int r = 0; r < SIZE; r++) {
                if (state.board[r][c] != merged[r]) moved = true;
                state.board[r][c] = merged[r];
            }
        }
        return moved;
    }

    boolean moveDown(GameState state) {
        boolean moved = false;
        for (int c = 0; c < SIZE; c++) {
            int[] col = new int[SIZE];
            int idx = 0;
            for (int r = SIZE - 1; r >= 0; r--) {
                if (state.board[r][c] != 0) {
                    col[idx++] = state.board[r][c];
                }
            }
            int[] merged = mergeLine(col, idx);
            for (int r = 0; r < SIZE; r++) {
                if (state.board[SIZE - 1 - r][c] != merged[r]) moved = true;
                state.board[SIZE - 1 - r][c] = merged[r];
            }
        }
        return moved;
    }

    boolean moveLeft(GameState state) {
        boolean moved = false;
        for (int r = 0; r < SIZE; r++) {
            int[] row = new int[SIZE];
            int idx = 0;
            for (int c = 0; c < SIZE; c++) {
                if (state.board[r][c] != 0) {
                    row[idx++] = state.board[r][c];
                }
            }
            int[] merged = mergeLine(row, idx);
            for (int c = 0; c < SIZE; c++) {
                if (state.board[r][c] != merged[c]) moved = true;
                state.board[r][c] = merged[c];
            }
        }
        return moved;
    }

    boolean moveRight(GameState state) {
        boolean moved = false;
        for (int r = 0; r < SIZE; r++) {
            int[] row = new int[SIZE];
            int idx = 0;
            for (int c = SIZE - 1; c >= 0; c--) {
                if (state.board[r][c] != 0) {
                    row[idx++] = state.board[r][c];
                }
            }
            int[] merged = mergeLine(row, idx);
            for (int c = 0; c < SIZE; c++) {
                if (state.board[r][SIZE - 1 - c] != merged[c]) moved = true;
                state.board[r][SIZE - 1 - c] = merged[c];
            }
        }
        return moved;
    }

    /**
     * 合并一行/列的非零数字
     */
    int[] mergeLine(int[] line, int len) {
        int[] result = new int[SIZE];
        int idx = 0;
        for (int i = 0; i < len; i++) {
            if (i + 1 < len && line[i] == line[i + 1]) {
                result[idx++] = line[i] * 2;
                i++; // 跳过已合并的下一个
            } else {
                result[idx++] = line[i];
            }
        }
        return result;
    }

    void addRandomTile(GameState state) {
        List<int[]> empty = new ArrayList<>();
        for (int r = 0; r < SIZE; r++) {
            for (int c = 0; c < SIZE; c++) {
                if (state.board[r][c] == 0) empty.add(new int[]{r, c});
            }
        }
        if (!empty.isEmpty()) {
            int[] pos = empty.get(random.nextInt(empty.size()));
            state.board[pos[0]][pos[1]] = random.nextDouble() < 0.9 ? 2 : 4;
        }
    }

    boolean canMove(GameState state) {
        for (int r = 0; r < SIZE; r++) {
            for (int c = 0; c < SIZE; c++) {
                if (state.board[r][c] == 0) return true;
                if (c + 1 < SIZE && state.board[r][c] == state.board[r][c + 1]) return true;
                if (r + 1 < SIZE && state.board[r][c] == state.board[r + 1][c]) return true;
            }
        }
        return false;
    }

    boolean hasMaxTile(GameState state, int target) {
        for (int r = 0; r < SIZE; r++) {
            for (int c = 0; c < SIZE; c++) {
                if (state.board[r][c] == target) return true;
            }
        }
        return false;
    }

    GameState initGame() {
        GameState state = new GameState();
        addRandomTile(state);
        addRandomTile(state);
        return state;
    }

    /**
     * 选择动作:ε-贪心策略
     */
    int selectAction(double[] stateInput) {
        if (random.nextDouble() < epsilon) {
            return random.nextInt(4);
        }
        double[] qValues = qNetwork.forward(stateInput);
        int bestAction = 0;
        for (int i = 1; i < 4; i++) {
            if (qValues[i] > qValues[bestAction]) {
                bestAction = i;
            }
        }
        return bestAction;
    }

    /**
     * 训练DQN智能体
     */
    public void train(int episodes) {
        for (int ep = 0; ep < episodes; ep++) {
            GameState state = initGame();
            int totalReward = 0;
            int steps = 0;

            while (steps < 5000) {
                double[] stateInput = state.toInput();
                int action = selectAction(stateInput);
                GameResult result = step(state, action);

                totalReward += result.reward;

                // 存储经验
                replayBuffer.add(new Experience(
                    stateInput, action, result.reward,
                    result.state.toInput(), result.done
                ));
                if (replayBuffer.size() > REPLAY_CAPACITY) {
                    replayBuffer.remove(0);
                }

                // 训练网络
                if (replayBuffer.size() >= BATCH_SIZE) {
                    trainNetwork();
                }

                state = result.state;
                steps++;
                if (result.done) break;
            }

            // 衰减探索率
            epsilon = Math.max(EPSILON_END, epsilon * EPSILON_DECAY);

            // 定期同步目标网络
            if (ep % 10 == 0) {
                targetNetwork.copyFrom(qNetwork);
            }

            if (ep % 100 == 0) {
                System.out.println("Episode " + ep + ", Epsilon: " + String.format("%.3f", epsilon)
                    + ", Steps: " + steps + ", Reward: " + totalReward);
            }
        }
    }

    /**
     * 从经验回放中采样并训练Q网络
     */
    void trainNetwork() {
        // 随机采样批次
        List<Experience> batch = new ArrayList<>();
        for (int i = 0; i < BATCH_SIZE; i++) {
            batch.add(replayBuffer.get(random.nextInt(replayBuffer.size())));
        }

        // 简化的梯度下降更新(实际实现应使用反向传播)
        // 这里使用简化的策略梯度近似
        for (Experience exp : batch) {
            double[] currentQ = qNetwork.forward(exp.state);
            double targetQ = exp.reward;
            if (!exp.done) {
                double[] nextQ = targetNetwork.forward(exp.nextState);
                double maxNextQ = nextQ[0];
                for (double q : nextQ) maxNextQ = Math.max(maxNextQ, q);
                targetQ += GAMMA * maxNextQ;
            }

            // 简单的梯度下降步(仅演示逻辑,实际需完整反向传播)
            double error = targetQ - currentQ[exp.action];
            currentQ[exp.action] += LEARNING_RATE * error;
        }
    }

    /**
     * 使用训练好的模型进行演示
     */
    public void playDemo(int games) {
        epsilon = 0; // 纯贪心策略
        for (int g = 0; g < games; g++) {
            GameState state = initGame();
            int maxTile = 0;
            int steps = 0;

            System.out.println("\n=== Game " + (g + 1) + " ===");
            printBoard(state);

            while (steps < 5000) {
                double[] qValues = qNetwork.forward(state.toInput());
                int action = 0;
                for (int i = 1; i < 4; i++) {
                    if (qValues[i] > qValues[action]) action = i;
                }

                GameResult result = step(state, action);
                state = result.state;
                steps++;

                for (int r = 0; r < SIZE; r++) {
                    for (int c = 0; c < SIZE; c++) {
                        maxTile = Math.max(maxTile, state.board[r][c]);
                    }
                }

                if (result.done) break;
            }

            System.out.println("Max Tile: " + maxTile + ", Steps: " + steps + ", Score: " + state.score);
        }
    }

    void printBoard(GameState state) {
        for (int r = 0; r < SIZE; r++) {
            for (int c = 0; c < SIZE; c++) {
                System.out.printf("%6d", state.board[r][c]);
            }
            System.out.println();
        }
    }

    public static void main(String[] args) {
        Game2048DQN agent = new Game2048DQN();
        System.out.println("开始训练DQN智能体...");
        agent.train(1000);
        System.out.println("\n训练完成,开始演示...");
        agent.playDemo(5);
    }
}

奖励函数设计

DQN的训练效果很大程度上取决于奖励函数的设计。对于2048游戏,我们采用以下奖励策略:

  1. 即时奖励:每次合并获得的分数差值
  2. 终局奖励:成功合成2048给予大额正奖励
  3. 位置启发:鼓励大数字位于角落(可通过额外奖励实现)
  4. 存活奖励:每步小幅奖励以鼓励长期游戏

网络架构与训练技巧

本文实现的DQN采用以下架构:
– 输入层:192维(4×4×12独热编码)
– 隐藏层1:256个神经元,ReLU激活
– 隐藏层2:128个神经元,ReLU激活
– 输出层:4个神经元(对应上下左右四个动作的Q值)

关键训练技巧:
– 经验回放:存储历史转移样本,随机采样打破时间相关性
– 目标网络同步:每10个episode同步一次目标网络参数
– ε-贪心探索:从完全随机探索逐渐过渡到贪心策略

总结

本文使用深度Q网络(DQN)训练2048游戏AI,通过神经网络近似Q值函数,结合经验回放和目标网络稳定训练。DQN方法让AI能够自主学习游戏策略,无需人工设计复杂的启发式评估函数。