Chainerハンズオン: Pythonによるディープラーニング入門

目的

ニューラルネットワークを用いた画像認識タスクを通じてChainerの使い方と特徴を理解する

このハンズオンで学べるChainerの特長

  1. デバッグのしやすさ
  2. CPUとGPUで共通のArray操作

目次

Section 1. パーセプトロンによるMNIST画像分類

単純なニューラルネットワークで数値画像の分類タスクに挑戦する

  • 多層パーセプトロンの構築と訓練
  • 結果の評価と可視化
  • モデルの改良とデバッグ

Section 2. Chainerの基礎

Chainerのデータ構造と実装の特徴をまとめる

  • NumPyとCuPy
  • VariableとFunction
  • LinkとChain
  • Define-by-Run
[1]:
## Install Chainer and CuPy!

!curl https://colab.chainer.org/install | sh -
Reading package lists... Done
Building dependency tree
Reading state information... Done
libcusparse8.0 is already the newest version (8.0.61-1).
libnvrtc8.0 is already the newest version (8.0.61-1).
libnvtoolsext1 is already the newest version (8.0.61-1).
0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Requirement already satisfied: cupy-cuda80 in /usr/local/lib/python3.6/dist-packages (4.3.0)
Requirement already satisfied: chainer in /usr/local/lib/python3.6/dist-packages (4.3.1)
Requirement already satisfied: fastrlock>=0.3 in /usr/local/lib/python3.6/dist-packages (from cupy-cuda80) (0.3)
Requirement already satisfied: six>=1.9.0 in /usr/local/lib/python3.6/dist-packages (from cupy-cuda80) (1.11.0)
Requirement already satisfied: numpy>=1.9.0 in /usr/local/lib/python3.6/dist-packages (from cupy-cuda80) (1.14.5)
Requirement already satisfied: protobuf>=3.0.0 in /usr/local/lib/python3.6/dist-packages (from chainer) (3.6.0)
Requirement already satisfied: filelock in /usr/local/lib/python3.6/dist-packages (from chainer) (3.0.4)
Requirement already satisfied: setuptools in /usr/local/lib/python3.6/dist-packages (from protobuf>=3.0.0->chainer) (39.1.0)

準備: Chainerのインポート

最初にChainerと関連モジュールをインポートする。CuPyについては後ほど説明する。

[2]:
## Import Chainer
from chainer import Chain, Variable, optimizers, serializers, datasets, training
from chainer.training import extensions
import chainer.functions as F
import chainer.links as L
import chainer

## Import NumPy and CuPy
import numpy as np
import cupy as cp

## Utilities
import time
import math

print('Chainer version: ', chainer.__version__)
Chainer version:  4.3.1

Section 1. パーセプトロンによるMNIST画像分類

MNISTは機械学習でよく用いられる分類問題のベンチマークデータセットである。手書き数値文字画像を7万サンプル含み、それぞれに0から9までの正解ラベルが与えられている。タスクは、与えられた画像の数値を予測することであり、10クラス分類問題に相当する。

各サンプルは縦横28x28のグレースケール画像(784次元ベクトル)である。

最も単純なニューラルネットワークモデルとして、2層の多層パーセプトロン(MLP2)を用いる。それは入力と出力、およびその間に1つの隠れユニットを持つ。2つの線形レイヤー(全結合層)がそれらの間にあり、それぞれ重み行列とバイアス項をパラメータとして内蔵している。隠れユニットに対する活性化関数はtanhを用いる、

以下がMLP2を実装したクラスである。コンストラクタ(__init__)の中では各レイヤーの種類と大きさしか定義されていないことに注意したい。実際の順方向計算は別の__call__メソッド内に直接書かれている。一方、逆方向計算は明示的に与えられてない。なぜなら、Chainerは順方向計算中に計算グラフを記憶し、それに沿って逆方向計算を行うからである(詳しくはSection 2で述べる)。

[ ]:
## 2-layer Multi-Layer Perceptron (MLP)
class MLP2(Chain):

    # Initialization of layers
    def __init__(self):
        super(MLP2, self).__init__()
        with self.init_scope():
            self.l1=L.Linear(784, 100)  # From 784-dimensional input to hidden unit with 100 nodes
            self.l2=L.Linear(100, 10)  # From hidden unit with 100 nodes to output unit with 10 nodes  (10 classes)

    # Forward computation by __call__
    def __call__(self, x):
        h1 = F.tanh(self.l1(x))     # Forward from x to h1 through activation with tanh function
        y = self.l2(h1)                 # Forward from h1to y
        return y

MNISTデータセットをchainer.datasets.get_mnist()を使ってメモリ上に読み込む。

元の70,000サンプルを2つに分け、60,000サンプルの訓練用データセット(x_trainとy_trainのペア)と、10,000サンプルのテスト用データセット(x_testとy_testのペア)を用意する。

[4]:
train, test = chainer.datasets.get_mnist()
print('Train:', len(train))
print('Test:', len(test))
Train: 60000
Test: 10000

以下の変数は実験を通して使用する。

[ ]:
results = [] # Store all of the experimental results
batchsize = 100 # Size of minibatches

Experiment 1.1 - CPUを用いたMLP2の訓練

最初の実験として、CPUベースでNumPyを用いた訓練を行う。エポック数(n_epoch、各サンプルが訓練中に何度使われるか)は2に固定する。

[ ]:
enable_cupy = False # No CuPy (Use NumPy)
n_epoch=2 # Only 2 epochs

定義: MNISTの訓練とテストを行うメソッド

下のtrain_and_test()は実際に実験を走らせるメソッドで、Chainer v1.11.0から導入されたTrainerを用いている。内容は以下の様な標準的な機械学習ワークフローの後ろ3つの処理を含んでいる。

Optimizerは誤差逆伝搬によってモデルのパラメータ(ここではMLP2中の線形レイヤー中の重み行列とバイアス項)を更新するために使われる。Chainerはよく使われる最適化アルゴリズムの多くをサポート している(SGD, AdaGrad, RMSProp, Adam, etc…)。ここではSGDを用いる。L.Classifier はニューラルネットワーク(ここではMLP2)を内蔵した分類モデルを表す。デフォルトの損失関数はSoftmax cross entropyである。

[ ]:
def  train_and_test():
    training_start = time.clock()
    log_trigger = 600, 'iteration'
    device = -1
    if enable_cupy:
        model.to_gpu()
        chainer.cuda.get_device(0).use()
        device = 0
    optimizer = optimizers.SGD()
    optimizer.setup(classifier_model)
    train_iter = chainer.iterators.SerialIterator(train, batchsize)
    test_iter = chainer.iterators.SerialIterator(test, batchsize, repeat=False, shuffle=False)
    updater = training.StandardUpdater(train_iter, optimizer, device=device)
    trainer = training.Trainer(updater, (n_epoch, 'epoch'), out='out')
    trainer.extend(extensions.dump_graph('main/loss'))
    trainer.extend(extensions.Evaluator(test_iter, classifier_model, device=device))
    trainer.extend(extensions.LogReport(trigger=log_trigger))
    trainer.extend(extensions.PrintReport(
        ['epoch', 'iteration', 'main/loss', 'validation/main/loss',
         'main/accuracy', 'validation/main/accuracy']), trigger=log_trigger)
    trainer.run()
    elapsed_time = time.clock() - training_start
    print('Elapsed time: %3.3f' % elapsed_time)

実行: 「test finished」が表示されれば完了

ここで実験を走らせて最初の結果を得る。10秒かそれ以上かかる場合もある。

[8]:
model = MLP2() # MLP2 model
classifier_model = L.Classifier(model)
train_and_test() # May take 30 sec or more
epoch       iteration   main/loss   validation/main/loss  main/accuracy  validation/main/accuracy
1           600         1.1531      0.65639               0.742483       0.8595
2           1200        0.566964    0.471303              0.865233       0.8856
Elapsed time: 12.876

評価: 1回目の結果を確認する

精度(accuracy)は90%に届かない程度だと思われる。悪くはないが、今後の実験で性能向上を目指す。

準備: 可視化ツールのインポート

Pythonでは標準的なmatplotlibを計算グラフと画像の可視化に用いる。

[9]:
## Import utility and visualization tools
!apt-get install graphviz
!pip install pydot
import pydot
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
from IPython.display import Image, display
import chainer.computational_graph as cg
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
  fontconfig libcairo2 libcdt5 libcgraph6 libdatrie1 libgd3 libgraphite2-3
  libgvc6 libgvpr2 libharfbuzz0b libjbig0 libltdl7 libpango-1.0-0
  libpangocairo-1.0-0 libpangoft2-1.0-0 libpathplan4 libpixman-1-0
  libthai-data libthai0 libtiff5 libwebp6 libxaw7 libxcb-render0 libxcb-shm0
  libxmu6 libxpm4 libxt6
Suggested packages:
  gsfonts graphviz-doc libgd-tools
The following NEW packages will be installed:
  fontconfig graphviz libcairo2 libcdt5 libcgraph6 libdatrie1 libgd3
  libgraphite2-3 libgvc6 libgvpr2 libharfbuzz0b libjbig0 libltdl7
  libpango-1.0-0 libpangocairo-1.0-0 libpangoft2-1.0-0 libpathplan4
  libpixman-1-0 libthai-data libthai0 libtiff5 libwebp6 libxaw7 libxcb-render0
  libxcb-shm0 libxmu6 libxpm4 libxt6
0 upgraded, 28 newly installed, 0 to remove and 0 not upgraded.
Need to get 4,122 kB of archives.
After this operation, 20.6 MB of additional disk space will be used.
Get:1 http://archive.ubuntu.com/ubuntu artful/main amd64 fontconfig amd64 2.11.94-0ubuntu2 [177 kB]
Get:2 http://archive.ubuntu.com/ubuntu artful/main amd64 libjbig0 amd64 2.1-3.1 [26.6 kB]
Get:3 http://archive.ubuntu.com/ubuntu artful/main amd64 libcdt5 amd64 2.38.0-16ubuntu2 [19.5 kB]
Get:4 http://archive.ubuntu.com/ubuntu artful/main amd64 libcgraph6 amd64 2.38.0-16ubuntu2 [40.0 kB]
Get:5 http://archive.ubuntu.com/ubuntu artful-updates/main amd64 libtiff5 amd64 4.0.8-5ubuntu0.1 [150 kB]
Get:6 http://archive.ubuntu.com/ubuntu artful/main amd64 libwebp6 amd64 0.6.0-3 [181 kB]
Get:7 http://archive.ubuntu.com/ubuntu artful/main amd64 libxpm4 amd64 1:3.5.12-1 [34.0 kB]
Get:8 http://archive.ubuntu.com/ubuntu artful/main amd64 libgd3 amd64 2.2.5-3 [119 kB]
Get:9 http://archive.ubuntu.com/ubuntu artful/main amd64 libpixman-1-0 amd64 0.34.0-1 [230 kB]
Get:10 http://archive.ubuntu.com/ubuntu artful/main amd64 libxcb-render0 amd64 1.12-1ubuntu1 [14.8 kB]
Get:11 http://archive.ubuntu.com/ubuntu artful/main amd64 libxcb-shm0 amd64 1.12-1ubuntu1 [5,482 B]
Get:12 http://archive.ubuntu.com/ubuntu artful/main amd64 libcairo2 amd64 1.14.10-1ubuntu1 [558 kB]
Get:13 http://archive.ubuntu.com/ubuntu artful/main amd64 libltdl7 amd64 2.4.6-2 [38.8 kB]
Get:14 http://archive.ubuntu.com/ubuntu artful/main amd64 libthai-data all 0.1.26-3 [132 kB]
Get:15 http://archive.ubuntu.com/ubuntu artful/main amd64 libdatrie1 amd64 0.2.10-5 [17.6 kB]
Get:16 http://archive.ubuntu.com/ubuntu artful/main amd64 libthai0 amd64 0.1.26-3 [17.7 kB]
Get:17 http://archive.ubuntu.com/ubuntu artful/main amd64 libpango-1.0-0 amd64 1.40.12-1 [152 kB]
Get:18 http://archive.ubuntu.com/ubuntu artful/main amd64 libgraphite2-3 amd64 1.3.10-2 [78.3 kB]
Get:19 http://archive.ubuntu.com/ubuntu artful/main amd64 libharfbuzz0b amd64 1.4.2-1 [211 kB]
Get:20 http://archive.ubuntu.com/ubuntu artful/main amd64 libpangoft2-1.0-0 amd64 1.40.12-1 [33.2 kB]
Get:21 http://archive.ubuntu.com/ubuntu artful/main amd64 libpangocairo-1.0-0 amd64 1.40.12-1 [20.8 kB]
Get:22 http://archive.ubuntu.com/ubuntu artful/main amd64 libpathplan4 amd64 2.38.0-16ubuntu2 [22.6 kB]
Get:23 http://archive.ubuntu.com/ubuntu artful/main amd64 libgvc6 amd64 2.38.0-16ubuntu2 [587 kB]
Get:24 http://archive.ubuntu.com/ubuntu artful/main amd64 libgvpr2 amd64 2.38.0-16ubuntu2 [167 kB]
Get:25 http://archive.ubuntu.com/ubuntu artful/main amd64 libxt6 amd64 1:1.1.5-1 [160 kB]
Get:26 http://archive.ubuntu.com/ubuntu artful/main amd64 libxmu6 amd64 2:1.1.2-2 [46.0 kB]
Get:27 http://archive.ubuntu.com/ubuntu artful/main amd64 libxaw7 amd64 2:1.0.13-1 [173 kB]
Get:28 http://archive.ubuntu.com/ubuntu artful/main amd64 graphviz amd64 2.38.0-16ubuntu2 [710 kB]
Fetched 4,122 kB in 0s (22.5 MB/s)
Selecting previously unselected package fontconfig.
(Reading database ... 18431 files and directories currently installed.)
Preparing to unpack .../00-fontconfig_2.11.94-0ubuntu2_amd64.deb ...
Unpacking fontconfig (2.11.94-0ubuntu2) ...
Selecting previously unselected package libjbig0:amd64.
Preparing to unpack .../01-libjbig0_2.1-3.1_amd64.deb ...
Unpacking libjbig0:amd64 (2.1-3.1) ...
Selecting previously unselected package libcdt5.
Preparing to unpack .../02-libcdt5_2.38.0-16ubuntu2_amd64.deb ...
Unpacking libcdt5 (2.38.0-16ubuntu2) ...
Selecting previously unselected package libcgraph6.
Preparing to unpack .../03-libcgraph6_2.38.0-16ubuntu2_amd64.deb ...
Unpacking libcgraph6 (2.38.0-16ubuntu2) ...
Selecting previously unselected package libtiff5:amd64.
Preparing to unpack .../04-libtiff5_4.0.8-5ubuntu0.1_amd64.deb ...
Unpacking libtiff5:amd64 (4.0.8-5ubuntu0.1) ...
Selecting previously unselected package libwebp6:amd64.
Preparing to unpack .../05-libwebp6_0.6.0-3_amd64.deb ...
Unpacking libwebp6:amd64 (0.6.0-3) ...
Selecting previously unselected package libxpm4:amd64.
Preparing to unpack .../06-libxpm4_1%3a3.5.12-1_amd64.deb ...
Unpacking libxpm4:amd64 (1:3.5.12-1) ...
Selecting previously unselected package libgd3:amd64.
Preparing to unpack .../07-libgd3_2.2.5-3_amd64.deb ...
Unpacking libgd3:amd64 (2.2.5-3) ...
Selecting previously unselected package libpixman-1-0:amd64.
Preparing to unpack .../08-libpixman-1-0_0.34.0-1_amd64.deb ...
Unpacking libpixman-1-0:amd64 (0.34.0-1) ...
Selecting previously unselected package libxcb-render0:amd64.
Preparing to unpack .../09-libxcb-render0_1.12-1ubuntu1_amd64.deb ...
Unpacking libxcb-render0:amd64 (1.12-1ubuntu1) ...
Selecting previously unselected package libxcb-shm0:amd64.
Preparing to unpack .../10-libxcb-shm0_1.12-1ubuntu1_amd64.deb ...
Unpacking libxcb-shm0:amd64 (1.12-1ubuntu1) ...
Selecting previously unselected package libcairo2:amd64.
Preparing to unpack .../11-libcairo2_1.14.10-1ubuntu1_amd64.deb ...
Unpacking libcairo2:amd64 (1.14.10-1ubuntu1) ...
Selecting previously unselected package libltdl7:amd64.
Preparing to unpack .../12-libltdl7_2.4.6-2_amd64.deb ...
Unpacking libltdl7:amd64 (2.4.6-2) ...
Selecting previously unselected package libthai-data.
Preparing to unpack .../13-libthai-data_0.1.26-3_all.deb ...
Unpacking libthai-data (0.1.26-3) ...
Selecting previously unselected package libdatrie1:amd64.
Preparing to unpack .../14-libdatrie1_0.2.10-5_amd64.deb ...
Unpacking libdatrie1:amd64 (0.2.10-5) ...
Selecting previously unselected package libthai0:amd64.
Preparing to unpack .../15-libthai0_0.1.26-3_amd64.deb ...
Unpacking libthai0:amd64 (0.1.26-3) ...
Selecting previously unselected package libpango-1.0-0:amd64.
Preparing to unpack .../16-libpango-1.0-0_1.40.12-1_amd64.deb ...
Unpacking libpango-1.0-0:amd64 (1.40.12-1) ...
Selecting previously unselected package libgraphite2-3:amd64.
Preparing to unpack .../17-libgraphite2-3_1.3.10-2_amd64.deb ...
Unpacking libgraphite2-3:amd64 (1.3.10-2) ...
Selecting previously unselected package libharfbuzz0b:amd64.
Preparing to unpack .../18-libharfbuzz0b_1.4.2-1_amd64.deb ...
Unpacking libharfbuzz0b:amd64 (1.4.2-1) ...
Selecting previously unselected package libpangoft2-1.0-0:amd64.
Preparing to unpack .../19-libpangoft2-1.0-0_1.40.12-1_amd64.deb ...
Unpacking libpangoft2-1.0-0:amd64 (1.40.12-1) ...
Selecting previously unselected package libpangocairo-1.0-0:amd64.
Preparing to unpack .../20-libpangocairo-1.0-0_1.40.12-1_amd64.deb ...
Unpacking libpangocairo-1.0-0:amd64 (1.40.12-1) ...
Selecting previously unselected package libpathplan4.
Preparing to unpack .../21-libpathplan4_2.38.0-16ubuntu2_amd64.deb ...
Unpacking libpathplan4 (2.38.0-16ubuntu2) ...
Selecting previously unselected package libgvc6.
Preparing to unpack .../22-libgvc6_2.38.0-16ubuntu2_amd64.deb ...
Unpacking libgvc6 (2.38.0-16ubuntu2) ...
Selecting previously unselected package libgvpr2.
Preparing to unpack .../23-libgvpr2_2.38.0-16ubuntu2_amd64.deb ...
Unpacking libgvpr2 (2.38.0-16ubuntu2) ...
Selecting previously unselected package libxt6:amd64.
Preparing to unpack .../24-libxt6_1%3a1.1.5-1_amd64.deb ...
Unpacking libxt6:amd64 (1:1.1.5-1) ...
Selecting previously unselected package libxmu6:amd64.
Preparing to unpack .../25-libxmu6_2%3a1.1.2-2_amd64.deb ...
Unpacking libxmu6:amd64 (2:1.1.2-2) ...
Selecting previously unselected package libxaw7:amd64.
Preparing to unpack .../26-libxaw7_2%3a1.0.13-1_amd64.deb ...
Unpacking libxaw7:amd64 (2:1.0.13-1) ...
Selecting previously unselected package graphviz.
Preparing to unpack .../27-graphviz_2.38.0-16ubuntu2_amd64.deb ...
Unpacking graphviz (2.38.0-16ubuntu2) ...
Setting up libpathplan4 (2.38.0-16ubuntu2) ...
Setting up libxcb-render0:amd64 (1.12-1ubuntu1) ...
Setting up libjbig0:amd64 (2.1-3.1) ...
Setting up libdatrie1:amd64 (0.2.10-5) ...
Setting up libtiff5:amd64 (4.0.8-5ubuntu0.1) ...
Setting up libgraphite2-3:amd64 (1.3.10-2) ...
Setting up libpixman-1-0:amd64 (0.34.0-1) ...
Processing triggers for libc-bin (2.26-0ubuntu2.1) ...
Setting up libltdl7:amd64 (2.4.6-2) ...
Setting up libxcb-shm0:amd64 (1.12-1ubuntu1) ...
Setting up libxpm4:amd64 (1:3.5.12-1) ...
Setting up libxt6:amd64 (1:1.1.5-1) ...
Setting up libthai-data (0.1.26-3) ...
Setting up libcdt5 (2.38.0-16ubuntu2) ...
Setting up fontconfig (2.11.94-0ubuntu2) ...
Regenerating fonts cache... done.
Setting up libcgraph6 (2.38.0-16ubuntu2) ...
Setting up libwebp6:amd64 (0.6.0-3) ...
Setting up libcairo2:amd64 (1.14.10-1ubuntu1) ...
Setting up libgvpr2 (2.38.0-16ubuntu2) ...
Setting up libgd3:amd64 (2.2.5-3) ...
Setting up libharfbuzz0b:amd64 (1.4.2-1) ...
Setting up libthai0:amd64 (0.1.26-3) ...
Setting up libxmu6:amd64 (2:1.1.2-2) ...
Setting up libpango-1.0-0:amd64 (1.40.12-1) ...
Setting up libxaw7:amd64 (2:1.0.13-1) ...
Setting up libpangoft2-1.0-0:amd64 (1.40.12-1) ...
Setting up libpangocairo-1.0-0:amd64 (1.40.12-1) ...
Setting up libgvc6 (2.38.0-16ubuntu2) ...
Setting up graphviz (2.38.0-16ubuntu2) ...
Processing triggers for libc-bin (2.26-0ubuntu2.1) ...
Collecting pydot
  Downloading https://files.pythonhosted.org/packages/c3/f1/e61d6dfe6c1768ed2529761a68f70939e2569da043e9f15a8d84bf56cadf/pydot-1.2.4.tar.gz (132kB)
    100% |████████████████████████████████| 133kB 7.8MB/s
Requirement already satisfied: pyparsing>=2.1.4 in /usr/local/lib/python3.6/dist-packages (from pydot) (2.2.0)
Building wheels for collected packages: pydot
  Running setup.py bdist_wheel for pydot ... - done
  Stored in directory: /content/.cache/pip/wheels/6a/a5/14/25541ebcdeaf97a37b6d05c7ff15f5bd20f5e91b99d313e5b4
Successfully built pydot
Installing collected packages: pydot
Successfully installed pydot-1.2.4

定義: 計算グラフの可視化メソッド

Chainerは入力から損失関数までの計算グラフを出力できる。

[ ]:
def display_graph():
    graph = pydot.graph_from_dot_file('out/cg.dot') # load from .dot file
    graph[0].write_png('graph.png')

    img = Image('graph.png', width=600, height=600)
    display(img)

実行: MLP2の計算グラフ可視化

display_graph()を実行することで、有向グラフが表示される。一番上の3つの楕円形がそれぞれ784次元の画像100サンプル、線形レイヤーの対応する784x100の大きさの重み行列、長さ100のバイアス項ベクトル、を表す。

中間の隠れユニット(100x100)は活性化関数tanhを通して次の線形レイヤーの入力となる。ニューラルネットワークの出力である長さ10の100ベクトルは正解であるint32のリストと比較され、SoftmaxCrossEntropy関数によって損失がfloat32の値として算出される。

このグラフを構築したあと、誤差逆伝搬が末端の損失から入力までを遡り、モデルのパラメータ(2つの線形レイヤーの係数行列とバイアス項)を更新する。

[11]:
display_graph()
../../../_images/notebook_hands_on_chainer_chainer_23_0.png

定義: 画像と予測ラベルのプロット

テスト用データセットのMNIST画像60サンプルをプロットする。画像上部の」Answer」がデータセットで与えられた正解であり、」Predict」が現在の分類モデルによる予測結果である。

[ ]:
def plot_examples():
    %matplotlib inline
    plt.figure(figsize=(12,50))
    if enable_cupy:
       model.to_cpu()
    for i in range(45, 105):
        x = Variable(np.asarray([test[i][0]]))  # test data
        t = Variable(np.asarray([test[i][1]]))  # labels
        y = model(x)
        prediction = y.data.argmax(axis=1)
        example = (test[i][0] * 255).astype(np.int32).reshape(28, 28)
        plt.subplot(20, 5, i - 44)
        plt.imshow(example, cmap='gray')
        plt.title("No.{0} / Answer:{1}, Predict:{2}".format(i, t.data[0], prediction[0]))
        plt.axis("off")
    plt.tight_layout()

実行: いくつかのサンプルが誤分類される

多くのサンプルの数字は正しく分類されているが、誤りも見受けられる。例えば、一行目のNo. 46の画像が’3’と予測されているかもしれないが、人間には正解の通り’1’に見える。同様に、いびつな形をした’6’である二行目のNo. 54を’2’と誤分類していないだろうか。

[13]:
plot_examples()
../../../_images/notebook_hands_on_chainer_chainer_27_0.png

Experiment 1.2 - エポック数を増加させてみる

精度向上のために、単にエポック数を増加させることを試みる。他の設定は全て同一のままである。

[ ]:
enable_cupy = False
n_epoch=5                 # Increased from 2 to 5

実行: エポック数5で再実験

エポック数が増えたので実験終了にはより長い時間がかかる。

[15]:
model = MLP2()
classifier_model = L.Classifier(model)
train_and_test()
epoch       iteration   main/loss   validation/main/loss  main/accuracy  validation/main/accuracy
1           600         1.12974     0.651618              0.758867       0.8604
2           1200        0.564204    0.471518              0.865533       0.8859
3           1800        0.453113    0.404142              0.883083       0.8959
4           2400        0.402744    0.368589              0.892033       0.9013
5           3000        0.372716    0.345529              0.897767       0.9063
Elapsed time: 33.290

評価: テスト精度が向上していることを確認

前回と比べ損失の値が低減され精度が向上(90%以上)となっていることがわかる。

実行: 誤分類が減ったか確認

先ほど誤分類されたNo.46もしくはNo.54が今回正しく分類されているか確認してみよう。

[16]:
plot_examples()
../../../_images/notebook_hands_on_chainer_chainer_34_0.png

Experiment 1.3 - CuPyでGPUを計算に使う

エポック数を増やすことで精度は向上したが、エポック数5ですでに30秒程度かかっている。次のケースでは、CuPyを有効にしてGPUを使うことでこの訓練を高速化する。

[ ]:
enable_cupy = True # Now use CuPy
n_epoch=5

実行: 同じモデルをGPUで訓練

(GPU計算の立ち上がりに時間がかかるが)各エポックの実行速度が明らかに違うことがわかる。

[18]:
model = MLP2()
classifier_model = L.Classifier(model)
train_and_test()
epoch       iteration   main/loss   validation/main/loss  main/accuracy  validation/main/accuracy
1           600         1.114       0.643112              0.759833       0.8623
2           1200        0.559301    0.465749              0.866233       0.8858
3           1800        0.450068    0.399867              0.883          0.8964
4           2400        0.400445    0.3642                0.891218       0.9014
5           3000        0.370953    0.342213              0.897984       0.9054
Elapsed time: 49.706

評価: 訓練の高速化結果を確認する

実行時間を比べると、この環境ではGPUによる訓練はCPUに比べて2倍程度である。

Experiment 1.4 - レイヤーを追加する

今度はレイヤーを増やした異なる多層パーセプトロンを用いる。

定義: 3層の多層パーセプトロン

以下のMLP3は3つの線形レイヤーでつながれた同じ100ノードからなる2つの隠れユニットを持つ。順方向計算でtanhを活性化関数を用いるのも同様である。

[ ]:
## 3-layer multi-Layer Perceptron (MLP)
class MLP3(Chain):

    def __init__(self):
        super(MLP3, self).__init__()
        with self.init_scope():
            self.l1=L.Linear(784, 100)
            self.l2=L.Linear(100, 100)   # Additional  layer
            self.l3=L.Linear(100, 10)

    def __call__(self, x):
        h1 = F.tanh(self.l1(x))   # Hidden unit 1
        h2 = F.tanh(self.l2(h1)) # Hidden unit 2
        y = self.l3(h2)
        return y

準備: 分類モデルのベースをMLP3とする

[ ]:
enable_cupy = True
n_epoch=5

Execution: 新しいMLP3ベースのモデルを訓練する

[21]:
model = MLP3()  # Use MLP3 instead of MLP2
classifier_model = L.Classifier(model)
train_and_test()
epoch       iteration   main/loss   validation/main/loss  main/accuracy  validation/main/accuracy
1           600         1.06402     0.561088              0.763766       0.872
2           1200        0.491097    0.410575              0.876967       0.8931
3           1800        0.398214    0.356118              0.893167       0.903
4           2400        0.355331    0.325703              0.901285       0.9094
5           3000        0.328767    0.306094              0.907801       0.9151
Elapsed time: 21.000

評価: MLP2とMLP3の精度を比較する

MLP3は高い表現力によって、MLP2よりも低い損失と高い精度を達成している。一方、内部パラメータも増えるため、訓練に要する時間はわずかに増加する。

実行: MLP3ベースの計算グラフを表示

新しい計算グラフは3つのLinearFunctionと2つのTanh活性化関数を含む。

[22]:
display_graph()
../../../_images/notebook_hands_on_chainer_chainer_49_0.png

実行: 誤分類サンプルはもうないか?

現在のモデルは先ほどと同様の60サンプルであればほぼ完璧に予測できることが確認できる。

[23]:
plot_examples()
../../../_images/notebook_hands_on_chainer_chainer_51_0.png

Chainerの特長 - (1) デバッグのしやすさ

複雑なニューラルネットワークのデバッグは面倒である、何故なら一般のフレームワークはモデル定義や実装のどこが間違っているのか、直接的には教えてくれない事が多いからだ。しかしChainerでは普通のプログラムをデバッグするかのように行うことができ、順方向計算中の型チェックもサポートしている。

定義: 予めバグを埋め込んだMLP実装

以下のMLP3Wrongは、MLP3に3つのバグをわざと埋め込んだものである。実行しながら1つ1つ解決していこう。

[ ]:
## Find three bugs in this model definition
class MLP3Wrong(Chain):

    def __init__(self):
        super(MLP3Wrong, self).__init__()
        with self.init_scope():
            self.l1=L.Linear(748, 100)
            self.l2=L.Linear(100, 100)
            self.l3=L.Linear(100, 10)

    def __call__(self, x):
        h1 = F.tanh(self.l1(x))
        h2 = F.tanh(self.l2(x))
        y = self.l3(h3)
        return y

enable_cupy = True
n_epoch=5

実行: スタックトレースを見るとエラー箇所が判る

以下を実行するとエラーが発生するが、スタックトレースの表示を下まで追うとそれが順方向計算においてどのソースコードのどの行で発生しているかがわかる。これはChainerのDefine-by-Runが、計算グラフを順方向計算中に直接構築しているため可能なことである(Section 2で説明する)。

3つのバグを修正し終わると、MLP3Wrongは先ほどのMLP3と一致するはずである。

[25]:
model = MLP3Wrong() # MLP3Wrong
classifier_model = L.Classifier(model)
train_and_test()
Exception in main training loop:
Invalid operation is performed in: LinearFunction (Forward)

Expect: in_types[0].shape[1] == in_types[1].shape[1]
Actual: 784 != 748
Traceback (most recent call last):
  File "/usr/local/lib/python3.6/dist-packages/chainer/training/trainer.py", line 306, in run
    update()
  File "/usr/local/lib/python3.6/dist-packages/chainer/training/updaters/standard_updater.py", line 149, in update
    self.update_core()
  File "/usr/local/lib/python3.6/dist-packages/chainer/training/updaters/standard_updater.py", line 160, in update_core
    optimizer.update(loss_func, *in_arrays)
  File "/usr/local/lib/python3.6/dist-packages/chainer/optimizer.py", line 650, in update
    loss = lossfun(*args, **kwds)
  File "/usr/local/lib/python3.6/dist-packages/chainer/links/model/classifier.py", line 134, in __call__
    self.y = self.predictor(*args, **kwargs)
  File "<ipython-input-24-498a68fb58f0>", line 11, in __call__
    h1 = F.tanh(self.l1(x))
  File "/usr/local/lib/python3.6/dist-packages/chainer/links/connection/linear.py", line 134, in __call__
    return linear.linear(x, self.W, self.b)
  File "/usr/local/lib/python3.6/dist-packages/chainer/functions/connection/linear.py", line 234, in linear
    y, = LinearFunction().apply(args)
  File "/usr/local/lib/python3.6/dist-packages/chainer/function_node.py", line 243, in apply
    self._check_data_type_forward(in_data)
  File "/usr/local/lib/python3.6/dist-packages/chainer/function_node.py", line 328, in _check_data_type_forward
    self.check_type_forward(in_type)
  File "/usr/local/lib/python3.6/dist-packages/chainer/functions/connection/linear.py", line 23, in check_type_forward
    x_type.shape[1] == w_type.shape[1],
  File "/usr/local/lib/python3.6/dist-packages/chainer/utils/type_check.py", line 524, in expect
    expr.expect()
  File "/usr/local/lib/python3.6/dist-packages/chainer/utils/type_check.py", line 482, in expect
    '{0} {1} {2}'.format(left, self.inv, right))
Will finalize trainer extensions and updater before reraising the exception.
---------------------------------------------------------------------------
InvalidType                               Traceback (most recent call last)
<ipython-input-25-9a17b0e6105a> in <module>()
      1 model = MLP3Wrong() # MLP3Wrong
      2 classifier_model = L.Classifier(model)
----> 3 train_and_test()

<ipython-input-7-78326c82d77b> in train_and_test()
     19         ['epoch', 'iteration', 'main/loss', 'validation/main/loss',
     20          'main/accuracy', 'validation/main/accuracy']), trigger=log_trigger)
---> 21     trainer.run()
     22     elapsed_time = time.clock() - training_start
     23     print('Elapsed time: %3.3f' % elapsed_time)

/usr/local/lib/python3.6/dist-packages/chainer/training/trainer.py in run(self, show_loop_exception_msg)
    318                 print('Will finalize trainer extensions and updater before '
    319                       'reraising the exception.', file=sys.stderr)
--> 320             six.reraise(*sys.exc_info())
    321         finally:
    322             for _, entry in extensions:

/usr/local/lib/python3.6/dist-packages/six.py in reraise(tp, value, tb)
    691             if value.__traceback__ is not tb:
    692                 raise value.with_traceback(tb)
--> 693             raise value
    694         finally:
    695             value = None

/usr/local/lib/python3.6/dist-packages/chainer/training/trainer.py in run(self, show_loop_exception_msg)
    304                 self.observation = {}
    305                 with reporter.scope(self.observation):
--> 306                     update()
    307                     for name, entry in extensions:
    308                         if entry.trigger(self):

/usr/local/lib/python3.6/dist-packages/chainer/training/updaters/standard_updater.py in update(self)
    147
    148         """
--> 149         self.update_core()
    150         self.iteration += 1
    151

/usr/local/lib/python3.6/dist-packages/chainer/training/updaters/standard_updater.py in update_core(self)
    158
    159         if isinstance(in_arrays, tuple):
--> 160             optimizer.update(loss_func, *in_arrays)
    161         elif isinstance(in_arrays, dict):
    162             optimizer.update(loss_func, **in_arrays)

/usr/local/lib/python3.6/dist-packages/chainer/optimizer.py in update(self, lossfun, *args, **kwds)
    648         if lossfun is not None:
    649             use_cleargrads = getattr(self, '_use_cleargrads', True)
--> 650             loss = lossfun(*args, **kwds)
    651             if use_cleargrads:
    652                 self.target.cleargrads()

/usr/local/lib/python3.6/dist-packages/chainer/links/model/classifier.py in __call__(self, *args, **kwargs)
    132         self.loss = None
    133         self.accuracy = None
--> 134         self.y = self.predictor(*args, **kwargs)
    135         self.loss = self.lossfun(self.y, t)
    136         reporter.report({'loss': self.loss}, self)

<ipython-input-24-498a68fb58f0> in __call__(self, x)
      9
     10     def __call__(self, x):
---> 11         h1 = F.tanh(self.l1(x))
     12         h2 = F.tanh(self.l2(x))
     13         y = self.l3(h3)

/usr/local/lib/python3.6/dist-packages/chainer/links/connection/linear.py in __call__(self, x)
    132             in_size = functools.reduce(operator.mul, x.shape[1:], 1)
    133             self._initialize_params(in_size)
--> 134         return linear.linear(x, self.W, self.b)

/usr/local/lib/python3.6/dist-packages/chainer/functions/connection/linear.py in linear(x, W, b)
    232         args = x, W, b
    233
--> 234     y, = LinearFunction().apply(args)
    235     return y

/usr/local/lib/python3.6/dist-packages/chainer/function_node.py in apply(self, inputs)
    241
    242         if configuration.config.type_check:
--> 243             self._check_data_type_forward(in_data)
    244
    245         hooks = chainer.get_function_hooks()

/usr/local/lib/python3.6/dist-packages/chainer/function_node.py in _check_data_type_forward(self, in_data)
    326         in_type = type_check.get_types(in_data, 'in_types', False)
    327         with type_check.get_function_check_context(self):
--> 328             self.check_type_forward(in_type)
    329
    330     def check_type_forward(self, in_types):

/usr/local/lib/python3.6/dist-packages/chainer/functions/connection/linear.py in check_type_forward(self, in_types)
     21             x_type.ndim == 2,
     22             w_type.ndim == 2,
---> 23             x_type.shape[1] == w_type.shape[1],
     24         )
     25         if type_check.eval(n_in) == 3:

/usr/local/lib/python3.6/dist-packages/chainer/utils/type_check.py in expect(*bool_exprs)
    522         for expr in bool_exprs:
    523             assert isinstance(expr, Testable)
--> 524             expr.expect()
    525
    526

/usr/local/lib/python3.6/dist-packages/chainer/utils/type_check.py in expect(self)
    480             raise InvalidType(
    481                 '{0} {1} {2}'.format(self.lhs, self.exp, self.rhs),
--> 482                 '{0} {1} {2}'.format(left, self.inv, right))
    483
    484

InvalidType:
Invalid operation is performed in: LinearFunction (Forward)

Expect: in_types[0].shape[1] == in_types[1].shape[1]
Actual: 784 != 748

Experiment 1.5 - 自分のモデルを作ってみよう

それではあなたの番だ。モデルを自分で変更してさらに高い精度を目指してみよう。

エポック数を増やして時間をかけるのは単純過ぎる解決策なので、エポック数10以下、かつ訓練時間100秒以内という制限の中で95%を超える精度を達成してみてほしい。

定義: オプションを駆使して新しいモデルを作る

ニューラルネットワークモデルをチューニングしてパフォーマンスを向上させよう。以下の様なオプションがある。

  • エポック数を増やす
  • 各ユニットのノード数を増やす
  • レイヤー数を増やす
  • 異なる活性化関数を用いる
[ ]:
## Let's create new Multi-Layer Perceptron (MLP)
class MLPNew(Chain):

    def __init__(self):
        # Add more layers?
        super(MLPNew, self).__init__()
        with self.init_scope():
            self.l1=L.Linear(784, 100)  # Increase output node as (784, 200)?
            self.l2=L.Linear(100, 100)  # Increase nodes as (200, 200)?
            self.l3=L.Linear(100, 10)      # Increase nodes as (200, 10)?

    def __call__(self, x):
        h1 = F.relu(self.l1(x))        # Replace F.tanh with F.sigmoid  or F.relu ?
        h2 = F.relu(self.l2(h1))        # Replace F.tanh with F.sigmoid  or F.relu ?
        y = self.l3(h2)
        return y

enable_cupy = True #  Use CuPy for faster training
n_epoch = 5 # Add more epochs?

実行: 95%以上の精度をもつモデルができただろうか

[27]:
model = MLPNew()
classifier_model = L.Classifier(model)
train_and_test()
epoch       iteration   main/loss   validation/main/loss  main/accuracy  validation/main/accuracy
1           600         1.37944     0.645228              0.642416       0.8442
2           1200        0.513565    0.40484               0.866883       0.8938
3           1800        0.386115    0.339986              0.893067       0.9085
4           2400        0.338946    0.314279              0.904051       0.9118
5           3000        0.311101    0.288255              0.911118       0.9214
Elapsed time: 21.501

実行: もう失敗しない?

95%以上の精度があれば、おそらく誤判別は見当たらないはずである。

[28]:
plot_examples()
../../../_images/notebook_hands_on_chainer_chainer_63_0.png

実行: 自分の作ったベストなモデルの内部を見てみよう

[29]:
display_graph()
../../../_images/notebook_hands_on_chainer_chainer_65_0.png

発展: 畳み込みニューラルネットワークの実装

このセクションでは、線形(全結合)層を持つ多層パーセプトロンのみを用いた。しかしながら、最近の画像認識におけるディープラーニングの進化は、畳み込みニューラルネットワーク(Convolutional Neural Network; CNN)という異なるタイプがもたらしている。

このハンズオンの内容を超えるためその詳細はここでは割愛するが、ChainerはImageNet画像分類タスクのexampleに、様々な畳み込みニューラルネットワークの実装を含んでいる。

定義: AlexNetモデル (ImageNetコンテスト2012優勝)

AlexNetは最も標準的なCNNで、ImageNet2012コンテストで優勝したモデルである。

基本的な関数やレイヤーは全てChainerでサポートされているため、ユーザーがこのような標準的なCNNを再実装したり、自分の問題のために拡張できる。例えばAlexNetは以下の様なモジュールを含んでいる。

  • Convolutional layer (L.Convolution2D)
  • Max pooling (F.max_pooling_2d)
  • Local response normalization (F.local_response_normalization)
  • Dropout (F.dropout)

関数等の詳細については、ChainerリファレンスマニュアルのStandard Function implementationsに説明がある。

[ ]:
## Definition of AlexNet
class AlexNet(chainer.Chain):

    def __init__(self):
        super(AlexNet, self).__init__()
        with self.init_scope():
            conv1=L.Convolution2D(3,  96, 11, stride=4)
            conv2=L.Convolution2D(96, 256,  5, pad=2)
            conv3=L.Convolution2D(256, 384,  3, pad=1)
            conv4=L.Convolution2D(384, 384,  3, pad=1)
            conv5=L.Convolution2D(384, 256,  3, pad=1)
            fc6=L.Linear(9216, 4096)
            fc7=L.Linear(4096, 4096)
            fc8=L.Linear(4096, 1000)

        self.train = True

    def __call__(self, x, t):
        h = F.max_pooling_2d(F.relu(
            F.local_response_normalization(self.conv1(x))), 3, stride=2)
        h = F.max_pooling_2d(F.relu(
            F.local_response_normalization(self.conv2(h))), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.relu(self.conv4(h))
        h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride=2)
        h = F.dropout(F.relu(self.fc6(h)), train=self.train)
        h = F.dropout(F.relu(self.fc7(h)), train=self.train)
        y = self.fc8(h)
        return y

Section 2. Chainerの基礎

Section 1では、画像認識をタスク例に、Chainerにおいてどのようにしてニューラルネットワークを定義し訓練するかを説明した。

ユーザーはこのようなパターン認識タスクだけでなく、自分の課題にもChainerを利用できる。しかし、上記の課題ではプリセットに存在するレイヤーや関数を組み合わせるだけでニューラルネットワークを構築していたが、今後はユーザーがゼロからより低レベルのコーディングを行うことで新しいネットワークを作り出す必要があるかもしれない。

Chainerはそのようなユーザーが新しいモデルを迅速にプロトタイピングし、テストし、トライアンドエラーを通じて改良しやすいようにデザインされている。以下では、Chainerのコアとなっているコンポーネントについて説明する。

3.1 NumPyとCuPy

NumPyはPythonの数値計算用に広く用いられているライブラリであり、Chainerの中でも用いられている。一方、ニューラルネットワークはGPGPUの多次元配列演算の高速化を必要としている。しかしNumPyはGPUをサポートしていないため、Chainerの初期バージョンではGPUを用いるためにはGPUに特化したコードを常に必要としていた。

そこでChainer開発陣は、NumPy互換性のあるCUDAライブラリとしてCuPyを開発し、Chainerに加えた。CuPyは現在NumPyの多くのAPIをサポートしていおり、ユーザーはほとんどの場合CPU/GPU非依存なコードを書くことができる。

実行: NumPyを試す

NumPyを用いて、行列の生成と処理を行って処理時間を計測する。具体的には、1000x1000の大きさの行列を作り、転置し、各要素を2倍にする操作を5000回繰り返す。

[31]:
## import numpy as np
a = np.arange(1000000).reshape(1000, -1)
t1 = time.clock()
for i in range(5000):
    a = np.arange(1000000).reshape(1000, -1)
    b = a.T * 2
t2 = time.clock()
print(t2 -t1)
16.400983999999994

実行: CuPyを試す

同じ処理をCuPyで試す。1行目の実行はCUDA関連の初期化に時間がかかるが、5000回のループ処理全体はNumPyと比べて10倍かそれ以上高速になる。

[32]:
## import cupy as cp
a = cp.arange(1000000).reshape(1000, -1)
t1 = time.clock()
for i in range(5000):
    a = cp.arange(1000000).reshape(1000, -1)
    b = a.T * 2
t2 = time.clock()
print(t2 -t1)
1.8124880000000019

Chainerの特長 - (2) CPU/GPU互換の行列演算

CuPyは可能な限りNumPyと同じインタフェースを提供しているため、ユーザーは計算ロジックを変更すること無く両者を以下のように切り替えられる。

[33]:
def xp_test(xp):
    a = xp.arange(1000000).reshape(1000, -1)
    t1 = time.clock()
    for i in range(5000):
        a = xp.arange(1000000).reshape(1000, -1)
        b = a.T * 2
    t2 = time.clock()
    print(t2 -t1)

enable_cupy = False
xp_test(np if not enable_cupy else cp)
enable_cupy = True
xp_test(np if not enable_cupy else cp)
18.556197999999995
1.3077269999999999

3.2 VariableとFunction

VariableとFunctionはそれぞれChainerで基礎的なクラスである。その名前から分かる通り、Variableは変数の値を表し、FunctionはVariableに対する操作を行う(静的な)関数である。

実行: Variableは多次元配列を含むクラス

VariableはNumPyかCuPyの多次元配列インスタンスと共に初期化され、その値は.dataに格納される。

[34]:
x = Variable(np.asarray([[0, 2],[1, -3]]).astype(np.float32))

print(type(x))
print(type(x.data))
print(x.data)
<class 'chainer.variable.Variable'>
<class 'numpy.ndarray'>
[[ 0.  2.]
 [ 1. -3.]]

実行: VariableはCPUとGPUの間を移動できる

to_gpu()やto_cpu()を呼ぶことで、.dataを含むVariable内の要素はCPU側のNumPyの多次元配列、GPU側のCuPyの多次元配列を行き来できる。

[35]:
x.to_gpu()
print(type(x.data))
x.to_cpu()
print(type(x.data))
<class 'cupy.core.core.ndarray'>
<class 'numpy.ndarray'>

実行: FunctionはVariableの変換を行う

実際の計算はforward()メソッド中に記述され、出力もまたVariableのインスタンスになる。

[36]:
from chainer import function

class MyFunc(function.Function):
    def forward(self, x):
        self.y = x[0] **2 + 2 * x[0] + 1 # y = x^2 + 2x + 1
        return self.y,

def my_func(x):
    return MyFunc()(x)

x = Variable(np.asarray([[0, 2],[1, -3]]).astype(np.float32))
y = my_func(x)
print(type(x))
print(x.data)
print(type(y))
print(y.data)
<class 'chainer.variable.Variable'>
[[ 0.  2.]
 [ 1. -3.]]
<class 'chainer.variable.Variable'>
[[1. 9.]
 [4. 4.]]

実行: Variableは履歴を記憶する

各Variableのインスタンスは自分を生成したFunctionを.creator以下に記憶している。それがNoneの場合、そのVariableインスタンスはrootと呼ばれる。

[37]:
x = Variable(np.asarray([[0, 2],[1, -3]]).astype(np.float32))

## y is created by MyFunc
y = my_func(x)
print(y.creator)

## z is created by F.sigmoid
z = F.sigmoid(x)
print(z.creator)

## x is created by user
print(x.creator)
<__main__.MyFunc object at 0x7f84c8845fd0>
<chainer.functions.activation.sigmoid.Sigmoid object at 0x7f84c884b5f8>
None

説明: Variableは誤差逆伝搬をネイティブサポートしている

誤差逆伝搬法(backpropagation)はニューラルネットワークを最適化する標準的な手法である。順方向計算のあと、出力に対して損失関数の値が勾配として与えられると、計算グラフを逆順に辿って、各中間ノードに対する勾配が与えられる。そして、その勾配情報を元に各パラメータの値が更新される。

Chainerでは、自動微分がサポートされ、かつ順方向計算中に全てのVariableインスタンスが記録されているため、勾配(.grad)をセットされた出力Variableにたいしてbackward()メソッドを呼ぶと、計算グラフ上をroot(つまり.creatorがNoneである入力ユニットのノード)まで遡って誤差逆伝搬が動作する。その後、Optimizerがモデル(のパラメータ)を更新する。

定義: 順方向計算としての2次式

前のSectionで示したように、順方向計算は、最終的な出力Variableを生成するための演算の連鎖と見なすことができる。その間、Chainerは全ての中間Variableインスタンスを記録している。

[ ]:
## A mock of forward computation
def forward(x):
    z = 2 * x
    y = x ** 2 - z + 1
    return y, z

実行: 勾配を与えるための逆方向計算

y.gradに勾配をセットしy.backward()を実行することで、勾配情報がxとzにまで伝わる。

[ ]:
x = Variable(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
y, z = forward(x)
y.grad = np.ones((2, 3), dtype=np.float32)
y.backward(retain_grad=True)
[40]:
## Gradient for x: 2*x - 2
print(x.grad)
[[ 0.  2.  4.]
 [ 6.  8. 10.]]
[41]:
## Gradient for z: -1
print(z.grad)
[[-1. -1. -1.]
 [-1. -1. -1.]]

3.3 LinkとChain

Functionは内部状態を持たない特定の演算のみを表していたため、ニューラルネットワーク中のレイヤーのように、パラメータを表す状態を持つ要素としては直接用いることができない。

Linkは、そのような演算へのラッパーとなり、状態を持つ。Linkが再利用可能なモジュールとして定義されるため、大きなネットワーク(後述するChainのインスタンス)の部品として用いることができる。多くの標準的なレイヤーはchianer.links中に用意されており、これまで用いてきたL.Linearと同様に、L.XYZというカタチで利用できる。

実行: Linkはパラメータを持つ状態付き関数

Linkクラスの多くのコンストラクタは内部パラメータと大きさを表すようないくつかの引数を持つ。それらのパラメータも、Variableのインスタンスとして表現される。

例えば、L.Linearは2つのパラメータ、係数行列のWとバイアス項のbを持つ。L.Linearのコンストラクタはそれらのサイズを決定する2つの引数を持つ。

[42]:
f = L.Linear(3, 2)
## Weight matrix for linear transformation (randomly initialized)
print(f.W.data)
## Bias term for linear transformation (initialized with zero)
print(f.b.data)
[[-0.40945035  1.0005482  -0.02247001]
 [ 0.09507062 -0.7451292   0.23615319]]
[0. 0.]

実行: Linkのインスタンスを使う

Linkのインスタンスは関数のように、直接呼び出すことができる。

[ ]:
## Apply linear transformation f()
x = Variable(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
y = f(x)
print(y.data)

実行: Linkに対して勾配を計算する

Link内のパラメータがVariableのインスタンスのため、逆方向計算はそれらにも勾配を与える。

[ ]:
## Initialize gradients of f
f.zerograds()
## Set gradient of y (= loss)
y.grad = np.ones((2, 2), dtype=np.float32)
## Backward computation
y.backward()
## Gradient for f.W and f.b
print(f.W.grad)
print(f.b.grad)
[[5. 7. 9.]
 [5. 7. 9.]]
[2. 2.]

定義: ChainerはLinkの集合、つまりネットワークを表す

以下のクラスはSection 1で扱ったMLP2そのものであるが、Chainクラスを継承している。ニューラルネットワークを表す基底クラスとして、Chainerはパラメータの管理や、CPU/GPU間のto_cpu()とto_gpu()、モデルのシリアライズとファイルへの保存/読み込みなどをサポートしている。

[ ]:
## 2-layer Multi-Layer Perceptron (MLP)
class MLP2(Chain):

    # Initialization of layers (Link)
    def __init__(self):
        super(MLP2, self).__init__(
            l1=L.Linear(784, 100),
            l2=L.Linear(100, 10),
        )

    # Forward computation by __call__
    def __call__(self, x):
        h1 = F.tanh(self.l1(x))
        y = self.l2(h1)
        return y

発展: Define-by-Runというアプローチ

既存ディープラーニングフレームワークのほとんどでは、モデルの構築と訓練は2つの分離したプロセスとして扱われる。具体的には訓練の前に、モデルの定義(モデル記述テキストやシンボリックなプログラム記述)に従って計算グラフを構築して固定している。この場合のモデル定義は、ディープラーニング用のドメイン特化言語(Domain Specific Language; DSL)の一種と考えられる。その後、訓練データセットを与えられ、実際の訓練プロセスに寄ってモデルを更新する。下の図はその2つのプロセスを表現している。我々はこれをDefine-and-Runと読んでいる。

Define-and-Runは極めて著感的で、かつ訓練の前に計算グラフを最適化しやすいという特徴をもっている。一方、そのデメリットもある。例えば、リカレントニューラルネットワークのようなモデルを記述するときには、特殊な文法を用いる必要がある。また、ネットワークが動的に変化する、例えば部分的に追加・削除される場合でも、可能性のある計算グラフの全てを訓練の最初から最後までメモリ上に保持しておく必要があるため、メモリ効率の面で最適とはいえない場合がある。

それゆえに、ChainerではDefine-by-Runを名付けた異なるアプローチを採用している。ここではモデルの定義が訓練と同時に順方向計算として与えられており、その場で計算グラフが構築される。これによってユーザーはループや条件分岐などを含む複雑なニューラルネットワークを、ホスト言語であるPythonの文法そのままを使って、容易に実装できる。また、Truncated BPTTのような計算グラフへの変更も、効率的に行われる。

さらなる詳細に興味のあるユーザーは論文を参照してほしい。


Section 3. まとめ

このnotebookではChainerを柔軟で直感的、かつパワフルなディープラーニングフレームワークとして紹介した。特に、最近の学術論文で提案されている複雑なニューラルネットワークモデルを容易に再実装したい時や、新しいアルゴリズムのプロトタイプを実装する際に、Chainerは威力を発揮する。

以下の画像は有名な 「A neural algorithm of Artistic style」という論文のChainer実装によって生成されたものである。

猫のコンテンツ画像が、横にある様々なスタイル画像と同様の画風となるように再描画されているのがわかる。

これは単なる一例に過ぎないが、githubのコードを見るとこのような面白いモデルがChainerを使って数百行で再実装できていることがお分かりいただけると思う。 その他にも、ユーザーが様々なユースケースで作成したコード例のリストがあるので、参考になれば幸いである。


これで本notebookは終了である。さらなる詳細は公式チュートリアルを参照されたい。