【強化学習】2018年度最強と噂のR2D2を実装/解説してみた

なんとか実装しましたが…
私の技術不足か Keras の限界かは分かりませんがburninの実装に関してはミニバッチ学習と併用できていません。
また、ネット上の情報をかき集めて自分なりに実装しているので正確ではないところがある点はご了承ください。

追記：ミニバッチ学習についても実装しました。
[【強化学習】R2D2を実装/解説してみたリベンジ 解説編（Keras-RL）](https://qiita.com/pocokhc/items/408f0f818140924ad4c4)

追記2：R2D3も実装しました。
[【強化学習】R2D3を実装/解説してみた（Keras-RL）](https://qiita.com/pocokhc/items/2e000df3fddddd3d0854)

追記3：Agent57も記事をあげました。
[【強化学習】ついに人間を超えた！？Agent57を解説/実装してみた（Keras-RL）](https://qiita.com/pocokhc/items/8684c6c96d3d2963e284)

追記4：改めて記事にしています。
また実装コードは新しい記事の方が正確です。

https://qiita.com/pocokhc/items/c456f5b034498ab18ab7



# 本シリーズ
+ [【強化学習】OpenAI Gym×Keras-rlで強化学習アルゴリズムを実装していくぞ（準備編）](https://qiita.com/pocokhc/items/a8120b0abd5941dd7a9f)
+ [【強化学習】OpenAI Gym×Keras-rlで強化学習アルゴリズムを実装していくぞ（Q学習編）](https://qiita.com/pocokhc/items/8ed40be84a144b28180d)
+ [【強化学習】OpenAI Gym×Keras-rlで強化学習アルゴリズムを実装していくぞ（DQN編）](https://qiita.com/pocokhc/items/125479c9ae0df1de4234)
+ [【強化学習】Keras-rlでRainbowを実装/解説](https://qiita.com/pocokhc/items/fc00f8ea9dca8f8c0297)
+ [【強化学習】Keras-rlでApe-Xを実装/解説（並列プログラミング）](https://qiita.com/pocokhc/items/980640c6a7b5dc9d524e)
+ [MNISTでSaliencyMapとGrad_CAMを試してDQN(Dueling Network)を可視化してみた(実装/解説)](https://qiita.com/pocokhc/items/6a9c0f602613c2deda75)
+ 【強化学習】2018年度最強と噂のR2D2を実装/解説してみた(ここ)
+ [【強化学習】複数の探索ポリシーを実装/解説して比較してみた](https://qiita.com/pocokhc/items/fd133053fa309bdb58e6)
+ [【強化学習】DQNのハイパーパラメータを3つのゲームで比較してみた](https://qiita.com/pocokhc/items/bb6cca5818dfe1cf1fbe)



# 概要
+ (R2D2 - 分散学習)のDRQNを実装して仕組みを理解してみた。
+ R2D2をその後実装(コードのみ)
+ ステートフルLSTMとミニバッチ学習は実装が分からなくて断念した。


# コード全体
本記事で作成したコードは以下です。
※1ファイル完結です。
※GoogleColaboratoryは実行結果付き

+ [RainbowR(Rainbow+DRQN)コード(GitHubGist)](https://gist.github.com/pocokhc/e693d0c96b1ae1b2d6116355143a87e4)
+ [RainbowR(Rainbow+DRQN)コード(GoogleColaboratory)](https://colab.research.google.com/drive/1E8ewtRJNT-eSvwgJ8bxyIqmm27YHtSXw)
+ [R2D2コード(GitHubGist)](https://gist.github.com/pocokhc/5890cd28c69d69078478278fda1e22ae)
+ [R2D2コード(GoogleColaboratory)](https://colab.research.google.com/drive/1GaHFkSbnMSRieQhEOVJK38lz1YaZaV8L)


# はじめに
本記事は本シリーズのDQN編、Rainbow編、Ape-X編と [KerasのステートレスLSTMとステートフルLSTMの違いについて](https://qiita.com/pocokhc/items/c94512e5536ddd5b1bba) の記事の内容を踏まえて書いている箇所が多々あります。
なるべく分かりやすく書くようにはしていますがよければ上記記事もご参照ください。
(DQNシリーズで使われている技術が多すぎて…)

# R2D2について
某ロボットを彷彿とさせる名前ですが、2018/9/28(最終更新:2019/1/25)に発表された最新の強化学習手法です。
2018年最強の強化学習と噂のこの手法を実装/解説していきたいと思います。

概要ですが、R2D2(Recurrent Replay Distributed DQN)はざっくりいうと分散学習の Ape-X に時系列データを学習できる LSTM を組み合わせたアルゴリズムです。

Ape-X に関しては本シリーズのApe-X編で解説しているので省略します。
LSTM に関しては DQN×LSTM を組み合わせた DRQN という先行研究があるのでまずはそちらを解説してから実装していきたいと思います。

参考
・[深層強化学習アルゴリズムまとめ](https://qiita.com/shionhonda/items/ec05aade07b5bea78081)
・[深層強化学習手法R2D2を解説する](https://qiita.com/kazupc/items/bcfc2c86a4f5326c283c)
・[Recurrent Experience Replay in Distributed Reinforcement Learning(論文)](https://openreview.net/forum?id=r1lyTjAqYX)


# DRQN
基本アイデアは、DQN に RNN(LSTM) を入れて時系列の影響を学習しようというもの。
DQN には window_length による直近のデータを保存する方法はありますが時系列情報を保存する方法はありませんからね。

一応それ以外にも MDP(マルコフ決定過程) と POMDP(部分観測マルコフ決定過程) の話もありますがここでは省略します。
(ざっくりいうと、Q学習はMDP(完璧な環境)が前提だけど、現実は一部しか観測できない(POMDP)よねって話)

参考：
+ [Deep Recurrent Q-Learning(DRQN) for Partially Observable MDPs](https://www.slideshare.net/ssuser77b8c6/deep-recurrent-qlearningdrqn-for-partially-observable-mdps)
+ [Deep Recurrent Q-Learning for Partially Observable MDPs(論文)](https://arxiv.org/abs/1507.06527)
+ [LSTM の初期状態の学習による DRQN の改善(pdf)](https://ipsj.ixsq.nii.ac.jp/ej/?action=repository_uri&item_id=192079&file_id=1&file_no=1)


## DRQN モデル
Conv層の後にLSTM層を入れるだけです。
DuelingNetwork じゃない場合はDense層とLSTM層を入れ替えていますが、
DuelingNetwork の場合は、Conv層と状態価値関数/Advantage関数の間に入れます。
(DuelingNetworkについては[Rainbow編](https://qiita.com/pocokhc/items/fc00f8ea9dca8f8c0297#dueling-network)を参照)

![qiita_08_md_1.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/663d6f95-4978-afdb-27f0-0e39c8fab658.png)

## DRQN の課題
LSTM では短期記憶の初期状態(hidden state)が重要となります。
hidden state については[こちら](https://qiita.com/pocokhc/items/c94512e5536ddd5b1bba)を参照。

hidden state の扱いについては、DRQNの論文では以下の２つの方法が提案されており、どちらでも学習自体は問題なく出来るとのことです。

### エピソード全体の学習(Bootstrapped Sequential Updates)
1エピソードの最初から最後までを学習する方法です。
hidden state を最初から学習できるので、時系列データとしては確実に学習できるのですが、DQN のランダムサンプリング([Experience Replay(DQN編)](https://qiita.com/pocokhc/items/125479c9ae0df1de4234#experience-replay)を参照)と相性が悪く、また1エピソードが長い場合はメモリサイズの問題も出てきます。

### ランダムシーケンスの学習(Bootstrapped Random Updates)
1エピソード内のランダムな点から始まるステップを学習する方法です。
DQN のランダムサンプリングに従う形ですが、hidden state を毎回 0 にするため時系列的に離れている要素(最初の方の要素)の学習が難しいという点があります。


# R2D2
基本は Ape-X と DRQN です。(Ape-Xについては[Ape-X編](https://qiita.com/pocokhc/items/980640c6a7b5dc9d524e)を参照してください)
違う点を説明していきます。

## Burn-in
DRQN では hidden state の扱いに課題がありました。
これはエピソードの途中の状態にも関わらず hidden state を 0 で初期化していることが原因です。
これを解決するために２段階踏んでいます。

1段階目はエピソードの途中の hidden state を保存し、学習時にはそれを呼び出して初期化する方法です。
これである程度正確な hidden state を復元できますが、これでもまだ現状(学習が進んでいる)のモデルとは違う状態になります。

そこで２段階目として、一定期間学習をせずにデータだけを流す期間を用意します。(hidden stateだけを更新)
すると hidden state は現状のモデルに即したかなり正確な状態に落ち着きます。
この２段階目の事を論文では Burn-in と呼んでいます。


## rescaling関数とPriorityの計算方法
### rescaling関数
DQN では報酬を-1～1でクリッピングしていましたがそれをやめて rescaling 関数を導入します。

$$h(x) = sign(x)(\sqrt{|x|+1}-1)+\epsilon x$$

 $\epsilon$ は定数で論文だと$10^{-3}$です。

逆関数も導出します…が $\epsilon x$項を含めると導出が分からなかったので除いた逆関数です。
(定数がすごい小さい数なので誤差とします…分かる方はコメント頂けると助かります)

$$h^{-1}(x) = sign(x) \Bigl( (x + sign(x))^2 -1 \Bigr)$$

python で記述すると以下ですね。

``` python:rescaling
import numpy from np
import math
def rescaling(x, epsilon=0.001):
    n = math.sqrt(abs(x)+1) - 1
    return np.sign(x)*n + epsilon*x

def rescaling_inverse(x):
    return np.sign(x)*( (x+np.sign(x) ) ** 2 - 1)
```

これはQ値に使うものらしく数式は以下です。
(分かりやすいようにQ学習版、Multi-Step learningなし)

$$y_{t} = h \Bigl(r_{t} + \gamma h^{-1}(\max_pQ_{target}(s_{t+1},a_{t}))\Bigr)$$

rescaling関数ですが、学習のQ値とPERの優先度のどちらか(または両方)に使ったらいいか書いていませんでした。
また、それぞれ適用場所で挙動がかなり違ったのでハイパーパラメータで制御できるようにしました。
(真ん中の$h^{-1}$は、逆関数の事で合ってるよね…？(論文では触れられていません))


### Priorityの計算方法

Ape-Xでは一つの経験データに対して Priority を与えていましたが、R2D2ではシーケンシャル経験データ１つ１つに対して Priority を与えなければなりません。
よって、Priority の計算方法を先ほどの$y_{y}$を用いて次のように変更します。

TD誤差：$\delta_{t}$
$$ \delta_{t} = y_{t} - Q(s_{t},a_{t})$$

Priority：$p$
$$ p = \eta \max_i |\delta_{i}| + (1-\eta)\bar{|\delta|}$$

左項が各経験データの Priority 最大値、右項が各経験データの Priority の平均値を表し、$\eta$ で反映の割合を決めています。
論文では $\eta=0.9$ で、あまり平均を重視すると長いシーケンスでエラーが加味されにくくなり、良い経験が学習されにくくなるとの事でした。


### rescaling関数に関する考察
rescaling関数をグラフ化するとこんな感じです。($\epsilon=0.001$)

![qiita_08_md_10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/ca996093-b6ca-a37c-0a5f-1eacc3ccc2ce.png)

青と赤の線はほぼ重なっています。

Priorityのグラフは以下です。

![qiita_08_md_11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/2512cde8-71c5-6180-2b5e-57bd8aa249b9.png)

$$ y1 = |r + x|$$

$$ y2 = |h(x)|$$

$$ y3 = |h^{-1}(x)|$$

$$ y4 = |h( r + h^{-1}(x))|$$

報酬$r$=1、$\gamma$(割引率)=1、現在のQ値が 0 の場合の Priority を想定しています。
絶対値で Priority の計算としています。

y1 が今まで通りの単純な場合です(次の状態のQ値をそのまま使用)
y2 はrescaling関数を適用した形です。
y3 はrescaling関数の逆関数を適用した形です。
y4 が Priority の計算結果です。

違う点としては現在得られる報酬を丸めている点でしょうか。
報酬のみをrescalingで丸めている形になるのでclipingの代わりを果たしているように見えます。
要するに報酬を軽く見ている事になるので学習が進むとQ値を優先して学習が進まなくなったり…?


# (R2D2 - 分散学習) で実装/解説
分散学習があるとデバッグが大変なので、まずは分散学習を除いた部分から実装していきます。
[Rainbow編のコード](https://gist.github.com/pocokhc/3e6cfec2de933f8320be1885e6fa6f00)を元に実装していきます。

## LSTM
ミニバッチ学習の関係でステートレスLSTM版とステートフルLSTM版を作成します。
冒頭でもいいましたが、ステートの違いは[こちらの記事](https://qiita.com/pocokhc/items/c94512e5536ddd5b1bba)を参照してください。

## ステートレスLSTM
DRQNの「ランダムシーケンスの学習(Bootstrapped Random Updates)」に相当する実装となります。

![qiita_08_md_4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/e5939ce2-7cc2-1007-c60a-935a212fcb5e.png)

ハイパーパラメータですが、DQNでは直近のフレーム数を window_length と言っていましたが、LSTMでは時系列となるので input_sequence に言い直しています。

dense のユニット数も([Rainbow編](https://qiita.com/pocokhc/items/fc00f8ea9dca8f8c0297#nn%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AE-dense-units-%E3%81%AE%E3%83%8F%E3%82%A4%E3%83%91%E3%83%BC%E3%83%91%E3%83%A9%E3%83%A1%E3%83%BC%E3%82%BF%E5%8C%96))で　dense_units_num を定義していましたが LSTM も同様にユニット数として lstm_units_num を定義しておきます。

### ステートレスLSTMのNNモデル

``` python:NNモデル
def build_network(self):
    c = input_ = Input(shape=(self.input_sequence,) + self.input_shape)
    
    if self.enable_image_layer:
        # (time steps, w, h) -> (time steps, w, h, ch)
        c = Reshape((self.input_sequence, ) + self.input_shape + (1,) )(c)
        
        c = TimeDistributed(Conv2D(32, (8, 8), strides=(4, 4), padding="same"),name="c1")(c)
        c = Activation("relu")(c)
        c = TimeDistributed(Conv2D(64, (4, 4), strides=(2, 2), padding="same"),name="c2")(c)
        c = Activation("relu")(c)
        c = TimeDistributed(Conv2D(64, (3, 3), strides=(1, 1), padding="same"),name="c3")(c)
        c = Activation("relu")(c)

        c = TimeDistributed(Flatten())(c)
            
    c = LSTM(self.lstm_units_num)(c)

    if self.enable_dueling_network:
        # value
        v = Dense(self.dense_units_num, activation="relu")(c)
        v = Dense(1, activation="relu", name="v")(v)

        # advance
        adv = Dense(self.dense_units_num, activation='relu')(c)
        adv = Dense(self.nb_actions, name="adv")(adv)

        # 連結で結合
        c = Concatenate()([v,adv])
        c = Lambda(lambda a: K.expand_dims(a[:, 0], -1) + a[:, 1:] - K.mean(a[:, 1:], axis=1, keepdims=True), output_shape=(self.nb_actions,))(c)

    else:
        c = Dense(self.dense_units_num, activation="relu")(c)
        c = Dense(self.nb_actions, activation="linear")(c)
    
    return Model(input_, c)
```

NoisyNet は省略しています。
まずは image layer の TimeDistributed ラッパーですが、こちらは時系列を残したままレイヤーを処理するラッパーとなります。
https://keras.io/layers/wrappers/

Conv2D層は、
入力形式；`(batch_size, width, height, channel)`
出力形式：`(batch_size, width, height, units)`
ですが、TimeDistributedでラップすると、
入力形式：`(batch_size, timesteps, width, height, channel)`
出力形式：`(batch_size, timesteps, width, height, units)`
となり、timesteps を保持したままレイヤーを処理する事ができます。
なので、LSTM に渡す前の層全てを TimeDistributed でラップしています。

LSTM層後ですが、[DRQNの論文](https://openreview.net/forum?id=r1lyTjAqYX)ではDense層をLSTMに置き換えています。
ただ、[こちらの論文](https://ipsj.ixsq.nii.ac.jp/ej/?action=repository_uri&item_id=192079&file_id=1&file_no=1)や[R2D2の論文](https://openreview.net/pdf?id=r1lyTjAqYX)ではDense層やDuelingNetworkの前に追加するとの記載があるのでそちらにならって画像処理とDense層の間にLSTM層を入れています。

### rescaling と Priority の計算(ステートレスLSTM)

foward_train 内のTD誤差を計算している箇所に追加します。

``` python:RainbowAgent.py
def forward_train(self):
    (省略)

    (indexes, batchs, weights) = self.memory.sample(self.batch_size, self.step)
    
    (省略)
    
    for i in range(self.batch_size):
        # Q値を出力(DoubleDQN版)
        action = np.argmax(state1_model_qvals_batch[i])
        maxq = state1_target_qvals_batch[i][action]

        # priority計算
        tmp = rescaling_inverse(maxq)
        tmp = reward_batch[i] + (self.gamma ** self.multireward_steps) * tmp
        tmp *= weights[i]
        tmp = rescaling(tmp, self.rescaling_epsilon)
        priority = abs(tmp - outputs[i][action_batch[i]])

        # Q値 update用
        maxq = rescaling_inverse(maxq)
        td_error = reward_batch[i] + (self.gamma ** self.multireward_steps) * maxq
        td_error *= weights[i]
        td_error = rescaling(td_error, self.rescaling_epsilon)
        outputs[i][action_batch[i]] = td_error

        # 今回使用したsamplingのTD誤差を更新
        self.memory.update(indexes[i], batchs[i], priority)

    # 学習
    self.model.fit(np.asarray(state0_batch), np.asarray(outputs), batch_size=self.batch_size, epochs=1, verbose=0)
```

rescaling 関数をQ学習と priotiry 両方に適用したものです。
priority の計算方法は、R2D2 では各sequenceに対するTD誤差を計算してその平均と最大値から算出しますが、
ステートレスLSTMでは sequence に対するQ値は１つなので今まで通りの priority の算出をしています。
※ステートフルLSTMでは実装しています

また、priority を学習側で計算しているため[PER](https://qiita.com/pocokhc/items/fc00f8ea9dca8f8c0297#priority-experience-reply%E5%84%AA%E5%85%88%E9%A0%86%E4%BD%8D%E4%BB%98%E3%81%8D%E7%B5%8C%E9%A8%93%E5%86%8D%E7%94%9F)側での計算をやめます。

``` python:PERProportionalMemory.py
class PERProportionalMemory():

    def update(self, index, experience, td_error):
        priority = (abs(td_error) + 0.0001) ** self.alpha
        self.tree.update(index, priority)

        if self.max_priority < priority:
            self.max_priority = priority
    
    ↓　こっちに変更します。

    def update(self, index, experience, priority):
        self.tree.update(index, priority)

        if self.max_priority < priority:
            self.max_priority = priority

```

PERProportionalMemoryを例にとっていますが他も同様です。


## ステートフルLSTM(Burn-in)
Kerasで hidden state を扱うにはステートフルLSTMにする必要があります。
ここは先に言っておきますが、ミニバッチ学習と hidden state の関連が分からずじまいでした。
なので、ミニバッチ学習はあきらめて実装しています。(ですので遅いです)

ステートフルLSTMの学習です。
イメージは以下です。

__NNモデルの予測/学習のイメージ__
![qiita_08_md_5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/02b71440-7edf-5e33-ffa1-77e9935098bf.png)

__Burn-inを含めた全体の学習イメージ__
![qiita_08_md_6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/332b3191-0afb-1b84-b414-1d201d16320e.png)

### ステートフルNNモデル

``` python:NNモデル
def build_network(self):

    c = input_ = Input(batch_shape=(1, 1,) + self.input_shape)
    
    (enable_image_layerはステートレスと同じ)
            
    c = LSTM(self.lstm_units_num, stateful=True, name="lstm")(c)

    (省略)

※コード全体ではステートレスLSTMと切り替えられるようにパラメータを用意していますが解説では省略しています。
```

Input層ですが、Kerasの仕様上`batch_shape`を使ってバッチサイズまで指定する必要があるので指定しています。
前述した通りステートフルLSTMではバッチサイズを増やした場合の挙動が分からず batch_size は 1 です。
また、timestepsも for文 で制御するので 1 にしています。

次にLSTMレイヤーを取得しておきます。
どこでもいいのですが、とりあえずcompileで取得。

``` python:LSTMレイヤーの取得
def compile(self, optimizer, metrics=[]):
    （省略）
    # compile
    self.model.compile(loss=clipped_error_loss, optimizer=optimizer, metrics=metrics)

    # LSTMレイヤーの取得
    self.lstm = self.model.get_layer("lstm")
    self.target_lstm = self.target_model.get_layer("lstm")

    # super用
    self.compiled = True
```

### hidden state の扱い (とaction,rewardの保存)

まずは毎ステップの action と reward、hidden state を保存します。
また、Burn-in期間を指定する burnin_length をハイパーパラメータとして定義しておきます。

``` python:初期化
def reset_states(self):
    self.repeated_action = 0

    # action を input_sequence 分確保
    self.recent_action = [ 0 for _ in range(self.input_sequence)]

    # rewardn を input_sequence と multistep reward 分確保
    self.recent_reward = [ 0 for _ in range(self.input_sequence + self.multireward_steps - 1)]

    # observation を確保
    # 長さは burnin_length + input_sequence + multireward_steps 分
    obs_length = self.burnin_length + self.input_sequence + self.multireward_steps
    self.recent_observations = [np.zeros(self.input_shape) for _ in range(obs_length)]

    # hidden state の確保
    self.model.reset_states()
    self.recent_hidden_states = [
        [K.get_value(self.lstm.states[0]), K.get_value(self.lstm.states[1])]
        for _ in range(self.burnin_length + self.input_sequence)
    ]
```

hidden state は`self.model.reset_states()`を呼ぶと0で初期化されます。
また、hidden state の取得はLSTMレイヤーの`self.lstm.states[0]`と`self.lstm.states[1]`に対して`K.get_value`関数を使うと取得できます。

observations の取得方法は以下のように変わります。

``` python
# self省略
state0 = recent_observations[burnin_length:burnin_length+input_sequence]
state1 = recent_observations[-input_sequence:]
```

上記に合わせて変更します。

``` python
def forward(self, observation):
    # observation の取得は変わらず
    self.recent_observations.append(observation)
    self.recent_observations.pop(0)

    self.forward_train()

    # 学習でhidden stateが変化しているため、前回の状態を復元
    self.lstm.reset_states(self.recent_hidden_states[-1])

    action = （アクション取得）

    # action後のhidden stateを保存
    hidden_state = [K.get_value(self.lstm.states[0]), K.get_value(self.lstm.states[1])]
    self.recent_hidden_states.append(hidden_state)
    self.recent_hidden_states.pop(0)

    # action も配列に貯める
    self.recent_action.append(action)
    self.recent_action.pop(0)
    return action

def backward(self, reward, terminal):
    # reward も変わらず
    self.recent_reward.append(reward)
    self.recent_reward.pop(0)  

    （省略）

    return []
```

action前後で前stepの hidden state を復元して保存しています。
`reset_states`関数は引数に hidden state を与える事ができ、その値で初期化します。

アクション取得部分の大きな変更はありませんが、注意点としてアクションを取得している`model.predict`関数でも hidden state が変わるため、呼び出すのは1回だけにしないといけないという所です。
また、長くなってきたので簡単にリファクタリングしておきます。

``` python
def forward(self, observation):
    (略)

    action = self.select_action()
    
    (略)

def select_action(self):
    # noisy netが有効の場合はそちらで探索する
    if self.training and not self.enable_noisynet:
        
        # ϵ をstepで減少。
        epsilon = self.initial_epsilon - self.step*self.epsilon_step
        if epsilon < self.final_epsilon:
            epsilon = self.final_epsilon
        
        # ϵ-greedy法
        if epsilon > np.random.uniform(0, 1):
            # ランダム
            action = np.random.randint(0, self.nb_actions)
        else:
            action = self._get_qmax_action()
    else:
        action = self._get_qmax_action()

    return action

# 2箇所あるのでこちらも関数化
# 現状の最大Q値のアクションを返す
def _get_qmax_action(self):
    if self.lstm_type == "lstm_ful":
        # 最後の状態のみ
        state1 = [self.recent_observations[-1]]
        q_values = self.model.predict(np.asarray([state1]), batch_size=1)[0]
    else:
        # sequence分の入力
        state1 = self.recent_observations[-self.input_sequence:]
        q_values = self.model.predict(np.asarray([state1]), batch_size=1)[0]

    return np.argmax(q_values)
```


### メモリの追加部分

``` python:メモリ追加
def forward_train(self):
    （省略）

    # Multi-Step learning
    rewards = []
    for i in range(self.input_sequence):
        r = 0
        for j in range(self.multireward_steps):
            r += self.recent_reward[i+j] * (self.gamma ** i)
        rewards.append(r)

    self.memory.add((
        self.recent_observations[:],
        self.recent_action[:],
        rewards,
        self.recent_hidden_states[0]
    ))

    （省略）
```

[Multi-Step learning](https://qiita.com/pocokhc/items/fc00f8ea9dca8f8c0297#multi-step-learning)で使う報酬の計算はちょっと手抜きです。
毎回計算するので報酬追加時に計算させる方がいいかも…

メモリにはとりあえず現在の状態をすべて突っ込んでいます。
`hidden state`は最初の状態だけあればいいので`[0]`を保存しています。


### 学習部分

見やすくするために[Double DQN](https://qiita.com/pocokhc/items/fc00f8ea9dca8f8c0297#double-dqn)のみの場合を記載しています。

``` python:学習
def forward_train(self):
    （省略）
    # memory から取り出し
    (indexes, batchs, weights) = self.memory.sample(self.batch_size, self.step)

    # 各経験毎に処理を実施
    for batch_i, batch in enumerate(batchs):
        states = batch[0]
        action = batch[1]
        reward = batch[2]
        hidden_state = batch[3]
        prioritys = []

        # burn-in
        self.lstm.reset_states(hidden_state)
        for i in range(self.burnin_length):
            self.model.predict(np.asarray([[states[i]]]), 1)
        # burn-in 後の結果を保存
        hidden_state = [K.get_value(self.lstm.states[0]), K.get_value(self.lstm.states[1])]
    
        # 以降は1sequenceずつ更新させる
        for i in range(self.input_sequence):
            state0 = [states[self.burnin_length + i]]
            state1 = [states[self.burnin_length + i + self.multireward_steps]]

            # 現在のQネットワークを出力
            self.lstm.reset_states(hidden_state)
            output = self.model.predict(np.asarray([state0]), 1)[0]

            # TargetネットワークとQネットワークの値を出力
            self.lstm.reset_states(hidden_state)
            self.target_lstm.reset_states(hidden_state)
            state1_model_qvals = self.model.predict(np.asarray([state1]), 1)[0]
            state1_target_qvals = self.target_model.predict(np.asarray([state1]), 1)[0]

            # 最大のQ値を取得(Double DQNにて)
            action_q = np.argmax(state1_model_qvals)
            maxq = state1_target_qvals[action_q]

            # priority計算
            tmp = rescaling_inverse(maxq)
            tmp = reward[i] + (self.gamma ** self.multireward_steps) * tmp
            tmp *= weights[i]
            tmp = rescaling(tmp, self.rescaling_epsilon)
            priority = abs(tmp - outputs[i][action_batch[i]]) ** self.per_alpha
            prioritys.append(priority)
            
            # Q値 update用
            maxq = rescaling_inverse(maxq)
            td_error = reward[i] + (self.gamma ** self.multireward_steps) * maxq
            td_error *= weights[batch_i]
            td_error = rescaling(td_error, self.rescaling_epsilon)
            output[action[i]] = td_error

            # 学習
            self.lstm.reset_states(hidden_state)
            self.model.fit(
                np.asarray([state0]), 
                np.asarray([output]), 
                batch_size=1, 
                epochs=1, 
                verbose=0, 
                shuffle=False
            )

            # 次の学習用に hidden state を保存
            hidden_state = [K.get_value(self.lstm.states[0]), K.get_value(self.lstm.states[1])]

        # 今回使用したsamplingのpriorityを更新
        priority = self.priority_exponent * np.max(prioritys) + (1-self.priority_exponent) * np.average(prioritys)
        self.memory.update(indexes[batch_i], batch, priority)

    （省略）
```

1バッチ、1シーケンスを順次学習していく形です。
学習後に1バッチ分のシーケンスを使い priority を計算しています。
priority_exponent は最大値を優先するか平均値を優先するかの割合です。


# R2D2 の実装
特に難しいところはありません。
今回の実装内容を[Ape-X編](https://qiita.com/pocokhc/items/980640c6a7b5dc9d524e)のコードをベースに反映させていくだけです。

## GPU対応
機械学習用のPC買いました。
それに伴ってグラボ対応用に書き換えます。

tensorflowのGPU版のインストール方法は他を参照してください。
「tensorflow GPU インストール」等でググればでてきます。

まずは tensorflow のコンフィグを変更します。
ファイルの最初に以下を追加。

``` python

import 群

#--- GPU設定
from keras.backend.tensorflow_backend import set_session
config = tf.ConfigProto(
    gpu_options=tf.GPUOptions(
        allow_growth=True,
        per_process_gpu_memory_fraction=1.0,
    )
)
set_session(tf.Session(config=config))

(クラスの定義)
```

次にLearnerをGPU処理、ActorをCPU処理になるように書き換えます。
enable_GPU で切り替えれるようにしておきます。

``` python
class R2D2Manager():
    def _create_process(self, model_args, create_processor_func):
        (省略)

        if args["enable_GPU"]:
            self.learner_ps = mp.Process(target=learner_run_gpu, args=args)
        else:
            self.learner_ps = mp.Process(target=learner_run, args=args)
        (省略)

        for i in range(self.num_actors):
            (省略)
            if args_org["enable_GPU"]:
                self.actors_ps.append(mp.Process(target=actor_run_cpu, args=args))
            else:
                self.actors_ps.append(mp.Process(target=actor_run, args=args))


def learner_run_gpu(省略):
    with tf.device("/device:GPU:0"):
        learner_run(省略)

def learner_run(省略):
    (省略)

def actor_run_cpu(省略):
    with tf.device("/device:CPU:0"):
        actor_run(省略)

def actor_run(省略):
    (省略)
```

多分これでできているはず…


# Pendiumゲームで学習

## ハイパーパラメータ(RainbowR)

``` python
import gym

import pickle
import os
import numpy as np
import random
import math

import tensorflow as tf

from keras.optimizers import Adam
from keras.models import Model
from keras.layers import *
from keras import backend as K

import rl.core

import matplotlib.pyplot as plt
from PIL import Image, ImageDraw


# 各クラスは別ファイル想定です。(全コードでは1ファイルにまとめています)
from PendulumProcessorForDQN import PendulumProcessorForDQN
from RainbowRAgent import RainbowRAgent
from ObservationLogger import ObservationLogger
from MovieLogger import MovieLogger

# global
agent = None
logger = None

def main(image=False, lstm_type="lstm"):
    global agent, logger

    env = gym.make("Pendulum-v0")
    nb_actions = 5  # PendulumProcessorで5個と定義しているので5

    if image:
        processor = PendulumProcessorForDQN(enable_image=True, image_size=84)
        input_shape = (84, 84)
    else:
        processor = PendulumProcessorForDQN(enable_image=False)
        input_shape = env.observation_space.shape

    # 引数が多いので辞書で定義して渡しています。
    args={
        "input_shape": input_shape, 
        "enable_image_layer": image, 
        "nb_actions": nb_actions, 
        "input_sequence": 4,         # 入力フレーム数
        "memory_capacity": 1_000_000,  # 確保するメモリーサイズ
        "nb_steps_warmup": 200,     # 初期のメモリー確保用step数(学習しない)
        "target_model_update": 500, # target networkのupdate間隔
        "action_interval": 1,  # アクションを実行する間隔
        "train_interval": 1,   # 学習する間隔
        "batch_size": 16,   # batch_size
        "gamma": 0.99,     # Q学習の割引率
        "initial_epsilon": 1.0,  # ϵ-greedy法の初期値
        "final_epsilon": 0.01,    # ϵ-greedy法の最終値
        "exploration_steps": 10000,  # ϵ-greedy法の減少step数
        "processor": processor,

        "memory_type": "per_proportional",  # メモリの種類
        "per_alpha": 0.8,            # PERの確率反映率
        "per_beta_initial": 0.0,     # IS反映率の初期値
        "per_beta_steps": 5000,   # IS反映率の上昇step数
        "per_enable_is": False,      # ISを有効にするかどうか
        "multireward_steps": 1,    # multistep reward
        "enable_double_dqn": True,
        "enable_dueling_network": True,
        "dueling_network_type": "ave",  # dueling networkで使うアルゴリズム
        "enable_noisynet": False,
        "dence_units_num": 64,    # Dence層のユニット数

        # 今回追加分
        "lstm_type": "",
        "lstm_units_num": 64,
        "priority_exponent": 0.9,   # priority優先度
        "enable_rescaling_priority": True,   # rescalingを有効にするか(priotrity)
        "enable_rescaling_train": True,      # rescalingを有効にするか(train)
        "rescaling_epsilon": 0.001,  # rescalingの定数
        "burnin_length": 40,        # burn-in期間
    }

    if lstm_type == "lstm":
        args["lstm_type"] = "lstm"
    elif lstm_type == "lstm_ful":
        args["lstm_type"] = "lstm_ful"
        args["batch_size"] = 1

    agent = RainbowRAgent(**args)
    agent.compile(optimizer=Adam(lr=0.0002))
    print(agent.model.summary())

    # 訓練
    print("--- start ---")
    print("'Ctrl + C' is stop.")
    history = agent.fit(env, nb_steps=50_000, visualize=False, verbose=1)
    weights_file = "lstm_weight.h5"
    agent.save_weights(weights_file, overwrite=True)
    agent.load_weights(weights_file)

    # 結果を表示
    plt.subplot(1,1,1)
    plt.plot(history.history["episode_reward"])
    plt.xlabel("episode")
    plt.ylabel("reward")
    plt.show()

    # 訓練結果を見る
    processor.mode = "test"  # env本来の報酬を返す
    agent.test(env, nb_episodes=5, visualize=True)
    view = MovieLogger()   # 動画用
    logger = ObservationLogger()
    agent.test(env, nb_episodes=1, visualize=False, callbacks=[logger,view])
    view.view(interval=1, gifname="anim1.gif")  # 動画用

    #--- NNの可視化
    if image:
        plt.figure(figsize=(8.0, 6.0), dpi = 100)  # 大きさを指定
        plt.axis('off')
        ani = matplotlib.animation.FuncAnimation(plt.gcf(), plot, frames=150, interval=5)
        #ani = matplotlib.animation.FuncAnimation(plt.gcf(), plot, frames=len(logger.observations), interval=5)

        #ani.save('anim2.mp4', writer="ffmpeg")
        ani.save('anim2.gif', writer="imagemagick", fps=60)
        #plt.show()

# コメントアウトで切り替え
#main(image=False, lstm_type="")
main(image=False, lstm_type="lstm")
#main(image=False, lstm_type="lstm_ful")
```


## ハイパーパラメータ(R2D2)

``` python
import gym

import pickle
import os
import numpy as np
import random
import time
import traceback
import math

import tensorflow as tf

from keras.optimizers import Adam
from keras.models import Model
from keras.layers import *
from keras import backend as K

import rl.core

import multiprocessing as mp

import matplotlib.pyplot as plt
from PIL import Image, ImageDraw


# 各クラスは別ファイル想定です。(全コードでは1ファイルにまとめています)
from PendulumProcessorForDQN import PendulumProcessorForDQN
from R2D2Manager import R2D2Manager
from ObservationLogger import ObservationLogger
from MovieLogger import MovieLogger

# global
agent = None
logger = None


ENV_NAME = "Pendulum-v0"
def create_processor():
    return PendulumProcessorForDQN(enable_image=False)

def create_processor_image():
    return PendulumProcessorForDQN(enable_image=True, image_size=84)

def create_optimizer():
    return Adam(lr=0.00025)

def actor_func(index, actor, callbacks):
    env = gym.make(ENV_NAME)
    if index == 0:
        verbose = 1
    else:
        verbose = 0
    actor.fit(env, nb_steps=200_000, visualize=False, verbose=verbose, callbacks=callbacks)
    
#--------------------------------------

def main(image):
    global agent, logger
    env = gym.make(ENV_NAME)

    if image:
        processor = create_processor_image
        input_shape = (84, 84)
    else:
        processor = create_processor
        input_shape = env.observation_space.shape

    # 引数
    args = {
        # model関係
        "input_shape": input_shape, 
        "enable_image_layer": image, 
        "nb_actions": 5, 
        "input_sequence": 4,     # 入力フレーム数
        "dense_units_num": 64,  # Dense層のユニット数
        "metrics": [],           # optimizer用
        "enable_dueling_network": True,  # dueling_network有効フラグ
        "dueling_network_type": "ave",   # dueling_networkのアルゴリズム
        "enable_noisynet": False,        # NoisyNet有効フラグ
        "lstm_type": "lstm",             # LSTMのアルゴリズム
        "lstm_units_num": 64,   # LSTM層のユニット数
        
        # learner 関係
        "remote_memory_capacity": 100_000,    # 確保するメモリーサイズ
        "remote_memory_warmup_size": 200,    # 初期のメモリー確保用step数(学習しない)
        "remote_memory_type": "per_proportional", # メモリの種類
        "per_alpha": 0.8,        # PERの確率反映率
        "per_beta_initial": 0.0,     # IS反映率の初期値
        "per_beta_steps": 100_000,   # IS反映率の上昇step数
        "per_enable_is": False,      # ISを有効にするかどうか
        "batch_size": 16,            # batch_size
        "target_model_update": 1500, #  target networkのupdate間隔
        "enable_double_dqn": True,   # DDQN有効フラグ
        "enable_rescaling_priority": False,   # rescalingを有効にするか(priotrity)
        "enable_rescaling_train": False,      # rescalingを有効にするか(train)
        "rescaling_epsilon": 0.001,  # rescalingの定数
        "burnin_length": 20,        # burn-in期間
        "priority_exponent": 0.9,    # priority優先度

        # actor 関係
        "local_memory_update_size": 50,    # LocalMemoryからRemoteMemoryへ投げるサイズ
        "actor_model_sync_interval": 500,  # learner から model を同期する間隔
        "gamma": 0.99,      # Q学習の割引率
        "epsilon": 0.3,        # ϵ-greedy法
        "epsilon_alpha": 1,    # ϵ-greedy法
        "multireward_steps": 1, # multistep reward
        "action_interval": 1,   # アクションを実行する間隔
        
        # その他
        "load_weights_path": "",  # 保存ファイル名
        #"load_weights_path": "qiita08r2d2.h5",  # 読み込みファイル名
        "save_weights_path": "qiita08_r2d2_image_lstm.h5",  # 保存ファイル名
        "save_overwrite": True,   # 上書き保存するか
        "logger_interval": 10,    # ログ取得間隔(秒)
        "enable_GPU": False,      # GPUを使うか
    }


    manager = R2D2Manager(
        actor_func=actor_func, 
        num_actors=1,    # actor数
        args=args, 
        create_processor_func=processor,
        create_optimizer_func=create_optimizer,
    )
    
    agent, learner_logs, actors_logs = manager.train()
    
    #--- plot
    plot_logs(learner_logs, actors_logs)

    #--- test
    agent.processor.mode = "test"  # env本来の報酬を返す
    agent.test(env, nb_episodes=5, visualize=True)

    view = MovieLogger()   # 動画用
    logger = ObservationLogger()
    agent.test(env, nb_episodes=1, visualize=False, callbacks=[view, logger])
    view.view(interval=1, gifname="anim1.gif")  # 動画用

    #--- NNの可視化
    if image:
        plt.figure(figsize=(8.0, 6.0), dpi = 100)  # 大きさを指定
        plt.axis('off')
        #ani = matplotlib.animation.FuncAnimation(plt.gcf(), plot, frames=150, interval=5)
        ani = matplotlib.animation.FuncAnimation(plt.gcf(), plot, frames=len(logger.observations), interval=5)

        #ani.save('anim2.mp4', writer="ffmpeg")
        ani.save('anim2.gif', writer="imagemagick", fps=60)
        #plt.show()


if __name__ == '__main__':
    # コメントアウトで切り替え
    main(image=False)
    #main(image=True)

```


## 結果
画像入力、ステートレスLSTMの結果です。

![qiita_08_r2d2_image_lstm.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/4adc7632-1d1b-19c3-237b-60779ef42762.png)

![qiita_08_r2d2_image_lstm.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/a5e44ced-0b12-160b-338d-d867a800f2ce.gif)




# あとがき
Ape-Xでも感じていましたが、GPU+CPUでないとあまり効果を感じないと思います。
(GoogleColaboratoryでも並列の恩師は微妙でした)
ただ、GPU+CPUと分かれている場合はやはり早いですね。

次回は今まで実装しっぱなしだったので各手法について比較してみたいと思い・・・ますが、
その前にアイデアとしてもっていた探索ポリシーについて実装してみます。