注意

此示例与 Gymnasium 1.2.0 版兼容。

实现自定义封装器

在本教程中,我们将描述如何实现您自己的自定义封装器。

封装器是模块化地为您的环境添加功能的绝佳方式。这将为您节省大量样板代码。

我们将展示如何通过以下方式创建封装器:

在学习本教程之前,请务必查看 gymnasium.wrappers 模块的文档。

继承自 gymnasium.ObservationWrapper

当您想对环境返回的观测值应用某些函数时,观测封装器非常有用。如果您实现了一个观测封装器,您只需要通过实现 gymnasium.ObservationWrapper.observation() 方法来定义这个转换。此外,如果转换改变了观测值的形状(例如,像下面的例子中那样将字典转换为 numpy 数组),您应该记住更新观测空间。

想象您有一个 2D 导航任务,其中环境以字典形式返回观测值,键为 "agent_position""target_position"。常见的做法是舍弃一些自由度,只考虑目标相对于智能体的位置,即 observation["target_position"] - observation["agent_position"]。为此,您可以实现一个观测封装器,如下所示

import numpy as np

import gymnasium as gym
from gymnasium import ActionWrapper, ObservationWrapper, RewardWrapper, Wrapper
from gymnasium.spaces import Box, Discrete


class RelativePosition(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"]

继承自 gymnasium.ActionWrapper

动作封装器可用于在将动作应用于环境之前对其进行转换。如果您实现了一个动作封装器,您需要通过实现 gymnasium.ActionWrapper.action() 来定义该转换。此外,您应该通过更新封装器的动作空间来指定该转换的领域。

假设您有一个动作空间类型为 gymnasium.spaces.Box 的环境,但您只想使用有限的动作子集。那么,您可能希望实现以下封装器

class DiscreteActions(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]


env = gym.make("LunarLanderContinuous-v3")
# print(env.action_space)  # Box(-1.0, 1.0, (2,), float32)
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)

继承自 gymnasium.RewardWrapper

奖励封装器用于转换环境返回的奖励。与之前的封装器一样,您需要通过实现 gymnasium.RewardWrapper.reward() 方法来指定该转换。

让我们看一个例子:有时(特别是当我们无法控制奖励因为它本质上是固有时),我们希望将奖励裁剪到某个范围以获得数值稳定性。为此,我们可以例如实现以下封装器

from typing import SupportsFloat


class ClipReward(RewardWrapper):
    def __init__(self, env, min_reward, max_reward):
        super().__init__(env)
        self.min_reward = min_reward
        self.max_reward = max_reward

    def reward(self, r: SupportsFloat) -> SupportsFloat:
        return np.clip(r, self.min_reward, self.max_reward)

继承自 gymnasium.Wrapper

有时您可能需要实现一个进行更复杂修改的封装器(例如,根据 info 中的数据修改奖励或更改渲染行为)。此类封装器可以通过继承自 gymnasium.Wrapper 来实现。

如果这样做,您可以通过访问属性 env 来访问传递给您的封装器的环境(该环境 *仍然* 可能被其他封装器封装)。

我们再来看一个此案例的示例。大多数 MuJoCo 环境会返回由不同项组成的奖励:例如,可能有一个奖励智能体完成任务的项,以及一个惩罚大动作(即能量消耗)的项。通常,您可以在环境初始化期间为这些项传递权重参数。但是,*Reacher* 不允许您这样做!尽管如此,奖励的所有单独项都返回在 info 中,因此让我们为 Reacher 构建一个封装器,允许我们对这些项进行加权

class ReacherRewardWrapper(Wrapper):
    def __init__(self, env, reward_dist_weight, reward_ctrl_weight):
        super().__init__(env)
        self.reward_dist_weight = reward_dist_weight
        self.reward_ctrl_weight = reward_ctrl_weight

    def step(self, action):
        obs, _, terminated, truncated, info = self.env.step(action)
        reward = (
            self.reward_dist_weight * info["reward_dist"]
            + self.reward_ctrl_weight * info["reward_ctrl"]
        )
        return obs, reward, terminated, truncated, info