创建自定义环境¶
本页简要概述了如何在 Gymnasium 中创建自定义环境,有关更完整的教程(含渲染),请先阅读基本用法再阅读本页。
我们将实现一个非常简单的游戏,称为 GridWorldEnv
,它由一个固定大小的二维方形网格组成。代理可以在每个时间步长在网格单元之间垂直或水平移动,代理的目标是导航到网格上在剧集开始时随机放置的目标。
有关游戏的基本信息
观察提供目标和代理的位置。
我们的环境中有 4 个离散动作,分别对应于动作“右”,“上”,“左”和“下”。
当代理导航到目标所在的网格单元时,环境将结束(终止)。
代理只有在到达目标时才会获得奖励,即,当代理到达目标时奖励为 1,否则为 0。
环境 __init__
¶
与所有环境一样,我们的自定义环境将继承自 gymnasium.Env
,它定义了环境的结构。环境的要求之一是定义观察空间和动作空间,它们声明环境可能的输入(动作)和输出(观察)的通用集合。正如我们关于游戏的基本信息中所述,我们的代理有四个离散动作,因此我们将使用具有四个选项的 Discrete(4)
空间。
对于我们的观察,有几个选项,在本教程中,我们将假设我们的观察类似于 {"agent": array([1, 0]), "target": array([0, 3])}
,其中数组元素表示代理或目标的 x 和 y 位置。表示观察的另一种选择是使用 2D 网格,其中值代表网格上的代理和目标,或者使用 3D 网格,其中每个“层”仅包含代理或目标信息。因此,我们将观察空间声明为 Dict
,其中代理和目标空间是一个 Box
,允许一个整数类型的数组输出。
有关可以在环境中使用的所有可能空间的完整列表,请参见 空间
from typing import Optional
import numpy as np
import gymnasium as gym
class GridWorldEnv(gym.Env):
def __init__(self, size: int = 5):
# The size of the square grid
self.size = size
# Define the agent and target location; randomly chosen in `reset` and updated in `step`
self._agent_location = np.array([-1, -1], dtype=np.int32)
self._target_location = np.array([-1, -1], dtype=np.int32)
# Observations are dictionaries with the agent's and the target's location.
# Each location is encoded as an element of {0, ..., `size`-1}^2
self.observation_space = gym.spaces.Dict(
{
"agent": gym.spaces.Box(0, size - 1, shape=(2,), dtype=int),
"target": gym.spaces.Box(0, size - 1, shape=(2,), dtype=int),
}
)
# We have 4 actions, corresponding to "right", "up", "left", "down"
self.action_space = gym.spaces.Discrete(4)
# Dictionary maps the abstract actions to the directions on the grid
self._action_to_direction = {
0: np.array([1, 0]), # right
1: np.array([0, 1]), # up
2: np.array([-1, 0]), # left
3: np.array([0, -1]), # down
}
构建观察结果¶
由于我们需要在 Env.reset()
和 Env.step()
中计算观察结果,因此通常有一个方法 _get_obs
可以将环境的状态转换为观察结果。但是,这不是强制性的,您可以在 Env.reset()
和 Env.step()
中分别计算观察结果。
def _get_obs(self):
return {"agent": self._agent_location, "target": self._target_location}
我们还可以为 Env.reset()
和 Env.step()
返回的辅助信息实现类似的方法。在我们的案例中,我们想提供代理和目标之间的曼哈顿距离
def _get_info(self):
return {
"distance": np.linalg.norm(
self._agent_location - self._target_location, ord=1
)
}
通常情况下,info 还会包含一些仅在 Env.step()
方法内部可用的数据(例如,单个奖励项)。在这种情况下,我们必须在 Env.step()
中更新由 _get_info
返回的字典。
重置函数¶
作为 reset()
的目的,是为环境启动一个新的剧集,它有两个参数:seed
和 options
。种子可以用于将随机数生成器初始化为确定性状态,而选项可以用于指定在重置中使用的值。在重置的第一行,您需要调用 super().reset(seed=seed)
,它将初始化随机数生成器(np_random
),以便在整个 reset()
中使用。
在我们的自定义环境中,reset()
需要随机选择代理和目标的位置(如果它们具有相同的位置,我们会重复此操作)。reset()
的返回类型是初始观察结果和任何辅助信息的元组。因此,我们可以使用之前实现的方法 _get_obs
和 _get_info
来实现这一点
def reset(self, seed: Optional[int] = None, options: Optional[dict] = None):
# We need the following line to seed self.np_random
super().reset(seed=seed)
# Choose the agent's location uniformly at random
self._agent_location = self.np_random.integers(0, self.size, size=2, dtype=int)
# We will sample the target's location randomly until it does not coincide with the agent's location
self._target_location = self._agent_location
while np.array_equal(self._target_location, self._agent_location):
self._target_location = self.np_random.integers(
0, self.size, size=2, dtype=int
)
observation = self._get_obs()
info = self._get_info()
return observation, info
步骤函数¶
step()
方法通常包含环境的大部分逻辑,它接受一个 action
并计算应用动作后环境的状态,返回一个包含下一个观察结果、结果奖励、环境是否终止、环境是否截断以及辅助信息的元组。
对于我们的环境,在步骤函数中需要发生几件事
我们使用 self._action_to_direction 将离散动作(例如,2)转换为具有我们代理位置的网格方向。为了防止代理超出网格范围,我们将代理位置限制在边界范围内。
我们通过检查代理的当前位置是否等于目标位置来计算代理的奖励。
由于环境不会内部截断(我们可以在 :meth:make 期间将时间限制包装器应用于环境),因此我们将截断永久设置为 False。
我们再次使用 _get_obs 和 _get_info 来获取代理的观察结果和辅助信息。
def step(self, action):
# Map the action (element of {0,1,2,3}) to the direction we walk in
direction = self._action_to_direction[action]
# We use `np.clip` to make sure we don't leave the grid bounds
self._agent_location = np.clip(
self._agent_location + direction, 0, self.size - 1
)
# An environment is completed if and only if the agent has reached the target
terminated = np.array_equal(self._agent_location, self._target_location)
truncated = False
reward = 1 if terminated else 0 # the agent is only reached at the end of the episode
observation = self._get_obs()
info = self._get_info()
return observation, reward, terminated, truncated, info
注册和创建环境¶
虽然现在可以立即使用新的自定义环境,但更常见的是使用 gymnasium.make()
初始化环境。在本节中,我们将解释如何注册自定义环境,然后初始化它。
环境 ID 由三个部分组成,其中两个是可选的:一个可选的命名空间(这里:gymnasium_env
),一个必须的名称(这里:GridWorld
)和一个可选但推荐的版本(这里:v0)。它也可以注册为 GridWorld-v0
(推荐的方式)、GridWorld
或 gymnasium_env/GridWorld
,在创建环境时应使用相应的 ID。
入口点可以是字符串或函数,由于本教程不是 Python 项目的一部分,因此我们无法使用字符串,但对于大多数环境来说,这是指定入口点的正常方式。
注册还具有可用于指定环境关键字参数的其他参数,例如,是否应用时间限制包装器等。有关更多信息,请参见 gymnasium.register()
。
gym.register(
id="gymnasium_env/GridWorld-v0",
entry_point=GridWorldEnv,
)
有关注册自定义环境(包括使用字符串入口点)的更完整指南,请阅读完整的 创建环境 教程。
环境注册后,您可以通过 gymnasium.pprint_registry()
检查,它将输出所有已注册的环境,然后可以使用 gymnasium.make()
初始化环境。具有多个相同环境实例并行运行的环境的向量化版本可以使用 gymnasium.make_vec()
实例化。
import gymnasium as gym
>>> gym.make("gymnasium_env/GridWorld-v0")
<OrderEnforcing<PassiveEnvChecker<GridWorld<gymnasium_env/GridWorld-v0>>>>
>>> gym.make("gymnasium_env/GridWorld-v0", max_episode_steps=100)
<TimeLimit<OrderEnforcing<PassiveEnvChecker<GridWorld<gymnasium_env/GridWorld-v0>>>>>
>>> env = gym.make("gymnasium_env/GridWorld-v0", size=10)
>>> env.unwrapped.size
10
>>> gym.make_vec("gymnasium_env/GridWorld-v0", num_envs=3)
SyncVectorEnv(gymnasium_env/GridWorld-v0, num_envs=3)
使用包装器¶
通常,我们希望使用自定义环境的不同变体,或者我们希望修改 Gymnasium 或其他方提供的环境的行为。包装器允许我们在不更改环境实现或添加任何样板代码的情况下做到这一点。查看 包装器文档 了解有关如何使用包装器以及实现您自己的包装器的说明。在我们的示例中,观察结果无法直接用于学习代码,因为它们是字典。但是,我们实际上不需要触碰环境实现来解决这个问题!我们可以简单地在环境实例之上添加一个包装器,以将观察结果展平成单个数组
>>> from gymnasium.wrappers import FlattenObservation
>>> env = gym.make('gymnasium_env/GridWorld-v0')
>>> env.observation_space
Dict('agent': Box(0, 4, (2,), int64), 'target': Box(0, 4, (2,), int64))
>>> env.reset()
({'agent': array([4, 1]), 'target': array([2, 4])}, {'distance': 5.0})
>>> wrapped_env = FlattenObservation(env)
>>> wrapped_env.observation_space
Box(0, 4, (4,), int64)
>>> wrapped_env.reset()
(array([3, 0, 2, 1]), {'distance': 2.0})