PyTorch强化学习实战(20)------优势演员-评论家(Advantage Actor-Critic, A2C)
0. 前言
我们已经介绍了一种基于策略的方法,讨论了REINFORCE 方法及其改进版本,后者使用折扣奖励来计算策略梯度(梯度指明了策略优化的方向),这两种方法在简单的CartPole问题中表现良好,但在更复杂的Pong 环境中却无法收敛。
本节中,我们将讨论原始策略梯度方法的另一种变体,即演员-评论家 (actor-critic)算法,显著提升了方法的稳定性和收敛速度,现已成为深度强化学习中最强大的方法之一。
1. 方差缩减
降低梯度方差是提升策略梯度法稳定性的重要手段,接下来,我们将深入理解其重要性及具体含义。在统计学中,方差是随机变量与其期望值的平方偏差期望值:
V a r x = E ( x − E \[ x ) 2 ] Varx=\mathbb E(x-\\mathbb E\[x)^2] Varx=E(x−E\[x)2]
方差反映了数据点与均值的离散程度。当方差较大时,随机变量可能取得偏离均值很远的数值。下图展示了一组均值相同 ( μ = 10 μ=10 μ=10) 但方差不同的正态(高斯)分布。

策略梯度的核心思想是增加优质动作的概率并减少劣质动作的概率。用数学符号表示,策略梯度可写作 ∇ J ≈ 𝔼 Q ( s , a ) ∇ l o g π ( a ∣ s ) ∇J ≈ 𝔼Q(s,a)∇logπ(a\|s) ∇J≈EQ(s,a)∇logπ(a∣s)。缩放因子 Q ( s , a ) Q(s,a) Q(s,a) 决定了在特定状态下对已执行动作概率的调整幅度。在 REINFORCE 方法中,我们使用折扣总奖励作为梯度缩放因子。为提升 REINFORCE 的稳定性,我们从梯度缩放因子中减去了平均奖励值。
为了理解这种改进的原理,假设某个优化步骤中存在三个具有不同折扣总奖励的动作: Q 1 Q_1 Q1、 Q 2 Q_2 Q2 和 Q 3 Q_3 Q3。接下来我们分析这些Q值的相对大小会对策略梯度产生何种影响。
第一种情况:假设 Q 1 Q_1 Q1 和 Q 2 Q_2 Q2 为较小正值, Q 3 Q_3 Q3 为较大负值。这意味着前两个步骤的动作获得较小奖励,而第三步动作效果不佳。此时三个步骤的综合梯度会使策略远离第三步的动作,同时略微偏向第一步和第二步的动作------这是完全合理的优化方向。
现在假设奖励值始终为正,仅数值大小不同。这相当于在前例每个奖励值上增加某个常数。此时 Q 1 Q_1 Q1 和 Q 2 Q_2 Q2 将变为较大正值, Q 3 Q_3 Q3 则变为较小正值。但策略更新方向会发生改变:我们将大力推动策略偏向第一步和第二步的动作,同时轻微偏向第三步的动作。严格来说,尽管相对奖励值保持不变,我们却不再试图规避第三步的动作。
这种策略更新对奖励常数的依赖性会严重拖慢训练进程,因为需要更多样本数据来抵消策略梯度中此类偏移的影响。更严重的是,随着智能体策略改进带来的折扣总奖励动态变化,策略梯度方差也会随之改变。以 Atari 乒乓球游戏环境为例,初始阶段平均奖励为 -21 至 -20,所有动作都显得同样糟糕。
为了解决这个问题,在 REINFORCE 方法中我们从Q值中减去了平均总奖励,并将这个平均值称为基线 (baseline)。这个技巧实现了策略梯度标准化:当平均奖励为 -21 时,获得 -20 奖励被视为智能体的成功,从而推动策略向已执行动作方向优化。
2. CartPole 方差
为了在实践中验证这一理论结论,我们需要绘制训练过程中使用基线版本与无基线版本策略梯度的方差变化图。在 cartpole_pg.py 中,代码大部分与 REINFORCE 方法相同。主要差异如下:
- 新增命令行参数
--baseline,用于启用奖励值减去均值的操作,默认情况下不使用基线 - 在每个训练循环中收集策略损失的梯度数据并计算方差
(1) 为单独获取策略损失梯度(排除探索用的熵奖励梯度),需要分两个阶段计算梯度。PyTorch 可轻松实现此操作,以下代码展示了训练循环中的相关部分:
python
optimizer.zero_grad()
logits_v = net(states_v)
log_prob_v = F.log_softmax(logits_v, dim=1)
log_p_a_v = log_prob_v[range(BATCH_SIZE), batch_actions_t]
log_prob_actions_v = batch_scale_v * log_p_a_v
loss_policy_v = -log_prob_actions_v.mean()
我们照常计算策略损失:先计算已执行动作概率的对数值,再乘以策略缩放因子(未使用基线时为折扣总奖励,使用基线时为总奖励减去基线值)。
(2) 接下来,调用 PyTorch 的反向传播功能计算策略损失梯度,并将梯度值保存在模型缓冲区中:
python
loss_policy_v.backward(retain_graph=True)
由于之前已执行过 optimizer.zero_grad(),此时梯度缓冲区将仅包含策略损失的梯度。这里需要注意的关键点是调用 backward() 时设置的 retain_graph=True 参数。该参数指示 PyTorch 保留计算图结构------通常情况下计算图会在反向传播后被销毁,但本例中我们需要保留它。虽然这种情况并不常见,但在需要执行多次反向传播后才调用优化器时,保留计算图会非常有用。
(3) 然后,遍历模型中的所有参数(模型的每个参数都是带有梯度的张量),并将它们的 grad 字段提取为展平的 NumPy 数组:
python
grads = np.concatenate([p.grad.data.numpy().flatten()
for p in net.parameters() if p.grad is not None])
这样我们就得到了包含模型所有变量梯度的长数组。但参数更新不仅要考虑策略梯度,还需要包含熵奖励提供的梯度。为此我们计算熵损失并再次调用 backward()。由于需要执行第二次反向传播,必须传递 retain_graph=True 参数。
(4) 在第二次调用 backward() 时,PyTorch 会反向传播熵损失并将梯度累加到内部梯度缓冲区中。此时我们只需让优化器使用这些组合后的梯度执行优化步骤:
python
prob_v = F.softmax(logits_v, dim=1)
entropy_v = -(prob_v * log_prob_v).sum(dim=1).mean()
entropy_loss_v = -ENTROPY_BETA * entropy_v
entropy_loss_v.backward()
optimizer.step()
(5) 随后,将感兴趣的统计信量写入 TensorBoard:
python
g_l2 = np.sqrt(np.mean(np.square(grads)))
g_max = np.max(np.abs(grads))
writer.add_scalar("grad_l2", g_l2, step_idx)
writer.add_scalar("grad_max", g_max, step_idx)
writer.add_scalar("grad_var", np.var(grads), step_idx)
通过分别运行带 --baseline 参数和不带该参数的两次实验,我们可以得到策略梯度方差的变化曲线。下图展示了平滑后的奖励值(最近 100 回合的平均值)和方差(采用窗口大小为 20 的平滑处理):

下图显示梯度的 L2 范数(幅度)和最大值(所有数值均采用窗口大小为 20 的平滑处理)。

可以看到,带有基线的版本其方差比无基线版本低 2-3 个数量级,这有助于系统更快收敛。
3. 优势演员-评论家 (Advantage Actor-Critic, A2C)
降低方差的下一步是使基线具备状态依赖性(因为不同状态可能对应截然不同的基线值)。实际上,为了决定某个状态下某个动作的适用性,我们使用该动作的折扣总奖励。但总奖励本身可表示为状态价值加上动作优势值: Q ( s , a ) = V ( s ) + A ( s , a ) Q(s, a) = V(s) + A(s, a) Q(s,a)=V(s)+A(s,a)。
那么,为何不直接使用 V ( s ) V(s) V(s) 作为基线?在这种情况下,梯度规模将仅由优势值 A ( s , a ) A(s,a) A(s,a) 决定,直观反映该行动相对于状态平均价值的优越程度。实际上,这是改进策略梯度方法的绝佳思路。唯一的问题在于:我们并不知道需要从折扣总奖励 Q ( s , a ) Q(s,a) Q(s,a) 中减去的状态价值 V ( s ) V(s) V(s)。为了解决这个问题,我们可以引入另一个神经网络来近似估计每个观测状态对应的 V ( s ) V(s) V(s)。训练该网络时,可采用深度Q网络 (Deep Q-Network, DQN)方法中的训练机制:执行贝尔曼步骤后通过最小化均方误差来优化 V ( s ) V(s) V(s) 的近似值。
当获得状态价值(或至少其近似值)后,即可用它计算策略梯度,并更新策略网络,以提升高优势值动作的概率,抑制低优势值动作的选择几率。输出动作概率分布的策略网络称为"演员" (actor),它负责决策动作方案;另一个返回 V ( s ) V(s) V(s) 的网络则称为"评论家" (critic),它通过评估状态价值来评判动作优劣。这一改进称为优势演员-评论家方法 (advantage actor-critic, A2C)。下图展示了其架构原理:

实践中,策略网络与价值网络通常存在部分结构重叠,这主要是出于效率与收敛性考量。在此时策略函数与价值函数会作为网络的两个不同输出端:共享公共主干网络提取的特征,再分别转换为动作概率分布和表示状态价值的单一数值。这种设计使两个网络能共享底层特征(例如 Atari 智能体中的卷积滤波器),同时以不同方式整合这些特征。下图展示了这种架构:

从训练角度来看,我们需完成以下步骤:
- 使用随机值初始化网络参数 θ \theta θ
- 使用当前策略 π 𝜃 π_𝜃 π𝜃 在环境中执行
N步,保存状态 s t s_t st、动作 a t a_t at 和奖励 r t r_t rt - 若到达回合终点则设 R ← 0 R←0 R←0,否则设 R ← V 𝜃 ( s t ) R←V𝜃(s_t) R←V𝜃(st)
- 从 i = t − 1 i=t-1 i=t−1 至 t s t a r t t_{start} tstart 反向处理数据:
- R ← r i + γ R R ← rᵢ + γR R←ri+γR
- 累积策略梯度:
∂ 𝜃 π ← ∂ 𝜃 π + ∇ θ l o g π θ ( a i ∣ s i ) ( R − ( V θ ( s i ) ) ∂𝜃_π←∂𝜃_π+\nabla_\theta log\pi_\theta (a_i|s_i)(R-(V_\theta(s_i)) ∂𝜃π←∂𝜃π+∇θlogπθ(ai∣si)(R−(Vθ(si)) - 累积价值梯度:
∂ 𝜃 v ← ∂ 𝜃 v + ∂ ( R − V θ ( s i ) ) 2 ∂ 𝜃 v ∂𝜃_v←∂𝜃_v+\frac{∂(R-V_\theta(s_i))^2}{∂𝜃_v} ∂𝜃v←∂𝜃v+∂𝜃v∂(R−Vθ(si))2
- 使用累积的梯度更新网络参数,朝着策略梯度方向 ( ∂ 𝜃 π ∂𝜃_π ∂𝜃π) 和价值梯度的相反方向 ( ∂ 𝜃 v ∂𝜃_v ∂𝜃v) 进行调整
- 重复步骤
2至5直至收敛
该算法与通常在研究论文中看到的类似。实际应用中通常采用以下扩展方法提升稳定性:
- 通常通过添加熵奖励项来增强探索能力,具体表现为在损失函数中引入熵值项:
L H = β ∑ i π θ ( s i ) l o g π θ ( s i ) \mathcal L_H=\beta\sum_i\pi_\theta(s_i)log\pi_\theta(s_i) LH=βi∑πθ(si)logπθ(si)
当概率分布均匀时该函数取得最小值,因此将其加入损失函数可防止智能体对自身行动过于确信。超参数β用于调节熵奖励的权重,从而在训练过程中优先保障探索行为。通常该参数在训练期间保持恒定或线性递减 - 梯度累积通常通过整合三个组件的损失函数来实现:策略损失、价值损失及熵损失。需特别注意这些损失函数的符号方向------策略梯度指示的是策略优化的方向,而价值损失与熵损失均需最小化
- 为提升稳定性,建议采用多个环境并行提供观测数据(当存在多重环境时,训练批次数据将来自这些环境的观测结果)
上述方法的并行环境运行版本称为异步优势演员-评论家算法 (Asynchronous Advantage Actor-Critic, A3C)。
4. 在 Pong 游戏中的应用 A2C 算法
我们已经尝试使用策略梯度方法解决 Pong 环境,但效果并不理想。在本节中,我们尝试使用优势演员-评论家(Advantage Actor-Critic, A2C)方法解决 Pong 环境 (pong_a2c.py)。
(1) 首先,定义超参数:
python
GAMMA = 0.99
LEARNING_RATE = 0.001
ENTROPY_BETA = 0.01
BATCH_SIZE = 128
NUM_ENVS = 50
REWARD_STEPS = 4
CLIP_GRAD = 0.1
超参数 CLIP_GRAD 指定梯度裁剪的阈值,主要用于防止优化阶段梯度值过大导致策略更新过度。虽然裁剪功能通过 PyTorch 实现,但其原理非常简单:若梯度的 L2 范数超过该超参数值,则梯度向量将被裁剪至此阈值。
(2) REWARD_STEPS 超参数决定了我们向前取多少步来估算每个动作的总折扣奖励。在策略梯度法中我们使用了约 10 个时间步,但在 A2C 方法中,我们将通过价值估计函数获取后续状态价值,因此可以适当减少步数。定义网络架构:
python
class AtariA2C(nn.Module):
def __init__(self, input_shape: tt.Tuple[int, ...], n_actions: int):
super(AtariA2C, self).__init__()
self.conv = nn.Sequential(
nn.Conv2d(input_shape[0], 32, kernel_size=8, stride=4),
nn.ReLU(),
nn.Conv2d(32, 64, kernel_size=4, stride=2),
nn.ReLU(),
nn.Conv2d(64, 64, kernel_size=3, stride=1),
nn.ReLU(),
nn.Flatten(),
)
size = self.conv(torch.zeros(1, *input_shape)).size()[-1]
self.policy = nn.Sequential(
nn.Linear(size, 512),
nn.ReLU(),
nn.Linear(512, n_actions)
)
self.value = nn.Sequential(
nn.Linear(size, 512),
nn.ReLU(),
nn.Linear(512, 1)
)
该网络包含共享的卷积主体和两个输出头:第一个输出头返回动作的概率分布策略,第二个输出头返回单个数值------即状态的估值。这种架构与 Dueling DQN 网络相似,但训练过程存在本质差异。
(3) 网络前向传播返回包含两个张量的元组------策略与价值:
python
def forward(self, x: torch.ByteTensor) -> tt.Tuple[torch.Tensor, torch.Tensor]:
xx = x / 255
conv_out = self.conv(xx)
return self.policy(conv_out), self.value(conv_out)
(4) 接下来,实现函数 unpack_batch(),该函数接收环境转换批次数据,并返回三个张量------状态批次、执行动作批次,以及通过公式 Q ( s , a ) = ∑ i = 0 N − 1 γ i r i + γ N V ( s N ) Q(s,a)=∑_{i=0}^{N-1}γ_ir_i+γ^NV(s_N) Q(s,a)=∑i=0N−1γiri+γNV(sN) 计算的Q值批次。这些Q值将在两个环节发挥作用:一是通过均方误差 (Mean Squared Error, MSE) 损失函数优化价值估计(与 DQN 训练方式类似),二是用于计算动作的优势函数:
python
def unpack_batch(batch: tt.List[ExperienceFirstLast], net: AtariA2C,
device: torch.device, gamma: float, reward_steps: int):
states = []
actions = []
rewards = []
not_done_idx = []
last_states = []
for idx, exp in enumerate(batch):
states.append(np.asarray(exp.state))
actions.append(int(exp.action))
rewards.append(exp.reward)
if exp.last_state is not None:
not_done_idx.append(idx)
last_states.append(np.asarray(exp.last_state))
首先,我们遍历转换批次数据并将其字段复制到列表中。需要注意的是,由于使用了 ExperienceSourceFirstLast 类,奖励值已经包含了经过 REWARD_STEPS 步长折现的回报。我们还需要处理回合终止的情况,并记录非终止回合在批次中的索引位置。
(5) 接下来,我们将收集到的状态和动作转换为 PyTorch 张量,并在需要时将它们复制到 GPU 中:
python
states_t = torch.FloatTensor(np.asarray(states)).to(device)
actions_t = torch.LongTensor(actions).to(device)
(6) 函数的其余部分计算Q值,并考虑了终止回合的情况:
python
# handle rewards
rewards_np = np.array(rewards, dtype=np.float32)
if not_done_idx:
last_states_t = torch.FloatTensor(np.asarray(last_states)).to(device)
last_vals_t = net(last_states_t)[1]
last_vals_np = last_vals_t.data.cpu().numpy()[:, 0]
last_vals_np *= gamma ** reward_steps
rewards_np[not_done_idx] += last_vals_np
上述代码准备了一个变量,用于保存转移链中的最后一个状态,并通过查询我们的网络来逼近 V ( s ) V(s) V(s)。随后将该值与折扣因子相乘后叠加到即时奖励中。
(7) 在函数末尾,我们将Q值打包成张量并返回:
python
ref_vals_t = torch.FloatTensor(rewards_np).to(device)
return states_t, actions_t, ref_vals_t
(8) 接下来,可以看到一种新的环境创建方式,使用 gym.vector.SyncVectorEnv 类,该类接收通过 lambda 函数列表创建底层环境:
python
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--dev", default="cpu", help="Device to use, default=cpu")
parser.add_argument("--use-async", default=False, action='store_true',
help="Use async vector env (A3C mode)")
parser.add_argument("-n", "--name", required=True, help="Name of the run")
args = parser.parse_args()
device = torch.device(args.dev)
env_factories = [
lambda: lib.common.wrappers.wrap_dqn(gym.make("PongNoFrameskip-v4"))
for _ in range(NUM_ENVS)
]
if args.use_async:
env = gym.vector.AsyncVectorEnv(env_factories)
else:
env = gym.vector.SyncVectorEnv(env_factories)
writer = SummaryWriter(comment="-pong-a2c_" + args.name)
gym.vector.SyncVectorEnv 类由 Gymnasium 提供,允许将多个环境封装为一个统一的"向量化"环境。底层环境必须具有相同的动作空间和观测空间,这使得向量化环境能够接收动作向量并返回批量的观测值和奖励。
同步向量化环境( SyncVectorEnv 类)与"多环境优化"中使用的优化方案几乎完全相同,时我们向经验源传递多个 gym 环境来提升 DQN 训练性能。但在向量化环境的情况下,需要使用不同的经验源类:VectorExperienceSourceFirstLast,该类考虑了向量化特性并优化了智能体对观测数据的处理。从外部接口来看,该经验源的使用方式与之前完全一致。
命令行参数 --use-async (用于将封装类从 SyncVectorEnv 切换为 AsyncVectorEnv )目前暂不相关------我们将在后续讨论A3C方法时使用该功能。
(9) 然后,我们创建网络、智能体和经验源:
python
net = common.AtariA2C(env.single_observation_space.shape,
env.single_action_space.n).to(device)
print(net)
agent = lib.agent.PolicyAgent(lambda x: net(x)[0], apply_softmax=True, device=device)
exp_source = VectorExperienceSourceFirstLast(
env, agent, gamma=GAMMA, steps_count=REWARD_STEPS)
optimizer = optim.Adam(net.parameters(), lr=LEARNING_RATE, eps=1e-3)
向优化器传递 eps 参数。在 Adam 算法中,epsilon 是添加到分母中的极小数值,用于防止除零错误。通常该值会设置为极小值(如 10 − 8 10^{-8} 10−8 或 10 − 10 10^{-10} 10−10),但在我们的场景中,这些值显得过小。使用默认的 epsilon 值时,方法完全无法收敛。很可能是除以 10 − 8 10^{-8} 10−8 这样极小的值会导致梯度变得过大,这不利于训练稳定性。
另一个细节是使用 VectorExperienceSourceFirstLast 替代 ExperienceSourceFirstLast。这是由于向量化环境封装了多个常规 Atari 环境所必需的。向量化环境还暴露了 single_observation_space 和 single_action_space 属性,这些属性代表单个环境的观测空间和动作空间。
(10) 在训练循环中,我们使用了两个包装器:
python
batch = []
with common.RewardTracker(writer, stop_reward=18) as tracker:
with TBMeanTracker(writer, batch_size=10) as tb_tracker:
for step_idx, exp in enumerate(exp_source):
batch.append(exp)
# handle new rewards
new_rewards = exp_source.pop_total_rewards()
if new_rewards:
if tracker.reward(new_rewards[0], step_idx):
break
if len(batch) < BATCH_SIZE:
continue
第一个包装器 common.RewardTracker 负责计算最近 100 个回合的平均奖励,并在该均值超过预期阈值时发出通知。另一个包装器 TBMeanTracker 负责测量参数的最近 10 步均值写入 TensorBoard。这非常实用,因为训练可能涉及数百万个步骤,我们不需要向 TensorBoard 写入数百万个数据点,而是每 10 步写入一次平滑值。
(11) 定义损失函数。首先,使用函数 unpack_batch() 解包批次数据,并要求网络返回该批次的策略和价值:
python
states_t, actions_t, vals_ref_t = common.unpack_batch(
batch, net, device=device, gamma=GAMMA, reward_steps=REWARD_STEPS)
batch.clear()
optimizer.zero_grad()
logits_t, value_t = net(states_t)
策略以非归一化形式返回,因此需要对其应用 softmax 函数转换为概率分布。由于策略损失需要概率分布的对数值,我们将使用 log_softmax 函数,这比先调用 softmax 再取对数在数值稳定性上更优。
(12) 在价值损失部分,我们计算网络返回值与使用贝尔曼方程向前展开四步所得估计值之间的均方误差:
python
loss_value_t = F.mse_loss(value_t.squeeze(-1), vals_ref_t)
(13) 接下来,计算策略损失以获取策略梯度:
python
log_prob_t = F.log_softmax(logits_t, dim=1)
adv_t = vals_ref_t - value_t.detach()
log_act_t = log_prob_t[range(BATCH_SIZE), actions_t]
log_prob_actions_t = adv_t * log_act_t
loss_policy_t = -log_prob_actions_t.mean()
前两个步骤获取策略的对数值并计算动作优势函数,即 A ( s , a ) = Q ( s , a ) − V ( s ) A(s,a)=Q(s,a)-V(s) A(s,a)=Q(s,a)−V(s)。调用 value_t.detach() 至关重要,因为我们不希望将策略梯度传播到价值估计头部。随后,我们取已执行动作的概率对数,并用优势函数进行缩放。策略梯度损失值将等于缩放后策略对数值均值的负值------因为策略梯度指引我们朝向策略改进的方向,而损失值需要被最小化。
(14) 损失函数的最后组成部分是熵损失:
python
prob_t = F.softmax(logits_t, dim=1)
entropy_loss_t = ENTROPY_BETA * (prob_t * log_prob_t).sum(dim=1).mean()
熵损失等于策略熵的缩放值并取负号(熵的计算公式为 H ( π ) = − ∑ π l o g π H(π)=-∑πlogπ H(π)=−∑πlogπ)。
(15) 接下来,计算并提取策略的梯度,这些梯度将用于跟踪最大梯度、方差和 L2 范数:
python
loss_policy_t.backward(retain_graph=True)
grads = np.concatenate([
p.grad.data.cpu().numpy().flatten()
for p in net.parameters() if p.grad is not None
])
(16) 训练的最后,我们反向传播熵损失与价值损失,裁剪梯度,并要求优化器更新网络:
python
loss_v = entropy_loss_t + loss_value_t
loss_v.backward()
nn_utils.clip_grad_norm_(net.parameters(), CLIP_GRAD)
optimizer.step()
loss_v += loss_policy_t
(17) 在训练循环结束时,我们跟踪所有需要在 TensorBoard 中监控的数值:
python
tb_tracker.track("advantage", adv_t, step_idx)
tb_tracker.track("values", value_t, step_idx)
tb_tracker.track("batch_rewards", vals_ref_t, step_idx)
tb_tracker.track("loss_entropy", entropy_loss_t, step_idx)
tb_tracker.track("loss_policy", loss_policy_t, step_idx)
tb_tracker.track("loss_value", loss_value_t, step_idx)
tb_tracker.track("loss_total", loss_v, step_idx)
tb_tracker.track("grad_l2", np.sqrt(np.mean(np.square(grads))), step_idx)
tb_tracker.track("grad_max", np.max(np.abs(grads)), step_idx)
tb_tracker.track("grad_var", np.var(grads), step_idx)
5. 运行结果
启动训练时需使用 --dev (指定 GPU )和 -n (为 TensorBoard 运行命名)选项运行 pong_a2c.py:
shell
$ python pong_a2c.py --dev cuda -n tt
奖励动态曲线相比REINFORCE 方法有明显改善:

左图展示了最近 100 个训练回合的平均奖励值。右图"批次价值"显示了通过贝尔曼方程近似的Q值,整体呈现正向动态趋势,表明我们的训练过程正在持续改进。
接下来四张图与损失函数相关,包含各损失组件及总损失值:

在这里,我们需要重点关注以下几点:
- 首先,价值损失持续下降,表明我们对 V ( s ) V(s) V(s) 的估计在训练过程中不断改进
- 其次可以观察到,熵损失在训练中期出现增长,但并未在总损失中占据主导地位。这本质上意味着随着策略分布逐渐偏离均匀分布,智能体对其行为变得更加自信
- 最后值得注意的是,策略损失在大部分时间内持续下降,且与总损失变化趋势一致------这是积极信号,因为策略梯度始终是我们关注的首要指标
最后一组图显示了优势值及策略梯度指标:

优势函数是策略梯度的缩放因子,其值为 Q ( s , a ) − V ( s ) Q(s,a)-V(s) Q(s,a)−V(s)。我们预期该值在 0 附近波动(因为单次动作对状态价值的影响通常不会很大),上图数据符合我们的预期。梯度图显示梯度值既不过小也不过大。在训练初期(前 200 万帧),梯度方差非常小,但后期开始增大,这表明策略正在发生变化
小结
在本节中,我们深入探讨了深度强化学习中最广泛应用的方法之一:优势演员-评论家算法 (Advantage Actor-Critic, A2C)。该方法巧妙地将策略梯度更新与状态价值估计相结合。我们分析了基线值对梯度统计特性及收敛性的影响,进而研究了基线思想的扩展形式------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强化学习实战(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)------强化学习训练加速