詳解ディープラーニングの5章の予習
勉強会の題材、詳解ディープラーニング
の5章を予習しておく。 RNNとかLSTMとかGRUとか。
5.1 RNN
まずは素のRNNの話を見ていく。 自分はこれまで、簡単に数式くらいは見た事あるが、ちゃんと計算追ってしっかりコード見るのは今回が初。
いい機会なので今回はちゃんと理解したい。
隠れ層周辺の基礎的な計算(5.1.2〜)
手で計算しないと良く分からないので計算してみる。
これはまぁいい。 とりあえずお尻から、という事でcとVから見てみるか。
よし、思い出してきたぞ。次はfの方という事で一番簡単なbから。
行列の足は割とちゃんとやらないと分からなくなるんだが、まぁこの位なら答えもあるしいいだろう。
という事で、あとトレーニング対象はUとWか。
思い出してきたのでバックプロパゲーションの形に整理して書く。 この手のは最初から整理されると良く分からんので、まずは全部書き下す所から毎回やるのが、結局は早い。
さて、これで5.1.3のBPTTの説明を読む準備は出来た。
5.1.3 BPTTの説明
読んだが何言ってるのかいまいち分からん。
ダイナミックベイズとかで2期間モデルを展開するのは結構やったから図5.4とかは理解出来るはずだが、説明が良く分からない。
まぁ説明はおいといて、t-1期の誤差を計算すれば良いらしいので、そのくらい自分で考えても分かるだろう、という事で考えてみる。
まずは最初の式をもう一度眺める。
h(t-1)の誤差を考えたいのだから、h(t-1)も展開しておこう。
UとWは同じ物を使うのがミソなんだよな、たぶん。 で、UとWの一期前の寄与を考えればいいはずか。
とりあえずUでの微分を考えるか。
こんな感じか。これと5.24を比べたいが、これはhの誤差があらわに書いてなくて、5.22を入れないといけないのかぁ。
最初にこんなの見ても分からんので仕方ない。自分で納得する目的で少し式を変形しよう。
5.24のUの更新式のシグマで、z=1の寄与を書き下してみる。
これをさっきの自分の計算結果と並べて眺めてみよう。
だいたい一致してそうだね。
さて、だいたい計算は理解したので、本書の式を解釈してみよう。
まず、\(e_h (t)\) は、hの中による誤差の微分だ。中はpで置いているので、それを踏まえて5.20からの式を眺める。
ふむ、\(e_h (t-1)\)はp(t-1)による誤差の微分で、これはチェインルールで5.20のように書けるな。
5.21はさらにp(t-1)の依存が唯一あるh(t-1)をチェインルールで挟む事で計算を行う。 5.21のカッコの中の左側はWで、右側はまさにt-1期のf’に他ならない。
計算は追えたが、どうしてこの形にしたかったのかの、その心をもう少し考えてみよう。 まず、training対象となる変数(例えばWとか)は、いつもActivation functionの中にある。 このActivation関数の引数自体をpとおけば、誤差のトレーニング対象の変数による微分(つまりWによる微分)は、チェインルールによりpによる微分とpのWによる微分に分ける事が出来る。
こう置く必然性は良く分からないな。ニューラルネットでも復習するか、と三章を見たがさっぱり。
でも自分はかつてPRMLでこの辺は完璧にやったのだった、ということで、PRMLを見直そう。
すると、PRMLのp242の5.48で同じ物をaと置いている。
ふむ、これはニューラルネットワークを、基底を自動で選ぶもの、と捉えてここまで説明してきたのだから自然だな。
しかもそこを基準に一段戻す事でPRML 5.56という局所的な関係が得られる、という話だったな、そういえば。
これがバックプロパゲーションの本質と言える。
やってて良かったPRML。
さて、以上のPRMLを見直して理解した事を元に本書の記述の意味を考えよう。
ネットワークの図式のあるノードの前後の誤差について考える。
バックプロパゲーションとは、各ノードのforwardの値が分かっている時に、各ノードの誤差を一番右から始めて、いつも一つ右の誤差と一つ左のforwardの値から求めていく手続きだった。
この各ノードの誤差を本文では \(e_h\) と置いているんだな。 なるほど。
各ノードの誤差とは、そのインプットによる微分だったな。だからインプットをpと置くのか。
つまりこういう事か。
バックプロパゲーションの時の各ノードの誤差をeほにゃららで置いていて、そうなるようにpとかqを決めているのか。
うーむ、これはp213の説明では自分には分からんなぁ。
「何故5.6とか5.7のように置くのか」に全然説明が無いのはなんでなのかなぁ。
こんなのニューラルネットなら当然、という態度で行く気なら、三章とか中途半端な章は要らないと思うのだが。
よその本で勉強してきて下さい、と言って、参考文献あげるくらいの方がまだ親切に思う。
BasicRNNCellってなんじゃらほい?
さて、式は理解した、という事でコードを読んでいく。長々と単なるnumpyのreshapeの解説が続き、「見れば分かるがな〜」とか思いつつ読み進める。
そして肝心のRNNの実装の所である5.1.4.2にさしかかってコードを見ると、BasicRNNCellという物が出て来る。
肝心な所はこれっぽいが、何故かこれの解説が全然無い。 なんだこれ?
まずは公式ドキュメントを読む
とりあえず公式のドキュメントを見ると、こちら。
https://www.tensorflow.org/api_docs/python/tf/contrib/rnn/BasicRNNCell
このドキュメントもなかなか酷いが、それでも肝心のcallの所を見ると、少し解説がある。
call( inputs, state )
Most basic RNN: output = new_state = act(W * input + U * state + B).
この書き方も酷いが、でもちゃんと重要な所は書いてあるな。 このcallableの第一引数がinput、第二引数がstateで、下の一行解説の計算をするのであろう。
このcallableの周辺は以前別件でLayer周辺のコードを読んだ事があるので自分は知ってるから、この説明でだいたいは分かる。
お次はソースコードを読む
さて、Tensorflowを理解していれば、RNNのセルはtrainableな値はWとbだけなので、こいつをtf.Variable として定義している事は予想がつく。
で、Recurrentといっても同じW とb を使ったmatmulを何度も実行してやれば、computation graph的には目的の物が出来そう。
という事でこれだけで何をやってるかはだいたい想像がつくが、ちょっと解せないのはわざわざそんなクラスが必要な理由だ。 一応その辺はコードを軽く確認しておこう。
https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/ops/rnn_cell_impl.py#L343
ふむ、単純にmatmulして足してactivation呼んでるだけだな。
_kernelも見ておこう。
(これは上のリンクの数行上のbuildのあたり。最初引用する気だったけど面倒なので各自で見て下さい)
ちゃんとadd_variableを追わないと正確な事は言えないが、意味的にはtf.Variableを作ってるだけと言って良いだろう(本当はスコープ回りの処理とか、trainableな時にはこのベースクラスのメンバのリストに保持したりとかの処理はある)。
ここまでを読むと、なんかわざわざクラスを作る必要は無さそうな内容だな。
ただ、ちょっと本書の記述で納得しがたいのが、p221ではoutputsをprintすると別々のbasic_rnn_cell _XX という変数がついていて、それをreuseするとかなんとか言っている。
普通に考えればtf.get_variable はbuild の時しか呼ばれてないので、カーネルは一つしか作られず、reuseの必要は無い気がするし、そもそもこうやって25個の出力が出ているのもおかしい気がする。
このoutsに入ってるのはmatmulのオペレータであってカーネルじゃないんじゃないの?という気もするが、それならreuseうんたらは、なんで必要なのか? というか本当にそれを指定すると結果変わるの?
メモ:
https://r2rt.com/recurrent-neural-networks-in-tensorflow-i.html
static_rnnではreuse_variables()している。
5.2 LSTM
次はLSTM。 5.41がどこから出てくるか分からないなぁ。
しばらく考えてみたが、まだここまでではfとは何か、という定義が終わってないので、この式は出てきようが無い気がする。
と思って読み進めると、次の5.42でfの定義が終わるんじゃないか? これを元に5.41は確かに簡単に出る。 うーむ、これ順番おかしいよなぁ。
さて、本の記述の話はおいといて、式の意味を考えよう。 5.36から見る。
fから活性化された信号をaで表している、という話だった。 そしてiはインプットのゲートとの事。
インプットのゲートが1の時はaの値と一期前のcの値を足した物が今期のcの値となる。
hは図5.8に一応示してあるが、いまいち何を指すのかは具体的には分からない。 RNNのような物、という事なのだろうが。
とにかく、hはこのcに出力ゲートの値を掛けたものとなる。 出力ゲートが1の時はhは1となり、出力ゲートが0の時はcの値は保持されるだけでhにはでてこない。
とりあえずこんなもんでコード見るか。
うが、コード見たらLSTMCell使えばいいです、で終わってる。なんじゃそりゃ〜〜!
仕方ないのでTensorflowのソースを読む。 手元のZipSourceCodeReadingで読んでるが、ブログ向けに一応外のリンクを貼っておこう。 https://github.com/tensorflow/tensorflow/blob/master/tensorflow/python/ops/rnn_cell_impl.py#L543
ふむ、stateとしてはhとcがあるのか。 そしてcは本文で解説されてる通りの式で更新される。
古いhは何に使われるのか?というと、inputとconcatされてkernelとmatmulされた物がgate_inputとして使われる。 kernelのshapeは?と見ると、rowがinputとhidden_unitをconcatした長さ、colは4だな(ユニットの数次第だが)。
つまり、input_gate、forget_gate、output_gate、そして通常の入力の4つのmatmulを一気にやってるんだな。
578行目が5.42式に相当しているな。
hとはなんぞや?というと、recurrentと同様のhidden unitだが、LSTMはcも似たような前期の信号を保持するという役割を持っている。
hはセルの出力か。そしてそれが次回のinputの一部としても入ってくる。
ふむ。だいたい分かってきたな。
RNNセルとは何か?
RNNのセルのインターフェースとしては
入力としては
- 今期の入力
- 前期のstate
を受け取り、いろいろ計算し、出力としては
- 今期の出力
- 次期に持ち越す為のstate
という物なのか。
で、LSTMは出力としてはhで、stateとしてはhとcのタプルだ。
で、これらにWを掛けて足し引きして次のhとcを求める。
この時に、各ゲートは別々のWを用意してやり、ゲートはsigmoidなどゲートっぽく振る舞うactivation関数を食わせて、本文にある解説のようにつなげてやる。
すると、学習とてしは単純にmatmulとかがいろいろくっついただけの、少し変なつながり方をしているニューラルネットに過ぎないので、普通のcompute_gradとapply_gradの仕組みで最適化される。
この時に本当に各Wが我らの狙ったゲートっぽく振る舞ってるかは別段コードで保証してる訳じゃないが、良きにはからったゲートになるようにWを学習するであろう、という事だな。
お、全てを理解した。