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

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

追記:

本記事はChainer 1.4以前の物になります。

現在の仕様とは大きく異なるので、参考程度にとどめてください。

それより新しいものを書かれた方もいらっしゃるようなのでそちらも参考にしてください。

i101330.hatenablog.com

    • -

私的な理由により和訳を求められているので雑に必要そうなところだけ和訳します。

英語苦手ですし、奇麗に整形する気もないので、和訳の内容は保証しません。

また、指摘は大歓迎です。是非おしえてください。

2を書いたらリンク張ります。

注) Chainerではnumpyというライブラリを頻繁に使うのでそちらも参照

Introduction to Chainer

これはChainer tutorialの1章です。この章では

  • なんでChainerを作ったか
  • 簡単な順伝播、逆伝播計算
  • パラメータを持つ関数の使い方とその勾配計算法
  • model
  • パラメーター調整

をやります。

が分かるでしょう。

Chainerの中心的コンセプト

Chainerは柔軟なニューラルネットワークフレームワークです。一つの大きな目的は柔軟性です。なので複雑な構造をシンプルに、直感的に書けることが必要です。

そのために、Chainerでは既存の多くのフレームワークと異なり定義してから実行ではなくて、定義とともに実行(Define by Run)する方法を取ります。

Note
このチュートリアル中のサンプルコードは以下のimportが済んでいる事が前提です。

import numpy as np
from chainer import cuda, Function, FunctionSet, gradient_check, Variable, optimizers
import chainer.functions as F

順伝播/逆伝播計算

"Define by Run" なので、Chainerでは順伝播計算はネットワークの定義そのものです。まず、順伝播計算を始めるために、入力配列をVariableオブジェクトに変換しましょう。以下で簡単な1変数の ndarray(numpyの配列オブジェクト)から始めましょう。

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

現在Chainerは32bit floatしかサポートしていません。

Variableオブジェクトは簡単な計算が可能です。例えば、

{ \displaystyle
y = x^2 - 2x +1
}

は、

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

のように書けます。

結果のyもVariableオブジェクトで、値にはdata属性でアクセスできます。

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

yは結果の値だけを持っている訳じゃありません。yは計算の記録も持っているので、これの微分も計算できます。backword()メソッドを呼ぶと計算できます。

>>> y.backward()

これによって、誤差逆伝播が実行されます。その時、勾配はxのgrad属性に入ります。

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

もちろん、仲介的な変数の勾配も計算できます。Chainerにおいては、メモリ効率の観点から標準で仲介的な変数の勾配配列を開放するようにしています。その代わり前の勾配情報は、retain_grad引数に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)
Note

沢山のVariableオブジェクトを返す関数がfunctionモジュールに定義されています。

実際に計算する複雑な関数のために、それらを組み合わせて使えば自動で逆伝播が
計算できます。

パラメータを持つ関数(各位訳語つけてくれ!)

ニューラルネットワークを書くのに、パラメータを持つ関数を使い、そのパラメータを調整する必要があります。前述のとおり、functionsモジュールで既に定義されている関数の中にはパラメータを持つ関数が含まれています。

基本的なパラメータを持つ関数の一つはLinear関数(別名全結合層、アフィン変換)です。この関数は  f(x) = Wx + b で表され、行列  W とベクトル  b がパラメータになります。この線形関数は三次元から二次元への関数として定義され、

>>> f = F.Linear(3, 2)
Note

ほとんどの関数はミニバッチ入力しか受け付けていません。入力の一つ目の次元を
バッチの次元として考えます。線形関数の場合では、入力は必ず(N, 3)次元の形を
していなければいけません。Nはミニバッチのサイズです。

線形関数のパラメータはwとa属性に格納されています。デフォルトでは、行列Wはランダムに初期化され、ベクトルbは0で初期化されます。

>>> f.W
array([[ 1.33545339, -0.01839679,  0.7662735 ],
       [-1.21562171, -0.44784674, -0.07128379]], dtype=float32)
>>> f.b
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.5974803 , -2.3251667 ],
       [ 9.84747124, -7.52942371]], dtype=float32)

パラメータの勾配はbackword()メソッドで計算されます。一つ気をつけるのは、この勾配は上書きされるわけではなく、加算されていきます。なので改めて計算する前に必ず勾配を0にしなければいけません。線形関数の勾配はgWとgb属性にあるので、

>>> f.gW.fill(0)
>>> f.gb.fill(0)

のように0をセットします。

Note

この処理はFunctionSetとOptimizerで簡略化されます。次のセクションをみて
ください。

今、勾配は単純にbackwordメソッドを呼ぶ事で以下のように計算できます。

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

FunctionSet

ほとんどのニューラルネットワークは多変数のパラメータを持つ関数を含みます。FunctionSetを使うと、それらを簡単に管理できるようになります。このクラスはキーワード引数で初期化された属性を持つ簡単なオブジェクトのように振る舞います。

>>> model = FunctionSet(
...     l1 = F.Linear(4, 3),
...     l2 = F.Linear(3, 2),
... )
>>> model.l1
<chainer.functions.linear.Linear object at 0x7f7f03e4f350>
>>> model.l2
<chainer.functions.linear.Linear object at 0x7f7f03e4f590>

さらに、後から属性をセットする事で追加する事もできます。

>>> model.l3 = F.Linear(2, 2)

modelはまさにこれらの属性を格納する関数を持つオブジェクトで、順伝播計算の中でこれらの関数が使えます。

>>> x = Variable(np.array([[1, 2, 3, 4], [5, 6, 7, 8]], dtype=np.float32))
>>> h1 = model.l1(x)
>>> h2 = model.l2(h1)
>>> h3 = model.l3(h2)

FunctionSetはパラメータと勾配を集める特徴を持っています。すべてのパラメータと勾配のタプルがFunctionSet.parametersとFunctionSet.gradientsプロパティに抽出されています。

Optimizer

このセクションで説明するOptimizerは最後のChainerの主な機能です。Optimizerはパラメータと勾配のタプルを算出する数値最適化アルゴリズムを実行します。沢山のアルゴリズムがoptimizersモジュールに実装されています。以下に単純なアルゴリズムの一つであるStochastic Gradient Descentを示します。

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

FunctionSetを使って用意したパラメータと勾配を最適化するために、setup()メソッドに指定します。

Note(ちょっと英語分からなかった)

Optimizerは渡された値が実体かどうか区別できないので、一度Optimizerに参照を
渡すと勝手に値は変更され、その後はOptimizerが変更するパラメータと勾配を
すべての順伝播/逆伝播計算を通じて使う事になります。

最適化を実行すると、まず勾配を計算する必要があります。0で初期化するには簡単にzero_grads()メソッドが使えます。

>>> optimizer.zero_grads()

一つ前のセクションでは手動で0にしました。この行では等価で簡単な勾配の初期化ができます。

勾配を計算した次に、update()メソッドを1回最適化イテレーションをまわすために実行します。

>>> (compute gradient)
>>> optimizer.update()

Optimizerはさらにパラメータと勾配の操作に関係したいくつかの機能を備えています。たとえば、weight decayやgradient clippingなど。

Example: Multi-layer Perceptron on MNIST

まだ!