理论基础:确定性策略梯度 (DPG)

在经典的策略梯度算法中,我们学习的是一个随机策略 $\pi_\theta(a|s)$(一个概率分布)。但在某些场景下,我们更希望直接学习一个确定性的映射 $a = \mu_\theta(s)$。 这就是 DPG 的核心。它的梯度计算公式非常优雅:

$$ \nabla_\theta J(\theta) \approx \mathbb{E}_{s_t \sim \rho^\beta} [\nabla_a Q(s_t, a)|_{a=\mu_\theta(s_t)} \nabla_\theta \mu_\theta(s_t)] $$

简单来说,就是 Critic 告诉 Actor:“在这个状态下,往哪个方向调整动作可以增加 Q 值”,然后 Actor 就往那个方向挪一点。

DDPG 算法架构

DDPG 可以看作是 DQN 在连续动作空间上的扩展。它的公式总结为:

DDPG = DPG + Target Network + Experience Replay + OU Noise

Ornstein-Uhlenbeck (OU) 噪声

由于策略是确定的,智能体不会主动探索。为了让它尝试新动作,我们必须手动添加噪声。 DDPG 使用的是 OU 噪声,它模拟了布朗运动,具有自相关性

$$ dx_t = \theta(\mu - x_t)dt + \sigma dW_t $$

探索性

相比于高斯噪声,OU 噪声更加“平滑”。如果上一帧的噪声是正的,这一帧很可能还是正的,这让机器人在探索时具有“惯性”,更容易保持动作的连续性。

可控性

噪声会围绕均值(通常为0)波动。随着训练进行,我们可以减小噪声方差,从探索逐渐转向利用。

进阶:TD3 算法 (Twin Delayed DDPG)

DDPG 虽然强大,但容易高估 Q 值,导致训练不稳定。TD3 提出了三大改进,专门解决这些问题。

1. 双 Q 网络 (Clipped Double Q-Learning)

同时训练两个 Critic 网络 $Q_1, Q_2$,计算目标值时取最小值。这有效遏制了 Q 值的高估。

y = r + gamma * min(Q1(s', a'), Q2(s', a'))

2. 延迟更新 (Delayed Policy Updates)

Critic 是 Actor 的“导师”。如果导师自己还没学好就乱指导,学生肯定学坏。所以 TD3 让 Actor 的更新频率比 Critic 低(例如 Critic 更新 2 次,Actor 更新 1 次)。

3. 目标策略平滑 (Target Policy Smoothing)

在计算目标 Q 值时,给动作加上被截断的噪声。这是一种正则化,让 Value Function 更加平滑。

a' = a' + clip(noise, -c, c)

常见问题与思考

DDPG 是 Off-policy 还是 On-policy?
DDPG 是典型的 Off-policy 算法。它使用 Experience Replay Buffer 存储历史数据,并且使用 Target Network 计算目标值,这都允许它利用不同策略产生的数据进行学习。
软更新 (Soft Update) 有什么好处?
软更新公式为 $\theta' \leftarrow \tau\theta + (1-\tau)\theta'$。相比于硬更新(直接复制),软更新让目标网络参数变化更平滑,减少了训练过程中的振荡,提高了稳定性。但也因此,目标网络的更新速度变慢了。
TD3 中 Critic 的更新频率比 Actor 快还是慢?
快。 这就是“延迟策略更新”。Critic 更新得更频繁,是为了提供更准确的价值估计(更准确的梯度方向),从而指导 Actor 更好地更新。

核心代码实现 (DDPG Update)

ddpg_agent.py
def update(self, batch_size):
    state, action, reward, next_state, done = self.replay_buffer.sample(batch_size)

    # 1. 计算 Critic Loss
    with torch.no_grad():
        next_action = self.actor_target(next_state)
        target_Q = self.critic_target(next_state, next_action)
        target_Q = reward + (1 - done) * self.gamma * target_Q

    current_Q = self.critic(state, action)
    critic_loss = F.mse_loss(current_Q, target_Q)

    self.critic_optimizer.zero_grad()
    critic_loss.backward()
    self.critic_optimizer.step()

    # 2. 更新 Actor (最大化 Q 值)
    actor_loss = -self.critic(state, self.actor(state)).mean()

    self.actor_optimizer.zero_grad()
    actor_loss.backward()
    self.actor_optimizer.step()

    # 3. 软更新目标网络
    self.soft_update(self.critic, self.critic_target, self.tau)
    self.soft_update(self.actor, self.actor_target, self.tau)