読者です 読者をやめる 読者になる 読者になる

オモンパカリスト

深層学習、計算論的神経科学に興味あります

Chainerチュートリアル(ver. 1.5.1)を和訳

Deep Learning Chainer

Chainerがversionを1.5以上にしてから随分変わってて、インストール方法や記述も違うから 正直アップデートせずに見送ってたけど、ここらでしっかり公式チュートリアルを読んで理解すべく和訳に取り組みました。

英語が苦手なので不十分かつ訳注も明示していないので至らないとこだらけです。 間違いなどありましたら是非指摘していただけるとうれしいです。

Chainerチュートリアルを和訳する必要があったからかいてみた(1)

そもそもこちらの記事を参考にさせていただいてたのですが、verUPして内容が古くなってしまったので勝手ながら最新のver. 1.5.1版を書き起こします。


Introduction to Chainer 翻訳

これはChainerチュートリアルの第1章です。 この章でやることは

です。この章を読んだ後は、

ができるようになります。

芯となるコンセプト

Chainerは柔軟なニューラルネットワークフレームワークです。 主要な目標は柔軟性で、複雑な構造を簡単かつ直感的に書くことができます。

ほとんどの既存の深層学習フレームワークは"Define-and-Run"定義がベースになっています。 (すっ飛ばします)

注意: この先の例となるコードはすでに以下のようにモジュールのimport前提です。
import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, Variable, optimizers, serializers, utils
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L

順伝播・逆伝播計算

上記のように、Chainerは"Define-by-Run"定義を用いています。順伝播はネットワークの定義になります。 順伝播計算をはじめるために、入力配列をVariableオブジェクトに変換します。それでは1元のndarrayでやってみましょう。

>>> x_data = np.array([5], dtype=np.float32)
>>> x = Variable(x_data)
警告:Chainerは計算に今のところ32bitのfloat型しかサポートしてません。

Variableオブジェクトは簡単な算術計算が可能です。

y = x^2 - 2x + 1

はつぎのように書きます:

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

結果のyもまたVariableオブジェクトになります。data属性にアクセスして値を取得できます。

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

yは結果の値を保持しているだけではありません。計算記録(または計算グラフ)も持っており、微分の計算も可能にします。backward()メソッドを呼び出すことで実行します。

>>> y.backward()

これで誤差逆伝播が実行されます。計算された勾配は入力の変数xのgrad属性に保存されます。

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

中間的な変数もまた勾配を計算できます。Chainerでは、標準ではメモリ効率のために中間的な変数の配列の勾配を解放しています。 その勾配の情報を保存するために、retain_grad引数をbackwardメソッドにTrueで渡します。

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

これらすべての計算は多要素配列に一般化できます。もし多要素配列の変数をすべて勾配計算したい場合、手動で初期誤差を設定する必要があります。これは出力変数のgrad属性を設定することで簡単に実現できます。

>>> 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)
注意: Variableオブジェクトを返すたくさんの関数がfunctionsモジュールに定義されています。
それらを組み合わせて逆伝播計算を自動で求められます。

Links

ニューラルネットワークを書くために、パラメータ付き関数を組み合わせてパラメータを最適化する必要があります。そのときlinksを用います。 Linkはパラメータを保持するオブジェクトです。 もっとも基本的なものはそのパラメータによっていくつかの引数を交換しながら通常の関数のように振る舞うlinksです。 よりハイレベルのlinksも導入されていますが、ここではパラメータを持つ関数のようなlinksのみ扱います。

注意: ver1.4から対応されてます。

最もよくつかわれるlinksのうちのひとつはLinearリンクです。 f(x) = Wx + bのような算術関数を表現し、行列Wとベクトルbがパラメータです。三次元空間から二次元空間へ写像する線形関数はこのように定義します。

>>> f = F.Linear(3, 2)
注意: ほとんどの関数はミニバッチ入力しか受け入れません。
ここで、入力配列の最初の次元はバッチの次元として考えます。
線形関数の場合、入力は(N, 3)のようにNはミニバッチのサイズでなければなりません。

linkのパラメータは属性として格納されます。それぞれのパラメータはVariable型のインスタンスとして生成されます。線形関数の場合、2つのパラメータWとbが格納されます。標準では、行列Wはランダムで初期化され、ベクトルbはゼロで初期化されます。

>>> f.W.data
array([[ 1.01847613,  0.23103087,  0.56507462],
       [ 1.29378033,  1.07823515, -0.56423163]], dtype=float32)
>>> f.b.data
array([ 0.,  0.], dtype=float32)

このインスタンスは通常の関数のように動作します:

>>> x = Variable(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
>>> y = f(x)
>>> y.data
array([[ 3.1757617 ,  1.75755572],
       [ 8.61950684,  7.18090773]], dtype=float32)

パラメータの勾配はbackward()メソッドにより計算されます。勾配は上書きされるのではなく 加算 されます。 最初に新しく計算するときに勾配を初期化しなければなりません。その際はzerograd()メソッドを呼ぶことになります。

>>> f.zerograds()

そしてbackwardメソッドを呼ぶことでパラメータの勾配を計算できます。

>>> y.grad = np.ones((2, 2), dtype=np.float32)
>>> y.backward()
>>> f.W.grad
array([[ 5.,  7.,  9.],
       [ 5.,  7.,  9.]], dtype=float32)
>>> f.b.grad
array([ 2.,  2.], dtype=float32)

Write a model as a chain

ほとんどのニューラルネットワーク構造は複数のlinkを含みます。例えば多層パーセプトロン複数の線形層で構成されます。Chainerでは複数のlinkのおまとめ管理を次のように書きます:

>>> l1 = L.Linear(4, 3)
>>> l2 = L.Linear(3, 2)
>>> def my_forward(x):
...     h = l1(x)
...     return l2(h)

ここでLchainer.linksモジュールを示します。でもこのように定義されちゃうと再利用が難しいです。 Pythonぽくクラスにプロシージャとリンクを組み合わせてかくとこうなります。

>>> class MyProc(object):
...     def __init__(self):
...         self.l1 = L.Linear(4, 3)
...         self.l2 = L.Linear(3, 2)
...
...     def forward(self, x):
...         h = self.l1(x)
...         return self.l2(h)

より再利用できるように、パラメータの管理、CPU/GPUマイグレーション、堅牢かつ柔軟なセーブ/ロード機能(v1.5以降)などをサポートしてるChainerのChainクラスから継承します。そのときの定義はこう書きます:

>>> class MyChain(Chain):
...     def __init__(self):
...         super(MyChain, self).__init__(
...             l1=L.Linear(4, 3),
...             l2=L.Linear(3, 2),
...         )
...
...     def __call__(self, x):
...         h = self.l1(x)
...         return self.l2(h)
注意: pythonに習って__call__を定義してます。
このようにlinksもchainsも通常の変数の関数のように振る舞い呼び出せます。

シンプルなlinksで構成された複雑なchainの方法を示しています。 l1l2のようなlinksはMyChainの子linksと呼ばれます。Chain自体はLinkを継承しています。つまり子linksとしてMyChainオブジェクトを保持するもっと複雑なchainsを定義できます。

他のchainの定義の方法はChainListクラスを使うことです。このようにlinksのリストのように振る舞います。

>>> class MyChain2(ChainList):
...     def __init__(self):
...         super(MyChain2, self).__init__(
...             L.Linear(4, 3),
...             L.Linear(3, 2),
...         )
...
...     def __call__(self, x):
...         h = self[0](x)
...         return self[1](h)

ChainListはlinksに任意の番号をつけるときに便利です。もしlinksの番号がこのように固定されている場合、Chainクラスは基本クラスとして推奨されます。

Optimizer

パラメーターが良い値になるように、Optimizerクラスで最適化を図ります。これは与えられたlinkの数値最適化アルゴリズムを実行します。 たくさんのアルゴリズムoptimizersモジュールに実装されています(SGDとかAdamとか)。ここではSGD(確率的勾配降下法)と呼ばれる最もシンプルなアルゴリズムのうちのひとつを見てみましょう:

>>> model = MyChain()
>>> optimizer = optimizers.SGD()
>>> optimizer.setup(model)

このsetup()はlinkに与えられる最適化を準備します。

最適化実行には二つの方法があります。ひとつはupdate()メソッド(引数なし)で呼び出し手動で勾配計算をすることです。 予め勾配はリセットすることを忘れないでください!

>>> model.zerograds()
>>> # compute gradient here...
>>> optimizer.update()

もうひとつの方法はupdate()メソッドにloss関数を渡します。このケースではzerogradsは更新メソッドで自動的に呼び出されるので、リセットを手動でする必要はありません。

>>> def lossfun(args...):
...     ...
...     return loss
>>> optimizer.update(lossfun, args...)

いくつかのパラメータ・勾配の操作にweight decayやgradient clippingなどが使用できます。そういったhook関数は実際の更新の前にupdate()メソッドにより呼び出されます。 例えばweight decay正則化は次のように予め実行します:

>>> optimizer.add_hook(chainer.optimizer.WeightDecay(0.0005))

もちろん、あなた自身でhook関数を書くこともできますよ。そのためには引数としてoptimizerを扱う関数または呼び出し可能オブジェクトである必要がありますけどね。

Serializer

このページでご紹介する最後の芯はserializerです。Serializerはシンプルなシリアライズまたはデシリアライズインターフェイスのオブジェクトです(シリアライズってのは他で扱えるようなデータ変換みたいな意味)。LinkOptimizerシリアライズを支援します。

sirializersモジュールでそれらが定義されいます。今のところ、HDF5フォーマットです。

linkオブジェクトをHDF5ファイルに変換するにはserializers.save_hdf5()を使います:

>>> serializers.save_hdf5('my.model', model)

modelのパラメータをファイル名'my.model'でHDF5ファイルとして保存します。保存されたmodelはserializers.load_hdf5()で読みこめます:

>>> serializers.load_hdf5('my.model', model)
注意: パラメータと永続的な値はこれらによってシリアライズされます。他の属性は自動的には保存されません。
配列、スカラ、または他の永続的な値としてシリアライズ可能のオブジェクトはLink.add_persistent()メソッドで登録できます。
登録された値はadd_persistentメソッドに属性名を渡すことでアクセスできます。

optimizerの状態もまた同じ関数で保存できます:

>>> serializers.save_hdf5('my.state', optimizer)
>>> serializers.load_hdf5('my.state', optimizer)
(英語よくわからんかった)
注意: optimizerのシリアライズは最適化(SGD等)の運動量ベクトルを繰り返し回数を内部状態で保存するだけです。
パラメータやlinkを対象にした永続的な値は保存できません。
保存された状態から最適化を再開するために明示的にoptimizerの対象となるlinkを保存する必要が有ります。

あとはMNISTの多層パーセプトロンの例。 また今度。