『ゼロから作るDeep Learning 3』のコードを全て読んでみた(2)

今話題のこの本を読もうと思ったのだが、アマゾンで在庫切れになっていた。

そこで、GitHubに公開されているこの本のソースコードだけを読んでいるのだが、自分自身の学習メモを何回かに分けて記載する。ライブラリを作っていくプロセスが60個の.pyファイルに分けられているので、いきなりライブラリ本体を読むのではなく、まずはこの60個の.pyファイルから読んでいく。

今回は第2回で、step07.pyからstep10.pyまでである。

第1回はこちら。

step07.py:バックプロパゲーションの自動化

step06.pyではバックプロパゲーションの処理をプログラマ自身が指定してあげる必要があった。つまり出力層側からさかのぼって順番に.backward()を呼び出していく、というコードを自分の手で書く必要があった。これは面倒くさ過ぎるため、普通はそんなことはしない。step07.pyではそれをプログラム内部で自動的に回せるように変更する。

コードは以下。

https://github.com/oreilly-japan/deep-learning-from-scratch-3/blob/master/steps/step07.py

Variableクラスにメンバ変数creatorと、creatorのsetterであるset_creator()が追加されている。creatorは関数を表し、Variable、つまり自分自身を出力した関数を覚えておくためのメンバである。関数はインプットのVariableを受け取ってアウトプットのVariableを出力する。このアウトプットのVariableを出力した関数がこのcreatorに格納される。そして関数を表すFunctionクラスにはinputというメンバがあり、これがインプットのVariableを覚えている。すると、以下のように出力層から入力層に向かって逆伝播させていくことができる。
・Variableが持つcreatorを見れば自分自身を出力した関数がわかる
・その関数が持つinputを見ればその関数にインプットされた変数がわかる
・inputはVariable型なので、それが持つcreatorを見れば自分自身を出力した関数がわかる
・その関数が持つinputを見ればその関数にインプットされた変数がわかる
・以下その繰り返し

この再帰処理を回しているのが、Variableクラスのbackward()である。
・f = self.creator で自分自身を出力した関数を取ってくる
・x = f.input でその関数のインプットのVariableを取ってくる
・x.grad = f.backward() で微分を計算する
・x.backward() で1つ手前の層のVariableで同じことをする
つまりVariableのbackward()の中で、1つ手前のレイヤーのVariableのbackward()を再帰的に呼び出している。

一番下にあるサンプルコードを見ると、step06.pyとの違いがよくわかる。step06.pyでは関数C、関数B、関数Aのbackward()を順番に呼び出すのでbackward()を3回書いていたが、今回はy.backward()ひとつで済んでいる。なぜなら、その中でb.backward()が呼び出され、さらにその中でa.backward()が呼び出されているからである。

step08.py:再帰からループへ

前回はバックプロパゲーションを関数の再帰呼び出しで実現していたが、それをループ処理で置き換える。理由は、再帰呼び出しだとスタックに情報を積むことを繰り返すので、メモリ効率が悪いからだと思われる。

コードは以下。

https://github.com/oreilly-japan/deep-learning-from-scratch-3/blob/master/steps/step08.py

Variableクラスのbackward()を見ると、関数のリストであるfuncsというものが追加され、それがNoneではない間、関数のbackward()を呼び出し続けている。
・自分自身を出力した関数を取ってきてfとする
・fのインプットとアウトプットを取ってきてx, yとする
・f.backward()の引数と戻り値にxとyを使う
・インプットであるxが関数から出力されたものであれば、その関数をfuncsにセットする
・その関数を取り出してfとする
・fのインプットとアウトプットを取ってきてx, yとする
・以下同様のことを繰り返す。
・インプットであるxが関数から出力されたものではない、つまり入力層に到達したら、そこで逆伝播を終了する

一番下にあるサンプルコードは前回と同じである。

step09.py:関数をより便利に

コードは以下。

https://github.com/oreilly-japan/deep-learning-from-scratch-3/blob/master/steps/step09.py

Variableクラスのコンストラクタにエラー処理が追加されている。これはインプットとしてndarray型しか受け入れないようにするためである。スカラーでもnumpyのndarray型でインプットすることを要求している。また、as_array()という関数が追加されている。Functionクラスでは、インプットがスカラーであればnp.array()で変換してから、Variableクラスのコンストラクタに渡すようになっている。

コードを簡潔に書くためのヘルパー関数square(), exp()が追加されている。これらは例えばSquare()(x)をsquare(x)と書き換えるためのものである。これにより関数をつなげて適用するコードが、例えば
square(exp(square(x)))
と簡潔に書ける。

step10.py:テストを行う

コードは以下。

https://github.com/oreilly-japan/deep-learning-from-scratch-3/blob/master/steps/step10.py

テストコードが追加されている。一番下に、unittest.TestCaseを継承したSquareTestクラスがあり、3つのテスト関数が実装されている。

test_forward()では、square()関数で2を二乗すると4になることを確認している。

test_backward()では、解析的な微分と比較することで、Squareクラスのbackward()関数をテストする。x^2を微分すると2xなので、xに3を与えると6が返ってくることを確認している。

test_gradient_checkでは、数値微分と比較することで、Squareクラスのbackward()関数をテストする。ランダムにインプットを与えて、backward()とnumerical_diff()の結果が近くなることを確認している。

これで、解析的な微分、数値微分、バックプロパゲーション、という3種類の微分を比較して確認したことになる。

次回はstep11.pyからスタートする。