PyTorch强化学习实战(24)——连续动作空间中的强化学习
PyTorch强化学习实战(24)——连续动作空间中的强化学习
0. 前言
在本节中,我们将探讨当动作空间不是离散的时,如何与环境进行交互。连续动作空间问题是强化学习 (Reinforcement Learning, RL) 的重要分支,无论在理论还是实践层面都具有重要意义,因为它在机器人学、控制问题及其他与物理对象交互的领域有着重要应用。在本节中,我们将了解此类情况下的挑战并掌握相应的解决方案。
这些内容甚至可以适用于我们已介绍过的问题和环境。例如浏览器环境鼠标点击一节中,点击位置的 x 和 y 坐标可视为两个需要预测的连续变量。这种表示从环境视角来看却极具意义:它更加紧凑且能自然捕捉可能的点击偏差。虽然对大多数任务而言,点击坐标 (x, y) 与点击 (x+1, y+1) 位置并无本质区别。
1. 连续空间
除了 Atari 游戏和经典强化学习 (Reinforcement Learning, RL) 问题,还有许多任务需要的不仅仅是从小型离散集合中选择动作。
举例来说,假设有一个简单机器人,其单个可控关节可在一定角度范围内旋转。通常控制物理关节需要指定目标位置或施加的力——这两种情况都需要对连续值做出决策。这种值与离散动作空间有本质区别:潜在决策值的集合可能是无限的。例如,可以要求关节旋转 13.5° 或 13.512°,结果可能截然不同。当然系统始终存在物理限制(无法无限精度指定动作),但潜在取值空间可能非常庞大。
事实上,当需要与物理世界交互时,连续动作空间的出现概率远高于离散动作集。例如各类机器人控制系统(如温控系统)皆属此类。RL 方法虽可应用于该领域,但在使用优势演员-评论家 (Advantage Actor-Critic, A2C) 或深度Q网络 (Deep Q-Network, DQN) 等方法前,仍需考虑某些细节。
在本节中,我们将探索如何处理这类问题,为解决连续动作空间问题奠定基础。
1.1动作空间
连续动作空间根本且显著的差异在于其连续性。与离散动作空间(动作定义为互斥的离散选项集合,如仅含两个元素的{左, 右})不同,连续动作需要从某个范围内选取具体值(例如
[
0
,
1
]
[0, 1]
[0,1] 区间包含无限个元素,如
0.5
0.5
0.5、
2
/
2
\sqrt2/2
2/2 等)。在每个时间步,智能体必须选择具体的动作值并传递给环境。
在 Gymnasium 中,连续动作空间由 gym.spaces.Box 类表示。Box 包含具有形状和边界值的集合。例如 Atari 模拟器的每个观测都表示为 Box(low=0, high=255, shape=(210,160,3)),这表示 100,800 个值组成一个三维张量,值范围是 0 到 255。而对于动作空间,通常不会处理如此大量的动作——例如我们将用作测试环境的四足机器人具有 8 个连续动作,对应每条腿的两个电机。该环境的动作空间定义为 Box(low=-1, high=1, shape=(8,)),意味着每个时间戳需要从
[
−
1
,
1
]
[-1, 1]
[−1,1] 范围内选取 8 个值来控制机器人。
这种情况下,每一步传递给 env.step() 的动作不再是整数,而是特定形状的 NumPy 向量(包含各动作值)。当然也存在更复杂的情况:当动作空间是离散与连续动作的组合时,可通过 gym.spaces.Tuple 类表示。
1.2 环境
大多数包含连续动作空间的环境都与物理世界相关,因此通常需要物理模拟器。现有大量软件包可模拟物理过程——从简单的开源工具到能模拟多物理场过程(如流体、燃烧和强度模拟)的复杂商业软件包。
在机器人领域,最流行的工具之一是 MuJoCo (多关节接触动力学模拟器)。该物理引擎允许定义系统组件及其交互属性,模拟器通过考虑您的干预来求解系统并获取组件参数(通常是位置、速度和加速度)。这使其成为理想的强化学习 (Reinforcement Learning, RL) 实验环境:可以定义复杂系统(如多足机器人、机械臂或人形机器人),将观测输入 RL 智能体并获取动作输出。
Farama Gymnasium 默认包含多个 MuJoCo 环境,需安装 gymnasium[mujoco] 包即可使用。
除 MuJoCo 外,还有其他可用于 RL 的物理模拟器,最流行的是 PyBullet。本节我们将使用 PyBullet 进行实验,后续学习也会探讨 MuJoCo。安装 PyBullet 需执行 pip install pybullet,由于 PyBullet 未更新至 Gymnasium API,还需安装 OpenAI Gym 保持兼容:
$ pip install gym==0.25.1
check_env.py 可用于验证 PyBullet 运行状态,它会检查动作空间并渲染本节用作实验对象的四足机器人环境图像:
import gymnasium as gym
ENV_ID = "MinitaurBulletEnv-v0"
ENTRY = "pybullet_envs.bullet.minitaur_gym_env:MinitaurBulletEnv"
RENDER = True
if __name__ == "__main__":
gym.register(ENV_ID, entry_point=ENTRY, max_episode_steps=1000,
reward_threshold=15.0, disable_env_checker=True)
env = gym.make(ENV_ID, render=RENDER)
print("Observation space:", env.observation_space)
print("Action space:", env.action_space)
print(env)
print(env.reset())
input("Press any key to exit\n")
env.close()
启动该工具后,将打开图形用户界面 (Graphical User Interface, GUI) 窗口显示四足机器人(如下图所示),我们将训练其移动:

该环境提供 28 个观测值,对应机器人的不同物理参数:速度、位置和加速度(具体可查看 MinitaurBulletEnv-v0 源代码)。动作空间为 8 个定义电机参数的数字,每条腿配备两个电机。环境奖励值为机器人移动距离减去能耗。
2. A2C 方法解决连续动作空间问题
我们将首先把优势演员-评论家 (Advantage Actor-Critic, A2C) 方法应用于行走机器人问题,因为 A2C 能轻松适配连续动作领域。简单快速回顾 A2C 核心思想:通过梯度估计
∇
J
=
∇
𝜃
l
o
g
π
𝜃
(
a
∣
s
)
(
R
−
V
𝜃
(
s
)
)
∇J = ∇_𝜃logπ_𝜃(a|s)(R - V_𝜃(s))
∇J=∇𝜃logπ𝜃(a∣s)(R−V𝜃(s)) 优化策略。其中策略
π
𝜃
(
s
)
π_𝜃(s)
π𝜃(s) 提供给定状态下的动作概率分布,
V
𝜃
(
s
)
V_𝜃(s)
V𝜃(s) 为状态价值评论家,通过贝尔曼方程估计值与实际回报的均方误差(MSE)进行训练。为提升探索性,通常会在损失函数中加入熵奖励项
L
H
=
π
𝜃
(
s
)
l
o
g
π
𝜃
(
s
)
L_H = π_𝜃(s)logπ_𝜃(s)
LH=π𝜃(s)logπ𝜃(s)。
显然,演员-评论家模型中的价值头在连续动作场景中保持不变,唯一需要调整的是策略表示方式。在离散案例中,我们处理的是互斥离散值的单一动作,此时策略自然表示为所有动作的概率分布。
而在连续情况下,通常存在多个动作,每个动作可在特定范围内取值。考虑到这一点,最最简单的策略表示就是直接输出每个动作的具体值——但需注意这些动作值不能与状态价值
V
(
s
)
V(s)
V(s) 混淆(后者表示从该状态可获得的潜在奖励)。为了说明区别,以简单汽车转向为例,在这种情况下我们只能转动方向盘,每个时刻的动作是方向盘的转动角度(动作值),而状态价值则是该状态的潜在折扣奖励(例如,汽车可以行驶的距离),二者截然不同。
回到我们的动作表示,将动作表示为具体值存在诸多缺陷(主要影响环境探索)。更优选择是采用随机表示,例如让网络输出高斯分布参数:对于 N 个动作,需要两个大小为 N 的向量,分别为均值
μ
μ
μ 和方差
σ
2
σ^2
σ2。这样,此时策略可表示为N维不相关正态分布随机向量,网络可决定每个变量的均值和方差。
根据定义,高斯分布的概率密度函数为:
f
(
s
∣
μ
,
σ
2
)
=
1
2
π
σ
2
e
−
(
x
−
μ
)
2
2
σ
2
f(s|\mu,\sigma^2)=\frac 1{\sqrt{2\pi\sigma^2}}e^{-\frac{(x-\mu)^2}{2\sigma^2}}
f(s∣μ,σ2)=2πσ21e−2σ2(x−μ)2
虽然可直接使用该公式计算概率,但为提升数值稳定性,最好通过数学推导简化
l
o
g
π
𝜃
(
a
∣
s
)
logπ_𝜃(a|s)
logπ𝜃(a∣s) 的表达式。最终结果为:
l
o
g
π
θ
(
a
∣
s
)
=
−
(
x
−
μ
)
2
2
σ
2
−
l
o
g
2
π
σ
2
log\pi_\theta(a|s)=-\frac{(x-\mu)^2}{2\sigma^2}-log\sqrt{2\pi\sigma^2}
logπθ(a∣s)=−2σ2(x−μ)2−log2πσ2
高斯分布的熵可通过微分熵定义求得:
l
n
2
π
e
σ
2
ln\sqrt{2πeσ^2}
ln2πeσ2, 至此我们已掌握实现 A2C 方法解决连续动作空间问题所需的所有要素。
3. 模型实现
完整的源代码位于 train_a2c.py、model.py 和 common.py 中。
(1) 我们从 model.py 中定义的模型类开始:
HID_SIZE = 128
class ModelA2C(nn.Module):
def __init__(self, obs_size: int, act_size: int):
super(ModelA2C, self).__init__()
self.base = nn.Sequential(
nn.Linear(obs_size, HID_SIZE),
nn.ReLU(),
)
self.mu = nn.Sequential(
nn.Linear(HID_SIZE, act_size),
nn.Tanh(),
)
self.var = nn.Sequential(
nn.Linear(HID_SIZE, act_size),
nn.Softplus(),
)
self.value = nn.Linear(HID_SIZE, 1)
可以看到,该网络有三个输出头(而非离散优势演员-评论家 (Advantage Actor-Critic, A2C) 常规的两个)。前两个头分别返回动作的均值和方差,最后一个评论家头返回状态价值。均值输出使用双曲正切激活函数(将输出压缩至 [-1, 1] 范围),方差通过 softplus 激活函数处理(即
l
o
g
(
1
+
e
x
)
log(1+e^x)
log(1+ex),其形状类似平滑的 ReLU 函数),该激活确保方差始终为正。价值头照例不应用激活函数。
(2) 前向传播先应用公共层,然后计算各输出头:
def forward(self, x: torch.Tensor):
base_out = self.base(x)
return self.mu(base_out), self.var(base_out), self.value(base_out)
(3) 实现 Agent 类(用于将观测转换为动作):
class AgentA2C(lib.agent.BaseAgent):
def __init__(self, net: ModelA2C, device: torch.device):
self.net = net
self.device = device
def __call__(self, states: lib.agent.States, agent_states: lib.agent.AgentStates):
states_v = lib.agent.float32_preprocessor(states)
states_v = states_v.to(self.device)
mu_v, var_v, _ = self.net(states_v)
mu = mu_v.data.cpu().numpy()
sigma = torch.sqrt(var_v).data.cpu().numpy()
actions = np.random.normal(mu, sigma)
actions = np.clip(actions, -1, 1)
return actions, agent_states
在离散情况中,我们使用了 agent.DQNAgent 和 agent.PolicyAgent 类,但对于本节的问题,我们需要自定义智能体类,这并不复杂:只需继承 agent.BaseAgent 类并重写 __call__ 方法(负责将观测转换为动作)。
在此类中,我们从网络获取均值和方差,并使用 NumPy 函数对正态分布进行采样。为防止动作超出环境规定的 [-1, 1] 边界,使用 np.clip() 将小于 -1 的值替换为 -1,大于 1 的值替换为 1。agent_states 参数虽未使用,但需与所选动作一同返回(因为 BaseAgent 支持保持智能体状态)。当前虽不需要此功能,但之后实现深度确定性策略梯度时,使用 Ornstein-Uhlenbeck (OU) 过程进行随机探索时会非常有用。
(4) 有了模型和智能体,现在可以进入 train_a2c.py 中定义的训练过程。该过程包含训练循环和两个函数:第一个函数用于在独立测试环境中定期检验模型(测试期间无需探索,直接使用模型返回的均值而无随机采样)。测试函数如下:
def test_net(net: model.ModelA2C, env: gym.Env, count: int = 10,
device: torch.device = torch.device("cpu")):
rewards = 0.0
steps = 0
for _ in range(count):
obs, _ = env.reset()
while True:
obs_v = lib.agent.float32_preprocessor([obs])
obs_v = obs_v.to(device)
mu_v = net(obs_v)[0]
action = mu_v.squeeze(dim=0).data.cpu().numpy()
action = np.clip(action, -1, 1)
obs, reward, done, is_tr, _ = env.step(action)
rewards += reward
steps += 1
if done or is_tr:
break
return rewards / count, steps / count
训练模块定义的第二个函数实现策略下已执行动作概率对数的计算,直接实现了先前看到的公式:
l
o
g
π
θ
(
a
∣
s
)
=
−
(
x
−
μ
)
2
2
σ
2
−
l
o
g
2
π
σ
2
log\pi_\theta(a|s)=-\frac{(x-\mu)^2}{2\sigma^2}-log\sqrt{2\pi\sigma^2}
logπθ(a∣s)=−2σ2(x−μ)2−log2πσ2
(5) 唯一差异在于使用 torch.clamp() 函数防止方差过小时出现除零错误:
def calc_logprob(mu_v: torch.Tensor, var_v: torch.Tensor, actions_v: torch.Tensor):
p1 = - ((mu_v - actions_v) ** 2) / (2*var_v.clamp(min=1e-3))
p2 = - torch.log(torch.sqrt(2 * math.pi * var_v))
return p1 + p2
(6) 训练循环照例创建网络和智能体,然后实例化两步经验源和优化器。使用的超参数如下所示:
GAMMA = 0.99
REWARD_STEPS = 2
BATCH_SIZE = 32
LEARNING_RATE = 5e-5
ENTROPY_BETA = 1e-4
TEST_ITERS = 1000
(7) 用于对收集批次执行优化步骤的代码与离散动作空间中的 A2C 训练非常相似,唯一区别在于使用自定义 calc_logprob() 函数以及不同的熵奖励项表达式:
states_v, actions_v, vals_ref_v = common.unpack_batch_a2c(
batch, net, device=device, last_val_gamma=GAMMA ** REWARD_STEPS)
batch.clear()
optimizer.zero_grad()
mu_v, var_v, value_v = net(states_v)
loss_value_v = F.mse_loss(value_v.squeeze(-1), vals_ref_v)
adv_v = vals_ref_v.unsqueeze(dim=-1) - value_v.detach()
log_prob_v = adv_v * calc_logprob(mu_v, var_v, actions_v)
loss_policy_v = -log_prob_v.mean()
ent_v = -(torch.log(2*math.pi*var_v) + 1)/2
entropy_loss_v = ENTROPY_BETA * ent_v.mean()
loss_v = loss_policy_v + entropy_loss_v + loss_value_v
loss_v.backward()
optimizer.step()
每经过 TEST_ITERS 帧,模型就会进行测试,并在获得最佳奖励时保存模型权重。
4. 运行结果
A2C 无论在最佳奖励还是收敛速度方面都表现较差。这很可能是因为仅使用单一环境收集经验——这正是策略梯度 (Policy Gradient, PG) 方法的弱点。因此,我们可以尝试研究多个并行环境对 A2C 的影响。
启动训练时,我们通过 -n 参数传递运行名称(用于 TensorBoard 记录和模型保存目录)。--dev 选项可启用 GPU 加速,但由于输入维度小且网络规模极小,速度提升微乎其微。
经过 1600 万帧优化后,训练过程在测试中达到最佳得分为 0.35,训练和测试期间的奖励与回合步数如下图所示:

回合步数图表显示回合结束前执行的平均步数。环境时间限制为 1000 步,因此低于 1000 的值表明回合因环境检查而终止。大多数 PyBullet 环境内部实现了自损伤检查机制,会主动停止模拟。
5. 使用模型和录制视频
物理模拟器可渲染环境状态,使我们能观察训练模型的行为。针对 A2C 模型,使用 play_a2c.py 工具,其逻辑与 test_net() 函数相同。
启动时需通过 -m 选项指定模型文件,并可选择使用 -r 参数指定目录名,用于通过 RecordVideo 包装器保存视频。在仿真结束时,该工具会显示步数和累计奖励。
小结
本节探讨了强化学习中的连续动作空间问题,这在机器人控制等物理交互领域具有重要应用。与离散动作空间不同,连续动作需从无限集合(如 [-1,1] 区间)中选取具体数值。针对连续动作空间,本节采用优势演员-评论家 (Advantage Actor-Critic, A2C) 方法进行求解,将策略表示为高斯分布——网络输出均值和方差两个向量,从中采样得到动作值,训练过程中使用自定义的概率对数计算函数和熵奖励项。实验以 PyBullet 四足机器人( 8 个连续动作)为测试环境,结果表明 A2C 在连续控制任务中具备可行性。
系列链接
PyTorch强化学习实战(1)——强化学习(Reinforcement Learning,RL)详解
PyTorch强化学习实战(2)——强化学习环境库Gymnasium
PyTorch强化学习实战(3)——Gymnasium API扩展功能
PyTorch强化学习实战(4)——PyTorch基础
PyTorch强化学习实战(5)——PyTorch Ignite 事件驱动机制与实践
PyTorch强化学习实战(6)——交叉熵方法详解与实现
PyTorch强化学习实战(7)——表格学习与贝尔曼方程
PyTorch强化学习实战(8)——Q学习详解与实现
PyTorch强化学习实战(9)——深度Q学习
PyTorch强化学习实战(10)——强化学习高级组件
PyTorch强化学习实战(11)——N步DQN(N-step DQN)
PyTorch强化学习实战(12)——Double DQN(DDQN)
PyTorch强化学习实战(13)——噪声网络(NoisyNet-DQN)
PyTorch强化学习实战(14)——优先经验回放机制
PyTorch强化学习实战(15)——Dueling DQN
PyTorch强化学习实战(16)——Categorical DQN
PyTorch强化学习实战(17)——强化学习训练加速
PyTorch强化学习实战(18)——基于DQN处理股票交易问题
PyTorch强化学习实战(19)——策略梯度法
PyTorch强化学习实战(20)——优势演员-评论家(Advantage Actor-Critic, A2C)
PyTorch强化学习实战(21)——异步优势演员-评论家(Asynchronous Advantage Actor-Critic, A3C)
PyTorch强化学习实战(22)——将强化学习应用于TextWorld互动小说游戏
PyTorch强化学习实战(23)——强化学习在网页导航中的应用
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)