Chainerの紹介

これはChainerチュートリアルの最初のセクションです。このセクションでは、次のことについて学習します。

  • 既存のフレームワークの長所と短所、なぜChainerを開発しているのか

  • 順伝搬と逆伝搬計算の簡単な例

  • Linksの使用法とその勾配計算

  • チェーン(ほとんどのフレームワークで “モデル” として知られる)の構築

  • パラメータの最適化

  • linksとoptimizersのシリアル化

このセクションを読んだら、次のことができるようになります:

  • いくつかの算術の勾配を計算する

  • Chainerで多層パーセプトロンを書く

コアコンセプト

フロントページで述べたように、Chainerはニューラルネットワークのための柔軟なフレームワークです。1つの主要な目標は柔軟性です。したがって、複雑なアーキテクチャを簡単かつ直感的に記述することができなければなりません。

既存のディープラーニングフレームワークのほとんどは、 “Define-and-Run” スキームに基づいています。つまり、まずネットワークが定義され、固定された後、定期的にミニバッチが送られます。ネットワークは順伝搬/逆伝搬計算の前に静的に定義されるため、すべてのロジックを データ としてネットワークアーキテクチャに組み込む必要があります。したがって、そのようなシステム(例えば、Caffe)でネットワークアーキテクチャを定義することは、宣言的アプローチに従っています。命令型言語(torch.nn、Theanoベースのフレームワーク、TensorFlowなど)を使用して、このような静的ネットワーク定義を生成することができることに注意してください。

対照的に、Chainerは、 “Define-by-Run” スキームを採用しています。すなわち、ネットワークは、実際の順伝搬計算の実行中に定義されます。より正確に言うと、Chainerはプログラミングロジックの代わりに計算履歴を保存します。この戦略により、Pythonのプログラミングロジックの能力を最大限に引き出すことができます。例えば、Chainerはネットワーク定義に条件とループを導入するための魔法を必要としません。Define-by-RunスキームはChainerのコアコンセプトです。このチュートリアルでは、ネットワークを動的に定義する方法を示します。

この戦略は、ロジックがネットワーク操作に近づくので、マルチGPUの並列化を簡単に書くこともできます。このチュートリアルの後半では、このような機能についてレビューします。

注釈

このチュートリアルのサンプルコードでは、簡単にするために、以下のモジュールが既にインポートされているものと想定しています。

import numpy as np
import chainer
from chainer import cuda, Function, gradient_check, report, training, utils, Variable
from chainer import datasets, iterators, optimizers, serializers
from chainer import Link, Chain, ChainList
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions

これらのimportは、Chainerのコードと例に広く示されています。簡単にするために、このチュートリアルではこれらのimportを省略しています。

順伝搬と逆伝搬計算

前述のように、Chainerは “Define-by-Run”スキームを採用しているため、順伝搬計算自体がネットワークを定義します。順伝搬計算を開始するには、入力配列を Variable オブジェクトにセットする必要があります。ここでは要素が1つのみの単純な ndarray から始めます。

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

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()

これは誤差逆伝搬法( 逆伝播法リバースモード自動微分 として知られる)を実行しています。次に、勾配が計算され、入力変数 xgrad 属性に格納されます。

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

また中間変数の勾配を計算することもできます。Chainerは、デフォルトではメモリ効率のために中間変数の勾配配列を解放することに注意してください。勾配情報を保持するためには、retain_grad 引数をbackwardメソッドに渡して下さい。

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

これらすべての計算は簡単に多次元配列へ一般化されます。多次元配列を保持する変数から逆伝播計算を開始したい場合、initial error を手動で設定する必要があることに注意してください。initial error* の設定は出力変数の 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 モジュールで定義されています。自動逆伝播計算で複雑な関数を実現するためにそれらを結合することができます。

チェーンモデルを書く

ほとんどのニューラルネットワークのアーキテクチャは、複数のLinkを含みます。例えば、多層パーセプトロンは、複数の線形層から構成されます。このように複数のLinkを組み合わせることで、パラメータで複雑な手続きを書くことができます:

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

Llinks モジュールを示しています。このように定義されたパラメータを持つ手続きは、再利用するのが難しいですよりPythonらしい方法は、Linkと手順をクラスにまとめることです。:

>>> 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移行サポート、堅牢で柔軟な保存/読み込み機能などをサポートしたいと考えています。これらの機能はすべてChainerの Chain クラスでサポートされています。Chainクラスの機能を利用するためには、上記のクラスを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)

注釈

私たちはしばしば、 __call__ 演算子によってLinkの単一のforwardメソッドを定義します。そのようなLinkとChainは呼び出し可能であり、Variablesの通常の機能のように動作します。

これはより単純なLinkによって複雑なChainがどのように構築されるかを示しています。l1l2 のようなLinkはMyChainの 子Link と呼ばれます。 Chain自体はLinkを継承することに注意してください。 つまり、MyChainオブジェクトを子Linkとして保持する、より複雑なChainを定義できますことを意味します。

Chainを定義する別の方法は、Linkリストのように動作する ChainList クラスを使用することです。

>>> 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は、任意の数のLinkを使用するときに便利です。上記のようにLink数を固定する場合、基本クラスはChainクラスを推奨します。

Optimizer

パラメータの値を適切に取得するには、 Optimizer クラスで最適化する必要があります。それはLinkを与えられた数値最適化アルゴリズムを実行します。多くのアルゴリズムが optimizers モジュールに実装されています。ここでは、確率的勾配降下法(SGD)と呼ばれる最も単純なものを使用します。

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

use_cleargrads() メソッドは効率を上げるためのメソッドです。詳細については、 use_cleargrads() を参照してください。

setup() メソッドは、与えられたLinkの最適化の準備をします。

いくつかのパラメータ/勾配の操作(Weight DecayやGradient Clipping)は、Optimizerに フック関数 をセットすることで実行できます。フック関数は、勾配計算の後と、パラメータの実際の更新の直前に呼び出されます。たとえば、事前に次の行を実行することによって、Weight Decayによる正則化をセットできます。

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

もちろん、独自のフック関数を記述することができます。Optimizerを引数とする関数またはcallableオブジェクトでなければなりません。

Optimizerを使用するには、2つの方法があります。1つは Trainer を使用しています。これについては、次のセクションで説明します。別の方法は、それを直接使用することです。私たちはここで後者のケースをレビューします。Optimizerを簡単な方法で使用できるようになることに興味があれば、このセクションをスキップして次のセクションに進んで下さい。

Optimizerを直接使用する方法は2つあります。1つは手動で勾配を計算し、次に引数なしで update() メソッドを呼び出す方法です。あらかじめ勾配をクリアすることは忘れないでください!

>>> x = np.random.uniform(-1, 1, (2, 4)).astype('f')
>>> model.cleargrads()
>>> # compute gradient here...
>>> loss = F.sum(model(chainer.Variable(x)))
>>> loss.backward()
>>> optimizer.update()

もう一つの方法は、loss関数を update() メソッドに渡すことです。この場合、 cleargrads() はupdateメソッドによって自動的に呼び出されるため、ユーザーは手動で呼び出す必要はありません。

>>> def lossfun(arg1, arg2):
...     # calculate loss
...     loss = F.sum(model(arg1 - arg2))
...     return loss
>>> arg1 = np.random.uniform(-1, 1, (2, 4)).astype('f')
>>> arg2 = np.random.uniform(-1, 1, (2, 4)).astype('f')
>>> optimizer.update(lossfun, chainer.Variable(arg1), chainer.Variable(arg2))

完全な仕様については、 Optimizer.update() を参照してください。

Trainer

ニューラルネットワークを訓練したいときは、パラメータを更新するために何度も 訓練ループ を実行する必要があります。典型的な訓練ループは、以下の手順からなります。:

  1. 訓練データセットのイテレーション

  2. 抽出されたミニバッチの前処理

  3. ニューラルネットワークのForward/backward計算

  4. パラメータの更新

  5. 検証データセットの現在のパラメータの評価

  6. 中間結果のロギングと表示

Chainerは、訓練プロセスを簡単に書くためのシンプルで強力な方法を提供します。訓練ループを抽象化すると、主に次の2つのコンポーネントで構成されます。

  • Dataset abstraction。上記のリストの1と2が実装されています。コアコンポーネントは、 dataset モジュールで定義されています。また、 datasetsiterators モジュールには、それぞれデータセットとイテレータの実装が多数あります。

  • Trainer。上記のリストの3,4,5、および6を実装しています。全体の手順は Trainer によって実装されています。パラメータ(3と4)を更新する方法は、自由にカスタマイズできる Updater によって定義されています。5と6は Extension のインスタンスによって実装され、訓練ループにさらに手順を追加します。ユーザーはExtensionを追加することで訓練の手順を自由にカスタマイズできます。ユーザーは独自のExtensionを実装することもできます。

以下の例のセクションで、Trainerの使い方を見ていきます。

Serializer

最初の例に進む前に、このページで説明されている最後のコア機能であるSerializerを紹介します。Serializerは、オブジェクトをシリアライズまたはデシリアライズするためのシンプルなインターフェイスです。LinkOptimizer 、および Trainer はシリアライゼーションをサポートしています。

具体的なSerializerは、 serializers モジュールで定義されています。これはNumPy NPZとHDF5フォーマットをサポートしています。

例えば、 serializers.save_npz() 関数でLinkオブジェクトをNPZファイルにシリアライズすることができます。:

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

model のパラメータをNPZ形式のファイル 'my.model' に保存します。保存されたモデルは、 serializers.load_npz() 関数で読み取ることができます。

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

注釈

パラメータと 永続的な値 のみがこれらのシリアライゼーションコードによってシリアライズされることに注意してください。他の属性は自動的には保存されません。Link.add_persistent() メソッドを使用すると、配列、スカラー、またはシリアライズ可能なオブジェクトを永続的な値として登録できます。登録された値は、add_persistentメソッドに渡される名前の属性によってアクセスできます。

Optimizerの状態は、同じ関数で保存することもできます。

>>> serializers.save_npz('my.state', optimizer)
>>> serializers.load_npz('my.state', optimizer)

注釈

Optimizerのシリアライゼーションは、イテレーションの回数、MomentumSGDのモメンタムベクトルなどの内部状態のみを保存することに注意してください。対象のLinkのパラメータと永続的な値は保存されません。保存された状態から最適化を再開するために、Optimizerで対象のLinkを明示的に保存する必要があります。

h5pyパッケージがインストールされている場合、HDF5形式のサポートが有効になります。HDF5形式のシリアライゼーションとデシリアライズは、NPZ形式のものとほぼ同じです。 save_npz()load_npz()save_hdf5()load_hdf5() でそれぞれ置き換えてください。

例:MNISTの多層パーセプトロン

これまでの概念を使って多層パーセプトロンを使用して、多クラス分類問題を解くことができます。ここでは機械学習の “hello world” の定番の1つである MNIST という手書き数字データセットを使用します。このMNISTの例は、公式リポジトリの examples/mnist ディレクトリにもありますこのセクションでは、 Trainer を使用して訓練ループを構築および実行する方法を示します。

まず、MNISTデータセットを準備する必要があります。MNISTデータセットは、サイズ28×28(784)ピクセルの70,000枚のグレースケール画像から構成され、それぞれに数字のラベルが対応します。データセットは、デフォルトで60,000枚の訓練画像と10,000枚のテスト画像に分かれています。datasets.get_mnist() によってベクトル化されたバージョン(784次元ベクトル)のデータセットを得ることができる。

>>> train, test = datasets.get_mnist()
...

このコードは自動的にMNISTデータセットをダウンロードし、NumPy配列を $(HOME)/.chainer ディレクトリに保存します。返り値の train と test は、画像とラベルのペアのリストとして見ることができます(厳密に言えば、 TupleDataset のインスタンスです)。

また、これらのデータセットを反復処理する方法も定義する必要があります。訓練データセットをあらゆる epoch 毎に、すなわちデータセットのスイープ開始時にシャッフルしたいです。この場合、 iterators.SerialIterator を使用できます。

>>> train_iter = iterators.SerialIterator(train, batch_size=100, shuffle=True)

一方、テストデータセットをシャッフルする必要はありません。この場合、シャッフルを無効にするために、 shuffle=False 引数を渡すことができます。基本となるデータセットが高速なスライスをサポートしている場合、反復処理が早くなります。

>>> test_iter = iterators.SerialIterator(test, batch_size=100, repeat=False, shuffle=False)

repeat=False も渡しましたが、これはすべてのサンプルを見たときに反復を停止することを意味します。このオプションは通常、test/validationデータセットが必要です。このオプションがなければ、繰り返しは無限ループに入ります。

次に、アーキテクチャを定義します。例として一層あたり100個のユニットを持つシンプルな3層正規化ネットワークを使用します。

>>> class MLP(Chain):
...     def __init__(self, n_units, n_out):
...         super(MLP, self).__init__(
...             # the size of the inputs to each layer will be inferred
...             l1=L.Linear(None, n_units),  # n_in -> n_units
...             l2=L.Linear(None, n_units),  # n_units -> n_units
...             l3=L.Linear(None, n_out),    # n_units -> n_out
...         )
...
...     def __call__(self, x):
...         h1 = F.relu(self.l1(x))
...         h2 = F.relu(self.l2(h1))
...         y = self.l3(h2)
...         return y

このLinkは、活性化関数として relu() を使用します。'l3' Linkは出力が10個の数字のスコアに対応する最終的な線形レイヤーであることに注意してください。

損失値を計算したり、予測の精度を評価するために、上記のMLP Chainの上にClassifier Chainを定義します。

>>> class Classifier(Chain):
...     def __init__(self, predictor):
...         super(Classifier, self).__init__(predictor=predictor)
...
...     def __call__(self, x, t):
...         y = self.predictor(x)
...         loss = F.softmax_cross_entropy(y, t)
...         accuracy = F.accuracy(y, t)
...         report({'loss': loss, 'accuracy': accuracy}, self)
...         return loss

このClassifierクラスは、精度と損失を計算し、損失値を返します。引数 xt のペアは、データセット(画像とラベルのタプル)のそれぞれの例に対応します。softmax_cross_entropy() は、与えられた予測値とGround Truthラベルの損失値を計算します。accuracy() は予測精度を計算します。Classifierのインスタンスには任意の予測Linkをセットすることができます。

report() 関数は、損失と精度の値をTrainerに報告します。訓練統計を収集する詳細なメカニズムについては、 Reporter を参照してください。同様の方法で活性化に関する統計のような他のタイプの観測を収集することもできます。

上記のClassifierに似たクラスが chainer.links.Classifier として定義されていることに注意してください。したがって、上記の例を使用する代わりに、この定義済みのClassifier Chainを使用します。

>>> model = L.Classifier(MLP(100, 10))  # the input size, 784, is inferred
>>> optimizer = optimizers.SGD()
>>> optimizer.setup(model)

これでTrainerオブジェクトを作ることができます。

>>> updater = training.StandardUpdater(train_iter, optimizer)
>>> trainer = training.Trainer(updater, (20, 'epoch'), out='result')

第2引数 (20, 'epoch') は訓練の期間を表します。epoch または iteration のいずれかをユニットとして使用できます。この場合、訓練セットを20回反復することにより、多層パーセプトロンを訓練します。

訓練ループを呼び出すために、 run() メソッドを呼び出します。

>>> trainer.run()

このメソッドは訓練シーケンス全体を実行します。

上記のコードはパラメータを最適化するだけです。ほとんどの場合、 run メソッドを呼び出す前に挿入されたextensionを使用できる訓練の進行状況を確認したいと考えています。

>>> trainer.extend(extensions.Evaluator(test_iter, model))
>>> trainer.extend(extensions.LogReport())
>>> trainer.extend(extensions.PrintReport(['epoch', 'main/accuracy', 'validation/main/accuracy']))
>>> trainer.extend(extensions.ProgressBar())
>>> trainer.run()  

これらの拡張機能は、次のタスクを実行します。

Evaluator

各エポックの終わりにテストデータセットに対する現在のモデルを評価します。

LogReport

報告された値を累積し、出力ディレクトリにログファイルに書き出します。

PrintReport

選択した項目をLogReportにプリントします。

ProgressBar

プログレスバー(進捗状況)を表示します。

chainer.training.extensions モジュールには多くの拡張機能が実装されています。上記に含まれていない最も重要な拡張機能は snapshot() です。これは訓練手順のスナップショット(つまりTrainerオブジェクト)を出力ディレクトリのファイルに保存する。

examples/mnist ディレクトリの example code にはGPUのサポートが追加されていますが、このチュートリアルのコードと基本的な部分は同じです。後のセクションでGPUの使い方を見ていきます。