1. 程式人生 > >深度增強學習DDPG演算法原始碼走讀

深度增強學習DDPG演算法原始碼走讀

環境setup好後,執行下面命令即可以開始訓練:

python3 -m baselines.ddpg.main

你可能會碰到下面錯誤:

gym.error.DeprecatedEnv: Env HalfCheetah-v1 not found (valid versions include ['HalfCheetah-v2'])

沒關係,把HalfCheetah-v1改成HalfCheetah-v2就行。可能由於Gym升級導致。

Gym中渲染時可能出現這個錯誤:ERROR: GLEW initalization error: Missing GL version。根據https://blog.csdn.net/gsww404/article/details/80636676,執行以下命令即可(如果裝的是Nvidia的卡):

export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libGLEW.so:/usr/lib/nvidia-384/libGL.so

接下去開始看程式碼實現流程。首先從檔案main.py開始。這裡主要解析引數。如果使用MPI的話確保只有第一個程序呼叫logger.configure()函式來初始化日誌。這裡基於mpi4py來使用MPI進行多程序加速。比如可以執行下面命令起兩個程序來訓練:

mpirun -np 2 python3 -m baselines.ddpg.main

緊接著進入到主要函式run()中。

def run(env_id, seed, ...):
    env =
gym.make(env_id) env = bench.Monitor(env, ...) # env.action_space.shape為(6,) nb_actions = env.action_space.shape[-1] # 往引數加入噪聲來鼓勵exploration。預設引數noise-type為adaptive-param_0.2, # 因此這裡建立AdaptiveParamNoiseSpec()。 param_noise = AdaptiveParamNoiseSpec() # 構建replay buffer。
memory = Memory(limit=int(1e6)...) # 構建Actor和Critic網路。實現在models.py中。 critic = Critic(layer_norm=layer_norm) actor = Actor(nb_actions, layer_norm=layer_norm) training.train(env=env, ...)

AdaptiveParamNoiseSpec的實現在noise.py檔案中。noise.py中實現了三種新增噪聲的方式,詳細可參見論文《Parameter Space Noise for Exploration》。

  • AdaptiveParamNoiseSpec: 詳見論文P3中Adaptive noise scaling一節,先定義擾動和非擾動動作空間策略的距離,然後根據這個距離是否高於指定閥值(程式碼中的desired_action_stddev)來調節引數噪聲的大小(程式碼中的current_stddev)。
  • NormalActionNoise: 根據給定的均值和方差從正態分佈中取樣。
  • OrnSteinUhlenbeckActionNoise: 使用Ornstein-Uhlenbeck process取樣。這也是原版DDPG中用於動作探索時用的方法。

第一種為引數噪聲方法,即通過在策略神經網路的引數中加入噪聲,進而達到對該網路產生的動作加入噪聲的效果;後兩種為動作噪聲方法,即直接對網路輸出的動作加噪聲。

初始化程式碼中的Memory就是replay buffer,實現在memory.py檔案,它用以實現experience replay。Experience replay之前應用在DQN演算法過,被證明可以有效減少樣本時序上的關聯,從而提高訓練的穩定性。它的結構主要就是一系列的RingBuffer(大小預設為100W),分別儲存當前和下一觀察狀態、動作、回報和是否終結資訊。它的兩個主要函式append()和sample()分別用於往這個buffer中新增資料,和從中進行取樣(一個batch)。

訓練的過程在training.py檔案中,先來看其初始化過程:

def train(env, ...):
    # 實現位於ddpg.py中。
    agent = DDPG(actor, critic, memory, ...)
        ...
        # 建立actor和critic的目標網路(target network)。.
        target_actor = copy(actor)
        self.target_actor = target_actor
        target_critic = copy(critic)
        self.target_critic = target_critic
        
        ...
        if self.param_noise is not None:
            # 引數加噪,詳見《Parameter Space Noise for Exploration》。
            self.setup_param_noise(normalized_obs0)
        # 建立actor和critic網路優化器。
        self.setup_actor_optimizer()
        self.setup_critic_optimizer()
        if self.normalize_returns and self.enable_popart:
            # 目標歸一化,詳見《Learning values across many orders of magnitude》。
            self.setup_popart() 
        # 用於線上網路(或稱主網路)向目標網路同步引數。
        self.setup_target_network_updates()
        

setup_param_noise()函式中主要處理引數加噪。首先它把actor網路拷貝一份到param_noise_actor。當開啟引數加噪選項時,訓練過程中要得到動作就是從該網路中獲得。使用這個加噪處理過的actor網路得到擾動後動作的TF操作為perturbed_actor_tf。接下去,get_perturbed_actor_updates()函式對於perturbed_actor中的所有變數新增一個正態分佈的噪聲,其中的標準差引數來自於前面提到的param_noise_stddev,最後把這些更新操作返回放到perturb_policy_ops當中。此外,為了學習加噪時所用標準差,還需要將actor網路拷貝一份,稱為adaptive_param_noise_actor,這個actor網路主要用於計算經加噪後輸出的擾動動作與原動作的距離(用L2範數為測度),這個距離用來調節加噪時的標準差引數。

函式setup_actor_optimizer()和setup_critic_optimizer()分別用來建立actor和critic網路的優化器。這裡主要分別實現論文中DDPG演算法中actor的引數梯度(sampled policy gradient):

θμJ1NiaQ(s,aθQ)s=si,a=μ(si)θμμ(sθμ)si \nabla_{\theta^\mu} J \approx \frac{1}{N} \sum_{i} \nabla_a Q(s,a | \theta^Q)|_{s=s_i, a= \mu(s_i)} \nabla_{\theta^\mu} \mu(s|\theta^\mu) |_{s_i}
和critic的引數梯度:
θQL=θQ[1NiyiQ(si,aiθQ))2] \nabla_{\theta^Q} L = \nabla_{\theta^Q} [\frac{1}{N} \sum_i y_i - Q(s_i, a_i | \theta^Q))^2 ]
其中yi=ri+γQ(si+1,μ(si+1θμ)θQ)y_i = r_i + \gamma Q'(s_{i+1}, \mu'(s_{i+1} | \theta^{\mu'}) | \theta^{Q'})。以actor網路優化器為例,通過flatgrad()函式先求actor_loss對actor網路中可學習引數的梯度,然後進行clip再flatten,返回存於actor_grads。actor_optimizer是型別為MpiAdam的優化器,建立時傳入Adam所需引數。這個MpiAdam與傳統Adam優化器相比主要區別是分散式的。因為訓練在多個程序中同時進行,因此在其引數更新函式update()中,它先會用Allreduce()函式將所有區域性梯度匯成全域性梯度,然後按Adam的規則更新引數。

setup_target_network_update()函式構建線上網路更新目標網路的操作。DeepMind在論文《Human-level control through deep reinforcement learning》中引入了目標網路提高訓練的穩定性。目標網路和線上網路結構是一樣的,只是每過指定步後,線上網路的引數會以θtarget=(1τ)θtarget+τθonline\theta_{target} = (1 - \tau) \theta_{target} + \tau \theta_{online}的形式拷貝給目標網路。

再看訓練主迴圈(位於training.py檔案):

def train(env, ...):
    ...
    agent.initialize(sess)
    agent.reset()
    obs = env.reset()
    ...
    for epoch in range(nb_epochs): # 500
        for cycle in range(nb_epoch_cycles): # 20
            for t_rollout in range(nb_rollout_steps): # 100
                # pi()函式位於ddpg.py,主要就是執行一把actor網路,得到動作與Q值。
                action, q = agent.pi(obs, apply_noise=True, compute_Q=True)
                    action, q = self.sess.run([actor_tf, self.critic_with_actor_tf], ...)
                ...
                # 首先將actor網路輸出的動作按env中動作空間進行scale,
                # 然後在env中按此動作執行一步,並獲得觀察、回報等資訊。
                new_obs, r, done, info = env.step(max_action * action)
                ...
                # 將每一步的資訊記錄到replay buffer中。
                epoch_actions.append(action)
                epoch_qs.append(q)
                agent.store_transition(obs, action, r, new_obs, done)
                    memory.append(obs0, action, reward, obs1, terminal1)
                obs = new_obs
                
                if done:
                    # episode結束,重置環境。
                    ...
                    agent.reset()
                    obs = env.reset()
                    
            # 訓練
            for t_train in range(nb_train_steps): # 50
                # batch_size = 64
                if memory.nb_entries >= batch_size and t_train % param_noise_adaption_interval == 0:
                    distance = agent.adapt_param_noise()
                    ...
                # 返回critic和actor網路的loss。然後將它們分別存入epoch_critic_losses和epoch_actor_losses。
                cl, al = agent.train()
                ...
                agent.update_traget_net()
	                self.sess.run(self.target_soft_updates)
                
            # 評估
            for t_rollout in range(nb_eval_steps):
                eval_action, eval_q = agent.pi(...)
                eval_obs, eval_r, eval_done, eval_info = eval_env.step(max_action * eval_action)

上面流程中,先呼叫agent的initialize()函式進行初始化。

def initialize(self, sess):
    self.sess = sess
    self.sess.run(tf.global_variables_initializer())
    self.actor_optimizer.sync()
    self.critic_optimizer.sync()
    self.sess.run(self.target_init_updates)

除了TF中常規初始化操作外,接下來呼叫actor和critic優化器的sync()函式。由於這裡的優化器使用的是MpiAdam,sync()函式中會將網路中引數進行flatten然後廣播給其它程序,其它程序會把這些引數unflatten到自己程序中的網路中。最後執行target_init_updates操作將線上網路中的引數拷貝到目標網路中。

接下來的reset()函式主要是了新增動作噪聲。這裡假設使用的是引數加噪,執行perturb_policy_ops為actor網路引數新增噪聲。

然後開始訓練。這個訓練過程有幾層迴圈。最外層為epoch,迴圈nb_epochs次。每次epoch最後會作一些統計。第二層迴圈為cycle,迴圈nb_epoch_cycles次。這個迴圈的每次迭代中,主要分三步:

一、執行rollout指定步數(nb_rollout_steps)。這部分的主要作用是採集訓練樣本。

  1. 呼叫agent.pi()根據當前的觀察狀態ss得到agent的動作aa。這裡實際是做了actor網路(這個網路的引數是加噪的)的inference得到動作aa,然後根據觀察狀態和動作通過critic網路得到Q(s,a)Q(s,a)的估計。
  2. 如果是當前為MPI的rank 0程序且渲染選項開啟,則呼叫env.render()畫出當前狀態。
  3. 呼叫env.step讓agent在Gym執行環境中執行上面步驟1中選取的動作aa,並返回下一觀察狀態,回報和episode是否結束等資訊。
  4. 把這次狀態轉移中的各項資訊(s,s,a,r,done)(s, s', a, r, done)通過agent.store_transition()存到replay buffer中。留作之後訓練使用。
  5. 如果Gym中執行後發現episode結束,則將episode相關資訊記錄後呼叫agent.reset()和env.reset()。它們分別對actor網路進行引數加噪,和Gym執行環境的重置。

二、訓練更新引數。訓練nb_train_steps輪。每一輪中:

  1. 當已有sample能夠填滿一個batch,且執行param_noise_adaption_interval步訓練,呼叫agent.adapt_param_noise()函式。這個函式主要用來自適應引數加噪時用的標準差。前面提到在setup_param_noise()函式中建立了adaptive_actor_tf,這時候就會對它進行引數加噪,然後基於sample batch中的觀察狀態得到其動作,再與原始actor網路輸出動作求距離。求出距離後,呼叫param_noise(即AdaptiveParamNoiseSpec)的adapt()函式調整引數加噪的標準差。
  2. 執行agent.train()學習網路引數。
# 先從replay buffer中取batch_size大小的樣本。
batch = self.memory.sample(batch_size=self.batch_size)
# 根據選項判斷是否要用Pop-Art演算法對critic網路輸出(即Q函式值)進行normalization。
# 這裡因為預設都為false,所以先忽略。
if self.normalize_returns and self.enable_popart:
    ...
else
    # 先以觀察狀態obs1為輸入通過目標critic網路得到Q函式估計值,
    # 然後考慮回報通過差分更新公式得到目標Q函式值。
    target_Q = self.sess.run(self.target_Q, feed_dict={
        self.obs1: batch['obs1'],
        self.rewards: batch['rewards'],
        self.terminals1: batch['terminals1'].astype('float32'),
    })
# Get all gradients and perform a synced update.
ops = [self.actor_grads, self.actor_loss, self.critic_grads, self.critic_loss]
# 計算actor和critic網路的引數梯度以及loss計算。
actor_grads, actor_loss, critic_grads, critic_loss = self.sess.run(ops, feed_dict={
    self.obs0: batch['obs0'],
    self.actions: batch['actions'],
    self.critic_target: target_Q,
})
# 執行引數的更新。如果是通過MPI多程序訓練,這裡呼叫Allreduce將各個程序
# 中actor和critic的梯度先收集起來求平均,然後使用Adam優化方式更新引數。
self.actor_optimizer.update(actor_grads, stepsize=self.actor_lr)
self.critic_optimizer.update(critic_grads, stepsize=self.critic_lr)
return critic_loss, actor_loss

3 . 記錄actor和critic網路的loss,最後呼叫agent.update_target_net()函式進行DDPG演算法中線上網路到目標網路的引數更新:
θQ=τθQ+(1τ)θQ\theta^{Q'} = \tau \theta^Q + (1-\tau) \theta^{Q'}
θμ=τθμ+(1τ)θμ\theta^{\mu'} = \tau \theta^\mu + (1 - \tau) \theta^{\mu'}

三、評估當前模型。大體過程是在Gym環境中執行nb_eval_steps步,每一步動作通過actor網路得到(這時不用引數加噪聲,因為不是訓練),最後統計相關資訊,如episode中reward的累積,與Q函式值等。

整個演算法大體的流轉如下圖:
這裡寫圖片描述

另外,可以看到,在上述實現中,還整合了其它幾篇論文中的idea:

《Learning values across many orders of magnitude》

在Atari中,回報會被clip到特定範圍。這種做法有利於讓多個遊戲使用同一演算法,但是對回報進行clipping會產生不同的行為。自適用歸一化(adaptive normalization)能移除這種領域相關的啟發,且不降低效果。對於輸入和層輸出的歸一化已經研究得比較多了,但對目標的歸一化研究得並不多。文中對目標 YtY_t進行仿射變換:Y~t=Σt1(Ytμt)\tilde{Y}_t = \Sigma^{-1}_t (Y_t - \mu_t)。如果記gg為未歸一化函式,而ff為歸一化函式,對於輸入xx的未歸一化近似可寫成f(x)=Σg(x)+μf(x) = \Sigma g(x) + \mu。損失函式可以通過g(Xt)g(X_t)和歸一化目標Y~t\tilde{Y}_t來定義。但是,這裡似乎又引入了scale Σ\Sigma