猫になりたい

IT企業のデータ分析屋、計量経済とか機械学習をやっています。pyてょnは3.6を使ってマスコレルウィンストングリーン。

VGG16をkerasで実装した


スポンサーリンク

kerasのpre-traindedモデルにもあるVGG16をkerasで実装しました。
単純にVGG16を使うだけならpre-traindedモデルを使えばいいのですが、自分でネットワーク構造をいじりたいときに不便+実装の勉強がしたかったので実装してみました。

VGG16とは

VGGチームがILSVRC-2014で使用し、localisation and classification tasksで1位と2位を取ったモデルです。論文がarXivで公開されています

VGG16の他にもレイヤー数が多いVGG19というのがあり、論文のTable 1のDとEがそれぞれ該当します。また名前の16と19という数字は重みがある(つまりConvとFC)レイヤの数に由来している様です。 VGG16のほうがVGG19よりよく使われていますが、これはVGG16と19の性能がそれほど変わらないのでそれならレイヤー数が少ない方で良いじゃん、という理由からだと思われます。

実装と学習・評価

Keras 2.1.5のtensorflow 1.7.0バックエンドでJupyter上で実装しました。
参考にしたのは原論文と本家のconfigファイル(prototxt)です。

本家との違いは以下の3点です。

  • データはImageNetの代わりにCIFAR10を用いた
  • 32x32のcifar10の画像を224x224にリサイズして入力するとメモリが足りないのかjupyterのkernelが死ぬので、最初のレイヤーでon-the-flyにアップスケールした
  • cifar10の画像はサイズが揃っているので、croppingはしなかった

まず必要なものをimportし、cifar10のデータをロードします。

import keras
from keras.layers import Conv2D, MaxPooling2D, Lambda, Input, Dense, Flatten, BatchNormalization
from keras.models import Model
from keras.layers.core import Dropout
from keras import optimizers
import tensorflow as tf
from keras.callbacks import ReduceLROnPlateau,TensorBoard

from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report

from sklearn.preprocessing import OneHotEncoder
from keras.datasets import cifar10
import cv2
# import gc
import numpy as np

print(tf.__version__)
print(keras.__version__)


# Prepare data
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
enc = OneHotEncoder()
y_train = enc.fit_transform(y_train).toarray()
y_test = enc.fit_transform(y_test).toarray()

モデル

次にモデルを定義しましょう。

# inputs = Input(shape=(224, 224, 3))
inputs = Input(shape=(32, 32, 3))
# Due to memory limitation, images will resized on-the-fly.
x = Lambda(lambda image: tf.image.resize_images(image, (224, 224)))(inputs)
x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv1')(inputs)
x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv2')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block1_pool')(x)
x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv1')(x)
x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv2')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block2_pool')(x)
x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv1')(x)
x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv2')(x)
x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv3')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block3_pool')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv1')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv2')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv3')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block4_pool')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv1')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv2')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv3')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block5_pool')(x)
flattened = Flatten(name='flatten')(x)
x = Dense(4096, activation='relu', name='fc1')(flattened)
x = Dropout(0.5, name='dropout1')(x)
x = Dense(4096, activation='relu', name='fc2')(x)
x = Dropout(0.5, name='dropout2')(x)
# CIFAR10は10クラスなので出力の数が違う
predictions = Dense(10, activation='softmax', name='predictions')(x)


BATCH_SIZE = 256
sgd = optimizers.SGD(lr=0.01,
                     momentum=0.9,
                     decay=5e-4)#, nesterov=False)

model = Model(inputs=inputs, outputs=predictions)


model.compile(optimizer=sgd,
              loss='categorical_crossentropy',
              metrics=['accuracy'])

折角なのでモデルのsummaryを見ておきましょう。

model.summary()
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
input_1 (InputLayer)         (None, 32, 32, 3)         0         
_________________________________________________________________
block1_conv1 (Conv2D)        (None, 32, 32, 64)        1792      
_________________________________________________________________
block1_conv2 (Conv2D)        (None, 32, 32, 64)        36928     
_________________________________________________________________
block1_pool (MaxPooling2D)   (None, 16, 16, 64)        0         
_________________________________________________________________
block2_conv1 (Conv2D)        (None, 16, 16, 128)       73856     
_________________________________________________________________
block2_conv2 (Conv2D)        (None, 16, 16, 128)       147584    
_________________________________________________________________
block2_pool (MaxPooling2D)   (None, 8, 8, 128)         0         
_________________________________________________________________
block3_conv1 (Conv2D)        (None, 8, 8, 256)         295168    
_________________________________________________________________
block3_conv2 (Conv2D)        (None, 8, 8, 256)         590080    
_________________________________________________________________
block3_conv3 (Conv2D)        (None, 8, 8, 256)         590080    
_________________________________________________________________
block3_pool (MaxPooling2D)   (None, 4, 4, 256)         0         
_________________________________________________________________
block4_conv1 (Conv2D)        (None, 4, 4, 512)         1180160   
_________________________________________________________________
block4_conv2 (Conv2D)        (None, 4, 4, 512)         2359808   
_________________________________________________________________
block4_conv3 (Conv2D)        (None, 4, 4, 512)         2359808   
_________________________________________________________________
block4_pool (MaxPooling2D)   (None, 2, 2, 512)         0         
_________________________________________________________________
block5_conv1 (Conv2D)        (None, 2, 2, 512)         2359808   
_________________________________________________________________
block5_conv2 (Conv2D)        (None, 2, 2, 512)         2359808   
_________________________________________________________________
block5_conv3 (Conv2D)        (None, 2, 2, 512)         2359808   
_________________________________________________________________
block5_pool (MaxPooling2D)   (None, 1, 1, 512)         0         
_________________________________________________________________
flatten (Flatten)            (None, 512)               0         
_________________________________________________________________
fc1 (Dense)                  (None, 4096)              2101248   
_________________________________________________________________
dropout1 (Dropout)           (None, 4096)              0         
_________________________________________________________________
fc2 (Dense)                  (None, 4096)              16781312  
_________________________________________________________________
dropout2 (Dropout)           (None, 4096)              0         
_________________________________________________________________
predictions (Dense)          (None, 10)                40970     
=================================================================
Total params: 33,638,218
Trainable params: 33,638,218
Non-trainable params: 0
_________________________________________________________________

学習

それでは学習をさせてみましょう。エポック数は原論文で最終的に回した74を指定しました。学習はTesla K80上で行い、59min 24sかかりました。 本来はImageNetでやるべきなんでしょうし、画像の蒐集もある程度は行ったのですが、学習時間と手間を考えてCIFAR10でテストを行いました。CIFAR10は32*32のサイズなので最初のレイヤーにリサイズするレイヤーを追加して学習させます。

%%time
# Train
rlop = ReduceLROnPlateau(monitor='val_acc',
                         factor=0.1,
                         patience=5,
                         verbose=1,
                         mode='auto',
                         epsilon=0.0001,
                         cooldown=0,
                         min_lr=0.00001)

 
model.fit(x_train, y_train, batch_size=BATCH_SIZE, epochs=74, verbose=1,
              callbacks=[rlop], validation_data=(x_test, y_test))

評価

学習が終わったら次は評価です。

y_pred = model.predict(x_test, verbose=1)

print(confusion_matrix(np.argmax(y_test, 1), np.argmax(y_pred, 1)))
print(classification_report(np.argmax(y_test, 1), np.argmax(y_pred, 1)))
10000/10000 [==============================] - 7s 663us/step
[[816  17  33  19  15   4   6  12  57  21]
 [ 10 895   2   6   3   2   6   1  19  56]
 [ 54   3 678  53  71  49  59  19   6   8]
 [ 20   6  69 593  53 150  56  29  11  13]
 [ 17   3  51  58 739  26  44  58   3   1]
 [  5   2  37 178  40 665  22  44   4   3]
 [  6   2  30  67  29  12 838   7   5   4]
 [ 15   1  22  44  59  50   3 797   1   8]
 [ 53  20   9  22   8   2   6   2 865  13]
 [ 25  62   7  14   3   5   5  10  22 847]]
             precision    recall  f1-score   support

          0       0.80      0.82      0.81      1000
          1       0.89      0.90      0.89      1000
          2       0.72      0.68      0.70      1000
          3       0.56      0.59      0.58      1000
          4       0.72      0.74      0.73      1000
          5       0.69      0.67      0.68      1000
          6       0.80      0.84      0.82      1000
          7       0.81      0.80      0.81      1000
          8       0.87      0.86      0.87      1000
          9       0.87      0.85      0.86      1000

avg / total       0.77      0.77      0.77     10000

結果を見ると3と5のクラスが当てられていないようですね。

改良

今時(もう古い?)はBatchNormalizationを入れないと、ということでBatchNormalizationを入れたモデルを回してみました。自分で組むと簡単にネットワークをいじれていいですね。

モデル

各Conv. レイヤーのactivationの後にのみBatchNormalizationを入れました。Dropoutがあるところでは両方入れなくてもいいだろうということで入れていません。

# inputs = Input(shape=(224, 224, 3))
inputs = Input(shape=(32, 32, 3))
# Due to memory limitation, images will resized on-the-fly.
x = Lambda(lambda image: tf.image.resize_images(image, (224, 224)))(inputs)
x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv1')(inputs)
x = BatchNormalization()(x)
x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv2')(x)
x = BatchNormalization()(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block1_pool')(x)
x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv1')(x)
x = BatchNormalization()(x)
x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv2')(x)
x = BatchNormalization()(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block2_pool')(x)
x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv1')(x)
x = BatchNormalization()(x)
x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv2')(x)
x = BatchNormalization()(x)
x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv3')(x)
x = BatchNormalization()(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block3_pool')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv1')(x)
x = BatchNormalization()(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv2')(x)
x = BatchNormalization()(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv3')(x)
x = BatchNormalization()(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block4_pool')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv1')(x)
x = BatchNormalization()(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv2')(x)
x = BatchNormalization()(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv3')(x)
x = BatchNormalization()(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='same', name='block5_pool')(x)
flattened = Flatten(name='flatten')(x)
x = Dense(4096, activation='relu', name='fc1')(flattened)
x = Dropout(0.5, name='dropout1')(x)
x = Dense(4096, activation='relu', name='fc2')(x)
x = Dropout(0.5, name='dropout2')(x)
predictions = Dense(10, activation='softmax', name='predictions')(x)

学習と評価

結果は以下のとおりです。

10000/10000 [==============================] - 8s 764us/step
[[847  10  33  18  11   4   3   7  42  25]
 [ 11 899   1   6   3   5   4   0  15  56]
 [ 52   4 697  49  72  52  42  23   3   6]
 [ 14  10  59 658  50 129  41  20  10   9]
 [ 15   4  63  39 779  17  31  44   4   4]
 [ 11   2  20 157  41 708  12  39   6   4]
 [  9   2  37  49  22  22 849   3   3   4]
 [  7   1  22  31  44  37   5 846   0   7]
 [ 47  19   9  10   4   3   1   2 891  14]
 [ 19  48   5   9   3   2   3  12  21 878]]
             precision    recall  f1-score   support

          0       0.82      0.85      0.83      1000
          1       0.90      0.90      0.90      1000
          2       0.74      0.70      0.72      1000
          3       0.64      0.66      0.65      1000
          4       0.76      0.78      0.77      1000
          5       0.72      0.71      0.72      1000
          6       0.86      0.85      0.85      1000
          7       0.85      0.85      0.85      1000
          8       0.90      0.89      0.89      1000
          9       0.87      0.88      0.87      1000

avg / total       0.81      0.81      0.81     10000

全体のf1も上昇していて、クラス3と5も改善しています。やはりBatchNormalizationは強力ですね。

まとめ

今回はVGG16をkerasとtensorflowで実装しました、楽しかったので他のモデルでもやりたいですね。 ただImageNetを用意するのが大変そうなので、データは恐らくまたcifar10とかに為ると思います。 notebookは気が向いたら公開します。

参考文献