1. 项目概述:用进化算法训练贪吃蛇AI
在传统游戏AI开发中,我们通常会手动编写规则或使用搜索算法(如A*)来指导AI行为。但这种方法存在明显局限——开发者需要预先设想所有可能场景,并为AI设计对应的应对策略。而今天我们要尝试一种截然不同的思路:让AI从零开始自主进化出游戏策略。
这个项目使用Python实现了基于NEAT(NeuroEvolution of Augmenting Topologies)算法的贪吃蛇AI。NEAT是一种进化神经网络的方法,它通过模拟自然选择的过程,让神经网络的结构和权重同时进化。与深度学习不同,NEAT不需要预先设计网络结构,也不依赖反向传播,而是让网络在解决任务的过程中自主优化。
关键优势:NEAT能够自动发现适合特定任务的网络拓扑结构,开发者只需定义输入输出和适应度函数,无需手动设计网络架构。
2. 核心原理与技术选型
2.1 NEAT算法工作机制
NEAT算法的核心思想是模拟生物进化过程,通过以下机制实现神经网络进化:
- 基因编码:每个神经网络被编码为一个基因组,包含节点基因和连接基因
- 变异操作:
- 权重突变:小幅调整连接权重
- 结构突变:添加新节点或新连接
- 激活函数突变:改变节点的激活函数
- 交叉操作:两个亲本基因组重组产生后代
- 物种形成:根据基因组相似度将个体分组,保护创新结构
python复制# NEAT基本工作流程示例
population = initialize_population()
for generation in range(max_generations):
fitnesses = evaluate(population)
new_population = []
for _ in range(pop_size):
parent1, parent2 = select_parents(population, fitnesses)
child = crossover(parent1, parent2)
child = mutate(child)
new_population.append(child)
population = speciate(new_population)
2.2 为什么选择NEAT而非传统方法
与传统贪吃蛇AI实现方式相比,NEAT具有独特优势:
| 方法 | 需要手动设计 | 适应新场景 | 策略多样性 | 实现复杂度 |
|---|---|---|---|---|
| 硬编码规则 | 高 | 低 | 低 | 中 |
| 搜索算法(A*/BFS) | 中 | 中 | 低 | 高 |
| 强化学习 | 低 | 高 | 中 | 高 |
| NEAT | 低 | 高 | 高 | 中 |
NEAT特别适合贪吃蛇这类问题,因为:
- 状态空间相对较小但策略空间大
- 需要平衡短期收益(吃食物)和长期生存
- 优秀策略往往有多个局部最优解
3. 游戏环境设计
3.1 平行宇宙架构
为确保进化过程的公平性,我们采用了"平行宇宙"设计:
python复制# 每条蛇有独立的食物实例
snakes = [Snake() for _ in range(pop_size)]
foods = [Food() for _ in range(pop_size)]
这种设计消除了以下干扰因素:
- 食物竞争导致的不公平
- 随机性对适应度评估的影响
- 个体间的直接干扰
3.2 精确的碰撞检测系统
贪吃蛇的核心机制依赖精确的碰撞检测,我们实现了三重保障:
- 网格对齐:所有对象位置必须是BLOCK_SIZE(20像素)的整数倍
python复制# 食物生成确保对齐网格
self.pos = (
random.randint(0, (WIDTH - BLOCK_SIZE) // BLOCK_SIZE) * BLOCK_SIZE,
random.randint(0, (HEIGHT - BLOCK_SIZE) // BLOCK_SIZE) * BLOCK_SIZE
)
- 防重叠机制:新食物不会出现在蛇身上
python复制while True:
new_food = Food()
if new_food.pos not in snake.body:
foods[x] = new_food
break
- 死亡判定:
- 撞墙:头部坐标超出边界
- 自噬:头部与身体任何部分重合
- 饿死:步数计数器归零
3.3 适应度函数设计
适应度函数引导进化方向,我们的设计包含三个关键要素:
- 基础生存奖励:每存活一步+0.1分
- 进食奖励:每次吃食物+10分
- 死亡惩罚:每次死亡-2分
这种设计鼓励AI:
- 尽可能长时间存活
- 主动寻找食物
- 避免无意义的移动
4. AI感知与决策系统
4.1 八向雷达感知系统
贪吃蛇AI的输入是24维向量,来自8个方向的雷达扫描:
python复制def get_state(snake, food):
state = []
directions = [
(0, -BLOCK_SIZE), (0, BLOCK_SIZE), # 上下
(-BLOCK_SIZE, 0), (BLOCK_SIZE, 0), # 左右
(-BLOCK_SIZE, -BLOCK_SIZE), # 左上
(-BLOCK_SIZE, BLOCK_SIZE), # 左下
(BLOCK_SIZE, -BLOCK_SIZE), # 右上
(BLOCK_SIZE, BLOCK_SIZE) # 右下
]
for dir_x, dir_y in directions:
dist_to_wall = 0
dist_to_food = 0
dist_to_body = 0
# ... 扫描实现 ...
state.extend([dist_to_wall, dist_to_food, dist_to_body])
return state
每个方向提供三个信息:
- 到墙壁的距离(归一化值)
- 该方向是否有食物(布尔值)
- 到自身身体的距离(归一化值)
4.2 神经网络架构与决策
NEAT自动演化网络结构,基本流程如下:
- 初始种群使用全连接网络(无隐藏层)
- 通过变异可能添加:
- 新的隐藏节点
- 新的连接
- 调整连接权重
- 输出层固定4个节点,对应上下左右移动
决策过程:
python复制output = net.activate(state)
action = output.index(max(output)) # 选择激活值最高的方向
4.3 进化参数配置
NEAT算法的行为由配置文件控制,关键参数包括:
ini复制[NEAT]
pop_size = 50 # 每代个体数
fitness_threshold = 3000 # 终止阈值
[DefaultGenome]
node_add_prob = 0.2 # 添加节点概率
conn_add_prob = 0.5 # 添加连接概率
weight_mutate_rate = 0.8 # 权重变异概率
[DefaultStagnation]
max_stagnation = 20 # 最大停滞代数
5. 完整实现与优化技巧
5.1 项目结构
code复制snake_ai/
├── config-feedforward.txt # NEAT配置文件
├── main.py # 主程序
├── requirements.txt # 依赖库
5.2 核心代码实现
游戏主循环的关键逻辑:
python复制while run and len(snakes) > 0:
# 处理事件
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
# 更新每条蛇的状态
for x in range(len(snakes)-1, -1, -1):
snake = snakes[x]
state = get_state(snake, foods[x])
output = nets[x].activate(state)
# 根据输出选择动作
action = output.index(max(output))
# ... 方向更新 ...
snake.move()
# 适应度计算
if snake.alive:
ge[x].fitness += 0.1
if snake.body[0] == foods[x].pos:
ge[x].fitness += 10
# ... 食物更新 ...
else:
ge[x].fitness -= 2
# ... 移除死亡个体 ...
# 绘制界面
screen.fill((20, 20, 20))
# ... 绘制逻辑 ...
pygame.display.update()
5.3 性能优化技巧
-
加速进化:
- 设置
clock.tick(0)取消帧率限制 - 减小窗口尺寸降低渲染开销
- 关闭可视化仅记录适应度
- 设置
-
提高成功率:
- 初始步数设为150(
steps_left) - 吃到食物奖励100步
- 死亡惩罚适度(-2分)
- 初始步数设为150(
-
避免局部最优:
- 适当提高变异率
- 保持足够种群多样性
- 使用物种保护机制
6. 典型问题与解决方案
6.1 常见运行错误
-
配置文件错误:
- 症状:
KeyError或ConfigError - 解决:确保使用提供的完整配置文件
- 特别注意:新版neat-python要求参数更严格
- 症状:
-
蛇不进食:
- 检查食物生成是否对齐网格
- 验证碰撞检测逻辑
- 确保适应度函数给予足够进食奖励
-
进化停滞:
- 增加种群大小
- 调整变异概率
- 检查适应度函数是否合理
6.2 调试技巧
- 可视化感知输入:
python复制print(f"State vector: {state}") # 查看24维输入值
- 记录进化历史:
python复制p.add_reporter(neat.StatisticsReporter())
p.add_reporter(neat.Checkpointer(5)) # 每5代保存检查点
- 分析优秀个体:
python复制winner = p.run(eval_genomes, 1000)
print(f"Best genome:\n{winner}") # 输出最佳基因组
7. 进阶改进方向
7.1 输入特征增强
当前24维输入可以扩展为:
- 蛇身长度
- 当前移动方向
- 上次进食后的步数
- 周围区域的食物密度
7.2 适应度函数优化
更精细的奖励设计:
python复制# 基于效率的奖励
fitness += (food_eaten / steps_taken) * 10
# 路径平滑惩罚
if direction_changed:
fitness -= 0.05
7.3 环境变化挑战
- 动态障碍物:随机出现的障碍物
- 移动食物:食物会周期性移动
- 毒食物:部分食物会扣分
7.4 混合训练方法
结合NEAT与其他技术:
- NEAT+Q-learning
- 预训练+微调
- 集成多个进化策略
8. 项目实践建议
-
硬件准备:
- CPU性能影响进化速度
- 不需要GPU加速
- 内存需求较低(约100MB)
-
参数调优顺序:
- 先确定合适的种群大小(通常50-100)
- 调整变异率(0.05-0.3)
- 优化适应度函数
- 最后调整网络结构参数
-
典型进化过程观察:
- 前10代:随机移动
- 10-30代:基本趋食行为
- 30-100代:简单避障
- 100+代:复杂策略形成
-
停止条件设定:
- 最大代数(1000)
- 适应度阈值(3000)
- 手动干预(观察到满意策略)
在实际操作中,我发现设置适当的饿死机制(steps_left)对防止蛇陷入无限循环至关重要。初期可以设置较小的初始步数(如100),随着AI进步逐步增加难度。另一个实用技巧是在适应度函数中加入移动效率奖励,鼓励AI用最少步数获取最多食物。