Challenges
のところを読んでるとテンションが上ってしまったw
実装の流れも良い感じにしてくれているんだろうから頑張らねば!
目次
MLP diagonal Gaussian policy for PPOを実装せよ
前提知識
KL-constrained & KL-divergence
Kullback-Leibler divergence ( KLダイバージェンス、KL情報量 )は、2つの確率分布がどの程度似ているかを表す尺度です。
actor-critic
Q関数を求めるところと状態に応じた行動を決定する部分を分けたのがActor-Criticという強化学習方法
PPO
まずPPOとは何か。
Proximal Policy Optimization
の略。
データを使ってpolicyを改善する。
TRPOも同じことをしているが、TRPOはsecond-order method、PPOはfirst-order methodを使っている。
PPOはon-policyのアルゴリズム。
discreteとcontinuousのaction spaceどちらでも使える。
PPO Penalty
TRPOと同様にKL-constrainedを更新するが、KL-constrainedそのものを更新するのではなく、KL-divergenceの関数を更新していく。
そのためスケーリングが容易になる。
PPO Clip
以前のpolicyからどれぐらい改良されたかは考慮されない。
実装
まずmlpメソッドでBuilds a multi-layer perceptron in Tensorflow.
と書いてある。
multi-layer perceptron(多層パーセプトロン)は、
1 2 |
入力層(Input Layer)と出力層(Output Layer)の間に1以上の隠れ層(Hidden Layer)を持つ |
だったとは…
(色々調べるとそれだけでもないらしい)
ここの引数をmlp_gaussian_policyで計算する。
恐らくその中からmlpを呼び出す。
a_phとx_phのシンボルってなんなんだ…
piはサンプルアクションだからaを入れればいいのか?
しっかり考えてから実装方法が全くわからなかったら解答をみるに限る。
解答
mlp_gaussian_policy
から。
1 2 3 4 5 6 7 8 9 10 |
def mlp_gaussian_policy(x, a, hidden_sizes, activation, output_activation, action_space):j act_dim = a.shape.as_list()[-1] mu = mlp(x, list(hidden_sizes)+[act_dim], activation, output_activation) log_std = tf.get_variable(name='log_std', initializer=-0.5*np.ones(act_dim, dtype=np.float32)) std = tf.exp(log_std) pi = mu + tf.random_normal(tf.shape(mu)) * std logp = gaussian_likelihood(a, mu, log_std) logp_pi = gaussian_likelihood(pi, mu, log_std) return pi, logp, logp_pi |
mlpの解答は以下。
1 2 3 4 5 |
def mlp(x, hidden_sizes=(32,), activation=tf.tanh, output_activation=None): for h in hidden_sizes[:-1]: x = tf.layers.dense(x, units=h, activation=activation) return tf.layers.dense(x, units=hidden_sizes[-1], activation=output_activation) |
まとめ
私は結局のところ何を実装していたのか…?
明らかにPPOのメインのところではなかった。
これら解決していきたい。
policyの部分の実装
名前からしてわかるが、policyの部分の実装をやっていたんだ僕たちは。
1行ずつ丁重にみていきたい。(core.pyの内容)
1 2 3 4 5 6 7 8 9 10 |
77 def mlp_gaussian_policy(x, a, hidden_sizes, activation, output_activation, action_space): 78 act_dim = a.shape.as_list()[-1] 79 mu = mlp(x, list(hidden_sizes)+[act_dim], activation, output_activation) 80 log_std = tf.get_variable(name='log_std', initializer=-0.5*np.ones(act_dim, dtype=np.float32)) 81 std = tf.exp(log_std) 82 pi = mu + tf.random_normal(tf.shape(mu)) * std 83 logp = gaussian_likelihood(a, mu, log_std) 84 logp_pi = gaussian_likelihood(pi, mu, log_std) 85 return pi, logp, logp_pi |
1 2 |
77 def mlp_gaussian_policy(x, a, hidden_sizes, activation, output_activation, action_space): |
まぁここはよさそうだ。
状態全体と行動全体、hidden layerのアウトプットのサイズ、activation function、最終的なアウトプットとなるactivation function、そして、actionのspaceがこのメソッドに代入される。
action_spaceは今回は関係がない。(必要なのはBoxクラスのときのみ)
1 2 |
78 act_dim = a.shape.as_list()[-1] |
行動を取得する。
これは取れる行動だと思っている。
1 2 |
79 mu = mlp(x, list(hidden_sizes)+[act_dim], activation, output_activation) |
今回もう1つ作成したメソッドに入れる。
隠れ層と取れる行動を全部hidden_sizeとして入れる。
####### 脱線:mlpメソッド 開始 #######
隠れ層は初期値がmlp_actor_criticメソッドより(64, 64)。
gymのenvはHalfCheetah-v2なので、act_dimの元となる初期値a_phは>>> core.placeholders_from_spaces(env.observation_space, env.action_space)
から計算される[<tf.Tensor 'Placeholder:0' shape=(?, 17) dtype=float32>, <tf.Tensor 'Placeholder_1:0' shape=(?, 6) dtype=float32>]
の2つ目。
1 2 3 4 5 6 7 8 9 |
>>> a <tf.Tensor 'Placeholder_3:0' shape=(?, 6) dtype=float32> >>> a.shape TensorShape([Dimension(None), Dimension(6)]) >>> a.shape.as_list() [None, 6] >>> a.shape.as_list()[-1] 6 |
act_dim = a.shape.as_list()[-1]なので、6となる。
hidden_sizeにはlist((64, 64)) + [6]が入り[64, 64, 6]となる。
mlpメソッド内では、
1 2 3 4 5 |
29 def mlp(x, hidden_sizes=(32,), activation=tf.tanh, output_activation=None): 30 for h in hidden_sizes[:-1]: 31 x = tf.layers.dense(x, units=h, activation=activation) 32 return tf.layers.dense(x, units=hidden_sizes[-1], activation=output_activation) |
となっており、[64, 64, 6][:-1]なので、[64, 64]をforする。
なので、unitサイズが64となり、最終的にreturnするのは、[64, 64, 6][-1]なので、unitサイズ6が返される。
####### 脱線:mlpメソッド 終了 #######
####### 脱線:tensor class 開始 #######
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
c = tf.constant([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) print(c.shape) ==> TensorShape([Dimension(2), Dimension(3)]) d = tf.constant([[1.0, 0.0], [0.0, 1.0], [1.0, 0.0], [0.0, 1.0]]) print(d.shape) ==> TensorShape([Dimension(4), Dimension(2)]) # Raises a ValueError, because `c` and `d` do not have compatible # inner dimensions. e = tf.matmul(c, d) f = tf.matmul(c, d, transpose_a=True, transpose_b=True) print(f.shape) ==> TensorShape([Dimension(3), Dimension(4)]) |
こういう感じでdimが表示されるらしい。
####### 脱線:tensor class 終了 #######
1 2 |
80 log_std = tf.get_variable(name='log_std', initializer=-0.5*np.ones(act_dim, dtype=np.float32)) |
変数の予約をして、初期値を-0.5のdim(6)にしています。
この初期値は特に理由はないようです。
1 2 |
81 std = tf.exp(log_std) |
80行で計算したlog_stdをexponencialでstdに変換。
1 2 |
82 pi = mu + tf.random_normal(tf.shape(mu)) * std |
いよいよ、returnする変数の1つpiが出てきました。
なのでここも6要素でしょう。
17のobservationをmlpメソッドを通して6つのactionにした結果muに+ tf.random_normal(tf.shape(mu)) * std
を足しています。
これはgreedyのためだったかな?
1 2 |
83 logp = gaussian_likelihood(a, mu, log_std) |
1つ前で扱ったgaussian_likelihood
を使うのですが、これこそがpolicyを判断する確率的手法。
現在の状態から取った行動のlog probability。
####### 脱線:gaussian_likelihood 開始 #######
docによると、
sampling actions from the policy,
and computing log likelihoods of particular actions, \log \pi_{\theta}(a|s).
なので、このand以下がgaussian_likelihoodで行うこと。
という分類ができるとここで気になるのは、gaussian_likelihood以前はsample actionを取得していたのか。
####### 脱線:gaussian_likelihood 終了 #######
####### 脱線:行動のサンプルとはなんだったのか? 開始 #######
Q値を参考に。
Q値は「報酬」ではなく「価値」であることに注意してください。つまり、Q値とは短期的な報酬ではなく、長期的な意味での価値を値として持っている関数です。
式1を式2に置き換えることができる。
そこで、期待値をとるのではなく、実際に行動を実施して次の時点の状態を確認しながら、少しずつQ値を更新していきます。実際に行動した結果のサンプルで期待値の代用としよう、というわけです。
とあるように、行動のサンプルでした。
ここでいう行動のサンプルは、pi
だったんですね。
####### 脱線:行動のサンプルとはなんだったのか? 終了 #######
1 2 |
84 logp_pi = gaussian_likelihood(pi, mu, log_std) |
行動サンプルのlog probability。
####### 脱線:なぜlog probabilityなのか? 開始 #######
Gradient methods generally work better optimizing logp(x) than p(x) because the gradient of logp(x) is generally more well-scaled.
便利だからってことなんでしょう。
####### 脱線:なぜlog probabilityなのか? 終了 #######
1 2 |
85 return pi, logp, logp_pi |
お返しします。
ppoメソッドでどうやって使われるか
本家実装コードのppo.py
では変数名actor_critic
として利用されているので、ここで使われている。
まずはplace_holderで初期化されているだけ。
そして、all_phs
に格納されているので、これはグラフのplace_holderとしてまとめたということだろう。
1 2 3 4 5 6 |
186 # Main outputs from computation graph 187 pi, logp, logp_pi, v = actor_critic(x_ph, a_ph, **ac_kwargs) 188 189 # Need all placeholders in *this* order later (to zip with data from buffer) 190 all_phs = [x_ph, a_ph, adv_ph, ret_ph, logp_old_ph] |
ac_kwargs
にはaction_space
だけ入っている。
1 2 3 |
179 # Share information about action space with policy architecture 180 ac_kwargs['action_space'] = env.action_space |
そして、いよいよsess.run
で使われるところ!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
253 # Main loop: collect experience in env and update/log each epoch 254 for epoch in range(epochs): 255 for t in range(local_steps_per_epoch): 256 a, v_t, logp_t = sess.run(get_action_ops, feed_dict={x_ph: o.reshape(1,-1)}) 257 258 # save and log 259 buf.store(o, a, r, v_t, logp_t) 260 logger.store(VVals=v_t) 261 262 o, r, d, _ = env.step(a[0]) 263 ep_ret += r 264 ep_len += 1 265 266 terminal = d or (ep_len == max_ep_len) 267 if terminal or (t==local_steps_per_epoch-1): 268 if not(terminal): 269 print('Warning: trajectory cut off by epoch at %d steps.'%ep_len) 270 # if trajectory didn't reach terminal state, bootstrap value target 271 last_val = r if d else sess.run(v, feed_dict={x_ph: o.reshape(1,-1)}) 272 buf.finish_path(last_val) 273 if terminal: 274 # only save EpRet / EpLen if trajectory finished 275 logger.store(EpRet=ep_ret, EpLen=ep_len) 276 o, r, d, ep_ret, ep_len = env.reset(), 0, False, 0, 0 277 278 # Save model 279 if (epoch % save_freq == 0) or (epoch == epochs-1): 280 logger.save_state({'env': env}, None) 281 282 # Perform PPO update! 283 update() |
feed_dict={x_ph: o.reshape(1,-1)}
でx_phの上書きをする。
o.reshapの初期値はgymから持ってきています。
1 2 |
262 o, r, d, _ = env.step(a[0]) |
その後、stepで更新していくという感じでした。
ppoメソッド
clipというのが数式に入っているが、どうやって実装するんだろうか?
ここでppoメソッドをみていきたい。
ログと乱数はちょっと飛ばして、
1 2 3 4 |
175 env = env_fn() 176 obs_dim = env.observation_space.shape 177 act_dim = env.action_space.shape |
ここは普通に、状態と行動のshapeをとってきてる。
1 2 3 |
179 # Share information about action space with policy architecture 180 ac_kwargs['action_space'] = env.action_space |
policyへaction_spaceを渡す準備。
1 2 3 4 |
182 # Inputs to computation graph 183 x_ph, a_ph = core.placeholders_from_spaces(env.observation_space, env.action_space) 184 adv_ph, ret_ph, logp_old_ph = core.placeholders(None, None, None) |
183, 184はplace_holderにしているだけです。
1 2 3 4 5 6 7 8 9 |
186 # Main outputs from computation graph 187 pi, logp, logp_pi, v = actor_critic(x_ph, a_ph, **ac_kwargs) 188 189 # Need all placeholders in *this* order later (to zip with data from buffer) 190 all_phs = [x_ph, a_ph, adv_ph, ret_ph, logp_old_ph] 191 192 # Every step, get: action, value, and logprob 193 get_action_ops = [pi, v, logp_pi] |
散々みてきたactor_critic周り。
1 2 3 4 |
195 # Experience buffer 196 local_steps_per_epoch = int(steps_per_epoch / num_procs()) 197 buf = PPOBuffer(obs_dim, act_dim, local_steps_per_epoch, gamma, lam) |
計算結果の保管用クラスの初期化。
1 2 3 4 |
199 # Count variables 200 var_counts = tuple(core.count_vars(scope) for scope in ['pi', 'v']) 201 logger.log('\nNumber of parameters: \t pi: %d, \t v: %d\n'%var_counts) |
scope名がpi
とv
それぞれの変数の数をログへ。
####### 脱線:core.count_vars 開始 #######
core.count_varsは以下の通りとなっている。
1 2 3 4 5 6 7 |
34 def get_vars(scope=''): 35 return [x for x in tf.trainable_variables() if scope in x.name] 36 37 def count_vars(scope=''): 38 v = get_vars(scope) 39 return sum([np.prod(var.shape.as_list()) for var in v]) |
get_varsでは、
- trainable_variablesはtrainable=Trueの変数を全て返す。
- そして、scopeを判断して該当するものだけ返す。
その返ってきた変数に対して、np.prodは次元関係なく乗じて変数をカウントする。
####### 脱線:core.count_vars 終了 #######
1 2 3 4 5 6 |
203 # PPO objectives 204 ratio = tf.exp(logp - logp_old_ph) # pi(a|s) / pi_old(a|s) 205 min_adv = tf.where(adv_ph>0, (1+clip_ratio)*adv_ph, (1-clip_ratio)*adv_ph) 206 pi_loss = -tf.reduce_mean(tf.minimum(ratio * adv_ph, min_adv)) 207 v_loss = tf.reduce_mean((ret_ph - v)**2) |
adv_ph, ret_ph, logp_old_phはbufに登録されたものをbuf.getで取得するようになっている。1つ言えることは全てplaceholderである。
ratioは、現在の状態から取った行動のlog probabilityの更新前後の差分。のexponential。
min_advのところでtf.where
というのははじめてみたんですが、1つ目のattrに入ったもの(配列など)が、Trueならば第二引数、Falseならば第三引数の要素を返すようです。
つまり、min_advではadvantageのplaceholderが0より大きかった場合(1+clip_ratio)*adv_ph
を、小さかった場合(1-clip_ratio)*adv_ph
となる。
clip_ratioはハイパーパラメータで初期値は0.2になっている。
以下の式となる。
ここでなぜPPOでこのような式があるかというと、TRPOなどではpolicyをアップデートする時にアップデート幅が大きくなってしまうため、PPOでは以下の式のようにアップデート幅を制限している。
ここでいうL functionが実装された式となる。
L functionのmin内の1つ目のattrは実際のアップデート幅、2つ目のattrは実装式なので、実際のアップデート幅が大きすぎるとclipしている式から計算された結果がアップデート幅として使われるようになっている。
1 2 |
206 pi_loss = -tf.reduce_mean(tf.minimum(ratio * adv_ph, min_adv)) |
minimum内は上記の通り。
reduce_meanで配列の平均を計算して、-
としている。
policyのloss functionとして使うための準備をしている。
1 2 |
207 v_loss = tf.reduce_mean((ret_ph - v)**2) |
報酬のloss function。
####### 脱線:tf.squeeze 開始 #######
vはcore.pyで以下のようになっている。
1 2 |
103 v = tf.squeeze(mlp(x, list(hidden_sizes)+[1], activation, None), axis=1) |
squeeze配下のように動作する。
1 2 3 4 5 |
# 't' is a tensor of shape [1, 2, 1, 3, 1, 1] tf.shape(tf.squeeze(t)) # [2, 3] # 't' is a tensor of shape [1, 2, 1, 3, 1, 1] tf.shape(tf.squeeze(t, [2, 4])) # [1, 2, 3, 1] |
####### 脱線:tf.squeeze 終了 #######
1 2 3 4 5 6 |
209 # Info (useful to watch during learning) 210 approx_kl = tf.reduce_mean(logp_old_ph - logp) # a sample estimate for KL-divergence, easy to compute 211 approx_ent = tf.reduce_mean(-logp) # a sample estimate for entropy, also easy to compute 212 clipped = tf.logical_or(ratio > (1+clip_ratio), ratio < (1-clip_ratio)) 213 clipfrac = tf.reduce_mean(tf.cast(clipped, tf.float32)) |
KL-divergenceのサンプル、entropyのサンプルは平均から計算している。
tf.logical_orは、tensorに対するor構文です。
なので、ratio > (1+clip_ratio)かratio < (1-clip_ratio)のどちらかがTrueの場合はTrue。
tf.cast(clipped, tf.float32)
ではboolをtf.float32に変換しているので、1か0になる。
それの平均をclipfrac
としている。
1 2 3 4 |
215 # Optimizers 216 train_pi = MpiAdamOptimizer(learning_rate=pi_lr).minimize(pi_loss) 217 train_v = MpiAdamOptimizer(learning_rate=vf_lr).minimize(v_loss) |
上記で求めていたpi_loss
とv_loss
をの最適化をする。
####### 脱線:MpiAdamOptimizerクラス 開始 #######
mpi_tf.pyにて。
1 2 3 4 5 6 7 8 |
29 class MpiAdamOptimizer(tf.train.AdamOptimizer): ... 41 def __init__(self, **kwargs): 42 self.comm = MPI.COMM_WORLD 43 tf.train.AdamOptimizer.__init__(self, **kwargs) |
なので、単純にAdamOptimizerで初期化しているだけ。
####### 脱線:MpiAdamOptimizerクラス 終了 #######
1 2 3 |
219 sess = tf.Session() 220 sess.run(tf.global_variables_initializer()) |
さて、ようやくsessionの開始ですぞ。
その他
gym
gymいい!
Robotics
というのが新しくあるし。
これいいのでは!
これやろう。
mojoco
必然的にmujocoも知らなくてはいけない気がする。
mujocoも一度読む。