功能描述:
OpenAI出品的baselines項目提供了一系列deep reinforcement learning(DRL,深度強化學(xué)習(xí)或深度增強學(xué)習(xí))算法的實現(xiàn)。現(xiàn)在已經(jīng)有包括DQN,DDPG,TRPO,A2C,ACER,PPO在內(nèi)的近十種經(jīng)典算法實現(xiàn),同時它也在不斷擴充中。它為對DRL算法的復(fù)現(xiàn)驗證和修改實驗提供了很大的便利。本文主要走讀其中的PPO(Proximal Policy Optimization)算法的源碼實現(xiàn)。PPO是2017年由OpenAI提出的一種DRL算法,它不僅有很好的performance(尤其是對于連續(xù)控制問題),同時相較于之前的TRPO方法更加易于實現(xiàn)。之前寫過一篇雜文《深度增強學(xué)習(xí)(DRL)漫談 - 信賴域(Trust Region)系方法》對其歷史、原理及相關(guān)方法做了簡單介紹,因此本文主要focus在代碼實現(xiàn)的學(xué)習(xí)了解上。
OpenAI baselines項目中對于PPO算法有兩個實現(xiàn),分別位于ppo1和ppo2目錄下。其中ppo2是利用GPU加速的,官方號稱會快三倍左右,所以下面主要是看ppo2。對應(yīng)論文為《Proximal Policy Optimization Algorithms》,以下簡稱PPO論文。本文我們就以atari這個經(jīng)典的DRL實驗場景為例看一下大體流程。啟動訓(xùn)練的命令在readme中有:
def main():
# 實現(xiàn)位于common/cmd_util.py。它主要為parser添加幾個參數(shù):
# 1) env:代表要執(zhí)行atari中的哪個游戲環(huán)境。默認(rèn)為BreakoutNoFrameskip-v4,即“打磚塊”。
# 2) seed:隨機種子。默認(rèn)為0。
# 3) num-timesteps:訓(xùn)練的頻數(shù)。默認(rèn)為10M次。
parser = atari_arg_parser()
# 通過參數(shù)選擇policy network的形式,實現(xiàn)在policies.py。默認(rèn)為CNN。這里有三種選擇:
# 1) CNN:相應(yīng)函數(shù)為CnnPolicy()。發(fā)表于《Nature》上的經(jīng)典DRL奠基論文《Human-level control through
# deep reinforcement learning》中使用的神經(jīng)網(wǎng)絡(luò)結(jié)構(gòu):conv->relu->conv->relu->conv->relu->
# fc->relu。注意它是雙頭網(wǎng)絡(luò),一頭輸出policy,一頭輸出value。
# 2) LSTM:相應(yīng)函數(shù)為LstmPolicy()。它將CNN的輸出之上再加上LSTM層,這樣就結(jié)合了時間域的信息。
# 3) LnLSTM:相應(yīng)函數(shù)為LnLstmPolicy()。其它的和上面一樣,只是在構(gòu)造LSTM層時添加了Layer normalization
# (詳見論文《Layer Normalization》)
parser.add_argument('--policy', help='Policy architecture', choices=['cnn', 'lstm', 'lnlstm'], default='cnn')
# 用剛才的構(gòu)建的parser解析命令行傳入的參數(shù)。
args = parser.parse_args()
# 這個項目中實現(xiàn)了簡單的日志系統(tǒng)。其中日志所在目錄和格式可以用過OPENAI_LOGDIR和OPENAI_LOG_FORMAT兩個環(huán)境
# 變量控制。實現(xiàn)類Logger中主要有兩個字典:name2val和name2cnt。它們分別是名稱到值和計數(shù)的映射。
logger.configure()
# 這里是開始正式訓(xùn)練了。
train(args.env, num_timesteps=args.num_timesteps, seed=args.seed,
policy=args.policy)
---------------------
def train(env_id, num_timesteps, seed, policy):
# 首先是一坨和TensorFlow相關(guān)的環(huán)境設(shè)置,比如根據(jù)cpu核數(shù)設(shè)定并行線程數(shù)等。
...
# 構(gòu)建運行環(huán)境。流程還比較長,下面會再詳細(xì)地理下。
env = VecFrameStack(make_atari_env(env_id, 8, seed), 4)
# 對應(yīng)前面說的三種策略網(wǎng)絡(luò)。用于根據(jù)參數(shù)選取相應(yīng)的實現(xiàn)函數(shù)。
policy = {'cnn' : CnnPolicy, 'lstm' : LstmPolicy, 'lnlstm' : LnLstmPolicy}[policy]
# 使用PPO算法進行學(xué)習(xí)。其中傳入的參數(shù)不少是模型的超參數(shù)。詳細(xì)可參見PPO論文中的Table 5。
ppo2.learn(policy=policy, env=env, nsteps=128, nminibatches=4,
lam=0.95, gamma=0.99, noptepochs=4, log_interval=1,
ent_coef=.01,
lr=lambda f : f * 2.5e-4,
cliprange=lambda f : f * 0.1,
total_timesteps=int(num_timesteps * 1.1))
---------------------
# make_atari_env()函數(shù)實現(xiàn)位于common/cmd_util.py。看函數(shù)名就知道主要就是創(chuàng)建atari環(huán)境。通過OpenAI gym
# 創(chuàng)建基本的atari環(huán)境后,還需要層層封裝。gym中提供了Wrapper接口,讓開發(fā)者通過decorator設(shè)計模式來改變環(huán)境中的設(shè)# 定。
def make_atari_env(env_id, num_env, ...): # 這里的num_env為8,意味著會創(chuàng)建8個獨立的并行運行環(huán)境。
def make_env(rank):
def _thunk():
# 創(chuàng)建由gym構(gòu)建的atari環(huán)境的封裝類。
env = make_atari(env_id):
# 通過OpenAI的gym接口創(chuàng)建gym環(huán)境。
env = gym.make(env_id)
# NoopResetEnv為gym.Wrapper的繼承類。每次環(huán)境重置(調(diào)用reset())時執(zhí)行指定步隨機動作。
env = NoopResetEnv(env)
# MaxAndSkipEnd也是gym.Wrapper的繼承類。每隔4幀返回一次。返回中的reward為這4幀reward
# 之和,observation為最近兩幀中最大值。
env = MaxAndSkipEnd(env)
return env
# 每個環(huán)境選取不同的隨機種子,避免不同環(huán)境跑得都一樣。
env.seed(seed + rank)
# 實現(xiàn)在monitor.py中。Monitor為gym中Wrapper的繼承類,對環(huán)境Env進行封裝,主要添加了對
# episode結(jié)束時信息的記錄。
env = Monitor(env, ...)
return wrap_deepmind(env, ...):
# 標(biāo)準(zhǔn)情況下,對于atari中的很多游戲(比如這兒的打磚塊),命掉光了(如該游戲有5條命)算episode
# 結(jié)束,環(huán)境重置。這個Wrapper的作用是只要掉命就讓step()返回done,但保持環(huán)境重置的時機不變
#(仍然是命掉完時)。原注釋中說這個trick在DeepMind的DQN中用來幫助value的估計。
env = EpisodeicLifeEnv(env)
# 通過OpenCV將原始輸入轉(zhuǎn)成灰度圖,且轉(zhuǎn)成84 x 84的分辨率。
env = WarpFrame(env)
# 將reward按正負(fù)值轉(zhuǎn)為+1, -1和0。
env = ClipRewardEnv(env)
...
return env
return _thunk
...
# 返回SubprocVecEnv對象。
return SubprocVecEnv([make_env(i + start_index) for i in range(num_env)])
---------------------
創(chuàng)建num_env個元素(這里為8)的數(shù)組,每一個元素為一個函數(shù)閉包_thunk()。VecEnv實現(xiàn)在baselines/common/vec_env/__init__.py,它是一個抽象類,代表異步向量化環(huán)境。其中包括幾個重要的抽象函數(shù):
reset()用于重置所有環(huán)境,step_async()用于通知所有環(huán)境開始根據(jù)給定動作執(zhí)行一步,step_wait()得到執(zhí)行完的結(jié)果。step_wait()等待step_async()的結(jié)果。step()就是step_async() 加上step_wait()。而VecEnvWrapper也為VecEnv的繼承類,和gym中提供的Wrapper功能類似,如果要對VecEnv實現(xiàn)的默認(rèn)行為做修改的話就可以利用它。
上面函數(shù)最后返回的SubprocVecEnv類為VecEnv的繼承類,它主要將上面創(chuàng)建好的函數(shù)放到各個子進程中去執(zhí)行。在SubprocVecEnv實現(xiàn)類中,構(gòu)造時傳入在子進程中執(zhí)行的函數(shù)。通過Process創(chuàng)建子進程,并通過Pipe進行進程間通信。make_atari_env()中創(chuàng)建SubprocVecEnv后,又立馬被VecFrameStack封裝了一把。VecFrameStack為VecEnvWrapper的實現(xiàn)類,實現(xiàn)在vec_frame_stack.py。在VecFrameStack的構(gòu)造函數(shù)中,wos為gym環(huán)境中的原始狀態(tài)空間,維度為[84,84,1]。low和high分別為這些維度的最低和最高值。stackedobs就是把幾個環(huán)境的狀態(tài)空間疊加起來,即維度變?yōu)?8, 84, 84, 4)。8為環(huán)境個數(shù),(84,84)為單幀狀態(tài)維度,也就是游戲的屏幕輸出,4代表最近4幀(因為會用最近4的幀的游戲畫面來作為網(wǎng)絡(luò)模型的輸入)。
可以看到,除了正常的封裝外,還需要做一些比較tricky,比較靠經(jīng)驗的處理。理論上我們希望這部分越少越好,因為越少算法就越通用。然而現(xiàn)狀是這一塊tuning對結(jié)果的好壞可能產(chǎn)生比較大的影響。。。
好了,接下去就可以看看PPO算法主體了。入口為ppo2.py的learn()函數(shù)。
---------------------
class Model(object):
def __init__(self, *, policy, ob_space, ac_space, nbatch_act, nbatch_train,
nsteps, ent_coef, vf_coef, max_grad_norm):
# 用前面指定的網(wǎng)絡(luò)類型構(gòu)造兩個策略網(wǎng)絡(luò)。act_model用于執(zhí)行策略網(wǎng)絡(luò)根據(jù)當(dāng)前observation返回
# action和value等,即只做inference。train_model顧名思義主要用于參數(shù)的更新(模型的學(xué)習(xí))。
# 注意這兩個網(wǎng)絡(luò)的參數(shù)是共享的,因此train_model更新的參數(shù)可以體現(xiàn)在act_model上。假設(shè)使用默
# 認(rèn)的CnnPolicy,其中的step()函數(shù)計算action, value function和action提供的信息量;
# value()函數(shù)計算value。
# nbatch_act = 8,就等于環(huán)境個數(shù)nenvs。因為每一次都分別對8個環(huán)境執(zhí)行,得到每個環(huán)境中actor的動作。
# 1為nsteps。其實在CNN中沒啥用,在LSTM才會用到(因為LSTM會考慮前nsteps步作為輸入)。
act_model = policy(sess, ob_space, ac_space, nbatch_act, 1, reuse=False)
h = nature_cnn(X) # 如前面所說,《Nature》上的網(wǎng)絡(luò)結(jié)構(gòu)打底。然后輸出policy和value。
pi = fc(h, 'pi', ...) # for policy
vf = fc(h, 'v') # for value function
# 根據(jù)action space創(chuàng)建相應(yīng)的參數(shù)化分布。如這里action space是Discrete(4),那分布
# 就是CategoricalPdType()。然后根據(jù)該分布類型,結(jié)合網(wǎng)絡(luò)輸出(pi),得到動作概率分
# 布CategoricalPd,最后在該分布上采樣,得到動作a0。neglogp0即為該動作的自信息量。
pdtype = make_pdtype()
pd = self.pdtype.pdfromflat(pi)
a0 = self.pd.sample()
neglogp0 = self.pd.neglogp(a0)
# 和構(gòu)建action model類似,構(gòu)建用于訓(xùn)練的網(wǎng)絡(luò)train_model。nbatch_train為256,因為是用于模型的學(xué)習(xí),
# 因此和act_model不同,這兒網(wǎng)絡(luò)輸入的batch size為256。
train_model = policy(sess, ob_space, ac_space, nbatch_train, nsteps, reuse=True)
# 創(chuàng)建一坨placeholder,這些是后面要傳入的。
A = train_model.pdtype.sample_placeholder([None]) # action
ADV = tf.placeholder(tf.float32, [None]) # advantage
R = tf.placeholder(tf.float32, [None]) # return
OLDNEGLOGPAC = tf.placeholder(tf.float32, [None]) # old -log(action)
OLDVPRED = tf.placeholder(tf.float32, [None]) # old value prediction
LR = tf.placeholder(tf.float32, []) # learning rate
CLIPRANGE = tf.placeholder(tf.float32, []) # clip range,就是論文中的epsilon。
neglogpac = train_model.pd.neglogp(A) # -log(action)
entropy = tf.reduce_mean(train_model.pd.entropy())
# 訓(xùn)練模型提供的value預(yù)測。
vpred = train_model.vf
# 和vpred類似,只是與上次的vpred相比變動被clip在由CLIPRANGE指定的區(qū)間中。
vpredclipped = OLDVPRED + tf.clip_by_value(train_model.vf - OLDVPRED, - CLIPRANGE, CLIPRANGE)
vf_losses1 = tf.square(vpred - R)
vf_losses2 = tf.square(vpredclipped - R)
# V loss為兩部分取大值:第一部分是網(wǎng)絡(luò)預(yù)測value值和R的差平方;第二部分是被clip過的預(yù)測value值
# 和return的差平方。這部分和論文中似乎不太一樣。主要目的應(yīng)該是懲罰value值的過大更新。
vf_loss = .5 * tf.reduce_mean(tf.maximum(vf_losses1, vf_losses2))
# 論文中的probability ratio。把這里的exp和log展開就是論文中的形式。
ratio = tf.exp(OLDNEGLOGPAC - neglogpac)
pg_losses = -ADV * ratio
pg_losses2 = -ADV * tf.clip_by_value(ratio, 1.0 - CLIPRANGE, 1.0 + CLIPRANGE)
# 論文公式(7),由于前面都有負(fù)號,這里是取maximum.
pg_loss = tf.reduce_mean(tf.maximum(pg_losses, pg_losses2))
approxkl = .5 * tf.reduce_mean(tf.square(neglogpac - OLDNEGLOGPAC))
clipfrac = tf.reduce_mean(tf.to_float(tf.greater(tf.abs(ratio - 1.0), CLIPRANGE)))
# 論文公式(9),ent_coef, vf_coef分別為PPO論文中的c1, c2,這里分別設(shè)為0.01和0.5。entropy為文中的S;pg_loss為文中的L^{CLIP}
loss = pg_loss - entropy * ent_coef + vf_loss * vf_coef
# 構(gòu)建trainer,用于參數(shù)優(yōu)化。
grads = tf.gradients(loss, params)
trainer = tf.train.AdamOptimizer(learning_rate=LR, max_grad_norm)
_train = trainer.apply_gradients()
---------------------
# Runnder是整個訓(xùn)練過程的協(xié)調(diào)者。
runner = Runner(env=env, model=model, nsteps=nsteps,...)
# total_timesteps = 11000000, nbatch = 1024,因此模型參數(shù)更新nupdates = 10742次。
nupdates = total_timesteps // nbatch
for update in range(1, nupdates+1) # 對應(yīng)論文中Algorithm的外循環(huán)。
obs, returns, masks, actions, values, ... = runner.run()
# 模型(上面的act_model)執(zhí)行nsteps步。有8個環(huán)境,即共1024步。該循環(huán)對應(yīng)論文中Algorithm的第2,3行。
for _ in range(self.nsteps):
# 執(zhí)行模型,通過策略網(wǎng)絡(luò)返回動作。
actions, values, self.states, ... = self.model.step(self.obs, self.status, ...)
# 通過之前創(chuàng)建的環(huán)境執(zhí)行動作,得到observation和reward等信息。
self.obs[:], rewards, self.dones, infos = self.env.step(actions)
# 上面環(huán)境執(zhí)行返回的observation, action, values等信息都加入mb_xxx中存起來,后面要拿來學(xué)習(xí)參數(shù)用。
mb_obs = np.asarray(mb_obs, dtype=self.obs.dtype)
mb_rewards = np.asarray(mb_rewards, dtype=np.float32)
mb_actions = np.asarray(mb_actions)
...
# 估計Advantage。對應(yīng)化文中Algorithm的第4行。
for t in reversed(range(self.nsteps)):
# 論文中公式(12)。
delta = mb_rewards[t] + self.gamma * nextvalues * nextnonterminal - mb_values[t]
# 論文中公式(11)。
mb_advs[t] = lastgaelam = delta + self.gamma * self.lam * nextnonterminal * lastgaelam
mb_returns = mb_advs + mb_values # Return = Advantage + Value
return (*map(sf01, (mb_obs, mb_returns, mb_dones, mb_actions, mb_values, mb_neglogpacs)), mb_states, epinfos)
epinfobuf.extend(epinfos) # Gym中返回的info。
# 論文中Algorithm 1第6行。
if states is None: # nonrecurrent version
inds = np.arange(nbatch)
for _ in range(noptepochs): # epoch為4
np.random.shuffle(inds)
# 8個actor,每個運行128步,因此單個batch為1024步。1024步又分為4個minibatch,
# 因此單次訓(xùn)練的batch size為256(nbatch_train)。
for start in range(0, nbatch, nbatch_train): # [0, 256, 512, 768]
end = start + nbatch_train
mbinds = inds[start:end]
slices = (arr[mbinds] for arr in (obs, ...))
# 將前面得到的batch訓(xùn)練數(shù)據(jù)作為參數(shù),調(diào)用模型的train()函數(shù)進行參數(shù)學(xué)習(xí)。
mblossvals.append(model.train(lrnow, cliprangenow, *slices))
# Advantage = Return - Value
advs = returns - values
# Normalization
advs = (advs - advs.mean()) / (advs.std() + 1e-8)
# cliprange是隨著更新的步數(shù)遞減的。因為一般來說訓(xùn)練越到后面越收斂,每一步的差異也會越來越小。
# neglogpacs和values都是nbatch_train維向量,即shape為(256, )。
td_map = {train_mode.X:obs, A:actions, ADV:advs, R:returns, LR:lr,
CLIPRANGE:cliprange, OLDNEGLOGPAC:neglogpacs, OLDVPRED:values}
return sess.run([pg_loss, vf_loss, entropy, approxkl, clipfrac, _train], td_map)
else:
...
# 每過指定間隔打印以下參數(shù)。
if update % log_interval == 0 or update == 1:
ev = explained_variance(values, returns)
logger.logkv("serial_timesteps", update*nsteps)
logger.logkv("nupdates", update)
...
# 滿足條件時保存模型。
if save_interval and (update % save_interval == 0 or update == 1) and logger.get_dir():
...
model.save(savepath)
env.close()
---------------------
import gym
from gym import spaces
import multiprocessing
import joblib
import sys
import os
import numpy as np
import tensorflow as tf
from baselines.ppo2 import ppo2
from baselines.common.cmd_util import make_atari_env, atari_arg_parser
from baselines.common.atari_wrappers import make_atari, wrap_deepmind
from baselines.ppo2.policies import CnnPolicy
from baselines.common.vec_env.vec_frame_stack import VecFrameStack
def main(argv):
ncpu = multiprocessing.cpu_count()
config = tf.ConfigProto(allow_soft_placement=True,
intra_op_parallelism_threads=ncpu,
inter_op_parallelism_threads=ncpu)
config.gpu_options.allow_growth = True
sess = tf.Session(config=config)
env_id = "BreakoutNoFrameskip-v4"
seed = 0
nenvs = 1
nstack = 4
env = wrap_deepmind(make_atari(env_id))
ob_space = env.observation_space
ac_space = env.action_space
wos = env.observation_space
low = np.repeat(wos.low, nstack, axis=-1)
high = np.repeat(wos.high, nstack, axis=-1)
stackedobs = np.zeros((nenvs,)+low.shape, low.dtype)
observation_space = spaces.Box(low=low, high=high, dtype=env.observation_space.dtype)
vec_ob_space = observation_space
act_model = CnnPolicy(sess, vec_ob_space, ac_space, nenvs, 1, reuse=False)
with tf.variable_scope('model'):
params = tf.trainable_variables()
#load_path = '/tmp/openai-2018-05-27-15-06-16-102537/checkpoints/00030'
load_path = argv[0]
loaded_params = joblib.load(load_path)
restores = []
for p, loaded_p in zip(params, loaded_params):
restores.append(p.assign(loaded_p))
sess.run(restores)
print("model " + load_path + " loaded")
obs = env.reset()
done = False
for _ in range(1000):
env.render()
obs = np.expand_dims(obs, axis=0)
stackedobs = np.roll(stackedobs, shift=-1, axis=-1)
stackedobs[..., -obs.shape[-1]:] = obs
actions, values, states, neglogpacs = act_model.step(stackedobs)
print("%d, action=%d" % (_, actions[0]))
obs, reward, done, info = env.step(actions[0])
if done:
print("done")
obs = env.reset()
stackedobs.fill(0)
sess.close()
if __name__ == '__main__':
if (len(sys.argv)) != 2:
sys.exit("Usage: %s ckpt_path" % sys.argv[0])
if not os.path.exists(sys.argv[1]):
sys.exit("ckpt file %s not found" % sys.argv[1])
main(sys.argv[1:])
---------------------
聯(lián)系:highspeedlogic
QQ :1224848052
微信:HuangL1121
郵箱:1224848052@qq.com
網(wǎng)站:http://www.mat7lab.com/
網(wǎng)站:http://www.hslogic.com/
微信掃一掃: