第十二章 演員評論家(Actor-Critic)-強化學習理論學習與代碼實現(強化學習導論第二版)

在這裏插入圖片描述
獲取更多資訊,趕快關注上面的公衆號吧!

【強化學習系列】

第十二章 Actor-Critic演員評論家

我們在上一章中介紹了策略梯度(Policy Gradient)方法,並代碼練習了蒙特卡羅策略梯度reinforce算法。但是由於該算法需要完整的狀態序列,同時單獨對策略函數進行迭代更新,不太容易收斂。
在本章我們將討論一種將策略(Policy Based)和價值(Value Based)相結合的方法:Actor-Critic算法,在強化學習領域最受歡迎的A3C算法,DDPG算法,PPO算法等都是AC框架,所以AC重要性不言而喻。

12.1 Actor-Critic算法簡介

Actor-Critic從名字上看包括兩部分,演員(Actor)和評價家(Critic)。其中Actor使用的是上一章講到的策略函數,負責生成動作(Action)並和環境交互。而Critic使用我們之前講到了的價值函數,負責評估Actor的表現,並指導Actor下一階段的動作。

回想上一篇的策略梯度,策略函數就是我們的Actor,但是那裏是沒有Critic的,當時使用了蒙特卡羅法來計算每一步的價值部分替代了Critic的功能,但是場景比較受限。因此現在我們使用類似DQN中用的價值函數來替代蒙特卡羅法,作爲一個比較通用的Critic。

也就是說在Actor-Critic算法中,我們需要做兩組近似,第一組是策略函數的近似:
πθ(s,a)=P(as,θ)π(as)\pi_{\theta}(s,a) = P(a|s,\theta)\approx \pi(a|s)

第二組是價值函數的近似,對於狀態價值和動作價值函數分別是:
v^(s,w)vπ(s)\hat{v}(s, w) \approx v_{\pi}(s)
q^(s,a,w)qπ(s,a)\hat{q}(s,a,w) \approx q_{\pi}(s,a)

對於我們上一節講到的蒙特卡羅策略梯度reinforce算法,需要進行改造才能變成Actor-Critic算法,將會在下一節中詳細介紹。

首先,在蒙特卡羅策略梯度reinforce算法中,策略的參數更新公式是:
θ=θ+αθlogπθ(st,at)vt\theta = \theta + \alpha \nabla_{\theta}log \pi_{\theta}(s_t,a_t) v_t

梯度更新部分中,θlogπθ(st,at)\nabla_{\theta}log \pi_{\theta}(s_t,a_t)是得分函數,無需改變,要變成Actor的話改動的是vtv_t,這塊不能再使用蒙特卡羅法來得到,而應該從Critic得到。

而對於Critic來說,完全可以參考之前DQN的做法,即用一個Q網絡來做爲Critic, 這個Q網絡的輸入可以是狀態,而輸出是每個動作的價值或者最優動作的價值。

總體上來說,就是Critic通過Q網絡計算狀態的最優價值vt, 而Actor利用vtv_t這個最優價值迭代更新策略函數的參數θ,進而選擇動作,並得到反饋和新的狀態,Critic使用反饋和新的狀態更新Q網絡參數w, 在後面Critic會使用新的網絡參數w來幫Actor計算狀態的最優價值vtv_t

12.2 Actor-Critic框架引出

在上一章中我們已經得到策略梯度的更新公式如下:
θJ(θ)1Nn=1Nt=1Tn(t=tTnγttrtnb)logπθ(atnstn)\nabla_{\theta} J(\theta)\approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}\left(\sum_{t=t'}^{T_{n}} \gamma^{t'-t} r_{t'}^{n}-b\right) \nabla \log \pi_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)
這裏將t=tTnγttrtn\sum_{t=t'}^{T_{n}} \gamma^{t'-t} r_{t'}^{n}記爲GtnG_t^n,由於GtnG_t^n通過交互得到,其值非常不穩定(由於環境的動態性,GtnG_t^n本身也是一個分佈),方差會比較大,因此需要尋找減少方差的辦法。一種方法就是在上一章中採用的添加基線b, 這個b會使得GtbG_t-b的期望不變,但是方差會變小,常用的baseline函數就是V(st)V(s_t),在此基礎上,爲了進一步降低GtG_t的隨機性,我們用GtnG_t^n的期望E(Gtn)E(G_t^n)替代GtnG_t^n,這樣上面的更新公式變爲:
θJ(θ)1Nn=1Nt=1Tn(E(Gtn)V(stn))logπθ(atnstn)\nabla_{\theta} J(\theta)\approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}\left(E(G_t^n)-V(s_t^n)\right) \nabla \log \pi_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)

在根據Q學習部分(),可知期望E(Gtn)E(G_t^n)就是在狀態sts_t下執行動作ata_t,並遵循策略π\pi所能得到的Q值,即E(Gt)E(G_t)=Qπθ(stn,atn)Q^{\pi_{\theta} }\left(s^n_t,a^n_t\right),由此得到下式:
θJ(θ)1Nn=1Nt=1Tn(Qπθ(stn,atn)V(stn))logπθ(atnstn)\nabla_{\theta} J(\theta)\approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}\left(Q^{\pi_{\theta} }\left(s^n_t,a^n_t\right)-V(s_t^n)\right) \nabla \log \pi_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)

上式中存在的問題是,需要兩個網絡來分別預測Q和V,這就無形中增加了誤差來源,考慮到貝爾曼等式,即:

Qπθ(stn,atn)=E[rtn+Vπ(st+1n)]Q^{\pi_{\theta} }\left(s^n_t,a^n_t\right)=E\left[ r_t^n+V^\pi(s_{t+1}^n)\right]

這裏將期望去掉(個人理解,雖然去掉期望會導致有偏,但是最終還是會收斂到真實值):

Qπθ(stn,atn)=rtn+Vπθ(st+1n)Q^{\pi_{\theta} }\left(s^n_t,a^n_t\right)= r_t^n+V^{\pi_\theta}(s_{t+1}^n)

那麼最終就得到:
θJ(θ)1Nn=1Nt=1Tn(rtn+Vπθ(st+1n)V(stn))logπθ(atnstn)\nabla_{\theta} J(\theta)\approx \frac{1}{N} \sum_{n=1}^{N} \sum_{t=1}^{T_{n}}\left(r_t^n+V^{\pi_\theta}(s_{t+1}^n)-V(s_t^n)\right) \nabla \log \pi_{\theta}\left(a_{t}^{n} | s_{t}^{n}\right)

這樣只需要一個網絡就可以估算出V值了,而估算V的網絡正是我們在Q-learning 中做的,所以我們就把這個網絡叫做Critic。這樣就在Policy Gradient算法的基礎上引進了Q-learning 算法了。

12.3 Actor-Critic算法流程

Critic使用神經網絡來計算TD誤差並更新網絡參數,Actor將TD誤差作爲輸入,也使用神經網絡來更新網絡參數

算法輸入:迭代輪數T,狀態特徵維度n, 動作集A, 步長α,β,衰減因子γ, 探索率ϵ, Critic網絡結構和Actor網絡結構。

輸出:Actor 網絡參數θ, Critic網絡參數w
1.隨機初始化所有的狀態和動作對應的價值Q
2.for i from 1 to T,進行迭代。

  a)初始化S爲當前狀態序列的第一個狀態, 得到其特徵向量ϕ(S)\phi(S)

  b)在Actor網絡中使用ϕ(S)\phi(S)作爲輸入,輸出動作A,基於動作A得到新的狀態S′,獎勵r。

  c)在Critic網絡中分別使用ϕ(S)\phi(S)ϕ(S)\phi(S’)作爲輸入,得到Q值輸出V(S),V(S′)

  d)計算TD誤差δ=R+γV(S)V(S)\delta = R +\gamma V(S’) -V(S)

  e)使用均方差損失函數(R+γV(S)V(S,w))2\sum\limits(R +\gamma V(S’) -V(S,w))^2作Critic網絡參數w的梯度更新

  f)更新Actor網絡參數θ:

θ=θ+αθlogπθ(St,A)δ\theta = \theta + \alpha \nabla_{\theta}log \pi_{\theta}(S_t,A)\delta

對於Actor的得分函數∇θlogπθ(St,A),可以選擇softmax或者高斯分值函數。

12.4 代碼練習

代碼針對的環境的是 CliffWalkingEnv,在該環境中智能體在一個 4x12 的網格中移動,狀態編號如下所示:

[[ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11],
 [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23],
 [24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35],
 [36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47]]

在任何階段開始時,初始狀態都是狀態 36,狀態 47 是唯一的終止狀態,懸崖對應的是狀態 37 到 46。智能體有 4 個可選動作(UP = 0,RIGHT = 1,DOWN = 2,LEFT = 3)。智能體每走一步都會得到-1 的獎勵,跌入懸崖會得到-100 的獎勵並重置到起點,當達到目標時,片段結束。

AC完整的代碼如下:

import gym
import itertools
import matplotlib
import numpy as np
import sys
import tensorflow as tf
import collections

if "../" not in sys.path:
    sys.path.append("../")
from Lib.envs.cliff_walking import CliffWalkingEnv
from Lib import plotting

matplotlib.style.use('ggplot')

env = CliffWalkingEnv()


class PolicyEstimator():
    """
    策略函數逼近
    """

    def __init__(self, learning_rate=0.01, scope="policy_estimator"):
        with tf.variable_scope(scope):
            self.state = tf.placeholder(tf.int32, [], "state")
            self.action = tf.placeholder(dtype=tf.int32, name="action")
            self.target = tf.placeholder(dtype=tf.float32, name="target")

            # This is just table lookup estimator
            state_one_hot = tf.one_hot(self.state, int(env.observation_space.n))
            self.output_layer = tf.contrib.layers.fully_connected(
                inputs=tf.expand_dims(state_one_hot, 0),
                num_outputs=env.action_space.n,
                activation_fn=None,
                weights_initializer=tf.zeros_initializer)

            self.action_probs = tf.squeeze(tf.nn.softmax(self.output_layer))
            self.picked_action_prob = tf.gather(self.action_probs, self.action)

            self.loss = -tf.log(self.picked_action_prob) * self.target

            self.optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
            self.train_op = self.optimizer.minimize(
                self.loss, global_step=tf.contrib.framework.get_global_step())

    def predict(self, state, sess=None):
        sess = sess or tf.get_default_session()
        return sess.run(self.action_probs, {self.state: state})

    def update(self, state, target, action, sess=None):
        sess = sess or tf.get_default_session()
        feed_dict = {self.state: state, self.target: target, self.action: action}
        _, loss = sess.run([self.train_op, self.loss], feed_dict)
        return loss


class ValueEstimator():
    """
    值函數逼近器
    """

    def __init__(self, learning_rate=0.1, scope="value_estimator"):
        with tf.variable_scope(scope):
            self.state = tf.placeholder(tf.int32, [], "state")
            self.target = tf.placeholder(dtype=tf.float32, name="target")

            # This is just table lookup estimator
            state_one_hot = tf.one_hot(self.state, int(env.observation_space.n))
            self.output_layer = tf.contrib.layers.fully_connected(
                inputs=tf.expand_dims(state_one_hot, 0),
                num_outputs=1,
                activation_fn=None,
                weights_initializer=tf.zeros_initializer)

            self.value_estimate = tf.squeeze(self.output_layer)
            self.loss = tf.squared_difference(self.value_estimate, self.target)

            self.optimizer = tf.train.AdamOptimizer(learning_rate=learning_rate)
            self.train_op = self.optimizer.minimize(
                self.loss, global_step=tf.contrib.framework.get_global_step())

    def predict(self, state, sess=None):
        sess = sess or tf.get_default_session()
        return sess.run(self.value_estimate, {self.state: state})

    def update(self, state, target, sess=None):
        sess = sess or tf.get_default_session()
        feed_dict = {self.state: state, self.target: target}
        _, loss = sess.run([self.train_op, self.loss], feed_dict)
        return loss


def actor_critic(env, estimator_policy, estimator_value, num_episodes, discount_factor=1.0):
    """
    Actor Critic 算法.通過策略梯度優化策略函數逼近器

    參數:
        env: OpenAI環境.
        estimator_policy: 待優化的策略函數
        estimator_value: 值函數逼近器,用作評論家
        num_episodes: 回合數
        discount_factor: 折扣因子

    返回值:
        EpisodeStats對象,包含兩個numpy數組,分別存儲片段長度和片段獎勵
    """

    # Keeps track of useful statistics
    stats = plotting.EpisodeStats(
        episode_lengths=np.zeros(num_episodes),
        episode_rewards=np.zeros(num_episodes))

    Transition = collections.namedtuple("Transition", ["state", "action", "reward", "next_state", "done"])

    for i_episode in range(num_episodes):
        state = env.reset()

        episode = []

        for t in itertools.count():

            action_probs = estimator_policy.predict(state)
            action = np.random.choice(np.arange(len(action_probs)), p=action_probs)
            next_state, reward, done, _ = env.step(action)

            episode.append(Transition(
                state=state, action=action, reward=reward, next_state=next_state, done=done))

            stats.episode_rewards[i_episode] += reward
            stats.episode_lengths[i_episode] = t

            # 計算TD目標
            value_next = estimator_value.predict(next_state)
            td_target = reward + discount_factor * value_next
            td_error = td_target - estimator_value.predict(state)

            # 更新值函數逼近
            estimator_value.update(state, td_target)

            # 更新策略逼近
            # 使用TD誤差作爲優勢估計
            estimator_policy.update(state, td_error, action)

            print("\rStep {} @ Episode {}/{} ({})".format(
                t, i_episode + 1, num_episodes, stats.episode_rewards[i_episode - 1]), end="")

            if done:
                break

            state = next_state

    return stats


tf.reset_default_graph()

global_step = tf.Variable(0, name="global_step", trainable=False)
policy_estimator = PolicyEstimator()
value_estimator = ValueEstimator()

with tf.Session() as sess:
    sess.run(tf.initialize_all_variables())
    stats = actor_critic(env, policy_estimator, value_estimator, 300)

plotting.plot_episode_stats(stats, smoothing_window=10)

結果如下:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章