ゼロから作るDeep Learning3 フレームワーク編を読む その④ステップ25~36
はじめに
以下のシリーズの続きです。 cha-kabu.hatenablog.com cha-kabu.hatenablog.com cha-kabu.hatenablog.com
本編
ステップ25~26
ステップ25では計算グラフを可視化するためにGraphvizを使ってDOT言語でグラフを記述する練習を行い、ステップ26でDeZeroでも可視化できるようにします。
と言ってもDeZeroが実際にDOT言語を扱っているのではなく「VariableやFunctionの各種インスタンスの持つ情報からDOT言語の記法に沿った文字列を作成し、dotコマンドにそれを投げて画像として保存、表示」しています。
GraphvizはJupyter Notebook上でも使えるライブラリがあり、そのイメージで直接動かすのかと思っていたので少し混乱しました。
ステップ27~36について
高階微分に関する話が続きます。記載されていることの理解は難しくないのですが、読んでいる最中も読み終わった今も、いまいちここの内容がどうDeep Learningに関わってくるのか分かっていません。ステップ36で
ディープラーニングに関連した用途でdouble backpropを使う研究はいくつもあります。
とあるのですが、自分のレベルでは全然出会っておらずイメージが湧かないので、とりあえず「ふむふむ」くらいで読み進めてしまったので全然ポイントをまとめられていないかも知れません。。
ステップ27 テイラー展開の微分
数学の知識がほぼ数ⅡBで止まっているので三角関数の微分とテイラー展開はちょっと面を喰らいましたが記載されていることは難しくありません。
テイラー展開は数式がどういう計算しているのかは分かったのですが、なぜこれで近似できるかまではイメージできなかったので以下の動画で理解を深めました。
【大学数学】テイラー展開の気持ち【解析学】 by 予備校のノリで学ぶ「大学の数学・物理」
また、書籍に載っているsin関数をテイラー展開した計算グラフがxとy以外のVariableインスタンス名が表示されていなくて計算過程を追いづらかったので、理解を深めるついでに少し改変したグラフを作成しました。左が書籍のもの、右が自作のものです。
何故か同じ層内でノードの位置関係が変わってしまうのだけ修正できませんでしたが、計算過程には変化ないので良しとしましょう…できたときちょっと嬉しかったのでこれを作るまでにやったことを備忘録として記載しておきます。
my_sin関数の修正
my_sin関数とは、書籍で作成するsin関数をマクローリン展開する関数のことです。以下が修正したものですが、後で紹介する他の修正も行わないとエラーになります。
def my_sin(x, threshold=0.0001): y = Variable(np.array(0)) y.name = "y_0" for i in range(100000): j = i j = 2 * j + 1 j = Variable(np.array(j)) d = x ** j c = (-1) ** i / math.factorial(2 * i + 1) c = Variable(np.array(c)) t = c * d y = y + t x.name = "x" j.name = "2*" + str(i) + "+1" c.name = "c_" + str(i+1) d.name = "d_" + str(i+1) t.name = "t_" + str(i+1) y.name = "y_" + str(i+1) if abs(t.data) < threshold: break return y
修正のポイントは以下です。
- いくつかの変数を手動でVariableインスタンス化
- 計算グラフ上に表現できるノードはVariableもしくはFunctionインスタンスに限られます。ここで、「ステップ20~で計算時にVariableでなかったら自動的に変換するようにしたから手動でやる必要ないのでは?」と思いますが、その計算が行われた段階で自動的にインスタンスができる=ノードの名前は全部Noneで、どのインスタンスの名称を変えればいいのか分からないので明示的に指定します。
- おそらくインスタンス変数を辿っていけばこうしないのでもできるのですが、ややこしくて諦めました…
- これにより、書籍のコードではx, y(と実はtも可能)にしか名前がついていませんでしたが、他のノードにも名前を付けられるようになります。
- 手動インスタンス化するタイミングも重要で、自動でインスタンス化される前(ノードができてしまう前)に行う必要があります。
- 後程説明するdの計算過程を明示するためにiもVariableに変換したかったのですが、そうしてしまうとmath.factorial()などのVariableを受け取る想定をしていない関数でエラーが起きてしまうのでjというコピーを作成して使い分けています。
- 計算グラフ上に表現できるノードはVariableもしくはFunctionインスタンスに限られます。ここで、「ステップ20~で計算時にVariableでなかったら自動的に変換するようにしたから手動でやる必要ないのでは?」と思いますが、その計算が行われた段階で自動的にインスタンスができる=ノードの名前は全部Noneで、どのインスタンスの名称を変えればいいのか分からないので明示的に指定します。
- コードを細分化
- 具体的にはt = c * dと表せるような変数dを作成しています。
- これも目的は手動インスタンス化同様でノードに名前を付けたいからです。
- forループの中で名前を付けて、名称で何ループ目の計算なのか分かる様に
Powクラスの修正
一層目の計算でがノードとして表現されています。これはがVariableになっていないとできませんが、Variableになっているとpowを通せません(powはVariableと普通の数値の入力を想定していましたが、今はxもも両方Variableで入力したい)。
これを計算されるためPowクラスとその初期化用関数を以下の様に変更しています。コメント部分が元々のコードです。ただ、この変更が今後どういったエラーを起こすか分からなかったので、対応後元に戻しています。
class Pow(Function): # def __init__(self, c): # 削除 # self.c = c # 削除 def forward(self, x0, x1): y = x0 ** x1 # y = x ** self.c return y # backwardは今回使用しないので変更なし def pow(x0, x1): # def pow(x, c): return Pow()(x0,x1) # return Pow(c)(x)
DOTファイルから直接色を調整する
ノードの色が全部同じだとどれがどの世代の計算か分かりづらかったために修正しました。やったことは泥臭く、書籍で実装されているget_dot_graph関数を使えばDOT言語仕様の文字列作成→DOTファイルが作成されるので、それを直接いじって各ノードの色を変え、コマンドでpngとして出力しただけです。
これをやるんだったら前段の処理もいらなくて最初っからDOTファイルいじれば良かったのでは
ステップ28 関数の最適化
勾配降下法を実装します。いわゆる普通の勾配降下法の実装(繰り返し回数指定したループを回して勾配に学習率かけて元の値を更新)なので、事前知識があれば難しいことはありません。
ただ、完全に誤解してたのですが勾配と微分って別物なんですね…少し改変していますが、
複数の微分をまとめたもの――ベクトルの形にしたもの――は、勾配(gradient)や勾配ベクトルと呼ばれます。
と説明されています。他の記事で微分のことを勾配と連呼していたので修正しなくては。。
ステップ29 ニュートン法を用いた最適化(手計算)
別途最適化アルゴリズムについて学んでいたとき1、「ニュートン法は収束は速いが計算量が多くてまだ実用されていない」と見たのでなぜ紹介されているのかちょっと混乱。ただ先々読み進めないと分からなそうなので気にせず先へ進みます。
ステップ30 高階微分(準備編)
これまでに実装したforward~backwardでどのようなフローで微分が求められるかの振り返りです。まとめながら読み進めていたのですんなり読めました。
ステップ31 高階微分(理論編)
「高階微分を求めるにはVariableのgradにも"つながり"を持たせる必要がある」という説明のステップです。これまでのことを理解していれば難しくありませんが、「gradはndarrayインスタンスを保持している」というのが頭にないと混乱するかも知れません。
これまでのDeZeroの各クラスが所持しているインスタンス変数と、そこで保持しているデータの型(?)をまとめると以下の様になっています。
クラス | 変数 | 型 |
---|---|---|
Variable | data | ndarray |
name | str | |
grad | ndarray | |
creator | Function | |
generation | int | |
Function(を継承したクラス) | generation | int |
inputs | Variable | |
outputs | Variable |
上の表のVariable.gradの型をndarray→Variableにしようというわけです。Variableにすればgrad自身がcreatorを持つことができ、逆伝播方向にも繋がりを作ることができます。
インスタンス変数に格納されたインスタンスの先頭を大文字で表すと、今まではInputs.creator-Creator.Inputs-...で繋がっていたのを、Grad.creator-Creator.grad-...でも繋げるイメージです。
ステップ32 高階微分(実装編)
メインでやっていることはgradをVariableにするだけです。ステップ20辺りでVariableをそのまま演算できる様にしているので、難しいことはありません。a→np.array(a)としている様なもんです。
Variableに逆伝播の無効モードを追加するところだけ少しややこしかったです。やっていることはステップ18でFunctionに無効モードを追加しているのとほとんど変わりませんが、なぜ今回はVariableに追加?となりました。
と頭に叩き込んでからフローを考えると整理しやすかったです。
ステップ33 ニュートン法を使った最適化(自動計算)
実装したものを使っているだけなので難しいことはありません。
ステップ34 sin関数の高階微分
ここも今までやったこと(Functionを継承した関数のクラスを作ったり、高階微分をしたり)をsin関数でやっているだけなので、すんなり読めます。
ステップ35 高階微分の計算グラフ
書籍の主題からそれますが、活性化関数にも使われるハイパボリックタンジェント(tanh関数)が出てきました。全然使ったことがなく頭に入っていないので、数学的性質をメモることで叩き込みます。
tanh関数は以下の式で表されます。
の時に
が大きくなるほど分母も分子もに近づくのでに近づきます。
が小さくなるほど分母は、分子はに近づくのでに近づきます。
グラフを描画してみます。
def tanh(x): return (np.exp(x)-np.exp(-x))/(np.exp(x)+np.exp(-x)) x = np.linspace(-5,5,1000) y = tanh(x) plt.plot(x,y) plt.show()
シグモイド関数が0を突き破って下方向に伸びた形をしています。
微分は書籍で紹介されている変形以外にもで表す方法とで表す方法があるようです。以下は書籍の例で、分数関数の微分の公式を用います。
、なので、以下の様に変形できます。
ステップ36 高階微分以外の用途
高階微分(ができる機能)を使うと実装できる論文が紹介されているのですが、とりあえず深入りせずに先へ進みます。
最後に
次のステップからいよいよニューラルネットワークを作る機能の実装で楽しみです!~ステップ41までをまとめる予定。