1 基本用法
1.1 初始化环境
在Gym中初始化环境非常简单,可以通过以下代码完成:
import gym
env = gym.make('CartPole-v0')
1.2 与环境交互
Gym实现了经典的“智能体-环境循环”:
智能体在环境中执行一些动作(通常是向环境传递一些控制输入,例如电机的扭矩输入)并观察环境状态的变化。一次这样的动作-观测交换称为一个时间步。
在强化学习中,目标是以某种特定方式操控环境。例如,我们希望智能体将机器人导航到空间中的某个特定点。如果成功做到这一点(或朝该目标取得了一些进展),智能体将在该时间步获得正奖励。奖励也可能是负的或0,如果智能体尚未成功(或未取得任何进展)。然后,智能体将被训练以最大化其在多个时间步中累积的奖励。
经过一些时间步后,环境可能进入终止状态。例如,机器人可能撞毁了!在这种情况下,我们希望将环境重置为一个新的初始状态。如果环境进入这样的终止状态,则会向智能体发出done
信号。并非所有done
信号都必须由“灾难性失败”触发:有时我们也希望在固定数量的时间步后发出done
信号,或者智能体在环境中完成某些任务后发出done
信号。
让我们看看在Gym中智能体-环境循环是什么样子的。此示例将运行一个LunarLander-v2
环境实例1000个时间步。由于我们传递了render_mode="human"
,你应该会看到一个弹出窗口渲染环境。
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()
输出应如下图所示:
每个环境通过提供一个env.action_space
属性来指定有效动作的格式。类似地,env.observation_space
指定有效观测的格式。在上述示例中,我们通过env.action_space.sample()
采样随机动作。请注意,我们需要将动作空间与环境分开设置种子,以确保可重现的样本。
1.3 空间(Spaces)
空间通常用于指定有效动作和观测的格式。每个环境都应该具有action_space
和observation_space
属性,这两个属性都应该是从Space
类继承的类的实例。Gym中有多种空间类型可用:
- Box:描述一个n维连续空间。这是一个有界空间,我们可以定义上下限,描述观测可以取的有效值。
- Discrete:描述一个离散空间,其中{0, 1, …, n-1}是观测或动作可以取的可能值。可以使用可选参数将值移动到{a, a+1, …, a+n-1}。
- Dict:表示一个简单空间的字典。
- Tuple:表示一个简单空间的元组。
- MultiBinary:创建一个n形状的二进制空间。参数n可以是一个数字或一个数字列表。
- MultiDiscrete:由一系列离散动作空间组成,每个元素中的动作数不同。
示例代码如下:
from gym.spaces import Box, Discrete, Dict, Tuple, MultiBinary, MultiDiscrete
import numpy as np
# Box空间示例
observation_space = Box(low=-1.0, high=2.0, shape=(3,), dtype=np.float32)
print(observation_space.sample())
# 输出示例:[ 1.6952509 -0.4399011 -0.7981693]
# Discrete空间示例
observation_space = Discrete(4)
print(observation_space.sample())
# 输出示例:1
observation_space = Discrete(5, start=-2)
print(observation_space.sample())
# 输出示例:-2
# Dict空间示例
observation_space = Dict({"position": Discrete(2), "velocity": Discrete(3)})
print(observation_space.sample())
# 输出示例:OrderedDict([('position', 0), ('velocity', 1)])
# Tuple空间示例
observation_space = Tuple((Discrete(2), Discrete(3)))
print(observation_space.sample())
# 输出示例:(1, 2)
# MultiBinary空间示例
observation_space = MultiBinary(5)
print(observation_space.sample())
# 输出示例:[1 1 1 0 1]
# MultiDiscrete空间示例
observation_space = MultiDiscrete([5, 2, 2])
print(observation_space.sample())
# 输出示例:[3 0 0]
1.4 包装器(Wrappers)
包装器是一种便捷的方法,可以在不直接修改底层代码的情况下修改现有环境。使用包装器可以避免大量样板代码,并使环境更加模块化。包装器还可以被链接以组合它们的效果。通过gym.make
生成的大多数环境已经默认被包装。
为了包装一个环境,首先必须初始化一个基础环境。然后可以将该环境及(可能是可选的)参数传递给包装器的构造函数:
import gym
from gym.wrappers import RescaleAction
base_env = gym.make("BipedalWalker-v3")
print(base_env.action_space)
# 输出:Box([-1. -1. -1. -1.], [1. 1. 1. 1.], (4,), float32)
wrapped_env = RescaleAction(base_env, min_action=0, max_action=1)
print(wrapped_env.action_space)
# 输出:Box([0. 0. 0. 0.], [1. 1. 1. 1.], (4,), float32)
包装器通常用于以下三种非常常见的操作:
- 在将动作应用于基础环境之前转换动作。
- 转换基础环境返回的观测。
- 转换基础环境返回的奖励。
可以通过继承ActionWrapper
、ObservationWrapper
或RewardWrapper
并实现相应的转换来轻松实现这些包装器。
然而,有时你可能需要实现一个包装器来进行一些更复杂的修改(例如,基于信息中的数据修改奖励)。可以通过继承Wrapper
来实现这些包装器。Gym已经为你提供了许多常用的包装器。一些例子包括:
- TimeLimit:如果超过最大时间步数(或基础环境发出
done
信号),则发出done
信号。 - ClipAction:将动作剪辑到动作空间内(类型为Box)。
- RescaleAction:将动作重新缩放到指定区间内。
- TimeAwareObservation:向观测中添加时间步索引的信息。在某些情况下,这有助于确保转换是马尔可夫的。
如果你有一个包装过的环境,并希望获取所有包装层下的未包装环境(以便手动调用函数或更改环境的一些底层方面),可以使用.unwrapped
属性。如果环境已经是基础环境,.unwrapped
属性将返回自身。
print(wrapped_env)
# 输出:<RescaleAction<TimeLimit<BipedalWalker<BipedalWalker-v3>>>>
print(wrapped_env.unwrapped)
# 输出:<gym.envs.box2d.bipedal_walker.BipedalWalker object at 0x7f87d70712d0>
1.5 在环境中游戏
你还可以使用键盘在环境中进行游戏,使用gym.utils.play
中的play
函数。
from gym.utils.play import play
play(gym.make('Pong-v0'))
这将打开一个环境窗口,允许你使用键盘控制智能体。
使用键盘进行游戏需要一个键-动作映射。此映射应为
类型dict[tuple[int], int | None]
,将按下的键映射到执行的动作。例如,如果同时按下键w
和空格键应执行动作2,则key_to_action
字典应如下所示:
{
# ...
(ord('w'), ord(' ')): 2,
# ...
}
更完整的示例:假设我们希望使用左右箭头键玩CartPole-v0
。代码如下:
import gym
import pygame
from gym.utils.play import play
mapping = {(pygame.K_LEFT,): 0, (pygame.K_RIGHT,): 1}
play(gym.make("CartPole-v0"), keys_to_action=mapping)
其中,我们从pygame
获取相应的键ID常量。如果未指定key_to_action
参数,则使用该环境提供的默认key_to_action
映射(如果有)。
此外,如果希望在游戏过程中实时绘制统计数据,可以使用gym.utils.play.PlayPlot
。以下是绘制最后5秒游戏奖励的示例代码:
def callback(obs_t, obs_tp1, action, rew, done, info):
return [rew,]
plotter = PlayPlot(callback, 30 * 5, ["reward"])
env = gym.make("Pong-v0")
play(env, callback=plotter.callback)
2 gym.Env 类
gym.Env
是一个用于定义环境的基本类。它包含几个重要方法,如 step
、reset
和 render
,用于与环境进行交互和显示。
2.1 step()方法
gym.Env.step(self, action: ActType) -> Tuple[ObsType, float, bool, bool, dict]
执行环境的一个时间步长。
当达到一个回合的结尾时,你需要调用 reset()
来重置环境的状态。此方法接受一个动作,并返回一个元组 (observation, reward, terminated, truncated, info)
。
-
参数:
action (ActType)
:由智能体提供的动作。
-
返回值:
observation (object)
:环境的观测空间中的一个元素。例如,这可能是一个包含某些对象位置和速度的 NumPy 数组。reward (float)
:执行动作所获得的奖励。terminated (bool)
:是否达到任务的终止状态(根据任务的马尔可夫决策过程定义)。如果是,进一步的step()
调用可能会返回未定义的结果。truncated (bool)
:是否满足 MDP 范围之外的截断条件。通常是时间限制,也可以用于指示智能体物理超出边界。可以用于在达到终止状态之前提前结束回合。info (dictionary)
:包含辅助诊断信息(对调试、学习和记录有帮助)。例如,可以包含描述智能体性能状态的度量、观测中隐藏的变量、或单独的奖励项信息。
-
已弃用:
done (bool)
:表示回合是否结束的布尔值。在某些情况下可能发出done
信号,比如任务成功解决、超过时间限制或物理模拟进入无效状态。
2.2 reset()方法
gym.env.reset(self, *, seed: int | None = None, options: dict | None = None) -> Tuple[ObsType, dict]
重置环境到初始状态,并返回初始观测。
此方法可以重置环境的随机数生成器,如果 seed
是整数或环境尚未初始化随机数生成器。如果已有随机数生成器且 seed=None
,则不重置 RNG。
-
参数:
seed (optional int)
:用于初始化环境伪随机数生成器的种子。如果环境没有 PRNG 且seed=None
,将从某些熵源中选择一个种子(如时间戳或/dev/urandom
)。如果传递整数,即使已有 PRNG 也将重置。options (optional dict)
:指定如何重置环境的附加信息(可选,取决于具体环境)。
-
返回值:
observation (object)
:初始状态的观测。这是observation_space
中的一个元素(通常是 NumPy 数组),类似于step()
返回的观测。info (dictionary)
:补充观测的辅助信息字典,与step()
返回的info
类似。
2.3 render()方法
gym.env.render(self) -> RenderFrame | List[RenderFrame] | None
根据环境初始化时指定的 render_mode
计算渲染帧。
支持的模式因环境而异。(一些第三方环境可能不支持渲染)。通常,若 render_mode
为:
None
(默认):不计算渲染。human
:返回None
。环境在当前显示或终端中连续渲染,通常用于人类消费。rgb_array
:返回一个表示环境当前状态的单帧。帧是一个形状为(x, y, 3)
的 NumPy 数组,表示 x-by-y 像素图像的 RGB 值。rgb_array_list
:返回一个帧列表,表示自上次重置以来的环境状态。每个帧是形状为(x, y, 3)
的 NumPy 数组。ansi
:返回字符串(str
)或StringIO.StringIO
,每个时间步长包含一个终端样式的文本表示。文本可以包含换行符和 ANSI 转义序列(如颜色)。
注意
确保类的元数据 render_modes
键包括支持的模式列表。在实现中建议调用 super()
以利用此方法的功能。
2.4 close()方法
gym.Env.close(self)
- 在子类中重写
close
以执行任何必要的清理。 - 环境在垃圾回收或程序退出时会自动调用
close()
。
3 属性
3.1 Env.action_space: Space[ActType]
此属性给出有效动作的格式。它的数据类型是由 Gym 提供的 Space
。例如,如果动作空间是 Discrete
类型,并给出值 Discrete(2)
,这意味着有两个有效的离散动作:0 和 1。
env.action_space
# 输出:Discrete(2)
3.2 Env.observation_space: Space[ObsType]
此属性给出有效观测的格式。它的数据类型是由 Gym 提供的 Space
。例如,如果观测空间是 Box
类型,并且对象的形状为 (4,)
,这表示一个有效的观测将是一个包含4个数字的数组。我们也可以通过属性检查 Box
的边界。
env.observation_space
# 输出:Box(-3.4028234663852886e+38, 3.4028234663852886e+38, (4,), float32)
env.observation_space.high
# 输出:array([4.8000002e+00, 3.4028235e+38, 4.1887903e-01, 3.4028235e+38], dtype=float32)
env.observation_space.low
# 输出:array([-4.8000002e+00, -3.4028235e+38, -4.1887903e-01, -3.4028235e+38], dtype=float32)
3.3 Env.reward_range = (-inf, inf)
此属性是一个元组,对应于可能的最小和最大奖励。默认范围设置为 (-inf, +inf)
。如果需要更窄的范围,可以自行设置。
4 包装器
4.1 gym.Wrapper
class gym.Wrapper(env: Env)
用于包装一个环境,以允许对 step()
和 reset()
方法进行模块化转换。
此类是所有包装器的基类。子类可以重写某些方法以更改原始环境的行为,而无需修改原始代码。
注意
如果子类重写了 __init__()
,不要忘记调用 super().__init__(env)
。
4.2 gym.ObservationWrapper
class gym.ObservationWrapper(env: Env)
包装器的超类,可以使用 observation()
方法修改 reset()
和 step()
的观测。
如果希望在将基础环境返回的观测传递给学习代码之前应用某个函数,可以简单地继承 ObservationWrapper
并重写方法 observation()
以实现该转换。该方法中定义的转换必须在基础环境的观测空间中定义。然而,它可能在不同的空间中取值。在这种情况下,需要在包装器的 __init__()
方法中设置 self.observation_space
以指定新的观测空间。
例如,你可能有一个二维导航任务,环境返回的观测是带有键 "agent_position"
和 "target_position"
的字典。常见的做法是丢弃一些自由度,只考虑目标相对于智能体的位置,即 observation["target_position"] - observation["agent_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"]
此外,Gym 提供了 TimeAwareObservation
观测包装器,它向观测中添加关于时间步索引的信息。
4.3 gym.RewardWrapper
class gym.RewardWrapper(env: Env)
包装器的超类,可以修改 step()
返回的奖励。
如果希望在将基础环境返回的奖励传递给学习代码之前应用某个函数,可以简单地继承 RewardWrapper
并重写方法 reward()
以实现该转换。此转换可能会更改奖励范围;可以通过在 __init__()
中定义 self.reward_range
来指定包装器的奖励范围。
让我们看一个例子:有时(尤其是在我们无法控制奖励时,因为它是内在的),我们希望将奖励剪裁到一个范围内,以获得一些数值稳定性。为此,我们可以实现如下包装器:
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)
4.4 gym.ActionWrapper
class gym.ActionWrapper(env: Env)
包装器的超类,可以在 env.step()
之前修改动作。
如果希望在将动作传递给基础环境之前应用某个函数,可以简单地继承 ActionWrapper
并重写方法 action()
以实现该转换。该方法中定义的转换必须在基础环境的动作空间中取值。然而,其定义域可能与原始动作空间不同。在这种情况下,需要在包装器的 __init__()
方法中设置 self.action_space
以指定新的动作空间。
假设你有一个动作空间类型为 gym.spaces.Box
的环境,但只希望使用有限的动作子集。然后,可以实现如下包装器:
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)
此外,Gym 提供了其他动作包装器,如 ClipAction
和 RescaleAction
。