ほのぼのしたエンジニアのブログ

触った技術についてまとめていくブログです。

Chainerに入門してみる:Variables and Derivatives

どうもShZgZです。

機械学習の概要はなんとなくわかってるけども、
実際にフレームワーク使ってやってみようとなると
全然重い腰が上がらない状態でした

流石にこのままじゃまずい、せめて入門くらいしよう!

ということでハッカソン申し込みました。
dllab.connpass.com
(参加する皆さん宜しくお願いします)

入門ぐらいは当然終わってるよね?という運営さんの
熱いプレッシャーを糧に入門していきます
(正確には参加対象が"Chainer入門レベルが理解できる方"です)

MNISTとかの写経はしましたが、どうにもChainer自体が
よくわからんと思ったので公式のガイドを追っていこうと
思います
Variables and Derivatives — Chainer 4.5.0 documentation

"補足"ってなってるところは付け足しているところで、
それ以外は大体原文の和訳になってます。

Variables and Derivatives

まずはインポート

import numpy as np
import chainer
from chainer.backends import cuda
from chainer import Function, gradient_check, report, training, utils, Variable
from chainer import datasets, iterators, optimizers, serializers
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions

Chainerは"Define-by-Run"スキームを利用しているため、forward計算自体がネットワークを定義します。
forward計算を始めるために、まずは入力配列をchainer.Variableオブジェクトにセットします。
ここでは1要素のndarray

x = Variable(np.array([5], dtype=np.float32))

Variableオブジェクトは基本的な算術演算子をサポートしているので、
y = x^2 + 2x + 1は以下のように書けます

y = x**2 - 2 * x + 1

この計算結果yもまたVariableオブジェクトです。
array属性にアクセスすることで値を取得できます。

y.array # array([16.], dtype=float32)

~~~補足:
公式にも書いてありますが、Variableオブジェクトの
array属性とdata属性は同じものを参照していますが、
data属性はnumpy.ndarray.data属性と
混同してしまうかもしれないのでarray属性を用いることが
推奨されているそうです。
実際、

y.data # array([16.], dtype=float32)

~~~

yは結果の値のみではなく計算の履歴(または計算グラフ:computational graph)も
保持しています。このおかげで微分できる。
微分する際には

y.backward()

これは誤差逆伝播を行います。この時、勾配が計算され、
xのgrad属性に代入されます。

x.grad # array([8.], dtype=float32)

~~~補足:
高校の数学ですね。texの練習も兼ねて書いてみます。
 x = 5
y = x^2 + 2x + 1
\frac{dy}{dx} = 2x - 2
なので微分した値は
 2 * 5 - 2 = 8
ですね。
~~~

中間変数の勾配計算も可能です。Chainerはデフォルトでは、
メモリの効率化のために中間変数の勾配配列を解放してしまいます。

z = 2*x
y = x**2 - z + 1
y.backward()
print(z.grad) # None

なので、勾配情報を保持するためにはy.backwardにretain_grad属性を渡しておきます。

z = 2*x
y = x**2 - z + 1
y.backward(retain_grad=True)
z.grad # array([-1.], dtype=float32)

これらの計算は全て複数要素の配列に一般化できます。
複数要素の配列を持つ変数からbackward計算を始める場合は
最初の誤差を手動でセットしなければなりません。

x = Variable(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
y = x**2 - 2*x + 1
y.grad = np.ones((2, 3), dtype=np.float32)
y.backward()
x.grad # array([[ 0.,  2.,  4.],
       #        [ 6.,  8., 10.]], dtype=float32)

~~~補足:
誤差の手動セットのところですが

x = Variable(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
y = x**2 - 2*x + 1
y.grad = np.array([[0, 0, 0],[1,2,3]], dtype=np.float32)
y.backward()
x.grad # array([[ 0.,  0.,  0.],
       #        [ 6., 16., 30.]], dtype=float32)

元のnp.onesは全て1の配列です。そこから今回のように
変えた場合、それぞれの要素で微分した結果にセットした値を
掛けたものとなるようです。
~~~

高階微分
Variableオブジェクトは高階微分をサポートしている。
(高階とはいえ2階微分までです。)
まずは、1階微分の計算です。
y.backward()にenable_double_backprop=Trueが
渡されているところに注目して下さい。

x = chainer.Variable(np.array([[0, 2, 3], [4, 5, 6]], dtype=np.float32))
y = x ** 3
y.grad = np.ones((2, 3), dtype=np.float32)
y.backward(enable_double_backprop=True)
print(x.grad_var) # variable([[  0.  12.  27.]
                  #           [ 48.  75. 108.]])
print(x.grad_var.array is x.grad) # True
print((x.grad == (3 * x**2).array).all()) # True

chainer.Variable.grad_varはchainer.Variable.gradの
Variableオブジェクトです。(型はndarray)backward()に
enable_double_backprop=Trueを渡すことにより、
backward計算のための計算グラフが記憶されます。
これにより、2階微分を計算するためにx.grad_varから
逆伝播を始めることができる。

gx = x.grad_var
x.cleargrad()
gx.grad = np.ones((2, 3), dtype=np.float32)
gx.backward()
print(x.grad) # array([[ 0., 12., 18.],
              #        [24., 30., 36.]], dtype=float32)
print((x.grad == (6 * x).array).all()) # True

~~~補足:
この辺り個人的にわかりずらかったので、いくつかまとめます。

・enable_double_backprop
まず、backward()に渡されるenable_double_backpropですが、これが無いと

gx.backward()
print(x.grad) # None

となり、意味のまんまですが2階微分が計算できません。

・cleargrad
途中にあるcleargradですが、これはx.gradをクリアする、
つまりNoneにするという操作です。
これをしないと、

gx = x.grad_var
print(x.grad) # array([[  0.  12.  27.]
              #        [ 48.  75. 108.]], dtype=float32)
#x.cleargrad()
gx.grad = np.ones((2, 3), dtype=np.float32)
gx.backward()
print(x.grad) # array([[  0.  24.  45.]
              #        [ 72. 105. 144.]], dtype=float32)

となり、1階微分に2階微分の値が加わってしまいます。

・数学的な微分との対応
数学的な微分とx.gradとの対応がはっきりしなかったためまとめます。
まず、xとその属性の型について
x:chainer.variable.Variable
x.array:np.ndarray
x.grad:np.ndarray
x.grad_var:chainer.variable.Variable
要するに、

Variable ndarray
x x.array
grad_var grad

また、backwardした後は
x.grad_varは大体\frac{dy}{dx}と対応しています。
そのため、上記の例でも2階微分をする際には

gx = x.grad_var

として

gxx = gx.backward()

というのは
gx = \frac{dy}{dx}
を表し、
gxx = \frac{d}{dx}(\frac{dy}{dx})
または
\frac{d^2 y}{(dx)^2}
を表します。