注意
转到末尾 下载完整的示例代码。
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
可通过此命令访问可用环境的列表
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())
<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)
环境设备¶
转换可以在设备上工作,当操作的计算量适中或很高时,这可以带来显著的加速。这些包括 ToTensorImage
、Resize
、GrayScale
等。
人们可能会合理地问这在包装环境方面意味着什么。对于常规环境来说,几乎没有什么:操作仍然会在它们应该发生的设备上发生。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
可以通过调用 start
、reset
或简单地调用 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