ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

gym环境库实战:接口详解、CartPole与自定义环境全攻略

gym环境库实战:接口详解、CartPole与自定义环境全攻略 很多刚接触强化学习的同学第一个崩溃点往往不在算法推导而在环境接口。网上教程张嘴就是env.step(action)可一旦你真去写reset返回什么、step返回几个值、action_space到底是个什么对象每一步都可能把人卡住。gym 环境库就是这套交互标准的核心载体它把智能体怎么跟任务世界打交道这件抽象的事固化成了几个极其具体的方法和数据结构。你不需要懂每个环境内部的物理引擎也不用关心游戏画面怎么渲染只要会按接口规范操作就能在几十个现成任务上快速验证强化学习算法。这篇文章我会先讲清楚 gym 环境库在整个强化学习里的定位然后手把手带你把 CartPole 跑起来再从观测空间、动作空间、内置环境、自定义环境、训练循环这些角度逐一拆开揉碎。适合刚学强化学习、想看明白别人代码里环境部分在干嘛的同学也适合准备自己造环境、跑实验的读者。读完你会发现环境库没那么玄乎它更像是一张写得明明白白的接口合同剩下的只是熟练。1. 为什么所有强化学习例程都从 gym.make 开始环境接口的历史角色1.1 算法实现与任务环境为什么要解耦强化学习的最基本框架是智能体不断在环境里试错观察状态、做出动作、拿到奖励、再看新状态。听起来很简单但一旦落到代码里麻烦就来了。不同任务的世界观完全不同控制一根倒立杆要看杆的角度玩射击游戏要看画面像素让小车爬坡要看位置和速度。如果每个任务都自己写一套接口那么每换一个任务算法代码就要跟着改一遍这几乎等于没法做研究也没法做工程复用。环境库存在的意义就是把任务怎么做和算法怎么学这两件事彻底分开。算法侧只认一个抽象的环境对象不需要知道里面是物理仿真还是游戏模拟器只要负责接收观测、给出动作、消费奖励。环境侧只需要按照统一规范把状态、奖励、终止信号交出来至于状态是怎么算出来的它不关心。于是同一套策略网络、同一套更新公式就能从一个任务无缝切到另一个任务。这也是为什么你在各种强化学习教程里都看到gym.make它更像是整个实验流程的入口文件。1.2 gym 环境库的合同到底是什么我把 gym 的接口理解成一份合同你遵守这份合同就能跟它底下的几十种任务无缝协作。合同的核心是五个要素。第一是环境构造通常用gym.make(环境名)直接加载一个现成任务。第二是reset()回合开始前把环境恢复到初始状态并给你一个初始观测。第三是step(action)你把智能体当前回合要做的动作传进去环境模拟一步后返回新的观测、奖励、是否结束等信息。第四是observation_space和action_space这两个属性告诉你状态和动作的合法范围与数据结构算法网络怎么搭、输出层怎么设计全看它们。第五是render()和close()负责可视化与资源释放。类比一下它就像家电的标准化插座。你不需要知道每一台电器内部的电路怎么走只需要把插头做成统一规格插上去就能用。环境库就是那个统一规格的插座算法是电器任务是电源。协议一旦固定下来维护成本直线下降。2. 把第一个环境跑起来CartPole 的完整交互循环2.1 安装与版本先确认你拿到的是哪一代接口安装 gym 非常简单一条命令的事pip install gym装完以后我强烈建议你先做一件事打印一下环境本身并执行一次reset和一次step确认自己手上的接口是哪个版本。因为 gym 社区经历过好几次 API 调整不同教程基于不同版本代码经常对不上。import gym env gym.make(CartPole-v1) print(env.reset()) print(env.step(env.action_space.sample()))如果你的输出像这样说明你拿的是新版接口(array([ 0.034, 0.018, -0.023, 0.014]), {}) (array([ 0.035, -0.032, -0.023, 0.055]), 1.0, False, False, {})新版reset()返回两个值初始观测和一个 info 字典step()返回五个值观测、奖励、终止标志、截断标志、info。如果你的输出比这少或者只剩一个观测说明教程是在老版本接口上写的。判断清楚版本再动手比硬着头皮跑通代码重要得多。很多报错根本不是逻辑问题而是接口字段数量对不上。2.2 最小交互循环装好之后我们用 CartPole 写一个最小可运行的交互循环。CartPole 的任务很直白一根杆子立在可移动的小车上你要通过对小车施加向左或向右的力让杆子尽量保持竖直不倒下。import gym env gym.make(CartPole-v1) obs, info env.reset(seed42) print(初始观测:, obs, 额外信息:, info) for step in range(5): action env.action_space.sample() # 随机选一个合法动作 obs, reward, terminated, truncated, info env.step(action) done terminated or truncated print(f第{step 1}步 | 动作: {action} | 观测: {obs} | 奖励: {reward} | 结束: {done}) if done: obs, info env.reset() break env.close()这段代码的关键点有三个动作要直接从action_space.sample()里取保证它一定合法terminated和truncated要用or合并成done因为这俩任何一种情况都意味着本回合结束回合结束之后一定要调用reset()让环境回到新的初始状态才能开始下一回合。2.3 理解 step 返回的值观测、奖励、终止、截断、额外信息step返回的五个值乍看简单但每个都有讲究。第一个是观测也就是智能体在下一步能看到的世界状态。CartPole 的观测是一个四维向量每一项都有明确的物理含义维度含义大致范围0小车位置-4.8 到 4.81小车速度-Inf 到 Inf2杆与竖直方向夹角-24° 到 24°3杆顶端角速度-Inf 到 Inf第二个是奖励。CartPole 的奖励很简单每存活一步就加 1。所以一回合如果坚持了 200 步总回报就是 200。第三个和第四个是结束标志。这里有一个很多新手容易忽略的区分terminated表示环境自己判定任务结束比如杆倒了、车冲出了边界truncated表示环境还没结束但因为达到最大步数或外部条件被强制截断。老版本里这俩合并成一个done新版本拆开了对训练过程影响不小后面我会详细说。第五个是 info 字典一般用来放调试信息比如某些游戏的真实计分、剪枝后额外使用的数据。正常情况下训练用不到它但如果你做算法调优它是很好的参照。3. 读懂 spaces 模块观测空间与动作空间就是算法的边界条件3.1 主流空间类型和它们的形状语义observation_space和action_space是环境给算法的最重要提示你的输入是什么形状你的输出该怎么约束。这两个属性不是普通元组而是 gym 的 spaces 对象写算法之前必须先搞懂它们。最常见的几种空间类型如下空间类型含义典型场景Discrete(n)离散整数动作取值 0 到 n-1CartPole 的左右推、走迷宫的方向Box(low, high, shape)连续向量或矩阵每个元素有上下界Pendulum 的连续扭矩、图像像素矩阵MultiDiscrete(nvec)多个离散维度组合同时控制多个可调档位MultiBinary(n)元素只能取 0 或 1 的向量开关组合类控制问题Dict(spaces)多个子空间组成的字典混合观测比如同时有传感器和图像为什么要强调这是 spaces 对象因为光有形状还不够你还需要知道取值边界、到底离散还是连续、采样一个随机合法动作怎么弄。这些信息都封装在对象的方法和属性里了。3.2 从 space 对象能提取什么信息实际写代码的时候我几乎每次都会先做这样一个打印import gym import numpy as np env gym.make(CartPole-v1) print(动作空间:, env.action_space) print(观测空间:, env.observation_space) print(离散动作数:, env.action_space.n) print(观测形状:, env.observation_space.shape) random_action env.action_space.sample() print(随机动作:, random_action, type(random_action)) sample_obs env.observation_space.sample() print(随机观测:, sample_obs) print(观测下界:, env.observation_space.low) print(观测上界:, env.observation_space.high)以 CartPole 为例你会看到动作空间是Discrete(2)说明合法的动作只有 0 和 1观测空间是Box(4,)形状是(4,)。这里observation_space.low和high非常有用它直接给出了每个维度的取值范围。如果你要做输入归一化或者要判断一个状态是否越界这两个数组就是依据。另一个容易踩坑的点是类型。Discrete采样出来的动作是整数类型而很多神经网络的输出是浮点概率分布或浮点标量。如果你要自己把 softmax 输出的概率转成动作记得用int()转一下否则传进step可能会直接报错或者被环境库静默强制转换造成难以排查的行为异常。4. 内置环境群像从 CartPole 到像素级街机每个任务都是一道强化学习考题4.1 几个典型内置环境的对照与教学点gym 自带的环境非常多从最简控制问题到像素级游戏都有。环境多不是重点重点是你应该从每个任务里读出它想考你什么。我整理了几个经典型号供你挑训练用环境动作空间观测空间奖励特征它考你什么CartPole-v1离散 2Box(4,)每步 1最基础的状态表示与稳定控制Pendulum-v1连续 1 维Box(3,)每步都有负分连续动作控制、负奖励设计MountainCar-v0离散 3Box(2,)每步 -1到达山顶才结束稀疏奖励与延迟奖励需要探索LunarLander-v2离散 4Box(8,)分段奖励成功 100多维度状态、多阶段目标、燃料控制像素级街机环境离散若干图像 Box取决于游戏规则视觉输入、CNN、高维观测学习拿 Pendulum 来说它的目标是让摆锤倒立起来动作是连续扭矩。它返回的 reward 永远是一个负的二次型数值越接近理想状态越接近 0。也就是说这不是攒分越高越好的环境而是亏损越少越好。你评估算法时就得用回合总回报的回升趋势来判断而不是看单步奖励是否为正。MountainCar 则更阴险。小车在谷底发动机功率不够直接冲坡永远上不去需要先来回摇晃积蓄动能。每步扣 1 分只有到达山顶才结束。这个环境是天然的稀疏奖励试金石很多初学者用最简单的随机策略跑这个任务会发现永远结束不了从而误以为环境卡死了。其实这正是它的教学目的让你理解奖励稀疏时探索有多难。4.2 跑陌生环境之前先来一次环境体检我养成了一个习惯拿到任何陌生环境不急着写算法先做一次体检。体检脚本大概长这样import gym import numpy as np env gym.make(环境名) obs, info env.reset(seed0) print(空间信息:, env.observation_space, env.action_space) returns [] for episode in range(10): obs, info env.reset() total_reward 0 done False while not done: action env.action_space.sample() obs, reward, terminated, truncated, info env.step(action) total_reward reward done terminated or truncated returns.append(total_reward) print(f回合 {episode 1} 总回报: {total_reward}) print(随机策略平均回报:, np.mean(returns)) print(随机策略回报标准差:, np.std(returns))这个过程能让你在 5 分钟内知道动作空间有多大、观测数据长什么样、随机策略大概能拿多少分、回合最长能跑多少步。这个东西称不上高级但能避免你在一个自身设计有问题的任务上盲目调算法。比如某个环境随机策略回报的标准差极大说明单次回合的随机性很高训练时就要适当多采几条轨迹。5. 手写一个自定义环境从空实现到可训练五步走完整过程5.1 自定义环境的骨架五个必须实现的方法很多场景下内置环境不够用比如你想研究一个全新的业务调度问题、想给本科生设计一个可视化教学环境这时候就得自己写环境。别怕只要照着骨架填内容就行。我以一个 5×5 网格寻宝为例智能体从左上角出发目标是右下角的宝藏每走一步可以用上、下、左、右四个动作。状态是当前所在格子编号碰到宝藏给 1 分走 50 步没拿到就截断。import gym import numpy as np from gym import spaces class GridWalkingEnv(gym.Env): def __init__(self, size5): super().__init__() self.size size self.action_space spaces.Discrete(4) self.observation_space spaces.Discrete(size * size) self.start_state 0 self.goal_state size * size - 1 self.state None self.steps 0 self.max_steps 50 def reset(self, *, seedNone, optionsNone): super().reset(seedseed) self.state self.start_state self.steps 0 return self._get_obs(), {} def _get_obs(self): return self.state def step(self, action): row, col divmod(self.state, self.size) if action 0: row max(0, row - 1) # 上 elif action 1: row min(self.size - 1, row 1) # 下 elif action 2: col max(0, col - 1) # 左 elif action 3: col min(self.size - 1, col 1) # 右 self.state row * self.size col self.steps 1 reward 1.0 if self.state self.goal_state else 0.0 terminated self.state self.goal_state truncated self.steps self.max_steps info {} return self._get_obs(), reward, terminated, truncated, info def render(self): for row in range(self.size): line for col in range(self.size): idx row * self.size col if idx self.state: line A elif idx self.goal_state: line G else: line . print(line) def close(self): pass这个类看起来不长但它把 gym 环境的全部契约都实现了reset返回(obs, info)step返回(obs, reward, terminated, truncated, info)并且定义了action_space和observation_space。5.2 从能跑到科学可信奖励设计、随机性、终止条件代码能跑只是第一步自定义环境真正难的是设计是否合理。首先看奖励。上面这个寻宝环境用的是 0/1 奖励只有到达终点才给 1 分。如果你直接拿它训练智能体前几百回合大概率什么都学不到因为随机走 5×5 网格碰到终点的概率太低了。常见的改进是增加距离惩罚每走一步扣 0.01或者给接近终点的状态一点塑形奖励让策略梯度有信号可循。但也别奖励给得太密太密会诱导智能体原地刷奖励而不是真正走向终点。其次看随机性。如果你的环境里涉及随机初始化比如随机放置起点和终点一定要把随机数的生成交给self.np_random而不是直接用 Python 的random模块。因为reset()里调用了super().reset(seedseed)gym 会帮你维护一个可复现的随机源。直接调用super().reset(seedseed)这件事尤其重要很多私有环境写完后 seed 无效问题就出在这里。第三看终止条件。我故意在寻宝环境里加了max_steps50就是为了让训练不会无限循环。如果你不设这个上限一个永远完不成任务的环境会让训练脚本挂在那里表面上看起来是算法还在学实际上已经死循环了。terminated和truncated的语义也要分开找到宝藏是terminated步数超限是truncated这两者对后续算法的影响不同。5.3 环境注册与直接使用两种加载方式自定义环境写完后有两种用法。第一种也是最简单的方式直接实例化这个类不用通过gym.make。这样在调试单脚本时最方便你可以直接在文件里 new 一个对象然后调reset、step跑循环。缺点是没法套用 gym 的包装器和统一配置。第二种是注册到环境注册表这样就可以像内置环境一样gym.make(GridWalk-v0)加载。方法是在模块里调用注册函数from gym.envs.registration import register register( idGridWalk-v0, entry_point你的模块名:GridWalkingEnv, max_episode_steps50, )注册的好处是统一管理你可以通过max_episode_steps给环境加上默认的时间截断还能配合后续要讲的并行环境工具一并用。但我得提醒你如果只在单个脚本里做实验注册反而多一层负担不如直接实例化省得处理模块路径问题。6. 写训练循环时的环境侧细节重置、截断、并行与随机种子的连锁反应6.1 最容易写错的循环结构环境代码本身写清楚了还要写对使用它的训练循环。我见过太多新手在循环结构上阴沟翻船。标准的智能体与环境交互大致是这样obs, info env.reset() for step in range(max_steps_per_episode): action agent.act(obs) next_obs, reward, terminated, truncated, info env.step(action) agent.learn(obs, action, reward, next_obs, terminated, truncated) obs next_obs if terminated or truncated: obs, info env.reset() break这里有个顺序问题obs必须在reset()之后立刻拿到接着在步进后立刻更新为next_obs而不是在图省事的情况下把reset()放在循环开头。如果每步都调一次reset()环境永远不会有进展如果不及时更新obs智能体拿到的就是上一时刻的状态时序就乱了。6.2 时间限制与回合统计的陷阱当我们从环境拿到terminatedTrue或truncatedTrue时很多训练算法都会把它们统一当成回合结束来处理。在简单代码里这问题不大但如果你写的是带自举的算法比如 DQN 或者带有价值网络的强化学习就必须区分。道理很简单terminatedTrue意味着任务真正结束之后没有下一状态价值目标里不应该加未来回报truncatedTrue只是步数到了上限任务本身还可以继续这时候如果也把未来回报砍掉会严重低估这个状态的价值。CartPole 里 500 步的截断就是一个典型例子。很多用老接口done一把抓的训练脚本在 CartPole 上长期训练后会出现价值估计偏差根子就在这里。我建议在训练循环里把这两个标志分开存进 info或者至少让 loss 计算时分别处理。如果你的算法库不区分那就把截断状态单独标记下来在计算回报时按未来还有延续的方式处理。6.3 多环境并行与随机种子的管理单环境交互的样本速度往往不够。实际训练里通常一次性开多个并行的环境副本让智能体同时采集大量轨迹再用这些样本更新网络。多环境并行在接口上不算复杂核心是向量化多个环境实例打包成一个向量环境对象一次step同时推进所有副本。并行环境的随机种子管理是重灾区。如果你给每个环境设置同一个 seed它们初始状态完全一样采集到的轨迹天然重复训练多样性大打折扣。正确做法是让每个副本获得不同的 seeddef make_env(rank, seed_base0): def _thunk(): env gym.make(CartPole-v1) env.reset(seedseed_base rank) return env return _thunk同时np.random.seed、Python 内置random.seed、环境的 random source 是三套互相独立的随机体系。很多实验复现不出来不是因为算法有随机性而是只给环境设了 seed忘了给 PyTorch 或 NumPy 设置种子。想要可复现所有随机源都要在入口统一初始化。7. 踩过的坑和绕过的弯gym 环境使用的七条经验7.1 版本接口不同引发的连锁问题我见过最浪费时间的一次 debug是把老教程里的done四元组硬套到新版本环境上。step返回五个值之后解包直接报错。这种问题表面上是语法错误实际上是整个接口语义的升级。遇到这种情况先打印一下当前环境的返回结构再对照教程去改比盲猜快得多。7.2 渲染用于检查不要开着训练render()确实能让你看到智能体在做什么但代价是大量 CPU 和内存开销。我早期经常开着渲染训练觉得看着它在学很有感觉结果一个步骤从毫秒级拖到几百毫秒训练速度掉了一个数量级。正确做法是训练时完全关闭渲染实在想看就把模式调成rgb_array隔一段时间手动采样一帧存下来观察。7.3 陌生环境先跑随机策略这一步我已强调过但值得再放一遍任何新环境先跑 10-20 回合随机策略记录奖励均值和回合长度。这不是浪费时间而是在给算法设定一个下限参照。如果随机策略的平均回报是负分而你的算法跑了一百回合还在负分附近说明训练可能没生效如果随机策略就能拿到正分说明任务太简单可能检验不出算法的提升。这个参照几乎能帮你在所有环境上快速判断训练是否进入正轨。7.4 奖励尺度会直接影响超参不同环境的奖励量级差得很远CartPole 是每步 1Pendulum 是每步负的小数某些环境一回合能拿上百分。同一个学习率在 CartPole 上可能没问题换到 LunarLander 就震荡到飞起。尽可能先把奖励归一化或者在算法层面对 reward 做标准化。自定义环境时尽量把奖励控制在容易比较的量级能少很多调参的苦。7.5 空间类型别想当然很多人拿到环境后不打印observation_space直接按自己的想象给神经网络输入。比如把Box(4,)当成(4,1)输入或者把Discrete(4)的动作空间当成连续值回归。我的习惯是先打印空间的 shape、low、high再写网络结构。动作层尤其要留意离散动作用分类头连续动作用带 tanh 或 scale 的输出层一旦弄混算法怎么训都不收敛。7.6 自定义环境要刻意制造随机性一个完全确定性的自定义环境训练出来是没法泛化的。比如前面的网格寻宝如果起点固定、终点固定、地图固定算法可能学会的只是一条记忆路径换个地图立刻失效。合理的做法是把起点和终点在每次reset时随机摆放但记得用self.np_random生成随机数保证 seed 可控。7.7 先做环境测试再写完整算法最后一个经验来自一次惨痛教训。我花了很长时间写了一个定制环境直接上手训练完整算法结果怎么调都不收敛最后发现自己写的终止条件有 bug某些状态下reward没给对导致智能体学到的是原地不动。从那以后我每次写完环境都会先拿随机策略反复跑几十回合逐字段打印观测、奖励、结束标志确认环境行为符合预期才敢把 agent 接上去。这个习惯给我省下的时间远超写测试本身花掉的时间。说到底gym 环境库的价值不只是提供一堆现成游戏它真正教会我的是把任务和算法分层思考的能力。无论你以后是继续研究新算法还是把强化学习落地到具体业务这套先理解接口、再做环境体检、最后动手训练的流程都通用。希望这篇拆解能让你下次看到gym.make时少一点迷茫多一点底气。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表