Skip to content
Go back

强化学习学习记录(三):TRPO、PPO、DDPG 与 SAC

Edit page
55 min read

19. 第 14 章:TRPO 算法

从 TRPO 开始,Actor-Critic 的问题不再只是“能不能更新”,而是“更新步子能不能稳”。TRPO 的全称是 Trust Region Policy Optimization,也就是信任区域策略优化。

它接在 Actor-Critic 后面。Actor-Critic 已经把 REINFORCE 的完整回报 GtG_t 换成了优势或 TD 误差,训练更稳定一些。但它本质上还是策略梯度方法:沿着让策略变好的方向更新参数。

普通策略梯度最大的问题是:如果更新步子太大,新策略可能突然变得很差。

TRPO 的核心想法是:

策略可以更新,但每次不能离旧策略太远。只允许在一个“信任区域”里更新。

这个“离得远不远”,用 KL 散度衡量。

从 Actor-Critic 到 TRPO。

Actor-Critic 的 Actor 更新可以写成:

θJ(θ)E[Aπ(s,a)θlogπθ(as)]\nabla_\theta J(\theta) \approx \mathbb{E} \left[ A^\pi(s,a) \nabla_\theta \log\pi_\theta(a\mid s) \right]

这里:

Aπ(s,a)=Qπ(s,a)Vπ(s)A^\pi(s,a) = Q^\pi(s,a)-V^\pi(s)

表示动作 aa 比当前状态的平均水平好多少。

如果优势大于 00,就提高这个动作概率;如果优势小于 00,就降低这个动作概率。

但深度网络策略不是线性函数。即使梯度方向是对的,步长太大也可能把策略改坏。比如旧策略本来是:

πθk(s)=[0.45,0.55]\pi_{\theta_k}(\cdot\mid s) = [0.45,0.55]

一次更新后如果变成:

πθ(s)=[0.01,0.99]\pi_{\theta'}(\cdot\mid s) = [0.01,0.99]

这就不是“小幅改进”,而是几乎换了一个策略。旧策略采样得到的数据也不再可靠。

TRPO 要解决的就是这个问题。

TRPO 的目标:既要提升策略,又不能离旧策略太远。

设旧策略是:

πθk\pi_{\theta_k}

新策略是:

πθ\pi_{\theta'}

TRPO 希望新策略的性能不低于旧策略:

J(θ)J(θk)J(\theta') \ge J(\theta_k)

先看策略性能差异。可以把新旧策略的差写成:

J(θ)J(θk)=11γEsνπθEaπθ(s)[Aπθk(s,a)]J(\theta')-J(\theta_k) = \frac{1}{1-\gamma} \mathbb{E}_{s\sim\nu^{\pi_{\theta'}}} \mathbb{E}_{a\sim\pi_{\theta'}(\cdot\mid s)} \left[ A^{\pi_{\theta_k}}(s,a) \right]

这里:

νπ(s)=(1γ)t=0γtPtπ(s)\nu^\pi(s) = (1-\gamma) \sum_{t=0}^{\infty} \gamma^t P_t^\pi(s)

是折扣状态访问分布。

这个式子的意思是:

如果新策略在自己会访问到的状态上,选出来的动作相对旧策略有正优势,那么新策略就比旧策略好。

但这个式子很难直接优化,因为它里面有:

sνπθs\sim\nu^{\pi_{\theta'}}

也就是说,我们要评估一个还没训练好的新策略,却需要先知道它会访问哪些状态。

替代目标:先用旧策略的数据估计新策略。

TRPO 做了一个近似:如果新策略和旧策略足够接近,那么它们访问状态的分布也差不多。

于是把:

νπθ\nu^{\pi_{\theta'}}

近似成:

νπθk\nu^{\pi_{\theta_k}}

然后定义替代目标:

Lθk(θ)=J(θk)+11γEsνπθkEaπθ(s)[Aπθk(s,a)]L_{\theta_k}(\theta') = J(\theta_k) + \frac{1}{1-\gamma} \mathbb{E}_{s\sim\nu^{\pi_{\theta_k}}} \mathbb{E}_{a\sim\pi_{\theta'}(\cdot\mid s)} \left[ A^{\pi_{\theta_k}}(s,a) \right]

但动作 aa 仍然来自新策略 πθ\pi_{\theta'}。为了能用旧策略采样到的数据,TRPO 再使用重要性采样:

Eaπθ(s)[A(s,a)]=Eaπθk(s)[πθ(as)πθk(as)A(s,a)]\mathbb{E}_{a\sim\pi_{\theta'}(\cdot\mid s)} \left[ A(s,a) \right] = \mathbb{E}_{a\sim\pi_{\theta_k}(\cdot\mid s)} \left[ \frac{\pi_{\theta'}(a\mid s)} {\pi_{\theta_k}(a\mid s)} A(s,a) \right]

于是实际优化的核心目标变成:

Es,aπθk[πθ(as)πθk(as)Aπθk(s,a)]\mathbb{E}_{s,a\sim\pi_{\theta_k}} \left[ \frac{\pi_{\theta'}(a\mid s)} {\pi_{\theta_k}(a\mid s)} A^{\pi_{\theta_k}}(s,a) \right]

这里的比值:

ρ(θ)=πθ(as)πθk(as)\rho(\theta') = \frac{\pi_{\theta'}(a\mid s)} {\pi_{\theta_k}(a\mid s)}

叫重要性采样比率。

它衡量的是:

对同一个动作 aa,新策略给它的概率是旧策略的多少倍。

KL 散度和信任区域。

如果新旧策略差得太远,上面的近似就不可靠。所以 TRPO 加了一个约束:

Esνπθk[DKL(πθk(s)πθ(s))]δ\mathbb{E}_{s\sim\nu^{\pi_{\theta_k}}} \left[ D_{\mathrm{KL}} \left( \pi_{\theta_k}(\cdot\mid s) \, \middle\| \, \pi_{\theta'}(\cdot\mid s) \right) \right] \le \delta

KL 散度衡量两个概率分布有多不一样。离散动作下:

DKL(pq)=ipilogpiqiD_{\mathrm{KL}}(p\|q) = \sum_i p_i \log\frac{p_i}{q_i}

在 TRPO 里:

所以 TRPO 的最终优化问题是:

maxθEs,aπθk[πθ(as)πθk(as)Aπθk(s,a)]\max_{\theta'} \mathbb{E}_{s,a\sim\pi_{\theta_k}} \left[ \frac{\pi_{\theta'}(a\mid s)} {\pi_{\theta_k}(a\mid s)} A^{\pi_{\theta_k}}(s,a) \right]

约束为:

Esνπθk[DKL(πθk(s)πθ(s))]δ\mathbb{E}_{s\sim\nu^{\pi_{\theta_k}}} \left[ D_{\mathrm{KL}} \left( \pi_{\theta_k}(\cdot\mid s) \middle\| \pi_{\theta'}(\cdot\mid s) \right) \right] \le \delta

直观理解:

新策略要让优势加权目标变大,但不能和旧策略差太远。

近似求解:一阶目标,二阶约束。

TRPO 的原始约束优化不好直接解,所以做泰勒近似。

令:

Δθ=θθk\Delta\theta = \theta'-\theta_k

对目标函数做一阶近似:

Lθk(θ)gΔθL_{\theta_k}(\theta') \approx g^\top\Delta\theta

其中:

g=θLθk(θ)g = \nabla_\theta L_{\theta_k}(\theta)

是替代目标的梯度。

对 KL 约束做二阶近似:

DKL12ΔθHΔθD_{\mathrm{KL}} \approx \frac{1}{2} \Delta\theta^\top H \Delta\theta

其中 HH 是 KL 散度对策略参数的黑塞矩阵。

于是问题变成:

maxΔθgΔθ\max_{\Delta\theta} g^\top\Delta\theta

约束为:

12ΔθHΔθδ\frac{1}{2} \Delta\theta^\top H \Delta\theta \le \delta

这个形式有解析解:

Δθ=2δgH1gH1g\Delta\theta = \sqrt{ \frac{2\delta} {g^\top H^{-1}g} } H^{-1}g

因此:

θk+1=θk+2δgH1gH1g\theta_{k+1} = \theta_k + \sqrt{ \frac{2\delta} {g^\top H^{-1}g} } H^{-1}g

这个式子是 TRPO 的核心更新方向。

可以把它理解成:

为什么需要共轭梯度。

上面的公式里有:

H1gH^{-1}g

但神经网络参数很多,HH 是一个巨大矩阵,直接求逆几乎不可行。

TRPO 不直接求:

H1H^{-1}

而是把问题改成解线性方程:

Hx=gHx=g

解出来:

x=H1gx=H^{-1}g

这个 xx 就是更新方向。

共轭梯度法的作用就是:在不显式构造 HH 的情况下,求近似的 xx

它只需要能算:

HpHp

也就是黑塞矩阵和某个向量 pp 的乘积。

代码中的函数:

def hessian_matrix_vector_product(self, states, old_action_dists, vector):

就是计算:

HvHv

具体做法是:

  1. 计算旧策略和新策略之间的平均 KL。
  2. 对 KL 求一次梯度。
  3. 把这个梯度和向量 vector 点乘。
  4. 再求一次梯度。

对应思想是:

Hv=θ[(θDKL)v]Hv = \nabla_\theta \left[ \left( \nabla_\theta D_{\mathrm{KL}} \right)^\top v \right]

这叫 Hessian-vector product。

共轭梯度代码在干什么。

代码:

def conjugate_gradient(self, grad, states, old_action_dists):
    x = torch.zeros_like(grad)
    r = grad.clone()
    p = grad.clone()
    rdotr = torch.dot(r, r)
    for i in range(10):
        Hp = self.hessian_matrix_vector_product(states, old_action_dists, p)
        alpha = rdotr / torch.dot(p, Hp)
        x += alpha * p
        r -= alpha * Hp
        new_rdotr = torch.dot(r, r)
        if new_rdotr < 1e-10:
            break
        beta = new_rdotr / rdotr
        p = r + beta * p
        rdotr = new_rdotr
    return x

这里要解的是:

Hx=gHx=g

变量含义:

循环结束后返回的 x 就近似等于:

H1gH^{-1}g

为什么还要线性搜索。

TRPO 前面做了泰勒近似。既然是近似,就可能出现两个问题:

  1. 新策略虽然按公式更新了,但真实目标没有变好。
  2. 新策略的真实 KL 超过了约束 δ\delta

所以 TRPO 还要做线性搜索。

更新方向先算出来:

x=H1gx=H^{-1}g

最大步长系数是:

β=2δxHx\beta = \sqrt{ \frac{2\delta}{x^\top Hx} }

候选更新为:

θk+1=θk+βx\theta_{k+1} = \theta_k + \beta x

如果这个候选更新不满足条件,就缩小步长:

θk+1=θk+αiβx\theta_{k+1} = \theta_k + \alpha^i \beta x

其中:

0<α<10<\alpha<1

代码中:

coef = self.alpha ** i
new_para = old_para + coef * max_vec

就是不断尝试更短的步长。

只有满足两个条件才接受:

if new_obj > old_obj and kl_div < self.kl_constraint:
    return new_para

也就是:

L(θnew)>L(θold)L(\theta_{new}) > L(\theta_{old})

并且:

DKL(πoldπnew)<δD_{\mathrm{KL}}(\pi_{old}\|\pi_{new}) < \delta

如果找不到合适的新参数,就返回旧参数。

广义优势估计 GAE。

TRPO 需要优势函数:

A(st,at)A(s_t,a_t)

本章使用 GAE,也就是 Generalized Advantage Estimation。

先定义 TD 误差:

δt=rt+γV(st+1)V(st)\delta_t = r_t + \gamma V(s_{t+1}) - V(s_t)

一步优势估计:

At(1)=δtA_t^{(1)} = \delta_t

两步优势估计:

At(2)=δt+γδt+1A_t^{(2)} = \delta_t + \gamma\delta_{t+1}

三步优势估计:

At(3)=δt+γδt+1+γ2δt+2A_t^{(3)} = \delta_t + \gamma\delta_{t+1} + \gamma^2\delta_{t+2}

GAE 把不同步数的优势估计加权平均:

AtGAE=l=0(γλ)lδt+lA_t^{\mathrm{GAE}} = \sum_{l=0}^{\infty} (\gamma\lambda)^l \delta_{t+l}

其中:

λ[0,1]\lambda\in[0,1]

当:

λ=0\lambda=0

只看一步 TD:

AtGAE=δtA_t^{\mathrm{GAE}} = \delta_t

方差小,但偏差可能大。

当:

λ=1\lambda=1

接近看完整多步回报,偏差小,但方差变大。

所以 λ\lambda 是在偏差和方差之间做平衡。

代码:

def compute_advantage(gamma, lmbda, td_delta):
    td_delta = td_delta.detach().numpy()
    advantage_list = []
    advantage = 0.0
    for delta in td_delta[::-1]:
        advantage = gamma * lmbda * advantage + delta
        advantage_list.append(advantage)
    advantage_list.reverse()
    return torch.tensor(advantage_list, dtype=torch.float)

为什么从后往前算?因为:

At=δt+γλAt+1A_t = \delta_t + \gamma\lambda A_{t+1}

后一个优势 At+1A_{t+1} 会被当前 AtA_t 用到,所以代码倒序计算。

TRPO 代码主线。

TRPO 的网络结构和 Actor-Critic 很像:

self.actor = PolicyNet(...)
self.critic = ValueNet(...)

但区别是:

self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr=critic_lr)

这里只给 Critic 配了优化器。Actor 不用普通 Adam 直接更新,而是用 TRPO 的信任域更新。

核心更新函数是:

def update(self, transition_dict):

先算 TD 目标:

yt=rt+γ(1dt)Vω(st+1)y_t = r_t + \gamma(1-d_t)V_\omega(s_{t+1})

代码:

td_target = rewards + self.gamma * self.critic(next_states) * (1 - dones)

再算 TD 误差:

δt=ytVω(st)\delta_t = y_t - V_\omega(s_t)

代码:

td_delta = td_target - self.critic(states)

然后用 GAE 算优势:

advantage = compute_advantage(self.gamma, self.lmbda, td_delta.cpu()).to(self.device)

保存旧策略下实际动作的 log 概率:

old_log_probs = torch.log(self.actor(states).gather(1, actions)).detach()

保存旧策略分布:

old_action_dists = torch.distributions.Categorical(
    self.actor(states).detach())

然后 Critic 按 Actor-Critic 的方式更新:

critic_loss = torch.mean(
    F.mse_loss(self.critic(states), td_target.detach()))

最后 Actor 用 TRPO 方式更新:

self.policy_learn(states, actions, old_action_dists, old_log_probs, advantage)

surrogate objective 代码。

代码:

def compute_surrogate_obj(self, states, actions, advantage, old_log_probs, actor):
    log_probs = torch.log(actor(states).gather(1, actions))
    ratio = torch.exp(log_probs - old_log_probs)
    return torch.mean(ratio * advantage)

这里:

log_probs

是新策略下实际动作的 log 概率:

logπθ(as)\log\pi_{\theta'}(a\mid s)

old_log_probs 是旧策略下实际动作的 log 概率:

logπθk(as)\log\pi_{\theta_k}(a\mid s)

所以:

ratio = torch.exp(log_probs - old_log_probs)

对应:

exp(logπθ(as)logπθk(as))=πθ(as)πθk(as)\exp \left( \log\pi_{\theta'}(a\mid s) - \log\pi_{\theta_k}(a\mid s) \right) = \frac{\pi_{\theta'}(a\mid s)} {\pi_{\theta_k}(a\mid s)}

最终:

torch.mean(ratio * advantage)

对应替代目标:

E[πθ(as)πθk(as)A(s,a)]\mathbb{E} \left[ \frac{\pi_{\theta'}(a\mid s)} {\pi_{\theta_k}(a\mid s)} A(s,a) \right]

policy_learn 代码。

代码主线:

surrogate_obj = self.compute_surrogate_obj(...)
grads = torch.autograd.grad(surrogate_obj, self.actor.parameters())
obj_grad = torch.cat([grad.view(-1) for grad in grads]).detach()
descent_direction = self.conjugate_gradient(obj_grad, states, old_action_dists)
Hd = self.hessian_matrix_vector_product(states, old_action_dists, descent_direction)
max_coef = torch.sqrt(2 * self.kl_constraint /
                      (torch.dot(descent_direction, Hd) + 1e-8))
new_para = self.line_search(...)
torch.nn.utils.convert_parameters.vector_to_parameters(
    new_para, self.actor.parameters())

逐步对应公式:

  1. 算替代目标:
Lθk(θ)L_{\theta_k}(\theta')
  1. 对 Actor 参数求梯度:
g=θLθk(θ)g = \nabla_\theta L_{\theta_k}(\theta)
  1. 用共轭梯度求:
x=H1gx = H^{-1}g
  1. 计算最大可行步长:
β=2δxHx\beta = \sqrt{ \frac{2\delta}{x^\top Hx} }
  1. 线性搜索找满足 KL 约束且目标变好的参数。

  2. 用新参数替换 Actor 参数。

离散动作和连续动作的区别。

CartPole 是离散动作,所以 Actor 输出动作概率:

πθ(as)\pi_\theta(a\mid s)

代码用:

torch.distributions.Categorical(probs)

Pendulum 是连续动作,所以 Actor 输出高斯分布的均值和标准差:

μθ(s),σθ(s)\mu_\theta(s),\quad \sigma_\theta(s)

动作从高斯分布采样:

aN(μθ(s),σθ(s)2)a\sim\mathcal{N}(\mu_\theta(s),\sigma_\theta(s)^2)

代码用:

torch.distributions.Normal(mu, std)

连续动作版本的策略网络:

mu = 2.0 * torch.tanh(self.fc_mu(x))
std = F.softplus(self.fc_std(x))
return mu, std

其中:

TRPO 和 PPO 的关系。

TRPO 的约束是:

DKL(πoldπnew)δD_{\mathrm{KL}}(\pi_{old}\|\pi_{new}) \le \delta

它很稳定,但实现复杂,因为要用:

PPO 后面会把这个思想简化。PPO 不再显式求解复杂约束,而是用裁剪目标限制策略变化。

所以可以这样理解:

PPO 是 TRPO 思想的工程化简化版本。

TRPO 的四个关键卡点。

这一节最后把 TRPO 最容易混的四个点放在一起看:重要性采样、泰勒近似和 Hessian、线性搜索、GAE。

重要性采样解决的是:

数据来自旧策略,但我们想估计新策略的目标。\text{数据来自旧策略,但我们想估计新策略的目标。}

核心公式是:

Exq[f(x)]=Exp[q(x)p(x)f(x)]\mathbb{E}_{x\sim q}[f(x)] = \mathbb{E}_{x\sim p} \left[ \frac{q(x)}{p(x)}f(x) \right]

放到 TRPO 里,旧策略是:

πθk\pi_{\theta_k}

新策略是:

πθ\pi_\theta

所以重要性采样比率是:

r(θ)=πθ(as)πθk(as)r(\theta) = \frac{\pi_\theta(a\mid s)} {\pi_{\theta_k}(a\mid s)}

这个比率的含义是:同一个动作 aa,新策略给它的概率是旧策略的多少倍。

泰勒近似解决的是:TRPO 原来的带约束优化问题太难直接解,所以在当前参数附近做局部近似。

目标函数用一阶近似:

L(θk+Δθ)L(θk)+gΔθL(\theta_k+\Delta\theta) \approx L(\theta_k) + g^\top\Delta\theta

其中:

g=θL(θ)g=\nabla_\theta L(\theta)

表示目标函数上升最快的方向。

KL 约束用二阶近似:

DKL12ΔθHΔθD_{\mathrm{KL}} \approx \frac{1}{2} \Delta\theta^\top H\Delta\theta

这里的 HH 是 KL 散度对策略参数的 Hessian 矩阵。它衡量的是:参数往某个方向动一点,会让策略分布变化多大。

所以 TRPO 的近似问题是:

maxΔθgΔθ\max_{\Delta\theta} g^\top\Delta\theta

约束为:

12ΔθHΔθδ\frac{1}{2} \Delta\theta^\top H\Delta\theta \le \delta

它的解是:

Δθ=2δgH1gH1g\Delta\theta = \sqrt{ \frac{2\delta} {g^\top H^{-1}g} } H^{-1}g

但神经网络参数很多,不能直接求 H1H^{-1},所以用共轭梯度解:

Hx=gHx=g

从而得到:

x=H1gx=H^{-1}g

线性搜索解决的是:上面的更新方向来自近似,直接走完整步可能不安全。所以沿着同一个方向不断缩短步长:

θnew=θold+αiΔθ\theta_{\mathrm{new}} = \theta_{\mathrm{old}} + \alpha^i\Delta\theta

直到同时满足:

L(θnew)>L(θold)L(\theta_{\mathrm{new}}) > L(\theta_{\mathrm{old}})

和:

DKL(πoldπnew)<δD_{\mathrm{KL}}(\pi_{\mathrm{old}}\|\pi_{\mathrm{new}}) < \delta

所以线性搜索不是重新找方向,而是确认这一步走多远安全。

GAE 解决的是:TRPO 需要优势函数 AtA_t,但真实优势不知道。

TD 误差是:

δt=rt+γV(st+1)V(st)\delta_t = r_t+\gamma V(s_{t+1})-V(s_t)

GAE 把后续多个 TD 误差加权累积:

AtGAE=l=0(γλ)lδt+lA_t^{\mathrm{GAE}} = \sum_{l=0}^{\infty} (\gamma\lambda)^l \delta_{t+l}

递推形式是:

At=δt+γλAt+1A_t = \delta_t+\gamma\lambda A_{t+1}

所以代码要从后往前算。λ\lambda 越小越像一步 TD,方差小但偏差可能大;λ\lambda 越大越像 MC,偏差小但方差更大。

最后把四个点串起来:

小结。

TRPO 解决的问题是:

策略梯度更新步子太大,策略可能突然变坏。\text{策略梯度更新步子太大,策略可能突然变坏。}

它的核心约束是:

E[DKL(πoldπnew)]δ\mathbb{E} \left[ D_{\mathrm{KL}} (\pi_{old}\|\pi_{new}) \right] \le \delta

它的核心优化目标是:

maxθE[πθ(as)πθk(as)Aπθk(s,a)]\max_{\theta'} \mathbb{E} \left[ \frac{\pi_{\theta'}(a\mid s)} {\pi_{\theta_k}(a\mid s)} A^{\pi_{\theta_k}}(s,a) \right]

它的近似解是:

θk+1=θk+2δgH1gH1g\theta_{k+1} = \theta_k + \sqrt{ \frac{2\delta} {g^\top H^{-1}g} } H^{-1}g

实现时不直接求 H1H^{-1},而是用共轭梯度解:

Hx=gHx=g

再用线性搜索保证:

L(θnew)>L(θold)L(\theta_{new})>L(\theta_{old})

并且:

DKL(πoldπnew)<δD_{\mathrm{KL}}(\pi_{old}\|\pi_{new})<\delta

我最后用三句话记这一章:

参考链接:

20. 第 15 章:PPO 算法

PPO 接在 TRPO 后面看最顺。它保留“新策略不要离旧策略太远”的想法,但把 TRPO 复杂的二阶约束换成了更容易写代码的目标函数。PPO 的全称是 Proximal Policy Optimization,近端策略优化。

TRPO 的思想很好:限制新旧策略距离,让策略不要一次更新太猛。但 TRPO 实现复杂,需要:

PPO 继承 TRPO 的核心思想:

新策略不能离旧策略太远\text{新策略不能离旧策略太远}

但它用更简单的目标函数来实现这个限制。

PPO 和 TRPO 的关系。

TRPO 的优化目标是:

maxθEs,aπθk[πθ(as)πθk(as)Aπθk(s,a)]\max_{\theta} \mathbb{E}_{s,a\sim\pi_{\theta_k}} \left[ \frac{\pi_\theta(a\mid s)} {\pi_{\theta_k}(a\mid s)} A^{\pi_{\theta_k}}(s,a) \right]

约束为:

Esνπθk[DKL(πθk(s)πθ(s))]δ\mathbb{E}_{s\sim\nu^{\pi_{\theta_k}}} \left[ D_{\mathrm{KL}} \left( \pi_{\theta_k}(\cdot\mid s) \middle\| \pi_\theta(\cdot\mid s) \right) \right] \le \delta

这里的核心仍然是重要性采样比率:

rt(θ)=πθ(atst)πθk(atst)r_t(\theta) = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_k}(a_t\mid s_t)}

它表示:新策略对当前动作的概率,相比旧策略放大或缩小了多少。

如果:

rt(θ)>1r_t(\theta)>1

说明新策略更倾向于这个动作。

如果:

rt(θ)<1r_t(\theta)<1

说明新策略更不倾向于这个动作。

TRPO 通过 KL 约束控制 rt(θ)r_t(\theta) 不要变化太极端。PPO 的思路是:不再复杂地解 KL 约束,而是在目标函数里直接惩罚或截断这种变化。

PPO 有两种形式:

实践中 PPO-Clip 更常用。

PPO-Penalty。

PPO-Penalty 把 TRPO 的 KL 约束放进目标函数中。

TRPO 是带约束优化:

maxθE[rt(θ)At]\max_\theta \mathbb{E} \left[ r_t(\theta)A_t \right]

约束为:

DKL(πθkπθ)δD_{\mathrm{KL}}(\pi_{\theta_k}\|\pi_\theta) \le \delta

PPO-Penalty 把它改成无约束优化:

maxθE[rt(θ)AtβDKL(πθk(s)πθ(s))]\max_\theta \mathbb{E} \left[ r_t(\theta)A_t - \beta D_{\mathrm{KL}} \left( \pi_{\theta_k}(\cdot\mid s) \middle\| \pi_\theta(\cdot\mid s) \right) \right]

这里:

如果实际 KL 太大,说明新策略变化太猛,就增大 β\beta

βk+1=2βk\beta_{k+1}=2\beta_k

如果实际 KL 太小,说明更新太保守,就减小 β\beta

βk+1=12βk\beta_{k+1}=\frac{1}{2}\beta_k

更具体地:

dk=DKL(πθkπθ)d_k = D_{\mathrm{KL}}(\pi_{\theta_k}\|\pi_\theta)

如果:

dk<δ1.5d_k < \frac{\delta}{1.5}

说明策略变化太小,惩罚太强,因此:

βk+1=βk2\beta_{k+1}=\frac{\beta_k}{2}

如果:

dk>1.5δd_k>1.5\delta

说明策略变化太大,惩罚太弱,因此:

βk+1=2βk\beta_{k+1}=2\beta_k

否则:

βk+1=βk\beta_{k+1}=\beta_k

PPO-Penalty 的直觉是:

不硬性禁止新策略走太远,而是在目标函数里给“走太远”加罚分。

从 TRPO 约束到 KL 惩罚。

PPO-Penalty 来自 TRPO。

TRPO 的目标是:

maxθE[rt(θ)At]\max_\theta \mathbb{E} \left[ r_t(\theta)A_t \right]

约束为:

DKL(πoldπθ)δD_{\mathrm{KL}} (\pi_{\mathrm{old}}\|\pi_\theta) \le \delta

其中:

rt(θ)=πθ(atst)πold(atst)r_t(\theta) = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\mathrm{old}}(a_t\mid s_t)}

TRPO 的意思是:让策略朝优势方向变好,但新策略不能离旧策略太远。

PPO-Penalty 不再把 KL 写成硬约束,而是把 KL 放进目标函数里当作惩罚项:

maxθE[rt(θ)AtβDKL(πold(st)πθ(st))]\max_\theta \mathbb{E} \left[ r_t(\theta)A_t - \beta D_{\mathrm{KL}} \left( \pi_{\mathrm{old}}(\cdot\mid s_t) \middle\| \pi_\theta(\cdot\mid s_t) \right) \right]

这里:

如果 β\beta 很大,策略更新会更保守;如果 β\beta 很小,策略更新会更激进。

PPO-Penalty 会根据实际 KL 动态调整 β\beta。令:

dk=DKL(πθkπθ)d_k = D_{\mathrm{KL}} (\pi_{\theta_k}\|\pi_\theta)

如果:

dk>1.5δd_k > 1.5\delta

说明新策略离旧策略太远,KL 惩罚不够强,所以:

βk+1=2βk\beta_{k+1} = 2\beta_k

如果:

dk<δ1.5d_k < \frac{\delta}{1.5}

说明更新太保守,KL 惩罚太强,所以:

βk+1=βk2\beta_{k+1} = \frac{\beta_k}{2}

否则:

βk+1=βk\beta_{k+1} = \beta_k

所以 PPO-Penalty 的一句话理解是:

把 TRPO 的 KL 硬约束变成 KL 软惩罚,并通过自适应 β\beta 控制策略更新幅度。

如果写成 PyTorch loss,因为优化器默认最小化,所以要取负号:

Lactor=E[rt(θ)AtβDKL(πoldπθ)]L_{\mathrm{actor}} = - \mathbb{E} \left[ r_t(\theta)A_t - \beta D_{\mathrm{KL}} (\pi_{\mathrm{old}}\|\pi_\theta) \right]

伪代码:

log_probs = torch.log(actor(states).gather(1, actions))
ratio = torch.exp(log_probs - old_log_probs)

new_dist = torch.distributions.Categorical(actor(states))
old_dist = torch.distributions.Categorical(old_probs)
kl = torch.mean(torch.distributions.kl.kl_divergence(old_dist, new_dist))

actor_objective = torch.mean(ratio * advantage - beta * kl)
actor_loss = -actor_objective

if kl > 1.5 * target_kl:
    beta *= 2
elif kl < target_kl / 1.5:
    beta /= 2

两个期望和拉格朗日惩罚。

TRPO 原来是带约束优化:

maxθf(θ)s.t.DKL(πoldπθ)δ\max_\theta f(\theta) \quad \text{s.t.} \quad D_{\mathrm{KL}}(\pi_{\mathrm{old}}\|\pi_\theta)\le\delta

拉格朗日乘数法的思想是把约束变成惩罚项:

maxθ[f(θ)βDKL(πoldπθ)]\max_\theta \left[ f(\theta)-\beta D_{\mathrm{KL}}(\pi_{\mathrm{old}}\|\pi_\theta) \right]

这里 β\beta 是惩罚强度。β\beta 越大,新旧策略距离的惩罚越强。

PPO-Penalty 的目标写成:

argmaxθEsνπθkEaπθk(s)[πθ(as)πθk(as)Aπθk(s,a)βDKL(πθk(s)πθ(s))]\arg\max_\theta \mathbb{E}_{s\sim\nu^{\pi_{\theta_k}}} \mathbb{E}_{a\sim\pi_{\theta_k}(\cdot\mid s)} \left[ \frac{\pi_\theta(a\mid s)} {\pi_{\theta_k}(a\mid s)} A^{\pi_{\theta_k}}(s,a) - \beta D_{\mathrm{KL}} \left( \pi_{\theta_k}(\cdot\mid s) \middle\| \pi_\theta(\cdot\mid s) \right) \right]

第一个期望:

Esνπθk\mathbb{E}_{s\sim\nu^{\pi_{\theta_k}}}

表示状态 ss 来自旧策略 πθk\pi_{\theta_k} 的状态访问分布。

第二个期望:

Eaπθk(s)\mathbb{E}_{a\sim\pi_{\theta_k}(\cdot\mid s)}

表示动作 aa 也是旧策略在状态 ss 下采出来的。

实际代码中,这两个期望就是对旧策略采来的 batch 求平均。

PPO-Clip。

PPO-Clip 是更常用的 PPO 版本。它不显式计算 KL 惩罚,而是直接限制重要性采样比率。

重要性采样比率为:

rt(θ)=πθ(atst)πθk(atst)r_t(\theta) = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_k}(a_t\mid s_t)}

普通的策略目标是:

rt(θ)Atr_t(\theta)A_t

如果 At>0A_t>0,说明动作好,最大化目标会让 rt(θ)r_t(\theta) 变大,也就是提高这个动作概率。

如果 At<0A_t<0,说明动作差,最大化目标会让 rt(θ)r_t(\theta) 变小,也就是降低这个动作概率。

但问题是:rt(θ)r_t(\theta) 可能变得太大或太小,导致策略更新过猛。

PPO-Clip 直接把 rt(θ)r_t(\theta) 限制在:

[1ϵ,1+ϵ][1-\epsilon,1+\epsilon]

截断函数为:

clip(rt(θ),1ϵ,1+ϵ)\mathrm{clip} (r_t(\theta),1-\epsilon,1+\epsilon)

PPO-Clip 的目标函数是:

LCLIP(θ)=E[min(rt(θ)At,clip(rt(θ),1ϵ,1+ϵ)At)]L^{\mathrm{CLIP}}(\theta) = \mathbb{E} \left[ \min \left( r_t(\theta)A_t, \mathrm{clip} (r_t(\theta),1-\epsilon,1+\epsilon)A_t \right) \right]

这里用 min 的目的是:只取更保守的那个目标,防止策略从极端概率变化中继续获得收益。

情况一:优势为正。

如果:

At>0A_t>0

说明动作好,策略应该提高这个动作概率。

但 PPO 不允许无限提高,只允许:

rt(θ)1+ϵr_t(\theta)\le 1+\epsilon

也就是说,如果 ϵ=0.2\epsilon=0.2,那么新策略对这个动作的概率最多相对旧策略提高到:

1.21.2

倍左右。

情况二:优势为负。

如果:

At<0A_t<0

说明动作差,策略应该降低这个动作概率。

但 PPO 不允许无限降低,只允许:

rt(θ)1ϵr_t(\theta)\ge 1-\epsilon

如果 ϵ=0.2\epsilon=0.2,那么新策略对这个动作的概率最多相对旧策略降低到:

0.80.8

倍左右。

所以 PPO-Clip 的一句话理解是:

好动作可以提高概率,但别提高太多;差动作可以降低概率,但别降低太多。

为什么 PPO-Clip 更常用。

PPO-Clip 不显式使用 KL 惩罚,而是直接限制概率比率:

rt(θ)=πθ(atst)πold(atst)r_t(\theta) = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\mathrm{old}}(a_t\mid s_t)}

它的目标函数是:

LCLIP(θ)=E[min(rt(θ)At,clip(rt(θ),1ϵ,1+ϵ)At)]L^{\mathrm{CLIP}}(\theta) = \mathbb{E} \left[ \min \left( r_t(\theta)A_t, \mathrm{clip} (r_t(\theta),1-\epsilon,1+\epsilon)A_t \right) \right]

PPO-Clip 的直觉是:

所以:

ϵ=0.2\epsilon=0.2

时,通常希望概率比率大致被限制在:

[0.8,1.2][0.8,1.2]

PPO-Penalty 和 PPO-Clip 的区别:

方法控制策略变化的方式特点
TRPOKL 硬约束稳定,但实现复杂
PPO-PenaltyKL 软惩罚比 TRPO 简单,但要调 β\beta
PPO-Clip概率比率截断最简单,实践中最常用

为什么截断后梯度会变为 0。

PPO-Clip 的目标是:

LCLIP=min(rA,clip(r,1ϵ,1+ϵ)A)L^{\mathrm{CLIP}} = \min \left( rA, \mathrm{clip}(r,1-\epsilon,1+\epsilon)A \right)

其中:

r=πθ(as)πold(as)r = \frac{\pi_\theta(a\mid s)} {\pi_{\mathrm{old}}(a\mid s)}

当:

A>0,r>1+ϵA>0,\quad r>1+\epsilon

有:

clip(r,1ϵ,1+ϵ)=1+ϵ\mathrm{clip}(r,1-\epsilon,1+\epsilon) = 1+\epsilon

所以:

rA>(1+ϵ)ArA>(1+\epsilon)A

PPO 取较小值:

LCLIP=(1+ϵ)AL^{\mathrm{CLIP}} = (1+\epsilon)A

这个式子已经不含 rr,因此:

LCLIPr=0\frac{\partial L^{\mathrm{CLIP}}}{\partial r} = 0

rr 才是和新策略参数 θ\theta 有关的量,所以该区域不会继续推动新策略提高该动作概率。直觉是:好动作可以增加概率,但超过 1+ϵ1+\epsilon 后不再给额外奖励。

PPO 的代码结构。

PPO 仍然是 Actor-Critic 框架:

self.actor = PolicyNet(...)
self.critic = ValueNet(...)

和 TRPO 不同,PPO 的 Actor 也直接用 Adam 优化:

self.actor_optimizer = torch.optim.Adam(self.actor.parameters(), lr=actor_lr)
self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr=critic_lr)

这就是 PPO 比 TRPO 简单的地方:它不需要共轭梯度和线性搜索。

PPO 的重要超参数:

self.epochs = epochs
self.eps = eps

其中:

PPO 的 update 主线。

先把数据转成张量:

states
actions
rewards
next_states
dones

每条数据都是:

(st,at,rt,st+1,dt)(s_t,a_t,r_t,s_{t+1},d_t)

然后算 TD 目标:

yt=rt+γ(1dt)Vω(st+1)y_t = r_t+\gamma(1-d_t)V_\omega(s_{t+1})

代码:

td_target = rewards + self.gamma * self.critic(next_states) * (1 - dones)

再算 TD 误差:

δt=ytVω(st)\delta_t = y_t - V_\omega(s_t)

代码:

td_delta = td_target - self.critic(states)

再用 GAE 算优势:

AtGAE=l=0(γλ)lδt+lA_t^{\mathrm{GAE}} = \sum_{l=0}^{\infty} (\gamma\lambda)^l\delta_{t+l}

代码:

advantage = rl_utils.compute_advantage(
    self.gamma, self.lmbda, td_delta.cpu()
).to(self.device)

然后保存旧策略下实际动作的 log 概率:

old_log_probs = torch.log(
    self.actor(states).gather(1, actions)
).detach()

注意这里必须 .detach()。因为旧策略概率应该作为固定基准,不应该在后续多轮更新中一起变化。

为什么 PPO 可以对同一批数据训练多轮。

代码中:

for _ in range(self.epochs):

表示同一批采样数据会被训练多轮。

这听起来和 on-policy 有点矛盾。因为策略更新后,数据已经不是新策略采的了。

PPO 的处理方式是:用旧策略概率作为基准,通过比率:

rt(θ)=πθ(atst)πθk(atst)r_t(\theta) = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_k}(a_t\mid s_t)}

追踪新策略和旧策略的差距。

再用 clip 限制:

rt(θ)[1ϵ,1+ϵ]r_t(\theta)\in[1-\epsilon,1+\epsilon]

所以 PPO 可以在一定范围内重复使用同一批 on-policy 数据,而不让策略偏离太远。

PPO 的数据属性。

PPO 通常归类为:

online on-policy 算法\text{online on-policy 算法}

它确实会对同一批数据训练多个 epoch,但这不等于 off-policy,也不等于 offline RL。

PPO 的流程是:

  1. 用当前策略 πθk\pi_{\theta_k} 和环境交互,采集一批数据。
  2. 保存旧策略概率:
πθk(atst)\pi_{\theta_k}(a_t\mid s_t)
  1. 用这批数据更新几轮。
  2. 用 ratio 和 clip 限制新策略别离旧策略太远:
rt(θ)=πθ(atst)πθk(atst)r_t(\theta) = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_k}(a_t\mid s_t)}
  1. 更新完后丢掉这批数据,用新策略重新采样。

所以 PPO 是近似 on-policy。它允许短期复用最近一批数据,但不长期维护 replay buffer。

对比:

算法类型数据来源数据使用方式
PPO当前策略刚采的数据用几个 epoch 后丢掉
DQN/SAC/DDPGreplay buffer 中的历史数据可长期反复采样
Offline RL固定离线数据集不再和环境交互

因此:

多轮复用最近数据off-policy 或 offline\text{多轮复用最近数据} \neq \text{off-policy 或 offline}

PPO 仍然需要不断和环境交互采新数据。

episode、rollout 和 epoch。

episode 是强化学习里的“一整局”。

从初始状态开始:

s0s_0

一直交互到终止:

done=Truedone=True

这一整条轨迹就是一个 episode。

rollout 是按某个策略采样出来的一段或一批轨迹。它可以是完整 episode,也可以只是一个 episode 的片段,还可以是多个 episode 拼成的一批数据。

所以:

episode 是一种完整 rollout\text{episode 是一种完整 rollout}

但:

rollout 不一定是完整 episode\text{rollout 不一定是完整 episode}

epoch 是神经网络训练里的概念,表示用已有数据完整训练一轮。

在 PPO 里:

for _ in range(self.epochs):
    ...

表示同一批刚采来的 rollout 数据,会被拿来重复训练多轮。

所以三者区别是:

概念含义属于哪个阶段
episode一整局环境交互采样阶段
rollout按策略采样出来的一段或一批轨迹采样阶段
epoch对已有数据训练一轮更新阶段

PPO 的典型流程可以写成:

采 rollout算 advantage训练多个 epoch丢掉旧 rollout再采新 rollout\text{采 rollout} \rightarrow \text{算 advantage} \rightarrow \text{训练多个 epoch} \rightarrow \text{丢掉旧 rollout} \rightarrow \text{再采新 rollout}

PPO-Clip 损失函数代码。

核心代码:

log_probs = torch.log(self.actor(states).gather(1, actions))
ratio = torch.exp(log_probs - old_log_probs)
surr1 = ratio * advantage
surr2 = torch.clamp(ratio, 1 - self.eps, 1 + self.eps) * advantage
actor_loss = torch.mean(-torch.min(surr1, surr2))

逐句对应:

新策略下实际动作的 log 概率:

logπθ(atst)\log\pi_\theta(a_t\mid s_t)

比率:

rt(θ)=exp(logπθ(atst)logπθk(atst))r_t(\theta) = \exp \left( \log\pi_\theta(a_t\mid s_t) - \log\pi_{\theta_k}(a_t\mid s_t) \right)

也就是:

rt(θ)=πθ(atst)πθk(atst)r_t(\theta) = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_k}(a_t\mid s_t)}

未截断目标:

surr1=rt(θ)Atsurr1 = r_t(\theta)A_t

截断目标:

surr2=clip(rt(θ),1ϵ,1+ϵ)Atsurr2 = \mathrm{clip} \left( r_t(\theta),1-\epsilon,1+\epsilon \right) A_t

PPO 目标:

min(surr1,surr2)\min(surr1,surr2)

代码里是 loss,所以要取负号:

Lactor=E[min(surr1,surr2)]L_{\mathrm{actor}} = - \mathbb{E} \left[ \min(surr1,surr2) \right]

因为 PyTorch 优化器默认是最小化 loss,而 PPO 原目标是最大化策略目标。

Critic loss 和 Actor-Critic 一样:

Lcritic=(Vω(st)yt)2L_{\mathrm{critic}} = \left( V_\omega(s_t)-y_t \right)^2

代码:

critic_loss = torch.mean(
    F.mse_loss(self.critic(states), td_target.detach()))

PPO 和 TRPO 的核心区别。

TRPO 是:

maxθE[rt(θ)At]\max_\theta \mathbb{E} \left[ r_t(\theta)A_t \right]

约束:

DKL(πoldπnew)δD_{\mathrm{KL}}(\pi_{old}\|\pi_{new}) \le \delta

PPO-Clip 是:

maxθE[min(rt(θ)At,clip(rt(θ),1ϵ,1+ϵ)At)]\max_\theta \mathbb{E} \left[ \min \left( r_t(\theta)A_t, \mathrm{clip}(r_t(\theta),1-\epsilon,1+\epsilon)A_t \right) \right]

TRPO 是显式约束 KL,求解复杂。

PPO 是隐式限制概率比率,实现简单。

所以可以这样记:

离散动作和连续动作 PPO。

离散动作下,Actor 输出动作概率:

πθ(as)\pi_\theta(a\mid s)

代码使用:

torch.distributions.Categorical(probs)

连续动作下,Actor 输出高斯分布参数:

μθ(s),σθ(s)\mu_\theta(s),\quad \sigma_\theta(s)

动作从高斯分布采样:

aN(μθ(s),σθ(s)2)a\sim\mathcal{N} \left( \mu_\theta(s), \sigma_\theta(s)^2 \right)

代码使用:

torch.distributions.Normal(mu, sigma)

连续动作版本中:

mu = 2.0 * torch.tanh(self.fc_mu(x))
std = F.softplus(self.fc_std(x))

其中:

收束。

PPO 可以用一句话收束:

PPO 是 Actor-Critic 框架下的安全策略更新方法,它不像 TRPO 那样显式求解 KL 约束,而是用 KL 惩罚或概率比率截断,让 Actor 在优势方向上更新但不要一次改太猛。

其中:

小结。

PPO 解决的问题和 TRPO 一样:

策略更新不能太猛。\text{策略更新不能太猛。}

TRPO 的方法是显式约束:

DKL(πoldπnew)δD_{\mathrm{KL}}(\pi_{old}\|\pi_{new}) \le \delta

PPO 的方法更简单。PPO-Penalty 把 KL 放进目标函数:

rt(θ)AtβDKL(πoldπnew)r_t(\theta)A_t - \beta D_{\mathrm{KL}} (\pi_{old}\|\pi_{new})

PPO-Clip 直接截断概率比率:

LCLIP(θ)=E[min(rt(θ)At,clip(rt(θ),1ϵ,1+ϵ)At)]L^{\mathrm{CLIP}}(\theta) = \mathbb{E} \left[ \min \left( r_t(\theta)A_t, \mathrm{clip} (r_t(\theta),1-\epsilon,1+\epsilon)A_t \right) \right]

本章最重要的是理解这个比率:

rt(θ)=πθ(atst)πθk(atst)r_t(\theta) = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\theta_k}(a_t\mid s_t)}

它表示新策略相对旧策略对当前动作的概率变化。

PPO-Clip 的最终直觉是:

参考链接:

21. 第 16 章:DDPG 算法

到 DDPG 时,动作空间从离散动作变成连续动作。这里不能再像 DQN 那样枚举所有动作取最大值,只能让 Actor 直接输出一个连续动作。DDPG 的全称是 Deep Deterministic Policy Gradient,深度确定性策略梯度。

前面 PPO、TRPO 都是 Actor-Critic 框架下的随机策略算法:

πθ(as)\pi_\theta(a\mid s)

它们输出的是动作概率分布,再从分布中采样动作。

DDPG 不一样。DDPG 学的是确定性策略:

a=μθ(s)a=\mu_\theta(s)

也就是说,给定状态 ss,Actor 直接输出一个具体动作 aa

DDPG 主要解决两个问题:

  1. DQN 可以 off-policy,但难以处理连续动作空间。
  2. PPO/TRPO 可以处理连续动作,但通常是 on-policy,样本效率较低。

DDPG 的目标是结合两边的优点:

连续动作+off-policy+Actor-Critic\text{连续动作} + \text{off-policy} + \text{Actor-Critic}

为什么连续动作不能直接用 DQN。

DQN 学的是动作价值函数:

Q(s,a)Q(s,a)

选动作时用:

a=argmaxaQ(s,a)a^* = \arg\max_a Q(s,a)

如果动作是离散的,比如:

a{0,1,2,3}a\in\{0,1,2,3\}

那可以把所有动作都算一遍,选最大的。

但如果动作是连续的,比如倒立摆环境中动作是一个连续力矩:

a[2,2]a\in[-2,2]

那动作有无限多个,不可能枚举所有动作再取最大。

DDPG 的思路是:既然不能枚举动作,那就再训练一个 Actor 网络,直接输出让 QQ 尽量大的动作:

μθ(s)argmaxaQω(s,a)\mu_\theta(s) \approx \arg\max_a Q_\omega(s,a)

所以 DDPG 里:

确定性策略和随机策略的区别。

随机策略输出一个分布:

πθ(as)\pi_\theta(a\mid s)

例如:

πθ(s)=[0.2,0.8]\pi_\theta(\cdot\mid s) = [0.2,0.8]

然后从这个分布中采样动作。

确定性策略直接输出动作:

μθ(s)=a\mu_\theta(s)=a

例如在连续动作环境中:

μθ(s)=0.73\mu_\theta(s)=0.73

这就是实际要执行的动作。

所以:

策略类型输出代表算法
随机策略动作概率分布 πθ(as)\pi_\theta(a\mid s)REINFORCE、A2C、TRPO、PPO
确定性策略具体动作 μθ(s)\mu_\theta(s)DDPG

确定性策略的好处是适合连续动作。坏处是探索能力弱,因为同一个状态总会输出同一个动作。

所以 DDPG 在执行动作时会加噪声:

at=μθ(st)+ϵta_t = \mu_\theta(s_t)+\epsilon_t

代码里通常用高斯噪声或 OU 噪声。

确定性策略梯度。

DDPG 的 Actor 目标是让 Critic 评价更高:

J(θ)=Es[Qω(s,μθ(s))]J(\theta) = \mathbb{E}_{s} \left[ Q_\omega(s,\mu_\theta(s)) \right]

这句话很重要。

Actor 的输出是动作:

a=μθ(s)a=\mu_\theta(s)

Critic 评价这个动作:

Qω(s,a)Q_\omega(s,a)

所以 Actor 想最大化:

Qω(s,μθ(s))Q_\omega(s,\mu_\theta(s))

对 Actor 参数 θ\theta 求梯度,用链式法则:

θJ(θ)=Es[aQω(s,a)a=μθ(s)θμθ(s)]\nabla_\theta J(\theta) = \mathbb{E}_{s} \left[ \nabla_a Q_\omega(s,a) \big|_{a=\mu_\theta(s)} \nabla_\theta \mu_\theta(s) \right]

这个公式就是确定性策略梯度的核心。

它可以这样理解:

所以 DDPG 的 Actor 不是直接用:

logπθ(as)\log\pi_\theta(a\mid s)

因为它没有概率分布。它直接通过 Critic 的 QQ 值反向传播来更新 Actor。

DDPG 的四个网络。

DDPG 有四个网络:

网络作用
Actor μθ(s)\mu_\theta(s)当前策略网络,输出动作
Critic Qω(s,a)Q_\omega(s,a)当前价值网络,评价状态动作对
Target Actor μθ(s)\mu_{\theta^-}(s)目标策略网络,计算 TD target
Target Critic Qω(s,a)Q_{\omega^-}(s,a)目标价值网络,计算 TD target

为什么需要目标网络?原因和 DQN 类似:TD target 里也包含神经网络输出,如果目标本身剧烈变化,训练会不稳定。

DDPG 的目标网络不是像 DQN 那样隔一段时间硬复制,而是使用软更新:

θτθ+(1τ)θ\theta^- \leftarrow \tau\theta + (1-\tau)\theta^-

其中:

0<τ10<\tau\ll 1

例如:

τ=0.005\tau=0.005

这表示目标网络每次只朝当前网络移动一点点。

Actor 和 Critic 都有软更新:

θactorτθactor+(1τ)θactor\theta_{\mathrm{actor}}^- \leftarrow \tau\theta_{\mathrm{actor}} + (1-\tau)\theta_{\mathrm{actor}}^- ωcriticτωcritic+(1τ)ωcritic\omega_{\mathrm{critic}}^- \leftarrow \tau\omega_{\mathrm{critic}} + (1-\tau)\omega_{\mathrm{critic}}^-

为什么需要 target_actor。

DDPG 的 Critic target 是:

y=r+γQω(s,μθ(s))y = r+\gamma Q_{\omega^-} \left( s', \mu_{\theta^-}(s') \right)

连续动作下不能像 DQN 那样枚举:

maxaQ(s,a)\max_{a'}Q(s',a')

所以需要 Actor 在下一状态 ss' 给出下一动作:

a=μθ(s)a'=\mu_{\theta^-}(s')

这就是 target_actor 的作用。

为什么不用当前 Actor?因为当前 Actor 每一步都在变化,如果用它计算 target,Critic 要追的目标也会快速变化。target_actor 是当前 Actor 的慢速版本,能让 TD target 更稳定。

DDPG 是 off-policy。

DDPG 使用经验回放池:

D={(s,a,r,s,d)}\mathcal{D} = \{(s,a,r,s',d)\}

训练时从 replay buffer 里采样 batch:

(si,ai,ri,si,di)D(s_i,a_i,r_i,s_i',d_i) \sim \mathcal{D}

这说明 DDPG 是 off-policy。

它执行环境时用的是带噪声的行为策略:

at=μθ(st)+ϵta_t = \mu_\theta(s_t)+\epsilon_t

但学习时 Actor 目标是最大化当前确定性策略:

Qω(s,μθ(s))Q_\omega(s,\mu_\theta(s))

行为策略和学习策略不完全一样,所以它是 off-policy。

这一点和 PPO 不同:

DDPG 的 off-policy 属性。

DDPG 的目标策略是确定性 Actor:

a=μθ(s)a=\mu_\theta(s)

但采样时为了探索,会执行带噪声的行为策略:

at=μθ(st)+ϵta_t=\mu_\theta(s_t)+\epsilon_t

行为策略和目标策略不同:

μbμθ\mu_b\ne\mu_\theta

并且 DDPG 使用 replay buffer:

D={(s,a,r,s,d)}\mathcal{D} = \{(s,a,r,s',d)\}

训练数据可能来自很多旧版本策略,而学习的是当前 Actor。因此 DDPG 是 off-policy。

Critic 怎么更新。

DDPG 的 Critic 学的是:

Qω(s,a)Q_\omega(s,a)

TD target 使用目标网络:

yt=rt+γ(1dt)Qω(st+1,μθ(st+1))y_t = r_t + \gamma(1-d_t) Q_{\omega^-} \left( s_{t+1}, \mu_{\theta^-}(s_{t+1}) \right)

这里:

Critic loss 是:

Lcritic(ω)=E[(Qω(st,at)yt)2]L_{\mathrm{critic}}(\omega) = \mathbb{E} \left[ \left( Q_\omega(s_t,a_t)-y_t \right)^2 \right]

代码对应:

next_q_values = self.target_critic(
    next_states,
    self.target_actor(next_states)
)
q_targets = rewards + self.gamma * next_q_values * (1 - dones)
critic_loss = torch.mean(
    F.mse_loss(self.critic(states, actions), q_targets)
)

这和 DQN 很像,只是 DQN 的 target 是:

r+γmaxaQ(s,a)r+\gamma\max_a Q(s',a)

而 DDPG 的 target 是:

r+γQω(s,μθ(s))r+\gamma Q_{\omega^-}(s',\mu_{\theta^-}(s'))

因为连续动作下不能枚举 maxa\max_a,所以用目标 Actor 给出动作。

Actor 怎么更新。

Actor 的目标是最大化 Critic 给自己的动作打出的分数:

maxθE[Qω(s,μθ(s))]\max_\theta \mathbb{E} \left[ Q_\omega(s,\mu_\theta(s)) \right]

但 PyTorch 优化器默认最小化 loss,所以写成:

Lactor(θ)=E[Qω(s,μθ(s))]L_{\mathrm{actor}}(\theta) = - \mathbb{E} \left[ Q_\omega(s,\mu_\theta(s)) \right]

代码:

actor_loss = -torch.mean(
    self.critic(states, self.actor(states))
)

这句代码的含义是:

  1. Actor 输入状态,输出动作:
μθ(s)\mu_\theta(s)
  1. Critic 评价这个动作:
Qω(s,μθ(s))Q_\omega(s,\mu_\theta(s))
  1. 加负号作为 loss:
Qω(s,μθ(s))-Q_\omega(s,\mu_\theta(s))
  1. 最小化这个 loss,等价于最大化 QQ

所以 DDPG 的 Actor 更新非常直接:

让 Actor 输出能被 Critic 打高分的动作。

探索噪声。

由于 DDPG 是确定性策略,如果不加噪声,同一个状态总会输出同一个动作:

a=μθ(s)a=\mu_\theta(s)

这会导致探索不足。

所以执行动作时加入噪声:

at=μθ(st)+ϵta_t = \mu_\theta(s_t)+\epsilon_t

代码里使用的是高斯噪声:

action = self.actor(state).item()
action = action + self.sigma * np.random.randn(self.action_dim)

其中:

ϵtN(0,σ2)\epsilon_t \sim \mathcal{N}(0,\sigma^2)

原始 DDPG 论文中常用 OU 噪声:

dNt=θ(μNt)dt+σdWtdN_t = \theta(\mu-N_t)dt+\sigma dW_t

OU 噪声是时间相关的,适合有惯性的物理控制任务。简单理解:它不会每一步完全独立乱跳,而是会有连续的探索趋势。

策略网络和价值网络代码。

策略网络:

class PolicyNet(torch.nn.Module):
    def __init__(self, state_dim, hidden_dim, action_dim, action_bound):
        super(PolicyNet, self).__init__()
        self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
        self.fc2 = torch.nn.Linear(hidden_dim, action_dim)
        self.action_bound = action_bound

    def forward(self, x):
        x = F.relu(self.fc1(x))
        return torch.tanh(self.fc2(x)) * self.action_bound

这里:

例如倒立摆动作范围是:

[2,2][-2,2]

那么 tanh 输出乘以 2 后,就能得到合法动作。

价值网络:

class QValueNet(torch.nn.Module):
    def __init__(self, state_dim, hidden_dim, action_dim):
        super(QValueNet, self).__init__()
        self.fc1 = torch.nn.Linear(state_dim + action_dim, hidden_dim)
        self.fc2 = torch.nn.Linear(hidden_dim, hidden_dim)
        self.fc_out = torch.nn.Linear(hidden_dim, 1)

    def forward(self, x, a):
        cat = torch.cat([x, a], dim=1)
        x = F.relu(self.fc1(cat))
        x = F.relu(self.fc2(x))
        return self.fc_out(x)

Critic 输入状态和动作:

(s,a)(s,a)

输出一个标量:

Qω(s,a)Q_\omega(s,a)

所以代码中要把状态和动作拼接:

cat = torch.cat([x, a], dim=1)

DDPG 类初始化。

DDPG 初始化里创建四个网络:

self.actor = PolicyNet(...)
self.critic = QValueNet(...)
self.target_actor = PolicyNet(...)
self.target_critic = QValueNet(...)

然后把当前网络参数复制给目标网络:

self.target_critic.load_state_dict(self.critic.state_dict())
self.target_actor.load_state_dict(self.actor.state_dict())

这是因为训练一开始,目标网络应该和当前网络一致。

然后分别创建优化器:

self.actor_optimizer = torch.optim.Adam(self.actor.parameters(), lr=actor_lr)
self.critic_optimizer = torch.optim.Adam(self.critic.parameters(), lr=critic_lr)

DDPG 和 Actor-Critic 一样,Actor 和 Critic 是分开训练的。但 DDPG 多了目标网络和 replay buffer。

Adam 优化器回顾。

Adam 可以理解为:

动量+自适应学习率\text{动量} + \text{自适应学习率}

普通梯度下降:

θθαgt\theta \leftarrow \theta-\alpha g_t

Adam 维护一阶动量:

mt=β1mt1+(1β1)gtm_t = \beta_1m_{t-1} + (1-\beta_1)g_t

它表示梯度方向的滑动平均。

Adam 还维护二阶动量:

vt=β2vt1+(1β2)gt2v_t = \beta_2v_{t-1} + (1-\beta_2)g_t^2

它表示梯度平方的滑动平均,用来给不同参数自适应调整步长。

偏差修正:

m^t=mt1β1t\hat m_t = \frac{m_t}{1-\beta_1^t} v^t=vt1β2t\hat v_t = \frac{v_t}{1-\beta_2^t}

最终更新:

θt=θt1αm^tv^t+ϵ\theta_t = \theta_{t-1} - \alpha \frac{\hat m_t} {\sqrt{\hat v_t}+\epsilon}

PyTorch 中:

optimizer.zero_grad()
loss.backward()
optimizer.step()

分别表示清空旧梯度、反向传播计算当前梯度、用 Adam 更新参数。

take_action。

代码:

def take_action(self, state):
    state = torch.tensor([state], dtype=torch.float).to(self.device)
    action = self.actor(state).item()
    action = action + self.sigma * np.random.randn(self.action_dim)
    return action

流程:

  1. 状态转成 tensor。
  2. Actor 输出确定性动作:
μθ(s)\mu_\theta(s)
  1. 加高斯噪声用于探索:
a=μθ(s)+ϵa=\mu_\theta(s)+\epsilon
  1. 返回动作给环境。

这里和 PPO 的区别很明显:

item() 是什么。

.item() 是 PyTorch 中把只有一个元素的 tensor 转成 Python 普通数字的方法。

例如:

x = torch.tensor([3.14])
x.item()

得到:

3.14

DDPG 中:

action = self.actor(state).item()

是把 Actor 输出的单元素 tensor 变成环境能接收的普通数值。.item() 只能用于单元素 tensor;多元素 tensor 要用索引或 .tolist()

soft_update。

代码:

def soft_update(self, net, target_net):
    for param_target, param in zip(target_net.parameters(), net.parameters()):
        param_target.data.copy_(
            param_target.data * (1.0 - self.tau) + param.data * self.tau
        )

对应公式:

θτθ+(1τ)θ\theta^- \leftarrow \tau\theta + (1-\tau)\theta^-

如果:

τ=0.005\tau=0.005

目标网络每次只吸收当前网络 0.5%0.5\% 的新参数,所以变化很慢,能稳定 TD target。

两个 target 网络的软更新。

DDPG 中 target_actortarget_critic 都通常在每次 update 后软更新:

θτθ+(1τ)θ\theta^- \leftarrow \tau\theta+(1-\tau)\theta^-

代码:

self.soft_update(self.actor, self.target_actor)
self.soft_update(self.critic, self.target_critic)

若:

τ=0.005\tau=0.005

表示目标网络每次只吸收当前网络 0.5%0.5\% 的参数。它不是 DQN 中常见的“隔一段时间硬复制”,而是更平滑的软更新。

update 主线。

DDPG 的 update 从 replay buffer 采样一批数据后执行。

先转 tensor:

states
actions
rewards
next_states
dones

然后计算 TD target:

next_q_values = self.target_critic(
    next_states,
    self.target_actor(next_states)
)
q_targets = rewards + self.gamma * next_q_values * (1 - dones)

公式:

yt=rt+γ(1dt)Qω(st+1,μθ(st+1))y_t = r_t + \gamma(1-d_t) Q_{\omega^-} \left( s_{t+1}, \mu_{\theta^-}(s_{t+1}) \right)

更新 Critic:

critic_loss = torch.mean(
    F.mse_loss(self.critic(states, actions), q_targets)
)
self.critic_optimizer.zero_grad()
critic_loss.backward()
self.critic_optimizer.step()

公式:

Lcritic=(Qω(st,at)yt)2L_{\mathrm{critic}} = \left( Q_\omega(s_t,a_t)-y_t \right)^2

更新 Actor:

actor_loss = -torch.mean(
    self.critic(states, self.actor(states))
)
self.actor_optimizer.zero_grad()
actor_loss.backward()
self.actor_optimizer.step()

公式:

Lactor=E[Qω(s,μθ(s))]L_{\mathrm{actor}} = - \mathbb{E} \left[ Q_\omega(s,\mu_\theta(s)) \right]

最后软更新目标网络:

self.soft_update(self.actor, self.target_actor)
self.soft_update(self.critic, self.target_critic)

DDPG、DQN、PPO 的对比。

算法动作空间策略类型是否 off-policy是否用 replay buffer
DQN离散动作argmaxaQ(s,a)\arg\max_a Q(s,a) 得到
PPO离散/连续都可随机策略 πθ(as)\pi_\theta(a\mid s)通常不是通常不用长期 replay buffer
DDPG连续动作确定性策略 μθ(s)\mu_\theta(s)

DDPG 可以理解成:

DQN 的 off-policy/replay buffer 思想+Actor-Critic 的连续动作策略网络\text{DQN 的 off-policy/replay buffer 思想} + \text{Actor-Critic 的连续动作策略网络}

它用 Actor 代替 DQN 里的:

argmaxaQ(s,a)\arg\max_a Q(s,a)

因为连续动作下无法枚举所有动作。

PPO 和 DDPG 的 Actor 更新差异。

PPO 的 Actor 更新来自概率比率:

rt(θ)=πθ(atst)πold(atst)r_t(\theta) = \frac{\pi_\theta(a_t\mid s_t)} {\pi_{\mathrm{old}}(a_t\mid s_t)}

PPO-Clip 的目标是:

min(rt(θ)At,clip(rt(θ),1ϵ,1+ϵ)At)\min \left( r_t(\theta)A_t, \mathrm{clip}(r_t(\theta),1-\epsilon,1+\epsilon)A_t \right)

其中 AtA_t 是优势,来自 Critic 估计的价值函数。常见做法是先算 TD 误差:

δt=rt+γ(1dt)Vω(st+1)Vω(st)\delta_t = r_t+\gamma(1-d_t)V_\omega(s_{t+1})-V_\omega(s_t)

再用 GAE:

At=δt+γλAt+1A_t = \delta_t+\gamma\lambda A_{t+1}

PPO 更新时,AtA_t 通常被当作固定权重。Actor 的梯度主要来自:

θlogπθ(atst)\nabla_\theta \log\pi_\theta(a_t\mid s_t)

DDPG 的 Actor 更新则是直接最大化 Critic 给 Actor 输出动作的评分:

Lactor=E[Qω(s,μθ(s))]L_{\mathrm{actor}} = - \mathbb{E} \left[ Q_\omega(s,\mu_\theta(s)) \right]

梯度路径是:

sμθ(s)Qω(s,μθ(s))Lactors \rightarrow \mu_\theta(s) \rightarrow Q_\omega(s,\mu_\theta(s)) \rightarrow L_{\mathrm{actor}}

所以 DDPG 的 Actor 更新会通过 Critic 对动作的梯度回传:

θJ=aQω(s,a)a=μθ(s)θμθ(s)\nabla_\theta J = \nabla_a Q_\omega(s,a) \big|_{a=\mu_\theta(s)} \nabla_\theta\mu_\theta(s)

一句话区别:

小结。

DDPG 是面向连续动作控制的 off-policy Actor-Critic 算法。

它的 Actor 是确定性策略:

a=μθ(s)a=\mu_\theta(s)

Critic 评价状态动作对:

Qω(s,a)Q_\omega(s,a)

Critic 的 TD target 是:

yt=rt+γ(1dt)Qω(st+1,μθ(st+1))y_t = r_t + \gamma(1-d_t) Q_{\omega^-} \left( s_{t+1}, \mu_{\theta^-}(s_{t+1}) \right)

Critic loss 是:

Lcritic=(Qω(st,at)yt)2L_{\mathrm{critic}} = \left( Q_\omega(s_t,a_t)-y_t \right)^2

Actor loss 是:

Lactor=E[Qω(s,μθ(s))]L_{\mathrm{actor}} = - \mathbb{E} \left[ Q_\omega(s,\mu_\theta(s)) \right]

目标网络软更新:

θτθ+(1τ)θ\theta^- \leftarrow \tau\theta + (1-\tau)\theta^-

我最后用四句话记这一章:

参考链接:

22. 第 17 章:SAC 算法

SAC 接在 DDPG 后面看更自然。DDPG 学确定性动作,样本效率高,但探索容易僵住;SAC 保留 off-policy 的经验回放,同时把策略改回随机分布,并在目标里加入熵。SAC 的全称是 Soft Actor-Critic。它可以理解为:

off-policy Actor-Critic+最大熵强化学习+双 Q 网络\text{off-policy Actor-Critic} + \text{最大熵强化学习} + \text{双 Q 网络}

上一章 DDPG 也是 off-policy,也能处理连续动作,但 DDPG 学的是确定性策略,探索依赖外加噪声,训练比较不稳定。SAC 学的是随机策略,并且主动把“策略熵”放进目标函数,让策略既追求高奖励,也保持足够探索。

最大熵强化学习。

熵用来衡量随机变量的不确定性。若随机变量 XX 的分布为 pp,熵定义为:

H(X)=Exp[logp(x)]H(X) = \mathbb{E}_{x\sim p} \left[ -\log p(x) \right]

在强化学习中,策略在状态 ss 下的熵是:

H(π(s))=Eaπ[logπ(as)]H(\pi(\cdot\mid s)) = \mathbb{E}_{a\sim\pi} \left[ -\log\pi(a\mid s) \right]

如果策略很确定,比如几乎总选同一个动作,熵小。

如果策略很随机,很多动作都有概率被选,熵大。

最大熵强化学习的目标是:

π=argmaxπEπ[tr(st,at)+αH(π(st))]\pi^* = \arg\max_\pi \mathbb{E}_\pi \left[ \sum_t r(s_t,a_t) + \alpha H(\pi(\cdot\mid s_t)) \right]

也可以写成:

Eπ[tr(st,at)αlogπ(atst)]\mathbb{E}_\pi \left[ \sum_t r(s_t,a_t) - \alpha \log\pi(a_t\mid s_t) \right]

因为:

H(π(st))=Eatπ[logπ(atst)]H(\pi(\cdot\mid s_t)) = \mathbb{E}_{a_t\sim\pi} \left[ -\log\pi(a_t\mid s_t) \right]

其中 α\alpha 是温度系数,控制熵的重要程度:

Soft Bellman 方程。

普通强化学习里,状态价值大致来自未来奖励。

最大熵强化学习中,价值还要加上熵奖励。

Soft Q 方程:

Q(st,at)=r(st,at)+γEst+1[V(st+1)]Q(s_t,a_t) = r(s_t,a_t) + \gamma \mathbb{E}_{s_{t+1}} \left[ V(s_{t+1}) \right]

Soft 状态价值:

V(st)=Eatπ[Q(st,at)αlogπ(atst)]V(s_t) = \mathbb{E}_{a_t\sim\pi} \left[ Q(s_t,a_t) - \alpha \log\pi(a_t\mid s_t) \right]

这里的:

αlogπ(atst)- \alpha\log\pi(a_t\mid s_t)

就是熵奖励。动作概率越低,logπ-\log\pi 越大;策略越有随机性,整体熵越大。

SAC 和 DDPG 的关系。

SAC 和 DDPG 都是 off-policy Actor-Critic,并且都使用 replay buffer。

但它们有关键区别:

对比DDPGSAC
策略类型确定性策略 μθ(s)\mu_\theta(s)随机策略 πθ(as)\pi_\theta(a\mid s)
探索方式外加动作噪声策略本身有熵
Critic一个 Q 网络和一个 target Q两个 Q 网络和两个 target Q
Actor 目标最大化 Q(s,μ(s))Q(s,\mu(s))最大化 QQ 同时最大化熵
稳定性较敏感通常更稳定

DDPG 的 Actor loss:

LactorDDPG=E[Qω(s,μθ(s))]L_{\mathrm{actor}}^{\mathrm{DDPG}} = - \mathbb{E} \left[ Q_\omega(s,\mu_\theta(s)) \right]

SAC 的 Actor loss:

Lπ(θ)=EsD,aπθ[αlogπθ(as)mini=1,2Qωi(s,a)]L_\pi(\theta) = \mathbb{E}_{s\sim D,a\sim\pi_\theta} \left[ \alpha\log\pi_\theta(a\mid s) - \min_{i=1,2}Q_{\omega_i}(s,a) \right]

最小化这个 loss,等价于最大化:

mini=1,2Qωi(s,a)αlogπθ(as)\min_{i=1,2}Q_{\omega_i}(s,a) - \alpha\log\pi_\theta(a\mid s)

也就是:

高 Q+高熵\text{高 Q} + \text{高熵}

为什么 SAC 用两个 Q 网络。

SAC 使用两个 Q 网络:

Qω1(s,a),Qω2(s,a)Q_{\omega_1}(s,a), \quad Q_{\omega_2}(s,a)

并且取较小的那个:

mini=1,2Qωi(s,a)\min_{i=1,2} Q_{\omega_i}(s,a)

这是 Double Q 的思想,用来缓解 Q 值过高估计。

如果只用一个 Q 网络,Actor 可能会利用 Critic 的高估错误,专门输出被误判为高价值的动作。取两个 Q 的较小值,可以让估计更保守。

SAC 还有两个目标 Q 网络:

Qω1,Qω2Q_{\omega_1^-}, \quad Q_{\omega_2^-}

用来计算稳定的 TD target,并用软更新:

ωiτωi+(1τ)ωi,i=1,2\omega_i^- \leftarrow \tau\omega_i + (1-\tau)\omega_i^-, \quad i=1,2

Critic 怎么更新。

SAC 的 replay buffer 中采样:

(st,at,rt,st+1,dt)D(s_t,a_t,r_t,s_{t+1},d_t) \sim D

在下一个状态,Actor 采样:

at+1πθ(st+1)a_{t+1}\sim\pi_\theta(\cdot\mid s_{t+1})

Soft TD target 是:

yt=rt+γ(1dt)[mini=1,2Qωi(st+1,at+1)αlogπθ(at+1st+1)]y_t = r_t + \gamma(1-d_t) \left[ \min_{i=1,2} Q_{\omega_i^-}(s_{t+1},a_{t+1}) - \alpha \log\pi_\theta(a_{t+1}\mid s_{t+1}) \right]

因为:

H=logπH=-\log\pi

代码里也常写成:

minQ+αH\min Q+\alpha H

两个 Critic 都向同一个 target 学:

LQi(ωi)=E[(Qωi(st,at)yt)2],i=1,2L_{Q_i}(\omega_i) = \mathbb{E} \left[ \left( Q_{\omega_i}(s_t,a_t)-y_t \right)^2 \right], \quad i=1,2

代码中:

td_target = self.calc_target(rewards, next_states, dones)
critic_1_loss = F.mse_loss(self.critic_1(states, actions), td_target.detach())
critic_2_loss = F.mse_loss(self.critic_2(states, actions), td_target.detach())

Actor 怎么更新。

SAC 的 Actor 输出随机策略。连续动作版本中,Actor 输出高斯分布的均值和标准差:

μθ(s),σθ(s)\mu_\theta(s), \quad \sigma_\theta(s)

然后从高斯分布采样动作。

Actor 的目标是:

Lπ(θ)=E[αlogπθ(as)mini=1,2Qωi(s,a)]L_\pi(\theta) = \mathbb{E} \left[ \alpha\log\pi_\theta(a\mid s) - \min_{i=1,2}Q_{\omega_i}(s,a) \right]

因为优化器最小化 loss,所以最小化上式等价于:

代码中:

new_actions, log_prob = self.actor(states)
entropy = -log_prob
q1_value = self.critic_1(states, new_actions)
q2_value = self.critic_2(states, new_actions)
actor_loss = torch.mean(
    -self.log_alpha.exp() * entropy - torch.min(q1_value, q2_value)
)

因为:

entropy=logπentropy=-\log\pi

所以:

αentropyminQ=αlogπminQ-\alpha entropy-\min Q = \alpha\log\pi-\min Q

这正是 SAC 的 Actor loss。

重参数化技巧。

SAC 连续动作策略从高斯分布采样动作。如果直接采样:

aN(μθ(s),σθ(s)2)a\sim\mathcal{N}(\mu_\theta(s),\sigma_\theta(s)^2)

采样操作本身不好对 θ\theta 反向传播。

重参数化技巧把采样写成:

ϵN(0,1)\epsilon\sim\mathcal{N}(0,1) a=μθ(s)+σθ(s)ϵa = \mu_\theta(s) + \sigma_\theta(s)\epsilon

随机性来自 ϵ\epsilon,而 μθ\mu_\thetaσθ\sigma_\theta 仍然是可导的。

代码中:

dist = Normal(mu, std)
normal_sample = dist.rsample()

rsample() 就是支持重参数化的采样。

为了把动作限制到环境范围,代码还使用:

action = torch.tanh(normal_sample)
action = action * self.action_bound

tanh 会把动作压到:

[1,1][-1,1]

再乘上动作上界,得到合法动作。

温度系数 alpha 自动调整。

SAC 中 α\alpha 控制熵项权重。

如果 α\alpha 太大,策略太随机。

如果 α\alpha 太小,策略太贪婪,探索不足。

SAC 可以自动学习 α\alpha。它希望策略熵接近目标熵:

H0\mathcal{H}_0

约束形式是:

E[logπ(atst)]H0\mathbb{E} \left[ -\log\pi(a_t\mid s_t) \right] \ge \mathcal{H}_0

α\alpha 的损失可以写成:

L(α)=E[αlogπ(atst)αH0]L(\alpha) = \mathbb{E} \left[ -\alpha\log\pi(a_t\mid s_t) - \alpha\mathcal{H}_0 \right]

代码中用 log_alpha 而不是直接用 α\alpha,这样可以保证:

α=exp(logα)>0\alpha=\exp(\log\alpha)>0

代码:

self.log_alpha = torch.tensor(np.log(0.01), dtype=torch.float)
self.log_alpha.requires_grad = True

更新:

alpha_loss = torch.mean(
    (entropy - self.target_entropy).detach() * self.log_alpha.exp()
)

直觉:

SAC 的网络结构。

连续动作 SAC 一共有 5 个网络:

网络作用
Actor πθ(as)\pi_\theta(a\mid s)输出随机策略并采样动作
Critic 1 Qω1(s,a)Q_{\omega_1}(s,a)第一个 Q 网络
Critic 2 Qω2(s,a)Q_{\omega_2}(s,a)第二个 Q 网络
Target Critic 1 Qω1(s,a)Q_{\omega_1^-}(s,a)第一个目标 Q 网络
Target Critic 2 Qω2(s,a)Q_{\omega_2^-}(s,a)第二个目标 Q 网络

和 DDPG 不同,SAC 代码中没有 target actor。下一个动作由当前随机 Actor 采样:

aπθ(s)a'\sim\pi_\theta(\cdot\mid s')

目标网络只用于 Q 值估计。

SAC 代码主线。

连续动作策略网络:

mu = self.fc_mu(x)
std = F.softplus(self.fc_std(x))
dist = Normal(mu, std)
normal_sample = dist.rsample()
log_prob = dist.log_prob(normal_sample)
action = torch.tanh(normal_sample)
action = action * self.action_bound
return action, log_prob

这里:

SAC 初始化:

self.actor = PolicyNetContinuous(...)
self.critic_1 = QValueNetContinuous(...)
self.critic_2 = QValueNetContinuous(...)
self.target_critic_1 = QValueNetContinuous(...)
self.target_critic_2 = QValueNetContinuous(...)

计算 TD target:

next_actions, log_prob = self.actor(next_states)
entropy = -log_prob
q1_value = self.target_critic_1(next_states, next_actions)
q2_value = self.target_critic_2(next_states, next_actions)
next_value = torch.min(q1_value, q2_value) + self.log_alpha.exp() * entropy
td_target = rewards + self.gamma * next_value * (1 - dones)

对应:

yt=rt+γ(1dt)[miniQωi(st+1,at+1)+αH(π(st+1))]y_t = r_t + \gamma(1-d_t) \left[ \min_iQ_{\omega_i^-}(s_{t+1},a_{t+1}) + \alpha H(\pi(\cdot\mid s_{t+1})) \right]

更新两个 Critic:

critic_1_loss = F.mse_loss(self.critic_1(states, actions), td_target.detach())
critic_2_loss = F.mse_loss(self.critic_2(states, actions), td_target.detach())

更新 Actor:

new_actions, log_prob = self.actor(states)
entropy = -log_prob
q1_value = self.critic_1(states, new_actions)
q2_value = self.critic_2(states, new_actions)
actor_loss = torch.mean(
    -self.log_alpha.exp() * entropy - torch.min(q1_value, q2_value)
)

更新 α\alpha

alpha_loss = torch.mean(
    (entropy - self.target_entropy).detach() * self.log_alpha.exp()
)

软更新两个目标 Q 网络:

self.soft_update(self.critic_1, self.target_critic_1)
self.soft_update(self.critic_2, self.target_critic_2)

SAC、DDPG、PPO 对比。

算法策略类型on/off-policy核心稳定机制探索方式
PPO随机策略近似 on-policyratio clip策略分布采样
DDPG确定性策略off-policytarget networks + soft update外加噪声
SAC随机策略off-policy最大熵 + 双 Q + target Q熵正则

SAC 可以理解成:

DDPG 的 off-policy 样本效率+PPO 式随机策略探索+最大熵目标\text{DDPG 的 off-policy 样本效率} + \text{PPO 式随机策略探索} + \text{最大熵目标}

它通常比 DDPG 更稳定,因为它不是只追求一个确定动作,而是让策略保持一定随机性,减少陷入局部最优的风险。

小结。

SAC 是一个高效稳定的 off-policy Actor-Critic 算法。

最大熵目标是:

maxπE[tr(st,at)+αH(π(st))]\max_\pi \mathbb{E} \left[ \sum_t r(s_t,a_t) + \alpha H(\pi(\cdot\mid s_t)) \right]

Soft 状态价值是:

V(s)=Eaπ[Q(s,a)αlogπ(as)]V(s) = \mathbb{E}_{a\sim\pi} \left[ Q(s,a) - \alpha\log\pi(a\mid s) \right]

Critic target 是:

yt=rt+γ(1dt)[miniQωi(st+1,at+1)αlogπθ(at+1st+1)]y_t = r_t + \gamma(1-d_t) \left[ \min_iQ_{\omega_i^-}(s_{t+1},a_{t+1}) - \alpha\log\pi_\theta(a_{t+1}\mid s_{t+1}) \right]

Actor loss 是:

Lπ(θ)=E[αlogπθ(as)miniQωi(s,a)]L_\pi(\theta) = \mathbb{E} \left[ \alpha\log\pi_\theta(a\mid s) - \min_iQ_{\omega_i}(s,a) \right]

温度系数 α\alpha 自动调节探索强度。α\alpha 大,策略更随机;α\alpha 小,策略更贪婪。

我最后用四句话记这一章:

参考链接:


Edit page
Share this post on:

Next Post
强化学习学习记录(二):Dyna-Q、DQN 与 Actor-Critic