0. 前言

在本节中,我们将探讨当动作空间不是离散的时,如何与环境进行交互。连续动作空间问题是强化学习 (Reinforcement Learning, RL) 的重要分支,无论在理论还是实践层面都具有重要意义,因为它在机器人学、控制问题及其他与物理对象交互的领域有着重要应用。在本节中,我们将了解此类情况下的挑战并掌握相应的解决方案。
这些内容甚至可以适用于我们已介绍过的问题和环境。例如浏览器环境鼠标点击一节中,点击位置的 xy 坐标可视为两个需要预测的连续变量。这种表示从环境视角来看却极具意义:它更加紧凑且能自然捕捉可能的点击偏差。虽然对大多数任务而言,点击坐标 (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 个值组成一个三维张量,值范围是 0255。而对于动作空间,通常不会处理如此大量的动作——例如我们将用作测试环境的四足机器人具有 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) 窗口显示四足机器人(如下图所示),我们将训练其移动:

PyBullet

该环境提供 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π𝜃(as)(RV𝜃(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πσ2 1e2σ2(xμ)2

虽然可直接使用该公式计算概率,但为提升数值稳定性,最好通过数学推导简化 l o g π 𝜃 ( a ∣ s ) logπ_𝜃(a|s) logπ𝜃(as) 的表达式。最终结果为:
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πθ(as)=2σ2(xμ)2log2πσ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.DQNAgentagent.PolicyAgent 类,但对于本节的问题,我们需要自定义智能体类,这并不复杂:只需继承 agent.BaseAgent 类并重写 __call__ 方法(负责将观测转换为动作)。
在此类中,我们从网络获取均值和方差,并使用 NumPy 函数对正态分布进行采样。为防止动作超出环境规定的 [-1, 1] 边界,使用 np.clip() 将小于 -1 的值替换为 -1,大于 1 的值替换为 1agent_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πθ(as)=2σ2(xμ)2log2πσ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)——强化学习在网页导航中的应用

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐