Pythonの強化学習ライブラリKeras-RLのパラメータ設定

# はじめに
Pythonライブラリ[Keras-RL](https://github.com/keras-rl/keras-rl)は強化学習のことがあまりわかっていなくても使えてしまうのですが、
細かいチューニングをしようと思うとパラメータの意味を理解していないといけません。

そうすると強化学習の数式レベルの理解がある程度必要になります
（行動価値関数などの最終的な式の意図や動作が把握できている程度でよい）。

よくわかっていないという人は個人的には以下の連載記事が一番わかりやすかったのでおすすめです。

[今さら聞けない強化学習（1）：状態価値関数とBellman方程式](https://qiita.com/triwave33/items/5e13e03d4d76b71bc802)

本記事では、Keras-RLの各パラメータがどのような意味を持つのかを説明しながら、
実際の設定値などを紹介していきたいと思います。

（私も完全に理解している訳ではないので誤りがありましたらコメントいただければと思います）

# 想定読者
- 強化学習の思想やKeras-RLの使い方の流れはわかったけど、細かいパラメータを適当に設定している人

# Keras-RLのパラメータ
[Keras-RL Documentation](https://keras-rl.readthedocs.io/en/latest/agents/overview/)の Available Agentsには以下のAgentが利用可能であると記載されています。

- DQN
- DDPG
- NAF
- CEM
- SARSA

また、DDQN(DoubleDQN)とDuelingDQNはDQNのパラメータで設定できます。

数が多いので、ここでは最もメジャーなDQNに絞ってパラメータを見ていきます。

## 環境、DNNモデル
まずはgymの環境とDNNモデルを作成しておきます。
環境は例としてCartPole-v0とします。

```python
from keras.models import Sequential
from keras.layers import Dense, Activation, Flatten, Dropout
import gym

env = gym.make('CartPole-v0')
nb_actions = env.action_space.n

model = Sequential()
model.add(Flatten(input_shape=(1,) + env.observation_space.shape))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(16))
model.add(Activation('relu'))
model.add(Dense(nb_actions))
model.add(Activation('linear'))
```

DNNの入力層と出力層はニューロン数が固定されます。
DQNのDNNは「環境の観測値に応じた行動価値関数の近似」をするために用いられるので、
「環境で観測される値の次元=env.observation_space.shape」が入力となり、
「行動空間の次元＝nb_actions」が出力となります。

その間の層は全結合（Dense）やDropout、活性化関数（Activation）を自由に積み重ねてください。

## Experience Replay用のメモリ

Experience ReplayとはDNNの学習を安定させるために用いられる仕組みです。

DNNは時系列に相関があるデータをその順番のまま使うとうまく学習できないらしいので、
メモリにaction、reward、observationsなどのデータを経験（Experience）として保管しておいて、後でランダムにデータを再生（Replay）して学習を行います。

```python
from rl.memory import SequentialMemory

memory = SequentialMemory(limit=50000, window_length=1, ignore_episode_boundaries=False)
```

|パラメータ|必須|意味|
|:--|:--:|:--|
|limit|○|メモリ（collections.deque）の上限サイズ。action（行動）、reward（報酬）、observations（観測）のそれぞれにdequeが用意される。上限を超えると古いデータが書き換えられていく|
|window_length|○|観測を何個連結して処理するか。例えば時系列の複数の観測をまとめて1つの状態とする場合に利用。|　
|ignore_episode_boundaries||エピソードの境界を無視するかどうか（別のエピソードの経験を利用する場合はTrue）。|

なお、もうひとつ`rl.memory`にある`EpisodeParameterMemory`クラスは[CEMで利用可能なメモリ](https://stackoverflow.com/questions/47140642/what-does-the-episodeparametermemory-of-keras-rl-do)とのことです。

## 行動ポリシー

```python
from rl.policy import LinearAnnealedPolicy
from rl.policy import SoftmaxPolicy
from rl.policy import EpsGreedyQPolicy
from rl.policy import GreedyQPolicy
from rl.policy import BoltzmannQPolicy
from rl.policy import MaxBoltzmannQPolicy
from rl.policy import BoltzmannGumbelQPolicy

# どれか選択
policy = LinearAnnealedPolicy(EpsGreedyQPolicy(), attr='eps', 
                      value_max=1., value_min=.1, value_test=.05, nb_steps=1000)
policy = SoftmaxPolicy()
policy = EpsGreedyQPolicy(eps=0.1)
policy = GreedyQPolicy()
policy = BoltzmannQPolicy(tau=1., clip=(-500., 500.))
policy = MaxBoltzmannQPolicy(eps=.1, tau=1., clip=(-500., 500.))
policy = BoltzmannGumbelQPolicy(C=1.0)
```

行動ポリシーとは、環境において行動を選択する基準となるものです。
上記以外にも、Policyクラスを継承して自作してもいいとのことです。

`LinearAnnealedPolicy`は引数に指定したポリシーについて、`attr`に指定したパラメータを`value_max`から`value_min`まで`nb_steps`回目までに線形に変化させるというものです。
`value_test`についてはテスト時の固定的なパラメータ値となります。

`BoltzmannGumbelQPolicy`についてはトレーニング時のみに利用できるポリシーとのことです。

|パラメータ|必須|意味|
|:--|:--:|:--|
|eps||ランダムな行動を選択する確率（イプシロン）。値が大きいほど探索に重きを置いて行動を決定する。|
|tau||ボルツマン分布を利用したソフトマックス手法のQ値を割る値。|
|clip||Q値を`tau`で割った後にクリップする範囲。|
|C||ガンベル分布の乗数`beta`の計算式`beta = C/np.sqrt(action_counts)`で利用。|


## エージェント

```python
from rl.agents.dqn import DQNAgent

agent = DQNAgent(nb_actions=nb_actions, memory=memory, gamma=.99, batch_size=32, nb_steps_warmup=1000,
                 train_interval=1, memory_interval=1, target_model_update=10000,
                 delta_range=None, delta_clip=np.inf, custom_model_objects={}, 
                 model=model, policy=None, test_policy=None, enable_double_dqn=False,
                 enable_dueling_network=False, dueling_type='avg')
```

大量にパラメータがありますが、必須のものはごく一部です。

|パラメータ|必須|意味|
|:--|:--:|:--|
|nb_actions|○|行動空間の次元数。環境により決まるためこの段階で変更することはない。|
|memory|○|事前に作ったExperience Replay用のメモリ。|
|gamma||行動価値関数の式に出てくる割引率。将来の価値をどれくらい考慮するかを決める（値が大きいほど将来の価値を大きく反映する）。1未満の値を設定する。|
|batch_size||学習のバッチサイズ|
|nb_steps_warmup||ウォームアップステップ数。学習の初期は安定しないため、学習率を徐々に上げていく期間。|
|train_interval||トレーニングのインターバル。`train_interval`ステップ毎に学習が実行される。|
|memory_interval||Experience Replay用のメモリにデータを貯めるインターバル。`memory_interval`ステップ毎にメモリに経験が保存される。|
|target_model_update||0以上の値。1未満の値の場合はSoft updateと呼ばれ、`(1 - target_model_update) * old + target_model_update * new`の式で重みが更新される。1以上の値の場合はHard updateと呼ばれ、`int(target_model_update)`ステップごとに重みが完全に更新される。|
|delta_range||非推奨のパラメータ。delta_clipを代わりに使ってくれとのこと。もし値が設定された場合は`delta_clip = delta_range[1]`となる。|
|delta_clip||Huber損失のデルタ値。|
|custom_model_objects||ターゲットモデルを生成する際のオプション。詳細は[こちら](https://www.tensorflow.org/api_docs/python/tf/keras/models/model_from_config)を参照。|
|model|○|事前に作ったDNNのモデル。|
|policy||行動ポリシー。設定しない場合はEpsGreedyQPolicyが適用される。|
|test_policy||テスト時の行動ポリシー。設定しない場合はGreedyQPolicyが適用される。|
|enable_double_dqn||DoubleDQN(DDQN)を適用するかどうか。|
|enable_dueling_network||DuelingDQNを適用するかどうか。Trueにすると出力層の前の層にDueling Networkを挿入してくれる。|
|dueling_type||Duelingネットワークのタイプ（enable_dueling_networkがTrueのときに使用）。行動価値関数 Q(s,a)を状態価値関数V(s)とアドバンテージA(s,a)から計算する際の計算方法を決める。avg、max、naiveが選択できるが、avgが推奨されているとのこと。具体的な式は以下を参照。|

avg: `Q(s,a;theta) = V(s;theta) + (A(s,a;theta)-Avg_a(A(s,a;theta)))`
max: `Q(s,a;theta) = V(s;theta) + (A(s,a;theta)-max_a(A(s,a;theta)))`
naive: `Q(s,a;theta) = V(s;theta) + A(s,a;theta)`

## エージェントのコンパイル

```python
from keras.optimizers import Adam

agent.compile(optimizer=Adam(lr=1e-3), metrics=['mse'])
```
|パラメータ|必須|意味|
|:--|:--:|:--|
|optimizer|○|DNN重み更新の最適化手法。利用可能な手法は[Kerasのoptimizers](https://keras.io/ja/optimizers/)。更新式は[この記事](https://qiita.com/tokkuman/items/1944c00415d129ca0ee9)に整理されている。Adamであれば学習率(Learning rate: lr)は1e-3が論文で推奨されているらしい|
|metrics||評価関数のリストで、[Kerasの評価関数](https://keras.io/ja/metrics/)が使える。または自作の`lambda y_true, y_pred: metric`の形式で指定できる。指定の有無にかかわらずmean_qという評価関数が追加される。|

## エージェントの学習

```python
agent.fit(env=env, nb_steps=nb_steps, action_repetition=1, callbacks=None, verbose=1,
          visualize=False, nb_max_start_steps=0, start_step_policy=None, log_interval=10000,
          nb_max_episode_steps=None)
```

|パラメータ|必須|意味|
|:--|:--:|:--|
|env|○|事前に作った強化学習環境。|
|nb_steps|○|シミュレーションのステップ数。１ステップは、１回の観測、行動、報酬の獲得。|
|action_repetition||行動の繰り返し回数。2以上にすると、観測なしに同じ行動を`action_repetition`回繰り返す。|
|callbacks||コールバックのリスト。`rl.callbacks.Callback`を継承したコールバックが使える。ステップの開始・終了、エピソードの開始・終了、アクションの開始・終了がイベントとなる。|
|verbose||ロギングのモード。0でロギングなし、1で`log_interval`ステップ毎にロギング、2でエピソード毎にロギング。具体的には`rl.callbacks.TrainIntervalLogger`が呼ばれる。|
|visualize||可視化のEnable。具体的には`rl.callbacks.Visualizer`が呼ばれる。環境で`render(mode='human'))`が実装されていないと例外が起きる。|
|nb_max_start_steps||この値を最大値とする乱数値回目からステップを開始する。シミュレーションの開始位置を変動させたい場合に利用。この間はExperience Replayに記録されない。|
|start_step_policy||`nb_max_start_steps`が0でない場合に有効。`nb_max_start_steps`までの間にどのような行動ポリシーとするかを`lambda observation: action`の形式で指定。Noneの場合はランダムに行動が選択される。|
|log_interval||ロギングするステップ間隔。|
|nb_max_episode_steps||1エピソードにおける最大のステップ数。|

このパラメータを見ればわかるように、Keras-RLではエピソード数を決めるのではなく、
ステップ数と1エピソードにおける最大のステップ数で「最小のエピソード数」が決まります。
つまり、`nb_steps / nb_max_episode_steps`が最小エピソード数となります。

例えば`nb_steps=3000`、`nb_maxepisode_steps=300`の場合は、最低`3000/300=10`回のエピソードが繰り返されます。
ただし、`nb_max_episode_steps`よりも少ないステップ数でエピソードが終了することもあるので、
エピソード数は最大で`nb_steps`回となります（ほぼありえませんが、毎回1ステップ目でシミュレーションが終了する場合）。

# おわりに
今回はDQNのパラメータを見てきましたが、他のAgentでも共通する部分もあればそうでない部分もあります。
（異なる部分は例えば、SARSAはExperience Replayをしないのでメモリが不要、など）

公式ドキュメントがそれほど充実しているわけではないので、
もし他のAgentのパラメータ設定について知りたい場合はソースコードを見るのが手っ取り早いと思います。