深度強化學習系列(7): Double DQN(DDQN)原理及實現

在這裏插入圖片描述
論文地址: https://arxiv.org/pdf/1509.06461.pdf

本文是Google DeepMind於2015年12月提出的一篇解決Q值"過估計(overestimate)"的文章,發表在頂級會議AAAI上,作者Hado van Hasselt在其2010年發表的Double Q-learning算法工作的基礎上結合了DQN的思想,提出了本文的state-of-the-art的Double DQN算法。給出了過估計的通用原因解釋和解決方法的數學證明,最後在Atari遊戲上有超高的分數實驗表現。

正常論文的閱讀方式,先看摘要和結論:
在這裏插入圖片描述
通常情況下,在Q-learning學習中“過估計”是經常發生的,並且影響實驗的性能,作者提出了一種可以回答這個問題,並在Double Q-learning算法的基礎上進行function approximation的方法,結果表明不僅可以減少觀察值的過估計,而且在許多遊戲上還有更好的性能表現。而結論部分如下:
在這裏插入圖片描述
作者將整個文章的貢獻總結了五點:前三點基本上說了過估計問題的存在,重要性和Double Q-learning算法能解決這個問題,本文重點是第四,作者提出了一種在Double Q-learning基礎上利用“DQN”算法網絡結構的方法“Double DQN”,並在第五點獲得state-of-the-art的效果,下面詳細介紹。

1. 問題闡述

1.1 過估計問題現象

Q-learning算法在低維狀態下的成功以及DQN和target DQN的效果已經很好了,但是人們發現了一個問題就是之前的Q-learning、DQN算法都會過高估計(overestimate)Q值。開始大家都將其原因歸結於函數逼近和噪音。

  • Q-learning拿到狀態對應的所有動作Q值之後是直接選取Q值最大的那個動作,這會導致更加傾向於估計的值比真實的值要高。爲了能夠使得標準的Q-learning學習去大規模的問題,將其參數化Q值函數表示爲:
    θt+1=θt+α(YtQQ(St,At;θt))θtQ(St,At;θt) \theta_{t+1}=\boldsymbol{\theta}_{t}+\alpha\left(Y_{t}^{\mathrm{Q}}-Q\left(S_{t}, A_{t} ; \boldsymbol{\theta}_{t}\right)\right) \nabla_{\boldsymbol{\theta}_{t}} Q\left(S_{t}, A_{t} ; \boldsymbol{\theta}_{t}\right)
    其中 YtQY_{t}^{\mathrm{Q}}表示爲:
    YtQRt+1+γmaxaQ(St+1,a;θt) Y_{t}^{\mathrm{Q}} \equiv R_{t+1}+\gamma \max _{a} Q\left(S_{t+1}, a ; \boldsymbol{\theta}_{t}\right)
    其實我們發現這個更新過程和梯度下降大同小異,此處均以更新參數 θ\theta 進行學習。

  • DQN算法非常重要的兩個元素是“經驗回放”和“目標網絡”,通常情況下,DQN算法更新是利用目標網絡的參數 θ\theta^{-},它每個τ\tau 步更新一次,其數學表示爲:
    YtDQNRt+1+γmaxaQ(St+1,a;θt) Y_{t}^{\mathrm{DQN}} \equiv R_{t+1}+\gamma \max _{a} Q\left(S_{t+1}, a ; \boldsymbol{\theta}_{t}^{-}\right)
    上述的標準的Q-learning學習和DQN中均使用了 max\max 操作,使得選擇和評估一個動作值都會過高估計,爲了解決這個問題,Double Q-learning率先使用了兩個值函數進行解耦,其互相隨機的更新兩個值函數,並利用彼此的經驗去更新網絡權重θ\thetaθ\theta^{-}, 爲了能夠明顯的對比,

  • Double Q-learning,2010年本文作者Hasselt就針對過高估計Q值的問題提出了Double Q-learning,他就是嘗試通過將選擇動作和評估動作分割開來避免過高估計的問題。在原始的Double Q-Learning算法裏面,有兩個價值函數(value function),一個用來選擇動作(當前狀態的策略),一個用來評估當前狀態的價值。這兩個價值函數的參數分別記做 θ\thetaθ\theta^{'} 。算法的思路如下:

YtQ=Rt+1+γQ(St+1,argmaxaQ(St+1,a;θt);θt) Y_{t}^{\mathrm{Q}}=R_{t+1}+\gamma Q\left(S_{t+1}, \underset{a}{\operatorname{argmax}} Q\left(S_{t+1}, a ; \boldsymbol{\theta}_{t}\right) ; \boldsymbol{\theta}_{t}\right)
通過對原始的Q-learning算法的改進,Double Q-learning的誤差表示爲:
YtDoubleQ Rt+1+γQ(St+1,argmaxaQ(St+1,a;θt);θt) Y_{t}^{\text {DoubleQ }} \equiv R_{t+1}+\gamma Q\left(S_{t+1}, \underset{a}{\operatorname{argmax}} Q\left(S_{t+1}, a ; \boldsymbol{\theta}_{t}\right) ; \boldsymbol{\theta}_{t}^{\prime}\right)
此處意味着我們仍然使用貪心策略去學習估計Q值,而使用第二組權重參數θ\theta^{'}去評估其策略。

1.2 估計誤差: “過估計”

1.2.1 上界估計

Thrun等人在1993年的時候就給出如果動作值包含在區間 [ϵ,ϵ][-\epsilon, \epsilon] 之間的標準分佈下的隨機的誤差,那麼上限估計爲: γϵm1m+1\gamma \epsilon \frac{m-1}{m+1} (m表示動作的數量)

1.2.2 下界估計

在這裏插入圖片描述
作者給出了一個定理1:

在一個狀態下如果動作 m>2m>2C=1ma(Qt(s,a)V(s))2>0C=\frac{1}{m} \sum_{a}\left(Q_{t}(s, a)-V_{*}(s)\right)^{2}>0,則
【1】maxaQt(s,a)V(s)+Cm1\max _{a} Q_{t}(s, a) \geq V_{*}(s)+\sqrt{\frac{C}{m-1}}
【2】Double Q-learning的下界絕對誤差爲0

根據定理1我們得到下界估計的值隨着 mm 的增大而減小,通過實驗,下面結果表明 mm對估計的影響,圖中明顯表明,Q-learning的隨m的增大越來越大,而Double Q-learning是無偏估計,並未隨着m增大而過度變化,基本上在0附近。
在這裏插入圖片描述

附錄:定理1證明過程
在這裏插入圖片描述

此處作者還得出一個定理結論
在這裏插入圖片描述

證明如下:
在這裏插入圖片描述

爲了進一步說明Q-learning, Double Q-learning估值偏差的區別,作者給出了一個有真實QQ值的環境:假設QQ值爲 Q(s,a)=sin(s)Q(s,a)=2exp(s2)Q_(s, a) = sin(s)以及Q_(s, a) = 2 exp(-s^2) ,然後嘗試用6階和9階多項式擬合這兩條曲線,一共進行了三組實驗,參見下面表格
在這裏插入圖片描述

這個試驗中設定有10個action(分別記做 a1,a2,…,a10 ),並且Q值只與state有關。所以對於每個state,每個action都應該有相同的true value,他們的值可以通過目標Q值那一欄的公式計算出來。此外這個實作還有一個人爲的設定是每個action都有兩個相鄰的state不採樣,比如說 a1 不採樣-5和-4(這裏把-4和-5看作是state的編號), a2 不採樣-4和-3等。這樣我們可以整理出一張參與採樣的action與對應state的表格:
在這裏插入圖片描述
淺藍色代表對應的格子有學習得到的估值,灰色代表這部分不採樣,也沒有對應的估值(類似於監督學習這部分沒有對應的標記,所以無法學習到東西)

這樣實驗過後得到的結果用下圖展示:
在這裏插入圖片描述

  • 最左邊三幅圖(對應 action2 那一列學到的估值)中紫色的線代表真實值(也就是目標Q值,通過s不同取值計算得出),綠色的線是通過Q-learning學習後得到的估值,其中綠點標記的是採樣點,也就是說是通過這幾個點的真實值進行學習的。結果顯示前面兩組的估值不準確,原因是我們有十一個值( s∈−6,−5,−2,−1,0,1,2,3,4,5,6 ),用6階多項式沒有辦法完美擬合這些點。對於第三組實驗,雖然能看出在採樣的這十一個點中,我們的多項式擬合已經足夠準確了,但是對於其他沒有采樣的點我們的誤差甚至比六階多項式對應的點還要大。
  • 中間的三張圖畫出了這十個動作學到的估值曲線(對應圖中綠色的線條),並且用黑色虛線標記了這十根線中每個位置的最大值。結果可以發現這根黑色虛線幾乎在所有的位置都比真實值要高。
  • 右邊的三幅圖顯示了中間圖中黑色虛線和左邊圖中紫線的差值,並且將Double Q-Learning實作的結果用同樣的方式進行比較,結果發現Double Q-Learning的方式實作的結果更加接近0。這證明了Double Q-learnign確實能降低Q-Learning中過高估計的問題。
  • 前面提到過有人認爲過高估計的一個原因是不夠靈活的value function,但是從這個實驗結果中可以看出,雖然說在採樣的點上,value function越靈活,Q值越接近於真實值,但是對於沒有采樣的點,靈活的value function會導致更差的結果,在RL領域中,大家經常使用的是比較靈活的value function,所以這一點的影響比較嚴重。
  • 雖然有人認爲對於一個state,如果這個state對應的action的估值都均勻的升高了,還是不影響我們的決策啊,反正估值最高的那個動作還是最高,我們選擇的動作依然是正確的。但是這個實驗也證明了:不同狀態,不同動作,相應的估值過高估計的程度也是不一樣的,因此上面這種說法也並不正確。

2. Double DQN 算法原理及過程

通過以上的證明和擬合曲線實驗表明,過高估計不僅真實存在,而且對實驗的結果有很大的影響,爲了解決問這個問題,在Double的基礎上作者提出了本文的“Double DQN”算法

下面我們提出Double DQN算法的更新過程:
YtDoubleDQN Rt+1+γQ(St+1,argmaxaQ(St+1,a;θt),θt) Y_{t}^{\text {DoubleDQN }} \equiv R_{t+1}+\gamma Q\left(S_{t+1}, \underset{a}{\operatorname{argmax}} Q\left(S_{t+1}, a ; \boldsymbol{\theta}_{t}\right), \boldsymbol{\theta}_{t}^{-}\right)

該過程和前面的Double Q-learning算法更新公式基本一樣,唯一的區別在於 θ\theta^{'}θ\theta^{-},兩者的區別在於Double Q-learning算法是利用交換來不斷的更新,Double DQN則使用了DQN的思想,直接利用目標網絡(θ\theta^{-})進行更新。

在實驗中,作者基本上
在這裏插入圖片描述
實驗結果如下:
在這裏插入圖片描述

對於Atari遊戲來講,我們很難說某個狀態的Q值等於多少,一般情況是將訓練好的策略去運行遊戲,然後根據遊戲中積累reward,就能得到平均的reward作爲true value了。

  • 從實驗第一行結果我們明顯可以看出在集中游戲中,值函數相對於Double DQN都明顯的比較高(如果沒有過高估計的話,收斂之後我們的估值應該跟真實值相同的),此處說明過高估計確實不容易避免。
  • Wizard of Wor和Asterix這兩個遊戲可以看出,DQN的結果比較不穩定。也表明過高估計會影響到學習的性能的穩定性。因此不穩定的問題的本質原因還是對Q值的過高估計
  • 對於魯棒性的測試

此外作者爲了對遊戲有一個統計學意義上的總結,對分數進行了正則化,表示爲:
 score normalized=scoreagent scorerandom scorehuman scorerandom \text { score }_{\text{normalized}}=\frac{\text {score}_{\text{agent}}-\text { score}_{\text{random}}}{\text { score}_{\text{human}}-\text { score}_{\text{random}}}
實驗結果如下:
在這裏插入圖片描述

以上基本上是本論文的內容,下面我們藉助實驗進行code的Double DQN算法。其實本部分的復現只是將更新的DQN的目標函數換一下。對於論文中的多項式擬合併不做復現。

3. 代碼復現

此處採用Morvan的代碼,實驗環境是:Tensorflow=1.0&gym=0.8.0,先coding一個智能體Agent

# file name Agent.py

import numpy as np
import tensorflow as tf

np.random.seed(1)
tf.set_random_seed(1)

# Double DQN
class DoubleDQN:
    def __init__(
            self,
            n_actions,
            n_features,
            learning_rate=0.005,
            reward_decay=0.9,
            e_greedy=0.9,
            replace_target_iter=200,
            memory_size=3000,
            batch_size=32,
            e_greedy_increment=None,
            output_graph=False,
            double_q=True,
            sess=None,
    ):
        self.n_actions = n_actions
        self.n_features = n_features
        self.lr = learning_rate
        self.gamma = reward_decay
        self.epsilon_max = e_greedy
        self.replace_target_iter = replace_target_iter
        self.memory_size = memory_size
        self.batch_size = batch_size
        self.epsilon_increment = e_greedy_increment
        self.epsilon = 0 if e_greedy_increment is not None else self.epsilon_max

        self.double_q = double_q    # decide to use double q or not

        self.learn_step_counter = 0
        self.memory = np.zeros((self.memory_size, n_features*2+2))
        self._build_net()
        t_params = tf.get_collection('target_net_params')
        e_params = tf.get_collection('eval_net_params')
        self.replace_target_op = [tf.assign(t, e) for t, e in zip(t_params, e_params)]

        if sess is None:
            self.sess = tf.Session()
            self.sess.run(tf.global_variables_initializer())
        else:
            self.sess = sess
        if output_graph:
            tf.summary.FileWriter("logs/", self.sess.graph)
        self.cost_his = []

    def _build_net(self):
        def build_layers(s, c_names, n_l1, w_initializer, b_initializer):
            with tf.variable_scope('l1'):
                w1 = tf.get_variable('w1', [self.n_features, n_l1], initializer=w_initializer, collections=c_names)
                b1 = tf.get_variable('b1', [1, n_l1], initializer=b_initializer, collections=c_names)
                l1 = tf.nn.relu(tf.matmul(s, w1) + b1)

            with tf.variable_scope('l2'):
                w2 = tf.get_variable('w2', [n_l1, self.n_actions], initializer=w_initializer, collections=c_names)
                b2 = tf.get_variable('b2', [1, self.n_actions], initializer=b_initializer, collections=c_names)
                out = tf.matmul(l1, w2) + b2
            return out
        # ------------------ build evaluate_net ------------------
        self.s = tf.placeholder(tf.float32, [None, self.n_features], name='s')  # input
        self.q_target = tf.placeholder(tf.float32, [None, self.n_actions], name='Q_target')  # for calculating loss

        with tf.variable_scope('eval_net'):
            c_names, n_l1, w_initializer, b_initializer = \
                ['eval_net_params', tf.GraphKeys.GLOBAL_VARIABLES], 20, \
                tf.random_normal_initializer(0., 0.3), tf.constant_initializer(0.1)  # config of layers

            self.q_eval = build_layers(self.s, c_names, n_l1, w_initializer, b_initializer)

        with tf.variable_scope('loss'):
            self.loss = tf.reduce_mean(tf.squared_difference(self.q_target, self.q_eval))
        with tf.variable_scope('train'):
            self._train_op = tf.train.RMSPropOptimizer(self.lr).minimize(self.loss)

        # ------------------ build target_net ------------------
        self.s_ = tf.placeholder(tf.float32, [None, self.n_features], name='s_')    # input
        with tf.variable_scope('target_net'):
            c_names = ['target_net_params', tf.GraphKeys.GLOBAL_VARIABLES]

            self.q_next = build_layers(self.s_, c_names, n_l1, w_initializer, b_initializer)

    def store_transition(self, s, a, r, s_):
        if not hasattr(self, 'memory_counter'):
            self.memory_counter = 0
        transition = np.hstack((s, [a, r], s_))
        index = self.memory_counter % self.memory_size
        self.memory[index, :] = transition
        self.memory_counter += 1

    def choose_action(self, observation):
        observation = observation[np.newaxis, :]
        actions_value = self.sess.run(self.q_eval, feed_dict={self.s: observation})
        action = np.argmax(actions_value)

        if not hasattr(self, 'q'):  # record action value it gets
            self.q = []
            self.running_q = 0
        self.running_q = self.running_q*0.99 + 0.01 * np.max(actions_value)
        self.q.append(self.running_q)

        if np.random.uniform() > self.epsilon:  # choosing action
            action = np.random.randint(0, self.n_actions)
        return action

    def learn(self):
        if self.learn_step_counter % self.replace_target_iter == 0:
            self.sess.run(self.replace_target_op)
            print('\ntarget_params_replaced\n')

        if self.memory_counter > self.memory_size:
            sample_index = np.random.choice(self.memory_size, size=self.batch_size)
        else:
            sample_index = np.random.choice(self.memory_counter, size=self.batch_size)
        batch_memory = self.memory[sample_index, :]

        q_next, q_eval4next = self.sess.run(
            [self.q_next, self.q_eval],
            feed_dict={self.s_: batch_memory[:, -self.n_features:],    # next observation
                       self.s: batch_memory[:, -self.n_features:]})    # next observation
        q_eval = self.sess.run(self.q_eval, {self.s: batch_memory[:, :self.n_features]})

        q_target = q_eval.copy()

        batch_index = np.arange(self.batch_size, dtype=np.int32)
        eval_act_index = batch_memory[:, self.n_features].astype(int)
        reward = batch_memory[:, self.n_features + 1]
        # Double DQN算法和DQN算法的區別。
        if self.double_q:
            max_act4next = np.argmax(q_eval4next, axis=1)        # the action that brings the highest value is evaluated by q_eval
            selected_q_next = q_next[batch_index, max_act4next]  # Double DQN, select q_next depending on above actions
        else:
            selected_q_next = np.max(q_next, axis=1)    # the natural DQN

        q_target[batch_index, eval_act_index] = reward + self.gamma * selected_q_next

        _, self.cost = self.sess.run([self._train_op, self.loss],
                                     feed_dict={self.s: batch_memory[:, :self.n_features],
                                                self.q_target: q_target})
        self.cost_his.append(self.cost)

        self.epsilon = self.epsilon + self.epsilon_increment if self.epsilon < self.epsilon_max else self.epsilon_max
        self.learn_step_counter += 1

主函數入口:

import gym
from Agent import DoubleDQN
import numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf


env = gym.make('Pendulum-v0')
env = env.unwrapped
env.seed(1)
MEMORY_SIZE = 3000
ACTION_SPACE = 11

sess = tf.Session()
with tf.variable_scope('Natural_DQN'):
    natural_DQN = DoubleDQN(
        n_actions=ACTION_SPACE, n_features=3, memory_size=MEMORY_SIZE,
        e_greedy_increment=0.001, double_q=False, sess=sess
    )

with tf.variable_scope('Double_DQN'):
    double_DQN = DoubleDQN(
        n_actions=ACTION_SPACE, n_features=3, memory_size=MEMORY_SIZE,
        e_greedy_increment=0.001, double_q=True, sess=sess, output_graph=True)

sess.run(tf.global_variables_initializer())


def train(RL):
    total_steps = 0
    observation = env.reset()
    while True:
        # if total_steps - MEMORY_SIZE > 8000: env.render()

        action = RL.choose_action(observation)

        f_action = (action-(ACTION_SPACE-1)/2)/((ACTION_SPACE-1)/4)   # convert to [-2 ~ 2] float actions
        observation_, reward, done, info = env.step(np.array([f_action]))

        reward /= 10     # normalize to a range of (-1, 0). r = 0 when get upright
        # the Q target at upright state will be 0, because Q_target = r + gamma * Qmax(s', a') = 0 + gamma * 0
        # so when Q at this state is greater than 0, the agent overestimates the Q. Please refer to the final result.

        RL.store_transition(observation, action, reward, observation_)

        if total_steps > MEMORY_SIZE:   # learning
            RL.learn()

        if total_steps - MEMORY_SIZE > 20000:   # stop game
            break

        observation = observation_
        total_steps += 1
    return RL.q

q_natural = train(natural_DQN)
q_double = train(double_DQN)

plt.plot(np.array(q_natural), c='r', label='natural')
plt.plot(np.array(q_double), c='b', label='double')
plt.legend(loc='best')
plt.ylabel('Q eval')
plt.xlabel('training steps')
plt.grid()
plt.show()

參考文獻:
[1]. Deep Reinforcement Learning with Double Q-learning by Hado van Hasselt and Arthur Guez and David Silver,DeepMind
[2].JUNMO的博客: junmo1215.github.io
[3]. Morvanzhou的Github

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