从 DQN 到 Policy Gradient

复习

Q-Learning 是一种用于训练 Q 函数的算法，该action-value 函数决定了在特定状态下采取某一特定动作的价值。通过维护 Q 表来保存所有state-action pair value 的记忆。

对于像《Space Invaders》这样的 Atari 游戏，我们需要把游戏帧作为状态和输入，单帧图像由 210x160 像素组成。由于图像为彩色（RGB），因此包含 3 个通道。所以观测空间形状为 (210, 160, 3)。每个像素的值范围从 0 到 255，所以共有 25 6 210×160×3=25 6100800 256^{210×160×3}=256^{100800}256210×160×3=256100800 种可能的观测结果。

在这种情况下生成和更新 Q 表效率会很低。因此我们会使用 Deep Q-Learning 而不是 Q-Learning 这样的 Tabular Method，选择用神经网络作为 Q 函数的近似器。该神经网络将根据给定状态，近似估计该状态下每个可能动作的 Q 值。

DQN

输入预处理与时序局限性

我们肯定希望降低状态复杂度以减少训练所需的计算时间。

灰度化

颜色并不提供重要信息，因此可以将三个颜色通道（RGB）减少到一个。

剪裁屏幕

不包含重要信息的区域可以剪裁掉。

捕获时序信息

无法通过单帧知道一个像素的运动信息（方向、速度），为了得到时序信息，我们将四帧堆叠在一起。

CNN

堆叠的帧通过三个卷积层进行处理，目的是 捕捉并利用图像中的空间关系。此外，由于帧是堆叠在一起的，我们还可以得到跨帧的时间信息。

MLP

最后是全连接层作为输出，为该状态下每个可能的动作输出一个 Q 值。

Copy
class QNetwork(nn.Module):
def __init__(self, env):
super().__init__()
self.network = nn.Sequential(
nn.Conv2d(4, 32, 8, stride=4),
nn.ReLU(),
nn.Conv2d(32, 64, 4, stride=2),
nn.ReLU(),
nn.Conv2d(64, 64, 3, stride=1),
nn.ReLU(),
nn.Flatten(), # 展平多维输入为一维
nn.Linear(3136, 512), # 全连接层，将3136维输入映射到512维
nn.ReLU(),
nn.Linear(512, env.single_action_space.n), # 输出层，对应动作空间的维度
)

def forward(self, x):
return self.network(x / 255.0) # 将输入归一化到[0,1]范围

网络的输入：通过网络传递的4 帧堆栈作为状态
输出：该状态下每个可能动作的Q 值向量。
然后与 Q-Learning 类似，我们只需使用 epsilon-greedy 策略来选择采取哪个动作。

在训练阶段，我们不再像 Q - 学习那样直接更新状态 - 动作对的 Q 值：通过设计损失函数和梯度下降来优化 DQN 的权重。

训练流程

深度 Q 学习训练算法有两个阶段：

采样：执行操作并将观察到的经验元组存储在重放缓存中。

训练：随机选择一小批量元组，并利用梯度下降更新步骤从该批次中学习。

由于深度 Q 学习（off-policy）中结合了非线性的 Q-Value 函数（神经网络）和自举法（即使用现有估计而非实际完整回报来更新目标，有偏），其训练过程可能会出现不稳定性。Sutton 和 Barto 提出的 “deadly triad” 指的正是这种情况。

稳定训练

为了帮助我们稳定训练，我们实施了三种不同的解决方案：

经验回放以更高效利用经验。

固定 Q-Target 以稳定训练。

双重 DQN，用于解决 Q 值过高估计的问题。

经验回放

深度 Q 学习中的经验回放具有两个功能：

更高效地利用训练过程中的经验。通常的在线强化学习中，智能体与环境交互获取经验（状态、动作、奖励和下一状态），从中学习（更新神经网络），然后丢弃这些经验的方式是非常低效的。经验回放通过更高效地利用训练经验来提供帮助。我们使用一个回放缓冲区来保存经验样本，以便在训练期间重复使用。

Agent 能够从相同经验中多次学习。

避免遗忘先前的经验（即灾难性干扰，或灾难性遗忘）并减少经验之间的关联性。Replay Buffer 的设置可以在与环境交互时存储经验元组，然后从中抽取一小批元组。这防止网络仅学习最近执行的操作。通过随机抽样经验，可以使接触到的经验多样化，防止过度拟合短期状态，并避免了动作值的剧烈波动或灾难性发散。

采样经验并计算损失：

Copy
rb = ReplayBuffer(
args.buffer_size, # 回放缓冲区大小，决定存储多少经验。
envs.single_observation_space,
envs.single_action_space,
device,
optimize_memory_usage=True,
handle_timeout_termination=False,
)

if global_step > args.learning_starts:
if global_step % args.train_frequency == 0:
data = rb.sample(args.batch_size) # 随机采样一个 batch
with torch.no_grad():
target_max, _ = target_network(data.next_observations).max(dim=1)
td_target = data.rewards.flatten() + args.gamma * target_max * (1 - data.dones.flatten())
old_val = q_network(data.observations).gather(1, data.actions).squeeze()
loss = F.mse_loss(td_target, old_val)

固定 Q-Target

在 Q-Learning 有一个关键的问题是，TD 目标（即 Q-Target）和当前 Q Value（即 Q 估计）的参数是共享的。这导致了 Q 目标和 Q 估计同时变化，就像你在追逐一个不断移动的目标。一个美妙的比喻方式是一个牛仔（Q 估计）试图追赶一头移动的奶牛（Q 目标）。尽管牛仔逐渐接近奶牛（误差减少），但目标仍然在移动，导致训练中的显著振荡。

好喜欢这个表示🥹

为了解决这个问题，我们引入了一个固定的 Q-Target。它的核心思想是引入一个独立的网络，它不会在每个时间步都更新，而是每隔 C 步将主网络的参数复制到这个目标网络中。这意味着我们在多个时间步内的目标（Q-Target）保持固定，并且仅根据旧的估计来更新网络。这样就能显著减少目标和估计之间的振荡问题。

如上面的伪代码所示，关键在于使用两个不同的网络：一个是主网络（用来选择动作并进行更新），另一个是目标网络（用来计算 Q-Target），并且每隔 C 步会将主网络的权重拷贝到目标网络中。这样，我们可以稳定训练过程，使 “牛仔能够更有效地追逐奶牛”，减少振荡并加快收敛速度。

Copy
q_network = QNetwork(envs).to(device) # 当前策略网络,负责选择动作和预测Q值
optimizer = optim.Adam(q_network.parameters(), lr=args.learning_rate)
target_network = QNetwork(envs).to(device) # 目标网络,计算TD目标,提供稳定的学习目标。
target_network.load_state_dict(q_network.state_dict()) # 初始化:目标网络的参数与当前策略网络相同

每隔 args.target_network_frequency 步将主网络的参数全量复制到目标网络。这意味着在多个时间步内，Q-Target 保持固定，仅根据旧的估计来更新网络，从而显著减少目标和估计之间的振荡问题。

Copy
tau = 1.0

if global_step % args.target_network_frequency == 0:
for target_param, param in zip(target_network.parameters(), q_network.parameters()):
target_param.data.copy_(args.tau * param.data + (1.0 - args.tau) * target_param.data)

Double DQN

Double DQN 是由 Hado van Hasselt 提出的，专门用于解决 Q 值过估计的问题。

在 Q-Learning 的 TD-Target 计算中，一个常见问题是 “如何确定下一个状态的最佳动作是 Q 值最高的动作？” 我们知道，Q 值的准确性取决于我们尝试的动作和探索过的邻近状态。因此，在训练初期，关于最佳动作的信息并不充分。如果仅根据最高 Q 值来选择动作，可能会导致错误判断。

举例来说，如果非最优的动作被赋予了一个高于最佳动作的 Q 值，学习过程将变得复杂，难以收敛。为了解决这个问题，Double DQN 引入了两个网络来解耦动作选择和 Q 值目标的生成：

主网络（DQN 网络） 用于选择下一个状态的最佳动作（即 Q 值最高的动作）。

目标网络（Target 网络） 用于计算执行该动作后产生的目标 Q 值。

名词解释

用主网络来选择最佳动作，但用目标网络来计算 Q 值。这种方法减少了 Q 值过估计的问题，并且使得训练过程更加稳定和高效。

Copy
with torch.no_grad():
# 使用主网络选择下一个状态的最佳动作
next_q_values = q_network(data.next_observations)
next_actions = torch.argmax(next_q_values, dim=1, keepdim=True)
# 使用目标网络评估这些动作的 Q Value
target_q_values = target_network(data.next_observations)
target_max = target_q_values.gather(1, next_actions).squeeze()
# 计算 TD-Target
td_target = data.rewards.flatten() + args.gamma * target_max * (1 - data.dones.flatten())
# 计算当前 Q Value
old_val = q_network(data.observations).gather(1, data.actions).squeeze()
loss = F.mse_loss(td_target, old_val)

现代的深度强化学习中还有进一步改进的技术，如优先级经验回放和决斗网络（Dueling Networks），这里暂不涉及。

Optuna

深度强化学习中最关键的任务之一是找到一组良好的训练超参数。Optuna 是一个帮助你自动化搜索最佳的超参数组合的库。

Policy Gradient

前面的 Q-Learning 和 DQN 都属于 Value-based 方法，它们通过估计价值函数来间接地寻找最优策略。策略（π\piπ）的存在完全依赖于动作价值的估计，因为策略是一个从价值函数生成的，比如贪婪策略，在给定状态下选择具有最高价值的动作。

而通过基于 Policy-based 的方法，我们希望直接优化策略，从而绕过学习价值函数的中间步骤。接下来，我们将深入学习其中的一个子集，即策略梯度（Policy Gradient）。

在基于策略的方法中，优化大多数情况下是 on-policy 的，因为在每次更新时，我们仅使用由 最新版本的 ​πθ\pi_{\theta}πθ​ 收集的数据（行动轨迹）。

参数化随机策略

例如让神经网络 πθ\pi_{\theta}πθ​​ 输出一个动作的概率分布（随机策略）πθ(a∣s)\pi_{\theta}(a|s)πθ​(a∣s)：

目标函数 J(θ)J(\theta)J(θ)，优化参数 θ\thetaθ，通过梯度上升最大化参数化策略的性能。

优点

方便集成

可以直接估计策略，而无需存储额外数据（action value），可以理解为是端到端的。

可以学习随机策略

由于输出是动作的概率分布，agent 能够探索状态空间而不总是遵循相同的轨迹，无需手动实现探索 / 利用权衡。DNQ 学习的是确定性策略（deterministic policy），我们是通过一些技巧（如 ε- 贪婪策略）引入了随机性，但这并不是价值函数方法的内在特性。同时能够自然地处理状态的不确定性，解决了感知混淆问题。

名词解释

感知混淆是指两个状态看似（或实际上）相同，但需要采取不同的行动。

例如在下面的情景中，agent 吸尘器要吸走灰尘并避免伤害仓鼠，吸尘器只能感知墙壁的位置。在图中这两个红色状态被称为 “混淆状态”（aliased states），因为在这些状态中，agent 感知到的是墙壁的位置 —— 即在上方和下方都有墙壁。这导致了状态的模糊性，无法区分出它是在哪个具体的红色状态。

在使用确定性策略时，吸尘器在红色状态下总是向右或向左移动，若选择错误方向，则会陷入循环。即使使用 ε- 贪心策略，吸尘器主要遵循最佳策略，但在错误状态下仍可能反复探索错误方向，导致效率低下。

高维、连续动作空间中有效

在高维或连续动作空间中策略梯度方法尤为有效。

自动驾驶汽车在每个状态下可能有无穷多的动作选择 —— 比如方向盘可以转动 15°，17.2°， 19.4°，或者进行其他动作如鸣笛。Deep Q-Learning 必须为每个可能动作计算 Q 值，而连续动作空间中选取最大 Q 值本身也是个优化问题。

相反，策略梯度方法直接输出动作的概率分布，无需计算和存储每个动作的 Q 值，在复杂的连续动作场景下更加高效。

收敛性更好

在值方法中，我们通过 argmax argmaxargmax 来取最大 Q 值来更新策略。这种情况下即使 Q 值发生细微变化，动作选择可能会剧烈改变。例如，训练时左转的 Q 值为 0.22，接着右转的 Q 值变为 0.23，策略将发生了显著变化，会更多地选择向右而不是向左。

而在策略梯度方法中，动作的概率随时间平滑变化，策略更稳定。

缺点

局部最优

常收敛于局部最优而非全局最优。

训练效率低

训练过程缓慢，效率低。

high variance

方差较大，这一点会在后面的 actor-critic 中探讨原因及解决方法。

具体分析

策略梯度是通过每次 agent 与环境交互来调整参数（策略），使动作的概率分布能够更多地采样到那些能最大化回报的良好动作。

目标函数

我们的目标是找到能够最大化期望回报的参数 θ\thetaθ：

Copy max⁡θ J(θ)=E τ∼πθ[R(τ)]\max_{\theta} J(\theta) = \mathbb{E}_{\tau \sim \pi_{\theta}} [R(\tau)]θmax​J(θ)=Eτ∼πθ​​[R(τ)]

由于这是一个凹函数（我们希望值最大），所以用梯度上升方法： θ←θ+α∗∇θ J(θ)θ←θ+α∗∇_θJ(θ)θ←θ+α∗∇θ​J(θ).

然而目标函数的真实梯度是无法计算的，因为它需要计算每条可能轨迹的概率，这在计算上极其昂贵。因此，我们希望通过基于样本的估计（收集一些轨迹）来计算梯度估计。

除此之外，环境的状态转移概率（或状态分布）往往是未知的，或者即使已知，它也是复杂的、非线性的，无法直接计算其导数，也就是无法直接对这种状态转移动力学（受马尔可夫决策过程控制）进行微分，来优化策略。

Policy Gradient Theorem

完整的推导可以在 Andrej Karpathy 的博客里看到，我这里之前也做了学习总结：
Policy Gradient 入门 6. PG 推导

强化算法（蒙特卡洛强化）

利用整个 episode 的估计回报来更新策略参数 θ\thetaθ.
使用策略 πθ π_θπθ​​ 收集一个片段 τ ττ，利用该 episode 来估计梯度 g^=∇θ J(θ)\hat{g}=∇_θJ(θ)g^​=∇θ​J(θ)
优化： θ←θ+α g^θ←θ+α\hat{g}θ←θ+αg^​

∇θ log⁡πθ(at∣st)\nabla_{\theta} \log \pi_{\theta}(a_t | s_t)∇θ​logπθ​(at​∣st​)：
这个部分表示对于某个状态 st s_tst​ 和动作 at a_tat​，我们计算的不是具体的动作值（Q 值），而是动作概率的对数的梯度。

R(τ)R(\tau)R(τ)：
这里 R(τ)R(\tau)R(τ) 是整个轨迹 τ\tauτ 上的累积回报，用来衡量执行策略 πθ\pi_\thetaπθ​ 后的总回报。回报率高则提高（状态，动作）组合的概率，反之则降低。

也可以 收集多个片段（轨迹） 来估计梯度：

参考资料

cleanrl 代码库