機械学習
DeepLearning
python3
Chainer

ライブラリを使ってディープラーニングを構築する ~chainer編

■0.はじめに

ディープラーニングを基本から学ぶ

こちらで、ゼロからディープラーニングを実装することを学びました。
実務的にはライブラリを使ってとなるのが現実的かと思い、ライブラリを使ったディープラーニングを構築してみようと考えました。
scikit-learn編に続き、第二弾はchainerです。

■1.chainer

Chainer (チェイナー) は、ニューラルネットワークの計算および学習を行うためのオープンソースソフトウェアライブラリである。 バックプロパゲーションに必要なデータ構造をプログラムの実行時に動的に生成する特徴があり、複雑なニューラルネットワークの構築を必要とする深層学習で主に用いられる。 Python2.x系および3.x系から利用でき、GPU による演算をサポートしている。 Preferred Networks の主導で開発が進められており、 Preferred Networks が日本の機械学習系のベンチャー企業であることから、日本国内を中心として利用が広がっている。
Wikipediaより

■2.環境

Windows 10
Anaconda 4.4.1
Python 3.6.1
Chainer 2.0.1

■3.使用データ

MNIST
・28x28ピクセルからなる手書き数字(0~9)のデータセット
・機械学習のサンプルデータとしてよく使われる
・データ数は70,000枚

■4.実装

▼4.1.データ準備

まずは入力データを準備します。
scikit-learnのようにchainerでもMNISTをロードするI/Fが用意されています。

# MNISTのデータセットをLoadする
train, test = chainer.datasets.get_mnist()

このようにget_mnist()の引数を全て省略すると下記のデフォルト値が設定されます。

chainer.datasets.get_mnist(withlabel=True, ndim=1, scale=1.0, dtype=, label_dtype=, rgb_format=False)

[参考]
chainer.datasets.get_mnist — Chainer 3.1.0 documentation

withlabel=True とすると、データセットにラベルが付加されます。
付加しない場合はFalseにします。

ndim=1 とすると、画像データ部の次元が一次元(784, )になります。
2 だと元画像と同じ形式の二次元(28, 28)
3 だとチャネル方向が追加された三次元(1, 28, 28)となります。
ndim=3とし、rgb_format=True とすると、チャネル方向がRGBとなった三次元(3, 28, 28)となります。

get_mnist()の戻り値として、chainer.datasets.TupleDataset型のデータセットが2つ返されるので、それぞれ学習用と評価用にセットします。
データ数は60,000と10,000、これはMNIST自体のデータ仕様。

# ミニバッチサイズを定義する
batch_size = 100
# Iterator生成
train_iter = chainer.iterators.SerialIterator(train, batch_size)
test_iter = chainer.iterators.SerialIterator(test, batch_size, repeat=False, shuffle=False)

取得したMNISTのデータセットをIteratorに格納します。
SerialIteratorは、データセットの中のデータを順番に取り出してくるシンプルなIteratorです。

class chainer.iterators.SerialIterator(dataset, batch_size, repeat=True, shuffle=True)

[参考]
chainer.iterators.SerialIterator — Chainer 3.1.0 documentation

データセットからbatch_sizeで指定された数のデータを束ねてミニバッチとして返します。
repeatにFalseを指定すると、データセットを一巡したらイテレーション終了となります。
shuffleにFalseを指定すると、エポックごとにデータセットの順番をシャッフルします。
今回の実装では、評価用(test_iter)は繰り返し使う必要もありませんし、シャッフルする必要もありませんので、両方Falseとしています。

▼4.2.ニューラルネットワークの定義

続いて、学習・予測に使用するニューラルネットワークを定義します。
3層構造で、活性化関数にReLUを用いています。

# ニューラルネットワークの定義
class MLP(chainer.Chain):

    def __init__(self, n_in, n_units, n_out):
        super(MLP, self).__init__()
        with self.init_scope():
            self.link1 = chainer.links.Linear(n_in, n_units)
            self.link2 = chainer.links.Linear(None, n_units)
            self.link3 = chainer.links.Linear(None, n_out)

    def __call__(self, x):
        h1 = chainer.functions.relu(self.link1(x))
        h2 = chainer.functions.relu(self.link2(h1))
        return self.link3(h2)

chainer.links.Linearは全結合層を表すクラスです。
初期化時の型はchainer.Variableとなります。

class chainer.links.Linear(in_size, out_size=None, nobias=False, initialW=None, initial_bias=None)

[参考]
chainer.links.Linear — Chainer 3.1.0 documentation

in_size=Noneとした場合は前の層の出力サイズを引き継いでくれるようです。
最初の層でも上手いことやってくれるらしいですが、この実装では明示的に指定しています。

chainer.functions.reluは、名前の通りReLU関数が実装されています。

chainer.functions.relu(x)
Parameters: x (Variable or numpy.ndarray or cupy.ndarray) – Input variable. A (s1,s2,...,sN)
-shaped float array.

[参考]
chainer.functions.relu — Chainer 3.1.0 documentation

# 分類モデルを定義
model = chainer.links.Classifier(MLP(784, 100, 10))

chainer.links.Classifierは、シンプルな分類モデルです。
先ほど定義したニューラルネットワークを引数に渡すことで、それを使用した分類モデルを定義できます。

class chainer.links.Classifier(predictor, lossfun=, accfun=, label_key=-1)

他には、lossfunで損失関数を、accfunで評価関数を指定することができます。

[参考]
chainer.links.Classifier — Chainer 3.2.0 documentation

label_keyは使い方がよくわからないです...

# 最適化手法の設定
optimizer = chainer.optimizers.Adam()
optimizer.setup(model)

パラメータの最適化手法はchainer.optimizersから選択して、定義したニューラルネットワークに適用します。

Optimizers — Chainer 3.2.0 documentation

他にはSGD、Momentum SGD、AdaGradなどが使えます。

▼4.3.学習と評価

続いて学習と評価の準備です。

max_epoch = 10
updater = training.StandardUpdater(train_iter, optimizer)
trainer = training.Trainer(updater, (max_epoch, 'epoch'))

StandardUpdaterは、その名の通りパラメータの更新を司るStandardなUpdaterです。
学習データのiteratorと先に設定したoptimizerを渡しています。

class chainer.training.StandardUpdater(iterator, optimizer, converter=, device=None, loss_func=None)

[参考]
chainer.training.StandardUpdater — Chainer 3.2.0 documentation

Trainerは、学習の実行を司ります。
先に設定したUpdaterを指定し、終了条件としてエポック数を指定しています。

class chainer.training.Trainer(updater, stop_trigger=None, out='result')

上記でいうstop_triggerが終了条件です。
outはログなどの出力ディレクトリです。

[参考]
chainer.training.Trainer — Chainer 3.2.0 documentation

最後に評価内容を設定します。
具体的にはTrainerクラスのextend関数を使い、評価内容を設定していきます。

# テストデータセットに対する現在のモデルを評価します
trainer.extend(extensions.Evaluator(test_iter, model))
# 報告された値を蓄積し、ログファイルを出力ディレクトリに書き出します
trainer.extend(extensions.LogReport(log_name="result.log", trigger=(1, 'epoch')))
# 進捗状況を可視化します
trainer.extend(extensions.ProgressBar())
# LogReportの内容を標準出力する
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy', 'validation/main/loss', 'validation/main/accuracy', 'elapsed_time']))
# ネットワークの形をグラフで表示できるようにdot形式で保存する(これをgraphvizで画像形式に変換できる)
trainer.extend(extensions.dump_graph('main/loss'))
# Trainerのout引数で指定した出力ディレクトリにTrainerオブジェクトを指定されたタイミング(デフォルトでは1エポックごと)に保存します。
trainer.extend(extensions.snapshot(filename='snapshot_epoch-{.updater.epoch}'))
# 学習済モデルを保存する
trainer.extend(extensions.snapshot_object(model.predictor, filename='model_epoch-{.updater.epoch}'))
# 引数のリストで指定された値の変遷をmatplotlibライブラリを使ってグラフに描画し、画像として保存する
trainer.extend(extensions.PlotReport(['main/loss', 'validation/main/loss'], x_key='epoch', file_name='loss.png'))
trainer.extend(extensions.PlotReport(['main/accuracy', 'validation/main/accuracy'], x_key='epoch', file_name='accuracy.png'))

extend関数の引数に渡しているのは、chainer.training.extensionsパッケージ下のクラスです。
これらにない評価をしたい場合は自作することもできるみたいです。

# 学習(訓練)実行
trainer.run()

最後にTrainerを実行します。

▼4.4.学習結果から予測する

学習させたモデルを使用して予測をしてみます。

# テストデータからランダムに抜き出す
idx = np.random.choice(len(test), 100)
for i in idx:
    # 入力データ(画像データ)
    x = test[i][0]
    # 予測結果を取得
    y_ = np.argmax(model.predictor(x=x.reshape(1,len(x))).data)
    # ラベルデータ
    y = test[i][1]
    # ラベルデータと予測結果が異なっている場合は入力データ(画像データ)を出力
    if(y != y_):
        plt.imshow(x.reshape(28, 28), cmap='gray')
        # ファイル名:label_(ラベル値)_predict_(予測値).png
        file_name = "label_" + str(y) + "_predict_" + str(y_) + ".png"
        plt.savefig(file_name)

ある程度エポックを重ねていくと認識精度はほぼほぼ100%になりますが、予測させてみるといくつか間違ってますね。

image.png

正解4、予測2

image.png

正解6、予測1

人の目でも正解するのは難しいレベルですから、まあしょうがないですね...

▼4.5.全実装

これまで説明してきた内容の全実装です。

# coding: utf-8
import numpy as np
import timeit
import matplotlib.pyplot as plt

import chainer
from chainer import training
from chainer.training import extensions

# ニューラルネットワークの定義
class MLP(chainer.Chain):

    def __init__(self, n_in, n_units, n_out):
        super(MLP, self).__init__()
        with self.init_scope():
            self.link1 = chainer.links.Linear(n_in, n_units)
            self.link2 = chainer.links.Linear(None, n_units)
            self.link3 = chainer.links.Linear(None, n_out)

    def __call__(self, x):
        h1 = chainer.functions.relu(self.link1(x))
        h2 = chainer.functions.relu(self.link2(h1))
        return self.link3(h2)

# MNISTのデータセットをLoadする
train, test = chainer.datasets.get_mnist()

# ミニバッチサイズを定義する
batch_size = 100
# Iterator生成
train_iter = chainer.iterators.SerialIterator(train, batch_size)
test_iter = chainer.iterators.SerialIterator(test, batch_size, repeat=False, shuffle=False)

# 分類モデルを定義
model = chainer.links.Classifier(MLP(784, 100, 10))

# 最適化手法の設定
optimizer = chainer.optimizers.Adam()
optimizer.setup(model)

max_epoch = 10
updater = training.StandardUpdater(train_iter, optimizer)
trainer = training.Trainer(updater, (max_epoch, 'epoch'))

# テストデータセットに対する現在のモデルを評価します
trainer.extend(extensions.Evaluator(test_iter, model))
# 報告された値を蓄積し、ログファイルを出力ディレクトリに書き出します
trainer.extend(extensions.LogReport(log_name="result.log", trigger=(1, 'epoch')))
# 進捗状況を可視化します
trainer.extend(extensions.ProgressBar())
# LogReportの内容を標準出力する
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy', 'validation/main/loss', 'validation/main/accuracy', 'elapsed_time']))
# ネットワークの形をグラフで表示できるようにdot形式で保存する(これをgraphvizで画像形式に変換できる)
trainer.extend(extensions.dump_graph('main/loss'))
# Trainerのout引数で指定した出力ディレクトリにTrainerオブジェクトを指定されたタイミング(デフォルトでは1エポックごと)に保存します。
#trainer.extend(extensions.snapshot(filename='snapshot_epoch-{.updater.epoch}'))
# 学習済モデルを保存する
trainer.extend(extensions.snapshot_object(model.predictor, filename='model_epoch-{.updater.epoch}'))
# 引数のリストで指定された値の変遷をmatplotlibライブラリを使ってグラフに描画し、画像として保存する
trainer.extend(extensions.PlotReport(['main/loss', 'validation/main/loss'], x_key='epoch', file_name='loss.png'))
trainer.extend(extensions.PlotReport(['main/accuracy', 'validation/main/accuracy'], x_key='epoch', file_name='accuracy.png'))

# 学習(訓練)実行
trainer.run()

# 学習させたモデルを使用して予測を行う

# テストデータからランダムに抜き出す
idx = np.random.choice(len(test), 100)
for i in idx:
    # 入力データ(画像データ)
    x = test[i][0]
    # 予測結果を取得
    y_ = np.argmax(model.predictor(x=x.reshape(1,len(x))).data)
    # ラベルデータ
    y = test[i][1]
    # ラベルデータと予測結果が異なっている場合は入力データ(画像データ)を出力
    if(y != y_):
        plt.imshow(x.reshape(28, 28), cmap='gray')
        # ファイル名:label_(ラベル値)_predict_(予測値).png
        file_name = "label_" + str(y) + "_predict_" + str(y_) + ".png"
        plt.savefig(file_name)

■5.畳み込みニューラルネットワークを実装してみる

chainerでは畳み込みニューラルネットワークを実装するI/Fも用意されていました。
これを使うと、これまでの実装をほとんど変えることなく畳み込みニューラルネットワークにも対応することができました。

ディープラーニングを基本から学ぶ Part5 畳み込みニューラルネットワーク - Qiita

畳み込みニューラルネットワークの理論については上記を参考にしてください。

▼5.1.畳み込みニューラルネットワークの定義

まずは畳みこみニューラルネットワーククラスを定義します。
畳み込み層×2→全結合層×2→出力層の全5層からなるニューラルネットワークです。

# 畳みこみニューラルネットワークの定義
class CNN(chainer.Chain):

    def __init__(self, class_labels=10):
        super(CNN, self).__init__()
        with self.init_scope():
            self.conv1 = chainer.links.Convolution2D(None, 20, 3) # 入力チャネル数、出力チャネル数、フィルタサイズ
            self.conv2 = chainer.links.Convolution2D(None, 50, 3)
            self.link1 = chainer.links.Linear(None, 500) 
            self.link2 = chainer.links.Linear(None, 500)
            self.link3 = chainer.links.Linear(None, class_labels)

    def __call__(self, x):
        h1 = chainer.functions.max_pooling_2d(chainer.functions.relu(self.conv1(x)), 2)
        h2 = chainer.functions.max_pooling_2d(chainer.functions.relu(self.conv2(h1)), 2)
        h3 = chainer.functions.relu(self.link1(h2))
        h4 = chainer.functions.relu(self.link2(h3))
        return self.link3(h4)

chainer.links.Convolution2D が畳み込み層。

class chainer.links.Convolution2D(self, in_channels, out_channels, ksize=None, stride=1, pad=0, nobias=False, initialW=None, initial_bias=None)

本実装では入力チャネル数、出力チャネル数、フィルタサイズを設定しています。
フィルタサイズがksizeですね。

chainer.functions.max_pooling_2dはプーリングを司るレイヤで、Max値を取るプーリング方法です。

chainer.functions.max_pooling_2d(x, ksize, stride=None, pad=0, cover_all=True)

本実装では、活性化関数ReLUを通した値を入力として、フィルタサイズ2のプーリングを適用しています。

[参考]
chainer.links.Convolution2D — Chainer 3.2.0 documentation
chainer.functions.max_pooling_2d — Chainer 3.2.0 documentation

▼5.2.入力データの形状

畳み込みニューラルネットワークを使用することの最大のメリットは、入力データの形状を維持した状態で学習が行える点にあります。

# MNISTのデータセットを(1, 28, 28)でLoadする
train, test = chainer.datasets.get_mnist(ndim=3)

入力データのMNISTも形状を維持した状態で取得しておきます。

▼5.3.分類モデルの定義

分類モデルを定義する際は、先に定義した畳み込みニューラルネットワークを使用します。

# 分類モデルを定義
model = chainer.links.Classifier(CNN())

これで畳み込みニューラルネットワークに対応できました。
この畳み込みニューラルネットワークを使用すると、数エポックで99.9%の認識精度に辿り着きます。

▼5.4.全実装

畳み込みニューラルネットワーク対応版の全実装です。

# coding: utf-8
import numpy as np
import timeit
import matplotlib.pyplot as plt

import chainer
from chainer import training
from chainer.training import extensions

# 畳みこみニューラルネットワークの定義
class CNN(chainer.Chain):

    def __init__(self, class_labels=10):
        super(CNN, self).__init__()
        with self.init_scope():
            self.conv1 = chainer.links.Convolution2D(None, 20, 3) # 入力チャネル数、出力チャネル数、フィルタサイズ
            self.conv2 = chainer.links.Convolution2D(None, 50, 3)
            self.link1 = chainer.links.Linear(None, 500) 
            self.link2 = chainer.links.Linear(None, 500)
            self.link3 = chainer.links.Linear(None, class_labels)

    def __call__(self, x):
        h1 = chainer.functions.max_pooling_2d(chainer.functions.relu(self.conv1(x)), 2)
        h2 = chainer.functions.max_pooling_2d(chainer.functions.relu(self.conv2(h1)), 2)
        h3 = chainer.functions.relu(self.link1(h2))
        h4 = chainer.functions.relu(self.link2(h3))
        return self.link3(h4)

# MNISTのデータセットを(1, 28, 28)でLoadする
train, test = chainer.datasets.get_mnist(ndim=3)

# ミニバッチサイズを定義する
batch_size = 100
# Iterator生成
train_iter = chainer.iterators.SerialIterator(train, batch_size)
test_iter = chainer.iterators.SerialIterator(test, batch_size, repeat=False, shuffle=False)

# 分類モデルを定義
model = chainer.links.Classifier(CNN())

# 最適化手法の設定
optimizer = chainer.optimizers.Adam()
optimizer.setup(model)

max_epoch = 5
# Set up a trainer
updater = training.StandardUpdater(train_iter, optimizer)
trainer = training.Trainer(updater, (max_epoch, 'epoch'))

# テストデータセットに対する現在のモデルを評価します
trainer.extend(extensions.Evaluator(test_iter, model))
# 報告された値を蓄積し、ログファイルを出力ディレクトリに書き出します
trainer.extend(extensions.LogReport(log_name="result.log", trigger=(1, 'epoch')))
# 進捗状況を可視化します
trainer.extend(extensions.ProgressBar())
# LogReportの内容を標準出力する
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'main/accuracy', 'validation/main/loss', 'validation/main/accuracy', 'elapsed_time']))
# ネットワークの形をグラフで表示できるようにdot形式で保存する(これをgraphvizで画像形式に変換できる)
trainer.extend(extensions.dump_graph('main/loss'))
# Trainerのout引数で指定した出力ディレクトリにTrainerオブジェクトを指定されたタイミング(デフォルトでは1エポックごと)に保存します。
#trainer.extend(extensions.snapshot(filename='snapshot_epoch-{.updater.epoch}'))
# 学習済モデルを保存する
#trainer.extend(extensions.snapshot_object(model.predictor, filename='model_epoch-{.updater.epoch}'))
# 引数のリストで指定された値の変遷をmatplotlibライブラリを使ってグラフに描画し、画像として保存する
trainer.extend(extensions.PlotReport(['main/loss', 'validation/main/loss'], x_key='epoch', file_name='loss.png'))
trainer.extend(extensions.PlotReport(['main/accuracy', 'validation/main/accuracy'], x_key='epoch', file_name='accuracy.png'))

# 学習(訓練)実行
trainer.run()

# 学習させたモデルを使用して予測を行う

# テストデータからランダムに抜き出す
idx = np.random.choice(len(test), 100)
for i in idx:
    # 入力データ(画像データ)
    x = test[i][0]
    # 予測結果を取得
    y_ = np.argmax(model.predictor(x=x[np.newaxis, :, :, :]).data)
    # ラベルデータ
    y = test[i][1]
    # ラベルデータと予測結果が異なっている場合は入力データ(画像データ)を出力
    if(y != y_):
        plt.imshow(x.reshape(28, 28), cmap='gray')
        # ファイル名:label_(ラベル値)_predict_(予測値).png
        file_name = "label_" + str(y) + "_predict_" + str(y_) + ".png"
        plt.savefig(file_name)

以上。