
如果你刷到过“AI 学习玩马力欧”这类视频多半会留下一个印象AI 一开始像个乱按手柄的新玩家原地跳跃、反复撞墙然后在一次次试错里慢慢学会跑步、踩怪、吃金币最后甚至可以一气呵成通关 1-1 关卡。Code Bullet 的经典项目就是用这种思路让 AI 在超级马里奥里从零开始进化。本文不打算做视频解说而是把背后的技术链路拆开从强化学习基础概念、环境搭建到完整的 PPO 训练代码再到奖励设计、训练调参和常见坑点带你在本地复现一个能逐步学会玩超级马里奥的 AI。这个项目非常适合三部分读者第一次接触强化学习的开发者想做一个能演示、能写进简历的 AI 游戏项目以及看完视频后想自己动手复现却不知道从哪开始的朋友。读完本文你会掌握超级马里奥强化学习环境的搭建方法能跑通训练流程也会理解为什么 AI 一开始会“乱跳”以及如何通过奖励设计让它朝正确方向进化。1. 背景与核心概念1.1 什么是“AI 学习玩马里奥”“AI 学习玩马里奥”本质上是一个游戏智能体训练任务。我们不给 AI 任何游戏规则也不告诉它马里奥可以吃蘑菇、踩乌龟只让它不断观察游戏画面输出手柄动作然后根据游戏给出的分数、通关进度等信号调整自己的策略。这个任务在强化学习里属于典型的 POMDP部分可观测马尔可夫决策过程问题AI 只能看到当前屏幕帧并不知道马里奥的完整内部状态它每一次决策既影响当前得分也影响后续所有状态。简单说AI 是在一个不完整信息环境中通过与环境反复交互来学习策略。项目中的经典做法大致有两个方向进化算法NEAT把神经网络结构当作基因通过变异和交叉不断进化留下表现更好的网络。强化学习PPO 等让智能体通过“试错 奖励反馈”学习一个策略网络直接根据画面输出动作。Code Bullet 在相关视频里更多展示了“看着 AI 慢慢变强”的戏剧化效果而工程复现时强化学习路线更稳定、更容易收敛所以本文核心实战以 PPO 为主NEAT 作为对比思路讲解。1.2 为什么要选择超级马里奥超级马里奥是一个非常合适的强化学习入门环境原因有几个。第一观测简单直观。AI 拿到的是 240×256 的 RGB 画面不需要额外传感器数据和人类玩家看到的画面几乎一样。第二状态空间足够典型。马里奥的动作包括左右移动、跳跃、加速、下蹲等组合起来有一定的策略深度同时 1-1 关卡又足够短AI 可以在数小时内完成基础学习。第三奖励信号清晰。马里奥环境原生会给出分数、金币、时间、位置信息我们可以直接构造奖励函数向前走给正奖励死亡给负奖励通关给大奖励。第四可视化效果好。训练过程中你能直接看到 AI 的每次操作这对理解强化学习非常有帮助。你不需要像看 loss 曲线那样靠脑补画面本身就在讲一个“学习过程”的故事。1.3 强化学习、进化算法与监督学习的区别很多新手会把“AI 玩游戏”理解成监督学习给它大量“正确操作”的图片然后让模型学习“看到这个画面就按这个键”。但游戏 AI 最大的问题是没有标准答案。你到底应该在哪个像素位置跳这本身就不是唯一解。三者对比如下方法是否需要标签主要思路适合场景监督学习需要学习输入到输出的映射有大量专家操作数据时强化学习不需要通过环境奖励学习策略交互式决策任务进化算法不需要通过变异、选择进化策略编码策略网络结构简单的小规模任务对比之后你会发现超级马里奥这类任务天然适合强化学习。AI 不需要知道“应该按什么键”只需要知道“按了什么键之后得分有没有变高”。这也正是它看起来神奇的地方没有任何人教它它却能在几百万步试错后形成自己的游戏策略。2. 环境准备与版本说明2.1 安装 Python 与依赖本项目使用 Python 3.8 以上版本建议使用 Python 3.10 进行测试。环境准备的核心依赖如下gym-super-mario-bros超级马里奥的强化学习环境封装基于 FCEUX 模拟器。nes-pyNintendo Entertainment System 模拟器由 gym-super-mario-bros 自动依赖安装。stable-baselines3强化学习算法库内置 PPO、DQN、A2C 等常用算法。opencv-python用于对马里奥画面做灰度化和缩放。tensorboard用于可视化训练曲线和智能体行为。使用 pip 安装即可pip install gym-super-mario-bros stable-baselines3 opencv-python tensorboard如果网络环境不太好可以去掉 tensorboard等训练稳定后再补pip install gym-super-mario-bros stable-baselines3 opencv-python需要注意gym-super-mario-bros 新版本已经切换到 gymnasium APIstable-baselines3 也需要 2.0 以上版本才原生支持 gymnasium。如果你之前安装过旧版 gym建议在虚拟环境中重新安装一遍避免两个环境 API 混用导致难以排查的报错。2.2 项目目录结构为了保持工程整洁建议按下面的结构创建项目ai-mario/ ├─ train.py # 训练脚本 ├─ play.py # 加载模型并运行测试 ├─ requirements.txt # 依赖清单 ├─ tb_logs/ # TensorBoard 日志目录 └─ models/ # 模型保存目录requirements.txt 内容参考gym-super-mario-bros7.4.0 stable-baselines32.0.0 opencv-python tensorboard创建好目录后我们先看一下核心训练脚本的写法。2.3 环境可用性验证安装完成后可以先写一段最简单的代码验证环境是否可用import gym_super_mario_bros from gym_super_mario_bros.actions import SIMPLE_MOVEMENT from nes_py.wrappers import JoypadSpace env gym_super_mario_bros.make(SuperMarioBros-1-1-v0) env JoypadSpace(env, SIMPLE_MOVEMENT) state env.reset() print(观测空间, env.observation_space) print(动作空间, env.action_space) print(初始状态形状, state.shape) env.close()如果运行成功你会看到类似输出观测空间Box(0, 255, (240, 256, 3), uint8) 动作空间Discrete(7) 初始状态形状(240, 256, 3)这里的 7 个动作来自 SIMPLE_MOVEMENT对应的是无操作、右、右跳、右跑、右跑跳、左、左跳。动作空间大小会直接影响神经网络输出层的设计后续训练就是让 AI 从这 7 个动作里选出最合适的操作。3. 强化学习基础概念3.1 状态、动作与奖励强化学习里三个核心概念分别是状态State、动作Action和奖励Reward。状态AI 观察到的游戏画面。在马里奥里可以是一帧或多帧图像。动作AI 可以执行的操作。在马里奥里是手柄按键的离散组合。奖励环境反馈给 AI 的数值信号告诉它刚才的动作是“好”还是“坏”。马里奥环境的默认奖励来自游戏内部统计包括移动距离、吃到金币、消灭敌人、时间扣分等。不过这些信号对 AI 来说不够线性比如 AI 一开始只会原地跳但分数可能也会因为金币发生变化模型很难分清到底哪个动作带来了高分。因此在实战项目中我们通常会在环境之上再做一层“奖励设计”把游戏信息转换成更适合强化学习算法的反馈。这一点后面会专门展开。3.2 PPO 算法的核心思想PPOProximal Policy Optimization近端策略优化是目前最常用的强化学习算法之一。它在 OpenAI 早期成果中大量使用也是普通开发者在自己的 GPU 上能稳定训练的算法。PPO 属于策略梯度算法核心思路是直接对策略进行参数化用一个神经网络接收图片输出每个动作的概率。当 AI 做出某个动作并获得较高奖励时网络会增加该动作的概率反之则降低概率。但简单策略梯度算法有一个致命问题一步更新得太多策略会突然变差导致整个训练崩溃。PPO 的解决办法是加入一个“裁剪机制”限制新策略和旧策略之间的更新幅度不让某个动作概率在一次更新中变化得过于剧烈。这样训练更稳定超参数调节也相对容易。简单理解就是PPO 一边学习一边提醒自己“步子不要太大以免扯到关键部位”。这种稳定性让它非常适合马里奥这类图像输入、稀疏奖励的任务。3.3 NEAT 进化算法为什么也值得了解NEATNeuroEvolution of Augmenting Topologies是另一种“训练”AI 的方法它不通过梯度下降调整权重而是把神经网络结构和权重一起当作基因通过选择、交叉、变异来进化。在 Code Bullet 这类视频里NEAT 的效果非常直观你会看到网络结构从简单到复杂AI 的智商“肉眼可见”地增长。不过 NEAT 在复杂游戏任务上有明显局限CNN 卷积神经网络很难用基因编码进行有效进化搜索空间太大收敛速度比强化学习慢很多。所以本文实战使用 PPO但如果你以后想复现“从零进化网络结构”的效果NEAT 仍然是一个很好的学习对象。4. 完整实战基于 PPO 训练 AI 玩马里奥4.1 创建训练脚本骨架打开 train.py先引入依赖并定义环境创建函数import cv2 import numpy as np import gymnasium as gym import gym_super_mario_bros from gym_super_mario_bros.actions import SIMPLE_MOVEMENT from nes_py.wrappers import JoypadSpace from stable_baselines3 import PPO from stable_baselines3.common.vec_env import ( DummyVecEnv, VecFrameStack, VecMonitor, VecTransposeImage, )这里引入 gymnasium 而不是旧版 gym是为了兼容新版 gym-super-mario-bros。如果你安装的是旧版本环境则可能需要改为import gym但整个代码结构不变。接着定义 ResizeAndGrayscale 包装器class ResizeAndGrayscale(gym.ObservationWrapper): def __init__(self, env, height84, width84): super().__init__(env) self.height height self.width width self.observation_space gym.spaces.Box( low0, high255, shape(height, width, 1), dtypenp.uint8 ) def observation(self, obs): obs cv2.cvtColor(obs, cv2.COLOR_RGB2GRAY) obs cv2.resize(obs, (self.width, self.height), interpolationcv2.INTER_AREA) obs np.expand_dims(obs, axis-1) return obs这个包装器的意义很关键。马里奥原始画面是 240×256×3色彩信息对策略网络帮助有限却会大幅增加计算量。我们先将 RGB 转为灰度图再缩放为 84×84和 DQN 论文里处理 Atari 游戏的方式一致。最后扩展一个通道维度保持图像为 (84, 84, 1)。4.2 封装环境并加入帧堆叠单独一张 84×84 的灰度图很难表达“马里奥正在落体”还是“正在奔跑”等动态信息。常见做法是把最近 4 帧堆叠起来作为一个状态输入让神经网络同时看到连续画面。创建环境的函数如下def make_env(): env gym_super_mario_bros.make(SuperMarioBros-1-1-v0) env JoypadSpace(env, SIMPLE_MOVEMENT) env ResizeAndGrayscale(env) return env训练主函数里使用 stable-baselines3 提供的向量环境工具def main(): vec_env DummyVecEnv([make_env]) vec_env VecFrameStack(vec_env, n_stack4) vec_env VecTransposeImage(vec_env) vec_env VecMonitor(vec_env) model PPO( policyCnnPolicy, envvec_env, learning_rate2.5e-4, n_steps1024, batch_size128, n_epochs10, gamma0.99, gae_lambda0.95, clip_range0.2, ent_coef0.01, tensorboard_log./tb_logs, verbose1, ) model.learn(total_timesteps500_000) model.save(mario_ppo) vec_env.close() print([OK] 训练完成模型保存为 mario_ppo.zip) if __name__ __main__: main()这里做四层包装DummyVecEnv把普通单环境包装成向量环境统一接口。VecFrameStack按帧堆叠 4 帧画面让模型感受到时序信息。VecTransposeImage将观测从 (height, width, channels) 转成 (channels, height, width)因为 CnnPolicy 默认接收通道优先输入。VecMonitor记录每个 episode 的奖励、长度方便写进日志。PPO 的参数中CnnPolicy 表示使用卷积神经网络处理图像输入n_steps1024 表示每轮采样 1024 步batch_size128 表示每次梯度更新使用 128 条样本ent_coef0.01 是熵奖励系数用来鼓励 AI 保持一定的动作随机性避免过早陷入局部最优。4.3 启动训练直接在命令行运行python train.py第一次运行会看到很多日志包括 rollout 的 mean reward、ep_len_mean 等。如果环境跑得慢可以先降低 total_timesteps例如改成 100_000确认整个流程没问题后再跑大规模训练。训练过程中建议同时启动 TensorBoardtensorboard --logdir ./tb_logs然后在浏览器打开 http://localhost:6006就可以实时看到 mean reward、policy gradient loss、entropy 等曲线。4.4 编写测试脚本训练完成后使用 play.py 加载模型看看 AI 的实际表现import cv2 import numpy as np import gymnasium as gym import gym_super_mario_bros from gym_super_mario_bros.actions import SIMPLE_MOVEMENT from nes_py.wrappers import JoypadSpace from stable_baselines3 import PPO from stable_baselines3.common.vec_env import ( DummyVecEnv, VecFrameStack, VecTransposeImage, ) class ResizeAndGrayscale(gym.ObservationWrapper): def __init__(self, env, height84, width84): super().__init__(env) self.height height self.width width self.observation_space gym.spaces.Box( low0, high255, shape(height, width, 1), dtypenp.uint8 ) def observation(self, obs): obs cv2.cvtColor(obs, cv2.COLOR_RGB2GRAY) obs cv2.resize(obs, (self.width, self.height), interpolationcv2.INTER_AREA) obs np.expand_dims(obs, axis-1) return obs def make_env(): env gym_super_mario_bros.make( SuperMarioBros-1-1-v0, render_modehuman, ) env JoypadSpace(env, SIMPLE_MOVEMENT) env ResizeAndGrayscale(env) return env def main(): vec_env DummyVecEnv([make_env]) vec_env VecFrameStack(vec_env, n_stack4) vec_env VecTransposeImage(vec_env) model PPO.load(mario_ppo.zip) obs vec_env.reset() total_reward 0.0 while True: action, _ model.predict(obs, deterministicTrue) obs, reward, done, info vec_env.step(action) total_reward reward[0] if done[0]: break print(f[OK] 游戏结束累计奖励: {total_reward:.2f}) vec_env.close() if __name__ __main__: main()运行python play.py如果环境支持render_modehuman 会弹出游戏窗口。如果你的版本不支持这个参数删除 render_modehuman 即可运行时可以调用 vec_env.render() 或直接忽略可视化只看最终奖励。这里有个小细节测试时 model.predict 使用了 deterministicTrue也就是每次只选概率最大的动作不再随机探索。这能体现模型在当前策略下最稳定的水平。如果想看更“活泼”的表现可以改成 deterministicFalse。4.5 预期结果说明在 50 万步训练内AI 大概率还不能成功通关但你会观察到非常明显的行为变化阶段前几万步AI 疯狂原地跳、跳跃频率极高经常卡在第一个障碍物前。中期AI 学会向右移动偶尔能跳过小型障碍物。后期AI 的移动更流畅可能抵达关卡中段甚至在部分随机种子下通关。如果你发现 AI 一直原地左右摇摆最常见原因是奖励函数没有给出足够清晰的“前进引导”。解决思路是引入基于马里奥 x 坐标的奖励设计这就进入了下一节内容。5. 优化奖励设计与训练策略5.1 默认奖励的局限gym-super-mario-bros 默认的奖励是从游戏内存里读取的包括分数、金币、时间、距离等综合信息。但这套奖励存在一个问题很多高分行为并不是“向前走”。例如 AI 原地反复顶金币分数也能上升AI 长时间不动时间分数可能还不会立刻扣很多又比如 AI 左右摇摆靠近敌人再踩掉也能获得分数。这些行为对通关没有直接帮助模型却可能因为局部奖励被“带偏”。另一个问题是奖励尺度不统一。距离变化是一个连续的大数值击杀敌人有时是 200 分有时是 1000 分网络很难判断哪个更重要。5.2 基于 x 坐标的奖励塑形更符合直觉的做法是让 AI 知道“向前走得越远奖励越高”。gym-super-mario-bros 的 info 字典中提供了 x_pos表示当前马里奥在关卡中的横向位置。在训练脚本的包装器链中我们可以插入一个自定义的 RewardShapingclass RewardShaping(gym.Wrapper): def __init__(self, env): super().__init__(env) self.last_x 0.0 def reset(self, **kwargs): result self.env.reset(**kwargs) if isinstance(result, tuple): obs, info result else: obs, info result, {} self.last_x 0.0 return obs, info def step(self, action): result self.env.step(action) if len(result) 5: obs, reward, terminated, truncated, info result else: obs, reward, done, info result terminated, truncated done, done current_x info.get(x_pos, 0.0) reward (current_x - self.last_x) / 10.0 self.last_x current_x if terminated or truncated: reward - 15.0 return obs, reward, terminated, truncated, info然后在 make_env 中加入这个包装器def make_env(): env gym_super_mario_bros.make(SuperMarioBros-1-1-v0) env JoypadSpace(env, SIMPLE_MOVEMENT) env RewardShaping(env) env ResizeAndGrayscale(env) return env这里的奖励逻辑是每一步如果 x_pos 比上一次更靠右就给予正向奖励如果原地不动或后退就不会获得前进奖励。同时在死亡时减去 15 分让 AI 更珍惜生命。除以 10 是为了控制奖励量级。马里奥每帧移动的像素差通常在 0 到 3 之间除以 10 后每步大约只有 0 到 0.3 的奖励和原生奖励合在一起时不会喧宾夺主。你可以根据实际训练曲线微调这个分母。5.3 分阶段训练与渐进关卡在完整项目中一个常用技巧是分阶段训练。阶段一只在 SuperMarioBros-1-1 上训练目标是让 AI 学会基本移动和跳跃。阶段二加载阶段一模型在 SuperMarioBros-1-2 继续训练让 AI 学会适应新的关卡结构。阶段三在多个关卡间随机切换提升泛化能力。对应到代码中只需要修改环境名称env gym_super_mario_bros.make(SuperMarioBros-1-2-v0)加载已有模型继续训练也很简单model PPO.load(mario_ppo_1_1.zip) model.set_env(vec_env) model