(2)料理分類部門 深層学習モデルの実装例

create date : Aug. 8, 2018 at 23:00:00

「人工知能技術戦略会議主催 第1回AIチャレンジコンテスト」( https://signate.jp/competitions/31 ) 料理分類部門の、深層学習モデルの実装例です.

実装にはpythonを用います. versionは2.7です. このチュートリアルの構成は次のようになります.

  • 必要なライブラリ
  • 必要なデータの用意
  • 実装
    1. 画像データの処理
    2. モデルの構築
    3. モデルの学習
    4. 予測値の出力
  • まとめ

この実装はあくまで一例なのでほかにも方法はたくさんあると思われますが, 参考になれば幸いです.
本チュートリアルを読んだ後にできるようになると期待されることは

  • 基本的な画像データの取り扱い
  • 深層学習モデルの構築の仕方

です.

必要なライブラリ

ファイルの処理にpandas, 基本的な数値計算にnumpy, 基本的な画像処理にPIL, 深層学習モデリングにchainerを使います. それぞれpipでインストール可能です. GPUがあって使用したい場合, http://docs.chainer.org/en/stable/install.html を参考にしてください.

必要なデータの用意

https://signate.jp/competitions/31/data へアクセスし, 「データをダウンロード」タブを押し, clf_train_images_labeled_1.zip, clf_train_images_labeled_2.zip, clf_train_master.tsv, clf_test_images_1.zip, clf_test_images_2.zip, clf_test.tsvをダウンロードしてください. 画像データをそれぞれ解凍し,
作業ディレクトリ上で学習データはclf_train_imagesフォルダへ, 評価データはclf_test_imagesフォルダへまとめておいてください.

実装

まず必要なライブラリをインポートします.

In [1]:
import pandas as pd
import numpy as np
import os
import six
import chainer
import chainer.functions as F
import chainer.links as L
import zipfile
from chainer.links import caffe
from chainer import link, Chain, optimizers, Variable
from PIL import Image

1. 画像データの処理

以下のようにImageDataクラスを定義し, 必要な処理メソッドを実装します.

In [2]:
class ImageData():
    def __init__(self, img_dir, meta_data):
        self.img_dir = img_dir

        assert meta_data.endswith('.tsv')
        self.meta_data = pd.read_csv(meta_data, sep='\t')
        self.index = np.array(self.meta_data.index)
        self.split = False

    def shuffle(self):
        assert self.split == True
        self.train_index = np.random.permutation(self.train_index)

    def split_train_val(self, train_size):
        self.train_index = np.random.choice(self.index, train_size, replace=False)
        self.val_index = np.array([i for i in self.index if i not in self.train_index])
        self.split = True

    def generate_minibatch(self, batchsize, img_size = 224, mode = None):
        i = 0
        if mode == 'train':
            assert self.split == True
            meta_data = self.meta_data.ix[self.train_index]
            index = self.train_index

        elif mode == 'val':
            assert self.split == True
            meta_data = self.meta_data.ix[self.val_index]
            index = self.val_index

        else:
            meta_data = self.meta_data
            index = self.index

        while i < len(index):
            data = meta_data.iloc[i:i + batchsize]
            images = []
            for f in list(data['file_name']):
                image = Image.open(os.path.join(self.img_dir, f))
                image = image.resize((img_size, img_size))
                images.append(np.array(image))
            images = np.array(images)
            images = images.transpose((0,3,1,2))
            images = images.astype(np.float32)

            if 'category_id' in data.columns:
                labels = np.array(list(data['category_id']))
                labels = labels.astype(np.int32)
                yield images, labels
            else:
                yield images
            i += batchsize

shuffleは学習データの順番を入れ替えます.
split_train_valは学習データ数(train_size)を指定した上で, 元の学習データを学習用と検証用に分けます.
generate_minibatchは指定された数(batchsize)だけ画像データを読み込み, resizeして大きさをそろえた上でモデルへ入力するために数値へ変換します. 大きさは224×224としています.

2. モデルの構築

以下のようにニューラルネットワーククラスAlexと, 誤差と正解率を出力するクラスClassifierを実装します.
このモデルは2012年にILSVRCで優勝した"AlexNet"というニューラルネットワークモデルです
(https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf).
本コンテストは25種類の料理に分類する問題なので, 出力層の素子数を25とします(元々は1000).

In [3]:
class Alex(Chain):
    def __init__(self):
        super(Alex, self).__init__(
            conv1=L.Convolution2D(3,  96, 11, stride=4),
            conv2=L.Convolution2D(96, 256,  5, pad=2),
            conv3=L.Convolution2D(256, 384,  3, pad=1),
            conv4=L.Convolution2D(384, 384,  3, pad=1),
            conv5=L.Convolution2D(384, 256,  3, pad=1),
            fc6=L.Linear(9216, 4096),
            fc7=L.Linear(4096, 4096),
            fc8=L.Linear(4096, 25),
        )
        self.train = True

    def __call__(self, x):
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv1(x))), 3, stride=2)
        h = F.max_pooling_2d(F.local_response_normalization(
            F.relu(self.conv2(h))), 3, stride=2)
        h = F.relu(self.conv3(h))
        h = F.relu(self.conv4(h))
        h = F.max_pooling_2d(F.relu(self.conv5(h)), 3, stride = 2)
        h = F.dropout(F.relu(self.fc6(h)), train=self.train)
        h = F.dropout(F.relu(self.fc7(h)), train=self.train)
        y = self.fc8(h)

        return y

class Classifier(Chain):
    def __init__(self, predictor):
        super(Classifier, self).__init__(predictor=predictor)

    def __call__(self, x, t):
        y = self.predictor(x)
        self.loss = F.softmax_cross_entropy(y, t)
        self.accuracy = F.accuracy(y, t)

        return self.loss

3. モデルの学習

学習誤差と検証誤差をおえるように以下のような関数を実装します.

In [4]:
def train_val(train_data, classifier, optimizer, num_train = 9000, epochs = 10, batchsize = 30, gpu = True):
    # split data to train and val
    train_data.split_train_val(num_train)

    for epoch in range(epochs):
        # train
        classifier.predictor.train = True
        num_samples = 0
        train_cum_loss = 0
        train_cum_acc = 0
        for data in train_data.generate_minibatch(batchsize, mode = 'train'):
            num_samples += len(data[0])
            #print num_samples, '/', len(train_data.train_index), '(epoch:%s)'%(epoch+1)
            optimizer.zero_grads()
            x, y = Variable(data[0]), Variable(data[1])
            if gpu:
                x.to_gpu()
                y.to_gpu()
            loss = classifier(x, y)

            train_cum_acc += classifier.accuracy.data*batchsize
            #print 'train_accuracy:', train_cum_acc/num_samples
            train_cum_loss += classifier.loss.data*batchsize
            #print 'train_loss:', train_cum_loss/num_samples

            loss.backward()    # back propagation
            optimizer.update() # update parameters

        train_accuracy = train_cum_acc/num_samples
        train_loss = train_cum_loss/num_samples

        # validation
        classifier.predictor.train = False
        num_samples = 0
        val_cum_loss = 0
        val_cum_acc = 0
        for data in train_data.generate_minibatch(batchsize, mode = 'val'):
            num_samples += len(data[0])
            #print num_samples, '/', len(train_data.val_index), '(epoch:%s)'%(epoch+1)
            x, y = Variable(data[0]), Variable(data[1])
            if gpu:
                x.to_gpu()
                y.to_gpu()
            loss = classifier(x, y)

            val_cum_acc += classifier.accuracy.data*batchsize
            #print 'val_accuracy:', val_cum_acc/num_samples
            val_cum_loss += classifier.loss.data*batchsize
            #print 'val_loss:', val_cum_loss/num_samples

        val_accuracy = val_cum_acc/num_samples
        val_loss = val_cum_loss/num_samples

        print '-----------------', 'epoch:', epoch+1, '-----------------'
        print 'train_accuracy:', train_accuracy, 'train_loss:', train_loss
        print 'val_accuracy:', val_accuracy, 'val_loss:', val_loss
        print '\n'

        # shuffle train data
        train_data.shuffle()

    return classifier, optimizer

GPUを使う場合は引数gpuをTrueにしてください. ここでは学習データを9000, 検証データを1000にし, epoch数を10,
batchsizeを30としています.

2012年にILSVRCで優勝した"AlexNet"モデルパラメータを初期値としてパラメータを学習させるために, そのモデルパラメータをダウンロードし,
ここで定義したモデルへそのパラメータをコピーするための関数を実装します.

In [5]:
def download_model(model_name):
    if model_name == 'alexnet':
        url = 'http://dl.caffe.berkeleyvision.org/bvlc_alexnet.caffemodel'
        name = 'bvlc_alexnet.caffemodel'
    elif model_name == 'caffenet':
        url = 'http://dl.caffe.berkeleyvision.org/' \
              'bvlc_reference_caffenet.caffemodel'
        name = 'bvlc_reference_caffenet.caffemodel'
    elif model_name == 'googlenet':
        url = 'http://dl.caffe.berkeleyvision.org/bvlc_googlenet.caffemodel'
        name = 'bvlc_googlenet.caffemodel'
    elif model_name == 'resnet':
        url = 'http://research.microsoft.com/en-us/um/people/kahe/resnet/' \
              'models.zip'
        name = 'models.zip'
    else:
        raise RuntimeError('Invalid model type. Choose from '
                           'alexnet, caffenet, googlenet and resnet.')
    print('Downloading model file...')
    six.moves.urllib.request.urlretrieve(url, name)
    if model_name == 'resnet':
        with zipfile.ZipFile(name, 'r') as zf:
            zf.extractall('.')
    print('Done.')
    return name

def copy_model(src, dst):
    assert isinstance(src, link.Chain)
    assert isinstance(dst, link.Chain)
    for child in src.children():
        if child.name not in dst.__dict__: continue
        dst_child = dst[child.name]
        if type(child) != type(dst_child): continue
        if isinstance(child, link.Chain):
            copy_model(child, dst_child)
        if isinstance(child, link.Link):
            match = True
            for a, b in zip(child.namedparams(), dst_child.namedparams()):
                if a[0] != b[0]:
                    match = False
                    break
                if a[1].data.shape != b[1].data.shape:
                    match = False
                    break
            if not match:
                print ('Ignore %s because of parameter mismatch' % child.name)
                continue
            for a, b in zip(child.namedparams(), dst_child.namedparams()):
                b[1].data = a[1].data
            print ('Copy %s' % child.name)

4. 予測値の出力

評価データに対して料理カテゴリを返す関数を実装します.

In [6]:
def predict(test_data, classifier, batchsize = 5, gpu = True):
    if gpu:
        classifier.predictor.to_gpu()
    else:
        classifier.predictor.to_cpu()
    classifier.predictor.train = False
    num_samples = 0
    predictions = np.zeros((len(test_data.index),25))
    for data in test_data.generate_minibatch(batchsize):
        num_samples += len(data)
        #print num_samples, '/', len(test_data.index)
        x = Variable(data)
        if gpu:
            x.to_gpu()
        yhat = classifier.predictor(x)
        yhat = F.softmax(yhat)
        yhat.to_cpu()
        predictions[num_samples-len(data):num_samples,:] = yhat.data

    return predictions

GPUを使用したい場合は引数gpuをTrueにしてください.

次に実際に学習から予測値の出力までを実行してみます. 評価データに対する予測値は
sample_submit.csvとして出力されます.

In [7]:
model_path = download_model('alexnet')  # モデルパラメータのダウンロード
func = caffe.CaffeFunction(model_path)
alex = Alex()
copy_model(func, alex)                  # モデルパラメータのコピー
alex.to_gpu()                           # gpuを使う場合
classifier = Classifier(alex)
optimizer = optimizers.MomentumSGD(lr=0.0005)  # パラメータの学習方法は慣性項付きの確率的勾配法で, 学習率は0.0005に設定.
optimizer.setup(classifier)
optimizer.add_hook(chainer.optimizer.WeightDecay(0.0001))  # l2正則化

train_data = ImageData('clf_train_images', 'clf_train_master.tsv')  # 学習データの読み込み
test_data = ImageData('clf_test_images', 'clf_test.tsv')            # 評価データの読み込み

classifier, optimizer = train_val(train_data, classifier, optimizer)               # 学習+検証
p = predict(test_data, classifier)                                                 # 予測値(確率値)の出力
pd.DataFrame(p.argmax(axis=1)).to_csv('sample_submit.csv', header=None)            # 予測結果を応募用ファイルとして出力
Copy conv1
Copy conv2
Copy conv3
Copy conv4
Copy conv5
Copy fc6
Copy fc7
Ignore fc8 because of parameter mismatch
----------------- epoch: 1 -----------------
train_accuracy: 0.195222228765 train_loss: 2.66634058952
val_accuracy: 0.38299998641 val_loss: 2.15838885307


----------------- epoch: 2 -----------------
train_accuracy: 0.364888876677 train_loss: 2.01462984085
val_accuracy: 0.43599998951 val_loss: 1.83628439903


----------------- epoch: 3 -----------------
train_accuracy: 0.447888880968 train_loss: 1.73830235004
val_accuracy: 0.451999992132 val_loss: 1.78062355518


----------------- epoch: 4 -----------------
train_accuracy: 0.505888879299 train_loss: 1.52991473675
val_accuracy: 0.488000005484 val_loss: 1.68698990345


----------------- epoch: 5 -----------------
train_accuracy: 0.555333316326 train_loss: 1.36061477661
val_accuracy: 0.523999989033 val_loss: 1.61477351189


----------------- epoch: 6 -----------------
train_accuracy: 0.605111122131 train_loss: 1.21629238129
val_accuracy: 0.505999982357 val_loss: 1.60293531418


----------------- epoch: 7 -----------------
train_accuracy: 0.654555559158 train_loss: 1.04220700264
val_accuracy: 0.526000022888 val_loss: 1.62266731262


----------------- epoch: 8 -----------------
train_accuracy: 0.695999979973 train_loss: 0.913958668709
val_accuracy: 0.509000003338 val_loss: 1.62541043758


----------------- epoch: 9 -----------------
train_accuracy: 0.756333351135 train_loss: 0.751057326794
val_accuracy: 0.535000026226 val_loss: 1.65066516399


----------------- epoch: 10 -----------------
train_accuracy: 0.800999999046 train_loss: 0.614716947079
val_accuracy: 0.54699999094 val_loss: 1.67834806442


結果, 評価データに対するaccuracyは0.52~0.54ほどとなります.
Tesla M40を使うと全体の処理は約15分ほどで終了します.

まとめ

画像の読み込み→前処理→モデルの構築→学習→予測までの実装を一通り説明しました.
まだ工夫の余地は十分にあるので, 別の初期モデルを試したり, オリジナルのモデルを構築する
などして精度改善に挑戦してみてください. 皆さんのご応募をお待ちしております.
※ 一部コードは https://github.com/pfnet/chainer/tree/master/examples/imagenet を参考に作成しました.

create date : Aug. 8, 2018 at 23:00:00