- gym 是 OpenAI 做的一套开源 RL 环境,在强化学习研究中使用非常广泛,贴一段 gym github 仓库的简介
Gym is an open source Python library for developing and comparing reinforcement learning algorithms by providing a standard API to communicate between learning algorithms and environments, as well as a standard set of environments compliant with that API. Since its release, Gym’s API has become the field standard for doing this.
- 本文对 gym 库进行简单介绍,主要参考
- github 库:openai/gym
- 官方文档:Gym Documentation
文章目录
1. 安装 gym
- 目前最新版 gym(0.26.2) 支持 python 3.7,3.8,3.9,3.10,安装前确保虚拟环境版本正确
- gym 包含 Classic Control、Atari、MuJoCo、Box2D、Toy Text 几组官方环境,可以按需求安装其中一部分
pip install gym
:安装基本的 gym 库,只含有 Classic Control 环境pip install gym[atari]
:安装 Atari 环境支持组件注意现在最新版的 gym 不再自带 Atari 游戏的 ROM 本体,需要手动到 Atari 2600 VCS ROM Collection 下载,得到一个 ROMS 文件夹,再使用命令
ale-import-roms ROMS/
进行导入。具体可以参考 Gym Atari: Gym no longer distributes ROMspip install gym[box2D]
:安装 Box2D 环境支持组件- 其他组件安装同理…
pip install gym[all]
:安装所有环境的支持组件
- 另外,gym 也允许自定义环境,自环境定义必须符合 gym 的 API 标准。假设你定义了
env
环境,可以使用如下代码检查env
是否满足 gym API 标准,同时这个还能检查你的实现是否遵循了最佳实践from gym.utils.env_checker import check_env check_env(env)
2. 基础使用
2.1 Agent-Env Loop
- 简单说一下 RL 中经典的 “agent-environment loop”
每个 timestep,agent 向环境输入一些控制信号(action),然后观测到环境奖励和状态变化。RL 的目标是以某种特定的方式操纵环境,如果 agent 取得某些进展,就应得到正向的环境奖励。一些 timestep 之后,我们可能希望环境重置到某个初始状态,这时环境应该发送一个 done 信号来激发重置行为,重置时刻包括- agent 出现 “灾难性失败”(比如撞墙了)
- agent 完成了任务
- 轨迹长度(timestep 数)超过设定上限
- 利用 gym 库可以简便地实现 “agent-environment loop”,下面给出一个例子(需要安装 Box2D 组件)
运行这段程序就会自动弹出如下窗口import gym env = gym.make("LunarLander-v2", render_mode="human") env.action_space.seed(42) observation, info = env.reset(seed=42) for _ in range(1000): observation, reward, terminated, truncated, info = env.step(env.action_space.sample()) # 完成任务 或 失败 if terminated or truncated: observation, info = env.reset() env.close()
两个注意事项:- 为了保证可重复性,环境和动作空间都需要设置随机种子
- 通过
gym.make()
方法创建环境实例时,render_moder
参数设置为"human"
,这样才能在屏幕上显示游戏画面Note:以前的 gym 版本好像没有
render_mode
相关的设置,另外以前版本执行env.reset()
时不会返回info
2.2 Spaces
-
任何一个环境都应该有
action_space
和observation_space
两个属性,它们指定 agent 可行动作和观测的格式。这两个属性都应该是Space
类的某个子类的实例,包括类名 描述 gym.spaces.Box
描述一个 n 维连续空间,我们可以定义取值范围的上下界 gym.spaces.Discrete
描述一个指定大小为 n n n 的离散空间 { 0 , 1 , . . . , n − 1 } \{0,1,...,n-1\} {0,1,...,n−1},通过设置 start
参数可以将其平移至 { a , a + 1 , . . . , a + n − 1 } \{a,a+1,...,a+n-1\} {a,a+1,...,a+n−1}gym.spaces.MultiBinary
描述一个指定尺寸的 0-1 空间,参数可以是一个数 a
或一个列表[a,b,c,...]
,分别得到维度为(a,)
和(a,b,c,...)
的 ndarraygym.spaces.MultiDiscrete
描述一组 gym.spaces.Discrete
组成的空间,输入是一个列表,每个元素指定一个Discrete
大小gym.spaces.Dict
描述由其他空间组成的字典 gym.spaces.Tuple
描述由其他空间组成的元组 -
对
Space
类实例调用.sample()
方法,可以从空间中采样from gym.spaces import Box, Discrete, Dict, Tuple, MultiBinary, MultiDiscrete import numpy as np space1 = Box(low=-1.0, high=2.0, shape=(3,), dtype=np.float32) space1.sample() # array([-0.9305908 , -0.33117652, -0.22603005], dtype=float32) space2 = Discrete(4) space2.sample() # 3 space3 = Discrete(5, start=-2) space3.sample() # -2 space4 = MultiBinary(5) space4.sample() # array([1, 0, 0, 1, 0], dtype=int8) space5 = MultiBinary([2,5]) space5.sample() # array([[1, 1, 0, 1, 1], # [1, 0, 0, 1, 1]], dtype=int8) space6 = MultiDiscrete([ 5, 2, 2 ]) space6.sample() # array([4, 1, 1], dtype=int64) space7 = Dict({"position": Discrete(2), "velocity": Discrete(3)}) space7.sample() # OrderedDict([('position', 1), ('velocity', 0)]) space8 = Tuple((Discrete(2), Discrete(3))) space8.sample() # (0, 2) space9 = Tuple((space7, space8)) space9.sample() # (OrderedDict([('position', 0), ('velocity', 2)]), (0, 2))
2.3 Wrappers
-
有时我们不满意 gym 环境原生的动作空间、观测空间或奖励函数设计,这时就可以用
Wrappers
包装类对其进行简单修改,Wrappers 实例在 “agent-environment loop” 中的地位如下
-
要使用
Wrappers
类,必须先构造一个 base 环境,用这个基环境实例和包装参数一起作为参数去构造包装类。大多数简单包装都可以通过继承ActionWrapper
,ObservationWrapper
或RewardWrapper
类重写来简单地实现;对于过于复杂的需求(比如基于env.step()
返回的info
信息修改 reward),就需要继承Wrapper
基类进行重写 -
gym 也提供了一些预定义的 Wrapper 类,比如
包装类名 描述 gym.wrappers.TimeLimit
如果超过给定的最大 timestep (或者 base environment 已经发出done信号),则发出done信号 gym.wrappers.ClipAction
自动对 agent 动作进行裁剪,使其落入可行的动作空间中( Box
类型)gym.wrappers.RescaleAction
自动将动作放缩到指定的间隔内 gym.wrappers.TimeAwareObservation
将有关 timestep 索引的信息添加到观察中 import gym from gym.wrappers import RescaleAction, TimeAwareObservation, ClipAction import numpy as np base_env = gym.make('CartPole-v1') wrapped_env = TimeAwareObservation(base_env) print(wrapped_env.reset()[0]) # [-0.00228092 -0.04528544 -0.01722934 -0.01119073 0. ] 最后一维是 timestep 索引 print(wrapped_env.step(wrapped_env.action_space.sample())[0]) # [-0.00318663 0.15007931 -0.01745316 -0.30925953 1. ] base_env = gym.make("BipedalWalker-v3") print(base_env.action_space) # Box(-1.0, 1.0, (4,), float32) wrapped_env = RescaleAction(base_env, min_action=0, max_action=np.array([0.0, 0.5, 1.0, 0.75])) print(wrapped_env.action_space) # Box(0.0, [0. 0.5 1. 0.75], (4,), float32) base_env = gym.make('BipedalWalker-v3') wrapped_env = ClipAction(base_env) print(wrapped_env.action_space) # Box(-1.0, 1.0, (4,), float32) 这和 base env 的动作空间一致,但是现在带有了裁剪功能 wrapped_env.reset() wrapped_env.step(np.array([5.0, 2.0, -10.0, 0.0])) # Executes the action np.array([1.0, 1.0, -1.0, 0]) in the base environment
-
包装可以嵌套使用,对于一个经过多层包装的环境,其
.env
属性去除一层包装,.unwrapped
属性去除所有包装,去除过程直到 base env 为止。请看如下示例import gym from gym.wrappers import TimeAwareObservation, ClipAction base_env = gym.make('BipedalWalker-v3') wrapped_env = ClipAction(base_env) double_wrapped_env = TimeAwareObservation(wrapped_env)
>>> double_wrapped_env <TimeAwareObservation<ClipAction<TimeLimit<OrderEnforcing<PassiveEnvChecker<BipedalWalker<BipedalWalker-v3>>>>>>> >>> double_wrapped_env.env <ClipAction<TimeLimit<OrderEnforcing<PassiveEnvChecker<BipedalWalker<BipedalWalker-v3>>>>>> >>> double_wrapped_env.unwrapped <gym.envs.box2d.bipedal_walker.BipedalWalker at 0x1b54e128340>
从这里也可看出,gym 原生环境已经经过了一些包装
2.4 手动交互
- 使用
gym.utils.play
方法,可以把环境的动作输入映射到键盘上,实现环境的手动交互 - 这时需要人为设定一个键位-动作映射字典,形如
dict[tuple[int], int | None]
,比如想要按 w 或者空格时执行动作 2,这个字典形如
另外,也可以使用{ # ... (ord('w'), ord(' ')): 2, # ... }
pygame
包提供的 key ID 来构造上述字典,比如pygame.K_UP
就是方向上键。如果没有向play
方法传入这个字典,会尝试调用环境默认的映射关系 - 如下代码可以使用方向上下键玩乒乓球游戏
import gym import pygame from gym.utils.play import play mapping = {(pygame.K_UP,): 2, (pygame.K_DOWN,): 3} env = gym.make("Pong-v0", render_mode='rgb_array') # 渲染模式必须设为 rgb_array 或 rgb_array_list 才能交互 play(env, keys_to_action=mapping)
3. Core API 简介
- 这一节只简单介绍 gym官方文档 中 API 的 Core 部分
3.1 gym.Env 类
3.1.1 gym.Env 类方法
gym.Env.step(self, action: ~ActType) -> Tuple[~ObsType, float, bool, bool, dict]
- 环境根据 agent 动作运行一个 timestep
- 输入 action,返回元组 (observation, reward, terminated, truncated, info)
- 返回的 terminated 和 truncated 分别标识成功或失败,有一个 True 就应该 reset 环境
- info 字典包含有助于调试或分析的附加信息,比如描述 agent 表现状态的度量,观测中的隐藏变量或组成总 reward 的各个成分 reward,以及区分 terminated 和 truncated 的信息。在以前版本的 gym 好像不会返回这个
gym.Env.reset(self, *, seed: Optional[int] = None, options: Optional[dict] = None) → Tuple[ObsType, dict]
- 将环境重置为初始状态,并返回初始观察值
- 输入可选的随机种子 seed 和可选的附加操作 options,返回元组 (observation, info)
- 如果 seed 参数输入 int 而非 None,就会设置环境的随机数生成器。构造环境实例后应该立即用 int 随机数种子进行 reset,之后 reset 时不要再带 seed。另外如果初次调用时
seed=None
,则会利用某个熵源(如时间戳)随机设置随机数生成器 - options 指定 reset 环境时的附加信息,根据具体环境需求设置
- 返回的 observation,info 同
gym.Env.step
gym.Env.render(self) → Optional[Union[RenderFrame, List[RenderFrame]]]
- 按照
gym.make()
时指定的render_mode
属性值计算渲染帧 - 不同环境支持的
render_mode
不同 (有些第三方环境可能根本不支持渲染)。关于render_mode
的约定为None (default)
: 不计算渲染human
:调用返回 None,会在终端不断渲染画面,供人员观察用rgb_array
:调用返回代表环境当前状态的单个 frame,它是 shape(x,y,3) 的 numpy.ndarray,表示 x × \times ×y 像素图像的RGB值rgb_array_list
:调用返回返回自上次重置以来代表环境状态的 frame 列表,frame 定义同上ansi
:返回一个 str 或 StringIO。StringIO 包含每个时间步长的终端样式文本表示(可以包括换行符和ANSI转义序列)。只有少数环境需要用这种渲染方式
- 按照
gym.Env.close(self)
:- 当 python 垃圾回收机制触发或程序退出时,环境会自动调用这个方法来关闭
- 在子类继承重写此方法来实现必要的清理工作
3.1.2 gym.Env 类属性
Env.action_space: Space[ActType]
:给出环境指定的有效 action 格式,是 2.2 节的 Space 类实例Env.observation_space: Space[ObsType]
:给出环境指定的有效 observation 格式,是 2.2 节的 Space 类实例Env.reward_range = (-inf, inf)
:给出代表 reward 取值范围的元组,默认 (-inf, +inf),可以设置成更窄的范围
3.2 gym.Wrapper 类
class gym.Wrapper(env: Env)
:- 所有包装类基类,包装类实例都会根据 2.3 节逻辑在调用
gym.Env.step()
和gym.Env.reset()
时发挥作用 - 这个不能直接用,使用时需要继承并重写其中的某些方法来控制包装行为
- 所有包装类基类,包装类实例都会根据 2.3 节逻辑在调用
3.2.1 gym.ObservationWrapper 子类
class gym.ObservationWrapper(env: Env)
:- 所有 observation 包装类的超类
- 通过继承
ObservationWrapper
并重写observation()
方法实现对原始 observation 的包装。包装方法必须取值于 base env 的 observation space,然后映射到一个新的 observation space,如果新 observation space 和原先的不同,可以在__init__()
时通过self.observation_space
参数进行设置 - 以 2D 导航任务为例,base env 的观测是一个 key 包括 “agent_position” 和 “target_position” 的字典。常见的做法可能是丢掉一些自由度,只考虑目标相对于代理的位置,这个包装可以如下实现
class RelativePosition(gym.ObservationWrapper): def __init__(self, env): super().__init__(env) self.observation_space = Box(shape=(2,), low=-np.inf, high=np.inf) # 新的观测空间 # 实现包装逻辑 def observation(self, obs): return obs["target"] - obs["agent"]
- 前文 2.3 节的
TimeAwareObservation
和TimeLimit
都是gym.ObservationWrapper
的子类
3.2.2 gym.RewardWrapper 子类
class gym.RewardWrapper(env: Env)
- 所有 reward 包装类的超类
- 通过通过继承
RewardWrapper
并重写reward()
方法实现对原始 reward 的包装,在__init__()
时通过self.reward_range
参数设置包装后 reward 的取值范围 - 例子:有时我们希望将奖励限制在一个范围内以获得一定的数值稳定性,这个包装可以如下实现
class ClipReward(gym.RewardWrapper): def __init__(self, env, min_reward, max_reward): super().__init__(env) self.min_reward = min_reward self.max_reward = max_reward self.reward_range = (min_reward, max_reward) def reward(self, reward): return np.clip(reward, self.min_reward, self.max_reward)
3.2.3 gym.ActionWrapper 子类
class gym.ActionWrapper(env: Env)
- 所有 action 包装类的超类
- 通过继承
ActionWrapper
并重写action()
方法实现对原始 reward 的包装,在__init__()
时通过self.action_space
参数设置包装后 action 的取值 space - 例子:base env 的 action_space 是
gym.spaces.Box
类型的连续空间,但现在想将其离散化为gym.spaces.Discrete
类型的离散空间,这个包装可以如下实现class DiscreteActions(gym.ActionWrapper): def __init__(self, env, disc_to_cont): super().__init__(env) self.disc_to_cont = disc_to_cont self.action_space = Discrete(len(disc_to_cont)) def action(self, act): return self.disc_to_cont[act] if __name__ == "__main__": env = gym.make("LunarLanderContinuous-v2") wrapped_env = DiscreteActions(env, [np.array([1,0]), np.array([-1,0]), np.array([0,1]), np.array([0,-1])]) print(wrapped_env.action_space) #Discrete(4)
- 前文 2.3 节的
ClipAction
和RescaleAction
都是gym.ActionWrapper
的子类