Monthly Hacker's Blog

毎月のテーマに沿ったプログラミング記事を中心に書きます。

MNIST分類コードをChainer-v1.11.0のTrainerで書き換える

2016年7月21日に更新しました。

はじめに

Chainerがv1.11.0にバージョンアップし、Trainerが新たに導入されました。

Trainerは今までChainerユーザーが自分たちで書いていた、学習ループやログ出力などを代わりに行ってくれるものです。早速、研究室の同期がTrainerの機能について記事を書いてくれました。本記事と併せて読むと理解度が増すので是非御覧ください。
www.monthly-hack.com

本記事ではv1.11.0以前のコードをTrainerで書き換えたい人のために、

  • MNISTの分類を行うプログラムを2つ紹介します。
  • v1.11.0以前のコードと比較します。
  • 新実装の魅力や疑問など、感想を述べます。

コード比較

Trainerを使ったコード(mnist_with_trainer.py)とv1.11.0以前のコード(mnist_without_trainer.py)を各機能ごとに比較します。*1Trainerによってコード自体が短くなるだけではなく、便利な機能が追加されているのでご確認ください。ソースコードの全体は記事後半の方に載せています。

データセット

予め、何らかの方法*2でMNISTのデータセットを用意します。データファイルの読み込みは共通ですが、TrainerではTupleDataset型に変換します。

# 共通部分
N = 60000
data = np.load('mnist_data.npy')
data = data.astype(np.float32)/255
label = np.load('mnist_label.npy')
label = label.astype(np.int32)
x_train, x_test = np.split(data, [N])
y_train, y_test = np.split(label, [N])

# Trainer用: TupleDataset型
train = tuple_dataset.TupleDataset(x_train, y_train)
test = tuple_dataset.TupleDataset(x_test, y_test)

TupleDataset型の次元数及び構成は、以下のコードのとおりです。

# train[i] = [data[i], label[i]]
train = [[x, y] for x, y in zip(x_train, y_train)]

イテレーション

学習ループを回すイテレーション部分です。v1.11.0以前ではfor文やシャッフル, ミニバッチのスライスを書いていましたね。

batchsize = 100
for epoch in six.moves.range(1, 20 + 1):
    print('epoch', epoch)

    perm = np.random.permutation(N)
    for i in six.moves.range(0, N, batchsize):
        x = chainer.Variable(x_train[perm[i:i + batchsize]])
        t = chainer.Variable(y_train[perm[i:i + batchsize]])
        # 省略

    for i in six.moves.range(0, N_test, batchsize):
        x = chainer.Variable(x_test[i:i + batchsize],
                             volatile='on')
        t = chainer.Variable(y_test[i:i + batchsize],
                             volatile='on')
        # 省略

Trainerではたったの2行で表現できます。素晴らしい。

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

パラメータ更新

誤差逆伝播してパラメータ更新を行う部分はoptimizer.update()で行っていました。

optimizer.update(model, x, t)

Trainerではupdaterにtrain_iter(学習データ)とoptimizer(最適化手法)を渡します。その後epoch数や結果保存フォルダを含めてtrainerにまとめます。最後にtrainer.run()で学習ループを実行するため、以降のコードは全てtrainerに紐付けます。
epoch*3以外にもiteration*4を指定することもできます。

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

評価

テストデータで評価するには、損失関数だけを計算するfor文を書いていました。

sum_accuracy = 0
sum_loss = 0
for i in six.moves.range(0, N_test, batchsize):
    x = chainer.Variable(x_test[i:i + batchsize],
                         volatile='on')
    t = chainer.Variable(y_test[i:i + batchsize],
                         volatile='on')
    loss = model(x, t)
    sum_loss += float(loss.data) * len(t.data)
    sum_accuracy += float(model.accuracy.data) * len(t.data)

Trainerではたったの1行、extensionsで拡張することで評価部分を表現できます。

trainer.extend(extensions.Evaluator(test_iter, model))

ログ出力

最後にログ出力です。今までは時間や誤差、精度を自分で記録して出力していました。

print('epoch', epoch)

start = time.time()
for i in six.moves.range(0, N, batchsize):
    # 省略
end = time.time()
elapsed_time = end - start
throughput = N / elapsed_time
print('train mean loss={}, accuracy={}, throughput={} images/sec'.format(
    sum_loss / N, sum_accuracy / N, throughput))

for i in six.moves.range(0, N_test, batchsize):
    # 省略
print('test  mean loss={}, accuracy={}'.format(
    sum_loss / N_test, sum_accuracy / N_test))

煩わしかった結果表示もたった3行で書けます。進捗状況や残り時間も表示してくれます。とても親切ですね。

trainer.extend(extensions.LogReport())
trainer.extend(extensions.PrintReport(
               ['epoch', 'main/accuracy', 'validation/main/accuracy']))
trainer.extend(extensions.ProgressBar())
実行例

こちらが今までの出力。見慣れた表示ですが、もう目にすることはないでしょう。

$ python mnist_without_trainer.py
epoch 1
train mean loss=1.2411813024183114, accuracy=0.7015333319827914, throughput=32632.799084107133 images/sec
test  mean loss=0.5680366323888302, accuracy=0.8612000012397766
epoch 2
train mean loss=0.4740862305710713, accuracy=0.8741833340128263, throughput=32455.325897418734 images/sec
test  mean loss=0.38570983584970236, accuracy=0.8948000019788742

こちらがTrainerでの出力。このプログレッシブバーには感動しました。

$ python mnist_with_trainer.py
epoch       main/accuracy  validation/main/accuracy
1           0.6698         0.8567
2           0.872967       0.8944
     total [#######...........................................] 14.17%
this epoch [#########################################.........] 83.33%
      1700 iter, 2 epoch / 20 epochs
       218 iters/sec. Estimated time to finish: 0:00:47.247561.

感想

既存のコードと比べて半分ぐらいの行数になりました*5。自前のデータをどうやってtrainerで使うかが課題でした。Tutorialやサンプルコードでは、

train, test = chainer.datasets.get_mnist()
train_iter = chainer.iterators.SerialIterator(train, args.batchsize)
test_iter = chainer.iterators.SerialIterator(test, args.batchsize,
                                                 repeat=False, shuffle=False)

としていたので、どんな前処理をしてからiteratorに渡せばいいかわかりませんでした*6。そこでget_mnist()のコードを読み解き、解決しました。

本記事で取り上げたMNISTのコードをTrainerで書き換え以外にも、CNNやDCGANでも試してみたいと思います。

ソースコード

mnist_without_trainer.py

# coding: utf-8

import six
import time
import numpy as np
import chainer
from chainer import optimizers, Chain, Variable
import chainer.functions as F
import chainer.links as L


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

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

N = 60000
N_test = 10000
data = np.load('mnist_data.npy')
data = data.astype(np.float32)/255
label = np.load('mnist_label.npy')
label = label.astype(np.int32)
x_train, x_test = np.split(data, [N])
y_train, y_test = np.split(label, [N])

batchsize = 100
for epoch in six.moves.range(1, 20 + 1):
    print('epoch', epoch)

    perm = np.random.permutation(N)
    sum_accuracy = 0
    sum_loss = 0
    start = time.time()
    for i in six.moves.range(0, N, batchsize):
        x = chainer.Variable(x_train[perm[i:i + batchsize]])
        t = chainer.Variable(y_train[perm[i:i + batchsize]])

        optimizer.update(model, x, t)

        sum_loss += float(model.loss.data) * len(t.data)
        sum_accuracy += float(model.accuracy.data) * len(t.data)
    end = time.time()
    elapsed_time = end - start
    throughput = N / elapsed_time
    print('train mean loss={}, accuracy={}, throughput={} images/sec'.format(
        sum_loss / N, sum_accuracy / N, throughput))

    sum_accuracy = 0
    sum_loss = 0
    for i in six.moves.range(0, N_test, batchsize):
        x = chainer.Variable(x_test[i:i + batchsize],
                             volatile='on')
        t = chainer.Variable(y_test[i:i + batchsize],
                             volatile='on')
        loss = model(x, t)
        sum_loss += float(loss.data) * len(t.data)
        sum_accuracy += float(model.accuracy.data) * len(t.data)

    print('test  mean loss={}, accuracy={}'.format(
        sum_loss / N_test, sum_accuracy / N_test))

mnist_with_trainer.py

# coding: utf-8

import numpy as np
import chainer
from chainer import report, training, Chain, datasets, iterators, optimizers
import chainer.functions as F
import chainer.links as L
from chainer.training import extensions
from chainer.datasets import tuple_dataset


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


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

N = 60000
data = np.load('mnist_data.npy')
data = data.astype(np.float32)/255
label = np.load('mnist_label.npy')
label = label.astype(np.int32)
x_train, x_test = np.split(data, [N])
y_train, y_test = np.split(label, [N])

train = tuple_dataset.TupleDataset(x_train, y_train)
test = tuple_dataset.TupleDataset(x_test, y_test)

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

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

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

*1:Trainerによる完全な置換ではないので、ご注意ください。

*2:MNISTのデータセットの入手方法は調べれば必ず見つかりますが、参考までに。 多層パーセプトロンでMNISTの手書き数字認識 - 人工知能に関する断創録

*3:epoch: データセットを全部使う回数

*4:iteration: 重み更新する回数

*5:あくまでMNIST分類を行うコードですが。

*6:もし公式ドキュメントに書いてあったらごめんなさい。