In [1]:
import chainer
import chainer.functions as F
import chainer.links as L
from chainer import training
from chainer.training import extensions

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
import pandas as pd
%matplotlib inline

Python 基礎

iPython (or jupyter) のヘルプテクニック

これを見ている人は jupyter notebook を使っていると思うが、次のコードを動かしてみてほしい。


In [2]:
np.array?


Docstring:
array(object, dtype=None, copy=True, order=None, subok=False, ndmin=0)

Create an array.

Parameters
----------
object : array_like
    An array, any object exposing the array interface, an
    object whose __array__ method returns an array, or any
    (nested) sequence.
dtype : data-type, optional
    The desired data-type for the array.  If not given, then
    the type will be determined as the minimum type required
    to hold the objects in the sequence.  This argument can only
    be used to 'upcast' the array.  For downcasting, use the
    .astype(t) method.
copy : bool, optional
    If true (default), then the object is copied.  Otherwise, a copy
    will only be made if __array__ returns a copy, if obj is a
    nested sequence, or if a copy is needed to satisfy any of the other
    requirements (`dtype`, `order`, etc.).
order : {'C', 'F', 'A'}, optional
    Specify the order of the array.  If order is 'C', then the array
    will be in C-contiguous order (last-index varies the fastest).
    If order is 'F', then the returned array will be in
    Fortran-contiguous order (first-index varies the fastest).
    If order is 'A' (default), then the returned array may be
    in any order (either C-, Fortran-contiguous, or even discontiguous),
    unless a copy is required, in which case it will be C-contiguous.
subok : bool, optional
    If True, then sub-classes will be passed-through, otherwise
    the returned array will be forced to be a base-class array (default).
ndmin : int, optional
    Specifies the minimum number of dimensions that the resulting
    array should have.  Ones will be pre-pended to the shape as
    needed to meet this requirement.

Returns
-------
out : ndarray
    An array object satisfying the specified requirements.

See Also
--------
empty, empty_like, zeros, zeros_like, ones, ones_like, fill

Examples
--------
>>> np.array([1, 2, 3])
array([1, 2, 3])

Upcasting:

>>> np.array([1, 2, 3.0])
array([ 1.,  2.,  3.])

More than one dimension:

>>> np.array([[1, 2], [3, 4]])
array([[1, 2],
       [3, 4]])

Minimum dimensions 2:

>>> np.array([1, 2, 3], ndmin=2)
array([[1, 2, 3]])

Type provided:

>>> np.array([1, 2, 3], dtype=complex)
array([ 1.+0.j,  2.+0.j,  3.+0.j])

Data-type consisting of more than one element:

>>> x = np.array([(1,2),(3,4)],dtype=[('a','<i4'),('b','<i4')])
>>> x['a']
array([1, 3])

Creating an array from sub-classes:

>>> np.array(np.mat('1 2; 3 4'))
array([[1, 2],
       [3, 4]])

>>> np.array(np.mat('1 2; 3 4'), subok=True)
matrix([[1, 2],
        [3, 4]])
Type:      builtin_function_or_method

iPython では ? をつけて実行することで、 Docstring (各プログラムの説明文) を簡単に参照することができる。
もし「関数はしってるんだけど、引数が分からない」という場合に試してみよう。

なお Python スクリプトにおける Docstring の書き方は

def hogehoge():
    """ docstring here! """
    return 0

である。(試しに次も実行してみよう)


In [3]:
def hogehoge():
    """ docstring here! """
    return 0

hogehoge?


Signature: hogehoge()
Docstring: docstring here!
File:      ~/Study/student/cs2/<ipython-input-3-c58cae9d5c38>
Type:      function

初めてのオブジェクト指向言語

Python は オブジェクト指向言語 かつ ライブラリを使う上で必須の知識 のため、簡単におさらいする。
class を簡単に言えば「C言語における構造体の発展させ、変数だけでなく関数も内包できて、引き継ぎもできるやつ」である

メモリ付き電卓を例に、以下のようなクラスを用意した。


In [4]:
class memory_sum:
     c = None
     def __init__(self, a):
         self.a = a
         print("run __init__ ")
     def __call__(self, b):
         self.c = b + self.a
         print("__call__\t:", b, "+", self.a, "=", self.c)
     def show_sum(self):
         print("showsum()\t:", self.c)

クラスは一種の 枠組み なので 実体を用意(インスタンス) する

このとき コンストラクタ と呼ばれる、インスタンスの初期化関数 def __init__() が実行される


In [5]:
A = memory_sum(15)


run __init__ 

インスタンスは特に関数を呼び出されない場合 def __call__() が実行される


In [6]:
A(30)


__call__	: 30 + 15 = 45

もちろん関数を呼び出すこともできる


In [7]:
A.show_sum()


showsum()	: 45

またインスタンス内の変数へ、直接アクセスできる


In [8]:
A.c


Out[8]:
45

クラスの引き継ぎ(継承)とは、すでに定義済みのクラスを引用することである。

例として memory_sum を引きついで、引き算機能もつけた場合以下のようになる。


In [9]:
class sum_sub(memory_sum):
    def sum(self, a, b):
        self.a = a
        self.b = b
        self.c = a + b
        print(self.c)
        
    def sub(self, a, b):
        self.a = a
        self.b = b
        self.c = a - b
        print(self.c)
        
    def show_result(self):
        print(self.c)

In [10]:
B = sum_sub(30)


run __init__ 

In [11]:
B.sum(30, 10)


40

In [12]:
B.sub(30, 10)


20

In [13]:
B.show_result()


20

sum_submemory_sum を継承しているため、 memory_sum で定義した関数も利用できる


In [14]:
B.show_sum()


showsum()	: 20

これだけ知っていれば Chainer のコードも多少は読めるようになる。

オブジェクト指向に興味が湧いた場合は、

がおすすめである(特にオブジェクト指向とJavaの結びつきは強いので、言語違いとは言わず読んでみてほしい)


Chainer

活性化関数の確認

chainer.functions では活性化関数や損失関数など基本的な関数が定義されている

ReLU (ランプ関数)

隠れ層の活性化関数として、今日用いられている関数。
入力値が $0$ 以下なら $0$ を返し、$0$ 以上なら入力値をそのまま出力するだけの関数。

$$ ReLU(x)=\max(0, x) $$

「数学的に微分可能なの?」と疑問になった方は鋭く、数学的には微分可能ではないものの、微分は以下のように定義している(らしい)。

$$ \frac{d ReLU(x)}{dx} = (if ~ 0 \leq x)~ 1,~ (else)~ 0 $$

In [15]:
arr = np.arange(-10, 10, 0.1)
arr1 = F.relu(arr, use_cudnn=False)
plt.plot(arr, arr1.data)


Out[15]:
[<matplotlib.lines.Line2D at 0x119c275c0>]

Sigmoid 関数

みんな大好きシグモイド関数

$$ sigmoid(x)= \frac{1}{1 + \exp(-x)} $$

In [16]:
arr = np.arange(-10, 10, 0.1)
arr2 = F.sigmoid(arr, use_cudnn=False)
plt.plot(arr, arr2.data)


Out[16]:
[<matplotlib.lines.Line2D at 0x11a144c88>]

softmax 関数

softmax は正規化指数関数とも言われ、値を確率にすることができる。

$$ softmax(x_i) = \frac{exp(x_i)}{\sum_j exp(x_j)} $$

また損失関数として交差エントロピーと組み合わせることで、多クラス分類を行なうことができる。

chainer.links.Classifier() における実装において、デフォルトが softmax_cross_entropy である


In [17]:
arr = chainer.Variable(np.array([[-5.0, 0.5, 6.0, 10.0]], dtype=np.float32))
plt.plot(F.softmax(arr).data[0])
print("softmax適用後の値: ", F.softmax(arr).data[0])
print("総和: ", sum(F.softmax(arr).data[0]))


softmax適用後の値:  [  3.00378133e-07   7.35001013e-05   1.79848839e-02   9.81941342e-01]
総和:  1.00000002669

Variable クラスについて

chainer においては、通常の配列や numpy配列 や cupy配列 をそのまま使うのではなく、 Variable というクラスを利用する
(chianer 1.1 以降は自動的に Variable クラスにラッピングされるらしい)

Variable クラスではデータアクセスや勾配計算などを容易に行える。

順伝搬


In [18]:
x1 = chainer.Variable(np.array([1]).astype(np.float32))
x2 = chainer.Variable(np.array([2]).astype(np.float32))
x3 = chainer.Variable(np.array([3]).astype(np.float32))

試しに下式を計算する(順方向の計算) $$ y = (x_1 - 2 x_2 - 1)^2 + (x_2 x_3 - 1)^2 + 1 $$

各パラメータを当てはめると $$ y = (1 - 2 \times 2 - 1)^2 + (2 \times 3 - 1)^2 + 1 = (-4)^2 + 5^2 + 1 = 42$$


In [19]:
y = (x1 - 2 * x2 - 1)**2 + (x2 * x3 - 1)**2 + 1
y.data


Out[19]:
array([ 42.], dtype=float32)

逆伝搬

では今度は y の微分値を求める(逆方向の計算)


In [20]:
y.backward()
$$ \frac{\delta y}{\delta x_1} = 2(x_1 - 2 x_2 - 1) = 2(1 - 2 \times 2 - 1) = -8$$

In [21]:
x1.grad


Out[21]:
array([-8.], dtype=float32)
$$ \frac{\delta y}{\delta x_2} = -4 (x_1 - 2 x_2 - 1) + 2 x_3 ( x_2 x_3 - 1) = -4 (1 - 2 \times 2 - 1) + 2 \times 3 ( 2 \times 3 - 1) = 46 $$

In [22]:
x2.grad


Out[22]:
array([ 46.], dtype=float32)
$$ \frac{\delta y}{\delta x_3} = 2 x_2 ( x_2 x_3 - 1) = 2 \times 2 (2 \times 3 - 1) = 20$$

In [23]:
x3.grad


Out[23]:
array([ 20.], dtype=float32)

chainer.linkschainer.Variable のサブセットのような存在

ニューラルネットにおいて、ある層から次の層へデータを変換する(線形作用素)関数は

$$ \boldsymbol{y} = W \boldsymbol{x} + \boldsymbol{b} $$

として表現でき、Chainer においては次のように表される。


In [24]:
l = L.Linear(2, 3)

In [25]:
l.W.data


Out[25]:
array([[ 0.02085289, -0.21861134],
       [-0.82913452,  0.27013522],
       [ 0.09521807,  0.45233259]], dtype=float32)

In [26]:
l.b.data


Out[26]:
array([ 0.,  0.,  0.], dtype=float32)

In [27]:
x = chainer.Variable(np.array(range(4)).astype(np.float32).reshape(2,2))
y = l(x)
y.data


Out[27]:
array([[-0.21861134,  0.27013522,  0.45233259],
       [-0.61412823, -0.84786338,  1.54743385]], dtype=float32)
$$ \boldsymbol{y} = W \boldsymbol{x} + \boldsymbol{b} $$

に当てはめ、確認すると同一であることがわかる


In [28]:
x.data.dot(l.W.data.T) + l.b.data # bias は 0 なので足しても足さなくとも同じ


Out[28]:
array([[-0.21861134,  0.27013522,  0.45233259],
       [-0.61412823, -0.84786338,  1.54743385]], dtype=float32)

データセットの読み込み


In [29]:
train, test = chainer.datasets.get_mnist()

traintest の型を確認する


In [30]:
type(train)


Out[30]:
chainer.datasets.tuple_dataset.TupleDataset

chainer.datasets.tuple_dataset.TupleDataset ということがわかる
(実は chainer.datasets.get_mnist() を確認すれば自明である)

では train の中身はどうか


In [31]:
print(len(train[0][0]))
print(type(train[0][0]))


784
<class 'numpy.ndarray'>

In [32]:
print(train[0][1])
print(type(train[0][1]))


5
<class 'numpy.int32'>

単純に 画像データ正解ラベル がセットになっている

なので画像データの方を reshape(28, 28) することで、画像として表示することも可能である
(ただし 0 - 255 の値ではなく 0.0 - 1.0 になっているので注意)

また chainer.datasets.get_mnist(ndim=2) とした場合は reshape 不要になる


In [33]:
plt.imshow(train[0][0].reshape(28,28))
plt.gray() # gray scale にする
plt.grid()



In [34]:
train_iter = chainer.iterators.SerialIterator(train, 100)
np.shape(train_iter.dataset)


Out[34]:
(60000, 2)

出力ファイルの処理

# Dump a computational graph from 'loss' variable at the first iteration
    # The "main" refers to the target link of the "main" optimizer.
    trainer.extend(extensions.dump_graph('main/loss'))

    # Write a log of evaluation statistics for each epoch
    trainer.extend(extensions.LogReport())

で出力される logcg.dot の処理方法についてここでは簡単に紹介する。

log

実は log は単なる json ファイルなのだが、馴染みがない人にとっては扱い方が地味面倒である。
ここでは pandas を利用し、データを処理、グラフ化してみる。

すでに import pandas as pd されており、 ./result/log にターゲットとなる log があるという状態において


In [35]:
log = pd.read_json('./result/log')

とするだけで json ファイルの読み込みは完了である。
次に log のテーブルを見てみると、


In [36]:
log


Out[36]:
elapsed_time epoch iteration main/accuracy main/loss validation/main/accuracy validation/main/loss
0 36.811424 1 600 0.941200 0.194004 0.9692 0.094925
1 71.671824 2 1200 0.976733 0.074369 0.9722 0.081449
2 115.297723 3 1800 0.984650 0.047661 0.9777 0.072641
3 154.256558 4 2400 0.988167 0.036482 0.9758 0.080170
4 193.438113 5 3000 0.990600 0.029105 0.9766 0.080211
5 230.734039 6 3600 0.991917 0.024174 0.9784 0.085389
6 270.462110 7 4200 0.993433 0.020381 0.9811 0.083578
7 310.094386 8 4800 0.993667 0.019233 0.9813 0.078592
8 349.393030 9 5400 0.994200 0.018419 0.9837 0.071666
9 388.633428 10 6000 0.995333 0.014344 0.9827 0.072558
10 427.906644 11 6600 0.995667 0.013155 0.9802 0.089228
11 466.785801 12 7200 0.995100 0.015256 0.9824 0.088098
12 507.220090 13 7800 0.997333 0.009558 0.9825 0.081367
13 549.933164 14 8400 0.996417 0.011274 0.9827 0.085351
14 589.219334 15 9000 0.996483 0.011474 0.9782 0.119885
15 629.917074 16 9600 0.996883 0.010903 0.9813 0.089161
16 672.134955 17 10200 0.997700 0.007548 0.9817 0.095545
17 715.216980 18 10800 0.996933 0.010962 0.9826 0.095049
18 759.905002 19 11400 0.996783 0.010007 0.9829 0.084876
19 803.555149 20 12000 0.997983 0.006782 0.9801 0.118473
# Print selected entries of the log to stdout
    # Here "main" refers to the target link of the "main" optimizer again, and
    # "validation" refers to the default name of the Evaluator extension.
    # Entries other than 'epoch' are reported by the Classifier link, called by
    # either the updater or the evaluator.
    trainer.extend(extensions.PrintReport(
        ['epoch', 'main/loss', 'validation/main/loss',
         'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))

によってコンソールに出力されたものと同じものが出て来ることがわかる。

では値だけでは分からないので、訓練とテストの過程をグラフ化してみよう。


In [37]:
epoch = log['epoch']
plt.plot(epoch, log['main/accuracy'])
plt.plot(epoch, log['validation/main/accuracy'])
plt.xlabel('epoch')
plt.ylabel('accuracy')
plt.legend(loc='best')


Out[37]:
<matplotlib.legend.Legend at 0x11768a710>

cg.dot

cg.dot は DOT言語 で書かれており、 Wikipedia によれば、

DOT言語

DOT は、プレーンテキストを用いてデータ構造としてのグラフを表現するための、データ記述言語の一種である。コンピュータで処理しやすく、かつ目で見ても分かり易い、単純化された形式でグラフを記述することができる。DOT言語で書かれたデータのファイルは、拡張子として .dot を付けることが多い。 DOT言語の処理系は多数実装されているが、どれもDOT言語の記述をファイルから読み込み、画像を生成するか、グラフを操作することができるようになっている。そのひとつ dot はドキュメンテーションジェネレータのdoxygenで使われている。dot は Graphviz パッケージの一部である。

とのことなので、理屈ではテキストエディタで開くこともできるが、おそらく把握できないと思われるので素直に画像化しよう。

ソフトインストール

画像化するソフトとして graphviz があるので、ダウンロードページからソフトをダウンロードしインストールすれば良い。
また Linux/Mac であれば CLI ツールをターミナルからインストールすることも可能である。

apt install graphviz
yum install graphviz
brew install graphviz

ここでは ubuntu 14.04 に apt install graphviz (CLIツール) をインストールした場合で紹介する。
dot -> png 画像にする場合は、

dot -Tpng cg.dot -o cg.png

とするだけである。他のコマンドライン引数などの詳細については

man dot
dot --help

で参照してほしい。

どうしてもソフトをインストールすることができない場合 は、 webgraphvizcg.dot の中身をコピペすれば画像化することも可能である。

画像化例

train_mnist.py で MLP を実行した際のグラフ


演習で使用したコードについて

train_mnist.py を元に、CNN も追加したコードを記載する。

#!/usr/bin/env python
from __future__ import print_function
import argparse

import chainer
import chainer.functions as F
import chainer.links as L
from chainer import training
from chainer.training import extensions


# Network definition
class MLP(chainer.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))
        return self.l3(h2)

class CNN(chainer.Chain):
    def __init__(self, n_unit, n_out):
        super(CNN, self).__init__(
            conv1=L.Convolution2D(None, 16, ksize=3),
            conv2=L.Convolution2D(None, 32, ksize=3),
            conv3=L.Convolution2D(None, 64, ksize=3),
            l1=L.Linear(None, n_unit), # 出力数の把握のために数値を入れるとベター、 Noneちょっとズル
            l2=L.Linear(None, n_out)
        )
    def __call__(self, x):
        h = F.max_pooling_2d(F.relu(self.conv1(x)), 2)
        h = F.max_pooling_2d(F.relu(self.conv2(h)), 2)
        h = F.max_pooling_2d(F.relu(self.conv3(h)), 2)
        h = F.dropout(F.relu(self.l1(h)))
        return self.l2(h)

def main():
    parser = argparse.ArgumentParser(description='Chainer example: MNIST')
    parser.add_argument('--mode', '-m', type=str, default="MLP",
                        help='MLP or CNN')
    parser.add_argument('--batchsize', '-b', type=int, default=100,
                        help='Number of images in each mini-batch')
    parser.add_argument('--epoch', '-e', type=int, default=20,
                        help='Number of sweeps over the dataset to train')
    parser.add_argument('--gpu', '-g', type=int, default=-1,
                        help='GPU ID (negative value indicates CPU)')
    parser.add_argument('--out', '-o', default='result',
                        help='Directory to output the result')
    parser.add_argument('--resume', '-r', default='',
                        help='Resume the training from snapshot')
    parser.add_argument('--unit', '-u', type=int, default=1000,
                        help='Number of units')
    args = parser.parse_args()

    print('MODE:{}'.format(args.mode))
    print('GPU: {}'.format(args.gpu))
    print('# unit: {}'.format(args.unit))
    print('# Minibatch-size: {}'.format(args.batchsize))
    print('# epoch: {}'.format(args.epoch))
    print('')

    # Set up a neural network to train
    # Classifier reports softmax cross entropy loss and accuracy at every
    # iteration, which will be used by the PrintReport extension below.

    if   args.mode == 'CNN':
        model = L.Classifier(CNN(args.unit, 10))
        # Load the MNIST dataset
        train, test = chainer.datasets.get_mnist(ndim=3) # channel, width and height
        args.out = 'CNN_result' if args.out == 'result' else args.out
    elif args.mode == 'MLP':
        model = L.Classifier(MLP(args.unit, 10))
        # Load the MNIST dataset
        train, test = chainer.datasets.get_mnist(ndim=1)
    else:
        from sys import exit
        print("Missing Classifier {} (exit)".format(args.mode))
        exit()

    if args.gpu >= 0:
        chainer.cuda.get_device(args.gpu).use()  # Make a specified GPU current
        model.to_gpu()  # Copy the model to the GPU

    # Setup an optimizer
    optimizer = chainer.optimizers.Adam()
    optimizer.setup(model)

    # Load the MNIST dataset
    train_iter = chainer.iterators.SerialIterator(train, args.batchsize)
    test_iter = chainer.iterators.SerialIterator(test, args.batchsize,
                                                 repeat=False, shuffle=False)

    # Set up a trainer
    updater = training.StandardUpdater(train_iter, optimizer, device=args.gpu)
    trainer = training.Trainer(updater, (args.epoch, 'epoch'), out=args.out)

    # Evaluate the model with the test dataset for each epoch
    trainer.extend(extensions.Evaluator(test_iter, model, device=args.gpu))

    # Dump a computational graph from 'loss' variable at the first iteration
    # The "main" refers to the target link of the "main" optimizer.
    trainer.extend(extensions.dump_graph('main/loss'))

    # Take a snapshot at each epoch
    trainer.extend(extensions.snapshot(), trigger=(args.epoch, 'epoch'))

    # もし分類器を再利用したい場合、以下を追記すると model と optimizer を書き出す
    # trainer.extend(extensions.snapshot_object(model, 'model_iter_{.updater.iteration}'), trigger=(1, 'epoch'))
    # trainer.extend(extensions.snapshot_object(optimizer, 'optimizer_iter_{.updater.iteration}'), trigger=(1, 'epoch'))

    # Write a log of evaluation statistics for each epoch
    trainer.extend(extensions.LogReport())

    # Print selected entries of the log to stdout
    # Here "main" refers to the target link of the "main" optimizer again, and
    # "validation" refers to the default name of the Evaluator extension.
    # Entries other than 'epoch' are reported by the Classifier link, called by
    # either the updater or the evaluator.
    trainer.extend(extensions.PrintReport(
        ['epoch', 'main/loss', 'validation/main/loss',
         'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))

    # Print a progress bar to stdout
    trainer.extend(extensions.ProgressBar())

    if args.resume:
        # Resume from a snapshot
        chainer.serializers.load_npz(args.resume, trainer)

    # Run the training
    trainer.run()

if __name__ == '__main__':
    main()