本文将从策略梯度开始介绍,理解策略梯度原理的基础上讲解REINFORCE算法并分析代码,总结其优缺点
一、value-based存在的一些缺点
- 不能处理连续动作空间或者高维的离散动作空间
二、policy-based的主要思想与策略梯度定理
policy-based不建模Q函数,而是得出累计期望奖励最大的策略,利用神经网络强大的表示能力将策略参数化。对参数采用梯度下降算法以更新策略,以优化累计期望奖励。下面证明为什么更新策略能使得累计期望奖励最大。
最大化目标函数,应该是梯度上升,但本质是一样的,可以实际中取负变为梯度下降问题
我们从期望奖励出发,考虑轨迹
τ
=
(
s
1
,
a
1
,
.
.
.
,
s
T
,
a
T
)
\tau = (s_1,a_1,...,s_T,a_T)
τ=(s1,a1,...,sT,aT)的概率为:
p
θ
(
s
0
,
a
0
,
.
.
.
,
s
T
,
a
T
)
=
p
(
s
0
)
∏
t
=
0
T
π
θ
(
a
t
∣
s
t
)
p
(
s
t
+
1
∣
s
t
,
a
t
)
p_{\theta}(s_0,a_0,...,s_T,a_T)=p(s_0)\prod_{t=0}^T\pi_\theta(a_t|s_t)p(s_{t+1}|s_t,a_t)
pθ(s0,a0,...,sT,aT)=p(s0)t=0∏Tπθ(at∣st)p(st+1∣st,at)
我们表示策略
π
θ
\pi_\theta
πθ生成轨迹
τ
\tau
τ的概率
p
(
τ
∣
π
)
p(\tau|\pi)
p(τ∣π)改写上式:
P
(
τ
∣
π
)
=
p
0
(
s
0
)
∏
t
=
0
T
π
(
a
t
∣
s
t
)
p
(
s
t
+
1
∣
s
t
,
a
t
)
P(\tau|\pi)=p_0(s_0)\prod_{t=0}^T\pi(a_t|s_t)p(s_{t+1}|s_t,a_t)
P(τ∣π)=p0(s0)t=0∏Tπ(at∣st)p(st+1∣st,at)
对于利用当前策略生成的多条轨迹,每条轨迹都有其累计期望奖励,将这些累计期望奖励取期望即可得到当前策略下的累计期望奖励。积分形式如下:
J
(
π
θ
)
=
E
τ
∼
π
θ
[
R
(
τ
)
]
=
∫
τ
P
(
τ
∣
θ
)
R
(
τ
)
J(\pi_\theta)=E_{\tau\thicksim\pi_\theta}[R(\tau)]=\int_\tau P(\tau|\theta)R(\tau)
J(πθ)=Eτ∼πθ[R(τ)]=∫τP(τ∣θ)R(τ)
策略梯度定理
对目标函数求导可以得出梯度下降的公式。接下来通过对公式求导。我们考虑没有折扣因子的情况,对上面的目标函数进行求导
∇
θ
J
(
π
θ
)
=
∇
θ
∫
τ
P
(
τ
∣
θ
)
R
(
τ
)
=
∫
τ
∇
θ
P
(
τ
∣
θ
)
R
(
τ
)
\nabla _\theta J(\pi_\theta)=\nabla _\theta\int_\tau P(\tau|\theta)R(\tau)=\int_\tau\nabla _\theta P(\tau|\theta)R(\tau)
∇θJ(πθ)=∇θ∫τP(τ∣θ)R(τ)=∫τ∇θP(τ∣θ)R(τ)
由于
θ
\theta
θ只会影响策略,不会影响环境中当前奖励大小,因此只需对
P
(
τ
∣
π
)
P(\tau|\pi)
P(τ∣π)求导。策略梯度定理告诉我们——
P
(
τ
∣
π
)
P(\tau|\pi)
P(τ∣π)的求导只需对策略求梯度而无需对状态转移概率函数求梯度,证明如下:
∫
τ
∇
θ
P
(
τ
∣
θ
)
R
(
τ
)
=
∫
τ
P
(
τ
∣
θ
)
∇
θ
P
(
τ
∣
θ
)
P
(
τ
∣
θ
)
R
(
τ
)
=
∫
τ
P
(
τ
∣
θ
)
∇
θ
l
o
g
P
(
τ
∣
θ
)
R
(
τ
)
(1)
\int_\tau\nabla _\theta P(\tau|\theta)R(\tau)=\int_\tau P(\tau|\theta) \frac{\nabla_\theta P(\tau|\theta)}{P(\tau|\theta)} R(\tau)=\int_\tau P(\tau|\theta) \nabla_\theta logP(\tau|\theta) R(\tau)\tag{1}
∫τ∇θP(τ∣θ)R(τ)=∫τP(τ∣θ)P(τ∣θ)∇θP(τ∣θ)R(τ)=∫τP(τ∣θ)∇θlogP(τ∣θ)R(τ)(1)
下面对
l
o
g
P
(
τ
∣
θ
)
logP(\tau|\theta)
logP(τ∣θ)展开
l
o
g
P
(
τ
∣
θ
)
=
l
o
g
p
0
(
s
0
)
+
∑
t
=
0
T
(
l
o
g
π
(
a
t
∣
s
t
)
+
l
o
g
p
(
s
t
+
1
∣
s
t
,
a
t
)
)
logP(\tau|\theta)=log\space p_0(s_0)+\sum_{t=0}^T(log\space\pi(a_t|s_t)+log\space p(s_{t+1}|s_t,a_t))
logP(τ∣θ)=log p0(s0)+t=0∑T(log π(at∣st)+log p(st+1∣st,at))
利用对数将累乘转化为累加,此时
θ
\theta
θ也不影响环境中状态转移概率的大小,这是环境自身的属性。则求导结果为:
∇
θ
l
o
g
P
(
τ
∣
θ
)
=
∑
t
=
0
T
∇
θ
l
o
g
π
(
a
t
∣
s
t
)
\nabla_\theta logP(\tau|\theta)=\sum_{t=0}^T\nabla_\theta log \space \pi(a_t|s_t)
∇θlogP(τ∣θ)=∑t=0T∇θlog π(at∣st)
最终形式如下:
∇
θ
J
(
π
θ
)
=
E
τ
∼
P
(
τ
∣
θ
)
[
∑
t
=
0
T
∇
θ
l
o
g
π
θ
(
a
t
∣
s
t
)
R
(
τ
)
]
\nabla _\theta J(\pi_\theta)=E_{\tau\thicksim P(\tau|\theta)}[\sum_{t=0}^T\nabla _{\theta}log\space\pi_\theta(a_t|s_t)R(\tau)]
∇θJ(πθ)=Eτ∼P(τ∣θ)[t=0∑T∇θlog πθ(at∣st)R(τ)]
与先前对比,策略梯度定理告诉我们——梯度下降过程中,只需求导策略部分,无需求导状态转移函数部分
三、REINFORCE算法
在实际计算中,我们无法精确计算 ∇ θ J ( π θ ) \nabla _\theta J(\pi_\theta) ∇θJ(πθ)——轨迹难以穷举,REINFORCE算法提出采用蒙特卡洛采样,采样当前策略下的多条轨迹迭代优化策略,每次策略迭代基于一条轨迹。
基本的REINFORCE算法有如下特点:
- on-policy算法:每次计算只能利用当前策略生成与环境交互的样本进行更新,而不能使用旧样本,因为旧样本不是当前策略产生的,所以这是一种 on-policy 算法。
- 高方差:高方差由两个部分造成
- 轨迹随机性:在环境随机性较大的情况下,相近迭代的策略的轨迹也有非常不同的结果,因此REINFORCE策略梯度估计有较高的方差。
- Monte Carlo方法更新梯度:梯度更新一次需要等整个轨迹结束再进行更新,方差较大
REINFORCE算法的公式为:
∇
θ
J
(
π
θ
)
=
E
τ
∼
P
(
τ
∣
θ
)
[
∑
t
=
0
T
∇
θ
l
o
g
π
θ
(
a
t
∣
s
t
)
R
(
τ
)
]
=
1
N
∑
τ
∑
t
∇
θ
l
o
g
π
(
a
t
∣
s
t
)
R
(
τ
)
\nabla_\theta J(\pi_\theta)=E_{\tau\thicksim P(\tau|\theta)}[\sum_{t=0}^T\nabla _{\theta}log\space\pi_\theta(a_t|s_t)R(\tau)]=\frac{1}{N}\sum_\tau\sum_t\nabla _{\theta}log\space\pi(a_t|s_t)R(\tau)
∇θJ(πθ)=Eτ∼P(τ∣θ)[t=0∑T∇θlog πθ(at∣st)R(τ)]=N1τ∑t∑∇θlog π(at∣st)R(τ)
在pytorch中,反向传播可以自动进行,我们需要定义该反向传播的正向传播公式,即
J
(
π
θ
)
J(\pi_{\theta})
J(πθ),我们将两侧梯度算子摘去:
J
(
π
θ
)
=
1
N
∑
τ
∑
t
l
o
g
π
(
a
t
∣
s
t
)
R
(
τ
)
+
C
(
可令
C
=
0
)
J(\pi_{\theta})=\frac{1}{N}\sum_\tau\sum_tlog\space\pi(a_t|s_t)R(\tau)+C(可令 C=0)
J(πθ)=N1τ∑t∑log π(at∣st)R(τ)+C(可令C=0)
我们考虑单条轨迹
τ
\tau
τ其中单步
t
t
t,取负,改为最小化的目标:
J
(
π
θ
)
=
−
l
o
g
π
(
a
t
∣
s
t
)
R
(
τ
)
J(\pi_\theta)=-log\pi(a_t|s_t)R(\tau)
J(πθ)=−logπ(at∣st)R(τ)
四、算法实现
参数化后的策略网络应该如何输出动作呢?我们要求策略的输出一定满足该条件 ∑ π ( a ∣ s ) = 1 \sum\pi(a|s)=1 ∑π(a∣s)=1
- 如果环境的动作空间是离散的,那么策略网络的输出一般是离散的softmax分布
- 如果环境的动作空间是连续的,那么策略网络的输出一般是正态分布
我们将policy-based,on-policy下REINFORCE算法和value-based,off-policy DQN算法进行对比
class PolicyNet(torch.nn.Module):
def __init__(self,state_dim,hidden_dim,action_dim):
super(PolicyNet,self).__init__()
self.fc1 = torch.nn.Linear(state_dim,hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim,action_dim)
def forward(self,x):
x = F.relu(self.fc1(x))
return F.softmax(self.fc2(x),dim=1)
REINFORCE在输出actions时需要将最后一层进行softmax,最终输出对应每个动作的概率
class REINFORCE():
def __init__(self,state_dim,hidden_dim,action_dim,lr,gamma,device):
...
def take_action(self,state):
state = torch.tensor([state],dtype=torch.float).to(self.device)
probs = self.PolicyNet(state)
action_dist = torch.distributions.Categorical(probs) # 生成一个分布,用于采样,probs是概率
action = action_dist.sample()
return action.item()
DQN向网络传入state,输出当前states下所有动作的Q值,gather(1,action)可取出指定动作的Q值
REINFORCE向网络传入state,输出softmax后的概率,Categorical利用概率建立一个动作分布,sample从动作中抽样一个动作并返回
def update(self,transition_dict):
reward_list = transition_dict['rewards']
state_list = transition_dict['states']
action_list = transition_dict['actions']
G = 0
self.optimizer.zero_grad()
for i in reversed(range(len(reward_list))):
reward = reward_list[i]
state = torch.tensor([state_list[i]],dtype=torch.float).to(self.device)
action = torch.tensor([action_list[i]],dtype=torch.long).view(-1,1).to(self.device)
log_prob = torch.log(self.PolicyNet(state).gather(1,action))
G = reward + self.gamma * G
loss = -log_prob * G
loss.backward()
self.optimizer.step()
return loss
下面是最重要的update函数
DQN每个step均进行一次update,从经验池中抽样多个样本(s,a,s’,r) 。off-policy:这些样本不一定来自本次策略,每次step均更新Qnet,DQN的核心代码如下:
#从replybuffer中选择多组states。计算每个state不同action的Q值,选取指定actions的Q(s,a)
q_values = self.q_net(states).gather(1,actions)
#利用多组next_states,计算每个s'不同action的值,返回每组s'最大的Q(s,a)
max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1,1)
q_targets = rewards + self.gamma * max_next_q_values * (1 - dones)
#计算损失函数
dqn_loss = torch.mean(F.mse_loss(q_values,q_targets))
dqn_loss.backward()
self.optimizer.step()
def update(self,transition_dict):
reward_list = transition_dict['rewards']
state_list = transition_dict['states']
action_list = transition_dict['actions']
G = 0
self.optimizer.zero_grad()
for i in reversed(range(len(reward_list))):
reward = reward_list[i]
state = torch.tensor([state_list[i]],dtype=torch.float).to(self.device)
action = torch.tensor([action_list[i]],dtype=torch.long).view(-1,1).to(self.device)
log_prob = torch.log(self.PolicyNet(state).gather(1,action)) #计算state采取该已知动作的概率,取对数
G = self.gamma * G + reward #计算奖励
loss = -log_prob * G #计算loss
loss.backward()
self.optimizer.step()
return loss
REINFORCE一条轨迹结束进行一次update,使用整条轨迹的全部样本(s,a,s’,r)。on-policy:这些样本均来自本次策略。
由于一条轨迹的整个样本都能收集,我们可以从最终状态开始,累加计算reward即
R
(
τ
)
R(\tau)
R(τ) ,将一条轨迹上的损失函数反向传播结束后,更新一次参数。单个元组计算的
Δ
J
\Delta J
ΔJ如下
J
(
π
θ
)
=
−
l
o
g
π
(
a
t
∣
s
t
)
R
(
τ
)
J(\pi_\theta)=-log\pi(a_t|s_t)R(\tau)
J(πθ)=−logπ(at∣st)R(τ)
五、REINFORCE改进
上文提到,REINFORCE算法方差太大,训练不稳定,下面根据主要原因分别进行优化
-
相近策略采样轨迹不同:随机性策略,相近策略动作可能不同,对奖励的影响较大,所以计算 J ( π θ ) = 1 N ∑ τ ∑ t l o g π ( a t ∣ s t ) R ( τ ) J(\pi_{\theta})=\frac{1}{N}\sum_\tau\sum_tlog\space\pi(a_t|s_t)R(\tau) J(πθ)=N1∑τ∑tlog π(at∣st)R(τ)时,主要影响因素在于 R ( τ ) R(\tau) R(τ)
有多种方法对 R ( τ ) R(\tau) R(τ)进行优化:
- 基线:我们希望 l o g π log\pi logπ和 R ( τ ) R(\tau) R(τ)的大小在同一个数量级,使得奖励的变化对整体数值的影响减小,因此考虑将其减去一个基线 b b b
- 修改:不考虑全程奖励 R ( τ ) R(\tau) R(τ),每次利用单个元组,计算Loss进行反向传播时,只计算该元组之后轨迹的奖励(上文的程序使用就是此方法)
-
蒙特卡洛方式更新:轨迹结束才进行更新,方差较大,考虑修改为时序差分更新——后续Actor-Critic借鉴了该思路