本博客的理论知识来自王树森老师《深度强化学习》,这本书写得简直太好了,强烈推荐,只是现在还在校对没出版,可能有些小瑕疵,但并不影响阅读和学习。
本次的 A2C 的原理我们从带基线的策略梯度开始,在对带基线的策略梯度做蒙特卡洛近似,得到策略梯度的一个无偏估计:
g ( s , a , ; θ ) = [ Q π ( s , a ) − V π ( s ) ⋅ ∇ ln π ( a ∣ s ; θ ) ] (1) \boldsymbol{g}(s,a,;\boldsymbol{\theta}) = \left[ Q_\pi(s, a) - V_\pi (s) \cdot \nabla \ln \pi(a|s;\theta)\right] \tag1 g(s,a,;θ)=[Qπ(s,a)−Vπ(s)⋅∇lnπ(a∣s;θ)](1)
公式中的 Q π − V π Q_\pi - V_\pi Qπ−Vπ 被称作优势函数 (Advantage Function)。因此,基于上面公式得到的 Actor-Critic 方法被称为 Advantage Actor-Critic,缩写 A2C。A2C 属于 Actor-Critic 方法。有一个策略网络 π ( a ∣ s ; θ ) \pi(a|s;\theta) π(a∣s;θ),相当于演员,用于控制智能体运动。还有一个价值网络 v ( s ; w ) v(s; w) v(s;w),相当于评委,他的评分可以帮助策略网络(演员)改进技术。
训练价值网络: 训练价值网络 v ( s ; w ) v(s; w) v(s;w) 的算法是从贝尔曼公式来的:
V π ( s t ) = E A t ∼ π ( ⋅ ∣ s t ; θ ) [ E S t + 1 ∼ p ( ⋅ ∣ s t , A t ) [ R t + γ ⋅ V π ( S t + 1 ) ] ] V_\pi(s_t) = \mathbb{E}_{A_t∼\pi(\cdot|s_t;\theta)}[\mathbb{E}_{S_{t+1}∼p(\cdot|s_t, A_t)}[R_t + \gamma \cdot V_\pi(S_{t+1})]] Vπ(st)=EAt∼π(⋅∣st;θ)[ESt+1∼p(⋅∣st,At)[Rt+γ⋅Vπ(St+1)]]
对贝尔曼方程左右两边做近似:
v ( s t ; w ) v(s_t; w) v(st;w) 和 y ^ t \hat y_t y^t 都是对动作价值 V π ( s t ) Vπ_(s_t) Vπ(st) 的估计。由于 y ^ t \hat y_t y^t 部分基于真实观测到的奖励 r t r_t rt,我们认为 y ^ t \hat y_t y^t 比 v ( s t ; w ) v(s_t;\boldsymbol{w}) v(st;w) 更可靠。所以把 y ^ t \hat y_t y^t 固定住,更新 w \boldsymbol{w} w,使得 v ( s t ; w ) v(s_t;\boldsymbol{w}) v(st;w) 更接近 y ^ t \hat y_t y^t。具体一点,定义损失函数:
L ( w ) : = 1 2 [ v ( s t ; w ) − y ^ t ] 2 L(\boldsymbol{w}) := \frac{1}{2}[v(s_t;\boldsymbol{w}) - \hat y_t]^2 L(w):=21[v(st;w)−y^t]2
设 v ^ t : = v ( s t ; w ) \hat v_t := v(s_t;\boldsymbol{w}) v^t:=v(st;w),损失函数的梯度为:
∇ w L ( w ) = ( v ^ t − y ^ t ) ⏟ T D 误差 δ t ⋅ ∇ w v ( s t ; w ) \nabla_{\boldsymbol{w}}L(\boldsymbol{w}) = \underbrace{(\hat v_t - \hat y_t)}_{TD\ 误差\ \delta_t} \cdot \nabla_{\boldsymbol{w}}v(s_t;\boldsymbol{w}) ∇wL(w)=TD 误差 δt (v^t−y^t)⋅∇wv(st;w)
定义 TD 误差为 δ : = v ^ t − y ^ t \delta := \hat v_t - \hat y_t δ:=v^t−y^t。做一轮梯度下降更新 w \boldsymbol{w} w:
w ← w − α ⋅ δ t ⋅ ∇ w v ( s t ; w ) \boldsymbol{w} \leftarrow \boldsymbol{w}- \alpha \cdot \delta_t \cdot \nabla_{\boldsymbol{w}}v(s_t;\boldsymbol{w}) w←w−α⋅δt⋅∇wv(st;w)
这样可以让价值网络的预测 v ( s t ; w ) v(s_t;\boldsymbol{w}) v(st;w) 更接近 y ^ t \hat y_t y^t。
训练策略网络:A2C 从公式 (1) 出发,对 g ( s , a , ; θ ) \boldsymbol{g}(s,a,;\boldsymbol{\theta}) g(s,a,;θ) 做近似,记作 g ~ \boldsymbol{\tilde{g}} g~,然后用 g ~ \boldsymbol{\tilde{g}} g~ 更新策略网络参数 θ \boldsymbol{\theta} θ。下面我们做数学推导。回忆一下贝尔曼公式:
Q π ( s t , a t ) = E S t + 1 ∼ p ( ⋅ ∣ s t , A t ) [ R t + γ ⋅ V π ( S t + 1 ) ] Q_\pi(s_t ,a_t) = \mathbb{E}_{S_{t+1}∼p(\cdot|s_t, A_t)}[R_t + \gamma \cdot V_\pi(S_{t+1})] Qπ(st,at)=ESt+1∼p(⋅∣st,At)[Rt+γ⋅Vπ(St+1)]
把近似策略梯度 g ( s , a , ; θ ) \boldsymbol{g}(s,a,;\boldsymbol{\theta}) g(s,a,;θ) 中的 Q π ( s t , a t ) Q_π(s_t, a_t) Qπ(st,at) 替换成上面的期望,得到:
g ( s , a , ; θ ) = [ Q π ( s t , a t ) − V π ( s t ) ] ⋅ ∇ θ ln π ( a t ∣ s t ; θ ) = [ E S t + 1 [ R t + γ ⋅ V π ( S t + 1 ) ] − V π ( S t ) ] ⋅ ∇ θ ln π ( a t ∣ s t ; θ ) \begin{aligned} \boldsymbol{g}(s,a,;\boldsymbol{\theta}) &= [Q_\pi(s_t, a_t) - V_\pi(s_t)] \cdot \nabla_{\boldsymbol{\theta}}\ln \pi(a_t|s_t;\boldsymbol{\theta}) \\ &=\left[ \mathbb{E}_{S_{t+1}}[R_t + \gamma \cdot V_\pi(S_{t+1})] - V_\pi(S_t)\right]\cdot \nabla_{\boldsymbol{\theta}}\ln \pi(a_t|s_t;\boldsymbol{\theta}) \end{aligned} g(s,a,;θ)=[Qπ(st,at)−Vπ(st)]⋅∇θlnπ(at∣st;θ)=[ESt+1[Rt+γ⋅Vπ(St+1)]−Vπ(St)]⋅∇θlnπ(at∣st;θ)
当智能体执行动作 a t a_t at 之后,环境给出新的状态 s t + 1 s_{t+1} st+1 和奖励 r t r_t rt;利用 s t + 1 s_{t+1} st+1 和 r t r_t rt 对上面的期望做蒙特卡洛近似,得到:
g ( s , a , ; θ ) ≈ [ r t + γ ⋅ V π ( s t + 1 ) − V π ( s t ) ] ⋅ ∇ ln π ( a ∣ s ; θ ) \boldsymbol{g}(s,a,;\boldsymbol{\theta}) \approx [ r_t + \gamma \cdot V_\pi(s_{t+1}) - V_\pi(s_t)] \cdot \nabla \ln \pi(a|s;\theta) g(s,a,;θ)≈[rt+γ⋅Vπ(st+1)−Vπ(st)]⋅∇lnπ(a∣s;θ)
进一步把状态价值函数 V π ( s ) V_π(s) Vπ(s) 替换成价值网络 v ( s ; w ) v(s; \boldsymbol{w}) v(s;w),得到:
g ~ ( s , a , ; θ ) : = [ r t + γ ⋅ v ( s ; w ) ⏟ T D 目标 y ^ t − v ( s ; w ) ⋅ ∇ ln π ( a ∣ s ; θ ) \boldsymbol{\tilde g}(s,a,;\boldsymbol{\theta}):=[ \underbrace{r_t + \gamma \cdot v(s; \boldsymbol{w})}_{TD\ 目标 \ \hat y_t} - v(s; \boldsymbol{w}) \cdot \nabla \ln \pi(a|s;\theta) g~(s,a,;θ):=[TD 目标 y^t rt+γ⋅v(s;w)−v(s;w)⋅∇lnπ(a∣s;θ)
前面定义了 TD 目标和 TD 误差, y t ^ : = r t + γ ⋅ v ( s t + 1 ; w ) \hat{y_t} := r_t + \gamma \cdot v(s_{t+1}; \boldsymbol{w}) yt^:=rt+γ⋅v(st+1;w) 和 δ t : = v ( s t ; w ) − y ^ t \delta_t := v(s_t;\boldsymbol{w}) - \hat y_t δt:=v(st;w)−y^t。因此,可以将 g ( s , a , ; θ ) : = − δ t ⋅ ∇ ln π ( a ∣ s ; θ ) \boldsymbol{g}(s,a,;\boldsymbol{\theta}) :=- \delta _t \cdot \nabla \ln \pi(a|s;\theta) g(s,a,;θ):=−δt⋅∇lnπ(a∣s;θ)
g ~ \boldsymbol{\tilde{g}} g~ 是 g \boldsymbol{{g}} g 的近似,所以也是策略梯度 ∇ θ J ( θ ) \nabla_{\boldsymbol{\theta}}J(\boldsymbol{\theta}) ∇θJ(θ) 的近似。用 g ~ \boldsymbol{\tilde{g}} g~更新策略网络参数 θ \boldsymbol{\theta} θ:
θ ← θ + β ⋅ g ( s , a , ; θ ) \boldsymbol{\theta} \leftarrow \boldsymbol{\theta} + \beta \cdot \boldsymbol{g}(s,a,;\boldsymbol{\theta}) θ←θ+β⋅g(s,a,;θ)
这样可以让目标函数 J ( θ ) J(\boldsymbol{\theta}) J(θ) 变大。
下面概括 A2C 训练流程。设当前策略网络参数是 θ n o w \boldsymbol{\theta_{now}} θnow,价值网络参数是 w n o w \boldsymbol{w_{now}} wnow。执行下面的步骤,将参数更新成 θ n e w \boldsymbol{\theta_{new}} θnew 和 w n e w \boldsymbol{w_{new}} wnew:
上述训练价值网络的算法存在自举——即用价值网络自己的估值 v ^ t + 1 \hat v_{t + 1} v^t+1 去更新价值网络自己。为了缓解自举造成的偏差,可以使用目标网络 (Target Network) 计算 TD 目标。把目标网络记作 v ( s ; w − ) v(s; \boldsymbol{w^{-}}) v(s;w−),它的结构与价值网络的结构相同,但是参数不同。使用目标网络计算 TD 目标,那么 A2C 的训练就变成了:
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import gym
import numpy as np
LR_ACTOR = 0.01 # 策略网络的学习率
LR_CRITIC = 0.001 # 价值网络的学习率
GAMMA = 0.9 # 奖励的折扣因子
EPSILON = 0.9 # ϵ-greedy 策略的概率
TARGET_REPLACE_ITER = 100 # 目标网络更新的频率
env = gym.make('CartPole-v0') # 加载游戏环境
N_ACTIONS = env.action_space.n # 动作数
N_SPACES = env.observation_space.shape[0] # 状态数量
env = env.unwrapped
# 网络参数初始化,采用均值为 0,方差为 0.1 的高斯分布
def init_weights(m) :
if isinstance(m, nn.Linear) :
nn.init.normal_(m.weight, mean = 0, std = 0.1)
# 策略网络
class Actor(nn.Module) :
def __init__(self):
super(Actor, self).__init__()
self.net = nn.Sequential(
nn.Linear(N_SPACES, 50),
nn.ReLU(),
nn.Linear(50, N_ACTIONS) # 输出为各个动作的概率,维度为 3
)
def forward(self, s):
output = self.net(s)
output = F.softmax(output, dim = -1) # 概率归一化
return output
# 价值网络
class Critic(nn.Module) :
def __init__(self):
super(Critic, self).__init__()
self.net = nn.Sequential(
nn.Linear(N_SPACES, 20),
nn.ReLU(),
nn.Linear(20, 1) # 输出值是对当前状态的打分,维度为 1
)
def forward(self, s):
output = self.net(s)
return output
# A2C 的主体函数
class A2C :
def __init__(self):
# 初始化策略网络,价值网络和目标网络。价值网络和目标网络使用同一个网络
self.actor_net, self.critic_net, self.target_net = Actor().apply(init_weights), Critic().apply(init_weights), Critic().apply(init_weights)
self.learn_step_counter = 0 # 学习步数
self.optimizer_actor = optim.Adam(self.actor_net.parameters(), lr = LR_ACTOR) # 策略网络优化器
self.optimizer_critic = optim.Adam(self.critic_net.parameters(), lr = LR_CRITIC) # 价值网络优化器
self.criterion_critic = nn.MSELoss() # 价值网络损失函数
def choose_action(self, s):
s = torch.unsqueeze(torch.FloatTensor(s), dim = 0) # 增加维度
if np.random.uniform() < EPSILON : # ϵ-greedy 策略对动作进行采取
action_value = self.actor_net(s)
action = torch.max(action_value, dim = 1)[1].item()
else :
action = np.random.randint(0, N_ACTIONS)
return action
def learn(self, s, a, r, s_):
if self.learn_step_counter % TARGET_REPLACE_ITER == 0 : # 更新目标网络
self.target_net.load_state_dict(self.critic_net.state_dict())
self.learn_step_counter += 1
s = torch.FloatTensor(s)
s_ = torch.FloatTensor(s_)
q_actor = self.actor_net(s) # 策略网络
q_critic = self.critic_net(s) # 价值对当前状态进行打分
q_next = self.target_net(s_).detach() # 目标网络对下一个状态进行打分
q_target = r + GAMMA * q_next # 更新 TD 目标
td_error = (q_critic - q_target).detach() # TD 误差
# 更新价值网络
loss_critic = self.criterion_critic(q_critic, q_target)
self.optimizer_critic.zero_grad()
loss_critic.backward()
self.optimizer_critic.step()
# 更新策略网络
log_q_actor = torch.log(q_actor)
actor_loss = log_q_actor[a] * td_error
self.optimizer_actor.zero_grad()
actor_loss.backward()
self.optimizer_actor.step()
a2c = A2C()
for epoch in range(10000) :
s = env.reset()
ep_r = 0
while True :
env.render()
a = a2c.choose_action(s) # 选择动作
s_, r, done, info = env.step(a)# 执行动作
x, x_dot, theta, theta_dot = s_
# 修改奖励,为了更快收敛
r1 = (env.x_threshold - abs(x)) / env.x_threshold - 0.8
r2 = (env.theta_threshold_radians - abs(theta)) / env.theta_threshold_radians - 0.5
r = r1 + r2
ep_r += r
# 学习
a2c.learn(s, a, r, s_)
if done :
break
s = s_
print(f'Ep: {epoch} | Ep_r: {round(ep_r, 2)}')