每日算法 — 使用java实现贪吃蛇:神经网络进化与遗传算法训练AI

贪吃蛇是检验AI进化能力的经典沙盒。本文用Java实现一套完整的神经进化系统:蛇的”大脑”是一个前馈神经网络,根据环境感知决定移动方向;网络权重不依赖反向传播,而是通过遗传算法在种群中逐代进化——优胜者保留、交叉重组、随机变异。你会看到几十条蛇如何从随机乱撞,逐渐学会避墙、追食物、防自咬。

一、问题分析:为什么神经进化适合贪吃蛇

传统强化学习(如Q-Learning)需要大量离散状态存储,而贪吃蛇的状态空间随蛇身长度指数增长。神经进化(Neuroevolution)的直觉是:把策略参数化为神经网络权重,用遗传算法直接搜索最优权重向量

优势在于:
– 状态输入可以是连续的(蛇头到障碍距离、食物方向等),无需离散化。
– 不需要梯度计算,避开反向传播的复杂性和局部最优问题。
– 遗传算法天然支持并行评估,适合种群级搜索。

二、神经网络设计:蛇的”大脑”

蛇的感知包括8个方向(上、下、左、右、左上、右上、左下、右下),每个方向探测三类信息:到墙的距离、到自身身体的距离、到食物的距离。共24维输入。隐藏层12个神经元,输出层3个神经元分别对应”直行””左转””右转”。

import java.util.Random;

/**
 * 前馈神经网络:感知层 -> 隐藏层 -> 输出层
 * 激活函数使用Sigmoid,输出经Softmax转为概率分布
 */
public class NeuralNetwork {
    private final int inputSize;
    private final int hiddenSize;
    private final int outputSize;

    // 权重矩阵
    private final double[][] w1; // inputSize x hiddenSize
    private final double[][] w2; // hiddenSize x outputSize

    // 偏置
    private final double[] b1;
    private final double[] b2;

    private final Random rand = new Random();

    public NeuralNetwork(int inputSize, int hiddenSize, int outputSize) {
        this.inputSize = inputSize;
        this.hiddenSize = hiddenSize;
        this.outputSize = outputSize;
        this.w1 = new double[inputSize][hiddenSize];
        this.w2 = new double[hiddenSize][outputSize];
        this.b1 = new double[hiddenSize];
        this.b2 = new double[outputSize];
        randomize();
    }

    /** 用Xavier初始化权重,避免早期饱和 */
    private void randomize() {
        double limit1 = Math.sqrt(6.0 / (inputSize + hiddenSize));
        for (int i = 0; i < inputSize; i++) {
            for (int j = 0; j < hiddenSize; j++) {
                w1[i][j] = (rand.nextDouble() * 2 - 1) * limit1;
            }
        }
        double limit2 = Math.sqrt(6.0 / (hiddenSize + outputSize));
        for (int i = 0; i < hiddenSize; i++) {
            for (int j = 0; j < outputSize; j++) {
                w2[i][j] = (rand.nextDouble() * 2 - 1) * limit2;
            }
        }
        // 偏置初始化为小随机数
        for (int j = 0; j < hiddenSize; j++) b1[j] = rand.nextDouble() * 0.2 - 0.1;
        for (int j = 0; j < outputSize; j++) b2[j] = rand.nextDouble() * 0.2 - 0.1;
    }

    /** 前向传播,返回三个动作的得分 */
    public double[] predict(double[] inputs) {
        // 隐藏层
        double[] hidden = new double[hiddenSize];
        for (int j = 0; j < hiddenSize; j++) {
            double sum = b1[j];
            for (int i = 0; i < inputSize; i++) {
                sum += inputs[i] * w1[i][j];
            }
            hidden[j] = sigmoid(sum);
        }
        // 输出层
        double[] output = new double[outputSize];
        for (int k = 0; k < outputSize; k++) {
            double sum = b2[k];
            for (int j = 0; j < hiddenSize; j++) {
                sum += hidden[j] * w2[j][k];
            }
            output[k] = sum; // 原始logits,后续Softmax
        }
        return softmax(output);
    }

    private double sigmoid(double x) {
        return 1.0 / (1.0 + Math.exp(-x));
    }

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

    /** 将网络所有参数展平为一维数组,用于遗传编码 */
    public double[] getGenome() {
        int genomeSize = inputSize * hiddenSize + hiddenSize * outputSize + hiddenSize + outputSize;
        double[] genome = new double[genomeSize];
        int idx = 0;
        for (int i = 0; i < inputSize; i++) {
            for (int j = 0; j < hiddenSize; j++) {
                genome[idx++] = w1[i][j];
            }
        }
        for (int i = 0; i < hiddenSize; i++) {
            for (int j = 0; j < outputSize; j++) {
                genome[idx++] = w2[i][j];
            }
        }
        for (int j = 0; j < hiddenSize; j++) genome[idx++] = b1[j];
        for (int j = 0; j < outputSize; j++) genome[idx++] = b2[j];
        return genome;
    }

    /** 从一维基因组恢复网络参数 */
    public void setGenome(double[] genome) {
        int idx = 0;
        for (int i = 0; i < inputSize; i++) {
            for (int j = 0; j < hiddenSize; j++) {
                w1[i][j] = genome[idx++];
            }
        }
        for (int i = 0; i < hiddenSize; i++) {
            for (int j = 0; j < outputSize; j++) {
                w2[i][j] = genome[idx++];
            }
        }
        for (int j = 0; j < hiddenSize; j++) b1[j] = genome[idx++];
        for (int j = 0; j < outputSize; j++) b2[j] = genome[idx++];
    }

    public int getGenomeSize() {
        return inputSize * hiddenSize + hiddenSize * outputSize + hiddenSize + outputSize;
    }
}

三、环境感知:24维特征向量

蛇看不见整个棋盘,只能”感知”周围。8个方向各探测3个值(归一化到0~1),构成神经网络的输入。

/**
 * 环境传感器:为神经网络提取24维特征
 */
public class SnakeSensor {
    // 8个探测方向:上、右上、右、右下、下、左下、左、左上
    private static final int[][] DIRS = {
        {0, -1}, {1, -1}, {1, 0}, {1, 1},
        {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}
    };

    /**
     * 提取特征向量,长度固定24
     * 每方向3个值:到墙距离/最大距离、到身体距离/最大距离(无则1)、食物是否在此方向(0/1)
     */
    public double[] getFeatures(SnakeGame game) {
        double[] features = new double[24];
        int w = game.getWidth();
        int h = game.getHeight();
        int hx = game.getHeadX();
        int hy = game.getHeadY();
        int fx = game.getFoodX();
        int fy = game.getFoodY();

        for (int d = 0; d < 8; d++) {
            int dx = DIRS[d][0];
            int dy = DIRS[d][1];

            // 到墙的距离
            int distWall = 0;
            int cx = hx + dx;
            int cy = hy + dy;
            while (cx >= 0 && cx < w && cy >= 0 && cy < h) {
                distWall++;
                cx += dx;
                cy += dy;
            }
            double maxDist = Math.max(w, h);
            features[d * 3] = distWall / maxDist;

            // 到身体的距离
            int distBody = Integer.MAX_VALUE;
            cx = hx + dx;
            cy = hy + dy;
            int step = 0;
            while (cx >= 0 && cx < w && cy >= 0 && cy < h) {
                step++;
                if (game.isBody(cx, cy)) {
                    distBody = step;
                    break;
                }
                cx += dx;
                cy += dy;
            }
            features[d * 3 + 1] = (distBody == Integer.MAX_VALUE) ? 1.0 : distBody / maxDist;

            // 食物方向指示
            int foodDir = 0;
            if (fx != hx || fy != hy) {
                // 检查食物是否大致在此方向
                int fdx = Integer.compare(fx, hx);
                int fdy = Integer.compare(fy, hy);
                // 简化:只看正交4方向的食物相对位置
                if (d % 2 == 0) { // 正方向
                    if (dx == fdx && dy == fdy) foodDir = 1;
                }
            }
            features[d * 3 + 2] = foodDir;
        }
        return features;
    }
}

四、游戏引擎:蛇的移动与规则

import java.util.ArrayDeque;
import java.util.Deque;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;

/**
 * 贪吃蛇游戏引擎
 * 坐标系:左上角为(0,0),x向右,y向下
 */
public class SnakeGame {
    private final int width;
    private final int height;
    private final Deque<int[]> body = new ArrayDeque<>();
    private final Set<String> bodySet = new HashSet<>();
    private int foodX, foodY;
    private int direction; // 0=上,1=右,2=下,3=左
    private boolean alive = true;
    private int score = 0;
    private int movesLeft = 100; // 每吃一个食物重置,防止原地打转
    private final Random rand = new Random();

    public SnakeGame(int width, int height) {
        this.width = width;
        this.height = height;
        int sx = width / 2;
        int sy = height / 2;
        body.addLast(new int[]{sx, sy});
        bodySet.add(key(sx, sy));
        direction = 0;
        spawnFood();
    }

    private String key(int x, int y) {
        return x + "," + y;
    }

    private void spawnFood() {
        do {
            foodX = rand.nextInt(width);
            foodY = rand.nextInt(height);
        } while (bodySet.contains(key(foodX, foodY)));
    }

    /**
     * 执行动作:0=直行,1=右转,2=左转
     * 返回是否存活
     */
    public boolean step(int action) {
        if (!alive) return false;

        // 更新方向
        if (action == 1) direction = (direction + 1) % 4;      // 右转
        else if (action == 2) direction = (direction + 3) % 4; // 左转
        // action==0 直行,方向不变

        int dx = 0, dy = 0;
        switch (direction) {
            case 0: dy = -1; break;
            case 1: dx = 1; break;
            case 2: dy = 1; break;
            case 3: dx = -1; break;
        }

        int[] head = body.peekFirst();
        int nx = head[0] + dx;
        int ny = head[1] + dy;

        // 撞墙
        if (nx < 0 || nx >= width || ny < 0 || ny >= height) {
            alive = false;
            return false;
        }

        // 撞自己(排除尾部,因为会移动)
        String nk = key(nx, ny);
        if (bodySet.contains(nk)) {
            alive = false;
            return false;
        }

        // 移动
        body.addFirst(new int[]{nx, ny});
        bodySet.add(nk);

        if (nx == foodX && ny == foodY) {
            score += 10;
            movesLeft += 50; // 奖励额外步数
            spawnFood();
        } else {
            int[] tail = body.pollLast();
            bodySet.remove(key(tail[0], tail[1]));
            movesLeft--;
        }

        if (movesLeft <= 0) alive = false;
        return alive;
    }

    public int evaluateFitness() {
        //  fitness = 得分 * 100 + 移动步数 + 存活长度奖励
        int len = body.size();
        return score * 100 + Math.max(0, movesLeft) + len * len * 10;
    }

    public boolean isAlive() { return alive; }
    public int getHeadX() { return body.peekFirst()[0]; }
    public int getHeadY() { return body.peekFirst()[1]; }
    public int getFoodX() { return foodX; }
    public int getFoodY() { return foodY; }
    public int getWidth() { return width; }
    public int getHeight() { return height; }
    public boolean isBody(int x, int y) { return bodySet.contains(key(x, y)); }
    public int getSnakeLength() { return body.size(); }
}

五、遗传算法:选择、交叉与变异

核心思想:用神经网络的基因组作为”染色体”,fitness作为生存标准。

import java.util.Arrays;
import java.util.Comparator;
import java.util.Random;

/**
 * 遗传算法引擎
 */
public class GeneticAlgorithm {
    private final int populationSize;
    private final double mutationRate;
    private final double crossoverRate;
    private final Random rand = new Random();
    private final int genomeSize;

    private Individual[] population;

    public GeneticAlgorithm(int populationSize, int genomeSize,
                            double mutationRate, double crossoverRate) {
        this.populationSize = populationSize;
        this.genomeSize = genomeSize;
        this.mutationRate = mutationRate;
        this.crossoverRate = crossoverRate;
        this.population = new Individual[populationSize];
        for (int i = 0; i < populationSize; i++) {
            population[i] = new Individual(genomeSize);
        }
    }

    /** 评估种群中每个个体的适应度 */
    public void evaluate(SnakeGame prototype, int maxSteps) {
        for (Individual ind : population) {
            SnakeGame game = new SnakeGame(prototype.getWidth(), prototype.getHeight());
            NeuralNetwork nn = new NeuralNetwork(24, 12, 3);
            nn.setGenome(ind.genome);
            SnakeSensor sensor = new SnakeSensor();

            int steps = 0;
            while (game.isAlive() && steps < maxSteps) {
                double[] features = sensor.getFeatures(game);
                double[] probs = nn.predict(features);
                // 选择概率最大的动作
                int action = 0;
                if (probs[1] > probs[0] && probs[1] > probs[2]) action = 1;
                else if (probs[2] > probs[0] && probs[2] > probs[1]) action = 2;
                game.step(action);
                steps++;
            }
            ind.fitness = game.evaluateFitness();
        }
    }

    /** 生成下一代:保留精英 + 交叉 + 变异 */
    public void evolve() {
        Arrays.sort(population, Comparator.comparingInt((Individual a) -> a.fitness).reversed());

        Individual[] nextGen = new Individual[populationSize];
        int eliteCount = Math.max(1, populationSize / 10); // 保留前10%
        for (int i = 0; i < eliteCount; i++) {
            nextGen[i] = new Individual(population[i]);
        }

        for (int i = eliteCount; i < populationSize; i++) {
            Individual parent1 = tournamentSelect(3);
            Individual parent2 = tournamentSelect(3);
            Individual child = crossover(parent1, parent2);
            mutate(child);
            nextGen[i] = child;
        }
        population = nextGen;
    }

    /** 锦标赛选择:从k个随机个体中选最优 */
    private Individual tournamentSelect(int k) {
        Individual best = population[rand.nextInt(populationSize)];
        for (int i = 1; i < k; i++) {
            Individual cand = population[rand.nextInt(populationSize)];
            if (cand.fitness > best.fitness) best = cand;
        }
        return best;
    }

    /** 均匀交叉:逐基因随机选择来自哪个父代 */
    private Individual crossover(Individual p1, Individual p2) {
        Individual child = new Individual(genomeSize);
        for (int i = 0; i < genomeSize; i++) {
            child.genome[i] = (rand.nextDouble() < 0.5) ? p1.genome[i] : p2.genome[i];
        }
        return child;
    }

    /** 高斯变异:以mutationRate概率对每个基因添加小扰动 */
    private void mutate(Individual ind) {
        for (int i = 0; i < genomeSize; i++) {
            if (rand.nextDouble() < mutationRate) {
                ind.genome[i] += rand.nextGaussian() * 0.3;
                // 限制权重范围,防止数值爆炸
                ind.genome[i] = Math.max(-5.0, Math.min(5.0, ind.genome[i]));
            }
        }
    }

    public Individual getBest() {
        return Arrays.stream(population)
                     .max(Comparator.comparingInt(a -> a.fitness))
                     .orElse(population[0]);
    }

    public double getAverageFitness() {
        return Arrays.stream(population).mapToInt(a -> a.fitness).average().orElse(0);
    }

    static class Individual {
        double[] genome;
        int fitness;

        Individual(int size) {
            this.genome = new double[size];
            Random r = new Random();
            for (int i = 0; i < size; i++) genome[i] = r.nextDouble() * 2 - 1;
        }

        Individual(Individual other) {
            this.genome = other.genome.clone();
            this.fitness = other.fitness;
        }
    }
}

六、主程序:进化循环

/**
 * 贪吃蛇神经进化主程序
 */
public class SnakeNeuroevolution {
    public static void main(String[] args) {
        final int POP_SIZE = 200;
        final int GENOME_SIZE = new NeuralNetwork(24, 12, 3).getGenomeSize();
        final int GENERATIONS = 500;
        final int BOARD_W = 10;
        final int BOARD_H = 10;
        final int MAX_STEPS_PER_GAME = 200;

        GeneticAlgorithm ga = new GeneticAlgorithm(POP_SIZE, GENOME_SIZE, 0.05, 0.7);
        SnakeGame prototype = new SnakeGame(BOARD_W, BOARD_H);

        System.out.println("=== 贪吃蛇神经进化开始 ===");
        System.out.println("种群大小: " + POP_SIZE);
        System.out.println("基因组长度: " + GENOME_SIZE);
        System.out.println("变异率: 5%, 交叉率: 70%");

        for (int gen = 1; gen <= GENERATIONS; gen++) {
            ga.evaluate(prototype, MAX_STEPS_PER_GAME);
            GeneticAlgorithm.Individual best = ga.getBest();
            double avg = ga.getAverageFitness();

            if (gen % 20 == 0 || gen == 1) {
                System.out.printf("Generation %4d | Best Fitness: %6d | Avg Fitness: %8.1f%n",
                    gen, best.fitness, avg);
            }

            ga.evolve();
        }

        // 展示最优个体
        System.out.println("\n=== 最优策略演示 ===");
        GeneticAlgorithm.Individual champion = ga.getBest();
        runDemo(champion.genome, BOARD_W, BOARD_H);
    }

    private static void runDemo(double[] genome, int w, int h) {
        SnakeGame game = new SnakeGame(w, h);
        NeuralNetwork nn = new NeuralNetwork(24, 12, 3);
        nn.setGenome(genome);
        SnakeSensor sensor = new SnakeSensor();

        int steps = 0;
        while (game.isAlive() && steps < 500) {
            double[] feat = sensor.getFeatures(game);
            double[] probs = nn.predict(feat);
            int action = 0;
            if (probs[1] > probs[0] && probs[1] > probs[2]) action = 1;
            else if (probs[2] > probs[0] && probs[2] > probs[1]) action = 2;
            game.step(action);
            steps++;
        }
        System.out.println("演示结束 | 最终长度: " + game.getSnakeLength() + " | 存活步数: " + steps);
    }
}

七、关键技巧:加速收敛的三个工程细节

1. 步数惩罚与奖励重置

引入movesLeft机制:每吃一个食物奖励额外步数,耗尽则死亡。这强制蛇必须积极寻找食物,而不是在安全区域无限绕圈。

2. 锦标赛选择替代轮盘赌

使用3人锦标赛选择而非轮盘赌,避免了早期超级个体垄断基因池的问题,保持种群多样性。

3. 权重截断与高斯变异

将权重限制在[-5, 5]范围内,配合N(0, 0.3)的高斯变异,既保证搜索空间足够大,又避免数值爆炸导致Sigmoid饱和。

八、复杂度分析

指标 复杂度 说明
神经网络前向 O(I·H + H·O) I=24输入, H=12隐藏, O=3输出
单局模拟 O(S·(I·H + H·O)) S为最大步数
单代评估 O(P·S·(I·H + H·O)) P为种群大小
基因组长度 O(I·H + H·O + H + O) 约24×12+12×3+12+3=327
存储空间 O(P·G) G为基因组长度

九、扩展方向

  1. 引入精英保存与多样性维护:使用物种形成(Speciation)或新颖性搜索(Novelty Search),防止种群过早收敛到局部最优。
  2. 改进网络结构:尝试循环神经网络(RNN/LSTM),让蛇”记住”之前的路径,避免重复绕圈。
  3. 多目标进化:同时优化”吃食物速度”和”存活时间”,获得更稳健的策略。
  4. 可视化进化过程:用JavaFX或Swing渲染每一代最优蛇的行为,直观观察策略演变。