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

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。
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 与职责边界
TimeLimit:max_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 一并作废。RecordEpisodeStatistics:info["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 或自动包装:
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:
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 为零;
VecNormalizestats 随 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 静默失效是同一类事故。
相关
也可以看看
- ·5 分钟阅读
墙钟时间:env steps 不是唯一成本
论文比 sample complexity;产线更关心 wall-clock 和真机交互上限——VecEnv 并行和 sim 速度决定算法可行域。
- ·16 分钟阅读
约束 MDP 的代价估计偏差:安全层看起来在工作
区分期望约束、条件风险与硬屏蔽,解释代价 critic 偏差如何让可行集虚胖;用约束违反轨迹占比、拉格朗日乘子轨迹和安全层旁路注入验收。
- ·17 分钟阅读
离线强化学习的支持集外动作:为什么 Q 值会自我抬高
从 Bellman backup 的分布外 maximization 解释外推误差;比较行为约束、保守价值与策略正则,并用覆盖率切片与 FQE 交叉验证部署边界。
johan's blog