俺とプログラミング

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

CNNの学習に最高の性能を示す最適化手法はどれか

Adam, AdaGrad, AdaDelta, RMSpropGraves, SGD, MomentumSGDなど数ある最適化手法の中で、畳み込みニューラルネットワーク(CNN:Convolutional Neural Network)の学習には、どのOptimizerをつかうのが最も適しているのかということを実験しました。

最適化とは

最適化 (Optimization) とは、関数などを最適な状態に近づけることをいいます。ディープラーニングにおける最適化とは、モデルの予測値と実際の値との誤差から、パラメータ(重み)を更新することです。その最適化手法は以下のように様々なものがあります。パラメータを更新する際の数式がそれぞれ異なっています。

SGDとは

SGDとはStochastic Gradient Descentの略で日本語では確率的勾配降下法といいます。最も基本的なアルゴリズムです。確率的勾配降下法は,訓練データから1つずつデータをランダムに取り出して,そのデータ点における勾配を計算してパラメータを更新します。詳しくはこちらを参照。

MomentumSGDとは

MomentumSGDはSGDに慣性項を付与したものです。慣性項(Momentum:モメンタム)とは勾配降下法の式を拡張して、より素早く収束するように工夫するためのパラメータとして存在しています。

Adamとは

Adamについては以下を参照。

実験環境

使用ライブラリ Chainer
データセット MNIST手書き文字認識
パラメータ デフォルト値
モデル 5層CNN
ミニバッチ数 100
評価手法 10分割交差検証
エポック数 300

実験環境は上記のようになっており、各最適化手法のパラメータはChainerデフォルトのものを利用しました。多分論文推奨の値にしてくれてるんじゃないかとおもう。評価手法として10分割-交差検証(クロスバリデーション)を利用することでモデルがどの程度安定して学習できるかを精密に計測できます。

実験結果

以下のグラフが実験の結果となります。
f:id:ttlg:20160211174018p:plain

分散があってみにくいので、ガウシアンフィルタで平均化したものがこちら。
f:id:ttlg:20160211174255p:plain

最大値のみをグラフにしたものがこちらになります。
f:id:ttlg:20160211174341p:plain

結果を精度の順にならべると、

1 RMSpropGraves 0.9930
2 MomentumSGD 0.9928
3 AdaDelta 0.9927
4 NesterovAG 0.9927
5 Adam 0.9925
6 SGD 0.9919
7 AdaGrad 0.9875

となり、RMSpropGravesが5層CNNのMNIST認識における最も優れた最適化手法らしいということがわかりました。
ニューラルネットのモデルや、データセットの難しさに左右されることも考えられるので、このへんはまた今度検証してみたいとおもいます。

標準偏差の値もグラフ化したので載せておきます。
f:id:ttlg:20160211175100p:plain
これを見ると、最も安定しているのはAdamということがわかります。

ソースコード

参考までに今回使ったソースコードを載せておきます。
レポジトリ: https://github.com/ttlg/cnn-lab

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 CNN(Chain):
    def __init__(self, channel=1, c1=16, c2=32, c3=64, f1=256, f2=512, filter_size1=3, filter_size2=3, filter_size3=3):
        super(CNN, self).__init__(
            conv1=L.Convolution2D(channel, c1, filter_size1),
            conv2=L.Convolution2D(c1, c2, filter_size2),
            conv3=L.Convolution2D(c2, c3, filter_size3),
            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.max_pooling_2d(h, 2)
        h = F.relu(self.conv3(h))
        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 'cnn'

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 which_is_best_optimizer(k=10, model=CNN()):
    k_fold_validation(k, copy.deepcopy(model), optimizer=optimizers.Adam(), tag='Adam')
    k_fold_validation(k, copy.deepcopy(model), optimizer=optimizers.SGD(), tag='SGD')
    k_fold_validation(k, copy.deepcopy(model), optimizer=optimizers.RMSpropGraves(), tag='RMSpropGraves')
    k_fold_validation(k, copy.deepcopy(model), optimizer=optimizers.RMSprop(), tag='RMSprop')
    k_fold_validation(k, copy.deepcopy(model), optimizer=optimizers.AdaDelta(), tag='AdaDelta')
    k_fold_validation(k, copy.deepcopy(model), optimizer=optimizers.AdaGrad(), tag='AdaGrad')
    k_fold_validation(k, copy.deepcopy(model), optimizer=optimizers.MomentumSGD(), tag='MomentumSGD')
    k_fold_validation(k, copy.deepcopy(model), optimizer=optimizers.NesterovAG(), tag='NesterovAG')


if __name__ == '__main__':
    which_is_best_optimizer(model=CNN())


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

Copyright © 2016 ttlg All Rights Reserved.