cha_kabuのNotebooks

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

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

はじめに

以下の記事の続編です。 cha-kabu.hatenablog.com cha-kabu.hatenablog.com

本編

ステップ15 複雑な計算グラフ(理論編)

これまで扱ってきたのは一直線の計算グラフですが、各層のノード数が複数になると逆伝播がうまくいかないことを説明するステップです。内容は難しくなく、コードも出てこないので理解しやすいです。

ステップ16 複雑な計算グラフ(実装編)

ステップ15で解説された問題を解決するために、①VariableクラスとFunctionクラスにそのインスタンスが生成されたときに"世代"を記憶できるように新しいインスタンス変数generationを追加します。②generationの順に逆伝播するようにVariableクラスのbackwardメソッドを変更します。

図示するほどのややこしさは無いですが、前の記事でまとめておいた順伝播~逆伝播のフローが変わるので、理解を深めるためにまとめておきます。

インスタンス変数generarion追加方法

まずはVariableクラスの場合。

class Variable:
    def __init__(self, data):
    ### 省略 ###
        self.creator = None
        self.generation = 0
        
    def set_creator(self,func):
        self.creator = func
        self.generation = func.generation + 1

    ### 省略 ###

0で初期化します。ですので手動でVariableインスタンスを生成した時は何か意図的に変更しない限りはgeneration=0となります。

一方Variableインスタンスは手動で生成するだけでなくFunctionインスタンスからも生成されます。そしてその場合、自動的にset_creatorメソッドが呼び出されるフローになっています。

set_creatorメソッドでは、前のステップまでの通り"生みの親"を記憶しておくcreatorに親であるFunctionインスタンスを格納しつつ、新しい機能としてgenerationを0から「生みの親の世代+1」に更新します。

続いてFunctionクラスの場合。

class Function(object):
    def __call__(self, *inputs):
    ### 省略 ###
        self.generation = max([x.generation for x in inputs])

    ### 省略 ###

Functionクラスは引数inputsとしてVariableインスタンスを受け取ります。つまり入力は必ずインスタンス変数generationを持っており、複数の入力のgenerationを全部調べてその中で最大のものを自身のgenerationとします。

②generationの順に逆伝播させる

これまでの逆伝播はVariableクラスのbackward()メソッド内で大雑把に以下のフローで実施されていました。

  1. 生みの親のFunctionインスタンスをリストfuncsに追加する
  2. funcsの後ろに格納されたFunctionインスタンスから勾配の更新を行う
  3. 勾配を更新したFuncitonインスタンスの引数となったVariableインスタンスに注目する
  4. 1-3をループする

こちらのフローの1~2の間に、「リストFuncsを世代昇順にソートする」というフローが加わるだけです。オリジナルの簡単な例で見てみます。

# インスタンス変数generationを持つだけのクラス
class Temp:
    def __init__(self, g):
        self.generation = g

# インスタンス化(うち二つは同じ引数で中身は一緒にしておく)
a = Temp(1)
b = Temp(2)
c = Temp(1)

# 空のlistとsetを作成
funcs = []
seen_set = set()

# 引数がseen_setに無ければfuncsとseen_setに追加しfuncsを各generation順でsortする関数
def add_func(f):
    if f not in seen_set:
        funcs.append(f)
        seen_set.add(f)
        funcs.sort(key=lambda x: x.generation)

# add_funcの実行
add_func(b)
add_func(a)
add_func(a)
add_func(c)

# funcsに格納された各インスタンスのgenerationを確認
print([i.generation for i in funcs])
# [1, 1, 2]

Tempクラスはオリジナルに作ったものですがadd_func関数は書籍と同じものです。

add_funcの実行はb→a→a→cの順で行っており、前ステップまでの状態ではfuncsリストに格納されるのはそのまま[b, a, a, c]でした。各インスタンスの世代で書き換えると[2, 1, 1, 1]です。

しかし今回実際にfuncsに格納された各インスタンスのgenerationを確認した結果は[1, 1, 2]となっており、変数名で書き換えると[a, c, b]の順になっています。ポイントは以下です。

  • aが一つになっているが、cとは区別されている
    if f not in seen_set~で同じインスタンスはlistに追加しない
  • 世代の昇順になっている
    funcs.sort~を、keyをgenerationにして実行

ステップ17~ステップ24

ここから「書いてあることは簡単だけど、本質的なことは恐らく全然分かってない」と思える内容が続きます。Deep Learningについてというよりかは、Pythonとかプログラミングそのものについての話なので、「本書を通してDeep Learningについて学びたい」が主目的の場合はあまり深入りしなくても良い部分かと思います。

自分も逃げる深くは立ち入らないでおこうと思います…が、デコレータと特殊メソッドについてはコンペのkernelとかでも良く見る処理で、良くわからずに真似してしまっているのでそのうちまとめて別記事にしたいなと思います。

最後に

逃げてしまったので前2記事と比べるとずいぶんとあっさりした記事になりました。次回は未定…というか知らない言葉が多くて目次見るだけだとどこでキリが良いのか分からない。。ステップ25~もDeep Learningそのものというよりかは数学力を試されるステップになりそうですが、ここについては逃げずに(ある程度は)ちゃんと理解しながら進めたいと思います。