「ゼロから作る Deep Learning 3 フレームワーク編」の読書まとめ。思い出すためのまとめメモです。解説ではないので書籍を読んでいる前提です。第3ステージ、目的は「高階微分を実現する」です。
コードはこっち
第3ステージ 高階微分を実現する
微分の微分を自動で行うようにするための手段を実現させるステージ。微分の微分の必然と併せて。その確認のためにGraphvizというツールに関しての解説もありますが、そこは割愛。テイラー展開やマクローリン展開に関してもやはりこのステージの本質ではなさそうなので割愛。高次微分の必然を最小化問題を解くことでまずは実感するところからその実装までを確認します。
ローゼンブロック関数の最適化
とある関数を最適化するときに微分とそのさらに微分がとても役に立つ。ということを実感するためのステップ。サンプルとなる関数は以下のようなローゼンブロック関数。
$$ y = 100(x_1 – x_0^2)^2 + (x_0 – 1)^2 $$
まずは勾配降下法を使います。この関数の微分(勾配)を求めて、その逆方向に進んで最小値を探しに行きます。これを繰り返すとそのうち最小値(厳密には局所的な最小値)にたどり着くことが期待できるでしょう。ってものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | def rosenbrock(x0, x1): y = 100 * (x1 - x0 * * 2 ) * * 2 + (x0 - 1 ) * * 2 return y x0 = Variable(np.array( 0.0 )) x1 = Variable(np.array( 2.0 )) lr = 0.001 for i in range ( 1000 ): y = rosenbrock(x0, x1) x0.cleargrad() # 係数を使いまわすので x1.cleargrad() # クリアしないと勾配が加算され続ける y.backward() x0.data - = lr * x0.grad # データの更新 x1.data - = lr * x1.grad |
このコードでは初期値を0.0、2.0から始めます。1000回ループ。学習率0.001でシンプルな勾配降下法で求めています。計算した結果微分を保持しているという仕組みがあるために、y.backward()でx0,x1の勾配(grad)が得られており、それをもとの位置の更新に使っています。
この学習率だと1000回程度では最小値にたどり着かず、50000回ほど必要なようです。これだと効率が悪いので、ニュートン法というのを用いるとよいそうです。ここでテーラー展開の効用が出てくるのですが、まぁそこはそういうものだと思って使うことにします。
ニュートン法
テーラー展開の2次の項までで打ち止めて、微分してゼロのところがひとまずの最小値であることが期待できる。ということに基づいています。そこに向かって学習を進める方法です。2次のテーラー展開が
$$ f(x)\simeq f(a) + f'(a)(x-a) + \frac{1}{2}f”(a)(x-a)^2 $$
これをxで微分して=0のxが極値なので
$$ \frac{d}{dy}(f(a) + f'(a)(x-a) + \frac{1}{2}f”(a)(x-a)^2)=0 $$
$$ f'(a)+f”(a)(x-1)=0 $$
$$ x = a – \frac{f'(a)}{f”(a)} $$
$$ x = a – \frac{1}{f”(a)}f'(a) $$
これまでの学習率を使った式と比較するとこれは、1階微分した結果に対して、2階微分の逆数を学習率として更新していることと等価な式になっています。速度だけで到達点を探すのではなく、加速度も使う。という概念です。そりゃ効率も良いですね。これを使うことで今回の初期値の場合、50000回のイテレーションが6回で済むそうです。そりゃすごい。
ただ2階微分は自動計算されないので、手計算が必要となります。この手作業を自動で行う仕組みを組み込む話です。
2階微分の実装
今のVariableの実装だとdataとgradのいずれもndarrayですが、このgradもVariableにしてしまうことで、微分結果にも微分(勾配)を持たすということになります。
まずはその勾配の初期化部ですが、これをndarrayからVariableに変更します。
1 2 3 4 5 6 7 | class Variable: : def backward( self , retain_grad = False , create_graph = False ): if self .grad is None : #self.grad = np.ones_like(self.data) # ↓ self .grad = Variable(np.ones_like( self .data)) |
あとはFunctionを継承したそれぞれのbackwardの計算を通常のndarrayの計算からVariableの計算へ変更します。演算子をオーバーロードしているので大きな変更は必要なさそうですが、微分後の関数を用意していないケース(例えばsinの微分にあたるcos)ではそれも併せてFunctionを継承したVariableの計算で実装する必要があります。
例えば掛け算Mulであれば、
1 2 3 4 5 6 7 8 | class Mul(Function): def forward( self , x0, x1): y = x0 * x1 return y def backward( self , gy): x0, x1 = self .inputs[ 0 ].data, self .inputs[ 1 ].data return gy * x1, gy * x0 |
から
1 2 3 4 5 6 7 8 | class Mul(Function): def forward( self , x0, x1): y = x0 * x1 return y def backward( self , gy): x0, x1 = self .inputs # 違いはここだけ return gy * x1, gy * x0 |
への変更です。backwardの見た目は演算子をオーバーロードしているのでかなりシンプルに見えます。このケースでは、backwardでの掛け算で同じクラスのforwardが呼ばれることになります。
入力xを起点にしてyを計算する順伝播をy.backwardしてやることで、xを起点にしてx.gradを計算する計算グラフが出来上がることになります。図31-2がそれです。なので、x.gradをbackwardしてやることで、x.gradに2階微分した結果が戻ってきます。
あとは順伝播の際に微分計算をキャンセルして高速化するモード(推論やテストで使うモード)を逆伝播時の微分計算キャンセルするために追加することで、余計な計算を省くようにします。だいぶ関数が入れ子になってややこしいですが、コードを見るとまぁ動きそうな気がします。テストが大変そう。
この結果ニュートン法の実装は、
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | def f(x): y = x * * 4 - 2 * x * * 2 return y def gx2(x): return 12 * x * * 2 - 4 x = Variable(np.array( 2.0 )) iters = 10 for i in range (iters): print (i, x) y = f(x) x.cleargrad() y.backward() x.data - = x.grad / gx2(x.data) |
これが
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | def f(x): y = x * * 4 - 2 * x * * 2 return y x = Variable(np.array( 2.0 )) iters = 10 for i in range (iters): print (i, x) y = f(x) x.cleargrad() y.backward(create_graph = True ) # 微分保持 gx = x.grad x.cleargrad() gx.backward() gx2 = x.grad x.data - = gx.data / gx2.data |
これになります。2階微分の関数をわざわざ作る必要がないです。
くどいですが、入力xを起点にしてyを計算する順伝播の結果をy.backwardしてやることで、xを起点にしてx.gradを計算する計算グラフが出来上がることになります。なので、x.gradをbackwardしてやることで、x.gradに2階微分した結果が戻ってきます。起点が同じなので、x.gradにn階微分した結果が常に戻ってきます。Valiable.backwardすると、辿って行って、Function.inputs.gradすなわちx.gradに答えが戻ってくる。forwardの時のinputsを覚えているので。
sin/cosのn階微分
微分を繰り返すことでsinとcosを行ったり来たりします。
1 2 3 4 5 6 7 8 9 10 11 | x = Variable(np.linspace(-7, 7, 200)) y = F.sin(x) y.backward(create_graph=True) logs = [y.data] for i in range(3): logs.append(x.grad.data) gx = x.grad x.cleargrad() gx.backward(create_graph=True) |
これも1度目の順伝播で作られたグラフを逆伝播させることでxを起点にgxを計算する計算グラフが作られ、都度gxの逆伝播を求めると、xのgradに答えが返ってきてます。
コメント