Recurrent Convolutional Neural Network で The Street View House Number Dataset を学習

最近,Amazon EC2のp2.xlarge(GPU インスタンス)で少し遊んでいました.スポットインスタンスなら一時間あたり$0.2くらいで使用できてそこそこ遊べます.GPUは2世代前のTesla K80で,そんなに速くはないですが(手持ちのGTX1060と同じか少し遅いくらいでした),メインメモリが61GiB,グラフィクスメモリも12GBもあり,大規模学習などには便利です.

今回はこの p2.xlarge 上で“The Street View House Numbers”の学習を行いました.ネットワークは識別率ランキングの上の方から見ていって,実装が楽そうだった“Recurrent Convolutional Neural Network for Object Recognition”にしました.実装にはchainerを使用しました.

Recurrent っていうと普通,テキストデータみたいな系列データ処理を考えると思うんですが,この論文の言う Recurrent Convolutional Layer はちょっと変わっていて,一つの入力に対して,その入力と,それを発展させたものを足し合わせていくというような形になっています.unfold した図を見るとわかりやすいですが,RCLの入力と出力の間には,直接つながるパスや,複数回フィルタするパスなどがあって,それらを足し合わせたものが出力になります.複数回の Recurrent feedback によってニューロンが見ることができる範囲を広げつつ,単純に Convolution を重ねるよりもパラメータ数を減らすことができ,さらに勾配が消えにくくて学習に有利だそうです.

#!/usr/bin/python
# Recurrent Convolutional Layer
import chainer
import chainer.cuda
import chainer.links as L
import chainer.functions as F


class RCL(chainer.Chain):
	# T : number of recurrent feedback
	# K : number of channels
	# ksize, stride, pad : parameters for convolution
	def __init__(self, T, K, ksize=3, stride=1, pad=1):
		super(RCL, self).__init__(
			conv_r=L.Convolution2D(K, K, ksize, stride, pad),
			conv_ff=L.Convolution2D(K, K, ksize, stride, pad)
		)
		self.T = T
		self.k = 1.0
		self.n = K/8 + 1
		self.alpha = 0.001
		self.beta = 0.75

	def __call__(self, x):
		rf = self.conv_ff(x)
		for i in range(1, self.T):
			rf = self.conv_r(rf) + self.conv_ff(x)

		rf = F.relu(rf)
		# rf = F.local_response_normalization(rf, self.n, self.k, self.alpha, self.beta)
		return rf

RCLは↑のような感じに実装しました.元論文では local response normalization が使われていましたが,LRNはもう用無しでBatch Normalizationが最強(?)という説を見た気がするので,今回は BN層だけ置いて LRN は省きました.実際,RCLでも学習してみたんですが,BNのほうが圧倒的に学習が速かったです.

#!/usr/bin/python
# Recurrent Convolutional Neural Network
import numpy
import chainer
import chainer.functions as F
import chainer.links as L
import chainer.computational_graph as graph
from chainer import Chain
from RCL import RCL


class RCNN(Chain):
	def __init__(self, T, K, C, train=False):
		super(RCNN, self).__init__(
			conv1=L.Convolution2D(None, K, 5, 1, 2),
			rcl2=RCL(T, K, 3),
			rcl3=RCL(T, K, 3),
			rcl4=RCL(T, K, 3),
			rcl5=RCL(T, K, 3),
			l6=L.Linear(None, C),

			bnorm1=L.BatchNormalization(K),
			bnorm2=L.BatchNormalization(K),
			bnorm3=L.BatchNormalization(K),
			bnorm4=L.BatchNormalization(K),
			bnorm5=L.BatchNormalization(K)
		)
		self.K = K
		self.train = train

	def __call__(self, x):
		hc1 = F.relu(self.conv1(x))
		hc1 = self.bnorm1(hc1)
		hc1 = F.max_pooling_2d(hc1, 3, 2)
		hc1 = F.dropout(hc1, train=self.train)

		rhc2 = self.rcl2(hc1)
		rhc2 = self.bnorm2(rhc2)
		rhc2 = F.dropout(rhc2, train=self.train)

		rhc3 = self.rcl3(rhc2)
		rhc3 = self.bnorm3(rhc3)
		rhc3 = F.dropout(F.max_pooling_2d(rhc3, 3, 2), train=self.train)

		rhc4 = self.rcl4(rhc3)
		rhc4 = self.bnorm4(rhc4)
		rhc4 = F.dropout(rhc4, train=self.train)

		rhc5 = self.rcl5(rhc4)
		rhc5 = self.bnorm5(rhc5)
		gmax = F.max(rhc5, axis=1)

		return F.softmax(self.l6(gmax))

ネットワーク全体はこんな感じ.コードは普通ではありますが,Computational Graph ↓はRCLの部分が禍々しくていい感じです.

#!/usr/bin/python
import numpy
import random
import scipy.io
import chainer
import chainer.links as L
from chainer import training
from chainer.training import extensions
from RCNN import RCNN


def load_digits(path):
	mat = scipy.io.loadmat(path)
	labels = mat['y'].astype(numpy.int32)
	labels[labels == 10] = 0
	data = mat['X'].astype(numpy.float32)

	data = numpy.swapaxes(numpy.swapaxes(data, 0, 3), 1, 2)
	mean = numpy.mean(data, axis=0)
	print len(data)

	return mean, map(lambda x: (x[0], x[1][0]), zip(data, labels))


def draw_samples(dataset, n):
	random.shuffle(dataset)
	buckets = map(lambda x: [], range(10))
	for data in dataset:
		buckets[data[1]].append(data)

	train = []
	test = []

	for bucket in buckets:
		train.extend(bucket[n:])
		test.extend(bucket[:n])

	return train, test


# loads the Street View House Number dataset
# and composes training and test datasets according
# to the procedure described in "Maxout networks"
def load_svhn(directory):
	print 'loading data...'
	trainmean, trainset = load_digits('train_32x32.mat')
	extmean, extraset = load_digits('extra_32x32.mat')

	print 'normalizing...'
	mean = (trainmean * len(trainset) + extmean * len(extraset)) / (len(trainset) + len(extraset))
	mean = mean.astype(numpy.float32)
	trainset = map(lambda x: (x[0]-mean, x[1]), trainset)
	extraset = map(lambda x: (x[0]-mean, x[1]), extraset)

	print 'creating validation set...'
	train, test = draw_samples(trainset, 400)
	extrain, extest = draw_samples(extraset, 200)

	train.extend(extrain)
	test.extend(extest)

	random.shuffle(train)
	random.shuffle(test)

	return train, test


def main():
	gpu_id = 0

	# construct a RCNN with
	# 4 recurrent feedback
	# 128 channels in each layer
	# 10 output channels
	model = L.Classifier(RCNN(4, 128, 10, False))
	if gpu_id >= 0:
		chainer.cuda.get_device(gpu_id).use()
		model.to_gpu()
	optimizer = chainer.optimizers.Adam()
	optimizer.setup(model)

	# train, test = chainer.datasets.get_mnist(ndim=3)
	train, test = load_svhn('./')

	train_iter = chainer.iterators.SerialIterator(train, 1024)
	test_iter = chainer.iterators.SerialIterator(test, 128, repeat=False, shuffle=False)

	updater = training.StandardUpdater(train_iter, optimizer, device=gpu_id)
	trainer = training.Trainer(updater, (512, 'epoch'))

	trainer.extend(extensions.Evaluator(test_iter, model, device=gpu_id))

	trainer.extend(extensions.LogReport())
	trainer.extend(extensions.PlotReport(['main/loss', 'validation/main/loss'], 'epoch', file_name='loss.png'))
	trainer.extend(extensions.PlotReport(['main/accuracy', 'validation/main/accuracy'], 'epoch', file_name='accuracy.png'))
	trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'validation/main/loss', 'main/accuracy', 'validation/main/accuracy', 'elapsed_time']))
	trainer.extend(extensions.ProgressBar())

	snapshot_trigger = (10, 'epoch')
	trainer.extend(extensions.snapshot(filename='snapshot_epoch_{.updater.epoch}'), trigger=snapshot_trigger)
	trainer.extend(extensions.snapshot_object(model.predictor, filename='model_epoch_{.updater.epoch}'), trigger=snapshot_trigger)
	trainer.extend(extensions.snapshot_object(optimizer, filename='optimizer_epoch_{.updater.epoch}'), trigger=snapshot_trigger)

	print 'run'
	trainer.run()

	chainer.serializers.save_npz('model.npz', model.predictor)

if __name__ == '__main__':
	main()

最後に学習部分.
src.zip [Amazon EC2, p2.xlarge, NVIDIA CUDA Toolkit 7.5 on Amazon Linux で動作確認,利用は自由ですが自己責任でうんぬん]

・学習結果
論文で報告されている識別精度は98.13%ですが,最終的な識別精度はtrainセットで98.66%,validationセットで95.77%でした.ちょっとオーバーフィットしている感じですね.調整の余地ありといったところでしょうか.以前に適当なCNNで学習したときは90%くらいだったので,それよりはずっといい結果だとは思います.

・Amazon EC2に関して
ただ,GPUの計算性能に関しては2世代前のモデルということもあり,若干不満が残りますね.今は諸事情で手元に計算資源がなかったので使ってみましたが,ちょっと良いデスクトップを持っていれば無理に使う必要は無いかなと思います.p2.16xlarge(Tesla K80 x 16)だとかなり変わるかな?ただ,スポットインスタンスの最低でも $2/hour なので財布的に厳しいです.Amazon EC2全般の使い勝手自体は非常に良くて好感触でした.さすがAmazonといったところ.今度は普通のCPUインスタンスで遊んでみたいです.

今回は途中でやり直したりして,およそ2日間学習,45時間で$7.71.平均$0.171/hourでした.別件の学習を行っていた先月は一週間近く続けて使用して,143時間で$28.04.平均$0.196/hour.ストレージ代は$2.55かかりました.継続利用しなければ,まあまあな金額でそこそこ遊べて良いかなというくらいですかね.

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください