资讯动态

从gym到Gymnasium:强化学习环境迁移与接口适配全指南

发布时间:2026/10/22 18:40:32 来源:尧图企业网站定制
其实这事情说起来有点滑稽我把一个搁置了半年的强化学习项目捡起来想换一下 Python 版本和依赖结果一启动就报错。仔细一看是环境对象没有按新规范返回接口。后来我花了一整个下午把代码里的 gym 全部换成 gymnasium才意识到这次“换代”比想象中动得多根本不只是 pip install 一个新包那么简单。如果你也在维护 RL 相关代码大概率会遇到同样的问题老的 gym 包从 2022 年底基本停止实质更新社区把维护工作迁到了 Gymnasium 上。表面上是换了包名实际上是接口规范的一轮收口。这篇文章会把“gym 更新到 Gymnasium 后需要修改哪些位置”拆得很细按安装依赖、import 语句、自定义环境、训练循环、Wrapper、spaces、注册表、常见报错这几个维度逐一说明适合正在翻新旧代码的强化学习开发者也适合第一次写自定义环境的新手。1. 为什么需要迁移Gymnasium 到底改了什么1.1 从 gym 到 Gymnasium 的背景gym 是早期强化学习生态中事实上的环境接口标准。OpenAI 发布它之后大量算法实现、教程、开源项目都基于它构建。2022 年以后gym 的核心维护者没有继续投入太多精力新环境、新接口特性基本停止社区逐渐转向由 Farama Foundation 维护的 Gymnasium。于是“更新”这件事不再是 pip 里升一个大版本而是切换到新的包名和新的代码规范。这一背景决定了后续所有修改位置只要你想在较新版本的 Python 上跑实验、想用较新的加速库或者想继续获取环境套件的新特性就需要把依赖从 gym 换到 gymnasium并检查所有引用 gym 命名空间的代码。1.2 接口层面的系统性变化Gymnasium 相对老 gym 最大的变化是整理了 Env 接口的返回约定。老的 gym 在较新版本里已经要求 reset() 返回 (obs, info) 元组Gymnasium 继承了这一点并强化了检查。step() 则正式把 done 拆分成 terminated 和 truncated 两个标志terminated 表示环境到达终止状态比如游戏角色死亡、小车冲出边界truncated 表示回合因为时间上限、步数上限或外部截断条件而提前结束。这个拆分不只是关于多一个返回值的语义变化还会影响训练逻辑。比如 PPO 这类算法在处理终止状态时价值估计通常不需要 bootstrap而截断状态却往往需要继续估计后续收益。如果只是简单地把 terminated 和 truncated 又或在一起迁移的意义就失去了一大半。所以我建议迁移后顺手检查一下算法框架是否分别处理了这两个标志。2. 第一步改动安装依赖与 import 语句2.1 安装 gymnasium 和对应环境套件迁移从依赖声明开始。最简单的是直接替换包名安装pip install gymnasium不过一个裸包往往不够。如果你平时用 CartPole、MountainCar、Pendulum 这类经典控制环境需要额外安装扩展项pip install gymnasium[classic-control]Atari、Box2D、MuJoCo 等套件也类似分别对应 gymnasium[atari]、gymnasium[box2d]、gymnasium[mujoco]。老 gym 的安装方式里这些环境可能被附带打包或者需要单独从 pip 里装。Gymnasium 把这些都变成显式扩展项安装声明更清楚但也意味着迁移时不能只看主包版本。这里给一个经验型建议在 requirements.txt 里最好加上版本约束gymnasium[classic-control]0.29锁定大版本能防止后续 API 再次变动时踩坑。如果你用 Poetry、conda 这类环境管理工具同样要把依赖声明里的 gym 替换成 gymnasium。2.2 import 全量替换代码层最明显的改动是 import 语句。把所有 import gym 改成 import gymnasium 是最基础的操作但别天真地以为搜一遍 import gym 就够了。代码里会出现很多子模块引用例如# 修改前 import gym from gym import spaces from gym.wrappers import TimeLimit, FlattenObservation # 修改后 import gymnasium as gym from gymnasium import spaces from gymnasium.wrappers import TimeLimit, FlattenObservation有一种取巧写法是在文件开头保留 import gymnasium as gym这样能减少一部分算法代码改动。不过这种写法有隐患因为代码里如果还有 gym.spaces、gym.wrappers、gym.utils、gym.error 这类子模块引用依然会因为找不到对应名字而报错。所以我更建议对每个文件单独处理把引用来源统一改成 gymnasium。另一个容易漏的地方是类型判断。用过 isinstance(env, gym.Env) 这类判断的代码也要同步换掉。迁移时最好用编辑器全局搜索“gym”把搜索结果列成清单逐个确认改法而不是只替换一种模式。提示只要代码里有残留的 import gym就会同时存在两个不同的环境体系。环境注册表、版本号、Wrapper 名字都可能互相干扰调试时非常难受所以迁移要一次做到位。2.3 依赖冲突与虚拟环境隔离实际项目中经常出现的情况是项目 A 还在用 gym项目 B 想迁移到 gymnasium。此时不要把两个包强行塞到同一个 Python 环境里。虽然 gym 和 gymnasium 的包名不同可以共存但环境注册表、包装器、底层依赖经常打架典型表现是 gym.make 能创建环境而 gymnasium.make 找不到同一个 ID 的环境。我在迁移某个模拟实验环境时遇到过这种情况。最终只有一个笨办法把项目统一到 gymnasium或者让旧项目继续留在专门的虚拟环境里。分环境管理虽然麻烦但能避免大量隐性 bug。2.4 环境验证工具的路径变化老代码里常用的环境自检工具from gym.utils.env_checker import check_env check_env(env)迁移后要改成from gymnasium.utils.env_checker import check_env check_env(env)check_env 在 Gymnasium 里会做更严格的一致性检查例如检查 reset 返回结构、step 返回结构、空间类型匹配等。迁移后跑一次 check_env能快速暴露旧环境类的兼容性问题。我实际用下来的感受是这一步比省掉之后的排错成本低很多强烈建议当成迁移标准动作。3. 环境类与训练循环的适配3.1 make 与环境创建方式最基础的环境创建方式改动不大# 修改前 env gym.make(CartPole-v1) # 修改后 env gymnasium.make(CartPole-v1)但 Gymnasium 更推荐在创建时显式传入 render_modeenv gymnasium.make(CartPole-v1, render_modehuman)这背后有个设计意图把渲染模式视为环境构造参数的一部分而不是调用期临时指定。老代码里常见的 env.render(modehuman) 用法仍然能跑但新的自定义环境规范里render() 方法不再接收 mode 参数渲染方式在init时确定。如果你想让自己的环境同时给别人用最好遵循这个约定。创建环境时还有个细节make 方法传入的 kwargs 会传给环境的构造函数。因此自定义环境如果支持某些配置项比如 max_steps、观察尺寸、难度等级都要在构造函数里用参数接住。老代码里常见的做法是创建后再修改环境属性进行配置这在 Gymnasium 里依然可行但不太推荐。3.2 自定义环境类怎么改几乎所有自定义环境类都要调整。先看一个典型的迁移对照。修改之前旧代码通常是import gym from gym import spaces class MyEnv(gym.Env): metadata {render.modes: [human]} def reset(self): self.state self.np_random.integers(0, 10) return self.state def step(self, action): obs self.state reward 1.0 done False info {} return obs, reward, done, info修改后import gymnasium as gym from gymnasium import spaces class MyEnv(gym.Env): metadata {render_modes: [human]} def __init__(self, render_modeNone): super().__init__() self.observation_space spaces.Box(low0, high10, shape(1,), dtypefloat) self.action_space spaces.Discrete(2) self.render_mode render_mode def reset(self, *, seedNone, optionsNone): super().reset(seedseed) # 自动处理 self.np_random self.state self.np_random.integers(0, 10) info {} return self.state, info def step(self, action): obs self.state reward 1.0 terminated False truncated False info {} return obs, reward, terminated, truncated, info这里面有几个关键点必须讲清楚。第一reset 的签名要改成 reset(self, *, seedNone, optionsNone)。seed 必须作为关键字参数接收并调用 super().reset(seedseed)这样 Env 基类才会自动初始化 self.np_random。如果跳过这步你会发现自己手动创建的随机数生成器没有正确跟随种子变化复现实验时很不稳定。第二reset 必须返回 (obs, info)。info 可以是一个空字典但返回结构不能省。Gymnasium 的检验机制会检查这一步一旦少了 info训练主循环会解构错误。第三step 必须返回 5 元组。这里最容易犯的错误是写成 obs, reward, truncated, terminated, info 之类的顺序。正确顺序是 obs, reward, terminated, truncated, info。顺序写错的情况下训练循环看起来不会崩但终止标志判断就乱了最后训练出来的策略也会很怪。第四metadata 里键名从 render.modes 变成了 render_modes。这个键名变化在渲染功能里影响最直接如果你的环境是给图像渲染用的迁移时尤其要注意。3.3 训练循环中的 reset 和 step 改法训练循环的改动点在 reset 和 step 两个位置。先说 reset旧代码普遍是这样obs env.reset()迁移后应改为obs, info env.reset()再看 step如果代码里原来用 done 作为回合结束标志迁移后需要把逻辑拆开或者至少用 terminated 和 truncated 两个值分别处理。# 修改前 obs, reward, done, info env.step(action) if done: obs env.reset() # 修改后 obs, reward, terminated, truncated, info env.step(action) if terminated or truncated: obs, info env.reset()如果你用的是现成的算法库比如 Stable-Baselines3要注意它的版本是否已经支持 Gymnasium。较新版本的 SB3 已经支持 gymnasium.make 出来的环境但旧版本仍按 gym 老接口运行。此时最简单的方案是升级 SB3或者在安装时保证“训练库依赖的 gym 版本”和“你写的环境代码”保持一致。3.4 种子的设置方式变了旧式代码经常这样设置随机种子env.seed(42) env.action_space.seed(42)这个 env.seed() 方法在 Gymnasium 里已经移除统一改为在 reset 时传入种子obs, info env.reset(seed42)如果想在不同 rollout 间延续某个随机序列需要在 reset 后不再手动调用 seed。Gymnasium 推荐的思路是在训练开始时调用一次 env.reset(seedSEED)之后每次 reset 如果不传 seed内部 RNG 会按之前的随机序列继续推进。如果需要复现实验你还可以在创建环境后使用 gymnasium.utils.seeding 模块来手动构造 np_random再传给自定义环境。但常规场景下直接在 reset(seed...) 中传入种子足够了。3.5 渲染调用方式调整渲染相关的改动容易被忽略。原来很多代码写成env gym.make(CartPole-v1) env.render(modehuman)新写法是创建环境时指定渲染模式env gymnasium.make(CartPole-v1, render_modehuman) env.reset() env.render()如果你的代码里有很多 env.render(modergb_array) 的调用建议统一改成创建环境时传 render_modergb_array然后直接 env.render()。这一改动的意义在于render_mode 成为环境构造参数的一部分逻辑更清晰也便于在训练时用 rgb_array 做录像在评估时用 human。4. Wrapper、Space、Registry 等其他改动点4.1 Wrapper 类从哪导入老代码可能这样用from gym.wrappers import TimeLimit, FrameStack, RecordVideo env gym.make(CartPole-v1) env TimeLimit(env, max_episode_steps500)Gymnasium 对应改成from gymnasium.wrappers import TimeLimit, FrameStack, RecordVideo env gymnasium.make(CartPole-v1) env TimeLimit(env, max_episode_steps500)如果你在自定义代码里自己实现了继承自 gym.Wrapper 的包装器也需要把基类改成 gymnasium.Wrapper。这一步不会报特别诡异的错误但如果旧包装器和新环境混用很可能出现返回值结构不匹配的问题比如包装器内部把 reset 的结果只解构成一个值然后在外面再取 info 就会报错。4.2 spaces 相关改动gym.spaces 和 gymnasium.spaces 的类基本相同Box、Discrete、Tuple、Dict 等命名一致。迁移时把 import 来源改掉即可。不过有些细节值得注意新版里空间对象的 dtype 检查更严格Box 构造时如果不传 dtype某些情况下会被警告或按默认 float32 处理。如果旧代码写的是from gym import spaces self.observation_space spaces.Box(low0, high255, shape(84, 84, 3))最好显式补上 dtypefrom gymnasium import spaces self.observation_space spaces.Box(low0, high255, shape(84, 84, 3), dtypeint)否则在后面处理图像像素时可能遇到类型断言问题尤其是配合神经网络输入时会有一堆 float 和 int 的兼容性小毛病。4.3 注册表和 EnvSpec如果你用过 gym.register 或 gym.envs.registration 来注册自定义环境现在要改成 gymnasium.register。老代码像是from gym.envs.registration import register register(idMyEnv-v0, entry_pointmy_module:MyEnv)新代码from gymnasium.envs.registration import register register(idMyEnv-v0, entry_pointmy_module:MyEnv)Gymnasium 还引入了 EnvSpec 的概念注册表里每个环境都对应一份规格信息包含了环境的 id、entry_point、reward_threshold、kwargs 等信息。当你通过 gymnasium.make(MyEnv-v0) 创建环境时如果找不到对应注册项会看到比老 gym 更明确的错误提示。迁移自定义环境注册时顺手在 register 里补上 reward_threshold 等参数有助于后续做 benchmark。4.4 版本字符串与检查工具如果你的代码里会读取 gym.version来做版本分支判断迁移后需要改成读 gymnasium.version。另外环境有效性检查工具也变了# 修改前 from gym.utils.env_checker import check_env check_env(env) # 修改后 from gymnasium.utils.env_checker import check_env check_env(env)check_env 在 Gymnasium 里会做更严格的一致性检查例如检查 reset 返回值结构、step 返回值结构、空间类型匹配等。迁移后跑一次 check_env能快速暴露旧环境类的兼容性问题。4.5 向量环境与并行采样如果项目用了向量环境导入路径也要换# 修改前 from gym.vector import SyncVectorEnv, AsyncVectorEnv # 修改后 from gymnasium.vector import SyncVectorEnv, AsyncVectorEnv此外向量环境的 reset 返回值在 Gymnasium 中是 (obs, info)其中 obs 的形状是第一维为 num_envs 的批量观测info 是包含各环境独立信息的字典。这个结构变化同样会影响 batch 采样代码。迁移时建议写一个小函数做统一封装把批量 reset 和 step 的结果整理成算法需要的格式避免在整套代码里到处修改。5. 常见报错与排查清单5.1 ModuleNotFoundError 与安装问题出现 No module named gymnasium 时直接安装即可。但要注意运行环境和安装环境是否一致。很多项目是在 Docker 里跑训练代码却在宿主机上装包这种错位不排查清楚会一直抓狂。类似地如果报错说找不到某个环境 id比如 gymnasium.make(CartPole-v1) 失败多半是没装对应环境套件先确认 pip list 里有没有 gymnasium[classic-control]。5.2 reset 相关报错TypeError: reset() got an unexpected keyword argument seed 是迁移自定义环境时的高频报错。原因很简单旧环境类的 reset 方法不接收 seed 参数。解法是改用 reset(self, *, seedNone, optionsNone)并在方法内部调用 super().reset(seedseed)。还有个常见提示是 reset() 应该返回两个值。这是 Gymnasium 在抛出检查警告说明你的 reset 只返回了 obs。把返回结构改成 (obs, info)。5.3 step 返回值不匹配Python 报错 ValueError: too many values to unpack (expected 4)对应的是你的代码还在用 4 元组接 step 返回值。快速修改方案是obs, reward, terminated, truncated, info env.step(action) done terminated or truncated如果第三方算法库内部还在用旧接口你会看到它在调用 env.step 时返回结构不匹配。此时不要试图在业务代码里兜底应该直接升级该库到兼容 Gymnasium 的版本。5.4 渲染与显示问题迁移后常见的一个坑是 env.render(modehuman) 在无头环境下报错。因为 Gymnasium 在构造环境时就确定渲染模式如果创建环境时没有传 render_modehuman后续渲染就没法正确触发。在服务器上做实验时我通常会结合环境变量配置图形渲染方式避免直接依赖本机显示窗口。5.5 gym 与 gymnasium 混装的隐性冲突最麻烦的问题不是单一报错而是两种包共存带来的混乱。具体表现很多例如 gym.make 能创建自定义环境而 gymnasium.make 却找不到或者同一个环境在两个体系里注册信息不一致。我见过有项目两套环境 ID 混着用在训练代码里创建 gymnasium 环境在评估脚本里又去 import 老 gym结果跑出来的结果毫无规律。排查方法很简单检查 pip list 里是否同时存在 gym 和 gymnasium再全局搜索代码里是否有残留的 import gym。一旦发现混装立即统一依赖尽量不要靠兼容写法蒙混过关。6. 迁移检查清单速查把这次迁移的核心改动点整理成一份清单可以让实操时少走弯路。我按修改优先级排序位置修改前修改后安装依赖pip install gympip install gymnasium[classic-control]import 主模块import gymimport gymnasium as gymimport spacesfrom gym import spacesfrom gymnasium import spacesimport wrappersfrom gym.wrappers import ...from gymnasium.wrappers import ...创建环境gym.make(CartPole-v1)gymnasium.make(CartPole-v1, render_mode...)reset 调用obs env.reset()obs, info env.reset(seed42)step 调用obs, reward, done, info env.step(a)obs, reward, terminated, truncated, info env.step(a)回合结束判断if done:if terminated or truncated:设置种子env.seed(42)env.reset(seed42)渲染env.render(modehuman)make 时传 render_mode注册表gym.envs.registration.registergymnasium.envs.registration.register环境检查gym.utils.env_checkergymnasium.utils.env_checkervector 环境gym.vectorgymnasium.vector版本号gym.versiongymnasium.version实际操作时我建议按“安装依赖-import 替换-环境类适配-训练循环适配-其他模块适配”的顺序来做。每一步做完都可以先跑一下环境自检确认没有报错再进入下一步。注意迁移做完后不要急着训练完整实验。先跑一个几百步的 rollout 小脚本打印每一步的 obs、reward、terminated、truncated、info肉眼确认这些值符合预期再进入正式训练。这个习惯能避免很多迁移残留问题带来的时间浪费。我个人体会最深的一次迁移是一个自定义多智能体环境。那个项目里把 agent 的回合结束条件写在 step 内部同时又把整局游戏的最大步数判断也混在同一个 done 标志里。迁移到 Gymnasium 后我花了不少时间把 terminated 和 truncated 拆开才在训练指标上看出明显区别。后面我做任何 RL 项目都会默认按 Gymnasium 的规范来写环境接口不再为老 gym 留兼容层。如果你手头还有旧代码建议早点迁移越晚改动面积越大。迁移过程本身并不神秘只要照着接口返回结构逐项改跑一轮 check_env 校验基本上就能平稳落地。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑