2016年7月21日に更新しました。
はじめに
Chainerがv1.11.0にバージョンアップし、Trainerが新たに導入されました。
Released #Chainer v1.11.0! Dataset/training loop abstraction is added. Get free from manual iteration over datasets! https://t.co/2YV1X7JMMr
— Chainer (@ChainerOfficial) July 12, 2016
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()