策略梯度(Policy Gradient)算法目标函数的梯度更新公式为:
▽ R ˉ θ = 1 N ∑ n = 1 N ∑ t = 1 T n ( ∑ t ′ = t T n γ t ′ − t r t ′ n − b ) ▽ l o g p θ ( a t n ∣ s t n ) (1) \bigtriangledown \bar{R}_{\theta } = \frac{1}{N}\sum_{n=1}^{N}\sum_{t=1}^{T_{n}}(\sum_{{t}'=t}^{T_{n}}\gamma ^{{t}'-t}r_{{t}'}^{n}-b)\bigtriangledown logp_{\theta }(a_{t}^{n}|s_{t}^{n})\tag{1} ▽Rˉθ=N1n=1∑Nt=1∑Tn(t′=t∑Tnγt′−trt′n−b)▽logpθ(atn∣stn)(1)
公式(1)各个参数详解: N N N表示采样的样本个数,因为我们要数值逼近奖励的期望; γ \gamma γ 表示折扣因子,指actor对未来奖励的重视程度,越接近1越重视未来各个时间步的奖励,越接近0越不重视; r t ′ n r_{{t}'}^{n} rt′n 表示第 n n n条采样样本第 t ′ {t}' t′ 时刻的奖励; b b b 表示baseline,加了baseline之后,奖励就会有正有负,负奖励对应的动作的概率就会在优化之后下降,而正的奖励对应的动作的概率就会增加,也就是baseline起到了不能将所有采样得到的动作对应的概率都增大,这是因为采样得到的动作未必是好的动作。 p θ ( a t n ∣ s t n ) p_{\theta }(a_{t}^{n}|s_{t}^{n}) pθ(atn∣stn) 表示给定状态 s t n s_{t}^{n} stn时采取动作 a t n a_{t}^{n} atn 的概率。
现在我们取
G t n = ∑ t ′ = t T n γ t ′ − t r t ′ n (2) G_{t}^{n} = \sum_{{t}'=t}^{T_{n}}\gamma ^{{t}'-t}r_{{t}'}^{n}\tag{2} Gtn=t′=t∑Tnγt′−trt′n(2)
我们知道无论是环境还是actor本身都具有随机性(比如我们生活的现实世界里面,环境的变化是不可测的,也就是随机性很大),因此如果不采样足够多的数据,我们很难较为准确的逼近奖励的期望,但是数据太多,就会给计算资源带来负担。因次 G t n G_{t}^{n} Gtn是一个非常不稳定的随机变量,也就方差很大,这样训练的模型的效果也会很差。只有足够很多的episode样本我们才可以更加准确的来数值逼近奖励的期望。如果还是不明白,可以参考我的策略梯度详解这篇博客。
上面我们引入了 G t n G_{t}^{n} Gtn十分不稳定这个问题,那么我们该如何解决这个问题呢?
问题:我们是否可以使用一个神经网络来估计这个 G t n G_{t}^{n} Gtn这个值?
答:可以,我们使用强化学习的值函数方法。
下面先来介绍两个值函数以及他们之间的关系。
由公式(2)我们有
G t n = ∑ t ′ = t T n γ t ′ − t r t ′ n (2) G_{t}^{n} = \sum_{{t}'=t}^{T_{n}}\gamma ^{{t}'-t}r_{{t}'}^{n}\tag{2} Gtn=t′=t∑Tnγt′−trt′n(2)
我们根据上面的两个值函数可以得到 G t n G_{t}^{n} Gtn的期望是:
E [ G t n ] = Q π ( s t n , a t n ) (4) E[G_{t}^{n}] = Q^{\pi}(s_{t}^{n},a_{t}^{n})\tag{4} E[Gtn]=Qπ(stn,atn)(4)
公式(3)的解释:为什么会成立?
由公式(2)可知, G t n G_{t}^{n} Gtn是奖励的累加和,因为奖励是actor观测到某个状态之后由环境反馈过来的,因次这和状态动作价值函数是一样的。主要是因为actor得到的奖励都是在确定所采取的动作之后才得到的,而状态动作价值函数 Q π ( s , a ) Q^{\pi}(s,a) Qπ(s,a)也是在actor观测到状态 s s s采取动作 a a a之后所获得奖励的期望。因此公式(3)成立。
我们可以使用 V π θ ( s t n ) V^{\pi_{\theta}}(s_{t}^{n}) Vπθ(stn) 来估计公式(1)中的baseline b b b,也就是 b = V π θ ( s t n ) (5) b = V^{\pi_{\theta}}(s_{t}^{n})\tag{5} b=Vπθ(stn)(5)
将公式(1),(4),(5)结合起来于是公式(1)就变为下面的公式(6).
▽ R ˉ θ = 1 N ∑ n = 1 N ∑ t = 1 T n ( Q π ( s t n , a t n ) − V π θ ( s t n ) ) ▽ l o g p θ ( a t n ∣ s t n ) (6) \bigtriangledown \bar{R}_{\theta } = \frac{1}{N}\sum_{n=1}^{N}\sum_{t=1}^{T_{n}}(Q^{\pi}(s_{t}^{n},a_{t}^{n})-V^{\pi_{\theta}}(s_{t}^{n}))\bigtriangledown logp_{\theta }(a_{t}^{n}|s_{t}^{n})\tag{6} ▽Rˉθ=N1n=1∑Nt=1∑Tn(Qπ(stn,atn)−Vπθ(stn))▽logpθ(atn∣stn)(6)
但是公式(6)会引入一个问题,我们使用神经网络来估计 Q π ( s t n , a t n ) Q^{\pi}(s_{t}^{n},a_{t}^{n}) Qπ(stn,atn) 和 V π θ ( s t n ) V^{\pi_{\theta}}(s_{t}^{n}) Vπθ(stn),但是我们可以看到这两个函数的自变量是不一样的,也就是如果使用公式(6)来作为梯度更新的公式的话,这会引入两个神经网络,那么模型的复杂度也就会增加,复杂的模型会导致一系列问题,比如过拟合,两个值函数估测不准的可能性也会增加等等。
问题:我们是否可以只引入一个神经网络来解决这个问题?
答案是显然的,因为公式(6)里面有两个值函数,上面的(3)描述了这两个值函数之间的关系,我们要使用公式(3)来简化问题。
由公式(3)我们有 Q π ( s , a ) = E [ r + V π ( s ′ ) ] (3) Q^{\pi}(s,a)=E[r + V^{\pi}({s}')]\tag{3} Qπ(s,a)=E[r+Vπ(s′)](3)
公式(3)只是提供了理论上的可能性,但是计算期望是一件很麻烦的事情,而且在无穷多的情况下,计算机在实操时根本无法计算,那么我们就需要简化问题,于是有 Q π ( s , a ) = r + V π ( s ′ ) (7) Q^{\pi}(s,a)=r + V^{\pi}({s}')\tag{7} Qπ(s,a)=r+Vπ(s′)(7)
公式解释:状态 s ′ {s}' s′ 是actor观测到状态 s s s 时采取动作 a a a之后,状态转移到了状态 s ′ {s}' s′,这种状态的转移可能是由于actor所采取的动作导致的,也可能是环境本身自己发生了变化而发生了转移。
将公式(7)带入到公式(6),于是就有:
▽ R ˉ θ = 1 N ∑ n = 1 N ∑ t = 1 T n r t n + V π θ ( s t + 1 n ) − V π θ ( s t n ) ▽ l o g p θ ( a t n ∣ s t n ) (8) \bigtriangledown \bar{R}_{\theta } = \frac{1}{N}\sum_{n=1}^{N}\sum_{t=1}^{T_{n}}r_{t}^{n}+V^{\pi_{\theta}}(s_{t+1}^{n})-V^{\pi_{\theta}}(s_{t}^{n})\bigtriangledown logp_{\theta }(a_{t}^{n}|s_{t}^{n})\tag{8} ▽Rˉθ=N1n=1∑Nt=1∑Tnrtn+Vπθ(st+1n)−Vπθ(stn)▽logpθ(atn∣stn)(8)
公式(8)解释:虽然上面是说使用 V π θ ( s t n ) V^{\pi_{\theta}}(s_{t}^{n}) Vπθ(stn)来作为baseline,但是我更加觉得这个是在使用 V π θ ( s t + 1 n ) − V π θ ( s t n ) V^{\pi_{\theta}}(s_{t+1}^{n})-V^{\pi_{\theta}}(s_{t}^{n}) Vπθ(st+1n)−Vπθ(stn)来逼近环境给出的奖励 r t n r_{t}^{n} rtn.我对这个公式的直观理解有点描述不出来,感觉和TD方法的思想有点类,从公式可以看出,计算这个梯度只需要使用 ( s t n , a t n , r t n , s t + 1 n ) (s_{t}^{n},a_{t}^{n},r_{t}^{n},s_{t+1}^{n}) (stn,atn,rtn,st+1n)这样的四元组,因为我们在采样数据的时候只需要存储这样的四元组就可以了,和TD方法的思想是一样的,我觉得是可以实现单步更新,无需等待一个回合结束再更新模型的。我们知道 r t n r_{t}^{n} rtn是一个random variable(r.v.),因此公式里面还是引入了具有随机性的东西,只有在考虑的期望的时候才是正确的,但是 r t n r_{t}^{n} rtn的随机性相对于 G t n G_{t}^{n} Gtn的随机性是小了很多,因为 G t n G_{t}^{n} Gtn是各个时间步奖励的和,因为相对来说,随机性不大。
也就是多个actor和环境互动,并行更新网络
最上面的那个网络我们认为是global network,然后将他的参数复制,弄出多个网络,然后数据,计算梯度,更新global network。
算法流程:
1、复制网络参数生成多个网络,放在不同的GPU上
2、每个网络都和环境互动,生成数据
3、计算梯度
4、更新global network
import random
import numpy as np
import gym
import torch.nn as nn
import torch as t
from torch.nn import functional as F
import matplotlib.pyplot as plt
import os
# env.close()
eplison = 0.1
criterion = nn.CrossEntropyLoss()
os.environ["KMP_DUPLICATE_LIB_OK"]="TRUE"
gamma = 0.9
actor_lr = 0.001
critic_lr = 0.01
env = gym.make("CartPole-v0")
env.seed(1) # reproducible, general Policy gradient has high variance
env = env.unwrapped
batch_size = 1
epochs = 500
class Share_layer(nn.Module):
def __init__(self):
super(Share_layer, self).__init__()
self.linear1 = nn.Linear(4, 20)
nn.init.normal_(self.linear1.weight, 0, 0.1)
nn.init.constant_(self.linear1.bias, 0.1)
def forward(self, out):
out = self.linear1(out)
out = F.relu(out)
return out
class Actor(nn.Module):
def __init__(self, sl):
super(Actor, self).__init__()
self.share_layer = sl
self.linear2 = nn.Linear(20, 2)
nn.init.normal_(self.linear2.weight, 0, 0.1)
nn.init.constant_(self.linear2.bias, 0.1)
def forward(self, x):
out = t.from_numpy(x).float()
out = self.share_layer(out)
out = self.linear2(out)
prob = F.softmax(out, dim = 1) #这个输出主要是用来使用概率来挑选动作
return prob, out
class Critic(nn.Module):
def __init__(self, sl):
super(Critic, self).__init__()
self.share_layer = sl
self.linear2 = nn.Linear(20, 1)
nn.init.normal_(self.linear2.weight, 0, 0.1)
nn.init.constant_(self.linear2.bias, 0.1)
def forward(self, x):
out = t.from_numpy(x).float()
out = self.share_layer(out)
out = self.linear2(out)
return out
#下面该函数用来选择动作
def choose_action(prob):
# print(prob)
action = np.random.choice(a = 2, p = prob[0].detach().numpy())
return action
# action = np.random.choice(a = 6, p = prob[0].detach().numpy())
# v = random.uniform(0, 1)
# p, index = t.topk(prob, 1, dim = 1)
# #下面开始eplison-greedy 算法
# if v > eplison:
# #这里是求最大的状态价值函数对应的动作
# action = index[0][0].item()
# else:
# #下面是随机产生动作
# action = random.randint(0, 1)
# return action
#下面的函数主要用来计算Actor部分训练的损失函数
def Actor_learn(optim, critic, s, s_, a, r, logits):
'''
根据当前的状态,奖励以及下一个时间步的章台完成损失函数的计算
Parameters
----------
critic : Critic()实例
用来估计当前时间的奖励的期望
s : float array
当前时间步的状态.
a : int scalar
当前时间步根据当前状态s所采取的动作
r : float scalar
当前时间步的奖励.
s_ : float array
下一个时间步的状态.
logits : actor网络最后一层的输出
用来计算交叉熵损失
Returns
-------
Actor 的损失.
'''
V_s = critic(s)
V_s_ = critic(s_)
#开始计算动作对应的交叉熵
a = t.tensor([a]).long()
logp_a = criterion(logits, a)
l = r + V_s_ - V_s
l = l.item()
loss = l * logp_a
# print('Actor loss is :', loss)
optim.zero_grad()
loss.backward()
optim.step()
def Critic_learn(optim, critic, s, r, s_):
'''
用来计算Critic网络的损失,来更新Critic网络
Parameters
----------
critic : Critic实例
用来计算各个时间步的值函数.
s : float array
当前时间步的状态.
r : float scalar
当前时间步的奖励.
s_ : float array
下一个时间步的状态.
Returns
-------
Critic网络的损失.
'''
V_s = critic(s)
V_s_ = critic(s_)
loss = (r + gamma * V_s_.item()- V_s)**2
optim.zero_grad()
loss.backward()
optim.step()
return r + gamma * V_s_.item()- V_s
def learn():
#下面采用时间差分方法学习,该方法的学习速度较快,且很稳,时间差分方法和蒙特卡洛方法各有自己的优势
sl = Share_layer()
actor = Actor(sl)
critic = Critic(sl)
actor.train()
critic.train()
#还需要两个优化器
actor_optim = t.optim.Adam(actor.parameters(), lr = actor_lr)
critic_optim = t.optim.Adam(critic.parameters(), lr = critic_lr)
train_rewards = []
for i in range(epochs):
state = env.reset()
done = False
sum_rewards_i = 0
step = 0
while not done:
step += 1
env.render()
state = np.expand_dims(state, axis = 0)
prob, logits = actor(state)
#下面开始选择动作
action = choose_action(prob)
state_, reward, done, info = env.step(action)
if done:
reward = -20.0
sum_rewards_i += reward
#下面开始学习,先学习的是critic网络,接着才是actor网络
l = Critic_learn(critic_optim, critic, state, reward, state_)
Actor_learn(actor_optim, critic, state, state_, action, reward, logits)
state = state_
train_rewards.append(sum_rewards_i)
print('epoch is :', i)
print('step nums is :', step)
plt.plot(train_rewards)
if __name__ == "__main__":
learn()
env.close()
参考内容主要是李宏毅老师的强化学习课程和莫烦python的代码