深度增強學習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):
和critic的引數梯度:
其中。以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》中引入了目標網路提高訓練的穩定性。目標網路和線上網路結構是一樣的,只是每過指定步後,線上網路的引數會以的形式拷貝給目標網路。
再看訓練主迴圈(位於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)。這部分的主要作用是採集訓練樣本。
- 呼叫agent.pi()根據當前的觀察狀態得到agent的動作。這裡實際是做了actor網路(這個網路的引數是加噪的)的inference得到動作,然後根據觀察狀態和動作通過critic網路得到的估計。
- 如果是當前為MPI的rank 0程序且渲染選項開啟,則呼叫env.render()畫出當前狀態。
- 呼叫env.step讓agent在Gym執行環境中執行上面步驟1中選取的動作,並返回下一觀察狀態,回報和episode是否結束等資訊。
- 把這次狀態轉移中的各項資訊通過agent.store_transition()存到replay buffer中。留作之後訓練使用。
- 如果Gym中執行後發現episode結束,則將episode相關資訊記錄後呼叫agent.reset()和env.reset()。它們分別對actor網路進行引數加噪,和Gym執行環境的重置。
二、訓練更新引數。訓練nb_train_steps輪。每一輪中:
- 當已有sample能夠填滿一個batch,且執行param_noise_adaption_interval步訓練,呼叫agent.adapt_param_noise()函式。這個函式主要用來自適應引數加噪時用的標準差。前面提到在setup_param_noise()函式中建立了adaptive_actor_tf,這時候就會對它進行引數加噪,然後基於sample batch中的觀察狀態得到其動作,再與原始actor網路輸出動作求距離。求出距離後,呼叫param_noise(即AdaptiveParamNoiseSpec)的adapt()函式調整引數加噪的標準差。
- 執行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演算法中線上網路到目標網路的引數更新:
三、評估當前模型。大體過程是在Gym環境中執行nb_eval_steps步,每一步動作通過actor網路得到(這時不用引數加噪聲,因為不是訓練),最後統計相關資訊,如episode中reward的累積,與Q函式值等。
整個演算法大體的流轉如下圖:
另外,可以看到,在上述實現中,還整合了其它幾篇論文中的idea:
《Learning values across many orders of magnitude》
在Atari中,回報會被clip到特定範圍。這種做法有利於讓多個遊戲使用同一演算法,但是對回報進行clipping會產生不同的行為。自適用歸一化(adaptive normalization)能移除這種領域相關的啟發,且不降低效果。對於輸入和層輸出的歸一化已經研究得比較多了,但對目標的歸一化研究得並不多。文中對目標 進行仿射變換:。如果記為未歸一化函式,而為歸一化函式,對於輸入的未歸一化近似可寫成。損失函式可以通過和歸一化目標來定義。但是,這裡似乎又引入了scale