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和深度神经网络:
- Q值函数:Q(s,a)表示在状态s下执行动作a的预期累积回报
- 神经网络近似:使用神经网络近似Q值函数,输入为游戏状态,输出为四个动作的Q值
- 经验回放(Experience Replay):存储历史转移样本(s, a, r, s’, done),随机采样打破数据相关性
- 目标网络(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游戏,我们采用以下奖励策略:
- 即时奖励:每次合并获得的分数差值
- 终局奖励:成功合成2048给予大额正奖励
- 位置启发:鼓励大数字位于角落(可通过额外奖励实现)
- 存活奖励:每步小幅奖励以鼓励长期游戏
网络架构与训练技巧
本文实现的DQN采用以下架构:
– 输入层:192维(4×4×12独热编码)
– 隐藏层1:256个神经元,ReLU激活
– 隐藏层2:128个神经元,ReLU激活
– 输出层:4个神经元(对应上下左右四个动作的Q值)
关键训练技巧:
– 经验回放:存储历史转移样本,随机采样打破时间相关性
– 目标网络同步:每10个episode同步一次目标网络参数
– ε-贪心探索:从完全随机探索逐渐过渡到贪心策略
总结
本文使用深度Q网络(DQN)训练2048游戏AI,通过神经网络近似Q值函数,结合经验回放和目标网络稳定训练。DQN方法让AI能够自主学习游戏策略,无需人工设计复杂的启发式评估函数。