SPP(Spatial Pyramid Pooling)を使ってCNNの精度を向上させよう
Max Poolingの代わりにSPP(Spatial Pyramid Pooling)と呼ばれる特別なプーリング層を用いることで、CNNの性能をお手軽に向上させることができる。この記事では、SPP-netの使い方や、有効性について検証した結果を紹介する。
SPP(Spatial Pyramid Pooling)とはなにか
SPPとはMicrosoft Research Asia(MSA)が2014年に開発した新しいPooling層である。SPPで最も重要なコンセプトは、入力画像のサイズが異なっている場合でも、決まったサイズの出力になるという点にある。その仕組みは単純で、画像を格子状に、1, 4, 16, ...と分割していき、その中で、最大プーリング(ほかのプーリングでもいい)を行う。その後、1 + 4 + 16 + ・・・と、つなげたベクトルをSPPの出力とする。つまり、出力のサイズはピラミッドのサイズとチャンネル数にのみ左右される。
MSAが公開しているスライド(http://image-net.org/challenges/LSVRC/2014/slides/sppnet_ilsvrc2014.pdf)がわかりやすい。詳細については、原著論文(http://arxiv.org/pdf/1406.4729v4.pdf)を参照。
SPPの有効性
スライドにも言及があるが、SPP層を導入することで、様々な構造のCNNにおいて精度が向上したことが示された。また、SPP-netを使うことで、Faster-RCNN(論文紹介: Fast R-CNN&Faster R-CNN)などの、物体検出を同時に行うネットワークの速度を大幅に向上させることができる。
SPPの使い方
SPPをどこに入れるかだが、全結合層の直前がいい。ほかは今までとおなじネットワーク構造で、MaxPoolingをSpatialPyramidPoolingに変更するだけで、精度の向上が見込める。
Chainerでは,spatial_pyramid_pooling_2dという関数が用意されているので、いままで、max_pooling_2dだったものをspatial_pyramid_pooling_2dにするだけでよい。ただし、2つ目の引数はピラミッドサイズを指定するが、あまり多いと画像をうまく分割できなくなるため、注意が必要だ。
import numpy as np from chainer import cuda, Function, gradient_check, Variable, optimizers, serializers, utils from chainer import Link, Chain, ChainList import chainer.functions as F import chainer.links as L xp = cuda.cupy if cuda.available else np class SPPnet(Chain): def __init__(self, channel=1, c1=16, c2=32, f1=672, f2=512, filter_size1=3, filter_size2=3): super(SPPnet, self).__init__( conv1=L.Convolution2D(channel, c1, filter_size1), conv2=L.Convolution2D(c1, c2, filter_size2), l1=L.Linear(f1, f2), l2=L.Linear(f2, 10) ) if cuda.available: cuda.get_device(0).use() self.to_gpu() def forward(self, x, train): h = F.relu(self.conv1(x)) h = F.max_pooling_2d(h, 2) h = F.relu(self.conv2(h)) h = F.spatial_pyramid_pooling_2d(h, 3, F.MaxPooling2D) h = F.dropout(F.relu(self.l1(h)), train=train) y = self.l2(h) return y def __call__(self, x, t, train=True): x = Variable(xp.asarray(x)) t = Variable(xp.asarray(t)) y = self.forward(x, train=train) loss = F.softmax_cross_entropy(y, t) self.loss = loss.data self.accuracy = F.accuracy(y, t).data return loss
SPPの認識実験
SPPを導入することで、精度が向上することを実験によって示した。
- 実験環境
使用ライブラリ | Chainer |
---|---|
データセット | MNIST手書き文字認識 |
Optimizer | Adam |
モデル | 4層CNN |
ミニバッチ数 | 100 |
評価手法 | 10分割交差検証 |
エポック数 | 300 |
こちらがクロスバリデーションで評価した実験結果となっている。Max Pooling を Spatial Pyramid Poolingにするだけで、精度が向上していることがわかる。
ソースコード
import numpy as np import chainer from chainer import cuda, Function, gradient_check, Variable, optimizers, serializers, utils from chainer import Link, Chain, ChainList import chainer.functions as F import chainer.links as L from sklearn.datasets import fetch_mldata xp = cuda.cupy if cuda.available else np class SPPnet(Chain): def __init__(self, spp=True, channel=1, c1=16, c2=32, f1=672, f2=512, filter_size1=3, filter_size2=3): super(SPPnet, self).__init__( conv1=L.Convolution2D(channel, c1, filter_size1), conv2=L.Convolution2D(c1, c2, filter_size2), l1=L.Linear(f1, f2), l2=L.Linear(f2, 10) ) self.spp = spp if cuda.available: cuda.get_device(0).use() self.to_gpu() def forward(self, x, train): h = F.relu(self.conv1(x)) h = F.max_pooling_2d(h, 2) h = F.relu(self.conv2(h)) if self.spp: h = F.spatial_pyramid_pooling_2d(h, 3, F.MaxPooling2D) else: h = F.max_pooling_2d(h, 2) h = F.dropout(F.relu(self.l1(h)), train=train) y = self.l2(h) return y def __call__(self, x, t, train=True): x = Variable(xp.asarray(x)) t = Variable(xp.asarray(t)) y = self.forward(x, train=train) loss = F.softmax_cross_entropy(y, t) self.loss = loss.data self.accuracy = F.accuracy(y, t).data return loss def __str__(self): return 'sppnet' class MNIST: def __init__(self): self.mnist = fetch_mldata('MNIST original', data_home=".") np.random.seed(1) @staticmethod def unison_shuffle(a, b): assert len(a) == len(b) p = np.random.permutation(len(a)) return a[p], b[p] def _fold_i_split(self, n, i, x, t): cls_num = 10 test_size = len(t)/n cls_test_size = test_size/cls_num cls_size = len(t)/cls_num x_train = x_test = t_train = t_test = None for j in range(cls_num): cls_head = cls_size*j cls_tail = cls_size*(j+1) head_idx = cls_head + cls_test_size*i tail_idx = cls_head + cls_test_size*(i+1) if j == 0: x_test = x[head_idx:tail_idx] t_test = t[head_idx:tail_idx] x_train = np.concatenate((x[cls_head:head_idx], x[tail_idx:cls_tail])) t_train = np.concatenate((t[cls_head:head_idx], t[tail_idx:cls_tail])) else: x_test = np.concatenate((x_test, x[head_idx:tail_idx])) t_test = np.concatenate((t_test, t[head_idx:tail_idx])) x_train = np.concatenate((x_train, x[cls_head:head_idx], x[tail_idx:cls_tail])) t_train = np.concatenate((t_train, t[cls_head:head_idx], t[tail_idx:cls_tail])) x_test, t_test = self.unison_shuffle(x_test, t_test) x_train, t_train = self.unison_shuffle(x_train, t_train) x_train = x_train.reshape((len(x_train), 1, 28, 28)) x_test = x_test.reshape((len(x_test), 1, 28, 28)) return x_train, x_test, t_train, t_test def get_fold_i(self, n, i): x = self.mnist.data t = self.mnist.target x = x.astype(np.float32) t = t.astype(np.int32) x /= x.max() return self._fold_i_split(n, i, x, t) def validate_model(model, optimizer, x_train, x_test, t_train, t_test, batch, n_epoch): train_size = t_train.size test_size = t_test.size print train_size, test_size optimizer.setup(model) sum_accuracies = [] sum_accuracies_train = [] sum_losses = [] for epoch in range(1, n_epoch + 1): perm = np.random.permutation(train_size) sum_loss = 0 sum_accuracy_train = 0 for i in range(0, train_size, batch): x_batch = x_train[perm[i:i + batch]] y_batch = t_train[perm[i:i + batch]] optimizer.update(model, x_batch, y_batch) sum_loss += float(model.loss) * len(y_batch) sum_accuracy_train += float(model.accuracy) * len(y_batch) sum_losses.append(sum_loss / train_size) sum_accuracies_train.append(sum_accuracy_train/train_size) print 'loss:'+str(sum_loss/train_size) sum_accuracy = 0 for i in range(0, test_size, batch): x_batch = x_test[i:i + batch] y_batch = t_test[i:i + batch] model(x_batch, y_batch, train=False) sum_accuracy += float(model.accuracy) * len(y_batch) print 'accuracy:'+str(sum_accuracy/test_size) sum_accuracies.append(sum_accuracy / test_size) print "train mean loss: %f" % (sum_losses[n_epoch-1]) print "test accuracy: %f" % (sum_accuracies[n_epoch-1]) return sum_losses, sum_accuracies_train, sum_accuracies def k_fold_validation(k, model, optimizer=optimizers.Adam(), tag='', batch=100, n_epoch=300): loss = [] acc = [] acc_train = [] mnist = MNIST() print str(model) for i in range(k): x_train, x_test, t_train, t_test = mnist.get_fold_i(k, i) print 'fold:' + str(i) l, at, a = validate_model(copy.deepcopy(model), copy.deepcopy(optimizer), x_train, x_test, t_train, t_test, batch, n_epoch) loss.append(l) acc.append(a) acc_train.append(at) save_result('results/'+str(model)+'_'+tag+'_loss.txt', loss) save_result('results/'+str(model)+'_'+tag+'_accuracy.txt', acc) save_result('results/'+str(model)+'_'+tag+'_accuracy_train.txt', acc_train) def spp_validation(): k_fold_validation(model=SPPnet(), tag='spp') k_fold_validation(model=SPPnet(spp=False, f1=1152), tag='max') if __name__ == '__main__': spp_validation()