19. 第 14 章:TRPO 算法
从 TRPO 开始,Actor-Critic 的问题不再只是“能不能更新”,而是“更新步子能不能稳”。TRPO 的全称是 Trust Region Policy Optimization,也就是信任区域策略优化。
它接在 Actor-Critic 后面。Actor-Critic 已经把 REINFORCE 的完整回报 G t G_t G t 换成了优势或 TD 误差,训练更稳定一些。但它本质上还是策略梯度方法:沿着让策略变好的方向更新参数。
普通策略梯度最大的问题是:如果更新步子太大,新策略可能突然变得很差。
TRPO 的核心想法是:
策略可以更新,但每次不能离旧策略太远。只允许在一个“信任区域”里更新。
这个“离得远不远”,用 KL 散度衡量。
从 Actor-Critic 到 TRPO。
Actor-Critic 的 Actor 更新可以写成:
∇ θ J ( θ ) ≈ E [ A π ( s , a ) ∇ θ log π θ ( a ∣ s ) ] \nabla_\theta J(\theta)
\approx
\mathbb{E}
\left[
A^\pi(s,a)
\nabla_\theta
\log\pi_\theta(a\mid s)
\right] ∇ θ J ( θ ) ≈ E [ A π ( s , a ) ∇ θ log π θ ( a ∣ s ) ]
这里:
A π ( s , a ) = Q π ( s , a ) − V π ( s ) A^\pi(s,a)
=
Q^\pi(s,a)-V^\pi(s) A π ( s , a ) = Q π ( s , a ) − V π ( s )
表示动作 a a a 比当前状态的平均水平好多少。
如果优势大于 0 0 0 ,就提高这个动作概率;如果优势小于 0 0 0 ,就降低这个动作概率。
但深度网络策略不是线性函数。即使梯度方向是对的,步长太大也可能把策略改坏。比如旧策略本来是:
π θ k ( ⋅ ∣ s ) = [ 0.45 , 0.55 ] \pi_{\theta_k}(\cdot\mid s)
=
[0.45,0.55] π θ k ( ⋅ ∣ s ) = [ 0.45 , 0.55 ]
一次更新后如果变成:
π θ ′ ( ⋅ ∣ s ) = [ 0.01 , 0.99 ] \pi_{\theta'}(\cdot\mid s)
=
[0.01,0.99] π θ ′ ( ⋅ ∣ s ) = [ 0.01 , 0.99 ]
这就不是“小幅改进”,而是几乎换了一个策略。旧策略采样得到的数据也不再可靠。
TRPO 要解决的就是这个问题。
TRPO 的目标:既要提升策略,又不能离旧策略太远。
设旧策略是:
π θ k \pi_{\theta_k} π θ k
新策略是:
π θ ′ \pi_{\theta'} π θ ′
TRPO 希望新策略的性能不低于旧策略:
J ( θ ′ ) ≥ J ( θ k ) J(\theta') \ge J(\theta_k) J ( θ ′ ) ≥ J ( θ k )
先看策略性能差异。可以把新旧策略的差写成:
J ( θ ′ ) − J ( θ k ) = 1 1 − γ E s ∼ ν π θ ′ E a ∼ π θ ′ ( ⋅ ∣ 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] J ( θ ′ ) − J ( θ k ) = 1 − γ 1 E s ∼ ν π θ ′ E a ∼ π θ ′ ( ⋅ ∣ s ) [ A π θ k ( s , a ) ]
这里:
ν π ( s ) = ( 1 − γ ) ∑ t = 0 ∞ γ t P t π ( s ) \nu^\pi(s)
=
(1-\gamma)
\sum_{t=0}^{\infty}
\gamma^t
P_t^\pi(s) ν π ( s ) = ( 1 − γ ) t = 0 ∑ ∞ γ t P t π ( s )
是折扣状态访问分布。
这个式子的意思是:
如果新策略在自己会访问到的状态上,选出来的动作相对旧策略有正优势,那么新策略就比旧策略好。
但这个式子很难直接优化,因为它里面有:
s ∼ ν π θ ′ s\sim\nu^{\pi_{\theta'}} s ∼ ν π θ ′
也就是说,我们要评估一个还没训练好的新策略,却需要先知道它会访问哪些状态。
替代目标:先用旧策略的数据估计新策略。
TRPO 做了一个近似:如果新策略和旧策略足够接近,那么它们访问状态的分布也差不多。
于是把:
ν π θ ′ \nu^{\pi_{\theta'}} ν π θ ′
近似成:
ν π θ k \nu^{\pi_{\theta_k}} ν π θ k
然后定义替代目标:
L θ k ( θ ′ ) = J ( θ k ) + 1 1 − γ E s ∼ ν π θ k E a ∼ π θ ′ ( ⋅ ∣ 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] L θ k ( θ ′ ) = J ( θ k ) + 1 − γ 1 E s ∼ ν π θ k E a ∼ π θ ′ ( ⋅ ∣ s ) [ A π θ k ( s , a ) ]
但动作 a a a 仍然来自新策略 π θ ′ \pi_{\theta'} π θ ′ 。为了能用旧策略采样到的数据,TRPO 再使用重要性采样:
E a ∼ π θ ′ ( ⋅ ∣ s ) [ A ( s , a ) ] = E a ∼ π θ k ( ⋅ ∣ s ) [ π θ ′ ( a ∣ s ) π θ k ( a ∣ s ) 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] E a ∼ π θ ′ ( ⋅ ∣ s ) [ A ( s , a ) ] = E a ∼ π θ k ( ⋅ ∣ s ) [ π θ k ( a ∣ s ) π θ ′ ( a ∣ s ) A ( s , a ) ]
于是实际优化的核心目标变成:
E s , a ∼ π θ k [ π θ ′ ( a ∣ s ) π θ k ( a ∣ s ) 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] E s , a ∼ π θ k [ π θ k ( a ∣ s ) π θ ′ ( a ∣ s ) A π θ k ( s , a ) ]
这里的比值:
ρ ( θ ′ ) = π θ ′ ( a ∣ s ) π θ k ( a ∣ s ) \rho(\theta')
=
\frac{\pi_{\theta'}(a\mid s)}
{\pi_{\theta_k}(a\mid s)} ρ ( θ ′ ) = π θ k ( a ∣ s ) π θ ′ ( a ∣ s )
叫重要性采样比率。
它衡量的是:
对同一个动作 a a a ,新策略给它的概率是旧策略的多少倍。
KL 散度和信任区域。
如果新旧策略差得太远,上面的近似就不可靠。所以 TRPO 加了一个约束:
E s ∼ ν π θ k [ D K L ( π θ 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 E s ∼ ν π θ k [ D KL ( π θ k ( ⋅ ∣ s ) ∥ π θ ′ ( ⋅ ∣ s ) ) ] ≤ δ
KL 散度衡量两个概率分布有多不一样。离散动作下:
D K L ( p ∥ q ) = ∑ i p i log p i q i D_{\mathrm{KL}}(p\|q)
=
\sum_i
p_i
\log\frac{p_i}{q_i} D KL ( p ∥ q ) = i ∑ p i log q i p i
在 TRPO 里:
p p p 是旧策略分布。
q q q 是新策略分布。
δ \delta δ 是最大允许变化幅度。
所以 TRPO 的最终优化问题是:
max θ ′ E s , a ∼ π θ k [ π θ ′ ( a ∣ s ) π θ k ( a ∣ s ) 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] θ ′ max E s , a ∼ π θ k [ π θ k ( a ∣ s ) π θ ′ ( a ∣ s ) A π θ k ( s , a ) ]
约束为:
E s ∼ ν π θ k [ D K L ( π θ 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 E s ∼ ν π θ k [ D KL ( π θ k ( ⋅ ∣ s ) ∥ π θ ′ ( ⋅ ∣ s ) ) ] ≤ δ
直观理解:
新策略要让优势加权目标变大,但不能和旧策略差太远。
近似求解:一阶目标,二阶约束。
TRPO 的原始约束优化不好直接解,所以做泰勒近似。
令:
Δ θ = θ ′ − θ k \Delta\theta
=
\theta'-\theta_k Δ θ = θ ′ − θ k
对目标函数做一阶近似:
L θ k ( θ ′ ) ≈ g ⊤ Δ θ L_{\theta_k}(\theta')
\approx
g^\top\Delta\theta L θ k ( θ ′ ) ≈ g ⊤ Δ θ
其中:
g = ∇ θ L θ k ( θ ) g
=
\nabla_\theta L_{\theta_k}(\theta) g = ∇ θ L θ k ( θ )
是替代目标的梯度。
对 KL 约束做二阶近似:
D K L ≈ 1 2 Δ θ ⊤ H Δ θ D_{\mathrm{KL}}
\approx
\frac{1}{2}
\Delta\theta^\top
H
\Delta\theta D KL ≈ 2 1 Δ θ ⊤ H Δ θ
其中 H H H 是 KL 散度对策略参数的黑塞矩阵。
于是问题变成:
max Δ θ g ⊤ Δ θ \max_{\Delta\theta}
g^\top\Delta\theta Δ θ max g ⊤ Δ θ
约束为:
1 2 Δ θ ⊤ H Δ θ ≤ δ \frac{1}{2}
\Delta\theta^\top
H
\Delta\theta
\le
\delta 2 1 Δ θ ⊤ H Δ θ ≤ δ
这个形式有解析解:
Δ θ = 2 δ g ⊤ H − 1 g H − 1 g \Delta\theta
=
\sqrt{
\frac{2\delta}
{g^\top H^{-1}g}
}
H^{-1}g Δ θ = g ⊤ H − 1 g 2 δ H − 1 g
因此:
θ k + 1 = θ k + 2 δ g ⊤ H − 1 g H − 1 g \theta_{k+1}
=
\theta_k
+
\sqrt{
\frac{2\delta}
{g^\top H^{-1}g}
}
H^{-1}g θ k + 1 = θ k + g ⊤ H − 1 g 2 δ H − 1 g
这个式子是 TRPO 的核心更新方向。
可以把它理解成:
g g g 告诉我们目标函数希望往哪个方向变好。
H H H 告诉我们这个方向会让策略分布变化多大。
δ \delta δ 限制最大允许变化。
H − 1 g H^{-1}g H − 1 g 是在 KL 几何下修正后的更新方向。
为什么需要共轭梯度。
上面的公式里有:
H − 1 g H^{-1}g H − 1 g
但神经网络参数很多,H H H 是一个巨大矩阵,直接求逆几乎不可行。
TRPO 不直接求:
H − 1 H^{-1} H − 1
而是把问题改成解线性方程:
H x = g Hx=g H x = g
解出来:
x = H − 1 g x=H^{-1}g x = H − 1 g
这个 x x x 就是更新方向。
共轭梯度法的作用就是:在不显式构造 H H H 的情况下,求近似的 x x x 。
它只需要能算:
H p Hp H p
也就是黑塞矩阵和某个向量 p p p 的乘积。
代码中的函数:
def hessian_matrix_vector_product ( self , states , old_action_dists , vector ) :
就是计算:
H v Hv H v
具体做法是:
计算旧策略和新策略之间的平均 KL。
对 KL 求一次梯度。
把这个梯度和向量 vector 点乘。
再求一次梯度。
对应思想是:
H v = ∇ θ [ ( ∇ θ D K L ) ⊤ v ] Hv
=
\nabla_\theta
\left[
\left(
\nabla_\theta D_{\mathrm{KL}}
\right)^\top
v
\right] H v = ∇ θ [ ( ∇ θ D KL ) ⊤ v ]
这叫 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
这里要解的是:
H x = g Hx=g H x = g
变量含义:
grad:就是 g g g 。
x:当前求出来的近似解。
r:残差,表示当前还差多少没解好。
p:当前搜索方向。
Hp:黑塞矩阵乘搜索方向。
循环结束后返回的 x 就近似等于:
H − 1 g H^{-1}g H − 1 g
为什么还要线性搜索。
TRPO 前面做了泰勒近似。既然是近似,就可能出现两个问题:
新策略虽然按公式更新了,但真实目标没有变好。
新策略的真实 KL 超过了约束 δ \delta δ 。
所以 TRPO 还要做线性搜索。
更新方向先算出来:
x = H − 1 g x=H^{-1}g x = H − 1 g
最大步长系数是:
β = 2 δ x ⊤ H x \beta
=
\sqrt{
\frac{2\delta}{x^\top Hx}
} β = x ⊤ H x 2 δ
候选更新为:
θ k + 1 = θ k + β x \theta_{k+1}
=
\theta_k
+
\beta x θ k + 1 = θ k + β x
如果这个候选更新不满足条件,就缩小步长:
θ k + 1 = θ k + α i β x \theta_{k+1}
=
\theta_k
+
\alpha^i
\beta x θ k + 1 = θ k + α i β x
其中:
0 < α < 1 0<\alpha<1 0 < α < 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 ( θ n e w ) > L ( θ o l d ) L(\theta_{new}) > L(\theta_{old}) L ( θ n e w ) > L ( θ o l d )
并且:
D K L ( π o l d ∥ π n e w ) < δ D_{\mathrm{KL}}(\pi_{old}\|\pi_{new})
<
\delta D KL ( π o l d ∥ π n e w ) < δ
如果找不到合适的新参数,就返回旧参数。
广义优势估计 GAE。
TRPO 需要优势函数:
A ( s t , a t ) A(s_t,a_t) A ( s t , a t )
本章使用 GAE,也就是 Generalized Advantage Estimation。
先定义 TD 误差:
δ t = r t + γ V ( s t + 1 ) − V ( s t ) \delta_t
=
r_t
+
\gamma V(s_{t+1})
-
V(s_t) δ t = r t + γV ( s t + 1 ) − V ( s t )
一步优势估计:
A t ( 1 ) = δ t A_t^{(1)}
=
\delta_t A t ( 1 ) = δ t
两步优势估计:
A t ( 2 ) = δ t + γ δ t + 1 A_t^{(2)}
=
\delta_t
+
\gamma\delta_{t+1} A t ( 2 ) = δ t + γ δ t + 1
三步优势估计:
A t ( 3 ) = δ t + γ δ t + 1 + γ 2 δ t + 2 A_t^{(3)}
=
\delta_t
+
\gamma\delta_{t+1}
+
\gamma^2\delta_{t+2} A t ( 3 ) = δ t + γ δ t + 1 + γ 2 δ t + 2
GAE 把不同步数的优势估计加权平均:
A t G A E = ∑ l = 0 ∞ ( γ λ ) l δ t + l A_t^{\mathrm{GAE}}
=
\sum_{l=0}^{\infty}
(\gamma\lambda)^l
\delta_{t+l} A t GAE = l = 0 ∑ ∞ ( γλ ) l δ t + l
其中:
λ ∈ [ 0 , 1 ] \lambda\in[0,1] λ ∈ [ 0 , 1 ]
当:
λ = 0 \lambda=0 λ = 0
只看一步 TD:
A t G A E = δ t A_t^{\mathrm{GAE}}
=
\delta_t A t GAE = δ t
方差小,但偏差可能大。
当:
λ = 1 \lambda=1 λ = 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 )
为什么从后往前算?因为:
A t = δ t + γ λ A t + 1 A_t
=
\delta_t
+
\gamma\lambda A_{t+1} A t = δ t + γλ A t + 1
后一个优势 A t + 1 A_{t+1} A t + 1 会被当前 A t A_t A 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 目标:
y t = r t + γ ( 1 − d t ) V ω ( s t + 1 ) y_t
=
r_t
+
\gamma(1-d_t)V_\omega(s_{t+1}) y t = r t + γ ( 1 − d t ) V ω ( s t + 1 )
代码:
td_target = rewards + self . gamma * self . critic ( next_states ) * ( 1 - dones)
再算 TD 误差:
δ t = y t − V ω ( s t ) \delta_t
=
y_t - V_\omega(s_t) δ t = y t − V ω ( 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 π θ ′ ( a ∣ s ) \log\pi_{\theta'}(a\mid s) log π θ ′ ( a ∣ s )
old_log_probs 是旧策略下实际动作的 log 概率:
log π θ k ( a ∣ s ) \log\pi_{\theta_k}(a\mid s) log π θ k ( a ∣ s )
所以:
ratio = torch . exp ( log_probs - old_log_probs )
对应:
exp ( log π θ ′ ( a ∣ s ) − log π θ k ( a ∣ s ) ) = π θ ′ ( a ∣ s ) π θ k ( a ∣ s ) \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)} exp ( log π θ ′ ( a ∣ s ) − log π θ k ( a ∣ s ) ) = π θ k ( a ∣ s ) π θ ′ ( a ∣ s )
最终:
torch . mean ( ratio * advantage )
对应替代目标:
E [ π θ ′ ( a ∣ s ) π θ k ( a ∣ s ) A ( s , a ) ] \mathbb{E}
\left[
\frac{\pi_{\theta'}(a\mid s)}
{\pi_{\theta_k}(a\mid s)}
A(s,a)
\right] E [ π θ k ( a ∣ s ) π θ ′ ( a ∣ s ) A ( s , a ) ]
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 ())
逐步对应公式:
算替代目标:
L θ k ( θ ′ ) L_{\theta_k}(\theta') L θ k ( θ ′ )
对 Actor 参数求梯度:
g = ∇ θ L θ k ( θ ) g
=
\nabla_\theta L_{\theta_k}(\theta) g = ∇ θ L θ k ( θ )
用共轭梯度求:
x = H − 1 g x
=
H^{-1}g x = H − 1 g
计算最大可行步长:
β = 2 δ x ⊤ H x \beta
=
\sqrt{
\frac{2\delta}{x^\top Hx}
} β = x ⊤ H x 2 δ
线性搜索找满足 KL 约束且目标变好的参数。
用新参数替换 Actor 参数。
离散动作和连续动作的区别。
CartPole 是离散动作,所以 Actor 输出动作概率:
π θ ( a ∣ s ) \pi_\theta(a\mid s) π θ ( a ∣ s )
代码用:
torch . distributions . Categorical ( probs )
Pendulum 是连续动作,所以 Actor 输出高斯分布的均值和标准差:
μ θ ( s ) , σ θ ( s ) \mu_\theta(s),\quad \sigma_\theta(s) μ θ ( s ) , σ θ ( s )
动作从高斯分布采样:
a ∼ N ( μ θ ( s ) , σ θ ( s ) 2 ) a\sim\mathcal{N}(\mu_\theta(s),\sigma_\theta(s)^2) a ∼ N ( μ θ ( s ) , σ θ ( 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
其中:
tanh 把均值限制在一定范围。
softplus 保证标准差为正。
TRPO 和 PPO 的关系。
TRPO 的约束是:
D K L ( π o l d ∥ π n e w ) ≤ δ D_{\mathrm{KL}}(\pi_{old}\|\pi_{new})
\le
\delta D KL ( π o l d ∥ π n e w ) ≤ δ
它很稳定,但实现复杂,因为要用:
KL 二阶近似
Hessian-vector product
共轭梯度
线性搜索
PPO 后面会把这个思想简化。PPO 不再显式求解复杂约束,而是用裁剪目标限制策略变化。
所以可以这样理解:
TRPO:严格限制新旧策略的 KL 距离。
PPO:用更简单的 clipping 近似限制策略变化。
PPO 是 TRPO 思想的工程化简化版本。
TRPO 的四个关键卡点。
这一节最后把 TRPO 最容易混的四个点放在一起看:重要性采样、泰勒近似和 Hessian、线性搜索、GAE。
重要性采样解决的是:
数据来自旧策略,但我们想估计新策略的目标。 \text{数据来自旧策略,但我们想估计新策略的目标。} 数据来自旧策略,但我们想估计新策略的目标。
核心公式是:
E x ∼ q [ f ( x ) ] = E x ∼ p [ 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] E x ∼ q [ f ( x )] = E x ∼ p [ p ( x ) q ( x ) f ( x ) ]
放到 TRPO 里,旧策略是:
π θ k \pi_{\theta_k} π θ k
新策略是:
π θ \pi_\theta π θ
所以重要性采样比率是:
r ( θ ) = π θ ( a ∣ s ) π θ k ( a ∣ s ) r(\theta)
=
\frac{\pi_\theta(a\mid s)}
{\pi_{\theta_k}(a\mid s)} r ( θ ) = π θ k ( a ∣ s ) π θ ( a ∣ s )
这个比率的含义是:同一个动作 a a a ,新策略给它的概率是旧策略的多少倍。
泰勒近似解决的是:TRPO 原来的带约束优化问题太难直接解,所以在当前参数附近做局部近似。
目标函数用一阶近似:
L ( θ k + Δ θ ) ≈ L ( θ k ) + g ⊤ Δ θ L(\theta_k+\Delta\theta)
\approx
L(\theta_k)
+
g^\top\Delta\theta L ( θ k + Δ θ ) ≈ L ( θ k ) + g ⊤ Δ θ
其中:
g = ∇ θ L ( θ ) g=\nabla_\theta L(\theta) g = ∇ θ L ( θ )
表示目标函数上升最快的方向。
KL 约束用二阶近似:
D K L ≈ 1 2 Δ θ ⊤ H Δ θ D_{\mathrm{KL}}
\approx
\frac{1}{2}
\Delta\theta^\top H\Delta\theta D KL ≈ 2 1 Δ θ ⊤ H Δ θ
这里的 H H H 是 KL 散度对策略参数的 Hessian 矩阵。它衡量的是:参数往某个方向动一点,会让策略分布变化多大。
所以 TRPO 的近似问题是:
max Δ θ g ⊤ Δ θ \max_{\Delta\theta}
g^\top\Delta\theta Δ θ max g ⊤ Δ θ
约束为:
1 2 Δ θ ⊤ H Δ θ ≤ δ \frac{1}{2}
\Delta\theta^\top H\Delta\theta
\le
\delta 2 1 Δ θ ⊤ H Δ θ ≤ δ
它的解是:
Δ θ = 2 δ g ⊤ H − 1 g H − 1 g \Delta\theta
=
\sqrt{
\frac{2\delta}
{g^\top H^{-1}g}
}
H^{-1}g Δ θ = g ⊤ H − 1 g 2 δ H − 1 g
但神经网络参数很多,不能直接求 H − 1 H^{-1} H − 1 ,所以用共轭梯度解:
H x = g Hx=g H x = g
从而得到:
x = H − 1 g x=H^{-1}g x = H − 1 g
线性搜索解决的是:上面的更新方向来自近似,直接走完整步可能不安全。所以沿着同一个方向不断缩短步长:
θ n e w = θ o l d + α i Δ θ \theta_{\mathrm{new}}
=
\theta_{\mathrm{old}}
+
\alpha^i\Delta\theta θ new = θ old + α i Δ θ
直到同时满足:
L ( θ n e w ) > L ( θ o l d ) L(\theta_{\mathrm{new}})
>
L(\theta_{\mathrm{old}}) L ( θ new ) > L ( θ old )
和:
D K L ( π o l d ∥ π n e w ) < δ D_{\mathrm{KL}}(\pi_{\mathrm{old}}\|\pi_{\mathrm{new}})
<
\delta D KL ( π old ∥ π new ) < δ
所以线性搜索不是重新找方向,而是确认这一步走多远安全。
GAE 解决的是:TRPO 需要优势函数 A t A_t A t ,但真实优势不知道。
TD 误差是:
δ t = r t + γ V ( s t + 1 ) − V ( s t ) \delta_t
=
r_t+\gamma V(s_{t+1})-V(s_t) δ t = r t + γV ( s t + 1 ) − V ( s t )
GAE 把后续多个 TD 误差加权累积:
A t G A E = ∑ l = 0 ∞ ( γ λ ) l δ t + l A_t^{\mathrm{GAE}}
=
\sum_{l=0}^{\infty}
(\gamma\lambda)^l
\delta_{t+l} A t GAE = l = 0 ∑ ∞ ( γλ ) l δ t + l
递推形式是:
A t = δ t + γ λ A t + 1 A_t
=
\delta_t+\gamma\lambda A_{t+1} A t = δ t + γλ A t + 1
所以代码要从后往前算。λ \lambda λ 越小越像一步 TD,方差小但偏差可能大;λ \lambda λ 越大越像 MC,偏差小但方差更大。
最后把四个点串起来:
重要性采样:用旧策略数据估计新策略目标。
Hessian 和二阶近似:衡量策略参数变化会让策略分布变化多大。
线性搜索:沿同一方向缩短步长,保证目标提升且 KL 不超标。
GAE:用多步 TD 误差给动作估计更稳定的优势。
小结。
TRPO 解决的问题是:
策略梯度更新步子太大,策略可能突然变坏。 \text{策略梯度更新步子太大,策略可能突然变坏。} 策略梯度更新步子太大,策略可能突然变坏。
它的核心约束是:
E [ D K L ( π o l d ∥ π n e w ) ] ≤ δ \mathbb{E}
\left[
D_{\mathrm{KL}}
(\pi_{old}\|\pi_{new})
\right]
\le
\delta E [ D KL ( π o l d ∥ π n e w ) ] ≤ δ
它的核心优化目标是:
max θ ′ E [ π θ ′ ( a ∣ s ) π θ k ( a ∣ s ) 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] θ ′ max E [ π θ k ( a ∣ s ) π θ ′ ( a ∣ s ) A π θ k ( s , a ) ]
它的近似解是:
θ k + 1 = θ k + 2 δ g ⊤ H − 1 g H − 1 g \theta_{k+1}
=
\theta_k
+
\sqrt{
\frac{2\delta}
{g^\top H^{-1}g}
}
H^{-1}g θ k + 1 = θ k + g ⊤ H − 1 g 2 δ H − 1 g
实现时不直接求 H − 1 H^{-1} H − 1 ,而是用共轭梯度解:
H x = g Hx=g H x = g
再用线性搜索保证:
L ( θ n e w ) > L ( θ o l d ) L(\theta_{new})>L(\theta_{old}) L ( θ n e w ) > L ( θ o l d )
并且:
D K L ( π o l d ∥ π n e w ) < δ D_{\mathrm{KL}}(\pi_{old}\|\pi_{new})<\delta D KL ( π o l d ∥ π n e w ) < δ
我最后用三句话记这一章:
TRPO 是 Actor-Critic 框架下更稳定的策略优化方法。
它用 KL 散度限制新旧策略距离,避免策略一步更新过大。
它用共轭梯度和线性搜索近似求解带 KL 约束的策略优化问题。
参考链接:
20. 第 15 章:PPO 算法
PPO 接在 TRPO 后面看最顺。它保留“新策略不要离旧策略太远”的想法,但把 TRPO 复杂的二阶约束换成了更容易写代码的目标函数。PPO 的全称是 Proximal Policy Optimization,近端策略优化。
TRPO 的思想很好:限制新旧策略距离,让策略不要一次更新太猛。但 TRPO 实现复杂,需要:
KL 约束
Hessian-vector product
共轭梯度
线性搜索
PPO 继承 TRPO 的核心思想:
新策略不能离旧策略太远 \text{新策略不能离旧策略太远} 新策略不能离旧策略太远
但它用更简单的目标函数来实现这个限制。
PPO 和 TRPO 的关系。
TRPO 的优化目标是:
max θ E s , a ∼ π θ k [ π θ ( a ∣ s ) π θ k ( a ∣ s ) 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] θ max E s , a ∼ π θ k [ π θ k ( a ∣ s ) π θ ( a ∣ s ) A π θ k ( s , a ) ]
约束为:
E s ∼ ν π θ k [ D K L ( π θ 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 E s ∼ ν π θ k [ D KL ( π θ k ( ⋅ ∣ s ) ∥ π θ ( ⋅ ∣ s ) ) ] ≤ δ
这里的核心仍然是重要性采样比率:
r t ( θ ) = π θ ( a t ∣ s t ) π θ k ( a t ∣ s t ) r_t(\theta)
=
\frac{\pi_\theta(a_t\mid s_t)}
{\pi_{\theta_k}(a_t\mid s_t)} r t ( θ ) = π θ k ( a t ∣ s t ) π θ ( a t ∣ s t )
它表示:新策略对当前动作的概率,相比旧策略放大或缩小了多少。
如果:
r t ( θ ) > 1 r_t(\theta)>1 r t ( θ ) > 1
说明新策略更倾向于这个动作。
如果:
r t ( θ ) < 1 r_t(\theta)<1 r t ( θ ) < 1
说明新策略更不倾向于这个动作。
TRPO 通过 KL 约束控制 r t ( θ ) r_t(\theta) r t ( θ ) 不要变化太极端。PPO 的思路是:不再复杂地解 KL 约束,而是在目标函数里直接惩罚或截断这种变化。
PPO 有两种形式:
PPO-Penalty:把 KL 散度作为惩罚项放进目标函数。
PPO-Clip:直接把重要性采样比率截断在一个范围内。
实践中 PPO-Clip 更常用。
PPO-Penalty。
PPO-Penalty 把 TRPO 的 KL 约束放进目标函数中。
TRPO 是带约束优化:
max θ E [ r t ( θ ) A t ] \max_\theta
\mathbb{E}
\left[
r_t(\theta)A_t
\right] θ max E [ r t ( θ ) A t ]
约束为:
D K L ( π θ k ∥ π θ ) ≤ δ D_{\mathrm{KL}}(\pi_{\theta_k}\|\pi_\theta)
\le
\delta D KL ( π θ k ∥ π θ ) ≤ δ
PPO-Penalty 把它改成无约束优化:
max θ E [ r t ( θ ) A t − β D K L ( π θ 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] θ max E [ r t ( θ ) A t − β D KL ( π θ k ( ⋅ ∣ s ) ∥ π θ ( ⋅ ∣ s ) ) ]
这里:
第一项 r t ( θ ) A t r_t(\theta)A_t r t ( θ ) A t 鼓励策略提升。
第二项 β D K L \beta D_{\mathrm{KL}} β D KL 惩罚新旧策略差太远。
β \beta β 控制惩罚强度。
如果实际 KL 太大,说明新策略变化太猛,就增大 β \beta β :
β k + 1 = 2 β k \beta_{k+1}=2\beta_k β k + 1 = 2 β k
如果实际 KL 太小,说明更新太保守,就减小 β \beta β :
β k + 1 = 1 2 β k \beta_{k+1}=\frac{1}{2}\beta_k β k + 1 = 2 1 β k
更具体地:
d k = D K L ( π θ k ∥ π θ ) d_k
=
D_{\mathrm{KL}}(\pi_{\theta_k}\|\pi_\theta) d k = D KL ( π θ k ∥ π θ )
如果:
d k < δ 1.5 d_k < \frac{\delta}{1.5} d k < 1.5 δ
说明策略变化太小,惩罚太强,因此:
β k + 1 = β k 2 \beta_{k+1}=\frac{\beta_k}{2} β k + 1 = 2 β k
如果:
d k > 1.5 δ d_k>1.5\delta d k > 1.5 δ
说明策略变化太大,惩罚太弱,因此:
β k + 1 = 2 β k \beta_{k+1}=2\beta_k β k + 1 = 2 β k
否则:
β k + 1 = β k \beta_{k+1}=\beta_k β k + 1 = β k
PPO-Penalty 的直觉是:
不硬性禁止新策略走太远,而是在目标函数里给“走太远”加罚分。
从 TRPO 约束到 KL 惩罚。
PPO-Penalty 来自 TRPO。
TRPO 的目标是:
max θ E [ r t ( θ ) A t ] \max_\theta
\mathbb{E}
\left[
r_t(\theta)A_t
\right] θ max E [ r t ( θ ) A t ]
约束为:
D K L ( π o l d ∥ π θ ) ≤ δ D_{\mathrm{KL}}
(\pi_{\mathrm{old}}\|\pi_\theta)
\le
\delta D KL ( π old ∥ π θ ) ≤ δ
其中:
r t ( θ ) = π θ ( a t ∣ s t ) π o l d ( a t ∣ s t ) r_t(\theta)
=
\frac{\pi_\theta(a_t\mid s_t)}
{\pi_{\mathrm{old}}(a_t\mid s_t)} r t ( θ ) = π old ( a t ∣ s t ) π θ ( a t ∣ s t )
TRPO 的意思是:让策略朝优势方向变好,但新策略不能离旧策略太远。
PPO-Penalty 不再把 KL 写成硬约束,而是把 KL 放进目标函数里当作惩罚项:
max θ E [ r t ( θ ) A t − β D K L ( π o l d ( ⋅ ∣ s t ) ∥ π θ ( ⋅ ∣ s t ) ) ] \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] θ max E [ r t ( θ ) A t − β D KL ( π old ( ⋅ ∣ s t ) ∥ π θ ( ⋅ ∣ s t ) ) ]
这里:
r t ( θ ) A t r_t(\theta)A_t r t ( θ ) A t 负责让策略变好。
D K L ( π o l d ∥ π θ ) D_{\mathrm{KL}}(\pi_{\mathrm{old}}\|\pi_\theta) D KL ( π old ∥ π θ ) 衡量新旧策略距离。
β \beta β 控制 KL 惩罚强度。
如果 β \beta β 很大,策略更新会更保守;如果 β \beta β 很小,策略更新会更激进。
PPO-Penalty 会根据实际 KL 动态调整 β \beta β 。令:
d k = D K L ( π θ k ∥ π θ ) d_k
=
D_{\mathrm{KL}}
(\pi_{\theta_k}\|\pi_\theta) d k = D KL ( π θ k ∥ π θ )
如果:
d k > 1.5 δ d_k
>
1.5\delta d k > 1.5 δ
说明新策略离旧策略太远,KL 惩罚不够强,所以:
β k + 1 = 2 β k \beta_{k+1}
=
2\beta_k β k + 1 = 2 β k
如果:
d k < δ 1.5 d_k
<
\frac{\delta}{1.5} d k < 1.5 δ
说明更新太保守,KL 惩罚太强,所以:
β k + 1 = β k 2 \beta_{k+1}
=
\frac{\beta_k}{2} β k + 1 = 2 β k
否则:
β k + 1 = β k \beta_{k+1}
=
\beta_k β k + 1 = β k
所以 PPO-Penalty 的一句话理解是:
把 TRPO 的 KL 硬约束变成 KL 软惩罚,并通过自适应 β \beta β 控制策略更新幅度。
如果写成 PyTorch loss,因为优化器默认最小化,所以要取负号:
L a c t o r = − E [ r t ( θ ) A t − β D K L ( π o l d ∥ π θ ) ] L_{\mathrm{actor}}
=
-
\mathbb{E}
\left[
r_t(\theta)A_t
-
\beta
D_{\mathrm{KL}}
(\pi_{\mathrm{old}}\|\pi_\theta)
\right] L actor = − E [ r t ( θ ) A t − β D KL ( π old ∥ π θ ) ]
伪代码:
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. D K L ( π o l d ∥ π θ ) ≤ δ \max_\theta f(\theta)
\quad
\text{s.t.}
\quad
D_{\mathrm{KL}}(\pi_{\mathrm{old}}\|\pi_\theta)\le\delta θ max f ( θ ) s.t. D KL ( π old ∥ π θ ) ≤ δ
拉格朗日乘数法的思想是把约束变成惩罚项:
max θ [ f ( θ ) − β D K L ( π o l d ∥ π θ ) ] \max_\theta
\left[
f(\theta)-\beta D_{\mathrm{KL}}(\pi_{\mathrm{old}}\|\pi_\theta)
\right] θ max [ f ( θ ) − β D KL ( π old ∥ π θ ) ]
这里 β \beta β 是惩罚强度。β \beta β 越大,新旧策略距离的惩罚越强。
PPO-Penalty 的目标写成:
arg max θ E s ∼ ν π θ k E a ∼ π θ k ( ⋅ ∣ s ) [ π θ ( a ∣ s ) π θ k ( a ∣ s ) A π θ k ( s , a ) − β D K L ( π θ 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] arg θ max E s ∼ ν π θ k E a ∼ π θ k ( ⋅ ∣ s ) [ π θ k ( a ∣ s ) π θ ( a ∣ s ) A π θ k ( s , a ) − β D KL ( π θ k ( ⋅ ∣ s ) ∥ π θ ( ⋅ ∣ s ) ) ]
第一个期望:
E s ∼ ν π θ k \mathbb{E}_{s\sim\nu^{\pi_{\theta_k}}} E s ∼ ν π θ k
表示状态 s s s 来自旧策略 π θ k \pi_{\theta_k} π θ k 的状态访问分布。
第二个期望:
E a ∼ π θ k ( ⋅ ∣ s ) \mathbb{E}_{a\sim\pi_{\theta_k}(\cdot\mid s)} E a ∼ π θ k ( ⋅ ∣ s )
表示动作 a a a 也是旧策略在状态 s s s 下采出来的。
实际代码中,这两个期望就是对旧策略采来的 batch 求平均。
PPO-Clip。
PPO-Clip 是更常用的 PPO 版本。它不显式计算 KL 惩罚,而是直接限制重要性采样比率。
重要性采样比率为:
r t ( θ ) = π θ ( a t ∣ s t ) π θ k ( a t ∣ s t ) r_t(\theta)
=
\frac{\pi_\theta(a_t\mid s_t)}
{\pi_{\theta_k}(a_t\mid s_t)} r t ( θ ) = π θ k ( a t ∣ s t ) π θ ( a t ∣ s t )
普通的策略目标是:
r t ( θ ) A t r_t(\theta)A_t r t ( θ ) A t
如果 A t > 0 A_t>0 A t > 0 ,说明动作好,最大化目标会让 r t ( θ ) r_t(\theta) r t ( θ ) 变大,也就是提高这个动作概率。
如果 A t < 0 A_t<0 A t < 0 ,说明动作差,最大化目标会让 r t ( θ ) r_t(\theta) r t ( θ ) 变小,也就是降低这个动作概率。
但问题是:r t ( θ ) r_t(\theta) r t ( θ ) 可能变得太大或太小,导致策略更新过猛。
PPO-Clip 直接把 r t ( θ ) r_t(\theta) r t ( θ ) 限制在:
[ 1 − ϵ , 1 + ϵ ] [1-\epsilon,1+\epsilon] [ 1 − ϵ , 1 + ϵ ]
截断函数为:
c l i p ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) \mathrm{clip}
(r_t(\theta),1-\epsilon,1+\epsilon) clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ )
PPO-Clip 的目标函数是:
L C L I P ( θ ) = E [ min ( r t ( θ ) A t , c l i p ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ] 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] L CLIP ( θ ) = E [ min ( r t ( θ ) A t , clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ]
这里用 min 的目的是:只取更保守的那个目标,防止策略从极端概率变化中继续获得收益。
情况一:优势为正。
如果:
A t > 0 A_t>0 A t > 0
说明动作好,策略应该提高这个动作概率。
但 PPO 不允许无限提高,只允许:
r t ( θ ) ≤ 1 + ϵ r_t(\theta)\le 1+\epsilon r t ( θ ) ≤ 1 + ϵ
也就是说,如果 ϵ = 0.2 \epsilon=0.2 ϵ = 0.2 ,那么新策略对这个动作的概率最多相对旧策略提高到:
1.2 1.2 1.2
倍左右。
情况二:优势为负。
如果:
A t < 0 A_t<0 A t < 0
说明动作差,策略应该降低这个动作概率。
但 PPO 不允许无限降低,只允许:
r t ( θ ) ≥ 1 − ϵ r_t(\theta)\ge 1-\epsilon r t ( θ ) ≥ 1 − ϵ
如果 ϵ = 0.2 \epsilon=0.2 ϵ = 0.2 ,那么新策略对这个动作的概率最多相对旧策略降低到:
0.8 0.8 0.8
倍左右。
所以 PPO-Clip 的一句话理解是:
好动作可以提高概率,但别提高太多;差动作可以降低概率,但别降低太多。
为什么 PPO-Clip 更常用。
PPO-Clip 不显式使用 KL 惩罚,而是直接限制概率比率:
r t ( θ ) = π θ ( a t ∣ s t ) π o l d ( a t ∣ s t ) r_t(\theta)
=
\frac{\pi_\theta(a_t\mid s_t)}
{\pi_{\mathrm{old}}(a_t\mid s_t)} r t ( θ ) = π old ( a t ∣ s t ) π θ ( a t ∣ s t )
它的目标函数是:
L C L I P ( θ ) = E [ min ( r t ( θ ) A t , c l i p ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ] 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] L CLIP ( θ ) = E [ min ( r t ( θ ) A t , clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ]
PPO-Clip 的直觉是:
如果 A t > 0 A_t>0 A t > 0 ,说明动作好,可以提高动作概率,但最多提高到 1 + ϵ 1+\epsilon 1 + ϵ 附近。
如果 A t < 0 A_t<0 A t < 0 ,说明动作差,可以降低动作概率,但最多降低到 1 − ϵ 1-\epsilon 1 − ϵ 附近。
所以:
ϵ = 0.2 \epsilon=0.2 ϵ = 0.2
时,通常希望概率比率大致被限制在:
[ 0.8 , 1.2 ] [0.8,1.2] [ 0.8 , 1.2 ]
PPO-Penalty 和 PPO-Clip 的区别:
方法 控制策略变化的方式 特点 TRPO KL 硬约束 稳定,但实现复杂 PPO-Penalty KL 软惩罚 比 TRPO 简单,但要调 β \beta β PPO-Clip 概率比率截断 最简单,实践中最常用
为什么截断后梯度会变为 0。
PPO-Clip 的目标是:
L C L I P = min ( r A , c l i p ( r , 1 − ϵ , 1 + ϵ ) A ) L^{\mathrm{CLIP}}
=
\min
\left(
rA,
\mathrm{clip}(r,1-\epsilon,1+\epsilon)A
\right) L CLIP = min ( r A , clip ( r , 1 − ϵ , 1 + ϵ ) A )
其中:
r = π θ ( a ∣ s ) π o l d ( a ∣ s ) r
=
\frac{\pi_\theta(a\mid s)}
{\pi_{\mathrm{old}}(a\mid s)} r = π old ( a ∣ s ) π θ ( a ∣ s )
当:
A > 0 , r > 1 + ϵ A>0,\quad r>1+\epsilon A > 0 , r > 1 + ϵ
有:
c l i p ( r , 1 − ϵ , 1 + ϵ ) = 1 + ϵ \mathrm{clip}(r,1-\epsilon,1+\epsilon)
=
1+\epsilon clip ( r , 1 − ϵ , 1 + ϵ ) = 1 + ϵ
所以:
r A > ( 1 + ϵ ) A rA>(1+\epsilon)A r A > ( 1 + ϵ ) A
PPO 取较小值:
L C L I P = ( 1 + ϵ ) A L^{\mathrm{CLIP}}
=
(1+\epsilon)A L CLIP = ( 1 + ϵ ) A
这个式子已经不含 r r r ,因此:
∂ L C L I P ∂ r = 0 \frac{\partial L^{\mathrm{CLIP}}}{\partial r}
=
0 ∂ r ∂ L CLIP = 0
而 r r r 才是和新策略参数 θ \theta θ 有关的量,所以该区域不会继续推动新策略提高该动作概率。直觉是:好动作可以增加概率,但超过 1 + ϵ 1+\epsilon 1 + ϵ 后不再给额外奖励。
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
其中:
epochs:同一批 on-policy 数据重复训练几轮。
eps:clip 范围 ϵ \epsilon ϵ 。
PPO 的 update 主线。
先把数据转成张量:
states
actions
rewards
next_states
dones
每条数据都是:
( s t , a t , r t , s t + 1 , d t ) (s_t,a_t,r_t,s_{t+1},d_t) ( s t , a t , r t , s t + 1 , d t )
然后算 TD 目标:
y t = r t + γ ( 1 − d t ) V ω ( s t + 1 ) y_t
=
r_t+\gamma(1-d_t)V_\omega(s_{t+1}) y t = r t + γ ( 1 − d t ) V ω ( s t + 1 )
代码:
td_target = rewards + self . gamma * self . critic ( next_states ) * ( 1 - dones)
再算 TD 误差:
δ t = y t − V ω ( s t ) \delta_t
=
y_t - V_\omega(s_t) δ t = y t − V ω ( s t )
代码:
td_delta = td_target - self . critic ( states )
再用 GAE 算优势:
A t G A E = ∑ l = 0 ∞ ( γ λ ) l δ t + l A_t^{\mathrm{GAE}}
=
\sum_{l=0}^{\infty}
(\gamma\lambda)^l\delta_{t+l} A t GAE = l = 0 ∑ ∞ ( γλ ) l δ 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 的处理方式是:用旧策略概率作为基准,通过比率:
r t ( θ ) = π θ ( a t ∣ s t ) π θ k ( a t ∣ s t ) r_t(\theta)
=
\frac{\pi_\theta(a_t\mid s_t)}
{\pi_{\theta_k}(a_t\mid s_t)} r t ( θ ) = π θ k ( a t ∣ s t ) π θ ( a t ∣ s t )
追踪新策略和旧策略的差距。
再用 clip 限制:
r t ( θ ) ∈ [ 1 − ϵ , 1 + ϵ ] r_t(\theta)\in[1-\epsilon,1+\epsilon] r t ( θ ) ∈ [ 1 − ϵ , 1 + ϵ ]
所以 PPO 可以在一定范围内重复使用同一批 on-policy 数据,而不让策略偏离太远。
PPO 的数据属性。
PPO 通常归类为:
online on-policy 算法 \text{online on-policy 算法} online on-policy 算法
它确实会对同一批数据训练多个 epoch,但这不等于 off-policy,也不等于 offline RL。
PPO 的流程是:
用当前策略 π θ k \pi_{\theta_k} π θ k 和环境交互,采集一批数据。
保存旧策略概率:
π θ k ( a t ∣ s t ) \pi_{\theta_k}(a_t\mid s_t) π θ k ( a t ∣ s t )
用这批数据更新几轮。
用 ratio 和 clip 限制新策略别离旧策略太远:
r t ( θ ) = π θ ( a t ∣ s t ) π θ k ( a t ∣ s t ) r_t(\theta)
=
\frac{\pi_\theta(a_t\mid s_t)}
{\pi_{\theta_k}(a_t\mid s_t)} r t ( θ ) = π θ k ( a t ∣ s t ) π θ ( a t ∣ s t )
更新完后丢掉这批数据,用新策略重新采样。
所以 PPO 是近似 on-policy。它允许短期复用最近一批数据,但不长期维护 replay buffer。
对比:
算法类型 数据来源 数据使用方式 PPO 当前策略刚采的数据 用几个 epoch 后丢掉 DQN/SAC/DDPG replay buffer 中的历史数据 可长期反复采样 Offline RL 固定离线数据集 不再和环境交互
因此:
多轮复用最近数据 ≠ off-policy 或 offline \text{多轮复用最近数据}
\neq
\text{off-policy 或 offline} 多轮复用最近数据 = off-policy 或 offline
PPO 仍然需要不断和环境交互采新数据。
episode、rollout 和 epoch。
episode 是强化学习里的“一整局”。
从初始状态开始:
s 0 s_0 s 0
一直交互到终止:
d o n e = T r u e done=True d o n e = T r u e
这一整条轨迹就是一个 episode。
rollout 是按某个策略采样出来的一段或一批轨迹。它可以是完整 episode,也可以只是一个 episode 的片段,还可以是多个 episode 拼成的一批数据。
所以:
episode 是一种完整 rollout \text{episode 是一种完整 rollout} episode 是一种完整 rollout
但:
rollout 不一定是完整 episode \text{rollout 不一定是完整 episode} 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} 采 rollout → 算 advantage → 训练多个 epoch → 丢掉旧 rollout → 再采新 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 π θ ( a t ∣ s t ) \log\pi_\theta(a_t\mid s_t) log π θ ( a t ∣ s t )
比率:
r t ( θ ) = exp ( log π θ ( a t ∣ s t ) − log π θ k ( a t ∣ s t ) ) r_t(\theta)
=
\exp
\left(
\log\pi_\theta(a_t\mid s_t)
-
\log\pi_{\theta_k}(a_t\mid s_t)
\right) r t ( θ ) = exp ( log π θ ( a t ∣ s t ) − log π θ k ( a t ∣ s t ) )
也就是:
r t ( θ ) = π θ ( a t ∣ s t ) π θ k ( a t ∣ s t ) r_t(\theta)
=
\frac{\pi_\theta(a_t\mid s_t)}
{\pi_{\theta_k}(a_t\mid s_t)} r t ( θ ) = π θ k ( a t ∣ s t ) π θ ( a t ∣ s t )
未截断目标:
s u r r 1 = r t ( θ ) A t surr1
=
r_t(\theta)A_t s u rr 1 = r t ( θ ) A t
截断目标:
s u r r 2 = c l i p ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t surr2
=
\mathrm{clip}
\left(
r_t(\theta),1-\epsilon,1+\epsilon
\right)
A_t s u rr 2 = clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t
PPO 目标:
min ( s u r r 1 , s u r r 2 ) \min(surr1,surr2) min ( s u rr 1 , s u rr 2 )
代码里是 loss,所以要取负号:
L a c t o r = − E [ min ( s u r r 1 , s u r r 2 ) ] L_{\mathrm{actor}}
=
-
\mathbb{E}
\left[
\min(surr1,surr2)
\right] L actor = − E [ min ( s u rr 1 , s u rr 2 ) ]
因为 PyTorch 优化器默认是最小化 loss,而 PPO 原目标是最大化策略目标。
Critic loss 和 Actor-Critic 一样:
L c r i t i c = ( V ω ( s t ) − y t ) 2 L_{\mathrm{critic}}
=
\left(
V_\omega(s_t)-y_t
\right)^2 L critic = ( V ω ( s t ) − y t ) 2
代码:
critic_loss = torch . mean (
F. mse_loss ( self . critic ( states ) , td_target. detach ()))
PPO 和 TRPO 的核心区别。
TRPO 是:
max θ E [ r t ( θ ) A t ] \max_\theta
\mathbb{E}
\left[
r_t(\theta)A_t
\right] θ max E [ r t ( θ ) A t ]
约束:
D K L ( π o l d ∥ π n e w ) ≤ δ D_{\mathrm{KL}}(\pi_{old}\|\pi_{new})
\le
\delta D KL ( π o l d ∥ π n e w ) ≤ δ
PPO-Clip 是:
max θ E [ min ( r t ( θ ) A t , c l i p ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ] \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] θ max E [ min ( r t ( θ ) A t , clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ]
TRPO 是显式约束 KL,求解复杂。
PPO 是隐式限制概率比率,实现简单。
所以可以这样记:
TRPO:用 KL 约束限制新旧策略距离。
PPO-Penalty:用 KL 惩罚限制新旧策略距离。
PPO-Clip:用概率比率截断限制新旧策略距离。
离散动作和连续动作 PPO。
离散动作下,Actor 输出动作概率:
π θ ( a ∣ s ) \pi_\theta(a\mid s) π θ ( a ∣ s )
代码使用:
torch . distributions . Categorical ( probs )
连续动作下,Actor 输出高斯分布参数:
μ θ ( s ) , σ θ ( s ) \mu_\theta(s),\quad \sigma_\theta(s) μ θ ( s ) , σ θ ( s )
动作从高斯分布采样:
a ∼ N ( μ θ ( s ) , σ θ ( s ) 2 ) a\sim\mathcal{N}
\left(
\mu_\theta(s),
\sigma_\theta(s)^2
\right) a ∼ N ( μ θ ( s ) , σ θ ( s ) 2 )
代码使用:
torch . distributions . Normal ( mu , sigma )
连续动作版本中:
mu = 2.0 * torch . tanh ( self . fc_mu ( x ))
std = F . softplus ( self . fc_std ( x ))
其中:
tanh 用来限制均值范围。
softplus 用来保证标准差为正。
收束。
PPO 可以用一句话收束:
PPO 是 Actor-Critic 框架下的安全策略更新方法,它不像 TRPO 那样显式求解 KL 约束,而是用 KL 惩罚或概率比率截断,让 Actor 在优势方向上更新但不要一次改太猛。
其中:
Critic 负责估计价值和优势。
Actor 负责更新策略。
PPO-Penalty 用 KL 惩罚控制更新幅度。
PPO-Clip 用 ratio 截断控制更新幅度。
PPO 是 online on-policy,只是短期复用最近 rollout 做多个 epoch。
小结。
PPO 解决的问题和 TRPO 一样:
策略更新不能太猛。 \text{策略更新不能太猛。} 策略更新不能太猛。
TRPO 的方法是显式约束:
D K L ( π o l d ∥ π n e w ) ≤ δ D_{\mathrm{KL}}(\pi_{old}\|\pi_{new})
\le
\delta D KL ( π o l d ∥ π n e w ) ≤ δ
PPO 的方法更简单。PPO-Penalty 把 KL 放进目标函数:
r t ( θ ) A t − β D K L ( π o l d ∥ π n e w ) r_t(\theta)A_t
-
\beta D_{\mathrm{KL}}
(\pi_{old}\|\pi_{new}) r t ( θ ) A t − β D KL ( π o l d ∥ π n e w )
PPO-Clip 直接截断概率比率:
L C L I P ( θ ) = E [ min ( r t ( θ ) A t , c l i p ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ] 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] L CLIP ( θ ) = E [ min ( r t ( θ ) A t , clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) ]
本章最重要的是理解这个比率:
r t ( θ ) = π θ ( a t ∣ s t ) π θ k ( a t ∣ s t ) r_t(\theta)
=
\frac{\pi_\theta(a_t\mid s_t)}
{\pi_{\theta_k}(a_t\mid s_t)} r t ( θ ) = π θ k ( a t ∣ s t ) π θ ( a t ∣ s t )
它表示新策略相对旧策略对当前动作的概率变化。
PPO-Clip 的最终直觉是:
如果动作好,增加它的概率,但最多增加到 1 + ϵ 1+\epsilon 1 + ϵ 附近。
如果动作差,降低它的概率,但最多降低到 1 − ϵ 1-\epsilon 1 − ϵ 附近。
因此 PPO 可以稳定地多轮更新同一批数据,而不让策略偏离太远。
参考链接:
21. 第 16 章:DDPG 算法
到 DDPG 时,动作空间从离散动作变成连续动作。这里不能再像 DQN 那样枚举所有动作取最大值,只能让 Actor 直接输出一个连续动作。DDPG 的全称是 Deep Deterministic Policy Gradient,深度确定性策略梯度。
前面 PPO、TRPO 都是 Actor-Critic 框架下的随机策略算法:
π θ ( a ∣ s ) \pi_\theta(a\mid s) π θ ( a ∣ s )
它们输出的是动作概率分布,再从分布中采样动作。
DDPG 不一样。DDPG 学的是确定性策略:
a = μ θ ( s ) a=\mu_\theta(s) a = μ θ ( s )
也就是说,给定状态 s s s ,Actor 直接输出一个具体动作 a a a 。
DDPG 主要解决两个问题:
DQN 可以 off-policy,但难以处理连续动作空间。
PPO/TRPO 可以处理连续动作,但通常是 on-policy,样本效率较低。
DDPG 的目标是结合两边的优点:
连续动作 + off-policy + Actor-Critic \text{连续动作}
+
\text{off-policy}
+
\text{Actor-Critic} 连续动作 + off-policy + Actor-Critic
为什么连续动作不能直接用 DQN。
DQN 学的是动作价值函数:
Q ( s , a ) Q(s,a) Q ( s , a )
选动作时用:
a ∗ = arg max a Q ( s , a ) a^*
=
\arg\max_a Q(s,a) a ∗ = arg a max Q ( s , a )
如果动作是离散的,比如:
a ∈ { 0 , 1 , 2 , 3 } a\in\{0,1,2,3\} a ∈ { 0 , 1 , 2 , 3 }
那可以把所有动作都算一遍,选最大的。
但如果动作是连续的,比如倒立摆环境中动作是一个连续力矩:
a ∈ [ − 2 , 2 ] a\in[-2,2] a ∈ [ − 2 , 2 ]
那动作有无限多个,不可能枚举所有动作再取最大。
DDPG 的思路是:既然不能枚举动作,那就再训练一个 Actor 网络,直接输出让 Q Q Q 尽量大的动作:
μ θ ( s ) ≈ arg max a Q ω ( s , a ) \mu_\theta(s)
\approx
\arg\max_a Q_\omega(s,a) μ θ ( s ) ≈ arg a max Q ω ( s , a )
所以 DDPG 里:
Critic 学 Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a ) ,负责评价状态动作对。
Actor 学 μ θ ( s ) \mu_\theta(s) μ θ ( s ) ,负责输出连续动作。
确定性策略和随机策略的区别。
随机策略输出一个分布:
π θ ( a ∣ s ) \pi_\theta(a\mid s) π θ ( a ∣ s )
例如:
π θ ( ⋅ ∣ s ) = [ 0.2 , 0.8 ] \pi_\theta(\cdot\mid s)
=
[0.2,0.8] π θ ( ⋅ ∣ s ) = [ 0.2 , 0.8 ]
然后从这个分布中采样动作。
确定性策略直接输出动作:
μ θ ( s ) = a \mu_\theta(s)=a μ θ ( s ) = a
例如在连续动作环境中:
μ θ ( s ) = 0.73 \mu_\theta(s)=0.73 μ θ ( s ) = 0.73
这就是实际要执行的动作。
所以:
策略类型 输出 代表算法 随机策略 动作概率分布 π θ ( a ∣ s ) \pi_\theta(a\mid s) π θ ( a ∣ s ) REINFORCE、A2C、TRPO、PPO 确定性策略 具体动作 μ θ ( s ) \mu_\theta(s) μ θ ( s ) DDPG
确定性策略的好处是适合连续动作。坏处是探索能力弱,因为同一个状态总会输出同一个动作。
所以 DDPG 在执行动作时会加噪声:
a t = μ θ ( s t ) + ϵ t a_t
=
\mu_\theta(s_t)+\epsilon_t a t = μ θ ( s t ) + ϵ t
代码里通常用高斯噪声或 OU 噪声。
确定性策略梯度。
DDPG 的 Actor 目标是让 Critic 评价更高:
J ( θ ) = E s [ Q ω ( s , μ θ ( s ) ) ] J(\theta)
=
\mathbb{E}_{s}
\left[
Q_\omega(s,\mu_\theta(s))
\right] J ( θ ) = E s [ Q ω ( s , μ θ ( s )) ]
这句话很重要。
Actor 的输出是动作:
a = μ θ ( s ) a=\mu_\theta(s) a = μ θ ( s )
Critic 评价这个动作:
Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a )
所以 Actor 想最大化:
Q ω ( s , μ θ ( s ) ) Q_\omega(s,\mu_\theta(s)) Q ω ( s , μ θ ( s ))
对 Actor 参数 θ \theta θ 求梯度,用链式法则:
∇ θ J ( θ ) = E s [ ∇ a Q ω ( 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] ∇ θ J ( θ ) = E s [ ∇ a Q ω ( s , a ) a = μ θ ( s ) ∇ θ μ θ ( s ) ]
这个公式就是确定性策略梯度的核心。
它可以这样理解:
∇ a Q ω ( s , a ) \nabla_a Q_\omega(s,a) ∇ a Q ω ( s , a ) :如果动作 a a a 往某个方向变一点,Q Q Q 会怎么变。
∇ θ μ θ ( s ) \nabla_\theta \mu_\theta(s) ∇ θ μ θ ( s ) :Actor 参数 θ \theta θ 怎么影响动作输出。
两者相乘:参数怎么改,才能让 Actor 输出更高价值的动作。
所以 DDPG 的 Actor 不是直接用:
log π θ ( a ∣ s ) \log\pi_\theta(a\mid s) log π θ ( a ∣ s )
因为它没有概率分布。它直接通过 Critic 的 Q Q Q 值反向传播来更新 Actor。
DDPG 的四个网络。
DDPG 有四个网络:
网络 作用 Actor μ θ ( s ) \mu_\theta(s) μ θ ( s ) 当前策略网络,输出动作 Critic Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a ) 当前价值网络,评价状态动作对 Target Actor μ θ − ( s ) \mu_{\theta^-}(s) μ θ − ( s ) 目标策略网络,计算 TD target Target Critic Q ω − ( s , a ) Q_{\omega^-}(s,a) Q ω − ( s , a ) 目标价值网络,计算 TD target
为什么需要目标网络?原因和 DQN 类似:TD target 里也包含神经网络输出,如果目标本身剧烈变化,训练会不稳定。
DDPG 的目标网络不是像 DQN 那样隔一段时间硬复制,而是使用软更新:
θ − ← τ θ + ( 1 − τ ) θ − \theta^-
\leftarrow
\tau\theta
+
(1-\tau)\theta^- θ − ← τ θ + ( 1 − τ ) θ −
其中:
0 < τ ≪ 1 0<\tau\ll 1 0 < τ ≪ 1
例如:
τ = 0.005 \tau=0.005 τ = 0.005
这表示目标网络每次只朝当前网络移动一点点。
Actor 和 Critic 都有软更新:
θ a c t o r − ← τ θ a c t o r + ( 1 − τ ) θ a c t o r − \theta_{\mathrm{actor}}^-
\leftarrow
\tau\theta_{\mathrm{actor}}
+
(1-\tau)\theta_{\mathrm{actor}}^- θ actor − ← τ θ actor + ( 1 − τ ) θ actor −
ω c r i t i c − ← τ ω c r i t i c + ( 1 − τ ) ω c r i t i c − \omega_{\mathrm{critic}}^-
\leftarrow
\tau\omega_{\mathrm{critic}}
+
(1-\tau)\omega_{\mathrm{critic}}^- ω critic − ← τ ω critic + ( 1 − τ ) ω critic −
为什么需要 target_actor。
DDPG 的 Critic target 是:
y = r + γ Q ω − ( s ′ , μ θ − ( s ′ ) ) y
=
r+\gamma
Q_{\omega^-}
\left(
s',
\mu_{\theta^-}(s')
\right) y = r + γ Q ω − ( s ′ , μ θ − ( s ′ ) )
连续动作下不能像 DQN 那样枚举:
max a ′ Q ( s ′ , a ′ ) \max_{a'}Q(s',a') a ′ max Q ( s ′ , a ′ )
所以需要 Actor 在下一状态 s ′ s' s ′ 给出下一动作:
a ′ = μ θ − ( s ′ ) a'=\mu_{\theta^-}(s') a ′ = μ θ − ( 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)\} D = {( s , a , r , s ′ , d )}
训练时从 replay buffer 里采样 batch:
( s i , a i , r i , s i ′ , d i ) ∼ D (s_i,a_i,r_i,s_i',d_i)
\sim
\mathcal{D} ( s i , a i , r i , s i ′ , d i ) ∼ D
这说明 DDPG 是 off-policy。
它执行环境时用的是带噪声的行为策略:
a t = μ θ ( s t ) + ϵ t a_t
=
\mu_\theta(s_t)+\epsilon_t a t = μ θ ( s t ) + ϵ t
但学习时 Actor 目标是最大化当前确定性策略:
Q ω ( s , μ θ ( s ) ) Q_\omega(s,\mu_\theta(s)) Q ω ( s , μ θ ( s ))
行为策略和学习策略不完全一样,所以它是 off-policy。
这一点和 PPO 不同:
PPO 用刚采样的 on-policy rollout,更新几轮后丢掉。
DDPG 用 replay buffer,可以反复利用历史 transition。
DDPG 的 off-policy 属性。
DDPG 的目标策略是确定性 Actor:
a = μ θ ( s ) a=\mu_\theta(s) a = μ θ ( s )
但采样时为了探索,会执行带噪声的行为策略:
a t = μ θ ( s t ) + ϵ t a_t=\mu_\theta(s_t)+\epsilon_t a t = μ θ ( s t ) + ϵ t
行为策略和目标策略不同:
μ b ≠ μ θ \mu_b\ne\mu_\theta μ b = μ θ
并且 DDPG 使用 replay buffer:
D = { ( s , a , r , s ′ , d ) } \mathcal{D}
=
\{(s,a,r,s',d)\} D = {( s , a , r , s ′ , d )}
训练数据可能来自很多旧版本策略,而学习的是当前 Actor。因此 DDPG 是 off-policy。
Critic 怎么更新。
DDPG 的 Critic 学的是:
Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a )
TD target 使用目标网络:
y t = r t + γ ( 1 − d t ) Q ω − ( s t + 1 , μ θ − ( s t + 1 ) ) y_t
=
r_t
+
\gamma(1-d_t)
Q_{\omega^-}
\left(
s_{t+1},
\mu_{\theta^-}(s_{t+1})
\right) y t = r t + γ ( 1 − d t ) Q ω − ( s t + 1 , μ θ − ( s t + 1 ) )
这里:
μ θ − ( s t + 1 ) \mu_{\theta^-}(s_{t+1}) μ θ − ( s t + 1 ) :目标 Actor 给出下一个状态的动作。
Q ω − ( s t + 1 , μ θ − ( s t + 1 ) ) Q_{\omega^-}(s_{t+1},\mu_{\theta^-}(s_{t+1})) Q ω − ( s t + 1 , μ θ − ( s t + 1 )) :目标 Critic 评价这个动作。
Critic loss 是:
L c r i t i c ( ω ) = E [ ( Q ω ( s t , a t ) − y t ) 2 ] L_{\mathrm{critic}}(\omega)
=
\mathbb{E}
\left[
\left(
Q_\omega(s_t,a_t)-y_t
\right)^2
\right] L critic ( ω ) = E [ ( Q ω ( s t , a t ) − y t ) 2 ]
代码对应:
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 + γ max a Q ( s ′ , a ) r+\gamma\max_a Q(s',a) r + γ a max Q ( s ′ , a )
而 DDPG 的 target 是:
r + γ Q ω − ( s ′ , μ θ − ( s ′ ) ) r+\gamma Q_{\omega^-}(s',\mu_{\theta^-}(s')) r + γ Q ω − ( s ′ , μ θ − ( s ′ ))
因为连续动作下不能枚举 max a \max_a max a ,所以用目标 Actor 给出动作。
Actor 怎么更新。
Actor 的目标是最大化 Critic 给自己的动作打出的分数:
max θ E [ Q ω ( s , μ θ ( s ) ) ] \max_\theta
\mathbb{E}
\left[
Q_\omega(s,\mu_\theta(s))
\right] θ max E [ Q ω ( s , μ θ ( s )) ]
但 PyTorch 优化器默认最小化 loss,所以写成:
L a c t o r ( θ ) = − E [ Q ω ( s , μ θ ( s ) ) ] L_{\mathrm{actor}}(\theta)
=
-
\mathbb{E}
\left[
Q_\omega(s,\mu_\theta(s))
\right] L actor ( θ ) = − E [ Q ω ( s , μ θ ( s )) ]
代码:
actor_loss = - torch . mean (
self . critic ( states , self . actor ( states ))
)
这句代码的含义是:
Actor 输入状态,输出动作:
μ θ ( s ) \mu_\theta(s) μ θ ( s )
Critic 评价这个动作:
Q ω ( s , μ θ ( s ) ) Q_\omega(s,\mu_\theta(s)) Q ω ( s , μ θ ( s ))
加负号作为 loss:
− Q ω ( s , μ θ ( s ) ) -Q_\omega(s,\mu_\theta(s)) − Q ω ( s , μ θ ( s ))
最小化这个 loss,等价于最大化 Q Q Q 。
所以 DDPG 的 Actor 更新非常直接:
让 Actor 输出能被 Critic 打高分的动作。
探索噪声。
由于 DDPG 是确定性策略,如果不加噪声,同一个状态总会输出同一个动作:
a = μ θ ( s ) a=\mu_\theta(s) a = μ θ ( s )
这会导致探索不足。
所以执行动作时加入噪声:
a t = μ θ ( s t ) + ϵ t a_t
=
\mu_\theta(s_t)+\epsilon_t a t = μ θ ( s t ) + ϵ t
代码里使用的是高斯噪声:
action = self . actor ( state ). item ()
action = action + self . sigma * np . random . randn ( self .action_dim )
其中:
ϵ t ∼ N ( 0 , σ 2 ) \epsilon_t
\sim
\mathcal{N}(0,\sigma^2) ϵ t ∼ N ( 0 , σ 2 )
原始 DDPG 论文中常用 OU 噪声:
d N t = θ ( μ − N t ) d t + σ d W t dN_t
=
\theta(\mu-N_t)dt+\sigma dW_t d N t = θ ( μ − N t ) d t + σ d W 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
这里:
输入是状态 s s s 。
输出是连续动作 a a a 。
tanh 的输出范围是 [ − 1 , 1 ] [-1,1] [ − 1 , 1 ] 。
乘上 action_bound 后,把动作缩放到环境允许范围。
例如倒立摆动作范围是:
[ − 2 , 2 ] [-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) ( s , a )
输出一个标量:
Q ω ( s , a ) Q_\omega(s,a) Q ω ( 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{自适应学习率} 动量 + 自适应学习率
普通梯度下降:
θ ← θ − α g t \theta
\leftarrow
\theta-\alpha g_t θ ← θ − α g t
Adam 维护一阶动量:
m t = β 1 m t − 1 + ( 1 − β 1 ) g t m_t
=
\beta_1m_{t-1}
+
(1-\beta_1)g_t m t = β 1 m t − 1 + ( 1 − β 1 ) g t
它表示梯度方向的滑动平均。
Adam 还维护二阶动量:
v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2 v_t
=
\beta_2v_{t-1}
+
(1-\beta_2)g_t^2 v t = β 2 v t − 1 + ( 1 − β 2 ) g t 2
它表示梯度平方的滑动平均,用来给不同参数自适应调整步长。
偏差修正:
m ^ t = m t 1 − β 1 t \hat m_t
=
\frac{m_t}{1-\beta_1^t} m ^ t = 1 − β 1 t m t
v ^ t = v t 1 − β 2 t \hat v_t
=
\frac{v_t}{1-\beta_2^t} v ^ t = 1 − β 2 t v t
最终更新:
θ t = θ t − 1 − α m ^ t v ^ t + ϵ \theta_t
=
\theta_{t-1}
-
\alpha
\frac{\hat m_t}
{\sqrt{\hat v_t}+\epsilon} θ t = θ t − 1 − α v ^ t + ϵ m ^ t
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
流程:
状态转成 tensor。
Actor 输出确定性动作:
μ θ ( s ) \mu_\theta(s) μ θ ( s )
加高斯噪声用于探索:
a = μ θ ( s ) + ϵ a=\mu_\theta(s)+\epsilon a = μ θ ( s ) + ϵ
返回动作给环境。
这里和 PPO 的区别很明显:
PPO 的 Actor 输出动作分布,再采样。
DDPG 的 Actor 直接输出动作,再人为加噪声探索。
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^- θ − ← τ θ + ( 1 − τ ) θ −
如果:
τ = 0.005 \tau=0.005 τ = 0.005
目标网络每次只吸收当前网络 0.5 % 0.5\% 0.5% 的新参数,所以变化很慢,能稳定 TD target。
两个 target 网络的软更新。
DDPG 中 target_actor 和 target_critic 都通常在每次 update 后软更新:
θ − ← τ θ + ( 1 − τ ) θ − \theta^-
\leftarrow
\tau\theta+(1-\tau)\theta^- θ − ← τ θ + ( 1 − τ ) θ −
代码:
self . soft_update ( self .actor , self .target_actor )
self . soft_update ( self .critic , self .target_critic )
若:
τ = 0.005 \tau=0.005 τ = 0.005
表示目标网络每次只吸收当前网络 0.5 % 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)
公式:
y t = r t + γ ( 1 − d t ) Q ω − ( s t + 1 , μ θ − ( s t + 1 ) ) y_t
=
r_t
+
\gamma(1-d_t)
Q_{\omega^-}
\left(
s_{t+1},
\mu_{\theta^-}(s_{t+1})
\right) y t = r t + γ ( 1 − d t ) Q ω − ( s t + 1 , μ θ − ( s t + 1 ) )
更新 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 ()
公式:
L c r i t i c = ( Q ω ( s t , a t ) − y t ) 2 L_{\mathrm{critic}}
=
\left(
Q_\omega(s_t,a_t)-y_t
\right)^2 L critic = ( Q ω ( s t , a t ) − y t ) 2
更新 Actor:
actor_loss = - torch . mean (
self . critic ( states , self . actor ( states ))
)
self . actor_optimizer . zero_grad ()
actor_loss . backward ()
self . actor_optimizer . step ()
公式:
L a c t o r = − E [ Q ω ( s , μ θ ( s ) ) ] L_{\mathrm{actor}}
=
-
\mathbb{E}
\left[
Q_\omega(s,\mu_\theta(s))
\right] L actor = − E [ Q ω ( s , μ θ ( s )) ]
最后软更新目标网络:
self . soft_update ( self .actor , self .target_actor )
self . soft_update ( self .critic , self .target_critic )
DDPG、DQN、PPO 的对比。
算法 动作空间 策略类型 是否 off-policy 是否用 replay buffer DQN 离散动作 由 arg max a Q ( s , a ) \arg\max_a Q(s,a) arg max a Q ( s , a ) 得到 是 是 PPO 离散/连续都可 随机策略 π θ ( a ∣ s ) \pi_\theta(a\mid s) π θ ( a ∣ s ) 通常不是 通常不用长期 replay buffer DDPG 连续动作 确定性策略 μ θ ( s ) \mu_\theta(s) μ θ ( s ) 是 是
DDPG 可以理解成:
DQN 的 off-policy/replay buffer 思想 + Actor-Critic 的连续动作策略网络 \text{DQN 的 off-policy/replay buffer 思想}
+
\text{Actor-Critic 的连续动作策略网络} DQN 的 off-policy/replay buffer 思想 + Actor-Critic 的连续动作策略网络
它用 Actor 代替 DQN 里的:
arg max a Q ( s , a ) \arg\max_a Q(s,a) arg a max Q ( s , a )
因为连续动作下无法枚举所有动作。
PPO 和 DDPG 的 Actor 更新差异。
PPO 的 Actor 更新来自概率比率:
r t ( θ ) = π θ ( a t ∣ s t ) π o l d ( a t ∣ s t ) r_t(\theta)
=
\frac{\pi_\theta(a_t\mid s_t)}
{\pi_{\mathrm{old}}(a_t\mid s_t)} r t ( θ ) = π old ( a t ∣ s t ) π θ ( a t ∣ s t )
PPO-Clip 的目标是:
min ( r t ( θ ) A t , c l i p ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t ) \min
\left(
r_t(\theta)A_t,
\mathrm{clip}(r_t(\theta),1-\epsilon,1+\epsilon)A_t
\right) min ( r t ( θ ) A t , clip ( r t ( θ ) , 1 − ϵ , 1 + ϵ ) A t )
其中 A t A_t A t 是优势,来自 Critic 估计的价值函数。常见做法是先算 TD 误差:
δ t = r t + γ ( 1 − d t ) V ω ( s t + 1 ) − V ω ( s t ) \delta_t
=
r_t+\gamma(1-d_t)V_\omega(s_{t+1})-V_\omega(s_t) δ t = r t + γ ( 1 − d t ) V ω ( s t + 1 ) − V ω ( s t )
再用 GAE:
A t = δ t + γ λ A t + 1 A_t
=
\delta_t+\gamma\lambda A_{t+1} A t = δ t + γλ A t + 1
PPO 更新时,A t A_t A t 通常被当作固定权重。Actor 的梯度主要来自:
∇ θ log π θ ( a t ∣ s t ) \nabla_\theta \log\pi_\theta(a_t\mid s_t) ∇ θ log π θ ( a t ∣ s t )
DDPG 的 Actor 更新则是直接最大化 Critic 给 Actor 输出动作的评分:
L a c t o r = − E [ Q ω ( s , μ θ ( s ) ) ] L_{\mathrm{actor}}
=
-
\mathbb{E}
\left[
Q_\omega(s,\mu_\theta(s))
\right] L actor = − E [ Q ω ( s , μ θ ( s )) ]
梯度路径是:
s → μ θ ( s ) → Q ω ( s , μ θ ( s ) ) → L a c t o r s
\rightarrow
\mu_\theta(s)
\rightarrow
Q_\omega(s,\mu_\theta(s))
\rightarrow
L_{\mathrm{actor}} s → μ θ ( s ) → Q ω ( s , μ θ ( s )) → L actor
所以 DDPG 的 Actor 更新会通过 Critic 对动作的梯度回传:
∇ θ J = ∇ a Q ω ( s , a ) ∣ a = μ θ ( s ) ∇ θ μ θ ( s ) \nabla_\theta J
=
\nabla_a Q_\omega(s,a)
\big|_{a=\mu_\theta(s)}
\nabla_\theta\mu_\theta(s) ∇ θ J = ∇ a Q ω ( s , a ) a = μ θ ( s ) ∇ θ μ θ ( s )
一句话区别:
PPO:Critic 给优势分数,Actor 改动作概率。
DDPG:Critic 直接通过 Q ( s , μ ( s ) ) Q(s,\mu(s)) Q ( s , μ ( s )) 的梯度指导 Actor 改动作输出。
小结。
DDPG 是面向连续动作控制的 off-policy Actor-Critic 算法。
它的 Actor 是确定性策略:
a = μ θ ( s ) a=\mu_\theta(s) a = μ θ ( s )
Critic 评价状态动作对:
Q ω ( s , a ) Q_\omega(s,a) Q ω ( s , a )
Critic 的 TD target 是:
y t = r t + γ ( 1 − d t ) Q ω − ( s t + 1 , μ θ − ( s t + 1 ) ) y_t
=
r_t
+
\gamma(1-d_t)
Q_{\omega^-}
\left(
s_{t+1},
\mu_{\theta^-}(s_{t+1})
\right) y t = r t + γ ( 1 − d t ) Q ω − ( s t + 1 , μ θ − ( s t + 1 ) )
Critic loss 是:
L c r i t i c = ( Q ω ( s t , a t ) − y t ) 2 L_{\mathrm{critic}}
=
\left(
Q_\omega(s_t,a_t)-y_t
\right)^2 L critic = ( Q ω ( s t , a t ) − y t ) 2
Actor loss 是:
L a c t o r = − E [ Q ω ( s , μ θ ( s ) ) ] L_{\mathrm{actor}}
=
-
\mathbb{E}
\left[
Q_\omega(s,\mu_\theta(s))
\right] L actor = − E [ Q ω ( s , μ θ ( s )) ]
目标网络软更新:
θ − ← τ θ + ( 1 − τ ) θ − \theta^-
\leftarrow
\tau\theta
+
(1-\tau)\theta^- θ − ← τ θ + ( 1 − τ ) θ −
我最后用四句话记这一章:
DDPG 用确定性 Actor 直接输出连续动作。
DDPG 用 Critic 评价 Q ( s , a ) Q(s,a) Q ( s , a ) ,Actor 通过最大化 Q ( s , μ ( s ) ) Q(s,\mu(s)) Q ( s , μ ( s )) 更新。
DDPG 是 off-policy,使用 replay buffer 提高样本效率。
DDPG 使用目标网络和软更新稳定训练,并通过动作噪声解决探索问题。
参考链接:
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 网络} off-policy Actor-Critic + 最大熵强化学习 + 双 Q 网络
上一章 DDPG 也是 off-policy,也能处理连续动作,但 DDPG 学的是确定性策略,探索依赖外加噪声,训练比较不稳定。SAC 学的是随机策略,并且主动把“策略熵”放进目标函数,让策略既追求高奖励,也保持足够探索。
最大熵强化学习。
熵用来衡量随机变量的不确定性。若随机变量 X X X 的分布为 p p p ,熵定义为:
H ( X ) = E x ∼ p [ − log p ( x ) ] H(X)
=
\mathbb{E}_{x\sim p}
\left[
-\log p(x)
\right] H ( X ) = E x ∼ p [ − log p ( x ) ]
在强化学习中,策略在状态 s s s 下的熵是:
H ( π ( ⋅ ∣ s ) ) = E a ∼ π [ − log π ( a ∣ s ) ] H(\pi(\cdot\mid s))
=
\mathbb{E}_{a\sim\pi}
\left[
-\log\pi(a\mid s)
\right] H ( π ( ⋅ ∣ s )) = E a ∼ π [ − log π ( a ∣ s ) ]
如果策略很确定,比如几乎总选同一个动作,熵小。
如果策略很随机,很多动作都有概率被选,熵大。
最大熵强化学习的目标是:
π ∗ = arg max π E π [ ∑ t r ( s t , a t ) + α H ( π ( ⋅ ∣ s t ) ) ] \pi^*
=
\arg\max_\pi
\mathbb{E}_\pi
\left[
\sum_t
r(s_t,a_t)
+
\alpha
H(\pi(\cdot\mid s_t))
\right] π ∗ = arg π max E π [ t ∑ r ( s t , a t ) + α H ( π ( ⋅ ∣ s t )) ]
也可以写成:
E π [ ∑ t r ( s t , a t ) − α log π ( a t ∣ s t ) ] \mathbb{E}_\pi
\left[
\sum_t
r(s_t,a_t)
-
\alpha
\log\pi(a_t\mid s_t)
\right] E π [ t ∑ r ( s t , a t ) − α log π ( a t ∣ s t ) ]
因为:
H ( π ( ⋅ ∣ s t ) ) = E a t ∼ π [ − log π ( a t ∣ s t ) ] H(\pi(\cdot\mid s_t))
=
\mathbb{E}_{a_t\sim\pi}
\left[
-\log\pi(a_t\mid s_t)
\right] H ( π ( ⋅ ∣ s t )) = E a t ∼ π [ − log π ( a t ∣ s t ) ]
其中 α \alpha α 是温度系数,控制熵的重要程度:
α \alpha α 大:更重视探索,策略更随机。
α \alpha α 小:更重视奖励,策略更接近贪婪。
Soft Bellman 方程。
普通强化学习里,状态价值大致来自未来奖励。
最大熵强化学习中,价值还要加上熵奖励。
Soft Q 方程:
Q ( s t , a t ) = r ( s t , a t ) + γ E s t + 1 [ V ( s t + 1 ) ] Q(s_t,a_t)
=
r(s_t,a_t)
+
\gamma
\mathbb{E}_{s_{t+1}}
\left[
V(s_{t+1})
\right] Q ( s t , a t ) = r ( s t , a t ) + γ E s t + 1 [ V ( s t + 1 ) ]
Soft 状态价值:
V ( s t ) = E a t ∼ π [ Q ( s t , a t ) − α log π ( a t ∣ s t ) ] V(s_t)
=
\mathbb{E}_{a_t\sim\pi}
\left[
Q(s_t,a_t)
-
\alpha
\log\pi(a_t\mid s_t)
\right] V ( s t ) = E a t ∼ π [ Q ( s t , a t ) − α log π ( a t ∣ s t ) ]
这里的:
− α log π ( a t ∣ s t ) -
\alpha\log\pi(a_t\mid s_t) − α log π ( a t ∣ s t )
就是熵奖励。动作概率越低,− log π -\log\pi − log π 越大;策略越有随机性,整体熵越大。
SAC 和 DDPG 的关系。
SAC 和 DDPG 都是 off-policy Actor-Critic,并且都使用 replay buffer。
但它们有关键区别:
对比 DDPG SAC 策略类型 确定性策略 μ θ ( s ) \mu_\theta(s) μ θ ( s ) 随机策略 π θ ( a ∣ s ) \pi_\theta(a\mid s) π θ ( a ∣ s ) 探索方式 外加动作噪声 策略本身有熵 Critic 一个 Q 网络和一个 target Q 两个 Q 网络和两个 target Q Actor 目标 最大化 Q ( s , μ ( s ) ) Q(s,\mu(s)) Q ( s , μ ( s )) 最大化 Q Q Q 同时最大化熵 稳定性 较敏感 通常更稳定
DDPG 的 Actor loss:
L a c t o r D D P G = − E [ Q ω ( s , μ θ ( s ) ) ] L_{\mathrm{actor}}^{\mathrm{DDPG}}
=
-
\mathbb{E}
\left[
Q_\omega(s,\mu_\theta(s))
\right] L actor DDPG = − E [ Q ω ( s , μ θ ( s )) ]
SAC 的 Actor loss:
L π ( θ ) = E s ∼ D , a ∼ π θ [ α log π θ ( a ∣ s ) − min i = 1 , 2 Q ω 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] L π ( θ ) = E s ∼ D , a ∼ π θ [ α log π θ ( a ∣ s ) − i = 1 , 2 min Q ω i ( s , a ) ]
最小化这个 loss,等价于最大化:
min i = 1 , 2 Q ω i ( s , a ) − α log π θ ( a ∣ s ) \min_{i=1,2}Q_{\omega_i}(s,a)
-
\alpha\log\pi_\theta(a\mid s) i = 1 , 2 min Q ω i ( s , a ) − α log π θ ( a ∣ s )
也就是:
高 Q + 高熵 \text{高 Q}
+
\text{高熵} 高 Q + 高熵
为什么 SAC 用两个 Q 网络。
SAC 使用两个 Q 网络:
Q ω 1 ( s , a ) , Q ω 2 ( s , a ) Q_{\omega_1}(s,a),
\quad
Q_{\omega_2}(s,a) Q ω 1 ( s , a ) , Q ω 2 ( s , a )
并且取较小的那个:
min i = 1 , 2 Q ω i ( s , a ) \min_{i=1,2}
Q_{\omega_i}(s,a) i = 1 , 2 min Q ω i ( s , a )
这是 Double Q 的思想,用来缓解 Q 值过高估计。
如果只用一个 Q 网络,Actor 可能会利用 Critic 的高估错误,专门输出被误判为高价值的动作。取两个 Q 的较小值,可以让估计更保守。
SAC 还有两个目标 Q 网络:
Q ω 1 − , Q ω 2 − Q_{\omega_1^-},
\quad
Q_{\omega_2^-} Q ω 1 − , Q ω 2 −
用来计算稳定的 TD target,并用软更新:
ω i − ← τ ω i + ( 1 − τ ) ω i − , i = 1 , 2 \omega_i^-
\leftarrow
\tau\omega_i
+
(1-\tau)\omega_i^-,
\quad i=1,2 ω i − ← τ ω i + ( 1 − τ ) ω i − , i = 1 , 2
Critic 怎么更新。
SAC 的 replay buffer 中采样:
( s t , a t , r t , s t + 1 , d t ) ∼ D (s_t,a_t,r_t,s_{t+1},d_t)
\sim D ( s t , a t , r t , s t + 1 , d t ) ∼ D
在下一个状态,Actor 采样:
a t + 1 ∼ π θ ( ⋅ ∣ s t + 1 ) a_{t+1}\sim\pi_\theta(\cdot\mid s_{t+1}) a t + 1 ∼ π θ ( ⋅ ∣ s t + 1 )
Soft TD target 是:
y t = r t + γ ( 1 − d t ) [ min i = 1 , 2 Q ω i − ( s t + 1 , a t + 1 ) − α log π θ ( a t + 1 ∣ s t + 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] y t = r t + γ ( 1 − d t ) [ i = 1 , 2 min Q ω i − ( s t + 1 , a t + 1 ) − α log π θ ( a t + 1 ∣ s t + 1 ) ]
因为:
H = − log π H=-\log\pi H = − log π
代码里也常写成:
min Q + α H \min Q+\alpha H min Q + α H
两个 Critic 都向同一个 target 学:
L Q i ( ω i ) = E [ ( Q ω i ( s t , a t ) − y t ) 2 ] , i = 1 , 2 L_{Q_i}(\omega_i)
=
\mathbb{E}
\left[
\left(
Q_{\omega_i}(s_t,a_t)-y_t
\right)^2
\right],
\quad i=1,2 L Q i ( ω i ) = E [ ( Q ω i ( s t , a t ) − y t ) 2 ] , 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) μ θ ( s ) , σ θ ( s )
然后从高斯分布采样动作。
Actor 的目标是:
L π ( θ ) = E [ α log π θ ( a ∣ s ) − min i = 1 , 2 Q ω 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] L π ( θ ) = E [ α log π θ ( a ∣ s ) − i = 1 , 2 min Q ω i ( s , a ) ]
因为优化器最小化 loss,所以最小化上式等价于:
让 Q Q Q 变大。
让 log π \log\pi log π 不要太大,即保持熵。
代码中:
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 )
)
因为:
e n t r o p y = − log π entropy=-\log\pi e n t ro p y = − log π
所以:
− α e n t r o p y − min Q = α log π − min Q -\alpha entropy-\min Q
=
\alpha\log\pi-\min Q − α e n t ro p y − min Q = α log π − min Q
这正是 SAC 的 Actor loss。
重参数化技巧。
SAC 连续动作策略从高斯分布采样动作。如果直接采样:
a ∼ N ( μ θ ( s ) , σ θ ( s ) 2 ) a\sim\mathcal{N}(\mu_\theta(s),\sigma_\theta(s)^2) a ∼ N ( μ θ ( s ) , σ θ ( s ) 2 )
采样操作本身不好对 θ \theta θ 反向传播。
重参数化技巧把采样写成:
ϵ ∼ N ( 0 , 1 ) \epsilon\sim\mathcal{N}(0,1) ϵ ∼ N ( 0 , 1 )
a = μ θ ( s ) + σ θ ( s ) ϵ a
=
\mu_\theta(s)
+
\sigma_\theta(s)\epsilon a = μ θ ( s ) + σ θ ( s ) ϵ
随机性来自 ϵ \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] [ − 1 , 1 ]
再乘上动作上界,得到合法动作。
温度系数 alpha 自动调整。
SAC 中 α \alpha α 控制熵项权重。
如果 α \alpha α 太大,策略太随机。
如果 α \alpha α 太小,策略太贪婪,探索不足。
SAC 可以自动学习 α \alpha α 。它希望策略熵接近目标熵:
H 0 \mathcal{H}_0 H 0
约束形式是:
E [ − log π ( a t ∣ s t ) ] ≥ H 0 \mathbb{E}
\left[
-\log\pi(a_t\mid s_t)
\right]
\ge
\mathcal{H}_0 E [ − log π ( a t ∣ s t ) ] ≥ H 0
α \alpha α 的损失可以写成:
L ( α ) = E [ − α log π ( a t ∣ s t ) − α H 0 ] L(\alpha)
=
\mathbb{E}
\left[
-\alpha\log\pi(a_t\mid s_t)
-
\alpha\mathcal{H}_0
\right] L ( α ) = E [ − α log π ( a t ∣ s t ) − α H 0 ]
代码中用 log_alpha 而不是直接用 α \alpha α ,这样可以保证:
α = exp ( log α ) > 0 \alpha=\exp(\log\alpha)>0 α = exp ( log α ) > 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 ()
)
直觉:
如果当前熵低于目标熵,α \alpha α 会变大,鼓励更多探索。
如果当前熵高于目标熵,α \alpha α 会变小,让策略更关注提高 Q 值。
SAC 的网络结构。
连续动作 SAC 一共有 5 个网络:
网络 作用 Actor π θ ( a ∣ s ) \pi_\theta(a\mid s) π θ ( a ∣ s ) 输出随机策略并采样动作 Critic 1 Q ω 1 ( s , a ) Q_{\omega_1}(s,a) Q ω 1 ( s , a ) 第一个 Q 网络 Critic 2 Q ω 2 ( s , a ) Q_{\omega_2}(s,a) Q ω 2 ( s , a ) 第二个 Q 网络 Target Critic 1 Q ω 1 − ( s , a ) Q_{\omega_1^-}(s,a) Q ω 1 − ( s , a ) 第一个目标 Q 网络 Target Critic 2 Q ω 2 − ( s , a ) Q_{\omega_2^-}(s,a) Q ω 2 − ( s , a ) 第二个目标 Q 网络
和 DDPG 不同,SAC 代码中没有 target actor。下一个动作由当前随机 Actor 采样:
a ′ ∼ π θ ( ⋅ ∣ s ′ ) a'\sim\pi_\theta(\cdot\mid s') a ′ ∼ π θ ( ⋅ ∣ 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
这里:
mu 是高斯均值。
std 是标准差,softplus 保证它为正。
rsample() 用重参数化采样。
tanh 限制动作范围。
返回动作和该动作的 log probability。
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)
对应:
y t = r t + γ ( 1 − d t ) [ min i Q ω i − ( s t + 1 , a t + 1 ) + α H ( π ( ⋅ ∣ s t + 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] y t = r t + γ ( 1 − d t ) [ i min Q ω i − ( s t + 1 , a t + 1 ) + α H ( π ( ⋅ ∣ s t + 1 )) ]
更新两个 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-policy ratio clip 策略分布采样 DDPG 确定性策略 off-policy target networks + soft update 外加噪声 SAC 随机策略 off-policy 最大熵 + 双 Q + target Q 熵正则
SAC 可以理解成:
DDPG 的 off-policy 样本效率 + PPO 式随机策略探索 + 最大熵目标 \text{DDPG 的 off-policy 样本效率}
+
\text{PPO 式随机策略探索}
+
\text{最大熵目标} DDPG 的 off-policy 样本效率 + PPO 式随机策略探索 + 最大熵目标
它通常比 DDPG 更稳定,因为它不是只追求一个确定动作,而是让策略保持一定随机性,减少陷入局部最优的风险。
小结。
SAC 是一个高效稳定的 off-policy Actor-Critic 算法。
最大熵目标是:
max π E [ ∑ t r ( s t , a t ) + α H ( π ( ⋅ ∣ s t ) ) ] \max_\pi
\mathbb{E}
\left[
\sum_t
r(s_t,a_t)
+
\alpha H(\pi(\cdot\mid s_t))
\right] π max E [ t ∑ r ( s t , a t ) + α H ( π ( ⋅ ∣ s t )) ]
Soft 状态价值是:
V ( s ) = E a ∼ π [ Q ( s , a ) − α log π ( a ∣ s ) ] V(s)
=
\mathbb{E}_{a\sim\pi}
\left[
Q(s,a)
-
\alpha\log\pi(a\mid s)
\right] V ( s ) = E a ∼ π [ Q ( s , a ) − α log π ( a ∣ s ) ]
Critic target 是:
y t = r t + γ ( 1 − d t ) [ min i Q ω i − ( s t + 1 , a t + 1 ) − α log π θ ( a t + 1 ∣ s t + 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] y t = r t + γ ( 1 − d t ) [ i min Q ω i − ( s t + 1 , a t + 1 ) − α log π θ ( a t + 1 ∣ s t + 1 ) ]
Actor loss 是:
L π ( θ ) = E [ α log π θ ( a ∣ s ) − min i Q ω i ( s , a ) ] L_\pi(\theta)
=
\mathbb{E}
\left[
\alpha\log\pi_\theta(a\mid s)
-
\min_iQ_{\omega_i}(s,a)
\right] L π ( θ ) = E [ α log π θ ( a ∣ s ) − i min Q ω i ( s , a ) ]
温度系数 α \alpha α 自动调节探索强度。α \alpha α 大,策略更随机;α \alpha α 小,策略更贪婪。
我最后用四句话记这一章:
SAC 是最大熵版本的 off-policy Actor-Critic。
SAC 学随机策略,不像 DDPG 那样学确定性动作。
SAC 用两个 Q 网络取较小值,缓解 Q 过高估计。
SAC 用熵项和自动温度系数保持探索,因此通常比 DDPG 更稳定。
参考链接: