RL策略梯度方法之(二): Actor-Critic算法

本专栏按照 https://lilianweng.github.io/lil-log/2018/04/08/policy-gradient-algorithms.html 顺序进行总结 。


文章目录

  • 原理解析
    • 策略梯度的直观解释
    • Actor-Critic框架引出
    • GAE
  • 算法实现
    • 算法流程
    • 代码实现


原理解析

AC算法框架被广泛应用于实际强化学习算法中,该框架集成了值函数估计算法和策略搜索算法,是解决实际问题时最常考虑的框架。

AC算法起源于策略梯度算法,因此在介绍AC算法时,我们先从策略梯度入手。(其实上篇已经介绍过了,这里再加赘述一下)

策略梯度的直观解释

随机策略梯度的计算公式为:
在这里插入图片描述
利用经验平均估计策略的梯度:
在这里插入图片描述
下面对上式进行直观上的解释:

  • 第一项 ∇ θ l o g P ( τ ; θ ) \nabla_\theta logP(\tau;\theta) θlogP(τ;θ) ,这是一个方向向量,而且其方向是: l o g P ( τ ; θ ) logP(\tau;\theta) logP(τ;θ) ;对于参数 θ \theta θ 变化最快的方向,参数在这个方向上更新可以增大或者降低 l o g P ( τ ; θ ) logP(\tau;\theta) logP(τ;θ) ,也就是能增大或者降低轨迹 τ \tau τ 的概率 P ( τ ; θ ) P(\tau;\theta) P(τ;θ)
  • 第二项 R ( τ ) R(\tau) R(τ) ,这是一个标量,在策略梯度中扮演着向量 ∇ θ l o g P ( τ ; θ ) \nabla_\theta logP(\tau;\theta) θlogP(τ;θ)幅值的角色, R ( τ ) R(\tau) R(τ) 越大,向量的幅值越大,轨迹 τ \tau τ 出现的概率 P ( τ ; θ ) P(\tau;\theta) P(τ;θ) 在参数更新后会更大。因此,策略梯度的直观含义是增大高回报轨迹的概率,降低低回报轨迹的概率
    RL策略梯度方法之(二): Actor-Critic算法_第1张图片

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 可以是下列任何一个:

  1. ∑ t = 0 ∞ r t \sum \limits_{t=0}^\infty r_t t=0rt:轨迹的总回报
  2. ∑ t ′ = t ∞ r t ′ \sum \limits_{t'=t}^\infty r_{t'} t=trt:动作后的回报
  3. ∑ t ′ = t ∞ r t ′ − b ( s t ) \sum \limits_{t'=t}^\infty r_{t'}-b(s_t) t=trtb(st):加入基线的形式
  4. Q π ( s t , a t ) Q^{\pi}(s_t,a_t) Qπ(st,at): 状态-行为值函数
  5. A π ( s t , a t ) A^{\pi}(s_t,a_t) Aπ(st,at): 优势函数
  6. δ ( t ) = r t + V π ( s t + 1 ) − V π ( s t ) \delta(t) = r_t+V^{\pi}(s_{t+1})-V^{\pi}(s_{t}) δ(t)=rt+Vπ(st+1)Vπ(st) 或者 r t + Q ( S t + 1 , A t + 1 ) − Q ( S t , A t ) r_t+Q(S_{t+1}, A_{t+1})-Q(S_{t}, A_{t}) rt+Q(St+1,At+1)Q(St,At): TD残差
  7. δ ( 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) πθ(as) 为Actor, Ψ t Ψ_t Ψt 称为Critic,因此(1.3)式是一个广义的AC框架。

Actor为策略函数,经常用神经网络来表示,因此称为策略网络。

Critic为评价函数,对于大部分问题, Ψ t Ψ_t Ψt 也常常用神经网络进行逼近, ω ω ω 它的参数常用表示,因此Critic又称为评价网络。

Ψ t Ψ_t Ψt 取TD残差,并且值函数 V π ( s t ) V^{\pi}(s_{t}) Vπ(st) 由参数为 ω ω ω 的神经网络进行逼近时。AC算法的更新步骤为:

  • 值函数网络的更新:
    在这里插入图片描述
  • 策略网络部分的更新:
    在这里插入图片描述

为了充分利用ac方法可以减小策略梯度的方差,同时弥补普通的ac算法中策略梯度存在较大偏差的缺点,Shulman在博士论文中提出一种GAE的方法。

GAE

GAE的方法是对优势函数进行估计。优势函数的定义为:
在这里插入图片描述
一般来说, Ψ t Ψ_t Ψt 取优势函数时,比取值函数 Q ( s t , a t ) Q(s_t,a_t) Q(st,at) 时计算得到的策略梯度,方差要小,收敛速度要快。

对于这个结论,我们可以从两个方面去理解:

  • 第一: 可以将值函数看成是策略梯度算法中的最优基线函数。基线函数的引入可以减小策略梯度的方差。
  • 第二:从更直观的角度去理解:在直观解释中,“ R ( τ ) R({\tau}) R(τ) 增大高回报轨迹的概率,降低低回报轨迹的概率。” 当采用优势函数时,这里的优势函数 A π A^\pi Aπ 相当于 R ( τ ) R({\tau}) R(τ)优势函数是动作值函数相对于值函数的优势,若动作值函数比值函数,那么优势函数为;若动作值函数比值函数小,那么优势函数为负。
    • 对于策略梯度而言,优势函数为时,其幅值为正,则参数沿着使得该轨迹概率增大的方向更新;优势函数为负时,策略梯度的幅值为负,则参数沿着使得该轨迹减小的方向更新。因此,采用优势函数时,算法的收敛速度更快。

然而,根据优势函数定义,优势函数中的值函数常常利用逼近算法近似计算,因此往往会引入偏差。

优势函数的一步估计可写为:
在这里插入图片描述
从优势函数的一步估计中我们看到, V ( s t ) V(s_t) V(st) V ( s t + 1 ) V(s_{t+1}) V(st+1) 的真实值都是未知的,而是用到了估计值,因此优势函数存在偏差。

GAE的方法是改进对优势函数的估计,将偏差控制到一定的范围内。其方法是对优势函数进行多步估计并将这些多步估计利用衰减因子进行组合。具体是这样做的:

优势函数的2步估计及无穷步估计分别为:

RL策略梯度方法之(二): Actor-Critic算法_第2张图片
从上面的公式我们看到, k k k 越大,产生偏差的项 γ k V ( s t + k ) \gamma^kV(s_{t+k}) γkV(st+k),因此优势函数的估计偏差越小。但,相应的方差也会变大

Shulman 提出广义优势函数估计 G A E ( γ , λ ) GAE(\gamma, \lambda) GAE(γ,λ)利用指数加权平均从1步到无穷步的优势函数估计,即:
RL策略梯度方法之(二): Actor-Critic算法_第3张图片
GAE算法可以从回报shapping的角度进行理解和解释。

在回报shapping中,定义转换回报函数为:
在这里插入图片描述
Φ = V \Phi = V Φ=V ,则转换回报函数的累积回报可写为:
在这里插入图片描述
也就是说,广义优势函数可以看成是回报为转换回报,折扣因子为 γ λ \gamma\lambda γλ 的折扣累积回报。因此, A ^ t G A E ( γ , λ ) \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

  1. 随机初始化所有的状态和动作对应的价值 Q
  2. 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网络参数 θ: θ = θ + α ∇ θ l o g π θ ( S t , A ) δ θ=θ+α∇_θlogπ_θ(S_t,A)δ θ=θ+αθlogπθ(St,A)δ

对于Actor的分值函数 ∇ θ l o g π θ ( S t , 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 pd

import pdb

OUTPUT_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 critic


class Actor(object):
    def __init__(self, sess, n_features, n_actions, lr=0.001):
        self.sess = sess

        self.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_error

        with tf.variable_scope('Actor'):
            l1 = tf.layers.dense(
                inputs=self.s,
                units=20,  # number of hidden units
                activation=tf.nn.relu,
                kernel_initializer=tf.random_normal_initializer(0., .1),  # weights
                bias_initializer=tf.constant_initializer(0.1),  # biases
                name='l1'
            )

            self.acts_prob = tf.layers.dense(
                inputs=l1,
                units=n_actions,  # output units
                activation=tf.nn.softmax,  # get action probabilities
                kernel_initializer=tf.random_normal_initializer(0., .1),  # weights
                bias_initializer=tf.constant_initializer(0.1),  # biases
                name='acts_prob'
            )

        with tf.variable_scope('exp_v'):
            pdb.set_trace()
            log_prob = tf.log(self.acts_prob[0, self.a])  # 
            self.exp_v = tf.reduce_mean(log_prob * self.q)  # advantage (TD_error) guided loss

        with 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.081048675

    def 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 或者 1


class Critic(object):
    def __init__(self, sess, n_features, n_actions, lr=0.01):
        self.sess = sess

        self.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 units
                activation=tf.nn.relu,  # None
                kernel_initializer=tf.random_normal_initializer(0., .1),  # weights
                bias_initializer=tf.constant_initializer(0.1),  # biases
                name='l1'
            )

            self.q = tf.layers.dense(
                inputs=l1,
                units=1,  # output units
                activation=None,
                kernel_initializer=tf.random_normal_initializer(0., .1),  # weights
                bias_initializer=tf.constant_initializer(0.1),  # biases
                name='Q'
            )

        with tf.variable_scope('squared_TD_error'):
            self.td_error = self.r + GAMMA * self.q_ - self.q
            self.loss = tf.square(self.td_error)  # TD_error = (r+gamma*V_next) - V_eval
        with 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=2
        s_ = 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.unwrapped

N_F = env.observation_space.shape[0]
N_A = env.action_space.n

sess = 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 = 0
    track_r = []
    while True:
        if RENDER: env.render()

        a = actor.choose_action(s)

        s_, r, done, info = env.step(a)

        if done: r = -20

        track_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 += 1

        if done or t >= MAX_EP_STEPS:
            ep_rs_sum = sum(track_r)

            if 'running_reward' not in globals():
                running_reward = ep_rs_sum
            else:
                running_reward = running_reward * 0.95 + ep_rs_sum * 0.05
            if running_reward > DISPLAY_REWARD_THRESHOLD: RENDER = True  # rendering
            print("episode:", i_episode, "  reward:", int(running_reward))
            res.append([i_episode, running_reward])

            break

pd.DataFrame(res, columns=['episode','ac_reward']).to_csv('../ac_reward.csv')


你可能感兴趣的:(RL,基础算法,强化学习)