本专栏按照 https://lilianweng.github.io/lil-log/2018/04/08/policy-gradient-algorithms.html 顺序进行总结 。
文章目录
- 原理解析
-
- 策略梯度的直观解释
- Actor-Critic框架引出
- GAE
- 算法实现
-
- 算法流程
- 代码实现
原理解析
AC算法框架被广泛应用于实际强化学习算法中,该框架集成了值函数估计算法和策略搜索算法,是解决实际问题时最常考虑的框架。
AC算法起源于策略梯度算法,因此在介绍AC算法时,我们先从策略梯度入手。(其实上篇已经介绍过了,这里再加赘述一下)
策略梯度的直观解释
随机策略梯度的计算公式为:
利用经验平均估计策略的梯度:
下面对上式进行直观上的解释:
- 第一项:?θlogP(τ;θ)\nabla_\theta logP(\tau;\theta)?θ?logP(τ;θ) ,这是一个方向向量,而且其方向是: logP(τ;θ)logP(\tau;\theta)logP(τ;θ) ;对于参数 θ\thetaθ 变化最快的方向,参数在这个方向上更新可以增大或者降低 logP(τ;θ)logP(\tau;\theta)logP(τ;θ) ,也就是能增大或者降低轨迹 τ\tauτ 的概率 P(τ;θ)P(\tau;\theta)P(τ;θ);
- 第二项: R(τ)R(\tau)R(τ) ,这是一个标量,在策略梯度中扮演着向量 ?θlogP(τ;θ)\nabla_\theta logP(\tau;\theta)?θ?logP(τ;θ) 的幅值的角色, R(τ)R(\tau)R(τ) 越大,向量的幅值越大,轨迹 τ\tauτ 出现的概率 P(τ;θ)P(\tau;\theta)P(τ;θ) 在参数更新后会更大。因此,策略梯度的直观含义是增大高回报轨迹的概率,降低低回报轨迹的概率。
Actor-Critic框架引出
Actor-Critic从名字上看包括两部分,演员(Actor)和评价者(Critic)。其中:
- Actor 使用 策略函数,负责生成动作(Action)并和环境交互。
- Critic使用 价值函数,负责评估Actor的表现,并指导Actor下一阶段的动作。
从策略梯度的直观解释我们可以看到,轨迹回报 R(τ)R(\tau)R(τ) 就像是一个评价器(Critic),该评价器(Critic)评价参数更新后,该轨迹出现的概率应该变大还是变小。如果变大,应该变大多少;如果减小,应该减小多少。也就是说,策略的参数调整幅度由轨迹回报 [公式] 进行评价。可以将 R(τ)R(\tau)R(τ) 进行推广而不影响策略梯度大小的计算。根据Shulman的博士论文,在保持策略梯度不变的情况下,策略梯度可写为:
ΨtΨ_tΨt? 可以是下列任何一个:
- ∑t=0∞rt\sum \limits_{t=0}^\infty r_tt=0∑∞?rt?:轨迹的总回报
- ∑t′=t∞rt′\sum \limits_{t'=t}^\infty r_{t'}t′=t∑∞?rt′?:动作后的回报
- ∑t′=t∞rt′?b(st)\sum \limits_{t'=t}^\infty r_{t'}-b(s_t)t′=t∑∞?rt′??b(st?):加入基线的形式
- Qπ(st,at)Q^{\pi}(s_t,a_t)Qπ(st?,at?): 状态-行为值函数
- Aπ(st,at)A^{\pi}(s_t,a_t)Aπ(st?,at?): 优势函数
- δ(t)=rt+Vπ(st+1)?Vπ(st)\delta(t) = r_t+V^{\pi}(s_{t+1})-V^{\pi}(s_{t})δ(t)=rt?+Vπ(st+1?)?Vπ(st?) 或者 rt+Q(St+1,At+1)?Q(St,At)r_t+Q(S_{t+1}, A_{t+1})-Q(S_{t}, A_{t})rt?+Q(St+1?,At+1?)?Q(St?,At?): TD残差
- δ(t)E(t)\delta(t)E(t)δ(t)E(t):TD(λ)误差是TD误差和效用迹E的乘积。
1—3:直接应用轨迹的回报累积回报,由此计算出来的策略梯度不存在偏差,但是由于需要累积多步的回报,因此方差会很大。
4—7: 利用动作值函数,优势函数和TD偏差代替累积回报,其优点是方差小,但是这三种方法中都用到了逼近方法,因此计算出来的策略梯度都存在偏差。这三种方法以牺牲偏差来换取小的方差。当 ΨtΨ_tΨt? 取4—6时,为经典的AC方法。
在式(1.3)中,πθ(a∣s)\pi_\theta(a|s)πθ?(a∣s) 为Actor, ΨtΨ_tΨt? 称为Critic,因此(1.3)式是一个广义的AC框架。
Actor为策略函数,经常用神经网络来表示,因此称为策略网络。
Critic为评价函数,对于大部分问题,ΨtΨ_tΨt? 也常常用神经网络进行逼近, ωωω 它的参数常用表示,因此Critic又称为评价网络。
当 ΨtΨ_tΨt? 取TD残差,并且值函数 Vπ(st)V^{\pi}(s_{t})Vπ(st?) 由参数为 ωωω 的神经网络进行逼近时。AC算法的更新步骤为:
- 值函数网络的更新:
- 策略网络部分的更新:
为了充分利用ac方法可以减小策略梯度的方差,同时弥补普通的ac算法中策略梯度存在较大偏差的缺点,Shulman在博士论文中提出一种GAE的方法。
GAE
GAE的方法是对优势函数进行估计。优势函数的定义为:
一般来说, ΨtΨ_tΨt? 取优势函数时,比取值函数 Q(st,at)Q(s_t,a_t)Q(st?,at?) 时计算得到的策略梯度,方差要小,收敛速度要快。
对于这个结论,我们可以从两个方面去理解:
- 第一: 可以将值函数看成是策略梯度算法中的最优基线函数。基线函数的引入可以减小策略梯度的方差。
- 第二:从更直观的角度去理解:在直观解释中,“ R(τ)R({\tau})R(τ) 增大高回报轨迹的概率,降低低回报轨迹的概率。” 当采用优势函数时,这里的优势函数 AπA^\piAπ 相当于 R(τ)R({\tau})R(τ) ,优势函数是动作值函数相对于值函数的优势,若动作值函数比值函数大,那么优势函数为正;若动作值函数比值函数小,那么优势函数为负。
- 对于策略梯度而言,优势函数为正时,其幅值为正,则参数沿着使得该轨迹概率增大的方向更新;优势函数为负时,策略梯度的幅值为负,则参数沿着使得该轨迹减小的方向更新。因此,采用优势函数时,算法的收敛速度更快。
然而,根据优势函数定义,优势函数中的值函数常常利用逼近算法近似计算,因此往往会引入偏差。
优势函数的一步估计可写为:
从优势函数的一步估计中我们看到,V(st)V(s_t)V(st?) 和 V(st+1)V(s_{t+1})V(st+1?) 的真实值都是未知的,而是用到了估计值,因此优势函数存在偏差。
GAE的方法是改进对优势函数的估计,将偏差控制到一定的范围内。其方法是对优势函数进行多步估计,并将这些多步估计利用衰减因子进行组合。具体是这样做的:
优势函数的2步估计及无穷步估计分别为:
从上面的公式我们看到,kkk 越大,产生偏差的项 γkV(st+k)\gamma^kV(s_{t+k})γkV(st+k?) 越小,因此优势函数的估计偏差越小。但,相应的方差也会变大。
Shulman 提出广义优势函数估计 GAE(γ,λ)GAE(\gamma, \lambda)GAE(γ,λ),利用指数加权平均从1步到无穷步的优势函数估计,即:
GAE算法可以从回报shapping的角度进行理解和解释。
在回报shapping中,定义转换回报函数为:
令 Φ=V\Phi = VΦ=V ,则转换回报函数的累积回报可写为:
也就是说,广义优势函数可以看成是回报为转换回报,折扣因子为 γλ\gamma\lambdaγλ 的折扣累积回报。因此, A^tGAE(γ,λ)\hat A_t^{GAE(\gamma,\lambda)}A^tGAE(γ,λ)? 很好地平衡了偏差和方差。讲了那么多,其实是为了说明,利用代替AC方法中的Critic会产生更好的效果。
GAE通过利用广义优势函数来平衡Critic的偏差和方差;
算法实现
算法流程
给一个Actor-Critic算法的流程总结,评估点基于TD误差,Critic使用神经网络来计算TD误差并更新网络参数,Actor也使用神经网络来更新网络参数:
算法输入:迭代轮数 T
,状态特征维度 n
, 动作集A
, 步长α,β
,衰减因子γ
, 探索率?
, Critic网络结构和Actor网络结构。
输出:Actor 网络参数θ
, Critic网络参数w
- 随机初始化所有的状态和动作对应的价值
Q
- for i from 1 to T,进行迭代。
a) 初始化S
为当前状态序列的第一个状态, 拿到其特征向量?(S)
b) 在Actor网络中使用?(S)
作为输入,输出动作A
,基于动作A
得到新的状态S′
,反馈R
。
c) 在 Critic 网络中分别使用?(S),?(S′)
作为输入,得到Q
值输出V(S),V(S′)
d) 计算TD误差δ=R+γV(S′)?V(S)
e) 使用均方差损失函数 ∑(R+γV(S′)?V(S,w))2∑(R+γV(S′)?V(S,w))^2∑(R+γV(S′)?V(S,w))2 作Critic网络参数w
的梯度更新
f) 更新Actor网络参数θ
: θ=θ+α?θlogπθ(St,A)δθ=θ+α?_θlogπ_θ(S_t,A)δθ=θ+α?θ?logπθ?(St?,A)δ
对于Actor的分值函数 ?θlogπθ(St,A)?_θlogπ_θ(S_t,A)?θ?logπθ?(St?,A), 可以选择softmax或者高斯分值函数。
上述Actor-Critic算法已经是一个很好的算法框架,但是离实际应用还比较远。主要原因是这里有两个神经网络,都需要梯度更新,而且互相依赖,难收敛。但是了解这个算法过程后,其他基于Actor-Critic的算法就好理解了。
目前改进的比较好的有两个经典算法,一个是DDPG算法,使用了双Actor神经网络和双Critic神经网络的方法来改善收敛性。这个方法我们在从DQN到Nature DQN的过程中已经用过一次了。另一个是A3C算法,使用了多线程的方式,一个主线程负责更新Actor和Critic的参数,多个辅线程负责分别和环境交互,得到梯度更新值,汇总更新主线程的参数。而所有的辅线程会定期从主线程更新网络参数。这些辅线程起到了类似DQN中经验回放的作用,但是效果更好。
以上解析来自于:https://www.cnblogs.com/pinard/p/10272023.html
代码实现
import numpy as np
import tensorflow as tf
import gym
import pandas as pdimport pdbOUTPUT_GRAPH = False
MAX_EPISODE = 500
DISPLAY_REWARD_THRESHOLD = 200 # renders environment if total episode reward is greater then this threshold
MAX_EP_STEPS = 2000 # maximum time step in one episode
RENDER = False # rendering wastes time
GAMMA = 0.9 # reward discount in TD error
LR_A = 0.001 # learning rate for actor
LR_C = 0.001 # learning rate for criticclass Actor(object):def __init__(self, sess, n_features, n_actions, lr=0.001):self.sess = sessself.s = tf.placeholder(tf.float32, [1, n_features], "state")self.a = tf.placeholder(tf.int32, None, "action")self.q = tf.placeholder(tf.float32, None, "q") # TD_errorwith tf.variable_scope('Actor'):l1 = tf.layers.dense(inputs=self.s,units=20, # number of hidden unitsactivation=tf.nn.relu,kernel_initializer=tf.random_normal_initializer(0., .1), # weightsbias_initializer=tf.constant_initializer(0.1), # biasesname='l1')self.acts_prob = tf.layers.dense(inputs=l1,units=n_actions, # output unitsactivation=tf.nn.softmax, # get action probabilitieskernel_initializer=tf.random_normal_initializer(0., .1), # weightsbias_initializer=tf.constant_initializer(0.1), # biasesname='acts_prob')with tf.variable_scope('exp_v'):pdb.set_trace()log_prob = tf.log(self.acts_prob[0, self.a]) # <tf.Tensor 'Actor/acts_prob/Softmax:0' shape=(1, 2) dtype=float32>self.exp_v = tf.reduce_mean(log_prob * self.q) # advantage (TD_error) guided losswith tf.variable_scope('train'):self.train_op = tf.train.AdamOptimizer(lr).minimize(-self.exp_v) # minimize(-exp_v) = maximize(exp_v)def learn(self, s, a, q):# s: array([ 0.03073904, 0.00145001, -0.03088818, -0.03131252]) # shape:(4,)# a: 0 ; q: array([[0.11351492]], dtype=float32) # shape:(1,1)s = s[np.newaxis, :]# s: array([[ 0.03073904, 0.00145001, -0.03088818, -0.03131252]]) # shape:(1,4)feed_dict = {
self.s: s, self.a: a, self.q: q}_, exp_v = self.sess.run([self.train_op, self.exp_v], feed_dict)return exp_v # -0.081048675def choose_action(self, s):# s: array([ 0.03073904, 0.00145001, -0.03088818, -0.03131252]) # shape:(4,)s = s[np.newaxis, :]# s: array([[ 0.03073904, 0.00145001, -0.03088818, -0.03131252]]) # shape:(1,4)probs = self.sess.run(self.acts_prob, {
self.s: s}) # get probabilities for all actions# array([[0.48727563, 0.51272434]], dtype=float32) # shape:(1, 2)return np.random.choice(np.arange(probs.shape[1]), p=probs.ravel()) # 表示从[0,1]中依概率p=[0.48, 0.52] 随机选择0或者 1 ;# return a int # 返回值是 0 或者 1class Critic(object):def __init__(self, sess, n_features, n_actions, lr=0.01):self.sess = sessself.s = tf.placeholder(tf.float32, [None, n_features], "state")self.a = tf.placeholder(tf.int32,[None, 1],"action")self.r = tf.placeholder(tf.float32, None, 'r')self.q_ = tf.placeholder(tf.float32,[None,1],'q_next')self.a_onehot = tf.one_hot(self.a, n_actions, dtype=tf.float32)self.a_onehot = tf.squeeze(self.a_onehot,axis=1)self.input = tf.concat([self.s, self.a_onehot],axis=1)with tf.variable_scope('Critic'):l1 = tf.layers.dense(inputs=self.input,units=20, # number of hidden unitsactivation=tf.nn.relu, # Nonekernel_initializer=tf.random_normal_initializer(0., .1), # weightsbias_initializer=tf.constant_initializer(0.1), # biasesname='l1')self.q = tf.layers.dense(inputs=l1,units=1, # output unitsactivation=None,kernel_initializer=tf.random_normal_initializer(0., .1), # weightsbias_initializer=tf.constant_initializer(0.1), # biasesname='Q')with tf.variable_scope('squared_TD_error'):self.td_error = self.r + GAMMA * self.q_ - self.qself.loss = tf.square(self.td_error) # TD_error = (r+gamma*V_next) - V_evalwith tf.variable_scope('train'):self.train_op = tf.train.AdamOptimizer(lr).minimize(self.loss)def learn(self, s, a, r, s_): # s:(4,) a:1 r:1.0 s_:(4,)s, s_ = s[np.newaxis, :], s_[np.newaxis, :] # 均为(1,4)next_a = [[i] for i in range(N_A)] # N_A=2s_ = np.tile(s_, [N_A, 1]) # (2,4) tile 复制N_A份q_ = self.sess.run(self.q, {
self.s: s_, self.a: next_a})q_ = np.max(q_, axis=0, keepdims=True)# pdb.set_trace()q, _ = self.sess.run([self.q, self.train_op],{
self.s: s, self.q_: q_, self.r: r, self.a: [[a]]})return q# action有两个,即向左或向右移动小车
# state是四维env = gym.make('CartPole-v0')
env.seed(1) # reproducible
env = env.unwrappedN_F = env.observation_space.shape[0]
N_A = env.action_space.nsess = tf.Session()actor = Actor(sess, n_features=N_F, n_actions=N_A, lr=LR_A)
critic = Critic(sess, n_features=N_F, n_actions=N_A, lr=LR_C)sess.run(tf.global_variables_initializer())res = []
for i_episode in range(MAX_EPISODE):s = env.reset() # 初始状态t = 0track_r = []while True:if RENDER: env.render()a = actor.choose_action(s)s_, r, done, info = env.step(a)if done: r = -20track_r.append(r)q = critic.learn(s, a, r, s_) # gradient = grad[r + gamma * V(s_) - V(s)]# pdb.set_trace()actor.learn(s, a, q) # true_gradient = grad[logPi(s,a) * td_error]s = s_t += 1if done or t >= MAX_EP_STEPS:ep_rs_sum = sum(track_r)if 'running_reward' not in globals():running_reward = ep_rs_sumelse:running_reward = running_reward * 0.95 + ep_rs_sum * 0.05if running_reward > DISPLAY_REWARD_THRESHOLD: RENDER = True # renderingprint("episode:", i_episode, " reward:", int(running_reward))res.append([i_episode, running_reward])breakpd.DataFrame(res, columns=['episode','ac_reward']).to_csv('../ac_reward.csv')