banner
Nagi-ovo

Nagi-ovo

Breezing
github
x

从 DQN 到 Policy Gradient

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

Pasted image 20241004000133

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

DQN#

输入预处理与时序局限性#

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

Pasted image 20241004000752

灰度化#

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

剪裁屏幕#

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

捕获时序信息#

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

CNN#

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

MLP#

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

Pasted image 20241004000340

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 的权重。

Pasted image 20241004002003

训练流程#

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

  • 采样:执行操作并将观察到的经验元组存储在重放缓存中
  • 训练:随机选择一小批量元组,并利用梯度下降更新步骤从该批次中学习。

Pasted image 20241004002352

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

稳定训练#

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

  1. 经验回放以更高效利用经验
  2. 固定 Q-Target 以稳定训练。
  3. 双重 DQN,用于解决 Q 值过高估计的问题

经验回放#

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

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

Pasted image 20241004003215

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

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

Pasted image 20241004003640

采样经验并计算损失:

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 目标)。尽管牛仔逐渐接近奶牛(误差减少),但目标仍然在移动,导致训练中的显著振荡。

Pasted image 20241004012634

Pasted image 20241004012646

Pasted image 20241004012412

好喜欢这个表示🥹

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

Pasted image 20241004012440

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

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 保持固定,仅根据旧的估计来更新网络,从而显著减少目标和估计之间的振荡问题。

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 值目标的生成:

  1. 主网络(DQN 网络) 用于选择下一个状态的最佳动作(即 Q 值最高的动作)。
  2. 目标网络(Target 网络) 用于计算执行该动作后产生的目标 Q 值。

Pasted image 20241004013644

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}​ 输出一个动作的概率分布(随机策略)πθ(as)\pi_{\theta}(a|s)

Pasted image 20241006164702

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

优点#

方便集成#

  • 可以直接估计策略,而无需存储额外数据(action value),可以理解为是端到端的。

可以学习随机策略#

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

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

Pasted image 20241006170520

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

Pasted image 20241006170145

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

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

自动驾驶汽车在每个状态下可能有无穷多的动作选择 —— 比如方向盘可以转动 15°,17.2°, 19.4°,或者进行其他动作如鸣笛。Deep Q-Learning 必须为每个可能动作计算 Q 值,而连续动作空间中选取最大 Q 值本身也是个优化问题。
image
相反,策略梯度方法直接输出动作的概率分布,无需计算和存储每个动作的 Q 值,在复杂的连续动作场景下更加高效。

收敛性更好#

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

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

缺点#

局部最优#

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

训练效率低#

训练过程缓慢,效率低。

high variance#

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

具体分析#

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

Pasted image 20241006172416

目标函数#

Pasted image 20241006174139

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

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

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

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

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

Policy Gradient Theorem#

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

Pasted image 20241006180211

强化算法(蒙特卡洛强化)#

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

Pasted image 20241006181910

  • θlogπθ(atst)\nabla_{\theta} \log \pi_{\theta}(a_t | s_t)
    这个部分表示对于某个状态 sts_t 和动作 ata_t,我们计算的不是具体的动作值(Q 值),而是动作概率的对数的梯度。

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

也可以 收集多个片段(轨迹) 来估计梯度:
Pasted image 20241006182438

参考资料#

cleanrl 代码库

加载中...
此文章数据所有权由区块链加密技术和智能合约保障仅归创作者所有。