覚え書きブログ

Deep Learning覚え書き(Chainerで複数ネットワークの切り替え)

chainer1.9.1のMNISTのサンプルコードに、回帰およびCNNのネットワーク構造を追加し、引数で切り替えられるようにしてみた。
現時点での最新のChainerのバージョンは、1.12.0で、MNISTのサンプルコードは大分変っている。Chainer1.9.1は下記からダウンロードできる。
https://github.com/pfnet/chainer/releases

変更を加えたのは、net.pyとtrain_mnist.pyである。
まず、net.pyだが、下記のようにMnistRegressionとMnistCNNクラスを追加した。
MnistCNNに関しては、下記のブログを参考にした。
http://bunji2.hatenablog.com/entry/2016/05/22/213114

import chainer
import chainer.functions as F
import chainer.links as L


class MnistMLP(chainer.Chain):

    """An example of multi-layer perceptron for MNIST dataset.

    This is a very simple implementation of an MLP. You can modify this code to
    build your own neural net.

    """
    def __init__(self, n_in, n_units, n_out=10):
        super(MnistMLP, self).__init__(
            l1=L.Linear(n_in, n_units),
            l2=L.Linear(n_units, n_units),
            l3=L.Linear(n_units, n_out)
        )

    def __call__(self, x):
        h1 = F.relu(self.l1(x))
        h2 = F.relu(self.l2(h1))
        return self.l3(h2)

class MnistRegression(chainer.Chain):
    """An example of regression for MNIST dataset.

    """

    def __init__(self, n_in, n_units, n_out=1):
        super(MnistRegression, self).__init__(
            l1=L.Linear(n_in, n_units),
            l2=L.Linear(n_units, n_units),
            l3=L.Linear(n_units,  n_out)
        )
        
    def __call__(self, x):
        h1 = F.dropout(F.relu(self.l1(x)))
        h2 = F.dropout(F.relu(self.l2(h1)))
        return self.l3(h2)        

    
class MnistCNN(chainer.Chain):

    """An example of convolutional neural network for MNIST dataset.

    """

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

    def __call__(self, x):
        # param x --- chainer.Variable of array

        x.data = x.data.reshape((len(x.data), 1, 28, 28))

        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)))
        y = self.l2(h)
        return y
        
class MnistMLPParallel(chainer.Chain):

    """An example of model-parallel MLP.

    This chain combines four small MLPs on two different devices.

    """
    def __init__(self, n_in, n_units, n_out):
        super(MnistMLPParallel, self).__init__(
            first0=MnistMLP(n_in, n_units // 2, n_units).to_gpu(0),
            first1=MnistMLP(n_in, n_units // 2, n_units).to_gpu(1),
            second0=MnistMLP(n_units, n_units // 2, n_out).to_gpu(0),
            second1=MnistMLP(n_units, n_units // 2, n_out).to_gpu(1),
        )

    def __call__(self, x):
        # assume x is on GPU 0
        x1 = F.copy(x, 1)

        z0 = self.first0(x)
        z1 = self.first1(x1)

        # sync
        h0 = z0 + F.copy(z1, 0)
        h1 = z1 + F.copy(z0, 1)

        y0 = self.second0(F.relu(h0))
        y1 = self.second1(F.relu(h1))

        # sync
        y = y0 + F.copy(y1, 0)
        return y

MnistRegressionは、MnistMLPと同じ3層のmulti-layer perceptron(MLP)であるが、出力層のユニット数がデフォルトで1つに設定されているところが異なる。また後述するが、loss関数がmean_squared_errorという点においても異なる。
一方、MnistCNNは、conv層が3層あり、その後full connected層が2層続いている。conv層の入力は画像なので、入力ベクトルx.dataを28x28の行列に変換している。

x.data = x.data.reshape((len(x.data), 1, 28, 28))

次に、train_mnist.pyは下記のように変更している。

#!/usr/bin/env python
"""Chainer example: train a multi-layer perceptron on MNIST

This is a minimal example to write a feed-forward net.

"""
from __future__ import print_function
import argparse
import time

import numpy as np
import six
import csv

import chainer
from chainer import computational_graph
from chainer import cuda
import chainer.links as L
from chainer import optimizers
from chainer import serializers
import chainer.functions as F


import data
import net

#---------------------------
# Setup parameters
parser = argparse.ArgumentParser(description='Chainer example: MNIST')
parser.add_argument('--initmodel', '-m', default='',
                    help='Initialize the model from given file')
parser.add_argument('--resume', '-r', default='',
                    help='Resume the optimization from snapshot')
parser.add_argument('--net', '-n', choices=('MLP', 'CNN', 'Regression', 'Parallel'),
                    default='regression', help='Network type')
parser.add_argument('--optimizer', '-o', choices=('SGD', 'MomentumSGD', 'NesterovAG', 'Adagrad','Adadelta','RMSprop','Adam'),
                    default='SGD', help='optimization type')
parser.add_argument('--gpu', '-g', default=-1, type=int,
                    help='GPU ID (negative value indicates CPU)')
parser.add_argument('--epoch', '-e', default=20, type=int,
                    help='number of epochs to learn')
parser.add_argument('--unit', '-u', default=1000, type=int,
                    help='number of units')
parser.add_argument('--batchsize', '-b', type=int, default=100,
                    help='learning minibatch size')
args = parser.parse_args()

batchsize = args.batchsize
n_epoch = args.epoch
n_units = args.unit
N = 60000

print('GPU: {}'.format(args.gpu))
print('# unit: {}'.format(args.unit))
print('# Minibatch-size: {}'.format(args.batchsize))
print('# epoch: {}'.format(args.epoch))
print('Network type: {}'.format(args.net))
print('')
#---------------------------

#---------------------------
# Prepare dataset
print('load MNIST dataset')
mnist = data.load_mnist_data()
mnist['data'] = mnist['data'].astype(np.float32)
mnist['data'] /= 255
mnist['target'] = mnist['target'].astype(np.int32)
#---------------------------

#---------------------------
# Prepare network defined in net.py
if args.net == 'MLP':
    model = L.Classifier(net.MnistMLP(784, n_units, 10))
    
elif args.net == 'CNN':
    model = L.Classifier(net.MnistCNN())
    
elif args.net == 'Regression':
    mnist['target'] = mnist['target'].astype(np.float32).reshape(len(mnist['target']), 1)
    model = L.Classifier(net.MnistRegression(784,n_units,1),lossfun=F.mean_squared_error)
    model.compute_accuracy = False
        
elif args.net == 'Parallel':
    cuda.check_cuda_available()
    model = L.Classifier(net.MnistMLPParallel(784, n_units, 10))
    xp = cuda.cupy
#---------------------------
    
#---------------------------
# Split data to train and test
x_train, x_test = np.split(mnist['data'],   [N])
y_train, y_test = np.split(mnist['target'], [N])
N_test = y_test.size
#---------------------------

#---------------------------
# Setup gpu
if args.net != 'parallel':
    if args.gpu >= 0:
        cuda.get_device(args.gpu).use()
        model.to_gpu()
    xp = np if args.gpu < 0 else cuda.cupy
#---------------------------
    
#---------------------------
# Setup optimizer
if args.optimizer == 'SGD':
    optimizer = optimizers.SGD()
elif args.optimizer == 'MomentumSGD':
    optimizer = optimizers.MomentumSGD
elif args.optimizer == 'NesterovAG':
    optimizer = optimizers.NesterovAG()
elif args.optimizer == 'Adagrad':
    optimizer = optimizers.AdaGrad()
elif args.optimizer == 'Adadelta':
    optimizer = optimizers.AdaDelta()
elif args.optimizer == 'RMSprop':
    optimizer = optimizers.RMSprop()
elif args.optimizer == 'Adam':
    optimizer = optimizers.Adam()
    
optimizer.setup(model)
#---------------------------

#---------------------------
# Init/Resume
if args.initmodel:
    print('Load model from', args.initmodel)
    serializers.load_npz(args.initmodel, model)
if args.resume:
    print('Load optimizer state from', args.resume)
    serializers.load_npz(args.resume, optimizer)
#---------------------------
    
#---------------------------
# Training loop with epoch
test_loss_accuracy=[]
for epoch in six.moves.range(1, n_epoch + 1):
    print('epoch', epoch)

    # Training Loop with minibatch
    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(xp.asarray(x_train[perm[i:i + batchsize]]))
        t = chainer.Variable(xp.asarray(y_train[perm[i:i + batchsize]]))

        # Pass the loss function (Classifier defines it) and its arguments
        optimizer.update(model, x, t)

        #---------------------------
        # Save network architecture
        if epoch == 1 and i == 0:
            with open('graph.dot', 'w') as o:
                variable_style = {'shape': 'octagon', 'fillcolor': '#E0E0E0',
                                  'style': 'filled'}
                function_style = {'shape': 'record', 'fillcolor': '#6495ED',
                                  'style': 'filled'}
                g = computational_graph.build_computational_graph(
                    (model.loss, ),
                    variable_style=variable_style,
                    function_style=function_style)
                o.write(g.dump())
            print('graph generated')
        #---------------------------
            
        #---------------------------
        # Compute loss and accuracy
        sum_loss += float(model.loss.data) * len(t.data)
        
        if (args.net == 'Regression'):            
            sum_accuracy += float(np.sum(model.y.data.astype(np.int32) == t.data.astype(np.int32)))
            
        else:
            sum_accuracy += float(model.accuracy.data) * len(t.data)
        #---------------------------
            
    #---------------------------
    # Compute throughput
    end = time.time()
    elapsed_time = end - start
    throughput = N / elapsed_time
    #---------------------------

    #---------------------------
    # Printout    
    print('train mean loss={}, accuracy={}, throughput={} images/sec'.format(
        sum_loss / N, sum_accuracy / N, throughput))
    #---------------------------
     
    #---------------------------
    # Test loss and accuracy
    sum_accuracy = 0
    sum_loss = 0
    
    # Test Loop with minibatch
    for i in six.moves.range(0, N_test, batchsize):
        x = chainer.Variable(xp.asarray(x_test[i:i + batchsize]),
                             volatile='on')
        t = chainer.Variable(xp.asarray(y_test[i:i + batchsize]),
                             volatile='on')
        
        #---------------------
        # Compute loss and accuracy
        loss = model(x, t)
        sum_loss += float(loss.data) * len(t.data)
        
        if (args.net == 'Regression'):
             accuracy = float(np.sum(model.y.data.astype(np.int32) == t.data.astype(np.int32)))
        else:
            accuracy = float(model.accuracy.data) * len(t.data)
          
        sum_accuracy += accuracy
        #---------------------
        
    #---------------------
    # Printout
    print('test  mean loss={}, accuracy={}'.format(
        sum_loss / N_test, sum_accuracy / N_test))
    #---------------------------
    
    #---------------------------    
    # Record test loss and accuracy
    test_loss_accuracy.append([sum_loss/N_test,sum_accuracy/N_test])
     #---------------------------
    
#---------------------------
# Save model, optimizer, loss and accuracy
print('save the model')
fname = args.net + '_' + args.optimizer + '.model'
serializers.save_npz(fname, model)

print('save the optimizer')
fname = args.net + '_' + args.optimizer + '.state'
serializers.save_npz(fname, optimizer)

print('save the losses and accuracies')
fname = args.net + '_' + args.optimizer + '_loss_accuracy.csv'
f=open(fname,'w')
writer=csv.writer(f,lineterminator='\n')
writer.writerow(['loss','accuracy'])
writer.writerows(test_loss_accuracy)
f.close()
#---------------------------

このtrain_mnist.pyでは、引数netにてMLP、CNNおよびRegressionを切り替えできるようになっている。また、引数optimizerにて、最適化の手法を、SGD, MomentumSGD, NesterovAG, Adagrad, Adadelta, RMSprop, およびAdamから選択することができる。
例えば、ネットワーク構造をMLP、最適化手法をSGDに設定して、GPUを用いて学習するためには、下記のように実行する。

> python train_mnist.py --gpu 0 --net=MLP --optimizer=SGD
GPU: 0
# unit: 1000
# Minibatch-size: 100
# epoch: 20
Network type: MLP

load MNIST dataset
epoch 1
graph generated
train mean loss=1.08769297053, accuracy=0.770116665624, throughput=3148.61461052 images/sec 

また、ネットワークとしてregressionが選択された場合は、loss関数として、mean_squared_errorを用いて、教師ラベルが連続値をとるので、mnist['target']をnp.int32からnp.float32に変更し、さらには縦ベクトルに変更している。また、分類問題ではないのでデフォルトのaccuracyの計算ではエラーが出るので、compute_accuracyをFalseに設定してオフにしている。

    mnist['target'] = mnist['target'].astype(np.float32).reshape(len(mnist['target']), 1)
    model = L.Classifier(net.MnistRegression(784,n_units,1),lossfun=F.mean_squared_error)
    model.compute_accuracy = False

ただし、他の分類問題とaccuracyを比較したいので、regressionの場合でも下記のように予測値yを四捨五入し真値tと比較して強引にaccuracyを計算している。

        if (args.net == 'regression'):
             accuracy = float(np.sum(model.y.data.astype(np.int32) == t.data.astype(np.int32)))
        else:
            accuracy = float(model.accuracy.data) * len(t.data)


一方、MLPおよびCNNでは、lossfunとして、chainer.links.Classifierのデフォルトのsoftmax_cross_entropyが用いられている。

model = L.Classifier(net.MnistMLP(784, n_units, 10))
model = L.Classifier(net.MnistCNN())

最後に、Testのlossとaccuracyの値を、後々プロットすることを考えて、test_loss_accuracyという変数にappendで格納し、csvファイルに保存している。ファイル名は、「引数のnet名_引数のoptimizer名_loss_accuracy.csv」である。

    #---------------------------    
    # Record test loss and accuracy
    test_loss_accuracy.append([sum_loss/N_test,sum_accuracy/N_test])
    #---------------------------

#---------------------------
# Save model, optimizer, loss and accuracy
print('save the model')
fname = args.net + '_' + args.optimizer + '.model'
serializers.save_npz(fname, model)

print('save the optimizer')
fname = args.net + '_' + args.optimizer + '.state'
serializers.save_npz(fname, optimizer)

print('save the losses and accuracies')
fname = args.net + '_' + args.optimizer + '_loss_accuracy.csv'
f=open(fname,'w')
writer=csv.writer(f,lineterminator='\n')
writer.writerow(['loss','accuracy'])
writer.writerows(test_loss_accuracy)
f.close()
#---------------------------

下記のようなバッチファイル(run.bat)を作成して、コマンドプロンプトで実行すると、全組み合わせを一括で実行し、それぞれの結果をcsvファイルに保存することができる。

python train_mnist.py --gpu 0 --net=MLP --optimizer=SGD
python train_mnist.py --gpu 0 --net=MLP --optimizer=MomentumSGD
python train_mnist.py --gpu 0 --net=MLP --optimizer=Adagrad
python train_mnist.py --gpu 0 --net=MLP --optimizer=Adadelta
python train_mnist.py --gpu 0 --net=MLP --optimizer=RMSprop
python train_mnist.py --gpu 0 --net=MLP --optimizer=Adam
python train_mnist.py --gpu 0 --net=CNN --optimizer=SGD
python train_mnist.py --gpu 0 --net=CNN --optimizer=MomentumSGD
python train_mnist.py --gpu 0 --net=CNN --optimizer=Adagrad
python train_mnist.py --gpu 0 --net=CNN --optimizer=Adadelta
python train_mnist.py --gpu 0 --net=CNN --optimizer=RMSprop
python train_mnist.py --gpu 0 --net=CNN --optimizer=Adam
python train_mnist.py --gpu 0 --net=Regression --optimizer=SGD
python train_mnist.py --gpu 0 --net=Regression --optimizer=MomentumSGD
python train_mnist.py --gpu 0 --net=Regression --optimizer=Adagrad
python train_mnist.py --gpu 0 --net=Regression --optimizer=Adadelta
python train_mnist.py --gpu 0 --net=Regression --optimizer=RMSprop
python train_mnist.py --gpu 0 --net=Regression --optimizer=Adam

ネットワーク構造としてMLPを用いた場合の、各最適化手法のaccuracyの推移は以下のようになった。
f:id:hirotaka_hachiya:20160802220214p:plain


また、ネットワーク構造としてCNNを用いた場合の、各最適化手法のaccuracyの推移は以下のようになった。
f:id:hirotaka_hachiya:20160802220957p:plain

どちらもAdadeltaとAdamが良い性能を出していた。RMSpropはMLPではAdagradと同程度の性能を出していたものの、CNNではaccuracyが最初から最後のepochまで、0.11台で横ばいで学習ができていなかった(accuracyが低すぎるためグラフに表れていない)。

一方、ネットワーク構造としてRegressionを用いた場合は、以下のようにlossは順調に下がっていくものの、連続の予測値を四捨五入して強引にクラスに割り振っているためか、accuracyは散々な結果になった。
f:id:hirotaka_hachiya:20160802222124p:plain
f:id:hirotaka_hachiya:20160802222132p:plain