Edited at

いまさらだけどTensorFlowでDQN(不完全版)を実装する

More than 3 years have passed since last update.


DQNって何?

もはやこの手の記事は至るところにあるので、あえて説明しなくてもいいような気がしますが、強化学習の一つの方法であるQ-LearningのQ関数部分を、DeepLearningを使って近似し、それまで難しかった画像から直接Q値を推定することを可能にした学習手法です。

Google DeepMindによって2014に発表され、Atariの数あるゲーム(インベーダーゲームとか、ブロック崩し)で、事前知識なしにコンピュータが高得点が取れるようになることが示されて、注目されました

ちなみに、DQNはDeepQNetworkの略です。


じゃあTensorFlowって何?

TensorFlowはGoogleが出している数値計算ライブラリです(雑)。

DeepLearning専用のライブラリではなく、様々な機能があります。

もちろん、DeepLearningを実装するのに必要なモジュールも充実しています。


DQN(不完全版)???

Nature論文のDQNはOptimizerとしてA.Gravesがこの論文で導入した、通常とは異なるRMSProp(以下、RMSPropGraves)を使っています。

標準のTensorFlowには、RMSPropGravesは実装されていないので、標準のTensorFlowにもある通常版RMSPropを使ったDQNが、この記事で言うDQN(不完全版)です。

Natureの論文を見るとRMSPropとして書いてあり、紛らわしいのですが、いろいろなところに転がっているサンプルソースも通常のRMSPropを使って、これがDQNだ!的な感じになってるので、他の人が書いているものを参考にする場合は、このあたり注意が必要です。

具体的なRMSPropとRMSPropGravesの違いは以下の通りです。


RMSProp

{\begin{align}

n &\leftarrow \rho n + (1-\rho)g_r^2 \\
m &\leftarrow \beta m - \frac \alpha {(n + \gamma)}g_r
\end{align}}


RMSPropGraves

{\begin{align}

n &\leftarrow \rho n + (1-\rho)g_r^2 \\
g &\leftarrow \rho g + (1-\rho)g_r \\
m &\leftarrow \beta m - \frac \alpha {(n - g^2 + \gamma)}g_r
\end{align}}

イメージ的には、RMSPropが平均的な傾きの大きさを見て、値の更新幅を変えるのに対して、RMSPropGravesは傾きの分散に応じて、更新幅を変える感じでしょうか。

あとここで実装したDQNが、もう一点オリジナルと違うのは、通常のRMSPropを使う関係で、論文にあるLoss Clippingも行いません。

理由は、Clippingしたら、まともに学習が進まなかったからです・・・


DQN(不完全版)を実装するには


必要なライブラリ等

の3つが最低でも必要です(python関係のライブラリはTensorFlowのインストールで入ります)

Romは検索すればそこら中に転がっていてすぐ見つかると思います。(Atari 2600 romとかで検索してみてください)

RL-glueを使ってる方もいますが、Arcade Learning Environmentは標準でPythonのAPIも用意されているので、必須ではないです。

ここでは上の3つが準備出来ているものとして進めます。

GitHubにコードは載せているので、以下はTensorFlowで実装する際の要点を書いておきます。


実装


TensorFlowでDQNのネットワークを書く

DQNのネットワークは下記のようになっています。入力画像を入れて、ゲームをするのに可能な各動き(上下左右など)に対するQ値を出力するような構成です。


  • 入力: 84*84のゲーム画面

  • 第1層: Filter数 32, Filter size 8*8, stride 4, paddingなしのconvolution layer

  • 第2層: Filter数 64, Filter size 4*4, stride 2, paddingなしの convolution layer

  • 第3層: Filter数 64, Filter size 3*3, stride 1, paddingなしのconvolution layer

  • 第4層: (input 3136), output 512のFully connected layer

  • 第5層: (input 512), output ゲームで使えるキー操作数(ブロックくずしなら4)のFully connected layer

  • 活性化関数: Reluを使う

TensorFlowでは、パラメータである重みと、実際の演算部分は分かれているので、実装は次のような感じになります(パラメータに関しては、第1層部分と、第4層部分の抜粋)。

  # Parameter部分

def __init__(self, image_width, image_height, num_channels, num_actions):
# conv1
self.conv1_filter_size = 8
self.conv1_filter_num = 32
self.conv1_stride = 4
self.conv1_output_size = (image_width - self.conv1_filter_size) / self.conv1_stride + 1 # 20 with 84 * 84 image and no padding
self.conv1_weights, self.conv1_biases = \
self.create_conv_net([self.conv1_filter_size, self.conv1_filter_size, num_channels, self.conv1_filter_num], name='conv1')

# inner product 1
self.inner1_inputs = self.conv3_output_size * self.conv3_output_size * self.conv3_filter_num # should be 3136 for default
self.inner1_outputs = 512
self.inner1_weights, self.inner1_biases = self.create_inner_net([self.inner1_inputs, self.inner1_outputs], name='inner1')

def create_conv_net(self, shape, name):
weights = tf.Variable(tf.truncated_normal(shape=shape, stddev=0.01), name=name + 'weights')
biases = tf.Variable(tf.constant(0.01, shape=[shape[3]]), name=name + 'biases')
return weights, biases

def create_inner_net(self, shape, name):
weights = tf.Variable(tf.truncated_normal(shape=shape, stddev=0.01), name=name + 'weights')
biases = tf.Variable(tf.constant(0.01, shape=[shape[1]]), name=name + 'biases')
return weights, biases

# 実際のNetworkの演算部分
def forward(self, data):
conv1 = tf.nn.conv2d(data, self.conv1_weights, [1, self.conv1_stride, self.conv1_stride, 1], padding='VALID')
conv1 = tf.nn.relu(conv1 + self.conv1_biases)
conv2 = tf.nn.conv2d(conv1, self.conv2_weights, [1, self.conv2_stride, self.conv2_stride, 1], padding='VALID')
conv2 = tf.nn.relu(conv2 + self.conv2_biases)
conv3 = tf.nn.conv2d(conv2, self.conv3_weights, [1, self.conv3_stride, self.conv3_stride, 1], padding='VALID')
conv3 = tf.nn.relu(conv3 + self.conv3_biases)
shape = conv3.get_shape().as_list()
reshape = tf.reshape(conv3, [shape[0], shape[1] * shape[2] * shape[3]])
inner1 = tf.nn.relu(tf.matmul(reshape, self.inner1_weights) + self.inner1_biases)
inner2 = tf.matmul(inner1, self.inner2_weights) + self.inner2_biases
return inner2


Replay MemoryとTarget Network

DQNは非常に学習が不安定で、得られた報酬からすぐにフィードバックをかけて学習させると、うまく学習が進みません。また、Q学習の下記更新式の$Q(s_{t+1},a)$部分は、ネットワークの出力値を利用する必要があるのですが、重みの更新が頻繁に走っているネットワークの出力は不安定でそのままではうまく行きません。

{\begin{align}

Q(s_t,a_t) &\leftarrow Q(s_t,a_t) + \alpha(r + \gamma \max_a Q(s_{t+1},a) - Q(s_t, a_t))
\end{align}}

そこで、DQNでは、報酬をすぐネットワークにフィードバックするのではなく、状態$s_t$で行動$a_t$をしたところ、状態$s_{t+1}$に変化し、いくつ報酬$r$を得たかを覚えておいて、この記憶した状態からランダムにサンプリングして、学習用のデータとして利用します。この記憶をReplayMemoryと呼びます。

また、$Q(s_{t+1},a)$は、重みの更新が走っているネットワークのコピーを持っておき、そこから算出します。このコピーをTarget Networkと呼びます。もちろんネットワークを毎Iteration起きにコピーするのでは意味がないので、一定の間隔(10000 Iterationおき)で、コピーを走らせます。

TensorFlowでネットワークのコピーは下記のような感じで実装できます。

  def weights_and_biases(self):

return [self.conv1_weights, self.conv1_biases,
self.conv2_weights, self.conv2_biases,
self.conv3_weights, self.conv3_biases,
self.inner1_weights, self.inner1_biases,
self.inner2_weights, self.inner2_biases]

def copy_network_to(self, target, session):
copy_operations = [target.assign(origin)
for origin, target in zip(self.weights_and_biases(), target.weights_and_biases())]
session.run(copy_operations)


Optimizer

上でも書きましたが、RMSPropを使って学習する場合、オリジナルの論文にある学習率0.00025では、全く学習がすすみません。そこで、0.000025とひと桁小さくして、さらにクリッピングはやめて学習させます。(他にも学習がうまく行く設定があるかもしれませんが、調べてません)

optimizer = tf.train.RMSPropOptimizer(learning_rate=0.000025).minimize(loss)


結果

最後に、上記の実装で得られた結果を載せておきます。

遊ぶゲーム:Breakout(ブロック崩し)

ReplayMemory数:50000(持っているマシンのメモリ制約による。オリジナルはこれの20倍)

それ以外は、Nature論文のパラメータのまま

これを動かすと、だいたい30万Iterationくらいで、学習の効果が見てわかるようになってきて、ブロック崩しの場合だと、10点から20点くらい取れるようになります。

さらに学習を続ける(200万Iterationくらい)と、40点位はとれるようになります。(オリジナルの論文の性能には届いていません)

こんな感じ(40万Iteration付近の動画):


DQN with tensorflow pic.twitter.com/nAOEXuEWqV

— EpsilonCode (@epsilon_code) 2016年7月24日


DQN(完全版)は?

別記事で後日載せる予定です→載せました


参考文献

DQNのNature論文

Chainerでの実装