俺とプログラミング

某IT企業でエンジニアをしてます。このブログではプログラミングに関わることを幅広く発信します。

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の出力とする。つまり、出力のサイズはピラミッドのサイズとチャンネル数にのみ左右される。
f:id:ttlg:20160215200117j:plain

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にするだけで、精度が向上していることがわかる。
f:id:ttlg:20160215191941p:plain

ソースコード

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


深層学習 (機械学習プロフェッショナルシリーズ)
深層学習: Deep Learning

Copyright © 2016 ttlg All Rights Reserved.