Help us understand the problem. What is going on with this article?

いまさらだけど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での実装

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away