メインコンテンツへスキップ

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

·2221 文字·5 分
目次

はじめに
#

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

Chainerと最適化
#

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

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

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

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

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

直線探索を使った最急降下法を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()

まず、ygradに値は何でも良いので配列を設定する。 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)

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

参考リンク
#

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

関連記事

ChainerのIteratorクラスによる学習用ミニバッチ作成
·2129 文字·5 分
データセットから学習用ミニバッチを作成してくれるIteratorクラスの動作を確認する。
Chainer入門 最小限のニューラルネットワーク実装
·1600 文字·4 分
ディープラーニング用のライブラリChainerの使い方を理解するため、ChainerのChainクラスとOptimizerを使って最小限のニューラルネットワーク (NN) を実装する。
SciPyを使ったFIRフィルタによる波形整形
·1247 文字·3 分
SciPyを使って、FIR (Finite Impulse Response, 有限インパルス応答) フィルタによる離散信号の波形を整形する。ローパス、ハイパス、バンドパス、バンドエリミネイトの各フィルタの設計から、信号への適用まで行う。
NumPyを使った高速フーリエ変換による周波数解析
·1276 文字·3 分
NumPyのfftパッケージを使って、FFT (Fast Fourier Transform, 高速フーリエ変換) による離散信号の周波数解析を行い、信号の振幅を求める。
Matplotlibのオブジェクト指向なカラーバーの表示
·744 文字·2 分
matplotlibライブラリで作成したヒートマップや等高線図のカラーバーを、オブジェクト指向スタイルで調整する。
Matplotlibでオブジェクト指向なグラフ作成
·1842 文字·4 分
matplotlibライブラリを用いてオブジェクト指向スタイルでグラフを作成する。