gym库文档学习(二)

本文档概述了为创建新环境而设计的 Gym 中包含的创建新环境和相关有用的装饰器、实用程序和测试。您可以克隆 gym-examples 以使用此处提供的代码。建议使用虚拟环境:

1 子类化gym.Env

在学习如何创建自己的环境之前,您应该查看 Gym 的 API 文档。链接:https://blog.csdn.net/weixin_45985148/article/details/125015252icon-default.png?t=M4ADhttps://blog.csdn.net/weixin_45985148/article/details/125015252我们将关注一个看起来像这样的gym-examples子集:

为了说明gym.Env 的子类化过程,我们将实现一个非常简单的游戏,称为GridWorldEnv。我们将在 gym-examples/gym_examples/envs/grid_world.py 中为我们的自定义环境编写代码。环境由一个固定大小的二维方形网格组成(在构造过程中通过 size 参数指定)。agent可以在每个时间步长的网格单元之间垂直或水平移动。agent的目标是导航到在游戏开始时随机放置的网格上的目标。

  • 观察目标和agent的位置。
  • 我们的环境中有 4 个动作,分别对应于“右”、“上”、“左”和“下”的动作。
  • 一旦agent导航到目标所在的网格单元,就会发出完成信号。
  • 奖励是二元和稀疏的,这意味着即时奖励始终为零,除非agent已达到目标,否则为 1。

此环境中的一个游戏情节(size=5)可能如下所示:

其中蓝点是agent,红色方块代表目标。 让我们一块一块地看一下GridWorldEnv的源码:

2 声明和初始化

我们的自定义环境将继承自抽象类gym.Env。不要忘记将元数据属性添加到类。还要指定环境支持的渲染模式(例如“human”、“rgb_array”、“ansi”)以及渲染环境的帧速率。每个环境都应该支持无作为渲染模式;无需将其添加到元数据中。在 GridWorldEnv 中,我们将支持“rgb_array”和“human”模式并以 4 FPS 渲染。

我们环境的 __init__ 方法将接受整数大小,它决定了方格的大小。我们将设置一些用于渲染的变量并定义 self.observation_space 和 self.action_space。在我们的例子中,观察应该提供关于二维网格中agent和目标位置的信息。我们将选择以带有“agent”和“target”键的字典的形式表示观察。观察结果可能类似于 {"agent": array([1, 0]), "target": array([0, 3])}。由于我们的环境中有 4 个动作(“右”、“上”、“左”、“下”),我们将使用 Discrete(4) 作为动作空间。下面是 GridWorldEnv 的声明和 __init__ 的实现:

import gym
from gym import spaces
import pygame
import numpy as np


class GridWorldEnv(gym.Env):
    metadata = {"render_modes": ["human", "rgb_array"], "render_fps": 4}

    def __init__(self, size=5):
        self.size = size  # 方格环境的大小
        self.window_size = 512  #  PyGame窗口的大小

        # 观察(observation)是记录agent和目标位置的字典。
        # 每个位置编码为{0,…,`size`}^2的元素,即多重离散([size,size])。
        self.observation_space = spaces.Dict(
            {
                "agent": spaces.Box(0, size - 1, shape=(2,), dtype=int),
                "target": spaces.Box(0, size - 1, shape=(2,), dtype=int),
            }
        )

        # 四个动作:0:右:1:上:2:左:3:下
        self.action_space = spaces.Discrete(4)
        self._action_to_direction = {
            0: np.array([1, 0]),
            1: np.array([0, 1]),
            2: np.array([-1, 0]),
            3: np.array([0, -1]),
        }
        """
        如果使用 human-rendering, `self.window`将作为参考到我们画的窗口。
        `self.clock` 是使用的时钟确保以正确的帧速率渲染环境
        """
        self.window = None
        self.clock = None

3 从环境状态构建观测

由于我们需要在reset()和step()中计算观察结果,因此使用(私有)方法 _get_obs 将环境状态转换为观察结果通常很方便。但是,这不是强制性的,您也可以在 reset 和 step 中分别计算观察结果:

 def _get_obs(self):
        return {"agent": self._agent_location, "target": self._target_location}

对于step和reset返回的辅助信息,我们也可以实现类似的方法。在我们的例子中,我们想提供agent和目标之间的曼哈顿距离:

 def _get_info(self):
        return {"distance": np.linalg.norm(self._agent_location - self._target_location, ord=1)}

通常,信息还会包含一些仅在 step 方法中可用的数据(例如奖励等)。在这种情况下,我们必须更新 _get_info 返回的字典。 

4 重置reset

通过调用 reset 方法来启动新的情节。您可以假设在调用 reset 之前不会调用 step 方法。此外,只要发出完成信号,就应该调用 reset。用户可以将种子关键字传递给 reset 以将环境使用的任何随机数生成器初始化为确定性状态。推荐使用环境基类gym.Env提供的随机数生成器self.np_random。如果你只使用这个 RNG,你不需要太担心seed,但是你需要记住调用 super().reset(seed=seed) 来确保gym.Env的RNG。完成后,我们可以随机设置环境的状态。在我们的例子中,我们随机选择agent的位置和随机采样的目标位置,直到它与agent的位置不一致。

重置方法应该返回初始状态的观察,或者初始观察的元组和一些辅助信息,这取决于 return_info 是否为 True。我们可以使用我们之前实现的方法 _get_obs 和 _get_info :

 def reset(self, seed=None, return_info=False, options=None):
        super().reset(seed=seed)

        # 随机选择agent的位置
        self._agent_location = self.np_random.integers(0, self.size, size=2)

        # 我们将随机抽样目标的位置,直到它与agent的位置不一致为止
        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)

        observation = self._get_obs()
        info = self._get_info()
        return (observation, info) if return_info else observation

5 步进Step

step 方法通常包含于环境的大部分逻辑。它接受一个动作,在应用该动作后计算环境状态并返回 4 元组(观察、奖励、完成、信息)。一旦计算了环境的新状态,我们就可以检查它是否是终端状态。由于我们在 GridWorldEnv 中使用稀疏二元奖励,一旦我们知道是否完成,计算奖励就十分简单。为了收集观察和信息,我们可以再次使用 _get_obs 和 _get_info:

 def step(self, action):
        # 将动作(item{0,1,2,3})映射到行走的方向
        direction = self._action_to_direction[action]
        # 使用`np.clip` 确保我们行动没有离开定义的方格区域
        self._agent_location = np.clip(
            self._agent_location + direction, 0, self.size - 1
        )
        # agent达到target,一个episode完成
        done = np.array_equal(self._agent_location, self._target_location)
        reward = 1 if done else 0  # 二元稀疏奖励
        observation = self._get_obs()
        info = self._get_info()

        return observation, reward, done, info

6 渲染Render

在这里,我们使用 PyGame 进行渲染。 Gym 包含的许多环境中都使用了类似的渲染方法,您可以将其用作您自己环境的骨架:

    def render(self, mode="human"):
        if self.window is None and mode == "human":
            pygame.init()
            pygame.display.init()
            self.window = pygame.display.set_mode((self.window_size, self.window_size))
        if self.clock is None and mode == "human":
            self.clock = pygame.time.Clock()

        canvas = pygame.Surface((self.window_size, self.window_size))
        canvas.fill((255, 255, 255))
        pix_square_size = (
            self.window_size / self.size
        )  # 单个网格正方形的大小(以像素为单位)

        # 首先画出target
        pygame.draw.rect(
            canvas,
            (255, 0, 0),
            pygame.Rect(
                pix_square_size * self._target_location,
                (pix_square_size, pix_square_size),
            ),
        )
        # 画出agent
        pygame.draw.circle(
            canvas,
            (0, 0, 255),
            (self._agent_location + 0.5) * pix_square_size,
            pix_square_size / 3,
        )

        # 最后,添加一些网格线
        for x in range(self.size + 1):
            pygame.draw.line(
                canvas,
                0,
                (0, pix_square_size * x),
                (self.window_size, pix_square_size * x),
                width=3,
            )
            pygame.draw.line(
                canvas,
                0,
                (pix_square_size * x, 0),
                (pix_square_size * x, self.window_size),
                width=3,
            )

        if mode == "human":
            # 我们的绘图从“canvas”复制到可见窗口
            self.window.blit(canvas, canvas.get_rect())
            pygame.event.pump()
            pygame.display.update()

            # 我们需要确保以预定义的帧速率进行渲染
            # 下一行将自动添加延迟以保持帧速率稳定。
            self.clock.tick(self.metadata["render_fps"])
        else:  # rgb_array
            return np.transpose(
                np.array(pygame.surfarray.pixels3d(canvas)), axes=(1, 0, 2)
            )

7 关闭close

close 方法应该关闭环境使用的所有打开的资源。在许多情况下,您实际上不必费心去实现这个方法。但是,在我们的示例中,render_mode 可能是“human”,我们可能需要关闭已打开的窗口:

def close(self):
        if self.window is not None:
            import pygame 
            
            pygame.display.quit()
            pygame.quit()

在其他环境中,close 也可能会关闭已打开的文件或释放其他资源。调用 close 后,便不可再与环境交互。

8 注册环境Registering Envs

为了让 Gym 检测到自定义环境,它们必须按如下方式注册。我们会选择把这段代码放在gym-examples/gym_examples/__init__.py中。

from gym.envs.registration import register

register(
    id='gym_examples/GridWorld-v0',
    entry_point='gym_examples.envs:GridWorldEnv',
    max_episode_steps=300,
)

环境 ID 由三个组件组成,其中两个是可选的:一个可选的命名空间(这里:gym_examples)、一个强制名称(这里:GridWorld)和一个可选但推荐的版本(这里:v0)。它也可能已注册为 GridWorld-v0(推荐方法)、GridWorld 或 gym_examples/GridWorld,然后在环境创建期间应使用适当的 ID。

关键字参数 max_episode_steps=300 将确保通过 gym.make 实例化的 GridWorld 环境将被包装在 TimeLimit 装饰器中。如果agent已达到目标或在当前情节中已执行 300 步,则将产生完成信号。要区分截断和终止,可以检查 info["TimeLimit.truncated"]。 

除了 id 和 entrypoint,还可以传递以下额外的关键字参数来注册:

名称 类型 默认值 描述
reward_threshold float None 奖励阈值应该在任务执行之前设置好
nondeterministic bool False 即使在seed之后,这个环境是否也是不确定的
max_episode_steps int None 一个episode可以包含的最大步骤数。如果不是 None,则添加 TimeLimit 装饰器
order_enforce bool True

是否将环境包装在 OrderEnforcing 装饰器中

autoreset bool False 是否将环境包装在 AutoResetWrapper 中
kwargs dict {} 传递给环境类的默认 kwargs

大多数这些关键字(除了 max_episode_steps、order_enforce 和 kwargs)不会改变环境实例的行为,而只是提供一些关于您的环境的额外信息。注册后,可以使用 env = gym.make('gym_examples/GridWorld-v0') 创建我们自定义的 GridWorldEnv 环境。

gym-examples/gym_examples/envs/__init__.py 应该有:

from gym_examples.envs.grid_world import GridWorldEnv

9 创建包

最后一步是将我们的代码构建为 Python 包。这涉及配置gym-examples/setup.py。如何执行此操作的示例如下:

from setuptools import setup

setup(name='gym_examples',
    version='0.0.1',
    install_requires=['gym==0.23.1', 'pygame==2.1.0']
)

10 创建环境实例

使用 pip install -e gym-examples 在本地安装软件包后,您可以通过以下方式创建环境实例:

import gym_examples
env = gym.make('gym_examples/GridWorld-v0')

您还可以将环境构造函数的关键字参数传递给 gym.make 以自定义环境。在我们的例子中,还可以这样做: 

env = gym.make('gym_examples/GridWorld-v0', size=10)

有时,您可能会发现跳过注册并自己调用环境的构造函数更方便。有些人可能会发现这种方法更 “Python化”,并且像这样实例化的环境也非常好(但请记住也要添加装饰器!)。

11 使用装饰器

通常,我们想要使用自定义环境的不同变体,或者我们想要修改由 Gym 或其他方提供的环境的行为。装饰允许我们在不更改环境实现或添加任何样板代码的情况下做到这一点。查看装饰器文档可以获取有关如何使用包装器的详细信息以及实施自己的装饰器的说明。在我们的示例中,观察不能直接用于学习代码,因为它们是字典。然而,我们实际上并不需要接触我们的环境实现来解决这个问题!我们可以简单地在环境实例之上添加一个装饰器,以将观察结果扁平化为单个数组:

import gym_examples
from gym.wrappers import FlattenObservation

env = gym.make('gym_examples/GridWorld-v0')
wrapped_env = FlattenObservation(env)
print(wrapped_env.reset())     # E.g.  [3 0 3 3]

装饰器的一大优势是它们使环境高度模块化。例如,您可能只想查看目标和代理的相对位置,而不是扁平化来自 GridWorld 的观察结果。在 ObservationWrappers 部分中,我们实现了一个包装器来完成这项工作。这个装饰器也可以在gym-examples中找到:

import gym_examples
from gym_examples.wrappers import RelativePosition

env = gym.make('gym_examples/GridWorld-v0')
wrapped_env = RelativePosition(env)
print(wrapped_env.reset())     # E.g.  [-3  3]

 

猜你喜欢

转载自blog.csdn.net/weixin_45985148/article/details/125015252