cha_kabuのNotebooks

個人的な機械学習関連勉強のアウトプット置き場です。素人の勉強録なので、こちらに辿り着いた稀有な方、情報はあまり信じない方が身のためです。

ゼロから作るDeep Learning3 フレームワーク編を読む その②ステップ8~14

はじめに

こちらの記事の続きのシリーズです。

本編

ステップ8 再帰からループへ

ステップ7でbackwardを自動実行する際、再帰的な実装を行いましたがそれをループに修正します。つまずきは無いものの現段階では実装の必要性がわかりません。しかしステップ15で役立つとのこと。

ステップ9 関数をより便利に

以下の4点を改善します。

  1. 今のままでは関数を使うのにいちいちインスタンス化→呼び出しと手間がかかってしまうので、インスタンス化&呼び出しを一括で行ってくれる関数を定義
  2.  yの勾配(絶対1)をいちいちy.gradに格納する必要がある手間を省くための実装
  3. Variableの引数dataにndarray以外が渡されたときに例外を発生させる
    • isinstance()raise TypeError()の活用
  4. 0次元のndarrayを計算した場合、結果がnumpy.float64などのndarray以外のスカラ系の型になってしまう問題の対処(対処しないと3の機能によりエラーが起きる)
    • isscalar()の活用

こちらもつまずきはありませんでしたが知らない実装が色々あったので忘れないようにしなければ…

ステップ10 テストを行う

知らない内容だらけですが難しくはなかったです。しかしこの機能を使いこなせる気がしない…Deep Learningについて学ぶだけが目的であればこのステップへの深入りは後回しでも良いかなと思います。

また、自分の環境(Jupyter Notebook)だと書籍の通りに実行するとテストモードを実行するコマンドunittest.main()でエラーになります。何がどうなってエラーが起きて解決できているのかはよく分かっていませんが、ネット上の知識人の情報からコマンドを以下に修正すればとりあえず動きます。

if __name__ == '__main__': unittest.main(argv=['first-arg-is-ignored'], exit=False)

ステップ11 可変長の引数(順伝播編)

入出力が複数になる場合の順伝播に対応します。これまでの関数は入出力ともに一つでそれだけを考慮した形でしたが、足し算や掛け算などの入力が複数になるケースや多次元配列を複数に分割する関数などの出力が複数になるパターンに対応させます。ここもまだ大丈夫そうです。

ステップ12 可変長の引数(改善編)

主に以下の2点を改善します。まだ大丈夫。

  1. Functionクラス(を継承した関数のクラス)から作ったインスタンスに与える引数が、現状だとリスト型にしないといけなくて面倒なので個別に渡せる様に修正
    add([x0, x1])としなくてはいけなかったところをadd(x0, x1)に対応させる感じ
  2. 1と同じようなことを関数のクラス自体を新しく作るときにもできるようにFunctionクラスを修正

ステップ13 可変長の引数(逆伝播)

説明されていることは分かるのですが、頭の中の関係図が追い付かなくなってきました…ダブる部分が多いので、次のステップ14とあわせてクラスどうしの関係を図式したいと思います。ここではつまずいた訳ではありませんが忘れがちな足し算と掛け算の逆伝播についてまとめておき知識を定着させたいと思います。

足し算の順伝播

上流から流れてきた勾配をそのまま二つの下流に流します。

f:id:cha_kabu:20201106222449p:plain

左側が順伝播、右側が逆伝播の図で、順伝播側は当然ですが x yを足したものなので、出力 z x+yとなります。

逆伝播側はまず基本のおさらいとして、あるノードから下流に流れる勾配は「上流から流れてきた勾配×自身の入力についての勾配」となるのでした。

図では、上流から流れてくる勾配は \dfrac{\partial L}{\partial z}ひとつだけで( zから先のグラフは省略しているので具体的な値はこの図からは分かりません)、これが xが入ってきた方向と yが入ってきた方向両方に作用します。

そしてそれぞれの方向に関する勾配はというと、 z=x+y xについて微分したものと yについて微分したもののなので、

 \dfrac{\partial z}{\partial x}=1

 \dfrac{\partial z}{\partial y}=1

とどちらも1になります。元の数に1を掛けても変わらないので、結局下流 xが入ってきた方向にも yが入ってきた方向にも流れる勾配は同じで、 \dfrac{\partial L}{\partial z}となります。

掛け算の順伝播

上流から流れてきた勾配に、他方の入力を掛けて下流に流します。順伝播の時の関係をひっくり返した関係になるので少しややこしいです。

f:id:cha_kabu:20201106222453p:plain

こちらも左側が順伝播、右側が逆伝播の図で、順伝播側は x yを掛け合わせたものなので、出力 z xyとなります。

逆伝播側で上流から流れてきた勾配は足し算のときと同じですが、それぞれの方向に関する勾配はというと、 z=xy xについて微分したものと yについて微分したもののなので、

 \dfrac{\partial z}{\partial x}=y

 \dfrac{\partial z}{\partial y}=x

とどちらも相手側の入力値になります。ということで図の通り、下流 xが入ってきた方向には上流からの勾配に yを掛けたもの、 yが入ってきた方向には上流からの勾配に xを掛けたものが流れることになります。

ステップ14 同じ変数を繰り返し使う

ここのステップの主題は同じ変数をつかって複数回関数に通すと勾配が誤った値になってしまうことへの対応と、その対応をすることにより新たに生じる同じ変数を使って複数回勾配を求めると誤った値になってしまうことへの対応です。ステップ13で保留していたものと合わせて、ステップ14時点での各クラスの関係を前回からの変更点に注目して図にまとめます。

※相変わらず表記の統一が取れていません…雰囲気で掴んでください。ステップ毎に表したい内容が変わるので探り探り「今回はこう書いたらまとまり良いかな」と都度考えを変えながらやっており、分かりづらくてすみません。

順伝播

f:id:cha_kabu:20201106222729p:plain

※書籍ではAddクラスのインスタンス化&引数渡しを一度で行うadd関数が実装されていますが、簡易化のため省略しています。

上図の通り、aとbをAddという二つを足し合わせる関数のクラスに通し、出力するまでの流れで確認します。

また、説明の前提としてaとbはVariableクラスを手動でインスタンス化したものとしています。手動で作成したものなので生みの親を保持するcreatorはどちらもNoneになっています。dataはどちらも1次元で要素を2つもつndarrayインスタンスです(スペースの関係上リストとの区別がでできなかったので、ndarrayはオレンジ、リストは黒で表現しています)。

AddはAddクラスをインスタンス化したものです。Addでは以下の順で処理が進みます。

  1. 複数の引数aとbを受け取とりそのdataを変数xsにリストで格納。
  2. xsを自身のforwardメソッド(2つの引数を足す関数)に通し、出力結果を変数ysにタプルで格納。
  3. ysの各要素をVariableの引数としインスタンス化、それをリストにまとめたものをoutputsとする。
    • これが次のノードに渡される出力になります。
    • ysの値は作成したVariableインスタンスのdataに格納されます。
  4. outputsの各要素のインスタンス変数creatorにAddを格納する。
  5. outputsを自身のインスタンス変数outputsに格納する(後で使う)。
  6. 引数a,bを自身のインスタンス変数inputsに格納する(後で使う)。
  7. outputsを返す。
    • outputsの要素が複数になる場合、リスト化されて格納されます。
    • 今回はoutputsが1つしかないので、リストではないことを表す意味でoutputsを破線の[]で囲っています。以下ややこしいのでご注意ください。
      • リストの状態になっているのはAddに保存されているoutputsで、次の各ノードにはそれぞれoutputs[0],outputs[1],...が渡されます。リスト型のオブジェクトがそのまま次のノードに渡されるわけではありません。
      • 今回outputsは1つですがoutput.dataに格納されたys(ndarray)の要素は2つです。「出力要素が複数」と「出力要素の要素が複数」は別の意味だと把握するのに混乱しました…
逆伝播

めちゃくちゃややこしい図になりました…図の説明の前に前提が2つあります。

  1. 順伝播の時の設定と異なり、後々の説明を考えてa, bより下流にも層があることとします。ただし、一つ下にいる関数が何なのかは図では不明なので、creatorは?としておきます。
  2. この図はこれより先にもノードが存在し、最終地点の出力の名をLとします。

2について補足すると、逆伝播ではLからどんどんと勾配が更新されながら下ってくるわけですが、図より先のノードがどうなっていようとも、上流から流れてくる勾配=Addに格納される勾配はLをoutputsで微分した値であることには変わりません。

outputsを簡略的にcで表わすとoutputsの持つ勾配は \dfrac{\partial L}{\partial c}で、この値が上流から流れてきてoutputsのインスタンス変数gradに格納された状態(黒線のフローまで完了したところ)までを前提としてそのあとの流れを説明します。

f:id:cha_kabu:20201106222756p:plain

長くなるので区切って説明します。まずは赤線のフローから。

  1. 上流からの流れを受けて、c.backward()が自動実行されます。

    • cはAddのインスタンス変数outputsのことで、FunctionではなくVariableインスタンスであることに注意してください。
    • 再三になりますがoutputsのことをcと呼んでおり、図だとcとoutputsが別物に見えますが同じものです。
  2. cは自身のcreatorに格納された親を確認し変数funcsに格納、そのうちの一つを変数fに格納します。つまり、fはここではAddの一時的なコピーです。

    • funcsはリスト型で、以降順次値が追加されていきます。話が飛んでしまいますが後程aとbのbackward()が(自動で)呼び出されたときには、aとbの親を探してきて新たに追加します。
    • ですので、実はここでの説明は少し嘘があります。cが変数funcsを作ったかの様な書き方をしていますが、実際は最初にLが作ったfuncsに下流の各ノードが生みの親を追加していきます。
  3. grad、すなわち上流から流れてきてcに格納されていた勾配を、新たな変数gysに渡します。

    • gysはリスト型でgradが保管されます。outputsが複数ある場合、上流から流れてくる勾配が複数になるのでgradも複数になります。
  4. f(今回は中身はAdd)のbackward()メソッドをgysを引数に実行します。ステップ13のところでまとめた通り、足し算の逆伝播は入力をそのまま下流に流すので、2つのgyを返します(そうなる様に、Addクラスのbackward()メソッドを実装しています)。

    • gysから急にgyに名前が変わったのは、Add.backward()の入力はひとつ(足し算の出力はひとつなので)で引数のデフォルト名称がgyと名付けられているためです。呼び方だけの話で値そのものは変わっていません。
  5. 二つの返り値gyを、タプルとして新しい変数gxsに格納します。

以上が赤線までの流れです。青線のフローに入る前に図で表現できていない処理の説明です。

今回はf.backward()の返り値が2つの前提なのでgxsに自然とタプルで格納されます。しかし要素が一つの時―すなわちf.backward()の返り値が一つの場合は値そのものが格納されてしまうので、if文を使って要素が一つの場合はタプルに変更します。これは後続の青線のフローの処理をする際にイテレータである必要があるためです。

ちなみに返り値2つだとタプルになるの、いつも複数出力は複数変数で受け取っていてピンとこなかったので一応試してみました。

a = 1,1
print(a)

# (1,1)

続いて青線のフローです。

  1. zip(f.inputs, gxs)でfor文を回します。1ループ目はaとgy(aに流すべき勾配)のセット、2ループ目はbとgy(bに流すべき勾配)が呼び出されます。
    • for文でgyを受け取る変数の名称が書籍上gxになっているので、以降のフローではgyをgxと呼び変えます。値はgyから変わりませんのでご注意ください。
  2. aとbのインスタンス変数gradにgxを格納します。このとき、aもしくはbのgradがNoneであればgxをそのまま格納、そうでなければ―すなわち、すでに他のノードから勾配を受け取っていた場合は、既存のgradにgxを足してgradを更新します。
  3. aとbのインスタンス変数creatorを確認し、funcsに格納します(Noneなら何も格納されない)。
  4. ループの頭(赤線フローの1)に戻り、a.backward()→b.backward()が実行されます。

以上!!

最後に

ひとつひとつの処理は簡単なのですがどんどん頭の中の設計図が追い付かなくなってきて地味にここまでまとめるのにめちゃくちゃ時間使いました。次は~ステップ19か24までのまとめを予定。