返回专辑
·Johan·4 分钟阅读

Gymnasium Wrapper 与 VecEnv

归一化、reward scale、frame skip、TimeLimit 应放在 wrapper 层,算法代码保持干净;换 env 时 wrapper 契约要一致。

Gymnasium Wrapper 与 VecEnv

1. 算法代码不该塞满归一化逻辑

PPO 训 HalfCheetah 时 episodic return 突然从 3000 掉到 800,排查半天发现 eval 没加载 VecNormalize 的 running stats——train 在归一化 obs 上跑,eval 在原始 obs 上跑,policy 等于盲打。更常见的是 wrapper 散落在 algorithm 文件里:TimeLimit 在 env 构造、RewardScale 在 trainer、NormalizeObs 又在 callback,换 MuJoCo v4→v5 时漏改一处,obs 维数对不上 checkpoint 静默报错。RViz 里一切正常、真机却行为异常,这类「配置漂移」在 RL 里往往就是 wrapper 链不一致。

根因是观测与奖励变换没有契约:顺序、参数、train 与 eval 是否一致,全靠口头传统。未写进 config 的 wrapper 让 demo 永远能跑,也让 checkpoint 复现永远不可审计。Wrapper 层应承载所有 env 语义变换,算法只接收标准 gymnasium.Env 接口——和 ROS 2 里 declare_parameter 把配置拉进阳光是同一类工程纪律。换项目、换同事、换机器,wrapper 链若不在 yaml 里,复现就是碰运气。

2. Wrapper 链:顺序即语义

推荐结构:RecordEpisodeStatistics(RewardScale(NormalizeObs(TimeLimit(RawEnv))))。先 TimeLimit 定 episode 边界,再 obs/reward 变换,最后统计日志。我习惯这个顺序,因为 episode 边界影响 GAE bootstrap 和 return 统计,必须最先确定;统计 wrapper 放最外层,保证日志反映最终进入算法的 obs/reward。

python
env = gym.make("HalfCheetah-v4")
env = gym.wrappers.TimeLimit(env, max_episode_steps=1000)
env = gym.wrappers.NormalizeObservation(env)
env = gym.wrappers.TransformReward(env, lambda r: r * 0.1)
env = gym.wrappers.RecordEpisodeStatistics(env)

NormalizeObs(RewardScale(...))RewardScale(NormalizeObs(...)) 行为不同——前者 reward scale 在 obs 归一化之后。改顺序可能 silently 改语义,config 里必须写清整条链。SB3 的 VecNormalize 在 vectorized env 上维护 running mean/var,save/load 必须和 model checkpoint 绑定;只存 model 不存 stats,eval 必崩。团队里常说「用了 normalize」,但 normalize 的是 obs 还是 reward、running stats 从哪来,不写进 yaml 就是不可复现。

3. 常用 wrapper 与职责边界

TimeLimitmax_episode_steps,触发 truncated 而非 terminated——GAE bootstrap 和 value target 对 timeout 的处理必须一致。NormalizeObservation / VecNormalize:running stats,train 时更新、eval 时冻结。RewardWrapper:scale 或 shape reward,改变 return 数值但不改 env 物理。FrameStack:叠帧改 obs 维数,旧 checkpoint 和 stats 一并作废。RecordEpisodeStatisticsinfo["episode"]["r"] 是 undiscounted return,和算法内部 discounted target 不同,画图别混。RecordVideo:按 interval 存 mp4,reward hack 调试必备——曲线看起来对、行为全错时,视频比 tensorboard 快。

Custom env 继承 gym.Env 时,metadata["render_modes"]reset/step 签名对齐 Gymnasium 0.29+(terminated/truncated 分离),否则 SB3 升级报错。Wrapper 不应改 action_space 语义,除非 TransformAction 文档写清映射关系。

4. VecEnv 并行与 SB3 契约

单 env MuJoCo 约 2000 fps;8 路 SubprocVecEnv 可线性提速到 15000+ fps,GPU 空转时加 env 数比加 network width 有效。SB3 要求 vectorized env 或自动包装:

python
from stable_baselines3.common.vec_env import SubprocVecEnv, VecNormalize

def make_env():
    return lambda: gym.make("HalfCheetah-v4")

env = SubprocVecEnv([make_env() for _ in range(8)])
env = VecNormalize(env, norm_obs=True, norm_reward=True)

VecEnv 里每个 worker 独立 seed,同 seed 单 env vs VecEnv 第一步 obs 应一致——不一致说明 wrapper 链或 seed 传递有 bug。32 env 并行但 CPU 打满,进程切换开销反而降 fps,要 log fps vs n_envs 曲线找 sweet spot。

5. Train 与 Eval 一致性

Train 有 TimeLimit eval 无 → length 分布不同,early termination 策略在 eval 失效。Train 有 reward scale eval 无 → return 数值不可比,选 checkpoint 等于选幻觉。Eval 必须 load 同一套 wrapper config:

python
env = VecNormalize.load("vecnormalize.pkl", venv)
env.training = False
env.norm_reward = False

换 env 版本(MuJoCo v4→v5)时 obs 维数、action bounds 显式校验,旧 stats 全部作废。Save model 时 save wrapper config 和 vecnormalize.pkl,三者版本绑定。

6. 失败模式

wrapper 在 algorithm 里硬编码,换 env 复制粘贴出错。Train 有 TimeLimit eval 无,length 分布不同。Monitor 写 csv 路径硬编码在 trainer 里,多 seed aggregate 时 parse 失败。

7. 验收

  • Wrapper 链写入 yaml/log,checkpoint 和 wrapper 版本绑定。
  • 同 seed 单 env vs VecEnv 第一步 obs 一致。
  • CI 跑 1k step smoke:wrapper 链 + env 创建 + 一步 step 不 throw。
  • Train/eval wrapper 链 diff 为零;VecNormalize stats 随 model 保存。
  • 换 env 版本时 obs 维数、action bounds 显式校验。
  • env.spec 和 wrapper 链 print 一遍存档。

8. 案例:normalize stats 未随 checkpoint 保存

某次 manipulation 实验,train return 稳步上升,eval 却接近 random——排查发现 VecNormalize 的 running stats 存在 /tmp,机器重启后 eval 脚本 load 的是默认 stats,obs 尺度完全错位。修复后 eval return 立刻对齐 train 趋势。教训:stats 和 model 同目录、同版本号打包,发布物应捆绑二者,只回滚 model 不回滚 stats 会出现「已回滚却依旧」的幻觉。

9. 与录包复盘

实验复现失败时,若 log 里没有 wrapper 链和 normalize stats 路径,只能猜 train 时 obs 是否归一化。训练脚本启动时应 print wrapper 链和 stats 文件路径,录进 experiment metadata。证据链完整,才分得清是算法问题还是 wrapper 契约问题。

Wrapper 是 env 与算法之间的契约层。顺序写进 config,stats 随 checkpoint 走,train 与 eval 对称——否则归一化成了看不见的陷阱,和未声明参数让 yaml 静默失效是同一类事故。

← 全部文章

johan's blog