注意
前往文末 下载完整示例代码。
TorchRL 环境¶
作者: Vincent Moens
环境在强化学习 (RL) 设置中起着至关重要的作用,通常在某种程度上类似于监督学习和无监督学习中的数据集。RL 社区对 OpenAI gym API 相当熟悉,该 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]
环境规范¶
与其他框架一样,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)
这些规范附带了一系列有用的工具:可以断言样本是否在定义的空间内。我们还可以使用一些启发式方法将超出空间的样本投影到该空间,并在该空间中生成随机(可能是均匀分布的)数字
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))
print("random action: \n", env.action_spec.rand())
在这些规范中,done_spec
值得特别关注。在 TorchRL 中,所有环境都至少写入两种类型的轨迹结束信号:"terminated"
(表示马尔可夫决策过程已达到最终状态 - __回合__结束)和 "done"
,表示这是__轨迹__的最后一步(但不一定是任务的结束)。通常,当 "terminal"
为 False
时,"done"
条目为 True
是由 "truncated"
信号引起的。Gym 环境考虑了这三个信号
print(env.done_spec)
环境还包含一个 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)
现在我们可以在环境中执行一个步进。由于我们没有策略,我们可以只生成一个随机动作
policy = TensorDictModule(env.action_spec.rand, in_keys=[], out_keys=["action"])
policy(reset_data)
tensordict_out = env.step(reset_data)
默认情况下,step
返回的 tensordict 与输入相同...
assert tensordict_out is reset_data
... 但带有新的键
tensordict_out
我们刚刚所做的事情(使用 action_spec.rand()
执行随机步进)也可以通过简单的快捷方式完成。
env.rand_step()
新键 ("next", "observation")
(如同 "next"
tensordict 下的所有键)在 TorchRL 中具有特殊作用:它们指示它们紧随同名但不带前缀的键之后。
我们提供一个函数 step_mdp
,它在 tensordict 中执行一个步进:它返回一个新的 tensordict,更新后满足 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()
)
我们可以观察到 step_mdp
已移除了所有与时间相关的键值对,但没有移除 "some other key"
。此外,新的观测值与之前的匹配。
最后,请注意 env.reset
方法也接受一个 tensordict 进行更新
data = TensorDict()
assert env.reset(data) is data
data
轨迹推演¶
TorchRL 提供的通用环境类允许您轻松运行给定步数的轨迹推演
tensordict_rollout = env.rollout(max_steps=20, policy=policy)
print(tensordict_rollout)
生成的 tensordict 的 batch_size
是 [20]
,即轨迹的长度。我们可以检查观测值是否与其下一个值匹配
(
tensordict_rollout.get("observation")[1:]
== tensordict_rollout.get(("next", "observation"))[:-1]
).all()
frame_skip
¶
在某些情况下,使用 frame_skip
参数在连续多个帧中使用相同的动作非常有用。
生成的 tensordict 将仅包含序列中观察到的最后一帧,但奖励将累加所有帧的奖励。
如果环境在此过程中达到完成状态,它将停止并返回截断链的结果。
env = GymEnv("Pendulum-v1", frame_skip=4)
env.reset()
渲染¶
渲染在许多强化学习设置中扮演着重要角色,这就是 torchrl 的通用环境类提供 from_pixels
关键字参数的原因,该参数允许用户快速请求基于图像的环境
env = GymEnv("Pendulum-v1", from_pixels=True)
data = env.reset()
env.close()
plt.imshow(data.get("pixels").numpy())
让我们看看 tensordict 包含什么
data
我们仍然有一个 "state"
,它描述了 "observation"
在之前案例中描述的内容(命名差异源于 gym 现在返回字典,如果存在,TorchRL 会从字典中获取名称,否则将步进输出命名为 "observation"
:简而言之,这是由于 gym 环境 step 方法返回的对象类型不一致造成的)。
您还可以通过只请求像素来丢弃这个附加输出
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("data: ", env.reset())
env.close()
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")
data = env.reset()
print("result of reset: ", data)
env.close()
当然,我们也可以使用基于像素的环境
env = DMControlEnv("acrobot", "swingup", from_pixels=True, pixels_only=True)
data = env.reset()
print("result of reset: ", data)
plt.imshow(data.get("pixels").numpy())
env.close()
转换环境¶
在将环境输出提供给策略读取或存储到缓冲区之前对其进行预处理是很常见的。
- 在许多情况下,RL 社区采用了以下类型的包装方案:
$ env_transformed = wrapper1(wrapper2(env))
来转换环境。这有许多优点:它使得访问环境规范变得显而易见(外部包装器是外部世界的真实来源),并且易于与向量化环境交互。然而,它也使得访问内部环境变得困难:假设您想从链中移除一个包装器(例如 wrapper2
),此操作需要我们获取
$ env0 = env.env.env
$ env_transformed_bis = wrapper1(env0)
TorchRL 采取了使用 transforms 序列的方式,这与其他 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()
要组合 transforms,只需使用 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()
Transforms 也可以一次添加一个
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())
此功能使得修改应用于环境输入和输出的 transforms 集合变得容易。实际上,transforms 在执行步进之前和之后都会运行:对于步进前处理,in_keys_inv
键列表将传递给 _inv_apply_transform
方法。这类 transform 的一个例子是将浮点动作(神经网络的输出)转换为 double 数据类型(由包装环境要求)。执行步进后,_apply_transform
方法将在 in_keys
键列表指示的键上执行。
环境 transforms 的另一个有趣特性是,它们允许用户检索包装情况下的 env.env
的等价物,换句话说就是父环境。通过调用 transform.parent
可以检索父环境:返回的环境将由一个 TransformedEnvironment
组成,包含直到(但不包括)当前 transform 的所有 transforms。这可以例如用于 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)
环境设备¶
Transforms 可以在设备上工作,这在操作计算需求中度或高度密集时可以带来显著的速度提升。这包括 ToTensorImage
, Resize
, GrayScale
等。
人们可能会合理地问,这对包装环境端意味着什么。对于常规环境来说影响很小:操作仍然会在它们应该发生的设备上进行。torchrl 中的环境设备属性指示传入数据应位于哪个设备以及输出数据将位于哪个设备。从该设备进行类型转换是 torchrl 环境类的责任。将数据存储在 GPU 上的主要优点是 (1) 如上所述的 transforms 加速,以及 (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
对象,而无需过多担心运行中的进程。可以通过调用 start
、reset
或直接调用 step
来启动 ParallelEnv
(如果不需要先调用 reset
)。
parallel_env.reset()
可以检查并行环境是否具有正确的 batch 大小。按照惯例,batch_size
的第一部分表示 batch,第二部分表示时间帧。让我们用 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"}],
)
data = parallel_env.reset()
plt.figure()
plt.subplot(121)
plt.imshow(data[0].get("pixels").permute(1, 2, 0).numpy())
plt.subplot(122)
plt.imshow(data[1].get("pixels").permute(1, 2, 0).numpy())
parallel_env.close()
del parallel_env
from matplotlib import pyplot as plt
转换并行环境¶
有两种等效的方法来转换并行环境:在每个进程中单独进行,或在主进程中进行。甚至可以同时进行这两种方法。因此可以仔细考虑 transform 设计,以利用设备能力(例如在 cuda 设备上的 transforms)并在可能的情况下在主进程上进行向量化操作。
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
data = parallel_env.reset()
print("grayscale data: ", data)
plt.figure()
plt.subplot(121)
plt.imshow(data[0].get("pixels").permute(1, 2, 0).numpy())
plt.subplot(122)
plt.imshow(data[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())
data = env.rollout(max_steps=100)
print("mean: :", data.get("observation").mean(0)) # Approx 0
print("std: :", data.get("observation").std(0)) # Approx 1
在并行环境中,事情稍微复杂一些,因为我们需要在进程之间共享运行统计数据。我们创建了一个名为 EnvCreator
的类,它负责查看环境创建方法,检索要在环境类中进程间共享的 tensordicts,并在创建后将每个进程指向正确的共享数据。
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)
print("env state dict:")
sd = TensorDict(make_env.state_dict())
print(sd)
# Zeroes all tensors
sd *= 0
data = env.rollout(max_steps=5)
print("data: ", data)
print("mean: :", data.get("observation").view(-1, 3).mean(0)) # Approx 0
print("std: :", data.get("observation").view(-1, 3).std(0)) # Approx 1
计数略高于步数(因为我们没有使用任何衰减)。两者之间的差异是由于 ParallelEnv
创建了一个虚拟环境来初始化用于从分派的环境收集数据的共享 TensorDict
。这个小差异通常会在整个训练过程中被吸收。
print(
"update counts: ",
make_env.state_dict()["_extra_state"]["observation_count"],
)
env.close()
del env