DQN-Chainerを動かした&ちょっとだけ中身見た

ラボのサーバーでDQN-Chainerを動かしました.

CUIだと動かないかもと思ったけど動いてます.

途中失敗したっぽかったけどなんとか動いてる.

 

DQN-chainerの記事はこちら.

qiita.com

基本参考にしたインストール手順

hirotaka-hachiya.hatenablog.com

 

後輩が「CUIだと動かないっぽいっすよ」と言っていたので恐る恐るやってたらちゃんと動きました.動いてる様子はこちら.

youtu.be

きもいけどなんとか動いてる.

全然学習できてない段階で動かしてるので右側のDQNちゃんが弱すぎですね.

lab_server@user:~/RL/Arcade-Learning-Environment-0.5.1$ make -j 4 Linking C executable doc/examples/RLGlueAgent Linking C executable doc/examples/RLGlueExperiment
/usr/bin/ld: cannot find -lrlexperiment collect2: error: ld returned 1 exit status /usr/bin/ld: cannot find -lrlagent collect2: error: ld returned 1 exit status make[2]: ***
[doc/examples/RLGlueExperiment] Error 1 make[1]: *** [CMakeFiles/RLGlueExperiment.dir/all] Error 2 make[1]: *** Waiting for unfinished jobs.... make[2]: *** 
[doc/examples/RLGlueAgent] Error 1 make[1]: *** [CMakeFiles/RLGlueAgent.dir/all] Error 2 [ 33%] Built target ale-bin [ 66%] Built target ale-c-lib make: *** [all] Error 2


 

こんなエラーが出て,ALEのインストールの66%の段階で失敗したからもうダメかと思ったけど,ディレクトリにaleっていう実行ファイルがあったからダメ元で動かしたら動きました.

僕の環境だとだいたい15分でEpisode20まで動きました.先駆者によると,Episode700くらいでHandMadeAgentより強くなります.現実的な範囲ですね.
先駆者の動画はこちら
www.youtube.com

簡単に中身を見たのでforwardのあたりだけ解説します.間違ってたらコメントくれるとありがたいです.

まず,DQN-Chainer/dqn_agent_nature.pyから.

ここがネットワーク部分

        self.model = FunctionSet(
            l1=F.Convolution2D(4, 32, ksize=8, stride=4, nobias=False, wscale=np.sqrt(2)),
            l2=F.Convolution2D(32, 64, ksize=4, stride=2, nobias=False, wscale=np.sqrt(2)),
            l3=F.Convolution2D(64, 64, ksize=3, stride=1, nobias=False, wscale=np.sqrt(2)),
            l4=F.Linear(3136, 512, wscale=np.sqrt(2)),
            q_value=F.Linear(512, self.num_of_actions,
                             initialW=np.zeros((self.num_of_actions, 512),
                                               dtype=np.float32))

覚え書きとしてl4の入力が何故3136になるのか書いておきます.
はじめ,AtariのPongというゲームは210*160の画像サイズで入力されますが,諸事情によりそれを84*84にします.
84*84の画像は,l1の畳み込み層によって20*20の画像になります.しかしそんなことはどこにも書いてありません.自分で計算します.
出力画像の幅は,こういう計算式で導き出されます.


out = floor( \frac{( lsize - ksize ) + Psize \times 2 }{stride }) + 1
つまり今回の場合,

 l1out = floor( \frac{( 84 - 8 ) + 0 \times 2 }{4}) + 1 = 20
 l2out = floor( \frac{( 20 - 4 ) + 0 \times 2 }{2}) + 1 = 9
 l3out = floor( \frac{( 9 - 3 ) + 0 \times 2 }{1}) + 1 = 7
 l4in = 7 \times 7 \times 64 = 3136
となります.
ネットワーク構造的には3つの畳み込み層の後,全結合層があり,最後にQ値を計算する全結合層が一つあるという感じです.
DQNは,強化学習であるところのQ学習におけるQ値の更新を,
本来なら価値関数近似を用いて行う部分にニューラルネットワークを用いていると,
なにやらかっこいいことを言っていますが,要するにQ値をCNNで決めてるってだけの話ですね.構造はとっても単純です.
l1のstrideが4なのはおそらくプレイヤーの横幅かボールのどちらかが4ピクセルだからじゃないでしょうか(適当).
strideの設定とか,ksizeの設定とかはなぜこうなっているのかわからないです.理論にもとづいているのか,いろいろ試した結果なのか.

ここがOptimizerの設定です.

        print "Initizlizing Optimizer"
        self.optimizer = optimizers.RMSpropGraves(lr=0.00025, alpha=0.95, momentum=0.95, eps=0.0001)
        self.optimizer.setup(self.model.collect_parameters())

RMSpropGravesを使っています.CNNの学習で,最高の性能を示すと言われているやつです.
lr, alpha, momentum, epsの値は,このPongというタスク(ゲーム)に最適化された値だと見ていいと思います.

ここが,実際にネットワークを使って学習する部分です.

    def forward(self, state, action, Reward, state_dash, episode_end):
        num_of_batch = state.shape[0]
        s = Variable(state)
        s_dash = Variable(state_dash)

        Q = self.Q_func(s)  # Get Q-value

ちょっと古いchainerのバージョンで開発されたっぽい書き方ですが,新しめのchainerでも動きます.(僕の環境だと1.7.0)
state(ゲームの状態)を受け取り,chainerで扱えるようにVariable型にキャストします.
state_dashは次の状態で,これも誰かから受け取っています.
Q_fanc(s)でQ値を計算します.
以下,Q_fancメソッド.

    def Q_func(self, state):
        h1 = F.relu(self.model.l1(state / 255.0))  # scale inputs in [0.0 1.0]
        h2 = F.relu(self.model.l2(h1))
        h3 = F.relu(self.model.l3(h2))
        h4 = F.relu(self.model.l4(h3))
        Q = self.model.q_value(h4)
        return Q

もちろんstateは画像ですので,配列内のすべての値を255で割ってます.

forwardに戻ります.

        # Generate Target Signals
        tmp = self.Q_func_target(s_dash)  # Q(s',*)
        tmp = list(map(np.max, tmp.data.get()))  # max_a Q(s',a)
        max_Q_dash = np.asanyarray(tmp, dtype=np.float32)
        target = np.asanyarray(Q.data.get(), dtype=np.float32)

ここで次状態のQ値の最大値(つまりは期待値)を取り出してますね.
このtargetというのを教師データにするようです.状態をいい方向へ持っていくという目的に沿っていると考えられます.
対戦ゲームを極めた人ならわかると思いますが,プレイヤーは有利状況を作り出し,不利状況をなるべく避けるものです.
ちなみにQ_func_targetはこの部分

    def Q_func_target(self, state):
        h1 = F.relu(self.model_target.l1(state / 255.0))  # scale inputs in [0.0 1.0]
        h2 = F.relu(self.model_target.l2(h1))
        h3 = F.relu(self.model_target.l3(h2))
        h4 = F.relu(self.model_target.l4(h3))
        Q = self.model_target.q_value(h4)
        return Q

です.Q_fancと同じです.
model_target.l1~model_target.q_valueは,

    def target_model_update(self):
        self.model_target = copy.deepcopy(self.model)

と,self.modelをまるごとコピーしたもののようです.

        for i in xrange(num_of_batch):
            if not episode_end[i][0]:
                tmp_ = np.sign(Reward[i]) + self.gamma * max_Q_dash[i]
            else:
                tmp_ = np.sign(Reward[i])

            action_index = self.action_to_index(action[i])
            target[i, action_index] = tmp_

ここで行動とそれに対する報酬(または報酬の期待値)をtargetに入れてるようです.
np.sign(x)は御存知の通り,x<0のとき-1, x=0のとき0, x>0のとき1を返します.
報酬を0~1までにクリッピングしているようです.
それだと報酬の重み付けができないじゃないか! とも思うんですが,これがデフォのようですね.
こっちのほうが学習が進みやすいそうなので,まあ良しとしましょう.
DeepLearningで学習が全然進まないのは悪夢なので,とりあえずこっち優先というのも頷けます.

        # TD-error clipping
        td = Variable(cuda.to_gpu(target)) - Q  # TD error
        td_tmp = td.data + 1000.0 * (abs(td.data) <= 1)  # Avoid zero division
        td_clip = td * (abs(td.data) <= 1) + td/abs(td_tmp) * (abs(td.data) > 1)

        zero_val = Variable(cuda.to_gpu(np.zeros((self.replay_size, self.num_of_actions), dtype=np.float32)))
        loss = F.mean_squared_error(td_clip, zero_val)
        return loss, Q

ここが1番むずいところかもしれません.
ここにTD誤差について詳しく書いてあります.
qiita.com
ぶっちゃけよくわかりません.まあここは改造することもなさそうなので一旦無視します.
2016/09/26 こっち読むと分かりやすかったです
TD誤差学習


forwardだけで結構長くなってしまいましたね.
ここからオリジナルの何かを作るときは,RL_glueの使い方をしっかり勉強する必要がありそうです.
次回はdqn_agentクラスを見つつ,RL_glueの使い方を書いていくことにしましょう.