機械学習フレームワークchainerを使って1からコードをかいてみる(mnist編)

  • 7
    いいね
  • 0
    コメント

はじめに

機械学習について基礎や仕組みについての資料はネット上にたくさん出回っていますが、では実際にコードを書いてみよう!と思うとなかなか手が出ないと思います。特にchainerやtensorflowなどは便利なんだろうと思いつつインストールしてもさっぱりわからず、例題を動かそうとして動かずに辞めてしまう人もいそうです。他にも、画像認識の例でchainerに付いてくるimagenetはコードを読もうとしてもなにがなんだかわからないってこともあると思います。そこでまずは一番シンプルな例である手書き文字認識mnistをchainerを使って1から実装し、仕組みやコードの書き方を理解しようという目的で今回の記事を書くことにしました。私も最近chainerを趣味程度に勉強し始めたど素人なので記事を書くことで自分の理解をチェックするのが目的だったりします。
なお、ニューラルネットワーク自体の仕組みや勉強はこことかが参考になると思います。

chainerの難しさ

chainerを触るまで私は基本C言語を使ってフルスクラッチをすることが多く、書いていたプログラムは「○を入力、×を出力」ということを常に意識していました。そのため、データの入力用のコード、データの取り寄せ、格納が隠蔽されていて、出力が学習後のモデルの情報しかでないサンプルコードに苦しみました(笑)。学習させたんだから、なんか入力して出力があってるかみたいなと思ってました。ライブラリを読むことをためらっていた罰です。

事前準備

pythonが実行できる環境を作ってください。自分の環境ではpyenvを用いてpython3.5を導入しています。chainerはpipかなにかでインストールしてみてください。
参考までに自分は

chainer==1.13.0

を使っています。
まず入力となるデータを取得し、ベクトルとして読み込まないといけません。
chainer/example/mnist/
以下にあるdata.pyにそのコードが用意されているためこれを自分の開発ディレクトリにコピーしてきてください。
またおまじないとして

chainer_test.py
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 data

をソースコードに書いてください。以下このソースファイルに追記していきます。

MNISTの実装

MNISTのNNの構成

ネットワーク構成ですが(インプット、隠れ層1、隠れ層2、アウトプット)の4つがあります。それぞれの次元は784, 100, 100, 10 とします。インプットについては28 * 28 pxだからです。アウトプットについては0〜9を表現するのに10次元を用いています。

chainer_test.py
class MLP(Chain):
    def __init__(self):
        super(MLP, self).__init__(
            l1=L.Linear(784, 100),
            l2=L.Linear(100, 100),
            l3=L.Linear(100, 10),
        )

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

と記述します。まず、initについてですがこれはレイヤ構造を定義しています。784->100, 100->100,100->10とベクトル次元が変わっていくといった感じです。
次に各層での伝搬の様子、つまりforwardを計算しているのがcallです。ニューラルネットにはお決まりで発火関数なるものを角層の出力にかけて伝搬するのですがこれはあらかじめchainerで準備されていて、ここではF.relu()を用いています。tanhなどももちろんあるはずです。

さらに個人的に重要なプレディクトという関数を作ります。なぜかと言うと、学習させてロス率とかが見られるのはいいとして、その学習させたニューラルネットを自分で使う時、APIが結局なんなのかわかりづらいからです。他にいい方法があるんでしょうけども自分でpredictを実装することにします。

chainer_test.py
def predict(model, x_data):
    x = Variable(x_data.astype(np.float32))
    y = model.predictor(x)
    return np.argmax(y.data, axis = 1)

この意味としては学習したモデル(後述)と入力ベクトル(784次元 * N(ベクトル数))を引数として、入力ベクトルをfloat32にしてプレディクターに入力し、アウトプットを出力(次元10)し、最大数値が入っているインデックスを返す。といったものです。これでわかりやすいアウトプットが得られます。

データ準備

データを準備します。

chainer_test.py
batchsize = 100
datasize = 60000
N = 10000


mnist = data.load_mnist_data()
x_all = mnist['data'].astype(np.float32) / 255
y_all = mnist['target'].astype(np.int32)
x_train, x_test = np.split(x_all, [datasize])
y_train, y_test = np.split(y_all, [datasize])

batchsizeとNは後述。data.load_mnist_data()でデータを格納します。次にmnist['data'],['target']で入力とその分類を取り出します。通常、用意されたデータをすべて学習に使うのではなく、ある程度を学習、残りをテストにするのが一般的です。今回もx_traint, x_testの二つを用意しています。

学習の準備

学習の準備としてモデルを作ります。

chainer_test.py

model = L.Classifier(MLP())
optimizer = optimizers.Adam()
optimizer.setup(model)

今回のような分類問題では損失関数の計算や誤差のレポートなどが実装されているL.Classifier()が事前に準備されているのでこれに上で定義したクラスを与え、modelとして出力します。
optimizerは数学的手法に基づきよいパラメータを自動で設定してくれる優れものです。

学習とテストについて

実際に学習にはいります。

chainer_test.py
for epoch in range(20):
    print('epoch % d' % epoch)
    indexes = np.random.permutation(datasize)
    sum_loss, sum_accuracy = 0, 0
    for i in range(0, datasize, batchsize):
        x = Variable(np.asarray(x_train[indexes[i : i + batchsize]]))
        t = Variable(np.asarray(y_train[indexes[i : i + batchsize]]))
        optimizer.update(model, x, t)
        sum_loss += float(model.loss.data) * batchsize
        sum_accuracy += float(model.accuracy.data) * batchsize
    print('train mean loss={}, accuracy={}'.format(sum_loss / datasize, sum_accuracy / datasize))


    sum_loss, sum_accuracy = 0, 0
    for i in range(0, N, batchsize):
        x = Variable(np.asarray(x_test[i : i + batchsize]),volatile='on')
        t = Variable(np.asarray(y_test[i : i + batchsize]),volatile='on')
        loss = model(x, t)
        sum_loss += float(loss.data) * batchsize
        sum_accuracy += float(model.accuracy.data) * batchsize
    print('test mean loss={}, accuracy={}'.format(sum_loss / N, sum_accuracy / N))

epochというのは学習を何回繰り返したかです。今回は1 epoch ごとに学習とテストを行います。学習時は配列を適当に並び替えて、0~batchsize を datasize/batchsize 回繰り返します。実際のテストはiからi + batchsizeのインデックスをもつ(入力、正解データ)=(x,t)としてつくり、optimizer(model,x,t)により学習させます。あとはこの操作を繰り返し、平均のloss率とaccuracyを出力します。テストの方もやっていることはほとんど同じです。

予測&モデルの保存について

学習したモデルになんらかのベクトルを与えて、答えがあっているかを確認します。

chainer_test.py
p_test = np.empty((0, 784), float)
p_test = np.append(p_test, np.array([x_test[0]]), axis=0)
p_test = np.append(p_test, np.array([x_test[1]]), axis=0)


print(p_test)
print(predict(model, p_test))
print(y_test)

serializers.save_hdf5('myMLP.model',model)

p_testは試したい入力のベクトルです。今回は自分で用意するのが面倒だったのでベクトルデータとしてテストに使ったベクトルのうち0番めと1番目のものを使いました。値のどこかをいじって見てもいいかもしれません。実際に術力させてみると、y_testの最初の二つがpredictの返り値として出ているのでこれは学習が成功しているということ(だと思います)です。

最後の一行はモデルをファイルとして書き出しています。これで再利用することもできます。

おわりに

chainerのmnistを1から実装しました。今回のテストにあたり和訳サイトが非常に頼りになりました。
個人的にはchainerクラス記述のしやすさに驚いています。今後は空いた時間を使い、ライブラリ自体のソースコード解説やimagenetについての1からの実装についても書きたいと思います。