JavaScriptを有効にしてください

Chainerの自動微分で勾配を求める

 ·   5 min read

はじめに

本記事では、ChainerのVariableクラスについて簡単に解説し、1階微分、2階微分の求め方についてまとめる。
自動微分を使うと、関数の勾配ベクトルを自動的に求めることができるので、勾配を使った最適化手法を容易に行える。

Chainerと最適化

Chainerは、株式会社Preferred Networksが提供しているディープラーニング用のライブラリである。
Chainerには、ニューラルネットワークの学習を高速に行うため、自動微分(定義した関数の勾配を自動で求める)機能が実装されている(この機能を使うことで、損失関数を最小化するために、ニューラルネットワークの各ノードの重みをどの方向に更新すれば良いか分かる)。

一方、最適化問題を解くとき、最急降下法などの手法では、関数の勾配が必要となる。
関数の勾配を求めるためには、以下の方法がある。

  1. 関数の導関数を手計算で求める方法
  2. 数値微分(少しだけ変化させた入力変数を与えて出力の差から勾配を求める)
  3. 自動微分

問題が複雑な場合、(1)は困難である。
また、(2)は計算時間を要する問題がある。
(3)は、実装に手間が掛かるという欠点があるが、問題ごとに導関数を求める手間も不要で、計算時間も短い利点がある。

Chianerには(3)の機能がVariableというクラスで実装されているので、最適化に活用するため仕様についてまとめた。
また、最急降下法のPythonでの実装については、過去記事をご参考まで。
直線探索を使った最急降下法をPythonで実装

環境

ソフトウェア バージョン
Spyder 3.3.3
Python 3.7.3
NumPy 1.16.2
Chainer 6.3.0

以下では、各ライブラリを以下のようにインポートしていることを前提とする。

1
2
3
import numpy as np
import chainer
from chainer import Variable

Variableクラス

ChainerのVariableクラスは、数値の配列データを保持するクラスである。
NumPy配列に近い感覚で扱えるが、相違点もあるので注意。

Variableオブジェクトは、32ビット精度のNumPy配列を使って作成される。

例:

1
2
>>> x0 = Variable(np.array([0, 1], dtype=np.float32))
>>> x1 = Variable(np.array([[0, 1, 2], [3, 4, 5]], dtype=np.float32))

生成されたオブジェクトは、Variable型の配列である。

1
2
3
4
5
>>> x0
variable([0., 1.])
>>> x1
variable([[0., 1., 2.],
          [3., 4., 5.]])

Variableオブジェクトでは、同じサイズの配列同士の演算ができる。
(配列のサイズが異なる場合、NumPyではブロードキャストしてくれるが、Variableオブジェクトはエラーを返す)

1
2
3
4
>>> x0 = Variable(np.array([0, 1], dtype=np.float32))
>>> x2 = Variable(np.array([2, 3], dtype=np.float32))
>>> x0+x2
variable([2., 4.])

また、配列のインデックスを指定して、要素を取り出すことも可能である。

1
2
3
4
5
>>> 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配列を取得できる。

1
2
3
4
5
>>> 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 ]^\mathrm{T} $$

この関数の勾配ベクトルは次式で与えられる。

$$ \nabla f(\boldsymbol{x}) = \left[ \frac{\partial f}{\partial x_0}, \frac{\partial f}{\partial x_1} \right]^\mathrm{T} = [4x_0 + 2, 2x_1 + 1 ]^\mathrm{T} $$

点$ (x_0, x_1)=(1, 2)$において、関数値と勾配ベクトルはそれぞれ以下のようになる。

$ f(\boldsymbol{x})=10 $
$ \displaystyle \nabla f(\boldsymbol{x}) = [6, 5 ]^\mathrm{T} $

上記の関数をVariableを使って記述すると以下のようになる。

1
2
x = Variable(np.array([1,2], dtype=np.float32))
y = 2*x[0]**2 + x[1]**2 + 2*x[0] + x[1]

関数の値(10)は既に得られている。

1
2
>>> y
variable(10.)

この段階では、勾配はまだ得られておらず、勾配を取得するためには以下を実行する。

1
2
>>> y.grad = np.ones_like(y.array, dtype=np.float32)
>>> y.backward()

まず、ygradに、値は何でも良いので配列を設定する。
np.ones_likeは、引数の配列と同じサイズで、要素が全て1の配列を返す関数である。
(ただし、厳密にはgradに初期値を与える必要があるのは、yが2つ以上の要素を持つ配列の場合である。
今回の例のようにyがスカラーの場合は、初期値の設定は省略できる)

次に、y.backward()メソッドを実行すると、自動微分が実行され、x.gradに勾配が格納される。

1
2
>>> x.grad
array([6., 5.], dtype=float32)

中間変数の勾配を求める場合

上記の方法では、以下のようにVariableオブジェクトの演算を2回以上重ねた場合に、中間の変数の勾配が保存されない。

1
2
3
4
5
6
7
8
>>> 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とする。

1
2
3
4
5
6
7
8
>>> 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階微分を求めるためには、以下のようにする。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
>>> 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)

目的関数ybackwardを実行するまでは同じであるが、
その後にx.grad_varを取得する。
次に、cleargradメソッドでxの勾配を削除したのち、
x.grad_varを格納した変数に対して、backwardを実行すると、
x.gradに2階微分が格納される。

参考リンク

Variables and Derivatives — Chainer 7.7.0 documentation
chainer.Variable — Chainer 7.7.0 documentation
ChainerのVariableを使って自動微分を簡単実装 | 自調自考の旅

シェアする

Helve
WRITTEN BY
Helve
関西在住、電機メーカ勤務のエンジニア。X(旧Twitter)で新着記事を配信中です

サイト内検索