不靠深度学习,用NEAT神经进化让AI学会玩超级马里奥
2026/9/12 11:25:30 网站建设 项目流程

AI学习玩马力欧,这个实验因为 Code Bullet 的视频被很多人认识和讨论过:一个智能体从完全不懂规则开始,通过不断尝试,慢慢学会起跳、躲坑、踩敌人,最终走完关卡。Code Bullet 使用的核心方法并不是大众印象里的深度学习,而是神经进化算法 NEAT。这个项目之所以适合拿来学习,是因为它能在单个 Python 脚本里完整展示状态感知、决策模型、奖励评估、存档加载的闭环,也可以作为进入强化学习、AI Agent 开发和本地模型部署的入门项目。

这篇博客会按一条可复现的路径展开:先说 NEAT 为什么适合这个任务,再准备 Python 环境和模拟器,然后定义状态空间和动作空间,接着写出适应度评估和训练主循环,最后回放结果并处理典型问题。整个流程在本地电脑上就能跑,不需要 GPU,也不需要调用云端 API。如果你对“AI 为什么能学会玩游戏”这件事感兴趣,或者想把一个 AI 训练 demo 改造成可维护的工程实验,这篇内容会给你一条相对完整的参考线。

1. 为什么选择 NEAT 来训练马里奥 AI

1.1 规则脚本的死在细节爆炸

最直观的玩法是手写规则:看到坑就跳,看到敌人就躲。第一关前半段确实能靠几行规则跑通,但越往后越难维护。马里奥的关卡里包含管道、金币、乌龟、蘑菇、悬崖、问号砖,每个对象在不同速度、不同高度、不同状态下都会产生不同行为。规则脚本很快会变成几百个 if 分支,而且这些分支之间互相影响,改一个条件可能弄坏另一段逻辑。

更麻烦的是,规则脚本无法处理“没见过的状态”。如果模型只学会了“前方有障碍就跳跃”,遇到敌人从背后出现、砖块上方有隐藏金币这种组合情况,就会做出看起来合理、实际无效的决策。这种现象和当前 AI 领域常说的“AI 幻觉”有相似之处:模型不是真的理解场景,而是在用局部经验强行生成一个结果。规则脚本本质上就是这个问题的极端版本。

1.2 NEAT 的通俗定义和技术原理

NEAT 全称是 NeuroEvolution of Augmenting Topologies,翻译过来是“增强拓扑结构的神经进化”。它属于进化算法和神经网络的结合体。

通俗地理解,可以把每个候选模型看成一只“生物”,它的神经网络结构就是基因。不同“生物”之间可以交叉、变异。交叉会把两个表现不错的模型混合起来,变异会让某个权重、某个连接或者某个神经元发生变化。每一轮进化结束后,系统根据表现打分,分数高的“生物”更容易留下后代,分数低的则被淘汰。

和普通神经网络训练不同,NEAT 不依赖梯度反向传播,也不依赖损失函数。它只依赖一个评估函数:谁在当前任务里表现好,谁的基因就更容易被保留。这个评估函数就是适应度函数,也就是 fitness。

NEAT 的一个重要特点是它同时优化网络结构和权重。经典神经网络通常需要人工决定多少层、每层多少神经元,而 NEAT 的初始网络可以很小时,进化过程中自动增加隐层神经元、自动增加连接,逐步把网络“改”得适合当前任务。

1.3 NEAT 在游戏控制里的优势

超级马里奥的原始画面是 240×256×3,如果直接把这个高维像素矩阵作为输入,NEAT 的输入节点会非常多,交叉和变异空间也会变得巨大,最终训练效率很低。但这个任务有一个天然优势:我们不需要让模型理解整张图片,只需要让模型根据一小部分关键特征做出决策。

NEAT 正好适合这种低维输入、离散动作输出的控制问题。它有几个明显优点:

  • 不需要 GPU。模型规模较小,CPU 上就能完成推理和进化。
  • 不需要存储经验回放。NEAT 每一代只评估模型,不保存大量历史样本。
  • 可解释性较强。训练完成后的网络结构可以直接导出,看哪些输入节点对输出影响大。
  • 对不连续决策问题容忍度高。动作输出只要梯度不明感也能进化,不依赖可导函数。

1.4 与强化学习和规则脚本的对比

方案是否需要 GPU样本效率可解释性实现复杂度适合场景
规则脚本状态有限的简单任务
NEAT 神经进化低维状态、离散动作、中小规模环境
DQN 深度 Q 学习推荐高维状态、连续控制、复杂任务
PPO 策略梯度推荐更稳定的深度强化学习训练

注意,NEAT 并不是万能方案。如果关卡规模变大、背景变复杂,手工特征很难覆盖所有信息,NEAT 的优势会下降。此时更适合转入深度强化学习,让 CNN 自动从像素中提取特征。

2. 复现前先准备环境和依赖

2.1 技术栈选型

复现一个 AI 玩马里奥项目,通常需要四类组件:

  1. 马里奥模拟器:可以使用gym-super-mario-bros,它是 OpenAI Gym 风格的封装。
  2. 神经进化算法库:使用neat-python
  3. 图像处理和降维:可以使用opencv-python,也可以只用 NumPy 做简单处理。
  4. 环境渲染和辅助:使用pygamematplotlib保存结果。

建议在 Python 3.9 到 3.11 的版本下操作。Python 3.12 及以上时,部分依赖包可能还没有预编译版本,需要本地编译,容易出现安装问题。这里给出的示例配置是常见组合,不是所有机器的最优解,落地前需要结合自己的系统版本确认兼容性。

2.2 创建虚拟环境并安装依赖

首先创建独立虚拟环境,避免污染系统 Python。

python -m venv .venv source .venv/bin/activate

Windows 系统下激活命令不同:

.venv\Scripts\activate

然后升级 pip 并安装依赖包。

pip install --upgrade pip pip install numpy==1.26.4 pip install gymnasium==0.29.1 pip install gym-super-mario-bros==7.4.0 pip install neat-python==0.92 pip install pygame==2.5.2 pip install opencv-python==4.9.0.80

如果自动安装失败,可以先安装能匹配当前 Python 版本的最新版本,再逐步降级。NEAT 和模拟器这类库对版本并不是非常敏感,但 gym 与 gymnasium 的接口差异必须注意。

依赖安装完成后,可以先运行一个简单脚本,确认环境能加载。

import gym_super_mario_bros from nes_py.wrappers import JoypadSpace env = gym_super_mario_bros.make('SuperMarioBros-1-1-v0') env = JoypadSpace(env, [['NOOP'], ['right'], ['right', 'B']]) state, info = env.reset() print('state shape:', state[0].shape if isinstance(state, tuple) else state.shape) print('info keys:', info.keys() if isinstance(info, dict) else 'no info') env.close()

这里有一个细节:新版 gymnasium 的reset()方法返回(state, info)两个值,旧版 OpenAI gym 的接口也类似。如果你的版本不同,返回值可能只是单个 state。看到state shape输出正常,就说明模拟器已经可以工作。

2.3 环境自检:跑一个随机动作智能体

在开始训练前,跑一个随机动作基线,确认环境本身没问题,同时也能知道纯随机策略能达到什么水平。

import random import gym_super_mario_bros from nes_py.wrappers import JoypadSpace from gymnasium.wrappers import GrayScaleObservation, ResizeObservation env = gym_super_mario_bros.make('SuperMarioBros-1-1-v0') env = JoypadSpace(env, [['NOOP'], ['right'], ['right', 'B']]) state, info = env.reset() total_reward = 0 for step in range(200): action = random.choice([0, 1, 2]) state, reward, terminated, truncated, info = env.step(action) total_reward += reward if terminated or truncated: break print('random total reward:', total_reward) env.close()

随机策略通常会在很短时间掉进坑里,这很正常。这个基线能帮你确认环境是否正常。

2.4 常见安装坑

问题现象常见原因检查方式处理建议
安装 gym-super-mario-bros 时报编译错误Python 版本过高,缺少预编译包查看 pip 报错末尾的编译信息切换 Python 3.9/3.10/3.11
运行渲染时出现黑屏缺少图形环境或 pygame 版本不兼容打印环境创建日志更新 pygame 或改用 headless 模式
reset 返回数量不对gym 和 gymnasium 接口混用打印env.reset()返回值统一使用 gymnasium API

3. 定义状态空间和动作空间

3.1 为什么不能直接把整张图片丢给 NEAT

NEAT 的输入节点数量直接影响搜索空间大小。原始画面是 240×256×3,即使简化成灰度图,也有 240×256=61440 个像素。如果每个像素作为一个输入节点,神经网络的权重数量会暴增,进化效率会显著下降,因为绝大多数连接都对任务没有直接帮助。

比较常见的做法是降维。两种思路比较常用:

  1. 图像缩略图:把原始画面缩放到很小的尺寸,比如 12×12 或 16×16,每个像素作为输入。
  2. 手工提取特征:检测马里奥的当前位置、前方障碍物距离、是否处于跳跃状态等,通常只需要 10 到 30 个输入。

NEAT 对低维特征更友好。手工特征虽然需要花时间设计,但训练效率会高很多。图像缩略图实现简单,但信息损失大,模型容易学到“局部纹理”而不是“全局路径”。

3.2 动作空间离散化

超级马里奥的原始动作有很多个按键组合。为了减少搜索空间,通常只保留几个关键动作。

动作编号按键组合含义适用场景
0NOOP不操作观察环境,暂停
1right向右走平地移动
2right + B向右跑加速前进
3right + A向右跳跳过障碍和坑
4right + A + B向右跑跳更远距离跳跃

动作数量越少,NcAT 在输出层需要学习的映射关系越简单。如果你发现动作 2 和动作 4 经常导致角色撞墙,可以动态调整动作组合。

3.3 特征提取示例代码

下面这段代码只用来表达结构,具体阈值和坐标需要根据你的模拟器版本和图像颜色来调整。它会把彩色画面转换成灰度图,再缩放成 12×12,并追加一个角色在画面中的大致水平位置信息。

import cv2 import numpy as np def extract_features(obs): # obs 是模拟器返回的画面,通常是 (240, 256, 3) if isinstance(obs, tuple): obs = obs[0] gray = cv2.cvtColor(obs, cv2.COLOR_RGB2GRAY) small = cv2.resize(gray, (12, 12), interpolation=cv2.INTER_AREA) features = small.flatten() / 255.0 # 追加一个简单统计量:画面下半部分的平均亮度,用于近似判断地面状态 bottom_mean = gray[180:240, :].mean() / 255.0 features = np.append(features, bottom_mean) return features

这里输入维度是 144+1=145,比 6 万个像素小得多。实际训练时,这个特征仍然可能丢失“马里奥当前是否踩到敌人”“前方是否有金币”等关键信息。更好的做法是继续补充特征,比如检测角色 bounding box、前方 10 列像素是否有非天空色障碍等。

3.4 特征设计和训练效果的关系

特征设计决定了模型的信息边界,而这直接决定了训练上限。

如果特征里没有“前方是否有坑”,模型只能靠“画面底部亮度”猜测地形,就很难学会在坑前跳跃。如果特征里没有“角色 y 坐标”,模型无法知道自己是否已经在空中,容易重复起跳,导致角色落地前按不出第二次起跳。

因此,在开始训练前,最好先画一张信息表:每个特征是什么、从哪里获取、对决策有什么用。

特征获取方式对决策的意义
角色水平位置图像颜色检测或模拟器 info判断是否前进
角色垂直位置图像颜色检测或模拟器 info判断是否在空中
前方障碍物距离扫描角色前方像素列决定何时起跳
前方是否有敌人检测特定颜色区域决定是否跳跃躲避
画面底部平均亮度灰度图统计粗略判断地面状态

这些特征不一定都能从info字典里直接拿到。gym-super-mario-bros 的信息字段会因版本不同而变化,建议先打印info看看有哪些可用键,再决定是自己做图像检测还是直接读 info。

4. 使用 NEAT 训练马里奥 AI

4.1 Genome 输入输出定义

neat-python里,一个 genome 代表一个神经网络。输入节点数量和特征向量的维度一致,输出节点数量和动作数量一致。

创建网络时使用neat.nn.FeedForwardNetwork.create(genome, config),然后把特征数组传入activate()方法,得到动作概率向量。

import neat import numpy as np def action_from_network(net, obs): features = extract_features(obs) output = net.activate(features) action_index = int(np.argmax(output)) return action_index

如果动作空间是 5 个,output就是长度为 5 的数组。argmax会取最大值的索引作为动作编号。

4.2 fitness 函数设计

fitness 是进化算法的唯一反馈信号,它的设计质量直接决定了模型能学到什么。最简单的方法是使用关卡内 x 坐标作为指标:模型走得越远,fitness 越高。

但只用 x 坐标有一个问题:模型可能只在开局附近左右抖动,x 坐标没有明显增长,但只要没有死亡,这个 episode 就不会结束。为了提升学习效率,通常加上时间限制和距离变化惩罚。

def evaluate_single_genome(genome, genome_id, config, env, max_steps=3000): net = neat.nn.FeedForwardNetwork.create(genome, config) obs, info = env.reset() total_fitness = 0.0 previous_x = 0 previous_y = 0 for _ in range(max_steps): action = action_from_network(net, obs) obs, reward, terminated, truncated, info = env.step(action) cur_x = info.get('x_pos', 0) cur_y = info.get('y_pos', 0) # 主要前进奖励 total_fitness += cur_x - previous_x # 给跳跃一点点奖励,鼓励模型尝试互动 if cur_y < previous_y: total_fitness += 0.1 previous_x = cur_x previous_y = cur_y if terminated or truncated: break return total_fitness

这里用x_pos能否从 info 获取取决于环境实现,不保证所有版本都有。如果没有,可以退回到用reward累计。

为了避免模型“反复原地跳跃”欺骗奖励,可以在 episode 结束时检查本次最大 x 和初始 x 的差值,如果差值过小,就对 fitness 乘一个惩罚系数。

if final_x - start_x < 30: total_fitness *= 0.5

这样模型只原地抖动时,即使环境不结束,它的 fitness 也会被压缩。

4.3 NEAT 配置项说明

neat-python使用配置文件来控制进化过程。一个简化版配置如下:

[NEAT] fitness_criterion = max fitness_threshold = 3000 pop_size = 50 reset_on_extinction = False [DefaultGenome] activation_default = tanh activation_options = tanh, relu, sigmoid num_hidden = 6 max_neurons = 30 weight_max_value = 3.0 bias_max_value = 3.0 compatibility_threshold = 3.0 [DefaultSpeciesSet] compatibility_threshold = 3.0 [DefaultStagnation] species_fitness_func = max max_stagnation = 15 [DefaultReproduction] elitism = 2 survival_threshold = 0.2

关键参数含义如下:

参数作用调大影响调小影响
pop_size每代个体数量搜索更全面,但速度更慢训练快,但容易早熟,陷入局部最优
fitness_threshold达到该 fitness 即停止需要更长时间训练可能提前停止,欠拟合
num_hidden初始各网络的隐藏节点数量起始模型更强,但进化空间变小起始模型弱,但组合可能更丰富
max_neurons单个网络最大神经元数允许更复杂模型限制表达能力
compatibility_threshold物种分界阈值物种更少,更激进物种更多,多样性更强
survival_threshold每代保留比例淘汰少,多样性高但进化慢淘汰多,进化快但容易退化

不需要一开始就追求最好的参数。建议先保持默认,能跑通以后,再针对“训练不收敛”和“速度太慢”两个方向单独调整。

4.4 训练主循环

neat-pythonPopulation会自动处理物种、交叉、变异逻辑,我们只需要提供 fitness 评估函数。

import neat import pickle def eval_genomes(genomes, config): global env for genome_id, genome in genomes: genome.fitness = evaluate_single_genome(genome, genome_id, config, env) def run_training(config_file): config = neat.Config( neat.DefaultGenome, neat.DefaultReproduction, neat.DefaultSpeciesSet, neat.DefaultStagnation, config_file ) population = neat.Population(config) population.add_reporter(neat.StdOutReporter(True)) population.add_reporter(neat.StatisticsReporter()) winner = population.run(eval_genomes, n=30) with open('winner.pkl', 'wb') as f: pickle.dump(winner, f) return winner

这里n=30表示直接跑 30 代。如果 30 代看不到明显进步,可以增大到 100 代。每个 episode 的最大步数也要控制,否则训练一局可能花很久。

如果机器有多个 CPU 核心,可以使用neat.parallel.ParallelEvaluator并行评估。需要注意 Windows 环境下多进程启动方式受限,容易反复创建子进程导致速度变慢甚至异常。

from neat.parallel import ParallelEvaluator def evaluate_parallel(genome, config, env): return evaluate_single_genome(genome, 0, config, env) parallel_evaluator = ParallelEvaluator(4, evaluate_parallel) population.run(parallel_evaluator.evaluate, n=30)

并行评估时,每个 worker 需要独立创建环境,避免把同一个环境对象传给多个进程。

4.5 保存和加载最佳基因组

训练结束后,population.run()返回的 winner 就是一个最优基因组。保存后用下面代码加载并回放。

import neat import pickle import gym_super_mario_bros from nes_py.wrappers import JoypadSpace with open('winner.pkl', 'rb') as f: winner = pickle.load(f) config = neat.Config( neat.DefaultGenome, neat.DefaultReproduction, neat.DefaultSpeciesSet, neat.DefaultStagnation, 'neat_config.ini' ) net = neat.nn.FeedForwardNetwork.create(winner, config) env = gym_super_mario_bros.make('SuperMarioBros-1-1-v0') env = JoypadSpace(env, [['NOOP'], ['right'], ['right', 'B']]) obs, info = env.reset() for _ in range(5000): action = action_from_network(net, obs) obs, reward, terminated, truncated, info = env.step(action) env.render() if terminated or truncated: break env.close()

如果保存时用的是不同的配置文件,加载时一定要使用同样的配置对象,否则网络结构可能无法还原。

5. 运行训练并评估结果

5.1 启动训练时观察什么

运行训练脚本后,控制台会输出类似下面的信息:

Generation 1: 500 individuals, best fitness 12.4, avg fitness 3.1 Generation 2: 500 individuals, best fitness 25.8, avg fitness 5.2

这里最重要的两个数值是最优 fitness 和平均 fitness。最优 fitness 稳步上升说明模型在进步;平均 fitness 上升说明整个种群都在进步,多样性保持得不错。

如果出现 fitness 长期停滞,前几代上升后就不再变化,通常是以下几种原因:

  • 特征信息不足,模型无法区分“安全前进”和“即将掉坑”。
  • fitness 中奖励曲线太平滑,原地不动也能拿分。
  • 种群缺少多样性,所有个体都收敛到一个局部最优。

5.2 日志落盘

生产环境里不会只盯着控制台输出。建议把每代最优 fitness、平均 fitness、species 数量、训练时长写入 CSV 或 JSON。

import csv import time with open('training_log.csv', 'w', newline='') as f: writer = csv.writer(f) writer.writerow(['generation', 'best_fitness', 'avg_fitness', 'species_count', 'elapsed']) def reporter(genome, config): with open('training_log.csv', 'a', newline='') as f: ...

实际项目里可以用自定义 Reporter 实现,不需要在 fitness 函数里维护全局状态。这样做的好处是训练结束后可以快速画图,观察 fitness 曲线是否收敛。

5.3 评估结果:什么时候算训练成功

不要只看“能走多远”。一个模型如果卡在开局的坑前,它的视频看起来像是在犹豫,但训练日志可能显示 fitness 很高,因为它通过反复跳跃获得了跳跃奖励。

建议用三个指标评估:

  1. 最大 x 坐标:是否通过了第一根管道、第一个坑、甚至到达终点。
  2. 完成时间:是否在有限步数内到达终点,还是靠大量重复动作硬磨过去。
  3. 行为稳定性:重复 10 次相同评估,每次的 x 坐标是否接近。

第一次训练往往不会有太惊艳的结果,能越过第一个砖块已经算是有效学习。Code Bullet 视频里的成功不是一蹴而就,而是大量调试特征和 fitmes 之后得到的结果。

5.4 结果中的“AI 幻觉”现象

训练过程中你可能会看到模型做出一些看起来非常奇怪的行为:站在坑前快速抖动、对着墙壁反复按跳、在敌人经过时选择原地不动而不是跳过去。这种行为和 LLM 生成虚假信息时的“AI 幻觉”本质上是类似的:模型在优化 fitness 的过程中找到了一个没想到的“捷径”,而这条捷径在人类视角下并不合理。

出现这种情况时,不要急着换算法,先检查 fitness 是不是给了模型空子。比如你为跳跃加了奖励,模型就会一直跳;你为 x 坐标增加奖励,但没限制时间,模型就会站在原地慢慢蹭。修正 fitness 往往比更换拓扑结构更有效。

6. 常见问题排查

6.1 训练不收敛,fitness 一直停留在个位数

现象:训练了 20 代以上,最优 fitness 仍在 10 以下,角色几乎一出生就死。

可能原因:

  • 特征向量全部近似为零,模型得不到有效输入。
  • 动作空间太大,模型随机探索不到有效动作。
  • 每代评估次数太少,导致随机性过大。

检查方式:

  • 打印extract_features(obs)的输出,确认特征不是全零或全一。
  • 手动控制动作,比如固定向右走,确认模拟器能正常前进。
  • 查看每个动作在随机策略下的平均生命周期,如果动作 1 和 2 都很快死,说明问题在环境本身。

解决建议:

  • 先从最简动作集['NOOP', 'right']开始,让模型先学会前进。
  • fitness 函数先只依赖info中的 x 坐标,不要混入太多奖励项。
  • 增大pop_size,比如从 50 调整到 200,同时增加代际数。

6.2 训练速度太慢,一局几千步要跑很久

现象:训练一局需要数秒甚至数十秒,无法接受。

可能原因:

  • episode 最大步数设置得太高,模型死亡前会一直原地抖动。
  • 渲染被打开,每次 step 都要刷新画面。
  • 没有开启并行评估,CPU 核心没有充分利用。

检查方式:

  • 训练过程中确认没有调用env.render()
  • 打印当前 episode 实际步数,看是否大多数都抵达了max_steps上限。

解决建议:

  • max_steps从 5000 降到 2000 或 1000。
  • 在 fitness 中增加“原地惩罚”,让长时间不前进的 episode 提前终止。
  • 使用ParallelEvaluator并行评估多个个体。
  • 如果发现很多个体存活时间过长,可以在 fitness 中扣减时间成本。

6.3 卡在墙边或无限跳跃

现象:模型学会了跳跃,但总是在同一面墙或同一个管道前反复跳,无法前进。

可能原因:

  • 跳跃奖励过多,模型把“跳跃”当作最高优先级行为。
  • 特征里没有距离信息,模型不知道墙已经很近了。
  • total fitness 中没有“完成奖励”,模型没有动力穿过障碍。

检查方式:

  • 保存训练过程中某几个 genome,分别回放到固定位置。
  • 打印关键帧的特征值,确认角色前方不是置信度接近 0 的未知区域。

解决建议:

  • 把跳跃奖励从 0.1 调低到 0.01。
  • 在特征中加入x_pos的变化值,让模型能感知到自己在前进。
  • 增加通关奖励,例如达到管道右侧区域时 fitness 加 100。

6.4 保存模型后回放结果和训练时不一致

现象:训练时角色能穿越管道,但载入保存的 genome 回放时,角色表现完全不同。

可能原因:

  • 回放环境没有使用相同的随机种子。
  • 模拟器版本或环境包装器不一致。
  • 训练时 online 评估和回放评价用了几套不同的特征提取逻辑。

检查方式:

  • 回放脚本中打印extract_features(obs)的维度,和训练时代码对比。
  • 对比加载的config文件是否和训练时完全一致。

解决建议:

  • 把特征提取函数放到独立模块,训练和回放统一 import。
  • 保存模型时同时保存 config 文件的副本。
  • 尽量固定模拟器的随机种子,减少评测波动。

7. 从训练马里奥到生产级 AI 工程

7.1 把进化实验拆成可维护模块

训练马里奥只是一次实验,但真正把这种实验做成可维护的项目,需要像处理业务代码一样处理训练流程。一个推荐的文件结构是:

mario_neat/ ├── config.py ├── environment.py ├── features.py ├── fitness.py ├── genome_repository.py ├── train.py ├── replay.py ├── neat_config.ini └── logs/

features.py只负责特征提取,fitness.py只负责计算适应度,train.py只负责启动训练。这样后续要修改奖励函数、调整输入特征、替换算法时,不会牵一发动全身。

7.2 从 NEAT 转向深度强化学习

当关卡变成第二关、第三关,或者加入更多敌人和可摧毁砖块时,NEAT 的搜索空间会越来越大。此时更有扩展性的方案是深度强化学习。

  • 状态输入可以继续使用图像,用 CNN 自动提取特征。
  • 动作输出可以通过策略网络直接得到动作概率分布。
  • 奖励函数可以复用 NEAT 实验里验证过的 x 坐标差异和跳跃奖励设计。

NEAT 实验的价值不在于最终模型有多强,而在于帮你快速验证“用这种状态表示 + 这种奖励函数是否能学到行为”。这个验证结果对后续深度强化学习仍然有效。

7.3 结合 AI Agent 和 LLM 做决策

在更复杂的游戏控制场景,比如开放世界游戏,底层移动控制和上层剧情决策可以分离。底层使用强化学习模型控制角色移动,上层使用大语言模型或规则 Agent 决定目标优先级。这种架构把“感知-控制”和“规划-决策”分开,能降低单个模型的训练难度。

实际项目里也可以把这个思路应用到机器人控制、自动化测试、NPC 行为设计等领域。核心是先规划任务边界,再决定哪一层用数据训练,哪一层用规则或大模型。

7.4 本地部署和模型推理的工程化

训练结束后,模型要从“实验结果”变成“可部署服务”。本地部署时需要注意负载能力、接口设计和日志记录。NEAT 模型可以被 pickle 序列化,但为了后续跨语言或跨平台使用,也可以把神经网络结构和权重导出成 JSON,再在目标语言里重新实现前向推理。

{ "nodes": { "0": {"type": "input", "bias": 0.0}, "12": {"type": "hidden", "bias": 0.12}, "14": {"type": "output", "bias": -0.03} }, "connections": { "0-12": {"weight": 1.12, "enabled": true}, "12-14": {"weight": -0.87, "enabled": true} } }

这样模型推理就不依赖neat-python,生产环境可以复用已有推理框架。对于深度学习模型则可以保存为 ONNX 或 TorchScript 格式。

8. 复盘 Code Bullet 实验中的关键工程启示

8.1 最小可复现的路线清单

如果你第一次做这个项目,建议按下面清单执行,不要在早期就追求完美。

  1. 先安装环境并跑通随机动作,确认模拟器可用。
  2. 定义 5 个动作的简化动作空间。
  3. 实现一个简单的特征提取函数,输出 20 到 160 维特征。
  4. 用固定动作“右跑”测试特征和 fitness 是否一致。
  5. 接入 NEAT,population 设为 50,fitness 只依赖当前 x 坐标。
  6. 跑 10 代后保存 winner,回放看表现。
  7. 根据回放结果修正特征和奖励函数。
  8. 重复第 6 到第 7 步,直到行为稳定。

这个清单可以复用到其他游戏 AI 项目,不限于马里奥。

8.2 “中配”复现不等于翻译视频

很多人看到 Code Bullet 的视频后,以为复现只是把英语环境改成中文。其实真正的“中配”难点在于环境适配、依赖调整、参数选择和中文日志整理。

Code Bullet 的视频展示的是实验结果,不是完整教程。直接按视频里的画面去猜测代码,会遇到大量环境差异:模拟器版本不同、Python 版本不同、Fitness 函数设计思路未公开。因此,中文复现的正确做法是先跑通最小流程,再逐步向视频效果靠拢。

这整个过程本质上是一次 AI 工程实践:不仅要懂算法,还要懂环境管理、日志分析、参数调试和模型版本控制。

8.3 给新手的练习建议

刚开始不要急着写完整的训练脚本。先完成以下五个练习,每个练习对应一个关键知识点。

  1. 练习修改动作空间:把 5 个动作改成 2 个,观察随机策略表现变化。
  2. 练习调整特征维度:从整张缩略图切到手工特征,对比训练速度和效果。
  3. 练习修改 fitness:把只奖励 x 坐标改成同时奖励跳跃,观察模型行为变化。
  4. 练习保存和加载模型:设计一个包含“过第一个管道”的评估脚本。
  5. 练习用日志定位问题:在训练日志里找出卡墙和原地抖动出现的代数。

这些练习做完以后,再回头看 NEAT 的论文和源码,理解会深很多。AI 训练项目的门槛不在算法本身,而在能不能把环境、状态、奖励和评估反复调整到一个自洽状态。马里奥是一个足够小、足够清晰的试验场,值得花一个周末把它做完整。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询