注意
转到末尾 下载完整的示例代码。
使用 TorchRL 教程进行强化学习 (PPO)¶
作者: Vincent Moens
本教程演示了如何使用 PyTorch 和 torchrl
训练一个参数化策略网络来解决来自 OpenAI-Gym/Farama-Gymnasium 控制库 的倒立摆任务。
关键学习点
如何在 TorchRL 中创建环境,转换其输出,并从此环境中收集数据;
如何使用
TensorDict
使您的类相互通信;使用 TorchRL 构建训练循环的基础知识
如何计算策略梯度方法的优势信号;
如何使用概率神经网络创建随机策略;
如何创建动态回放缓冲区并在其中进行无重复采样。
我们将介绍 TorchRL 的六个关键组件
如果您在 Google Colab 中运行此程序,请确保安装以下依赖项
!pip3 install torchrl
!pip3 install gym[mujoco]
!pip3 install tqdm
近端策略优化 (PPO) 是一种策略梯度算法,其中收集一批数据并直接用于训练策略,以在某些邻近约束下最大化预期回报。您可以将其视为 REINFORCE(基础策略优化算法)的复杂版本。有关更多信息,请参阅 近端策略优化算法 论文。
PPO 通常被认为是一种快速有效的在线、在线策略强化算法。TorchRL 提供了一个损失模块来为您完成所有工作,因此您可以依靠此实现并专注于解决您的问题,而不是每次想要训练策略时都重新发明轮子。
为了完整起见,以下是损失计算的简要概述,即使这由我们的 ClipPPOLoss
模块处理——该算法的工作原理如下:1. 我们将通过在环境中执行策略一定数量的步数来采样一批数据。2. 然后,我们将使用此批次的随机子样本执行一定数量的优化步骤,使用 REINFORCE 损失的剪辑版本。3. 剪辑将对我们的损失施加悲观边界:与更高的回报估计相比,将优先考虑更低的回报估计。损失的精确公式为
该损失有两个组成部分:在最小运算符的第一部分,我们只是计算 REINFORCE 损失的重要性加权版本(例如,我们已针对当前策略配置滞后于用于数据收集的策略配置这一事实进行了校正的 REINFORCE 损失)。最小运算符的第二部分是类似的损失,其中我们在比率超过或低于给定阈值对时对其进行了剪辑。
此损失确保无论优势是正数还是负数,都会阻止会导致与先前配置产生重大偏移的策略更新。
本教程的结构如下
首先,我们将定义一组用于训练的超参数。
接下来,我们将专注于使用 TorchRL 的包装器和转换创建我们的环境或模拟器。
接下来,我们将设计策略网络和价值模型,这对于损失函数是必不可少的。这些模块将用于配置我们的损失模块。
接下来,我们将创建回放缓冲区和数据加载器。
最后,我们将运行我们的训练循环并分析结果。
在本教程中,我们将使用 tensordict
库。 TensorDict
是 TorchRL 的通用语言:它帮助我们抽象出模块读取和写入的内容,并减少对特定数据描述的关注,更多地关注算法本身。
from collections import defaultdict
import matplotlib.pyplot as plt
import torch
from tensordict.nn import TensorDictModule
from tensordict.nn.distributions import NormalParamExtractor
from torch import nn
from torchrl.collectors import SyncDataCollector
from torchrl.data.replay_buffers import ReplayBuffer
from torchrl.data.replay_buffers.samplers import SamplerWithoutReplacement
from torchrl.data.replay_buffers.storages import LazyTensorStorage
from torchrl.envs import (
Compose,
DoubleToFloat,
ObservationNorm,
StepCounter,
TransformedEnv,
)
from torchrl.envs.libs.gym import GymEnv
from torchrl.envs.utils import check_env_specs, ExplorationType, set_exploration_type
from torchrl.modules import ProbabilisticActor, TanhNormal, ValueOperator
from torchrl.objectives import ClipPPOLoss
from torchrl.objectives.value import GAE
from tqdm import tqdm
定义超参数¶
我们为算法设置超参数。根据可用资源,可以选择在 GPU 或其他设备上执行策略。frame_skip
将控制单个动作执行的帧数。必须针对此值校正计算帧的其他参数(因为一个环境步骤实际上将返回 frame_skip
帧)。
is_fork = multiprocessing.get_start_method() == "fork"
device = (
torch.device(0)
if torch.cuda.is_available() and not is_fork
else torch.device("cpu")
)
num_cells = 256 # number of cells in each layer i.e. output dim.
lr = 3e-4
max_grad_norm = 1.0
数据收集参数¶
在收集数据时,我们将能够通过定义 frames_per_batch
参数来选择每个批次的大小。我们还将定义允许自己使用的帧数(例如,与模拟器的交互次数)。通常,RL 算法的目标是以环境交互次数来尽可能快地解决任务:total_frames
越低越好。
frames_per_batch = 1000
# For a complete training, bring the number of frames up to 1M
total_frames = 10_000
PPO 参数¶
在每次数据收集(或批次收集)中,我们将在一定数量的时期内运行优化,每次在嵌套训练循环中使用我们刚刚获取的全部数据。这里,sub_batch_size
与上面的 frames_per_batch
不同:请记住,我们正在使用来自收集器的“数据批次”,其大小由 frames_per_batch
定义,并且我们将在内部训练循环中将其进一步拆分为更小的子批次。这些子批次的大小由 sub_batch_size
控制。
sub_batch_size = 64 # cardinality of the sub-samples gathered from the current data in the inner loop
num_epochs = 10 # optimisation steps per batch of data collected
clip_epsilon = (
0.2 # clip value for PPO loss: see the equation in the intro for more context.
)
gamma = 0.99
lmbda = 0.95
entropy_eps = 1e-4
定义环境¶
在 RL 中,环境通常是我们指代模拟器或控制系统的方式。各种库为强化学习提供模拟环境,包括 Gymnasium(以前称为 OpenAI Gym)、DeepMind 控制套件等等。作为一个通用库,TorchRL 的目标是为大量的 RL 模拟器提供一个可互换的接口,使您可以轻松地交换一个环境与另一个环境。例如,使用少量字符即可创建包装的 gym 环境
这段代码中需要注意几点:首先,我们通过调用 GymEnv
包装器创建了环境。如果传递了额外的关键字参数,它们将被传递给 gym.make
方法,因此涵盖了最常见的环境构建命令。或者,也可以使用 gym.make(env_name, **kwargs)
直接创建一个 gym 环境,并将其包装在 GymWrapper 类中。
还有 device
参数:对于 gym,这仅控制存储输入动作和观察状态的设备,但执行始终在 CPU 上进行。这样做的原因很简单,gym 不支持设备上的执行,除非另有说明。对于其他库,我们可以控制执行设备,并且尽可能地保持存储和执行后端的统一性。
转换¶
我们将向我们的环境添加一些转换,以便为策略准备数据。在 Gym 中,这通常通过包装器来实现。TorchRL 采用了一种不同的方法,更类似于其他 PyTorch 领域库,通过使用转换来实现。要向环境添加转换,只需将其包装在 TransformedEnv
实例中,并将转换序列附加到其中。转换后的环境将继承包装环境的设备和元数据,并根据其包含的转换序列转换这些数据。
归一化¶
第一个编码是一个归一化转换。根据经验,最好让数据大致匹配单位高斯分布:为了实现这一点,我们将运行环境中的特定数量的随机步,并计算这些观察结果的汇总统计信息。
我们将附加另外两个转换:DoubleToFloat
转换将双精度条目转换为单精度数字,准备供策略读取。 StepCounter
转换将用于计算环境终止之前的步数。我们将使用此度量作为性能的补充度量。
正如我们稍后将看到的,TorchRL 的许多类都依赖于 TensorDict
进行通信。您可以将其视为一个 Python 字典,具有一些额外的张量特性。在实践中,这意味着我们将要使用的许多模块都需要被告知在它们将接收的 tensordict
中读取哪个键(in_keys
)和写入哪个键(out_keys
)。通常,如果省略了 out_keys
,则假设将就地更新 in_keys
条目。对于我们的转换,我们唯一感兴趣的条目称为 "observation"
,并且我们的转换层将被告知仅修改此条目。
env = TransformedEnv(
base_env,
Compose(
# normalize observations
ObservationNorm(in_keys=["observation"]),
DoubleToFloat(),
StepCounter(),
),
)
您可能已经注意到,我们创建了一个归一化层,但没有设置其归一化参数。为此,ObservationNorm
可以自动收集我们环境的汇总统计信息。
env.transform[0].init_stats(num_iter=1000, reduce_dim=0, cat_dim=0)
ObservationNorm
转换现在已填充了将用于归一化数据的定位和比例。
让我们对汇总统计信息的形状进行一个小小的健全性检查。
print("normalization constant shape:", env.transform[0].loc.shape)
normalization constant shape: torch.Size([11])
环境不仅由其模拟器和转换定义,还由一系列描述其执行过程中预期内容的元数据定义。出于效率考虑,TorchRL 在环境规范方面非常严格,但您可以轻松检查您的环境规范是否足够。在我们的示例中,GymWrapper
和继承自它的 GymEnv
已经负责为您的环境设置正确的规范,因此您无需关心这一点。
尽管如此,让我们通过查看其规范来查看使用转换环境的具体示例。有三个规范需要查看:observation_spec
定义在环境中执行动作时预期什么,reward_spec
指示奖励域,最后是 input_spec
(包含 action_spec
),它表示环境执行单个步骤所需的一切。
print("observation_spec:", env.observation_spec)
print("reward_spec:", env.reward_spec)
print("input_spec:", env.input_spec)
print("action_spec (as defined by input_spec):", env.action_spec)
observation_spec: CompositeSpec(
observation: UnboundedContinuousTensorSpec(
shape=torch.Size([11]),
space=None,
device=cpu,
dtype=torch.float32,
domain=continuous),
step_count: BoundedTensorSpec(
shape=torch.Size([1]),
space=ContinuousBox(
low=Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.int64, contiguous=True),
high=Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.int64, contiguous=True)),
device=cpu,
dtype=torch.int64,
domain=continuous),
device=cpu,
shape=torch.Size([]))
reward_spec: UnboundedContinuousTensorSpec(
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)
input_spec: CompositeSpec(
full_state_spec: CompositeSpec(
step_count: BoundedTensorSpec(
shape=torch.Size([1]),
space=ContinuousBox(
low=Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.int64, contiguous=True),
high=Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.int64, contiguous=True)),
device=cpu,
dtype=torch.int64,
domain=continuous),
device=cpu,
shape=torch.Size([])),
full_action_spec: CompositeSpec(
action: 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),
device=cpu,
shape=torch.Size([])),
device=cpu,
shape=torch.Size([]))
action_spec (as defined by input_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)
check_env_specs()
函数运行一个小型的 rollout 并将其输出与环境规范进行比较。如果未引发错误,我们可以确信规范已正确定义。
check_env_specs(env)
为了好玩,让我们看看简单的随机 rollout 看起来是什么样子。您可以调用 env.rollout(n_steps) 并概述环境的输入和输出是什么样的。动作将自动从动作规范域中抽取,因此您无需关心设计随机采样器。
通常,在每个步骤中,RL 环境都会接收一个动作作为输入,并输出一个观察结果、一个奖励和一个完成状态。观察结果可能是复合的,这意味着它可能由多个张量组成。这对 TorchRL 来说不是问题,因为所有观察结果的集合都会自动打包在输出 TensorDict
中。在执行了一定数量的步骤的 rollout(例如,一系列环境步骤和随机动作生成)后,我们将检索一个 TensorDict
实例,其形状与该轨迹长度匹配。
rollout = env.rollout(3)
print("rollout of three steps:", rollout)
print("Shape of the rollout TensorDict:", rollout.batch_size)
rollout of three steps: TensorDict(
fields={
action: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.float32, is_shared=False),
done: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False),
next: TensorDict(
fields={
done: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False),
observation: Tensor(shape=torch.Size([3, 11]), device=cpu, dtype=torch.float32, is_shared=False),
reward: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.float32, is_shared=False),
step_count: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.int64, is_shared=False),
terminated: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False),
truncated: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False)},
batch_size=torch.Size([3]),
device=cpu,
is_shared=False),
observation: Tensor(shape=torch.Size([3, 11]), device=cpu, dtype=torch.float32, is_shared=False),
step_count: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.int64, is_shared=False),
terminated: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False),
truncated: Tensor(shape=torch.Size([3, 1]), device=cpu, dtype=torch.bool, is_shared=False)},
batch_size=torch.Size([3]),
device=cpu,
is_shared=False)
Shape of the rollout TensorDict: torch.Size([3])
我们的 rollout 数据的形状为 torch.Size([3])
,这与我们运行它的步数相匹配。"next"
条目指向当前步骤之后的数据。在大多数情况下,时间 t 处的 "next"
数据与 t+1
处的数据匹配,但如果我们使用某些特定的转换(例如,多步),则可能并非如此。
策略¶
PPO 利用随机策略来处理探索。这意味着我们的神经网络必须输出分布的参数,而不是对应于所采取动作的单个值。
由于数据是连续的,我们使用 Tanh-Normal 分布来尊重动作空间边界。TorchRL 提供了这种分布,我们唯一需要关心的是构建一个神经网络,该网络输出策略工作所需的正确数量的参数(一个位置或均值和一个比例)。
这里唯一增加的难度是将我们的输出分成两等份,并将第二部分映射到严格的正空间。
我们分三个步骤设计策略。
定义一个神经网络
D_obs
->2 * D_action
。实际上,我们的loc
(mu) 和scale
(sigma) 的维度都是D_action
。附加一个
NormalParamExtractor
来提取位置和比例(例如,将输入分成两等份,并将正变换应用于比例参数)。创建一个概率
TensorDictModule
,它可以生成此分布并从中采样。
actor_net = nn.Sequential(
nn.LazyLinear(num_cells, device=device),
nn.Tanh(),
nn.LazyLinear(num_cells, device=device),
nn.Tanh(),
nn.LazyLinear(num_cells, device=device),
nn.Tanh(),
nn.LazyLinear(2 * env.action_spec.shape[-1], device=device),
NormalParamExtractor(),
)
为了使策略能够通过 tensordict
数据载体与环境“对话”,我们将 nn.Module
包装在一个 TensorDictModule
中。此类将简单地准备提供的 in_keys
,并在注册的 out_keys
处就地写入输出。
policy_module = TensorDictModule(
actor_net, in_keys=["observation"], out_keys=["loc", "scale"]
)
现在我们需要根据正态分布的位置和比例构建一个分布。为此,我们指示 ProbabilisticActor
类从位置和比例参数构建一个 TanhNormal
。我们还提供了此分布的最小值和最大值,这些值是从环境规范中收集的。
in_keys
的名称(以及因此来自上面 TensorDictModule
的 out_keys
的名称)不能设置为任何可能的值,因为 TanhNormal
分布构造函数将期望 loc
和 scale
关键字参数。也就是说,ProbabilisticActor
也接受 Dict[str, str]
类型的 in_keys
,其中键值对指示对于要使用的每个关键字参数应使用哪个 in_key
字符串。
policy_module = ProbabilisticActor(
module=policy_module,
spec=env.action_spec,
in_keys=["loc", "scale"],
distribution_class=TanhNormal,
distribution_kwargs={
"low": env.action_spec.space.low,
"high": env.action_spec.space.high,
},
return_log_prob=True,
# we'll need the log-prob for the numerator of the importance weights
)
价值网络¶
价值网络是 PPO 算法的一个关键组成部分,即使它在推理时不会被使用。此模块将读取观察结果并返回对后续轨迹的折扣回报的估计。这使我们能够通过依赖在训练期间动态学习的一些效用估计来摊销学习。我们的价值网络与策略共享相同的结构,但为简单起见,我们为其分配了自己的参数集。
value_net = nn.Sequential(
nn.LazyLinear(num_cells, device=device),
nn.Tanh(),
nn.LazyLinear(num_cells, device=device),
nn.Tanh(),
nn.LazyLinear(num_cells, device=device),
nn.Tanh(),
nn.LazyLinear(1, device=device),
)
value_module = ValueOperator(
module=value_net,
in_keys=["observation"],
)
让我们尝试我们的策略和价值模块。正如我们之前所说,TensorDictModule
的使用使得能够直接读取环境的输出以运行这些模块,因为它们知道读取哪些信息以及在哪里写入信息。
print("Running policy:", policy_module(env.reset()))
print("Running value:", value_module(env.reset()))
Running policy: 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),
loc: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),
observation: Tensor(shape=torch.Size([11]), device=cpu, dtype=torch.float32, is_shared=False),
sample_log_prob: Tensor(shape=torch.Size([]), device=cpu, dtype=torch.float32, is_shared=False),
scale: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),
step_count: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.int64, 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=cpu,
is_shared=False)
Running value: TensorDict(
fields={
done: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.bool, is_shared=False),
observation: Tensor(shape=torch.Size([11]), device=cpu, dtype=torch.float32, is_shared=False),
state_value: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.float32, is_shared=False),
step_count: Tensor(shape=torch.Size([1]), device=cpu, dtype=torch.int64, 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=cpu,
is_shared=False)
数据收集器¶
TorchRL 提供了一组 DataCollector 类。简单来说,这些类执行三个操作:重置环境,根据最新的观察结果计算动作,在环境中执行一步,并重复最后两个步骤,直到环境发出停止信号(或达到完成状态)。
它们允许你控制每次迭代收集多少帧(通过 frames_per_batch
参数),何时重置环境(通过 max_frames_per_traj
参数),在哪个 device
上执行策略等等。它们也设计成可以高效地与批处理和多进程环境一起工作。
最简单的的数据收集器是 SyncDataCollector
:它是一个迭代器,你可以用它来获取给定长度的数据批次,并且它将在收集到一定数量的帧 (total_frames
) 后停止。其他数据收集器 (MultiSyncDataCollector
和 MultiaSyncDataCollector
) 将以同步和异步的方式在一组多进程工作器上执行相同的操作。
与之前的策略和环境一样,数据收集器将返回 TensorDict
实例,其元素总数将与 frames_per_batch
相匹配。使用 TensorDict
将数据传递到训练循环允许你编写完全不考虑展开内容的实际特性的数据加载管道。
collector = SyncDataCollector(
env,
policy_module,
frames_per_batch=frames_per_batch,
total_frames=total_frames,
split_trajs=False,
device=device,
)
回放缓冲区¶
回放缓冲区是离策略 RL 算法的常见构建块。在策略内环境中,每次收集一批数据时,回放缓冲区都会重新填充,并且其数据会重复使用一定数量的 epochs。
TorchRL 的回放缓冲区是使用一个公共容器 ReplayBuffer
构建的,它将缓冲区的组件作为参数:存储、写入器、采样器以及一些可能的转换。只有存储(指示回放缓冲区的容量)是强制性的。我们还指定了一个无重复的采样器,以避免在一个 epoch 中多次采样同一个项目。对于 PPO 使用回放缓冲区不是强制性的,我们可以简单地从收集到的批次中采样子批次,但使用这些类使我们能够以可重复的方式构建内部训练循环。
replay_buffer = ReplayBuffer(
storage=LazyTensorStorage(max_size=frames_per_batch),
sampler=SamplerWithoutReplacement(),
)
损失函数¶
为了方便起见,PPO 损失可以直接从 TorchRL 中导入,使用 ClipPPOLoss
类。这是利用 PPO 的最简单方法:它隐藏了 PPO 的数学运算及其相关的控制流。
PPO 需要计算一些“优势估计”。简而言之,优势是一个反映回报值的期望值的数值,同时处理偏差/方差权衡。要计算优势,只需要 (1) 构建优势模块,该模块利用我们的值运算符,以及 (2) 在每个 epoch 之前将每个数据批次传递给它。GAE 模块将使用新的 "advantage"
和 "value_target"
条目更新输入 tensordict
。 "value_target"
是一个无梯度的张量,表示值网络应该用输入观察结果表示的经验值。这两者都将被 ClipPPOLoss
用于返回策略和值损失。
advantage_module = GAE(
gamma=gamma, lmbda=lmbda, value_network=value_module, average_gae=True
)
loss_module = ClipPPOLoss(
actor_network=policy_module,
critic_network=value_module,
clip_epsilon=clip_epsilon,
entropy_bonus=bool(entropy_eps),
entropy_coef=entropy_eps,
# these keys match by default but we set this for completeness
critic_coef=1.0,
loss_critic_type="smooth_l1",
)
optim = torch.optim.Adam(loss_module.parameters(), lr)
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optim, total_frames // frames_per_batch, 0.0
)
训练循环¶
现在我们拥有了编写训练循环所需的所有部分。步骤包括
收集数据
计算优势
循环遍历收集到的数据以计算损失值
反向传播
优化
重复
重复
重复
logs = defaultdict(list)
pbar = tqdm(total=total_frames)
eval_str = ""
# We iterate over the collector until it reaches the total number of frames it was
# designed to collect:
for i, tensordict_data in enumerate(collector):
# we now have a batch of data to work with. Let's learn something from it.
for _ in range(num_epochs):
# We'll need an "advantage" signal to make PPO work.
# We re-compute it at each epoch as its value depends on the value
# network which is updated in the inner loop.
advantage_module(tensordict_data)
data_view = tensordict_data.reshape(-1)
replay_buffer.extend(data_view.cpu())
for _ in range(frames_per_batch // sub_batch_size):
subdata = replay_buffer.sample(sub_batch_size)
loss_vals = loss_module(subdata.to(device))
loss_value = (
loss_vals["loss_objective"]
+ loss_vals["loss_critic"]
+ loss_vals["loss_entropy"]
)
# Optimization: backward, grad clipping and optimization step
loss_value.backward()
# this is not strictly mandatory but it's good practice to keep
# your gradient norm bounded
torch.nn.utils.clip_grad_norm_(loss_module.parameters(), max_grad_norm)
optim.step()
optim.zero_grad()
logs["reward"].append(tensordict_data["next", "reward"].mean().item())
pbar.update(tensordict_data.numel())
cum_reward_str = (
f"average reward={logs['reward'][-1]: 4.4f} (init={logs['reward'][0]: 4.4f})"
)
logs["step_count"].append(tensordict_data["step_count"].max().item())
stepcount_str = f"step count (max): {logs['step_count'][-1]}"
logs["lr"].append(optim.param_groups[0]["lr"])
lr_str = f"lr policy: {logs['lr'][-1]: 4.4f}"
if i % 10 == 0:
# We evaluate the policy once every 10 batches of data.
# Evaluation is rather simple: execute the policy without exploration
# (take the expected value of the action distribution) for a given
# number of steps (1000, which is our ``env`` horizon).
# The ``rollout`` method of the ``env`` can take a policy as argument:
# it will then execute this policy at each step.
with set_exploration_type(ExplorationType.MEAN), torch.no_grad():
# execute a rollout with the trained policy
eval_rollout = env.rollout(1000, policy_module)
logs["eval reward"].append(eval_rollout["next", "reward"].mean().item())
logs["eval reward (sum)"].append(
eval_rollout["next", "reward"].sum().item()
)
logs["eval step_count"].append(eval_rollout["step_count"].max().item())
eval_str = (
f"eval cumulative reward: {logs['eval reward (sum)'][-1]: 4.4f} "
f"(init: {logs['eval reward (sum)'][0]: 4.4f}), "
f"eval step-count: {logs['eval step_count'][-1]}"
)
del eval_rollout
pbar.set_description(", ".join([eval_str, cum_reward_str, stepcount_str, lr_str]))
# We're also using a learning rate scheduler. Like the gradient clipping,
# this is a nice-to-have but nothing necessary for PPO to work.
scheduler.step()
0%| | 0/10000 [00:00<?, ?it/s]
10%|█ | 1000/10000 [00:02<00:19, 460.31it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.0908 (init= 9.0908), step count (max): 13, lr policy: 0.0003: 10%|█ | 1000/10000 [00:02<00:19, 460.31it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.0908 (init= 9.0908), step count (max): 13, lr policy: 0.0003: 20%|██ | 2000/10000 [00:04<00:17, 464.44it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.1121 (init= 9.0908), step count (max): 14, lr policy: 0.0003: 20%|██ | 2000/10000 [00:04<00:17, 464.44it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.1121 (init= 9.0908), step count (max): 14, lr policy: 0.0003: 30%|███ | 3000/10000 [00:06<00:15, 464.58it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.1395 (init= 9.0908), step count (max): 16, lr policy: 0.0003: 30%|███ | 3000/10000 [00:06<00:15, 464.58it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.1395 (init= 9.0908), step count (max): 16, lr policy: 0.0003: 40%|████ | 4000/10000 [00:08<00:12, 467.00it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.1736 (init= 9.0908), step count (max): 23, lr policy: 0.0002: 40%|████ | 4000/10000 [00:08<00:12, 467.00it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.1736 (init= 9.0908), step count (max): 23, lr policy: 0.0002: 50%|█████ | 5000/10000 [00:10<00:10, 470.14it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2139 (init= 9.0908), step count (max): 24, lr policy: 0.0002: 50%|█████ | 5000/10000 [00:10<00:10, 470.14it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2139 (init= 9.0908), step count (max): 24, lr policy: 0.0002: 60%|██████ | 6000/10000 [00:12<00:08, 472.33it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2239 (init= 9.0908), step count (max): 26, lr policy: 0.0001: 60%|██████ | 6000/10000 [00:12<00:08, 472.33it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2239 (init= 9.0908), step count (max): 26, lr policy: 0.0001: 70%|███████ | 7000/10000 [00:14<00:06, 474.12it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2280 (init= 9.0908), step count (max): 37, lr policy: 0.0001: 70%|███████ | 7000/10000 [00:14<00:06, 474.12it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2280 (init= 9.0908), step count (max): 37, lr policy: 0.0001: 80%|████████ | 8000/10000 [00:16<00:04, 476.40it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2482 (init= 9.0908), step count (max): 42, lr policy: 0.0001: 80%|████████ | 8000/10000 [00:16<00:04, 476.40it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2482 (init= 9.0908), step count (max): 42, lr policy: 0.0001: 90%|█████████ | 9000/10000 [00:19<00:02, 464.81it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2540 (init= 9.0908), step count (max): 44, lr policy: 0.0000: 90%|█████████ | 9000/10000 [00:19<00:02, 464.81it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2540 (init= 9.0908), step count (max): 44, lr policy: 0.0000: 100%|██████████| 10000/10000 [00:21<00:00, 469.15it/s]
eval cumulative reward: 92.4054 (init: 92.4054), eval step-count: 9, average reward= 9.2471 (init= 9.0908), step count (max): 43, lr policy: 0.0000: 100%|██████████| 10000/10000 [00:21<00:00, 469.15it/s]
结果¶
在达到 1M 步上限之前,算法应该已经达到了 1000 步的最大步数,这是轨迹被截断之前的最大步数。
plt.figure(figsize=(10, 10))
plt.subplot(2, 2, 1)
plt.plot(logs["reward"])
plt.title("training rewards (average)")
plt.subplot(2, 2, 2)
plt.plot(logs["step_count"])
plt.title("Max step count (training)")
plt.subplot(2, 2, 3)
plt.plot(logs["eval reward (sum)"])
plt.title("Return (test)")
plt.subplot(2, 2, 4)
plt.plot(logs["eval step_count"])
plt.title("Max step count (test)")
plt.show()
结论和后续步骤¶
在本教程中,我们学习了
如何使用
torchrl
创建和定制环境;如何编写模型和损失函数;
如何设置一个典型的训练循环。
如果你想对本教程进行更多实验,可以应用以下修改
从效率的角度来看,我们可以并行运行多个模拟以加快数据收集速度。查看
ParallelEnv
以获取更多信息。从日志记录的角度来看,可以在请求渲染后向环境添加
torchrl.record.VideoRecorder
转换,以获得倒立摆动作的视觉渲染。查看torchrl.record
以了解更多信息。
脚本的总运行时间:(1 分 25.493 秒)
估计内存使用量:318 MB