快捷方式

TorchRL 环境

作者: Vincent Moens

环境在 RL 设置中起着至关重要的作用,通常与监督和无监督设置中的数据集有些类似。RL 社区已经非常熟悉 OpenAI gym API,它提供了一种灵活的方式来构建环境、初始化环境以及与环境交互。但是,存在许多其他库,并且与它们交互的方式可能与使用 gym 所期望的方式大不相同。

让我们首先描述 TorchRL 如何与 gym 交互,这将作为对其他框架的介绍。

Gym 环境

要运行本教程的这一部分,您需要安装最新版本的 gym 库以及 Atari 套件。您可以通过安装以下包来完成安装

为了统一所有框架,torchrl 环境是在 __init__ 方法内使用名为 _build_env 的私有方法构建的,该方法将参数和关键字参数传递给根库构建器。

对于 gym,这意味着构建环境就像这样简单:

import torch
from matplotlib import pyplot as plt
from tensordict import TensorDict
from torchrl.envs.libs.gym import GymEnv

env = GymEnv("Pendulum-v1")

可通过此命令访问可用环境的列表

list(GymEnv.available_envs)[:10]
['Acrobot-v1', 'Ant-v2', 'Ant-v3', 'Ant-v4', 'BipedalWalker-v3', 'BipedalWalkerHardcore-v3', 'Blackjack-v1', 'CarRacing-v2', 'CartPole-v0', 'CartPole-v1']

环境规格

与其他框架一样,TorchRL 环境具有指示观测、动作、完成和奖励的空间的属性。因为通常会检索多个观测值,所以我们期望观测规格为 CompositeSpec 类型。奖励和动作没有此限制

print("Env observation_spec: \n", env.observation_spec)
print("Env action_spec: \n", env.action_spec)
print("Env reward_spec: \n", env.reward_spec)
Env observation_spec:
 CompositeSpec(
    observation: BoundedTensorSpec(
        shape=torch.Size([3]),
        space=ContinuousBox(
            low=Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, contiguous=True),
            high=Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, contiguous=True)),
        device=cpu,
        dtype=torch.float32,
        domain=continuous),
    device=None,
    shape=torch.Size([]))
Env action_spec:
 BoundedTensorSpec(
    shape=torch.Size([1]),
    space=ContinuousBox(
        low=Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, contiguous=True),
        high=Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, contiguous=True)),
    device=cpu,
    dtype=torch.float32,
    domain=continuous)
Env reward_spec:
 UnboundedContinuousTensorSpec(
    shape=torch.Size([1]),
    space=None,
    device=cpu,
    dtype=torch.float32,
    domain=continuous)

这些规格带有一系列有用的工具:可以断言样本是否在定义的空间内。如果样本超出空间,我们还可以使用一些启发式方法将样本投影到空间中,并在该空间中生成随机(可能是均匀分布的)数字

action = torch.ones(1) * 3
print("action is in bounds?\n", bool(env.action_spec.is_in(action)))
print("projected action: \n", env.action_spec.project(action))
action is in bounds?
 False
projected action:
 tensor([2.])
print("random action: \n", env.action_spec.rand())
random action:
 tensor([1.0797])

在这些规格中,done_spec 值得特别注意。在 TorchRL 中,所有环境都至少写入两种类型的轨迹结束信号:"terminated"(表示马尔可夫决策过程已达到最终状态 - __情节__ 已结束)和 "done",表示这是__轨迹__的最后一步(但不一定是任务的结束)。通常,当 "terminal"False"done"True 是由 "truncated" 信号引起的。Gym 环境考虑了这三个信号

print(env.done_spec)
CompositeSpec(
    done: DiscreteTensorSpec(
        shape=torch.Size([1]),
        space=DiscreteBox(n=2),
        device=cpu,
        dtype=torch.bool,
        domain=discrete),
    terminated: DiscreteTensorSpec(
        shape=torch.Size([1]),
        space=DiscreteBox(n=2),
        device=cpu,
        dtype=torch.bool,
        domain=discrete),
    truncated: DiscreteTensorSpec(
        shape=torch.Size([1]),
        space=DiscreteBox(n=2),
        device=cpu,
        dtype=torch.bool,
        domain=discrete),
    device=None,
    shape=torch.Size([]))

环境还包含一个 env.state_spec 属性,其类型为 CompositeSpec,其中包含所有作为环境输入但不是动作的规格。对于有状态环境(例如 gym),这在大多数情况下将为空。对于无状态环境(例如 Brax),这还应包括先前状态的表示或任何其他输入到环境(包括重置时的输入)。

播种、重置和步骤

环境的基本操作是 (1) set_seed、(2) reset 和 (3) step

让我们看看这些方法如何在 TorchRL 中工作

torch.manual_seed(0)  # make sure that all torch code is also reproductible
env.set_seed(0)
reset_data = env.reset()
print("reset data", reset_data)
reset data TensorDict(
    fields={
        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        observation: Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, is_shared=False),
        terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        truncated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False)},
    batch_size=torch.Size([]),
    device=None,
    is_shared=False)

现在,我们可以在环境中执行一步。由于我们没有策略,因此我们只需生成一个随机动作即可

policy = TensorDictModule(env.action_spec.rand, in_keys=[], out_keys=["action"])


policy(reset_data)
tensordict_out = env.step(reset_data)

默认情况下,step 返回的张量字典与输入相同……

assert tensordict_out is reset_data

……但具有新的键

tensordict_out
TensorDict(
    fields={
        action: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),
        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        next: TensorDict(
            fields={
                done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
                observation: Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, is_shared=False),
                reward: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),
                terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
                truncated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False)},
            batch_size=torch.Size([]),
            device=None,
            is_shared=False),
        observation: Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, is_shared=False),
        terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        truncated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False)},
    batch_size=torch.Size([]),
    device=None,
    is_shared=False)

我们刚刚所做的(使用 action_spec.rand() 进行随机步骤)也可以通过简单的快捷方式完成。

env.rand_step()
TensorDict(
    fields={
        action: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),
        next: TensorDict(
            fields={
                done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
                observation: Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, is_shared=False),
                reward: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),
                terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
                truncated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False)},
            batch_size=torch.Size([]),
            device=None,
            is_shared=False)},
    batch_size=torch.Size([]),
    device=None,
    is_shared=False)

新的键 ("next", "observation")(以及 "next" 张量字典下的所有键)在 TorchRL 中具有特殊作用:它们表明它们位于具有相同名称但没有前缀的键之后。

我们提供了一个函数 step_mdp,该函数在张量字典中执行一步:它返回一个新的张量字典,该字典已更新,使得 t < -t’

from torchrl.envs.utils import step_mdp

tensordict_out.set("some other key", torch.randn(1))
tensordict_tprime = step_mdp(tensordict_out)

print(tensordict_tprime)
print(
    (
        tensordict_tprime.get("observation")
        == tensordict_out.get(("next", "observation"))
    ).all()
)
TensorDict(
    fields={
        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        observation: Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, is_shared=False),
        some other key: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),
        terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        truncated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False)},
    batch_size=torch.Size([]),
    device=None,
    is_shared=False)
tensor(True)

我们可以观察到 step_mdp 已删除所有与时间相关的键值对,但未删除 "some other key"。此外,新的观测值与之前的观测值匹配。

最后,请注意,env.reset 方法也接受张量字典进行更新

tensordict = TensorDict({}, [])
assert env.reset(tensordict) is tensordict
tensordict
TensorDict(
    fields={
        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        observation: Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, is_shared=False),
        terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        truncated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False)},
    batch_size=torch.Size([]),
    device=None,
    is_shared=False)

展开

TorchRL 提供的通用环境类允许您轻松地为给定数量的步骤运行展开

tensordict_rollout = env.rollout(max_steps=20, policy=policy)
print(tensordict_rollout)
TensorDict(
    fields={
        action: Tensor(shape=torch.Size([20, 1]), device=cpu, dtype=torch.float32, is_shared=False),
        done: Tensor(shape=torch.Size([20, 1]), device=cpu, dtype=torch.bool, is_shared=False),
        next: TensorDict(
            fields={
                done: Tensor(shape=torch.Size([20, 1]), device=cpu, dtype=torch.bool, is_shared=False),
                observation: Tensor(shape=torch.Size([20, 3]), device=cpu, dtype=torch.float32, is_shared=False),
                reward: Tensor(shape=torch.Size([20, 1]), device=cpu, dtype=torch.float32, is_shared=False),
                terminated: Tensor(shape=torch.Size([20, 1]), device=cpu, dtype=torch.bool, is_shared=False),
                truncated: Tensor(shape=torch.Size([20, 1]), device=cpu, dtype=torch.bool, is_shared=False)},
            batch_size=torch.Size([20]),
            device=None,
            is_shared=False),
        observation: Tensor(shape=torch.Size([20, 3]), device=cpu, dtype=torch.float32, is_shared=False),
        terminated: Tensor(shape=torch.Size([20, 1]), device=cpu, dtype=torch.bool, is_shared=False),
        truncated: Tensor(shape=torch.Size([20, 1]), device=cpu, dtype=torch.bool, is_shared=False)},
    batch_size=torch.Size([20]),
    device=None,
    is_shared=False)

生成的张量字典具有 batch_size[20],这是轨迹的长度。我们可以检查观测值是否与其下一个值匹配

(
    tensordict_rollout.get("observation")[1:]
    == tensordict_rollout.get(("next", "observation"))[:-1]
).all()
tensor(True)

frame_skip

在某些情况下,使用 frame_skip 参数对连续几帧使用相同的动作很有用。

生成的张量字典将仅包含序列中观察到的最后一帧,但奖励将在帧数上求和。

如果环境在此过程中达到完成状态,它将停止并返回截断链的结果。

env = GymEnv("Pendulum-v1", frame_skip=4)
env.reset()
TensorDict(
    fields={
        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        observation: Tensor(shape=torch.Size([3]), device=cpu, dtype=torch.float32, is_shared=False),
        terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        truncated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False)},
    batch_size=torch.Size([]),
    device=None,
    is_shared=False)

渲染

渲染在许多 RL 设置中起着重要作用,这就是为什么来自 torchrl 的通用环境类提供 from_pixels 关键字参数,允许用户快速请求基于图像的环境

env = GymEnv("Pendulum-v1", from_pixels=True)
tensordict = env.reset()
env.close()
plt.imshow(tensordict.get("pixels").numpy())
torchrl envs
<matplotlib.image.AxesImage object at 0x7f6f9e77b4f0>

让我们看看张量字典包含什么

tensordict
TensorDict(
    fields={
        done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        pixels: Tensor(shape=torch.Size([500, 500, 3]), device=cpu, dtype=torch.uint8, is_shared=False),
        terminated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
        truncated: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False)},
    batch_size=torch.Size([]),
    device=None,
    is_shared=False)

我们仍然有一个 "state",它描述了之前情况下 "observation" 所描述的内容(名称差异来自 gym 现在返回一个字典,并且 TorchRL 从字典中获取名称(如果存在),否则它将步骤输出命名为 "observation":简而言之,这是由于 gym 环境步骤方法返回的对象类型不一致造成的)。

也可以通过仅请求像素来丢弃此补充输出

env = GymEnv("Pendulum-v1", from_pixels=True, pixels_only=True)
env.reset()
env.close()

某些环境仅以基于图像的格式提供

env = GymEnv("ALE/Pong-v5")
print("from pixels: ", env.from_pixels)
print("tensordict: ", env.reset())
env.close()
Traceback (most recent call last):
  File "/pytorch/rl/docs/source/reference/generated/tutorials/torchrl_envs.py", line 275, in <module>
    env = GymEnv("ALE/Pong-v5")
  File "/pytorch/rl/torchrl/envs/libs/gym.py", line 571, in __call__
    instance: GymWrapper = super().__call__(*args, **kwargs)
  File "/pytorch/rl/torchrl/envs/common.py", line 176, in __call__
    instance: EnvBase = super().__call__(*args, **kwargs)
  File "/pytorch/rl/torchrl/envs/libs/gym.py", line 1371, in __init__
    super().__init__(**kwargs)
  File "/pytorch/rl/torchrl/envs/libs/gym.py", line 784, in __init__
    super().__init__(**kwargs)
  File "/pytorch/rl/torchrl/envs/common.py", line 3047, in __init__
    self._env = self._build_env(**kwargs)  # writes the self._env attribute
  File "/pytorch/rl/torchrl/envs/libs/gym.py", line 1423, in _build_env
    env = self.lib.make(env_name, **kwargs)
  File "/pytorch/rl/env/lib/python3.8/site-packages/gym/envs/registration.py", line 569, in make
    _check_version_exists(ns, name, version)
  File "/pytorch/rl/env/lib/python3.8/site-packages/gym/envs/registration.py", line 219, in _check_version_exists
    _check_name_exists(ns, name)
  File "/pytorch/rl/env/lib/python3.8/site-packages/gym/envs/registration.py", line 187, in _check_name_exists
    _check_namespace_exists(ns)
  File "/pytorch/rl/env/lib/python3.8/site-packages/gym/envs/registration.py", line 182, in _check_namespace_exists
    raise error.NamespaceNotFound(f"Namespace {ns} not found. {suggestion_msg}")
gym.error.NamespaceNotFound: Namespace ALE not found. Have you installed the proper package for ALE?

DeepMind Control 环境

要运行本教程的这一部分,请确保您已安装 dm_control

$ pip install dm_control

我们还提供了一个用于 DM Control 套件的包装器。同样,构建环境很容易:首先让我们看看可以访问哪些环境。现在,available_envs 返回一个包含环境和可能任务的字典

from matplotlib import pyplot as plt
from torchrl.envs.libs.dm_control import DMControlEnv

DMControlEnv.available_envs
env = DMControlEnv("acrobot", "swingup")
tensordict = env.reset()
print("result of reset: ", tensordict)
env.close()

当然,我们也可以使用基于像素的环境

env = DMControlEnv("acrobot", "swingup", from_pixels=True, pixels_only=True)
tensordict = env.reset()
print("result of reset: ", tensordict)
plt.imshow(tensordict.get("pixels").numpy())
env.close()

转换环境

在策略读取或缓冲区存储环境输出之前对其进行预处理是很常见的。

在许多情况下,强化学习社区采用了以下类型的包装方案

$ env_transformed = wrapper1(wrapper2(env))

来转换环境。这有很多优点:它使访问环境规范变得很明显(外部包装器是外部世界的真相来源),并且它使与矢量化环境交互变得容易。但是,它也使访问内部环境变得困难:假设想要从链中移除一个包装器(例如 wrapper2),此操作需要我们收集

$ env0 = env.env.env

$ env_transformed_bis = wrapper1(env0)

TorchRL 采用使用转换序列的方法,就像在其他 PyTorch 领域库(例如 torchvision)中一样。这种方法也类似于 torch.distribution 中转换分布的方式,其中 TransformedDistribution 对象构建在 base_dist 分布和(一系列)transforms 周围。

from torchrl.envs.transforms import ToTensorImage, TransformedEnv

# ToTensorImage transforms a numpy-like image into a tensor one,
env = DMControlEnv("acrobot", "swingup", from_pixels=True, pixels_only=True)
print("reset before transform: ", env.reset())

env = TransformedEnv(env, ToTensorImage())
print("reset after transform: ", env.reset())
env.close()

要组合转换,只需使用 Compose 类即可

from torchrl.envs.transforms import Compose, Resize

env = DMControlEnv("acrobot", "swingup", from_pixels=True, pixels_only=True)
env = TransformedEnv(env, Compose(ToTensorImage(), Resize(32, 32)))
env.reset()

也可以一次添加一个转换

from torchrl.envs.transforms import GrayScale

env.append_transform(GrayScale())
env.reset()

正如预期的那样,元数据也会更新

print("original obs spec: ", env.base_env.observation_spec)
print("current obs spec: ", env.observation_spec)

如果需要,我们还可以连接张量

from torchrl.envs.transforms import CatTensors

env = DMControlEnv("acrobot", "swingup")
print("keys before concat: ", env.reset())

env = TransformedEnv(
    env,
    CatTensors(in_keys=["orientations", "velocity"], out_key="observation"),
)
print("keys after concat: ", env.reset())

此功能使修改应用于环境输入和输出的转换集变得容易。实际上,转换在执行步骤之前和之后都会运行:对于预步骤传递,in_keys_inv 键列表将传递给 _inv_apply_transform 方法。此类转换的一个示例是将浮点动作(来自神经网络的输出)转换为双精度数据类型(包装环境需要)。执行步骤后,_apply_transform 方法将在 in_keys 键列表指示的键上执行。

环境转换的另一个有趣功能是,它们允许用户检索包装案例中 env.env 的等效项,或者换句话说,父环境。可以通过调用 transform.parent 来检索父环境:返回的环境将包含一个 TransformedEnvironment,其中包含直到(但不包括)当前转换的所有转换。例如,这可以在 NoopResetEnv 案例中使用,当重置时,它执行以下步骤:在该环境中执行一定数量的随机步骤之前重置父环境。

env = DMControlEnv("acrobot", "swingup")
env = TransformedEnv(env)
env.append_transform(
    CatTensors(in_keys=["orientations", "velocity"], out_key="observation")
)
env.append_transform(GrayScale())

print("env: \n", env)
print("GrayScale transform parent env: \n", env.transform[1].parent)
print("CatTensors transform parent env: \n", env.transform[0].parent)

环境设备

转换可以在设备上工作,当操作的计算量适中或很高时,这可以带来显著的加速。这些包括 ToTensorImageResizeGrayScale 等。

人们可能会合理地问这在包装环境方面意味着什么。对于常规环境来说,几乎没有什么:操作仍然会在它们应该发生的设备上发生。torchrl 中的环境设备属性指示传入数据应该在哪个设备上,以及输出数据将在哪个设备上。从该设备到该设备的转换是 torchrl 环境类的责任。在 GPU 上存储数据的最大优点是 (1) 如上所述加速转换,以及 (2) 在多处理设置中在工作进程之间共享数据。

from torchrl.envs.transforms import CatTensors, GrayScale, TransformedEnv

env = DMControlEnv("acrobot", "swingup")
env = TransformedEnv(env)
env.append_transform(
    CatTensors(in_keys=["orientations", "velocity"], out_key="observation")
)

if torch.has_cuda and torch.cuda.device_count():
    env.to("cuda:0")
    env.reset()

并行运行环境

TorchRL 提供了并行运行环境的实用程序。预计各种环境读取和返回的张量的形状和数据类型相似(但可以设计掩码函数,以在这些张量的形状不同时使其成为可能)。创建此类环境非常容易。让我们看看最简单的案例

from torchrl.envs import ParallelEnv


def env_make():
    return GymEnv("Pendulum-v1")


parallel_env = ParallelEnv(3, env_make)  # -> creates 3 envs in parallel
parallel_env = ParallelEnv(
    3, [env_make, env_make, env_make]
)  # similar to the previous command

SerialEnv 类类似于 ParallelEnv,只是环境是按顺序运行的。这主要用于调试目的。

ParallelEnv 实例以延迟模式创建:环境仅在调用时开始运行。这使我们能够将 ParallelEnv 对象从一个进程移动到另一个进程,而无需过多担心运行进程。ParallelEnv 可以通过调用 startreset 或简单地调用 step 来启动(如果不需要首先调用 reset)。

parallel_env.reset()

可以检查并行环境是否具有正确的批次大小。按照惯例,batch_size 的第一部分表示批次,第二部分表示时间范围。让我们用 rollout 方法来检查一下

parallel_env.rollout(max_steps=20)

关闭并行环境

**重要提示**:在关闭程序之前,务必关闭并行环境。通常,即使使用常规环境,也建议在函数中使用 close 调用来结束。在某些情况下,如果未执行此操作,TorchRL 将引发错误(并且通常会在程序结束时,当环境超出范围时发生!)。

parallel_env.close()

播种

在播种并行环境时,我们面临的困难是我们不想为所有环境提供相同的种子。TorchRL 使用的启发式方法是,我们以一种——可以说——马尔可夫的方式,根据输入种子生成一个确定性的种子链,以便可以从它的任何元素中重建它。所有 set_seed 方法都将返回要使用的下一个种子,以便可以轻松地根据最后一个种子继续该链。当多个收集器都包含一个 ParallelEnv 实例并且我们希望每个子子环境具有不同的种子时,这很有用。

out_seed = parallel_env.set_seed(10)
print(out_seed)

del parallel_env

访问环境属性

有时会发生包装环境具有一个感兴趣的属性的情况。首先,请注意 TorchRL 环境包装器包含访问此属性的工具。这是一个例子

from time import sleep
from uuid import uuid1


def env_make():
    env = GymEnv("Pendulum-v1")
    env._env.foo = f"bar_{uuid1()}"
    env._env.get_something = lambda r: r + 1
    return env


env = env_make()
# Goes through env._env
env.foo
parallel_env = ParallelEnv(3, env_make)  # -> creates 3 envs in parallel

# env has not been started --> error:
try:
    parallel_env.foo
except RuntimeError:
    print("Aargh what did I do!")
    sleep(2)  # make sure we don't get ahead of ourselves
if parallel_env.is_closed:
    parallel_env.start()
foo_list = parallel_env.foo
foo_list  # needs to be instantiated, for instance using list
list(foo_list)

类似地,也可以访问方法

something = parallel_env.get_something(0)
print(something)
parallel_env.close()
del parallel_env

并行环境的 kwargs

可能需要向各个环境提供 kwargs。这可以在构建时或之后实现

from torchrl.envs import ParallelEnv


def env_make(env_name):
    env = TransformedEnv(
        GymEnv(env_name, from_pixels=True, pixels_only=True),
        Compose(ToTensorImage(), Resize(64, 64)),
    )
    return env


parallel_env = ParallelEnv(
    2,
    [env_make, env_make],
    create_env_kwargs=[{"env_name": "ALE/AirRaid-v5"}, {"env_name": "ALE/Pong-v5"}],
)
tensordict = parallel_env.reset()

plt.figure()
plt.subplot(121)
plt.imshow(tensordict[0].get("pixels").permute(1, 2, 0).numpy())
plt.subplot(122)
plt.imshow(tensordict[1].get("pixels").permute(1, 2, 0).numpy())
parallel_env.close()
del parallel_env

from matplotlib import pyplot as plt

转换并行环境

转换并行环境有两种等效的方法:在每个进程中单独转换,或在主进程中转换。甚至可以同时进行两者。因此,可以仔细考虑转换设计,以利用设备功能(例如 CUDA 设备上的转换)并在可能的情况下对主进程上的操作进行矢量化。

from torchrl.envs import (
    Compose,
    GrayScale,
    ParallelEnv,
    Resize,
    ToTensorImage,
    TransformedEnv,
)


def env_make(env_name):
    env = TransformedEnv(
        GymEnv(env_name, from_pixels=True, pixels_only=True),
        Compose(ToTensorImage(), Resize(64, 64)),
    )  # transforms on remote processes
    return env


parallel_env = ParallelEnv(
    2,
    [env_make, env_make],
    create_env_kwargs=[{"env_name": "ALE/AirRaid-v5"}, {"env_name": "ALE/Pong-v5"}],
)
parallel_env = TransformedEnv(parallel_env, GrayScale())  # transforms on main process
tensordict = parallel_env.reset()

print("grayscale tensordict: ", tensordict)
plt.figure()
plt.subplot(121)
plt.imshow(tensordict[0].get("pixels").permute(1, 2, 0).numpy())
plt.subplot(122)
plt.imshow(tensordict[1].get("pixels").permute(1, 2, 0).numpy())
parallel_env.close()
del parallel_env

VecNorm

在强化学习中,我们通常会遇到在将数据输入模型之前对其进行归一化的问题。有时,我们可以从环境中收集到的数据(例如,使用随机策略或演示)中获得归一化统计量的良好近似值。但是,建议“动态”地归一化数据,将归一化常数逐步更新为迄今为止观察到的值。当我们期望归一化统计量随着任务性能的变化而变化,或者当环境由于外部因素而发生变化时,这尤其有用。

**警告**:在离策略学习中应谨慎使用此功能,因为由于使用以前有效的归一化统计量对旧数据进行归一化,旧数据将“过时”。在策略设置中,此功能也使学习不稳定,并可能产生意想不到的影响。因此,建议用户谨慎使用此功能,并将其与根据归一化常数的固定版本进行数据归一化进行比较。

在常规设置中,使用 VecNorm 非常容易

from torchrl.envs.libs.gym import GymEnv
from torchrl.envs.transforms import TransformedEnv, VecNorm

env = TransformedEnv(GymEnv("Pendulum-v1"), VecNorm())
tensordict = env.rollout(max_steps=100)

print("mean: :", tensordict.get("observation").mean(0))  # Approx 0
print("std: :", tensordict.get("observation").std(0))  # Approx 1

在**并行环境**中,情况稍微复杂一些,因为我们需要在进程之间共享运行统计量。我们创建了一个名为 EnvCreator 的类,它负责查看环境创建方法,检索要在环境类中进程之间共享的张量字典,并在创建后将每个进程指向正确的公共共享张量字典

from torchrl.envs import EnvCreator, ParallelEnv
from torchrl.envs.libs.gym import GymEnv
from torchrl.envs.transforms import TransformedEnv, VecNorm

make_env = EnvCreator(lambda: TransformedEnv(GymEnv("CartPole-v1"), VecNorm(decay=1.0)))
env = ParallelEnv(3, make_env)
make_env.state_dict()["_extra_state"]["td"]["observation_count"].fill_(0.0)
make_env.state_dict()["_extra_state"]["td"]["observation_ssq"].fill_(0.0)
make_env.state_dict()["_extra_state"]["td"]["observation_sum"].fill_(0.0)

tensordict = env.rollout(max_steps=5)

print("tensordict: ", tensordict)
print("mean: :", tensordict.get("observation").view(-1, 3).mean(0))  # Approx 0
print("std: :", tensordict.get("observation").view(-1, 3).std(0))  # Approx 1

计数略高于步骤数(因为我们没有使用任何衰减)。两者之间的差异是由于 ParallelEnv 创建了一个虚拟环境来初始化用于从调度环境收集数据的共享 TensorDict。这个小差异通常会在整个训练过程中被吸收。

print(
    "update counts: ",
    make_env.state_dict()["_extra_state"]["td"]["observation_count"],
)

env.close()
del env

**脚本的总运行时间:**(1 分 34.426 秒)

**估计内存使用量:**2873 MB

由 Sphinx-Gallery 生成的图库

文档

访问 PyTorch 的全面开发者文档

查看文档

教程

获取初学者和高级开发人员的深入教程

查看教程

资源

查找开发资源并获得问题的解答

查看资源