Gymnasium + MuJoCo 连续控制环境从零跑通与避坑指南
【免费下载链接】GymnasiumA standard API for single-agent reinforcement learning environments, with popular reference environments and related utilities (formerly Gym)项目地址: https://gitcode.com/GitHub_Trending/gy/Gymnasium
PPO 在 HalfCheetah 上训到 3 万步奖励还在 200 附近横盘,把网络加宽、学习率换三轮,曲线纹丝不动——连续控制任务里,这类卡死比报错更常见,也更难查。因为 Gymnasium 配 MuJoCo 的环境不会抛异常给你,它只是默默把一个"看起来不对"的策略当作合理输出。
先给出一个可以直接对照的诊断入口:MuJoCo 环境的观测都是Box空间,由广义坐标qpos和广义速度qvel拼接而成,个别环境还会追加接触力项。环境类上的observation_structure字典会把这段拼接的边界标出来,拆对维度是后面一切工作的前提。
第一步:把观测空间拆成 qpos + qvel,再确认动作范围
拿 HalfCheetah 开刀。创建环境后,打印两个空间:
import gymnasium as gym env = gym.make("HalfCheetah-v5") print(env.observation_space) # Box(-inf, inf, (17,), float64) print(env.action_space) # Box(-1.0, 1.0, (6,), float32)这 17 维是 8 维qpos加 9 维qvel。注意一个反直觉的细节:机器人自己的 x 坐标被默认从观测里剔掉了(exclude_current_positions_from_observation=True)。这是设计出来的归纳偏置——不给你绝对位置,逼策略学会"凭速度和关节角维持前进",而不是"凭位置决定往哪跑"。想恢复这维,把exclude_current_positions_from_observation=False传给gym.make即可,观测会变成 18 维。
v5 系列还给了一个省事的东西:observation_structure直接告诉你观测由哪几段拼成:
print(env.observation_structure) # {'skipped_qpos': 1, 'qpos': 7, 'qvel': 9}Ant 就更典型了:qpos13 维 +qvel14 维,再加 78 维接触力(cfrc_ext),凑成 105 维观测。如果策略输入层按 27 维建,加载权重时就会当场炸;不炸的情况更隐蔽——维度对上了但语义错位,训练曲线自然难看。
动作侧更值得逐个核对,因为范围并不统一:
- 多数环境(HalfCheetah 6 维、Walker2d 6 维、Ant 8 维、Hopper 3 维)都是
Box(-1, 1); - Humanoid 和 HumanoidStandup 是
Box(-0.4, 0.4, (17,), float32),上界不是 1; - Pusher 是
Box(-2, 2, (7,), float32); - InvertedPendulum 的动作是施加在滑块上的力,范围
[-3, 3],单位牛顿。
策略网络输出经tanh压到 ±1 后直接送进动作空间,遇到上面三个"特例"时控制量会被静默截断。这类 bug 在训练日志里毫无征兆,只会让策略显得"使不上劲"。
选环境:按任务类型建一张速查表 🤖
Gymnasium 自带的 MuJoCo 环境按任务形态分成四族:摆杆平衡、奔跑/跳跃、末端操控、人形。维度差异比想象的大,从 4 维观测一路涨到 348 维:
| 环境 | 观测维度 | 动作维度 | 奖励在激励什么 / 惩罚什么 | 适合回答的问题 |
|---|---|---|---|---|
| InvertedPendulum-v5 | 4 | 1(范围 ±3) | 杆角度绝对值小于 0.2 弧度时每步 +1,碰线即终止 | 最小闭环:调试代码路径和日志 |
| Reacher-v5 | 10 | 2 | 末端到固定目标的距离负相关,另扣控制量平方 | 低维操作与收敛速度 |
| Swimmer-v5 | 8 | 2 | 头部前进速度,扣控制量 | 水动力学下的协调摆动 |
| Hopper-v5 | 11 | 3 | 前进速度 + 保持站立奖励,扣控制量 | 单腿动态平衡 |
| Pusher-v5 | 17 | 7(范围 ±2) | 末端到目标的距离驱动 | 带接触的桌面操控 |
| HalfCheetah-v5 | 17 | 6 | x 方向前进速度,扣 0.1 倍动作平方和 | 连续控制基准,几乎每篇论文都有它 |
| Ant-v5 | 105 | 8 | 前进速度 + 存活 +1/步,扣控制量(0.5 倍)与接触力项 | 高维观测 + 接触动力学 |
| Walker2d-v5 | 17 | 6 | 前进速度 + 站立奖励,扣控制量 | 双足步态 |
| Humanoid-v5 | 348 | 17(范围 ±0.4) | 前进速度 + 朝上姿态奖励,扣控制量 | 全身协调与数值稳定 |
| HumanoidStandup-v5 | 348 | 17(范围 ±0.4) | 从躺姿起身并维持站立 | 姿态切换类任务 |
几个选环境的经验值:只想验证 pipeline 用 InvertedPendulum,观测 4 维、奖励规则一句话说清,训不训得出来一眼可见;要和旧结果对比、或做方法消融,HalfCheetah 和 Ant 是默认选项;要压算法的数值稳定性,Humanoid 的 348 维观测 + ±0.4 动作范围是最好的压力测试。
版本后缀决定后端。新实验直接用 v5(要求mujoco>=2.3.3);复现早期论文时先看清版本号——v2/v3 跑在 mujoco-py 上,v4 起换到官方 mujoco 原生绑定,同一组超参在两个后端下数值不会完全一致。做基线时把后缀锁死,写进配置。
上图为仓库教程mujoco_reinforce.py训练 InvertedPendulum 的回报曲线:前期贴着 0 磨,之后快速拉升。MuJoCo 环境里"先平后陡"是正常形态,别在前几千个 episode 就下结论。
跑一段诊断循环,让 reward 和 info 说话
奖励函数的套路在整套环境里高度一致:前进类环境给"位移除以 dt"的奖励,再扣控制量(有的再加接触力项);平衡类给存活奖励;操控类给目标距离。具体权重和分解项都放在step()返回的info字典里,比如reward_forward、reward_ctrl、reward_contact。训练前跑一段随机策略的诊断循环,比盯着曲线猜原因快得多:
obs, info = env.reset(seed=0) total = 0.0 for step_i in range(1000): action = env.action_space.sample() obs, reward, terminated, truncated, info = env.step(action) total += reward if step_i in (9, 99, 999): print(step_i, round(total, 1), {k: round(v, 2) for k, v in info.items() if isinstance(v, (int, float))}) env.close()看两样东西就够了。其一是info里的各项奖励占比:HalfCheetah 随机策略下reward_forward应该接近 0,说明机器人还在原地打转,这符合预期;若训了几万步它还是 0,问题多半在动作侧(范围、符号、归一化),而不是奖励设计。其二是观测的统计量——把obs存下来看均值和量级,某几维恒为常数或者量级差出三个数量级,都是线索。
另外两个循环里的点:
terminated和truncated要分开统计。Hopper、Ant、Humanoid 在躯干高度跌出健康区间时会terminated=True提前结束,其余环境靠 1000 步截断。存活率掉到 50% 以下时,平均奖励的下降主要来自"死得早",而不是"走得慢"。step的有效时长dt = frame_skip × 0.01,HalfCheetah 默认frame_skip=5,即每步 0.05 秒。前进奖励按dx/dt计算,改frame_skip会同时改变奖励尺度和任务难度,动它之前先想清楚。
跑速与数值:真正的杠杆只有几个 ⚡
PPO 这类算法是数据饥饿型,采样速度通常才是瓶颈。三个杠杆按性价比排序:
向量化。用SyncVectorEnv(同进程多环境,简单稳定)或AsyncVectorEnv(子进程,吞吐更高)。注意 v5 的观测是 float64,多环境并行时留意数组拷贝开销。
from gymnasium.vector import SyncVectorEnv n_envs = 4 vec_envs = SyncVectorEnv( [lambda: gym.make("HalfCheetah-v5") for _ in range(n_envs)] ) obs, infos = vec_envs.reset(seed=0) actions = vec_envs.action_space.sample() obs, rewards, terminateds, truncateds, infos = vec_envs.step(actions)渲染与采集分离。训练时不要开render_mode="human"——GLFW 窗口刷新会把采样速度拖慢一个数量级。无显示器的服务器上,rgb_array也依赖图形后端:GLFW 起不来,EGL 走 GPU 离屏,OSMesa 纯 CPU 兜底。需要在gym导入前设置环境变量MUJOCO_GL(取值为egl或osmesa),否则默认 GLFW 会在无头环境直接失败。要留训练视频,在评估环境外面包一层RecordVideo,别在训练主循环里实时渲染。
输入输出对齐。观测是 float64 的Box(-inf, inf, ...),网络吃 float32,量级和尺度靠观测归一化 wrapper(如NormalizeObservation)稳定住;动作侧用RescaleAction显式对齐目标环境的动作范围,比在策略代码里手写缩放系数可靠。这两个 wrapper 的组合在连续控制里几乎是标配,不展开代码。
卡住了先查这张清单
按出现频率从高到低:
- 维度错位:策略输入层没和
observation_space.shape对账,输出层没和action_space.shape对账。HalfCheetah 是 17 进 6 出,Ant 是 105 进 8 出,Humanoid 是 348 进 17 出。 - 动作范围假设错误:默认"所有环境都是 ±1"。Humanoid ±0.4、Pusher ±2、InvertedPendulum ±3,
tanh输出撞上截断时策略永远差一截。 - 只看总奖励不看分解:把
info里的reward_forward/reward_ctrl分开画,曲线才能告诉你钱花在哪儿。 - terminated 与 truncated 混为一谈:存活率不达标时,平均奖励的跌幅是假信号。
- 后端漂移:换过 v4/v5、换过 mujoco 版本之后不复盘基线。锁后缀、锁依赖版本。
种子也要两边一起固定:环境侧reset(seed=...)或env.reset_options,算法侧自己的随机源。评估时用确定性策略(关掉探索噪声)、固定种子跑 10 个 episode 以上取均值,单回合结论基本不可信。
接下来可以做的三件事
- 选 HalfCheetah 或 Ant,用 PPO 跑一个基线,把每个 episode 的平均奖励和
info中的各项奖励分解都记下来,作为后续所有调参的对照线。 - 挑一个奖励权重单独动(比如
ctrl_cost_weight或forward_reward_weight),看info分解和总曲线如何联动——这一步比调网络结构更能让你理解环境。 - 采集速度不够时,把
SyncVectorEnv换成AsyncVectorEnv并开多核;需要离屏渲染就切到 EGL 后端,把渲染开销从关键路径上移走。
想复现旧论文的数字,先锁版本后缀,再核对exclude_current_positions_from_observation这类默认行为是否和原文一致——多数"复现不出来"最后都栽在这类默认值上。
【免费下载链接】GymnasiumA standard API for single-agent reinforcement learning environments, with popular reference environments and related utilities (formerly Gym)项目地址: https://gitcode.com/GitHub_Trending/gy/Gymnasium
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考