とある京大生の作業ログと日々の雑記

コンピュータサイエンスについて学んだことを可視化したり日々の雑記をまとめてます。

試験前でバタバタしてるけどPredictronの解説をしてみた話

こんばんは!コミさん(@komi3__ )です!



みなさんいかがお過ごしでしょうか?



ぼくら大学生はまさに試験期間へ突入しようかという時期です.....



試験準備めんどくさいですね...



勉強自体は楽しいんですけど、なんかこう、テストの為にする勉強ってどうしてこんな退屈になるのか....




ちょっと前までは留学準備の書類でバタバタしてて、ようやく落ち着いたと思ったら今度は試験勉強.....




そんなこんなで割と最近は鬱です(笑)



しかもぼくの興味分野と授業内容ってあんまりかぶってなくて、だから余計にやる気が....w



来年からは履修する授業をゼロにして(京都大学理学部は必修が語学だけなのでそういうことが可能なんです)機械学習に全振りにしようかなって考えてるんで、これが最後の山場かなって感じでがんばろうと思います.....




これは完璧にぼくの私見というか性格の問題なんですけど、どんな分野であれ、どうも授業で習うとクッッッッッッッッッソつまんないんですよね。



自分でやるからこそ勉強は面白いわけで、なんか授業っていらなくねって思ったり...



まあそんなこと言ってると単位はもらえないわけで、仕方ないからいい子ちゃんの顔して授業行って出席カードに名前記入してるんですけど(笑)




まあそんなこんなブーブー文句言っててもどうしようもないので試験勉強がんばります。




あ、それでちょっと話題が変わるんですけど、この前留学先の履修登録をしました!




こんな感じになります↓

f:id:komi1230:20180121235640j:plain




普段所属してる学部が理学部で、あんまりソフトウェアとかネットワークとかの工学的なことって勉強したことなくて、だから今回留学ではこう言ったことを勉強してこようかなって思ってます!




割と最近ネットワークセキュリティとかも興味出てきて、ちょっとそこらへんも知識としてしっかり抑えようかなって感じでああったり。




ぼくなんでパソコンとかそういうの好きなのかな〜って前に考えたことあるんですけど、思い当たる節としては昔からパソコンとかハードをいじくりまわしてたこととか、あとはゲーム(メタルギア)の影響ですかね?



父親がたまに暇だからってゲームの改造とかしたりしてて、そういうのを見よう見まねでやってたりでパソコンいじりに親しみがあったんですよね。




あとメタルギアとかのおかげでコンピューターの可能性みたいなのはすごい感じたり(笑)




そんなこんなでやっぱりコンピューターの勉強はすごい面白いなぁって感じてたりしてます!




はぁ〜〜〜〜試験勉強クソ〜〜〜〜〜




はい。そんなこんなでPredictronの解説記事書きますね。


Predictronの解説


強化学習関係ではPredictronって結構有名なやつだと思ってたんですけど一般の認知度ってどんなもんなんでしょう?


とりあえず論文はこちら↓


[1612.08810] The Predictron: End-To-End Learning and Planning


汎化型強化学習としてはかなり有名どころかなぁって思ってたんですけど、2016年以降は汎化手法がたくさん出てきて、Predictronはその中の一つです。


簡単にググってみたらこんな記事もありました。



www.nikkan.co.jp



まあPredictronはこんな感じです。



とりあえずざざっとアーキテクチャと実装解説をしていきます。


アーキテクチャ

汎化性能ということでまあ手法としては色々あるんですけど、Predictronでは現在状態からkステップ後までの状態価値と報酬を推定して多段階報酬を先読みによって規定することで汎化性能を獲得するというものです。


段階としては

  • 現在(時刻t)状態s_tをCNNに入力して、時刻t+1での報酬r_{t+1}と割引率{\gamma _{t+1}}を出す
  • これを繰り返し、最後に時刻t+kにおける状態価値を出す
  • それらを掛けたり足したりしたものg^kを求める
  • 得られたg^kに対して重み付けを行ったg^{\lambda}を定義

ざっとフローとしてはこんな感じです。


定式化すると以下の通り。


{
g^k = r_1 + \gamma _1 ( r_2 + \gamma _2 (r_3 + \dots \gamma _{k-1} (r_k + \gamma ^k V^k  )  \dots )) \\

g^{\lambda} = \sum^K_{i=0} w_i g^i \\

w_i = (I - \lambda _i) \lambda _0 \lambda _1 \cdots \lambda _{i-1} \\
}


イメージとしては以下の図の通り。


f:id:komi1230:20180122003712p:plain


これによって、報酬とか状態価値の先読みが可能になっているという寸法です。


各フローを迷路問題に適用した際の可視化したものが以下の通りです。


f:id:komi1230:20180122003915p:plain


各ステップでの状態価値が高い場所(とるべき経路, g^k)が明確化されていますね。


また、それらの重み付けg^{\lambda}によって迷路でのとるべきルートが明らかになってます。


これによって汎化が成功するらしい。


すごいなぁ....


ということで実装へいってみましょう。

実装

めんどくさいので環境情報の方は省かせていただきます。


コアとなるネットの部分をパパッと書きますね。


論文の実装って(論文次第なんですけど)比較的簡単で、式に書いてあることをそのまま再現するとだいたいなんとかなります。

import torch
import torch.nn as  nn
import torch.autograd.Variable as Variable
import torch.autograd.functional as F


class PredictronCore(nn.Module):

    def __init__(self, n_tasks, n_channels):
        super(PredictronCore, self).__init__()
        state2hidden=nn.Sequence(
                nn.Conv2d(n_channels, n_channels, ksize=3, pad=1),
                nn.BatchNorm2d(n_channels),
                nn.relu(),
            ),
        hidden2nextstate=Sequence(
                nn.Conv2d(n_channels, n_channels, ksize=3, pad=1),
                nn.BatchNorm2d(n_channels),
                nn.relu(),
                nn.Conv2d(n_channels, n_channels, ksize=3, pad=1),
                nn.BatchNorm2d(n_channels),
                nn.relu(),
            ),
        hidden2reward=Sequence(
                nn.Linear(None, n_channels),
                nn.BatchNorm2d(n_channels),
                nn.relu(),
                nn.Linear(n_channels, n_tasks),
            ),
        hidden2gamma=Sequence(
                nn.Linear(None, n_channels),
                nn.BatchNorm2d(n_channels),
                nn.relu(),
                nn.Linear(n_channels, n_tasks),
                nn.sigmoid(),
            ),
        hidden2lambda=Sequence(
                nn.Linear(None, n_channels),
                nn.BatchNorm2d(n_channels),
                nn.relu(),
                nn.Linear(n_channels, n_tasks),
                nn.sigmoid(),
            ),
        )

    def forward(self, x,):
        hidden = self.state2hidden(x)
        # No skip
        nextstate = self.hidden2nextstate(hidden)
        reward = self.hidden2reward(hidden)
        gamma = self.hidden2gamma(hidden)
        # lambda doesn't backprop errors to states
        lmbda = self.hidden2lambda(
            chainer.Variable(hidden.data))
        return nextstate, reward, gamma, lmbda


class Predictron(nn.Module):

    def __init__(self, n_tasks, n_channels, model_steps,
                 use_reward_gamma=True, use_lambda=True, usage_weighting=True):
        self.model_steps = model_steps
        self.use_reward_gamma = use_reward_gamma
        self.use_lambda = use_lambda
        self.usage_weighting = usage_weighting
        super(Predictron).__init__()
        obs2state=nn.Sequence(
                nn.Conv2d(None, n_channels, ksize=3, pad=1),
                nn.BatchNorm2d(n_channels),
                nn.relu(),
                nn.Conv2d(n_channels, n_channels, ksize=3, pad=1),
                nn.BatchNorm2d(n_channels),
                nn.relu(),
            ),
        core=PredictronCore(n_tasks=n_tasks, n_channels=n_channels),
        state2value=nn.Sequence(
                nn.Linear(None, n_channels),
                nn.BatchNormalization(n_channels),
                nn.relu(),
                nn.Linear(n_channels, n_tasks),
            ),
        )

    def forward(self, x):
        # Compute g^k and lambda^k for k=0,...,K
        g_k = []
        lambda_k = []
        state = self.obs2state(x)
        g_k.append(self.state2value(state))  # g^0 = v^0
        reward_sum = 0
        gamma_prod = 1
        for k in range(self.model_steps):
            state, reward, gamma, lmbda = self.core(state)
            if not self.use_reward_gamma:
                reward = 0
                gamma = 1
            if not self.use_lambda:
                lmbda = 1
            lambda_k.append(lmbda)  # lambda^k
            v = self.state2value(state)
            reward_sum += gamma_prod * reward
            gamma_prod *= gamma
            g_k.append(reward_sum + gamma_prod * v)  # g^{k+1}
        lambda_k.append(0)  # lambda^K = 0
        # Compute g^lambda
        lambda_prod = 1
        g_lambda = 0
        w_k = []
        for k in range(self.model_steps + 1):
            w = (1 - lambda_k[k]) * lambda_prod
            w_k.append(w)
            lambda_prod *= lambda_k[k]
            # g^lambda doesn't backprop errors to g^k
            g_lambda += w *  Variable(g_k[k].data)
        return g_k, g_lambda, w_k


まあざっとこんな感じでしょうか?


これで論文中の提案モデルは実装はできると思います。


途中のuse_lambdaみたいなのは、論文中のモデルはg^kとかg^{\lambda}とかで成り立ってたわけじゃないですか。


これを\lambda = 1とすることでただの1ステップ学習にしたりみたいな?感じになるらしいのです。



ぼく自身が完璧に調べたわけじゃないんですけど、どうもこのPredictronってのはTD(\lambda)法をベースとしていて、詳しくはTD(\lambda)を参照していただければと思います。



学習に際してのBack propagationは以下の通りの関数で用意してあげます。


def loss(self, x, t):
        g_k, g_lambda, w_k = self.forward(x)
        if self.usage_weighting:
            g_k_loss = sum(F.sum(w * (g - t) ** 2) / x.shape[0]
                           for g, w in zip(g_k, w_k))
        else:
            g_k_loss = sum(F.mean_squared_error(g, t) for g in g_k) / len(g_k)
        g_lambda_loss = F.mean_squared_error(g_lambda, t)
        return g_k_loss, g_lambda_loss

この入力のtは教師データで、現実の報酬とか状態価値に当たります。


これで学習は進みます。

まとめ


ものすごいテキトーに実装を載せました。



とりあえずぼくの備忘録的なところもあるので、そこまで完璧な解説というわけではないですがご容赦ください。



それにしてもこんな簡単なアーキテクチャで汎化できるってAIってすごいなぁって思ったり。



今はいろんな汎化手法が溢れてるのでどんどん実装していこうと思います。



お疲れ様でした!