ニューラルネットでQを推定するモデルqnet を説明します。
qnet の概要
前章のQ学習は、未知の観測に対して行動選択をすることができませんでした。
そこで、観測を入力としてQ値を出力するニューラルネットを作ることを考えます(ディープラーニングと言った方が聞こえはよいのですが、今回は、中間層を1層か2層までしか考えませんので、控えめにニューラルネットと呼ぶことにします)。
ニューラルネットを使う理由は、ニューラルネットは、未知の観測が入力されても、入力情報の特徴からそれらしい値を出すことができるからです。
ニューラルネットを強化学習に応用する試みは昔から行われてきたのですが、テレビゲームを学習させたDQN(deep Q-network) が有名です(Mnih 2015 nature)。DQN は、学習がより安定するようにExperience ReplayとFixed Target Q-Networkという技法を取り入れていますが、ここで紹介するqnet は、ニューラルネットの効果が分かりやすいように、ニューラルネットだけを使ったものにしました。
ニューラルネットをQ学習で利用するには、教師あり学習のテクニックを使います。
ニューラルネット・教師あり学習については、ここで要点だけの解説としますが、もっと分かりやすくきちんと知りたいという方は「新しい機械学習の教科書」にその数学的な基本とpython/tensorflowでの動かし方を説明しましたので、良かったら参考にしてください。
qnet で使う教師あり学習
ニューラルネットモデルもディープラーニングモデルも単なる関数です。入力[math]\bf{x}[/math]に対して、[math]\bf{y}[/math]を出力するものです。ここでは、多次元入力[math]\bf{x}[/math]に対してベクトル値[math]\bf{y}[/math]を出力する回帰のモデルを考えます。
イメージとしてはこのような関数です。
[math]
\bf{y} = f(\bf{x}; \bf{w})
[/math]
[math]\bf{w}[/math]はモデルのパラメータ(重み)であり、これを適切に変化させることで、出力[math]\bf{y}[/math]が望む目標値[math]\bf{t}[/math]になるように調節します。
今、教師データとして、N個の[math]\bf{x}_n[/math]と[math]\bf{t}_n[/math]のペアを持っているとします。[math]n[/math]は、[math]0 \cdots N-1[/math]の値をとるデータのインデックスです。
[math]\bf{x}_n[/math]に対するモデルの出力[math]\bf{y}_n[/math]をターゲットである[math]\bf{t}_n[/math]に近づけることが学習の目的になります。
まず、[math]\bf{x}_n[/math]に対するモデルの出力[math]\bf{y}_n[/math]と、その目標(ターゲット)[math]\bf{t}_n[/math]との誤差[math]E_n[/math]を、二乗誤差で定義します。
[math]
E_n = \sum_a (y_{n, a} – t_{n, a})^2
[/math]
ここで、[math]y_{n, a}, t_{n, a}[/math]は、ベクトル[math]\bf{y}_n[/math], [math]\bf{t}_n[/math]の[math]a[/math]番目の成分を意味します。
これを全てデータで和を取った二乗和誤差[math]E[/math]を定義します。この二乗和誤差を最小にする[math]\bf{w}[/math]を見つめることが学習の目的となります。
[math]
E = \sum_{n=0}^{N-1} E_n
[/math]
二乗和誤差[math]E[/math]を勾配法で減らしていくための学習則を成分表記で表すと、
[math]
w_i \leftarrow w_i – \alpha \frac{\partial E}{\partial w_i}|_{\bf{w}}
[/math]
となります。これは、個々の誤差で表すと、
[math]
w_i \leftarrow w_i – \alpha \sum_{n=0}^{N-1} \frac{\partial E_n}{\partial w_i}|_{\bf{w}}
[/math]
と書けるので、1つずつ[math]n[/math] を選んで、以下の更新を繰り返しても同じ学習の効果が得られることになります。
[math]
w_i \leftarrow w_i – \alpha \frac{\partial E_n}{\partial w_i}|_{\bf{w}}
[/math]
qnet ではこの更新則を使います。
0からプログラムを作る場合には、最後の偏微分を計算する必要があります。しかし、本記事ではライブラリ(tensorflow)を使うので偏微分の計算は不要です。
ライブラリの関数に渡す入力[math]\bf{x}_n[/math]とターゲット[math]\bf{t}_n[/math]を準備すれば、ネットワークのパラメータを更新することができます。
Q学習に教師あり学習を適用
それでは、いよいよ、ニューラルネットワークにQ-table の代わりをさせることを考えます。
今の観測[math]\bf{x}_n[/math]を入力して、出力[math]\bf{y}[/math]がQ値となるようにネットワークをトレーニングするのです。
前章のQ学習の学習則を思い出してみましょう。
[math]
\begin{eqnarray}
Q(x, a) & \leftarrow & Q(x, a) \\
& & – \alpha (\text{output} – \text{target})
\end{eqnarray}
[/math]
[math]
\begin{eqnarray}
\text{output} &=& Q(x, a) \\
\text{target} &=& r + \gamma \max_{a’} Q(x’, a’)
\end{eqnarray}
[/math]
ここで、[math]x=x(t), a=a(t), r=r(t), x’=x(t+1)[/math]としています。
ここでの観測[math]x[/math] をニューラルネットの入力[math]\bf{x}[/math]とし、その出力[math]\bf{y}[/math] が [math][Q(x, a=0), Q(x, a=1), …]^T[/math]に対応するとします。
そして、target はニューラルネットの出力の目標値[math]\bf{t}[/math] に対応させます。ただし、目標値[math]\bf{t}[/math]はベクトルです。一方、ここでのtarget はスカラー値です。これは、選択された行動[math]a[/math] の要素 [math]t_{a}[/math] のみの目標値に対応しているからです。
そこで、上図のように、いったん[math]\bf{t}[/math] に[math]\bf{y}[/math]をコピーしておき、[math]a[/math]番目の成分のみtarget に書き換えるということをします。
これで、[math]\bf{x}[/math]と[math]\bf{t}[/math]を、1ペア分準備することができましたので、ライブラリーに渡して学習をさせることができます。
この手続きを、強化学習の1ステップ毎に繰り返すことを想定します。
意味的には、上に書いた一つ分の誤差[math]E_n[/math]だけで学習を進めるということに対応します。
[math]
w_i \leftarrow w_i – \alpha \frac{\partial E}{\partial w_i}|_{\bf{w}}
[/math]
この学習を何度も繰り返すことで、目的の二乗和誤差を減らしていることになってきます(ただし、選ばれやすい[math]n[/math]とたまにしか選ばれない[math]n[/math]の偏りがあることは心にとどめておきましょう)。
[math]
w_i \leftarrow w_i – \alpha \sum_n \frac{\partial E}{\partial w_i}|_{\bf{w}}
[/math]
パラメータ設定、__init_()
それでは、qnet が実装されているagt_qnet.py の class Agt を見ていきます。
まず、__init__()では、qnet で使用するパラメーターをセットします。
class Agt(core.coreAgt): """ Q値をNNで近似するエージェント """ def __init__( self, n_action=2, input_size=(7, ), epsilon=0.1, gamma=0.9, n_dense=32, n_dense2=None, filepath=None, ): """ Parameters ---------- n_action: int 行動の種類の数 input_size: tuple of int 例 (7,), (5, 5) 入力の次元 epsilon: float (0から1まで) Q学習のε、乱雑度 gammma: float (0から1まで) Q学習のγ、割引率 n_dense: int 中間層1のニューロン数 n_dense2: int or None 中間層2のニューロン数 None の場合は中間層2はなし filepath: str セーブ用のファイル名 """ self.n_action = n_action self.input_size = input_size self.epsilon = epsilon self.gamma = gamma self.n_dense = n_dense self.n_dense2 = n_dense2 self.filepath = filepath super().__init__() # 変数 self.time = 0
ここで、n_dense は中間ユニットの数です。また、n_dense2に数字を入れると中間層の2層目がn_dense2の数のユニットで構成されます。
このプログラムは、sim_swamptour.py を実行することで動きますが、その時のパラメータは、sim_swamptour.py の中に記載されています。
モデルの構築、build_model
tensorflow でモデルを構築するには、Sequential かfunctional API の二つの方法がありますが、ここでは、後者のfunctional APIの方式を選びました。
functional APIでもSequentialと同じくらい簡単だと思いますし、
その方が、分岐させるなどの拡張がしやすいです。
以下、modelを構築するメソッド build_model()です。
class Agt(core.coreAgt): --- 省略 def build_model(self): inputs = tf.keras.Input(shape=(self.input_size)) x = tf.keras.layers.Flatten()(inputs) x = tf.keras.layers.Dense(self.n_dense, activation='relu')(x) if self.n_dense2 is not None: x = tf.keras.layers.Dense(self.n_dense2, activation='relu')(x) outputs = tf.keras.layers.Dense(self.n_action, activation='linear')(x) self.model = tf.keras.Model(inputs=inputs, outputs=outputs) self.model.compile( optimizer='adam', loss='mean_squared_error', metrics=['mse'] )
行動選択、action_selection()
行動選択は、おなじみの epsilon-greedy 法です。
まず、(A) の get_Q() でネットワークに各action のQを出力させ、Q値が一番多いaction を、(B)で選んでいます。
class Agt(core.coreAgt): --- 省略 def select_action(self, observation): Q = self.get_Q(observation) # (A) if self.epsilon < np.random.rand(): action = np.argmax(Q) # (B) else: action = np.random.randint(0, self.n_action) return action def get_Q(self, observation): obs = self._trans_code(observation) Q = self.model.predict(obs.reshape((1,) + self.input_size))[0] return Q def _trans_code(self, observation): """ observationを内部で変更する場合はこの関数を記述 """ return observation
学習、learn()
コアとなる学習部分ですが、コードは長くありません。
(A)で今の観測 observation に対するQを得て、(B1)(B2)でtarget を計算します。
(C)でtarget の値をQのaction 番目の要素に入れます。
(D)で、このQを[math]\bf{t}[/math]として学習の関数 model.fit()に渡します。
class Agt(core.coreAgt): --- 省略 def learn(self, observation, action, reward, next_observation, done): """ Q(obs, act) <- (1-alpha) Q(obs, act) + alpha ( rwd + gammma * max_a Q(next_obs)) input : (obs, act) output: Q(obs, act) target: rwd * gamma * max_a Q(next_obs, a) """ obs = self._trans_code(observation) next_obs = self._trans_code(next_observation) Q = self.model.predict(obs.reshape((1,) + self.input_size)) # (A) if done is False: next_Q = self.model.predict(next_obs.reshape((1,) + self.input_size))[0] target = reward + self.gamma * max(next_Q) # (B1) else: target = reward # (B2) Q[0][action] = target # (C) self.model.fit(obs.reshape((1,) + self.input_size), Q, verbose=0, epochs=1) # (D)
保存と読み出し、save_weights(), load_weights()
最後、ネットワークのパラメータ(重み)の保存と読み出しのメソッドです。
class Agt(core.coreAgt): --- 省略 def save_weights(self, filepath=None): if filepath is None: filepath = self.filepath self.model.save_weights(filepath, overwrite=True) def load_weights(self, filepath=None): if filepath is None: filepath = self.filepath self.model.load_weights(filepath)
以上がagt_qnet.py の全てになります。
agt_qnet で出来るタスクとできないタスク
agt_qnet は、qが解くことのできる silent_ruin, open_fieldを解くことができます。
ruin_1swampもqnetと同じように大体できます。
そして、qができなかった、many_swampを割と良い感じで解くことができます。これが、q_net の成果です。
many_swamp です。
ruin_1swamp は、そこそこです。近くのゴールなら行くことができても、回り込む必要がある場合には失敗します。ただこれは、学習を追加したりパラメータをチューニングすることで改善する可能性があると思います。
しかし、予想通り短期記憶を必要とするTmaze_both, Tmaze_either, ruin_2swampを解くことはできません。