深度強化學習系列(16): 從DPG到DDPG算法的原理講解及tensorflow代碼實現

1、背景知識

在前文系列博客第二篇中講解了DQN(深度強化學習DQN原理),可以說它是神經網絡在強化學習中取得的重大突破,也爲強化學習的發展提供了一個方向和基礎,Sliver等人將其應用在Atari遊戲中取得了重大突破, 後來大批量的論文均採用了DQN的思想,同時提出了更多的但是該算法有以下缺點:

Atari 遊戲所需的動作是離散的,且屬於低維(只有少數幾個動作),但現實生活中很多問題都是連續的,且維度比較高,比如機器人控制(多個自由度)、汽車方向盤轉向角度,油門大小、天氣預報推薦指數等。雖然可以對連續性高維度的動作做離散型的處理,但是對於一個經過離散處理的大狀態空間,使用DQN訓練仍然是仍然是一個比較棘手的問題,因爲DQN算法的核心思想是利用隨機策略進行探索,對於高維度的來說,第一個問題是:模型很難收斂,第二個問題是需要在探索和利用之間進行協調。

因此用DQN解決高維度連續狀態空間的任務變得非常的困難。爲了解決該問題,David Sliver在2014年ICML大會論文中提出了確定性策略梯度((Deterministic Policy Gradient Algorithms)), 是的,這就是福音!下面先從DPG算法開始,然後再說Deep DPG(DDPG)

1、確定性策略梯度(Deterministic policy gradient )

首先解決一個強化學習問題,我們想到的就是累計折扣獎勵的定義,即狀態滿足ρπ\rho^{\pi} 分佈上的累計獎勵,如下:
θJ(πθ)=Sρπ(s)Aπθ(as)r(s,a)dads=Esρπ,aπθ[r(s,a)] \nabla_{\theta}J(\pi_{\theta}) = \int_{S}\rho^{\pi}(s) \int_{A}\pi_{\theta}(a|s)r(s, a) dads \\ =E_{s\sim \rho^{\pi},a\sim \pi_{\theta}}[r(s, a)]
那麼什麼是策略梯度呢?策略梯度就是沿着使目標函數變大的方向調整策略的參數,它被定義爲:

θJ(πθ)=Sρπ(s)Aθπθ(as)Qπ(s,a)dads=Esρπ,aπθ[θlogπθ(as)Qπ(s,a)] \nabla_{\theta}J(\pi_{\theta}) = \int_{S}\rho^{\pi}(s) \int_{A}\nabla_{\theta}\pi_{\theta}(a|s)Q^{\pi}(s, a) dads \\ = E_{s\sim \rho^{\pi},a\sim \pi_{\theta}}[\nabla_{\theta} \log \pi_{\theta}(a|s)Q^{\pi}(s, a)]
公式非常直白的告訴我們,J()函數主要與策略梯度和值函數的期望有關,儘管狀態空間分佈ρπ\rho^{\pi}的分佈依賴於策略參數,但是策略梯度並不依賴於狀態分佈上的梯度。另外,在DQN中我們使用了評價網絡和target網絡,採用了experimence replay的方式打亂了數據之間的相關性而使得滿足獨立同分布條件,利用softupdating方式更新target網絡,但總結一句話,它的策略網路和值函數網絡使用的是同一個網絡。在DPG的公式表明,J()和策略梯度與值函數有關,因此爲了解決策略和值函數之間的問題,採用了一種新的解決思路將兩個網絡分開,即:Actor-critic異步框架。

1.1 、Actor-critic框架

從框架的 名字我們就可以知道一些信息:Actor(演員)-Critic(評論家)框架,相當於演員和評論家共同來提升表演,演員跳舞的姿態可能動作不到位,於是評論家告訴演員,你這樣跳舞不好,它會建議演員修改一下舞姿了,當演員在某個舞姿上表演的比較好,那評論家就會告訴演員, 不錯,你可以加大力度往這個方向發展,是不是明白其中的意思了?
那麼對應到RL中,Actor就是策略網絡,來做動作選擇(空間探索)。Critic就是值函數,對策略函數進行評估, 具體的流程如圖:
這裏寫圖片描述

簡單描述一下圖:其中的TD-error就是Critic告訴Actor的偏差。
具體的更新過程:
δt=rt+γQw(st+1,μθ(st+1))Qw(st,at)wt+1=wt+αwδtwQw(st,at)θt+1=θt+αθμθ(st)Qw(st,at)a=μθ(s) \delta _{t} = r_{t}+ \gamma Q^{w}(s_{t+1},\mu_{\theta}(s_{t+1}))-Q^{w}(s_{t},a_{t})\\ w_{t+1}=w_{t}+\alpha_{w}\delta_{t}\nabla_{w}Q^{w}(s_{t},a_{t}) \\ \theta_{t+1} = \theta_{t}+\alpha_{\theta}\mu_{\theta}(s_{t})\nabla Q^{w}(s_{t},a_{t})|_{a=\mu_{\theta}(s)}

我們現在考慮如何將策略梯度框架擴展到確定性政策。 我們的主要結果是一個確定性的策略梯度定理,類似於上一節中介紹的隨機政策梯度定理。 我們在確定性政策梯度的形式背後提供一種非正式的直覺。 然後,我們從第一原則給出確定性政策梯度定理的形式證明。 最後,我們證明確定性政策梯度定理實際上是隨機政策梯度定理的一個極限情況(具體的證明見論文附錄)
那麼極限情況是啥呢?就是當概率策略的方差趨近於0的時候,就是確定性策略,即
θJ(μθ)=Sρμ(s)θμθ(s)aQμ(s,a)a=μθ(s)ds=Esρμ[θμθ(s)Qμ(s,a)a=μθ(s)] \nabla_{\theta} J(\mu_{\theta}) = \int_{S}\rho^{\mu}(s)\nabla_{\theta}\mu_{\theta}(s)\nabla_{a}Q^{\mu}(s, a)|_{a=\mu_{\theta}(s)}ds \\ = E_{s \sim \rho^{\mu}}[\nabla_{\theta}\mu_{\theta}(s)\nabla Q^{\mu}(s, a)|_{a=\mu_{\theta}(s)}]
公式推到依據: 對於連續性變量,期望通過積分求得:
ExP[f(x)]=p(x)f(x)dx E_{x\sim P }[f(x)] = \int p(x)f(x)dx

1.2 兼容函數近似

一般而言,將近似Qw(s,aQ^{w}(s, a代入確定性政策梯度並不一定會遵循真正的梯度(事實上它也不一定是上升方向)。 類似於隨機情況,我們現在找到一類兼容函數逼近器Qw(s,aQ^{w}(s, a,使得真正的梯度被保留。 換句話說,我們找到了 critic Qw(s,aQ^{w}(s, a,使得梯度aQμ(s,a)\nabla_{a}Q^{\mu}(s, a)可以用aQw(s,a)\nabla_{a}Q^{w}(s, a)替代,而不會影響確定性政策梯度。 以下定理適用於on-policy E[]=Esρμ[]E [·] = E_{s \sim \rho^{\mu}}[·]和off-policy政策,E[]=Esρβ[]E [·] = E_{s \sim \rho^{\beta}}[·],
這裏寫圖片描述

總結一下:
回顧前文的內容,我們知道廣義的值函數求解包括策略評估和策略改善,當值函數最優的餓時候,策略也是最優的(此處的策略是貪婪策略),而策略搜索是將策略參數化,即πθ(s)\pi_{\theta}(s), 因此,我們可以使用參數化或者非參數化(神經網路) 逼近策略,找到最優的θ\theta, 使得累計折扣獎勵最大化。確定性策略梯度就是在確定的梯度策略方向上進行學習。


2、深度確定性策略(DDPG)

感興趣的可以看看原文《continuous control with deep reinforcement learning》
之所以使用確定性策略的原因是相對與隨機策略,就是因爲數據的採樣少,算法效率高,深度確定性策略就是使用了深度神經網絡去近似值函數和策略梯度網絡。總結一下DDPG算法使用以下核心思想:
(1)採用經驗回放方法
(2)採用target目標網絡更新(爲什麼要用target網絡?因爲訓練的網絡特別不穩定)
(3)AC框架
(4)確定性策略梯度

2.1、DDPG算法流程圖

這裏寫圖片描述

算法採用AC框架,Actor獲取狀態SS, s可以是一組向量(速度,位置等),經過Actor網絡選取動作action,Critic根據動作action和S進行評價,採用策略梯度最終更新兩個網絡的權重。具體見下文:

2.2、DDPG算法原理

DPG算法通過確定性地將狀態映射到特定動作來維護指定當前策略的參數化參與者函數μ(sθμ)\mu(s|\theta^{\mu})。 評論者Q(s,a)Q(s, a)是在Q學習中使用Bellman方程學習的。 通過將鏈規則應用於從起始分佈J相對於參數參數的預期回報來更新參與者,原理部分儘量少對公式,下面我們結合算法僞代碼詳細解釋,:
這裏寫圖片描述

(1)初始化網絡

第1行:首先隨機初始化了Actor網絡μ(sθμ)\mu(s|\theta^{\mu}) 和Critic網絡Q(s, a)
第2行:初始化target網絡,它的結構和actor和critic的一樣,並且參數也一樣,相當於複製了一份
第3行:初始化Replay Buffer R,因爲強化學習的馬爾科夫序列之間的數據具有非常大的關聯性,採用R的目的就是打亂數據之間的相關性,使得數據之間滿足獨立同分布。

#####(2)訓練Episode
第5行:初始化一個隨機的N,它相當於動作空間的探索度,後面講解
第6行:獲得觀察指s1s_1
第8行:選取動作,這個動作是由兩部分組成:1、策略網絡μ\mu的輸出+ 探索度(確保有足夠的探索度,避免滅有學習到足夠多的知識)
第9行:執行代碼,根據觀察值sts_{t} 和動作,執行action,得到對應的獎勵R和ss^{'}

存儲和讀取Replay Buffer R

第10,11行:將學習的序列存儲到R中,然後隨機批量的讀取R中的序列進行學習模型。
注:由於強化學習過程學習過程的序列之間有相關性,一般隨機批量的batch-size= 64,128,256(原因是根據計算機內存或者顯存,具體參考:https://danluu.com/3c-conflict/)

更新Critic網絡結構

第12,13行,首先定義了yiy_{i}, 這裏使用了RMSE誤差,更新的時候直接更新值函數的損失。

更新Actor網絡結構

第15行:此處直接更新的也是Actor策略梯度

更新目標target網絡的參數

最後兩行採用了DQN的soft updating 方式更新目標網絡,即用τ\tau 來延遲更新。


最後的最後, 用一張圖總結一下:
這裏寫圖片描述


代碼實現:

本部分對gym中的立杆(Pendulum-v0)進行代碼講解,效果圖如下,其中gym的安裝詳見github:https://github.com/openai/gym
這裏寫圖片描述

注:直接可以粘貼在pycharm中使用(切記把編碼改爲utf-8,否則中文註釋出錯)


import tensorflow as tf
import numpy as np
import gym
import time

# 定義超參數
MAX_EPISODES = 200
MAX_EP_STEPS = 200
LR_A = 0.001    # actor學習率
LR_C = 0.002    # critic學習率
GAMMA = 0.9     # 累計折扣獎勵因子
TAU = 0.01      # 軟更新tao
MEMORY_CAPACITY = 10000  # buffer R, 經驗回放容器
BATCH_SIZE = 32  # 每批隨機讀取批次大小

RENDER = False
ENV_NAME = 'Pendulum-v0'

# 定義DDPG類
class DDPG(object):
    def __init__(self, a_dim, s_dim, a_bound,):
        # memory 存放的是序列(s,a,r,s+1)= s*2+a+1(r=1)
        self.memory = np.zeros((MEMORY_CAPACITY, s_dim * 2 + a_dim + 1), dtype=np.float32)
        self.pointer = 0
        self.sess = tf.Session()

        self.a_dim, self.s_dim, self.a_bound = a_dim, s_dim, a_bound,
        self.S = tf.placeholder(tf.float32, [None, s_dim], 's')
        self.S_ = tf.placeholder(tf.float32, [None, s_dim], 's_')
        self.R = tf.placeholder(tf.float32, [None, 1], 'r')
        
        # 建立網絡,actor網絡輸入是S,critic輸入是s,a
        self.a = self._build_a(self.S,)
        q = self._build_c(self.S, self.a, )
        
        a_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='Actor')
        c_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope='Critic')
        # soft updating
        """
        tf.train.ExponentialMovingAverage(decay)是採用滑動平均的方法更新參數。這個函數初始化需要提供一個衰減速率(decay),用於控制模型的更新速度。這個函數還會維護一個影子變量(也就是更新參數後的參數值),這個影子變量的初始值就是這個變量的初始值,影子變量值的更新方式如下:
   shadow_variable = decay * shadow_variable + (1-decay) * variable
   shadow_variable是影子變量,variable表示待更新的變量,也就是變量被賦予的值,decay爲衰減速率。decay一般設爲接近於1的數(0.99,0.999)。decay越大模型越穩定,因爲decay越大,參數更新的速度就越慢,趨於穩定。
        """
        
        ema = tf.train.ExponentialMovingAverage(decay=1 - TAU)          # soft replacement
        def ema_getter(getter, name, *args, **kwargs):
            return ema.average(getter(name, *args, **kwargs))

        target_update = [ema.apply(a_params), ema.apply(c_params)]      # soft update operation
        a_ = self._build_a(self.S_, reuse=True, custom_getter=ema_getter)   # replaced target parameters
        q_ = self._build_c(self.S_, a_, reuse=True, custom_getter=ema_getter)

        a_loss = - tf.reduce_mean(q)  # maximize the q
        self.atrain = tf.train.AdamOptimizer(LR_A).minimize(a_loss, var_list=a_params)

        with tf.control_dependencies(target_update):    # soft replacement happened at here
            q_target = self.R + GAMMA * q_
            td_error = tf.losses.mean_squared_error(labels=q_target, predictions=q)
            self.ctrain = tf.train.AdamOptimizer(LR_C).minimize(td_error, var_list=c_params)

        self.sess.run(tf.global_variables_initializer())
    
    # 選取動作函數
    def choose_action(self, s):
        return self.sess.run(self.a, {self.S: s[np.newaxis, :]})[0]
    
    # 從R buffer中學習
    def learn(self):
        indices = np.random.choice(MEMORY_CAPACITY, size=BATCH_SIZE)
        bt = self.memory[indices, :]
        bs = bt[:, :self.s_dim]
        ba = bt[:, self.s_dim: self.s_dim + self.a_dim]
        br = bt[:, -self.s_dim - 1: -self.s_dim]
        bs_ = bt[:, -self.s_dim:]

        self.sess.run(self.atrain, {self.S: bs})
        self.sess.run(self.ctrain, {self.S: bs, self.a: ba, self.R: br, self.S_: bs_})
  
    # 存儲序列
    def store_transition(self, s, a, r, s_):
        transition = np.hstack((s, a, [r], s_))
        index = self.pointer % MEMORY_CAPACITY  # replace the old memory with new memory
        self.memory[index, :] = transition
        self.pointer += 1
   
    # 建立actor網絡(輸入S_dim,輸出a_dim, 採用tanh激活函數)
    def _build_a(self, s, reuse=None, custom_getter=None):
        trainable = True if reuse is None else False
        with tf.variable_scope('Actor', reuse=reuse, custom_getter=custom_getter):
            net = tf.layers.dense(s, 30, activation=tf.nn.relu, name='l1', trainable=trainable)
            a = tf.layers.dense(net, self.a_dim, activation=tf.nn.tanh, name='a', trainable=trainable)
            return tf.multiply(a, self.a_bound, name='scaled_a')
    
    # 建立critic網絡(輸入S_dim,s_dim, 輸出q)
    def _build_c(self, s, a, reuse=None, custom_getter=None):
        trainable = True if reuse is None else False
        with tf.variable_scope('Critic', reuse=reuse, custom_getter=custom_getter):
            n_l1 = 30
            w1_s = tf.get_variable('w1_s', [self.s_dim, n_l1], trainable=trainable)
            w1_a = tf.get_variable('w1_a', [self.a_dim, n_l1], trainable=trainable)
            b1 = tf.get_variable('b1', [1, n_l1], trainable=trainable)
            net = tf.nn.relu(tf.matmul(s, w1_s) + tf.matmul(a, w1_a) + b1)
            return tf.layers.dense(net, 1, trainable=trainable)  # Q(s,a)


# training process

# 環境初始化
env = gym.make(ENV_NAME)
env = env.unwrapped
env.seed(1)

# 獲取s,a的維度
s_dim = env.observation_space.shape[0]
a_dim = env.action_space.shape[0]
a_bound = env.action_space.high

ddpg = DDPG(a_dim, s_dim, a_bound)

var = 3  # 定義探索因子

t1 = time.time()
for i in range(MAX_EPISODES):
    s = env.reset()
    ep_reward = 0
    for j in range(MAX_EP_STEPS):
        if RENDER:
            env.render()

        # 添加探索噪音
        a = ddpg.choose_action(s)
        a = np.clip(np.random.normal(a, var), -2, 2)    # 隨機選取動作探索
        # np.clip()函數是,如果隨機生成的數字大於2,則爲2 ,如果小於-2,則爲-2,其他則爲本身
        
        s_, r, done, info = env.step(a)
        ddpg.store_transition(s, a, r / 10, s_)

        if ddpg.pointer > MEMORY_CAPACITY:
            var *= .9995    # 減緩動作探索度,即衰減速率
            ddpg.learn()

        s = s_
        ep_reward += r
        if j == MAX_EP_STEPS-1:
            print('Episode:', i, ' Reward: %i' % int(ep_reward), 'Explore: %.2f' % var, )
            # if ep_reward > -300:RENDER = True
            break

print('Running time: ', time.time() - t1)

注:此處沒有保存模型,tf採用saver保存和調用模型,後期會在其他模型中添加。

參考資料:
[1].https://morvanzhou.github.io/tutorials/machine-learning/reinforcement-learning/6-2-DDPG/
[2].https://zhuanlan.zhihu.com/p/28549596
[3].https://blog.csdn.net/kenneth_yu/article/details/78478356

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