勉強会の題材、詳解ディープラーニング

の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のセルのインターフェースとしては

入力としては

  1. 今期の入力
  2. 前期のstate

を受け取り、いろいろ計算し、出力としては

  1. 今期の出力
  2. 次期に持ち越す為のstate

という物なのか。

で、LSTMは出力としてはhで、stateとしてはhとcのタプルだ。

で、これらにWを掛けて足し引きして次のhとcを求める。

この時に、各ゲートは別々のWを用意してやり、ゲートはsigmoidなどゲートっぽく振る舞うactivation関数を食わせて、本文にある解説のようにつなげてやる。

すると、学習とてしは単純にmatmulとかがいろいろくっついただけの、少し変なつながり方をしているニューラルネットに過ぎないので、普通のcompute_gradとapply_gradの仕組みで最適化される。

この時に本当に各Wが我らの狙ったゲートっぽく振る舞ってるかは別段コードで保証してる訳じゃないが、良きにはからったゲートになるようにWを学習するであろう、という事だな。

お、全てを理解した。