引言
Flappy Bird是一款风靡全球的极简风格休闲游戏,玩家通过控制小鸟的跳跃来穿越管道间隙。本文将使用Java实现一个基于Actor-Critic强化学习的Flappy Bird AI,让AI自主学习何时跳跃、何时下落的决策策略。Actor-Critic算法结合了策略梯度(Policy Gradient)与价值函数逼近,是强化学习领域最经典的方法之一。
核心算法思路
Actor-Critic架构
Actor-Critic算法由两个神经网络组成:
– Actor(策略网络):输入当前状态,输出动作概率分布(跳跃或不跳)
– Critic(价值网络):输入当前状态,输出状态价值评估(预期累计奖励)
策略梯度更新
Actor网络通过策略梯度定理更新:
– 如果Critic评估某个动作能带来高于预期的好处(优势函数Advantage > 0),则增加该动作的概率
– 反之则降低该动作的概率
时序差分学习
Critic网络使用时序差分(TD)误差来更新价值估计,使预测值逐步逼近真实的累计回报。
Java实现
游戏环境模拟
import java.util.*;
/**
* Flappy Bird游戏环境
*/
public class FlappyBirdEnv {
private static final int SCREEN_WIDTH = 288;
private static final int SCREEN_HEIGHT = 512;
private static final int BIRD_X = 50; // 小鸟固定x坐标
private static final int BIRD_WIDTH = 34;
private static final int BIRD_HEIGHT = 24;
private static final int PIPE_WIDTH = 52;
private static final int PIPE_GAP = 100; // 管道间隙
private static final int GRAVITY = 1; // 重力加速度
private static final int JUMP_VELOCITY = -9; // 跳跃初速度
private double birdY; // 小鸟y坐标
private double birdVel; // 小鸟垂直速度
private List<Pipe> pipes; // 管道列表
private int score;
private int frame;
private boolean done;
private Random random = new Random();
public FlappyBirdEnv() {
reset();
}
/**
* 重置游戏
*/
public void reset() {
birdY = SCREEN_HEIGHT / 2.0;
birdVel = 0;
score = 0;
frame = 0;
done = false;
pipes = new ArrayList<>();
// 生成初始管道
pipes.add(new Pipe(SCREEN_WIDTH + 100));
pipes.add(new Pipe(SCREEN_WIDTH + 250));
}
/**
* 执行动作
* @param action 0=不操作(下落),1=跳跃
* @return [下一状态, 即时奖励, 是否结束]
*/
public Object[] step(int action) {
if (done) {
return new Object[]{getState(), 0.0, true};
}
// 执行动作
if (action == 1) {
birdVel = JUMP_VELOCITY;
}
// 物理更新
birdVel += GRAVITY;
birdY += birdVel;
frame++;
// 更新管道位置
for (Pipe pipe : pipes) {
pipe.x -= 2; // 管道向左移动
}
// 移除越界管道并生成新管道
if (!pipes.isEmpty() && pipes.get(0).x < -PIPE_WIDTH) {
pipes.remove(0);
pipes.add(new Pipe(SCREEN_WIDTH));
score++;
}
// 碰撞检测
done = checkCollision();
// 计算奖励
double reward;
if (done) {
reward = -10.0; // 死亡惩罚
} else {
// 存活奖励 + 通过管道奖励
reward = 0.1 + (frame % 90 == 0 ? 1.0 : 0);
}
return new Object[]{getState(), reward, done};
}
/**
* 获取当前状态向量(用于神经网络输入)
* 状态包含:小鸟y坐标归一化、小鸟速度归一化、
* 下一管道水平距离、下一管道上沿高度、下一管道下沿高度
*/
public double[] getState() {
double[] state = new double[5];
state[0] = birdY / SCREEN_HEIGHT;
state[1] = (birdVel + 10) / 20.0; // 归一化到[0,1]
// 找到下一根管道
Pipe nextPipe = null;
for (Pipe pipe : pipes) {
if (pipe.x + PIPE_WIDTH >= BIRD_X) {
nextPipe = pipe;
break;
}
}
if (nextPipe != null) {
state[2] = (nextPipe.x - BIRD_X) / SCREEN_WIDTH;
state[3] = nextPipe.gapY / SCREEN_HEIGHT;
state[4] = (nextPipe.gapY + PIPE_GAP) / SCREEN_HEIGHT;
} else {
state[2] = 1.0;
state[3] = 0.4;
state[4] = 0.6;
}
return state;
}
private boolean checkCollision() {
// 边界碰撞
if (birdY < 0 || birdY + BIRD_HEIGHT > SCREEN_HEIGHT) {
return true;
}
// 管道碰撞
for (Pipe pipe : pipes) {
if (BIRD_X + BIRD_WIDTH > pipe.x && BIRD_X < pipe.x + PIPE_WIDTH) {
if (birdY < pipe.gapY || birdY + BIRD_HEIGHT > pipe.gapY + PIPE_GAP) {
return true;
}
}
}
return false;
}
public boolean isDone() { return done; }
public int getScore() { return score; }
public int getFrame() { return frame; }
/**
* 管道内部类
*/
private class Pipe {
double x;
double gapY; // 间隙上沿y坐标
Pipe(double x) {
this.x = x;
// 随机生成间隙位置(留出上下边距)
this.gapY = 50 + random.nextInt(SCREEN_HEIGHT - PIPE_GAP - 150);
}
}
}
Actor-Critic神经网络
import java.util.Random;
/**
* Actor-Critic神经网络
*/
public class ActorCriticNetwork {
private Random random = new Random(42);
private double learningRate = 0.001;
private double gamma = 0.99; // 折扣因子
// 共享隐藏层参数
private double[][] w1; // 输入->隐藏层 [input_size][hidden_size]
private double[] b1; // 隐藏层偏置
// Actor输出层参数
private double[][] wActor; // 隐藏层->Actor [hidden_size][2]
private double[] bActor;
// Critic输出层参数
private double[] wCritic; // 隐藏层->Critic [hidden_size]
private double bCritic;
private int inputSize, hiddenSize;
// 前向传播缓存
private double[] hiddenInput, hiddenOutput;
private double[] actorInput, actorOutput;
private double criticInput, criticOutput;
public ActorCriticNetwork(int inputSize, int hiddenSize) {
this.inputSize = inputSize;
this.hiddenSize = hiddenSize;
w1 = new double[inputSize][hiddenSize];
b1 = new double[hiddenSize];
wActor = new double[hiddenSize][2];
bActor = new double[2];
wCritic = new double[hiddenSize];
bCritic = 0;
initializeWeights();
}
private void initializeWeights() {
double limit1 = Math.sqrt(2.0 / inputSize);
for (int i = 0; i < inputSize; i++) {
for (int j = 0; j < hiddenSize; j++) {
w1[i][j] = random.nextGaussian() * limit1;
}
}
double limitActor = Math.sqrt(2.0 / hiddenSize);
for (int h = 0; h < hiddenSize; h++) {
for (int a = 0; a < 2; a++) {
wActor[h][a] = random.nextGaussian() * limitActor;
}
wCritic[h] = random.nextGaussian() * limitActor;
}
}
/**
* 前向传播
* @return [actor_probs[2], critic_value]
*/
public Object[] forward(double[] state) {
// 共享隐藏层
hiddenInput = new double[hiddenSize];
hiddenOutput = new double[hiddenSize];
for (int j = 0; j < hiddenSize; j++) {
double sum = b1[j];
for (int i = 0; i < inputSize; i++) {
sum += state[i] * w1[i][j];
}
hiddenInput[j] = sum;
hiddenOutput[j] = relu(sum);
}
// Actor输出
actorInput = new double[2];
actorOutput = new double[2];
for (int a = 0; a < 2; a++) {
double sum = bActor[a];
for (int h = 0; h < hiddenSize; h++) {
sum += hiddenOutput[h] * wActor[h][a];
}
actorInput[a] = sum;
}
actorOutput = softmax(actorInput);
// Critic输出
criticInput = bCritic;
for (int h = 0; h < hiddenSize; h++) {
criticInput += hiddenOutput[h] * wCritic[h];
}
criticOutput = criticInput; // 线性输出
return new Object[]{actorOutput, criticOutput};
}
/**
* 根据Actor策略选择动作
*/
public int selectAction(double[] state) {
Object[] result = forward(state);
double[] probs = (double[]) result[0];
// 按概率采样
double r = random.nextDouble();
return r < probs[0] ? 0 : 1;
}
/**
* 训练一步
* @param state 当前状态
* @param action 执行的动作
* @param reward 即时奖励
* @param nextState 下一状态
* @param done 是否结束
*/
public void train(double[] state, int action, double reward,
double[] nextState, boolean done) {
// 前向传播当前状态
Object[] current = forward(state);
double[] actorProbs = (double[]) current[0];
double currentValue = (Double) current[1];
// 前向传播下一状态
Object[] next = forward(nextState);
double nextValue = done ? 0 : (Double) next[1];
// TD误差(优势函数近似)
double tdTarget = reward + gamma * nextValue;
double tdError = tdTarget - currentValue;
// === Critic更新 ===
// 价值损失梯度 dL/dv = -(tdTarget - v)
double criticGrad = -tdError;
// === Actor更新(策略梯度)===
// 策略梯度: dlog(pi(a)) / dtheta * Advantage
double[] actorGrad = new double[2];
for (int a = 0; a < 2; a++) {
if (a == action) {
actorGrad[a] = (1 - actorProbs[a]) * tdError; // 执行的动作
} else {
actorGrad[a] = -actorProbs[a] * tdError; // 未执行的动作
}
}
// === 反向传播 ===
// 1. 计算隐藏层梯度
double[] hiddenGrad = new double[hiddenSize];
for (int h = 0; h < hiddenSize; h++) {
double grad = 0;
// Actor贡献
for (int a = 0; a < 2; a++) {
grad += actorGrad[a] * wActor[h][a];
}
// Critic贡献
grad += criticGrad * wCritic[h];
// ReLU导数
hiddenGrad[h] = grad * reluDerivative(hiddenInput[h]);
}
// 2. 更新Actor权重
for (int h = 0; h < hiddenSize; h++) {
for (int a = 0; a < 2; a++) {
wActor[h][a] -= learningRate * actorGrad[a] * hiddenOutput[h];
}
}
for (int a = 0; a < 2; a++) {
bActor[a] -= learningRate * actorGrad[a];
}
// 3. 更新Critic权重
for (int h = 0; h < hiddenSize; h++) {
wCritic[h] -= learningRate * criticGrad * hiddenOutput[h];
}
bCritic -= learningRate * criticGrad;
// 4. 更新共享隐藏层权重
for (int i = 0; i < inputSize; i++) {
for (int h = 0; h < hiddenSize; h++) {
w1[i][h] -= learningRate * hiddenGrad[h] * state[i];
}
}
for (int h = 0; h < hiddenSize; h++) {
b1[h] -= learningRate * hiddenGrad[h];
}
}
private double relu(double x) {
return Math.max(0, x);
}
private double reluDerivative(double x) {
return x > 0 ? 1.0 : 0.0;
}
private double[] softmax(double[] x) {
double max = Math.max(x[0], x[1]);
double exp0 = Math.exp(x[0] - max);
double exp1 = Math.exp(x[1] - max);
double sum = exp0 + exp1;
return new double[]{exp0 / sum, exp1 / sum};
}
public void setLearningRate(double lr) { this.learningRate = lr; }
}
训练引擎
/**
* Actor-Critic训练引擎
*/
public class ActorCriticTrainer {
private ActorCriticNetwork network;
private FlappyBirdEnv env;
public ActorCriticTrainer(ActorCriticNetwork network) {
this.network = network;
this.env = new FlappyBirdEnv();
}
/**
* 训练一个episode
*/
public void trainEpisode() {
env.reset();
double[] state = env.getState();
int steps = 0;
final int MAX_STEPS = 10000;
while (!env.isDone() && steps < MAX_STEPS) {
int action = network.selectAction(state);
Object[] result = env.step(action);
double[] nextState = (double[]) result[0];
double reward = (Double) result[1];
boolean done = (Boolean) result[2];
network.train(state, action, reward, nextState, done);
state = nextState;
steps++;
}
}
/**
* 批量训练
*/
public void train(int episodes) {
for (int ep = 0; ep < episodes; ep++) {
trainEpisode();
if ((ep + 1) % 100 == 0) {
double avgScore = evaluate(10);
System.out.printf("Episode %d: 平均得分 %.1f%n", ep + 1, avgScore);
}
}
}
/**
* 评估当前策略
*/
public double evaluate(int games) {
int totalScore = 0;
for (int g = 0; g < games; g++) {
env.reset();
double[] state = env.getState();
while (!env.isDone()) {
Object[] result = network.forward(state);
double[] probs = (double[]) result[0];
int action = probs[1] > probs[0] ? 1 : 0; // 贪婪选择
Object[] stepResult = env.step(action);
state = (double[]) stepResult[0];
}
totalScore += env.getScore();
}
return totalScore / (double) games;
}
}
主程序入口
/**
* Flappy Bird Actor-Critic AI主程序
*/
public class FlappyBirdAI {
public static void main(String[] args) {
// 创建Actor-Critic网络
// 输入: 5维状态 [birdY_norm, birdVel_norm, pipeDist, pipeTop, pipeBottom]
// 隐藏层: 64个神经元
ActorCriticNetwork network = new ActorCriticNetwork(5, 64);
network.setLearningRate(0.001);
System.out.println("开始Actor-Critic训练...");
ActorCriticTrainer trainer = new ActorCriticTrainer(network);
trainer.train(5000);
System.out.println("\n训练完成!进行最终评估...");
double finalScore = trainer.evaluate(50);
System.out.printf("最终平均得分: %.1f%n", finalScore);
// 演示一局
System.out.println("\n=== AI演示 ===");
FlappyBirdEnv demoEnv = new FlappyBirdEnv();
demoEnv.reset();
double[] state = demoEnv.getState();
int frame = 0;
while (!demoEnv.isDone() && frame < 5000) {
Object[] result = network.forward(state);
double[] probs = (double[]) result[0];
double value = (Double) result[1];
int action = probs[1] > probs[0] ? 1 : 0;
if (frame % 30 == 0) {
System.out.printf("Frame %d: 跳跃概率=%.3f, 价值=%.3f, 动作=%s%n",
frame, probs[1], value, action == 1 ? "跳跃" : "下落");
}
Object[] stepResult = demoEnv.step(action);
state = (double[]) stepResult[0];
frame++;
}
System.out.println("\n演示结束!得分: " + demoEnv.getScore());
}
}
算法复杂度分析
| 操作 | 时间复杂度 | 空间复杂度 |
|---|---|---|
| 前向传播 | O(I×H + H×A + H) | O(H + A) |
| 反向传播 | O(I×H + H×A + H) | O(H + A) |
| 单步训练 | O(I×H + H×A) | O(I + H + A) |
| 完整训练(N episode) | O(N × S × (I×H + H×A)) | O(I + H + A) |
其中:I=5(输入维度),H=64(隐藏层),A=2(动作数),S=每局最大步数,N=训练轮数。
总结
本文通过Java实现了基于Actor-Critic的Flappy Bird AI,核心要点包括:
- Actor网络学习策略,输出动作概率分布
- Critic网络评估价值,提供TD误差指导策略更新
- 共享隐藏层让状态表示同时服务于策略和价值估计
- 时序差分学习使价值估计逐步逼近真实回报
读者可以尝试调整网络结构(增加隐藏层维度、使用RNN处理时序信息)、改进奖励函数(引入管道中心对齐奖励)或探索更先进的PPO、A3C等策略梯度变体算法。