1. 程式人生 > >學習筆記:強化學習之A3C程式碼詳解

學習筆記:強化學習之A3C程式碼詳解

寫在前面:我是根據莫煩的視訊學習的Reinforce learning,具體程式碼實現包括Q-learning,SARSA,DQN,Policy-Gradient,Actor-Critic以及A3C。(莫凡老師的網站:https://morvanzhou.github.io/tutorials/machine-learning/reinforcement-learning/)
今天先發表最後一個也是最複雜的一個,將來會應用到自己課設中的A3C演算法,以後會把前面幾個程式碼和註解一起釋出。

演算法理解:首先說一下A3C演算法的個人理解,他是基於Actor-Critic的一種改進演算法,主要是利用多執行緒維持多個agent學習並分享各自的經驗,達到更快的學習速率和更好的訓練效用。類似於三個工人和一個管理者共同解決一個難題,三個工人在解決過程中向管理者彙報自己的解題經驗,而管理者彙總三個人的經驗再將總結後的經驗發放給三個工人,讓他們獲得集體的經驗並繼續解決問題。
具體到A3C演算法上,這個演算法維持著一箇中央大腦global,以及n個獨立個體worker(n一般取cpu數量,有多少個cpu就有多少個worker)。global和worker各自維持著結構完全相同的網路,Actor網路和Critic網路。worker通過actor—critic網路獲取近幾個回合所獲得的經驗並上傳到global網路中,global彙集幾個worker的經驗後下發給每一個worker,這裡所說的經驗,其實就是actor和critic網路的引數。
需要著重注意的兩點,首先是這個上傳經驗,上傳的是損失loss對神經網路引數求導所得到的梯度,global從worker中獲取到這個梯度並更新自己的網路引數。下載經驗,是worker直接將global中的神經網路引數全盤接受,並覆蓋當前自己的神經網路actor和critic。
另外,相較於普通的Actor-critic網路,每一個worker沒有自我學習的過程,在Actor-critic中,worker通過optimizer最小化損失loss並更新自己的網路引數。而在A3C演算法中,這一過程被global-net取代了,更新自己的網路引數只需從global中獲取最新的引數即可(集體的智慧)。

具體程式碼
環境:開發環境python3.7。 遊戲環境:gym中的 ‘CartPole-v0’ 一級倒立擺
遊戲環境:一級倒立擺

import multiprocessing  #多執行緒模組
import threading  #執行緒模組
import tensorflow as tf
import numpy as np
import gym
import os
import shutil  #拷貝檔案用
import matplotlib.pyplot as plt

Game='CartPole-v0'
N_workers=multiprocessing.cpu_count()    #獨立玩家個體數為cpu數
MAX_GLOBAL_EP=2000  #中央大腦最大回合數
GLOBALE_NET_SCOPE='Globale_Net' #中央大腦的名字
UPDATE_GLOBALE_ITER=10   #中央大腦每N次提升一次
GAMMA=0.9    #衰減度
LR_A=0.0001   #Actor網路學習率
LR_C=0.001    #Critic 網路學習率

GLOBALE_RUNNING_R=[]   #儲存總的reward
GLOBALE_EP=0   #中央大腦步數

env=gym.make(Game)   #定義遊戲環境


N_S=env.observation_space.shape[0]  #觀測值個數
N_A=env.action_space.n              #行為值個數


class ACnet(object):     #這個class即可用於生產global net,也可生成 worker net,因為結構相同
    def __init__(self,scope,globalAC=None):   #scope 用於確定生成什麼網路
        if scope==GLOBALE_NET_SCOPE:   #建立中央大腦
            with tf.variable_scope(scope):
                self.s=tf.placeholder(tf.float32,[None,N_S],'S')   #初始化state,None代表batch,N—S是每個state的觀測值個數
                self.build_net(scope)                               #建立中央大腦神經網路
                self.a_params=tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES,scope=scope+'/actor')
                self.c_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope + '/critic')
                #定義中央大腦actor和critic的引數


        else:                        #建立worker兩個網路的具體步驟
            with tf.variable_scope(scope):    #這裡的scope傳入的是worker的名字
                self.s=tf.placeholder(tf.float32,[None,N_S],'S')  #初始化state
                self.a_his = tf.placeholder(tf.int32, [None,1], 'A_his')         #初始化action,是一個[batch,1]的矩陣,第二個維度為1,
                                                                                  #格式類似於[[1],[2],[3]]
                self.v_target=tf.placeholder(tf.float32,[None,1],'Vtarget')     #初始化v現實(V_target),資料格式和上面相同


                self.acts_prob,self.v=self.build_net(scope)   #建立神經網路,acts_prob為返回的概率值,v為返回的評價值
                self.a_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope + '/actor')
                self.c_params = tf.get_collection(tf.GraphKeys.TRAINABLE_VARIABLES, scope=scope + '/critic')


                td=tf.subtract(self.v_target,self.v,name='TD_error')  #計算td—error即v現實和v估計之差
                                                                      #v—target和v都是一串值,v-target(現實)已經計算好並傳入了,v估計由傳入的
                                                                      #一系列state送入critic網路確定

                with tf.name_scope('c_loss'):    #計算Critic網路的loss
                    self.c_loss=tf.reduce_mean(tf.square(td))  #Critic的loss就是td—error加平方避免負數


                with tf.name_scope('a_loss'):    #計算actor網路的損失
                    log_prob = tf.reduce_sum(tf.log(self.acts_prob +1e-5)*tf.one_hot(self.a_his,N_A,dtype=tf.float32),axis=1,keep_dims=True)
                    #這裡是矩陣乘法,目的是篩選出本batch曾進行的一系列選擇的概率值,acts—prob類似於一個向量[0.3,0.8,0.5],
                    #one—hot是在本次進行的的操作置位1,其他位置置為0,比如走了三次a—his為[1,0,3],N—A是4,則one—hot就是[[0,1,0,0],[1,0,0,0],[0,0,0,1]]
                    #相乘以後就是[[0,0.3,0,0],[0.8,0,0,0],[0,0,0,0.5]],log_prob就是計算這一系列選擇的log值。

                    self.exp_v = log_prob * td     #td決定梯度下降的方向
                    self.a_loss=tf.reduce_mean(-self.exp_v)    #計算actor網路的損失a-loss


                with tf.name_scope('local_grad'):
                    self.a_grads=tf.gradients(self.a_loss,self.a_params)   #實現a_loss對a_params每一個引數的求導,返回一個list
                    self.c_grads=tf.gradients(self.c_loss,self.c_params)   #實現c_loss對c_params每一個引數的求導,返回一個list


            with tf.name_scope('sync'):   #worker和global的同步過程
                with tf.name_scope('pull'):   #獲取global引數,複製到local—net
                    self.pull_a_params_op=[l_p.assign(g_p) for l_p,g_p in zip(self.a_params,globalAC.a_params)]
                    self.pull_c_params_op=[l_p.assign(g_p) for l_p,g_p in zip(self.c_params,globalAC.c_params)]
                with tf.name_scope('push'):   #將引數傳送到gloabl中去
                    self.update_a_op=OPT_A.apply_gradients(zip(self.a_grads,globalAC.a_params))
                    self.update_c_op = OPT_C.apply_gradients(zip(self.c_grads, globalAC.c_params))
                    #其中傳送的是local—net的actor和critic的引數梯度grads,具體計算在上面定義
                    #apply_gradients是tf.train.Optimizer中自帶的功能函式,將求得的梯度引數更新到global中


    def build_net(self,scope): #建立神經網路過程
        w_init=tf.random_normal_initializer(0.,.1)  #初始化神經網路weights
        with tf.variable_scope('actor'):            #actor神經網路結構
            l_a=tf.layers.dense(inputs=self.s,units=200,activation=tf.nn.relu6,
                                kernel_initializer=w_init,bias_initializer=tf.constant_initializer(0.1),name='la')  #建立第一層神經網路
            acts_prob=tf.layers.dense(inputs=l_a,units=N_A,activation=tf.nn.softmax,
                               kernel_initializer=w_init,bias_initializer=tf.constant_initializer(0.1),name='act_prob')  #第二層神經網路其中之一輸出為動作的均值

        with tf.variable_scope('critic'):     #critic神經網路結構,輸入為位置的觀測值,輸出為評價值v
            l_c=tf.layers.dense(self.s,20,tf.nn.relu6,kernel_initializer=w_init,bias_initializer=tf.constant_initializer(0.1),name='lc')  #建立第一層神經網路
            v=tf.layers.dense(l_c,1,kernel_initializer=w_init,bias_initializer=tf.constant_initializer(0.1),name='v')   #第二層神經網路

        return acts_prob,v    #建立神經網路後返回的是輸入當前state得到的actor網路的動作概率和critic網路的v估計


    def update_global(self,feed_dict):    #定義更新global引數函式
        SESS.run([self.update_a_op,self.update_c_op],feed_dict)    #分別更新actor和critic網路

    def pull_global(self):   #定義更新local引數函式
        SESS.run([self.pull_a_params_op,self.pull_c_params_op])

    def choose_action(self,s):   #定義選擇動作函式
        s=s[np.newaxis, :]
        probs=SESS.run(self.acts_prob, feed_dict={self.s: s})
        return np.random.choice(range(probs.shape[1]), p=probs.ravel())   #從probs中按概率選取出某一個動作



class Worker(object):
    def __init__(self,name,globalAC):    #傳入的name是worker的名字,globalAC是已經建立好的中央大腦GLOBALE—AC
        self.env=gym.make(Game).unwrapped
        self.name=name                   #worker的名字
        self.AC=ACnet(name,globalAC)     #第二個引數當傳入的是已經建立好的GLOBALE—AC時建立的是local net
                                         #建立worker的AC網路

    def work(self):   #定義worker執行的的具體過程
        global  GLOBALE_RUNNING_R,GLOBALE_EP   #兩個全域性變數,R是所有worker的總reward,ep是所有worker的總episode
        total_step=1                            #本worker的總步數
        buffer_s,buffer_a,buffer_r=[],[],[]    #state,action,reward的快取

        while not COORD.should_stop() and GLOBALE_EP<MAX_GLOBAL_EP:   #停止本worker執行的條件
                                                                     #本迴圈一次是一個回合

            s=self.env.reset()       #初始化環境
            ep_r=0                   #本回合總的reward

            while True:      #本迴圈一次是一步
                if self.name=='W_0':    #只有worker0才將動畫影象顯示
                    self.env.render()

                a=self.AC.choose_action(s)    #將當前狀態state傳入AC網路選擇動作action

                s_,r,done,info=self.env.step(a)   #行動並獲得新的狀態和回報等資訊

                if done:r=-5    #如果結束了,reward給一個懲罰數

                ep_r+=r              #記錄本回合總體reward
                buffer_s.append(s)   #將當前狀態,行動和回報加入快取
                buffer_a.append(a)
                buffer_r.append(r)


                if total_step % UPDATE_GLOBALE_ITER==0 or done:  #每iter步完了或者或者到達終點了,進行同步sync操作
                    if done:
                        v_s_=0   #如果結束了,設定對未來的評價值為0
                    else:
                        v_s_=SESS.run(self.AC.v,feed_dict={self.AC.s:s_[np.newaxis,:]})[0,0]   #如果是中間步驟,則用AC網路分析下一個state的v評價

                    buffer_v_target=[]
                    for r in buffer_r[::-1]:    #將下一個state的v評價進行一個反向衰減傳遞得到每一步的v現實
                        v_s_=r + GAMMA* v_s_
                        buffer_v_target.append(v_s_)  #將每一步的v現實都加入快取中
                    buffer_v_target.reverse()    #反向後,得到本系列操作每一步的v現實(v-target)

                    buffer_s,buffer_a,buffer_v_target=np.vstack(buffer_s),np.vstack(buffer_a),np.vstack(buffer_v_target)

                    feed_dict={
                        self.AC.s:buffer_s,                 #本次走過的所有狀態,用於計算v估計
                        self.AC.a_his:buffer_a,             #本次進行過的所有操作,用於計算a—loss
                        self.AC.v_target:buffer_v_target    #走過的每一個state的v現實值,用於計算td
                    }

                    self.AC.update_global(feed_dict)  #update—global的具體過程在AC類中定義,feed-dict如上

                    buffer_s,buffer_a,buffer_r=[],[],[]   #清空快取

                    self.AC.pull_global()    #從global—net提取出引數賦值給local—net

                s=s_   #跳轉到下一個狀態
                total_step+=1  #本回合總步數加1


                if done:   #如果本回合結束了
                    if len(GLOBALE_RUNNING_R)==0:  #如果尚未記錄總體running
                        GLOBALE_RUNNING_R.append(ep_r)
                    else:
                        GLOBALE_RUNNING_R.append(0.9*GLOBALE_RUNNING_R[-1]+0.1*ep_r)

                    print(self.name,'EP:',GLOBALE_EP)
                    GLOBALE_EP+=1       #加一回合
                    break   #結束本回合



if __name__=='__main__':
    SESS=tf.Session()

    with tf.device('/cpu:0'):
        OPT_A=tf.train.RMSPropOptimizer(LR_A,name='RMSPropA')    #定義actor訓練過程,後續主要是使用該optimizer中的apply—gradients操作
        OPT_C = tf.train.RMSPropOptimizer(LR_C, name='RMSPropC')  #定義critic訓練過程
        GLOBALE_AC=ACnet(GLOBALE_NET_SCOPE)  #建立中央大腦GLOBALE_AC,只建立結構(A和C的引數)
        workers=[]
        for i in range(N_workers):    #N—workers等於cpu數量
            i_name='W_%i'%i   #worker name
            workers.append(Worker(name=i_name,globalAC=GLOBALE_AC))   #建立獨立的worker

        COORD=tf.train.Coordinator()    #多執行緒
        SESS.run(tf.global_variables_initializer())   #初始化所有引數

        worker_threads=[]
        for worker in workers:    #並行過程
            job= lambda:worker.work()   #worker的工作目標,此處呼叫Worker類中的work
            t=threading.Thread(target=job)  #每一個執行緒完成一個worker的工作目標
            t.start()                 # 啟動每一個worker
            worker_threads.append(t)   #每一個worker的工作都加入thread中
        COORD.join(worker_threads)     #合併幾個worker,當每一個worker都執行完再繼續後面步驟

        plt.plot(np.arange(len(GLOBALE_RUNNING_R)),GLOBALE_RUNNING_R)   #繪製reward影象
        plt.xlabel('step')
        plt.ylabel('Total moving reward')
        plt.show()

寫在後面:這是A3C演算法的實現,也算是做過裡面最複雜的一個了,需要Actor-critic的基礎(後面還會貼上其他幾個RL演算法)。當中也遇到了很多問題,首先是tensorflow運用不熟練,由於是從RL才開始真正使用tensorflow框架,所以對其中很多東西理解不深,比如張量的資料型別,由於不像numpy等可以直接輸出,張量的資料型別全靠理解,佔位符的各種運算也基於對張量的型別理解,因此比較吃力。其次是TF中眾多的函式還需要加深理解,像本程式碼中的tf.train.Optimizer中地apply-gradient這些函式過去都沒有遇到過。另外是面向物件的思想,由於java是我們的選修課,儘管放假時再次學習了java基礎,但對於面向物件的程式設計使用的不多,而這樣的程式設計又是現在軟體開發的主流思想,因此也需要多多學習。
在RL的學習過程中,萬分感謝莫煩老師的教程,相比於其他尚不成熟的RL學習書籍,莫煩老師的教程更加清晰明瞭,配合程式碼能夠理解的較為深入。其次是學長聰哥的答疑解惑,很多問題都是在聰哥的點撥下才理解清楚,在這裡表示感謝!另外有任何指正或者建議意見都可以留在下面一起討論!謝謝!