cha_kabuのNotebooks

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

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

はじめに

以下のシリーズ記事の続きです。

cha-kabu.hatenablog.com

本編

ステップ42 線形回帰の理論

ここまでのステップの内容理解&線形回帰について知っていれば難しくありません。全体の計算グラフは以下の様な形です。

f:id:cha_kabu:20201119154131p:plain

 x yも計算グラフの一員ですが、値を変化させたいのは W,bだけですので、これまでの例と違い全てのノードのgradには興味が無く、 W,bについてのみ興味があります。そんなわけで、loss.backward()ですべてのノードのgradはこれまで通り算出されるのですが、cleargrad()を行ったりdataを書き換えるのはこの二つだけになります。

ステップ43 ニューラルネットワークの実装

非線形なsin関数の予測のためにステップ42で使った線形回帰とシグモイド関数を使って2層のニューラルネットワークを作成します。こちらもニューラルネットワークに詳しければ難しくないと思います。ただ、自分は恥ずかしい話とても基本的なところを良く分かっていなかったと気づかされました…いよいよニューラルネットワークなので「良く見る例のネットワーク図を書いておこう!」と思ったのですが、あれ縦に列要素が並んでいるんですね…流石に分かってはいたのですが、図にしようとしてから「あれ?」となったので身についてはいなかったのだ思います。。戒めを込めてこの気付きを図にしておきます。

f:id:cha_kabu:20201119154142p:plain

改めて確認することでもないんですけどね…各層のノードは縦に並んでいるので頭の中で行を意味しているイメージがこびりついていました。

あとグラフ描画のためのコードが書籍GitHubに記載されているのですが、次元を合わせるために使用されているnp.newaxisを知らなかったので参考にしたサイトをメモ。

パラメータをまとめるレイヤ

いくつかプログラミング部分で知らないことがあったのでまとめておきます。

特殊メソッド__setattr__

詳細はこちらのサイト様が分かりやすかったです。

まずそもそもこいつが何なのかですが、「インスタンス変数を設定するときに裏で呼び出されているメソッド」と考えておけば良さそうです。例えば以下のシンプルなクラスにインスタンス変数を設定し、それを変更することを考えます。

class Human():
    def __init__(self, age):
        self.age = age

# ①インスタンス化
human = Human(age=5)
print(human.age) # 5

# ②インスタンス変数の修正
human.age = 10
print(human.age) # 10

# ③インスタンス変数の修正(わざわざ__setattr__呼び出し)
human.__setattr__("age",20)
print(human.age) # 20

①でインスタンス化し、②でインスタンス変数ageにアクセスして修正する方法については通常良く行われることなので問題ないと思います。ですが、実は②の裏では③の様に__setattr__が呼び出されることでageの値が上書きされています。__setattr__の引数はコードの通りインスタンス変数名とそれに格納したい値です。図で表すと以下の様な関係です。

f:id:cha_kabu:20201119154156p:plain

上図の通常挙動で問題なければクラス内でわざわざ__setattr__メソッドを定義する必要はないのですが、特別な挙動を加えたいのであればオーバーライドして挙動を変えてあげます。書籍では以下の様にオーバーライドしています。

class Layer:
    def __init__(self):
        self._params = set()
    
    def __setattr__(self, name, value):
        # 変更点
        if isinstance(value, Parameter):
            self._params.add(name)
        # 通常の処理の呼び出し
        super().__setattr__(name, value)

変更点の通り、インスタンス変数を定義するときにその値valueがParameterのとき、インスタンス変数paramsにそのnameを渡します。その後は通常の処理を行います(通常処理も忘れずに書かないとparamsにnameを渡しただけで処理が終わってしまう)。これでLayerクラスはインスタンス変数を設定する際にそれがParameterインスタンスであれば_paramsにその変数名を格納する様になりました。

ここまでで未だによく分かっていないのが、Layerクラスは何も継承していないのにsuper()を使って誰を呼び出しているのでしょう…Layerクラス内では書き換えてしまっているので何かしら大本の処理を呼び出す必要があるのは分かるのですが、何を参照しているのかわかりませんでした…「すべてのクラスの親玉がいるんだろうな」ということにして先に進もうと思います。。

yield

Layerクラスの実装時に使用されています。初めて見たためこちらのサイト様で概要を掴みました。

クラスのまとめ

一つ一つの処理は難しいものではありませんが、Layerクラスの導入によって再び全体像が分かりづらくなってきましたので、改めて各クラスについてまとめておきたいと思います。まずは、どんなクラスがあるかからです。

f:id:cha_kabu:20201119154206p:plain

大きく分けると図の通り3つのクラスがあります。

  1. VariableとParameter
    この二つは兄弟の様な関係で、まったく同じ機能を持っています。なぜ同じ機能なのに分けているかというと、二つを区別するためです。Parameterはその名の通り「パラメーター」(値を変化させたい)を扱うクラスで、Layerクラスのインスタンス変数として機能し、学習時に値を更新していきます。そういう意味ではParameterの方が若干機能が多いのでお兄ちゃん的立ち位置かも知れません。 Variableは変数というよりも、「データ」などと認識したほうが良いかも知れません。Variableの値自体を更新することはほとんどなく、最初に渡した値を持ち続けます。ParameterはLayerクラスのインスタンス変数として使われるものでしたが、VariableはFuncitionのインスタンス変数にもなるし、入出力にもなります。パラメーター以外の大抵の数値が(意識してインスタンス化せずとも)Variableになります。

  2. Function属
    Functionを継承して作成される各クラスです。大きくわけてfunctionモジュールで定義されるものとcoreモジュールで定義されるものがあります。 coreモジュールで定義されるものはいわゆる「四則演算(+α)」で、基本的な計算を行います。ステップ20~22で演算子オーバーロードを行っているので、あまりこれがクラスだと意識する必要はありません。 functionモジュールで定義されるのは「関数」です。関数といっても以下の様な色々な種類があります。

    • Matmulやreshapeの様なプログラミング的な意味での「関数」
    • 線形変換を行うlinear、活性化関数(ex.Sigmoid)、誤差関数(ex.MeanSquaredError)などの機械学習アルゴリズムの一部

    coreモジュールも含め様々な種類がありますが、「計算グラフのどこかに存在する計算処理」という意味ではすべて同じ働きをするものです。

  3. Layer属
    Layerを継承して作成される、その名の通りDeepLearningの「層」を作るクラスです。Functionと似たようなコードが多くあるので親戚の様にも思えますが、イメージとしては「上司」の様な役割だと思います。Layerクラスの命令で(一部の)Functionが動き、Parameterが更新されるイメージです。

それぞれのクラスのネットワーク上での働きを、すべての働きはもう一枚絵にはできませんが書籍のLinear2層の例を使って図にしておきます。

f:id:cha_kabu:20201119154218p:plain

黒線が順伝播、赤線が逆伝播の動きです。

  1. Linear層2つをインスタンス化、x(訓練データ)、y(教師データ)を用意しておく
    • Linear層の中ではParameterも自動的にインスタンス化される
    • xとyはVariableとしてインスタンス化しても良いが、計算処理時に自動的にVariableになるのでndarray形式でOK
  2. predict関数を作成しておく ※Functionクラスの関数ではなく、いわゆる自作関数
  3. 以下処理をループしパラメータの更新を行う
    1. predict関数にxを渡して実行
      1. xを引数に、Linear1の__call__メソッドが呼び出される→linearのforwardメソッドが実行される
      2. 1の結果を引数に、sigmoidを実行(内部で__call__メソッドが呼び出され、自身のforwardメソッドを実行)
      3. 2の結果を引数に、Linear2の__call__メソッドが呼び出される→linearのforwardメソッドが実行される
    2. 1の結果y_pred(予測値)とy(教師データ)を引数にMeanSquaredErrorを実行(内部で__call__メソッドが呼び出され、自身のforwardメソッドを実行)
    3. 2の結果lossのbackwardメソッドを呼び出し、各ノードに伝播する勾配を求める
    4. 3の結果から、パラメータW1,W2,b1,b2を別途定めた学習率に則り更新する

ステップ45 レイヤをまとめるレイヤ

現状のLayerインスタンスは複数使うにも関わらず別個に管理する必要があります。これらを一元的に管理できるようにするステップです。一気にDeZeroが他の有名なライブラリの様に動くようになりますが、基本的な処理の流れは先ほどの図から大きな変更はありません。

一つ注意したいのは最初にインスタンス化するのがLayerクラスそのものだということです。Functionクラスは常に継承されて利用され、それ自体がインスタンス化されることはありませんでした。Layerクラスも継承されますが、こちらは自身をインスタンス化することがあり得ます。大元のLayerクラスのインスタンス変数に、それを継承したクラス(Linearなど)を格納していくイメージです。こうすることで、パラメーター更新のコードを短縮することができます。

また、書籍では他のライブラリで良く使用するModelクラス(Layerクラスとほとんどの機能は同じで、可視化のコードを追加したもの)を作成し、それを継承してMLPクラスなど汎用的はニューラルネットモデルを作成します。

そこで未だに解決できていないバグにあたりました…ステップ別のGitHubに公開されているコードを実行する(Modelクラスをimportして使う)と問題ないのですが、notebook内でLayerクラスの定義→Modelクラスの定義→TwoLayerNetクラスを定義して学習とするとmodel._paramsに値が何も格納されず学習が進まないんですよね…色々試したところLayerクラスをnotebook内で定義するのではなくimportして使ってやると問題ないので、Modelクラス定義以降の処理は問題なさそうで、notebook内のLayerクラスに問題がありそうなのですがソースコードコピーしてもダメで…クラスをimportして使うのと同じコードを書いて使うでは挙動は同じ思っていたので何が起こっているのかさっぱりです。。

バグが気持ち悪いですが、最後にLayer-Model-MLPクラスの関係性を簡単に図にして次に進もうと思います。

f:id:cha_kabu:20201119154231p:plain

Layerクラスを継承したModelクラスを継承したMLPクラスのインスタンス変数にLayerクラスを継承したLinearクラスがいて…と正直もう頭がこんがらがってきました。。

最後に

だいぶ既存のDeepLearningライブラリの様な実装ができるようになってきました!次は~ステップ48までのまとめを予定。