はじめに #
本記事では、ChainerのVariableクラスについて簡単に解説し、1階微分、2階微分の求め方についてまとめる。 自動微分を使うと関数の勾配ベクトルを自動的に求めることができるので、勾配を使った最適化手法を容易に行える。
Chainerと最適化 #
Chainerは株式会社Preferred Networksが提供しているディープラーニング用のライブラリである。 Chainerには、ニューラルネットワークの学習を高速に行うため自動微分(定義した関数の勾配を自動で求める)機能が実装されている(この機能を使うことで、損失関数を最小化するために、ニューラルネットワークの各ノードの重みをどの方向に更新すれば良いか分かる)。
一方、最適化問題を解くとき、最急降下法などの手法では関数の勾配が必要となる。 関数の勾配を求めるためには以下の方法がある。
- 関数の導関数を手計算で求める方法
- 数値微分(少しだけ変化させた入力変数を与えて出力の差から勾配を求める)
- 自動微分
問題が複雑な場合、(1)は困難である。 また、(2)は計算時間を要する問題がある。 (3)は、実装に手間が掛かるという欠点があるが、問題ごとに導関数を求める手間も不要で、計算時間も短い利点がある。
Chainerには(3)の機能がVariableというクラスで実装されているので、最適化に活用するため仕様についてまとめた。 また、最急降下法のPythonでの実装については過去記事をご参考まで。
環境 #
ソフトウェア | バージョン |
---|---|
Spyder | 3.3.3 |
Python | 3.7.3 |
NumPy | 1.16.2 |
Chainer | 6.3.0 |
以下では、各ライブラリを以下のようにインポートしていることを前提とする。
import numpy as np
import chainer
from chainer import Variable
Variableクラス #
ChainerのVariableクラスは数値の配列データを保持するクラスである。 NumPy配列に近い感覚で扱えるが、相違点もあるので注意。
Variableオブジェクトは32ビット精度のNumPy配列を使って作成される。
例:
>>> x0 = Variable(np.array([0, 1], dtype=np.float32))
>>> x1 = Variable(np.array([[0, 1, 2], [3, 4, 5]], dtype=np.float32))
生成されたオブジェクトはVariable型の配列である。
>>> x0
variable([0., 1.])
>>> x1
variable([[0., 1., 2.],
[3., 4., 5.]])
Variableオブジェクトでは同じサイズの配列同士の演算ができる。 (配列のサイズが異なる場合、NumPyではブロードキャストしてくれるが、Variableオブジェクトはエラーを返す)
>>> x0 = Variable(np.array([0, 1], dtype=np.float32))
>>> x2 = Variable(np.array([2, 3], dtype=np.float32))
>>> x0+x2
variable([2., 4.])
また、配列のインデックスを指定して要素を取り出すことも可能である。
>>> x1 = Variable(np.array([[0, 1, 2], [3, 4, 5]], dtype=np.float32))
>>> x1[0]
variable([0., 1., 2.])
>>> x1[0, 2]
variable(2.)
Variableオブジェクトのarray
属性からNumPy配列を取得できる。
>>> x0 = Variable(np.array([0, 1], dtype=np.float32))
>>> x0.array
array([0., 1.], dtype=float32)
>>> type(x0.array)
numpy.ndarray
なお、同様にdata
属性からもNumPy配列を取得できるが、chainerの公式リファレンスでは「NumPy配列のdata
属性」と紛らわしいという理由で、array
属性の使用を推奨している。
Variables and Derivatives — Chainer 7.7.0 documentation
さらに、Variableオブジェクトのgrad
属性から勾配のNumPy配列を取得できる(詳細は後述)。
1階微分の求め方 #
Variableオブジェクトを使った1階微分の求め方について述べる。
まず、以下の2変数関数を考える。
$$ f(\boldsymbol{x}) = 2x_0^2 + x_1^2 + 2x_0 + x_1, \boldsymbol{x}=[ x_0, x_1 ]^\top $$この関数の勾配ベクトルは次式で与えられる。
$$ \nabla f(\boldsymbol{x}) = \left[ \frac{\partial f}{\partial x_0}, \frac{\partial f}{\partial x_1} \right]^\top = [4x_0 + 2, 2x_1 + 1]^\top $$点\( (x_0, x_1)=(1, 2)\)において、関数値と勾配ベクトルはそれぞれ以下のようになる。
$$ f(\boldsymbol{x})=10 \\ \nabla f(\boldsymbol{x}) = [6, 5]^\top $$上記の関数をVariableを使って記述すると以下のようになる。
x = Variable(np.array([1,2], dtype=np.float32))
y = 2*x[0]**2 + x[1]**2 + 2*x[0] + x[1]
関数の値(10)は既に得られている。
>>> y
variable(10.)
この段階では勾配はまだ得られておらず、勾配を取得するためには以下を実行する。
>>> y.grad = np.ones_like(y.array, dtype=np.float32)
>>> y.backward()
まず、y
のgrad
に値は何でも良いので配列を設定する。
np.ones_like
は、引数の配列と同じサイズで、要素が全て1の配列を返す関数である。
(ただし、厳密にはgrad
に初期値を与える必要があるのは、y
が2つ以上の要素を持つ配列の場合である。
今回の例のようにy
がスカラーの場合、初期値の設定は省略できる)
次に、y.backward()
メソッドを実行すると、自動微分が実行され、x.grad
に勾配が格納される。
>>> x.grad
array([6., 5.], dtype=float32)
中間変数の勾配を求める場合 #
上記の方法では、以下のようにVariableオブジェクトの演算を2回以上重ねた場合、中間の変数の勾配が保存されない。
>>> x = Variable(np.array([1], dtype=np.float32)) # 入力
>>> y = x**2 # 中間変数
>>> z = y**2
>>> z.backward()
>>> y.grad # 勾配が格納されない (None)
>>> x.grad # zに対するxの勾配
array([4.], dtype=float32)
中間の変数の勾配が欲しい場合、backward
メソッドでretain_grad=True
とする。
>>> x = Variable(np.array([1], dtype=np.float32)) # 入力
>>> y = x**2 # 中間変数
>>> z = y**2
>>> z.backward(retain_grad=True)
>>> y.grad # zに対するyの勾配
array([2.], dtype=float32)
>>> x.grad # zに対するxの勾配
array([4.], dtype=float32)
2階微分の求め方 #
2階微分を求めるためには以下のようにする。
>>> x = Variable(np.array([1], dtype=np.float32))
>>> y = x**3
>>>
>>> y.grad = np.ones_like(y.array, dtype=np.float32)
>>> y.backward(enable_double_backprop=True)
>>>
>>> gx = x.grad_var
>>> x.cleargrad()
>>> gx.backward()
>>>
>>> x.grad # yに対するxの2階微分
array([6.], dtype=float32)
目的関数y
のbackward
を実行するまでは同じであるが、その後にx.grad_var
を取得する。
次に、cleargrad
メソッドでx
の勾配を削除したのち、x.grad_var
を格納した変数に対してbackward
を実行すると、x.grad
に2階微分が格納される。