Keras: ImageNetで学習済みのVGG16をPlaces365へ転移学習する
Kerasを使って、ImageNetで学習済みモデル (VGG16) をPlaces365の分類タスクへ転移学習する、ということに取り組みます。
今回使用するパッケージたちです。
import numpy as np import pandas as pd import os import shutil from keras.applications.vgg16 import VGG16, preprocess_input, decode_predictions from keras.models import Sequential from keras.layers import Flatten, Dense from keras.preprocessing import image from sklearn.model_selection import train_test_split
VGG16
VGG16は23層 (内 学習が必要な層は16層) からなる画像クラス分類タスクに特化したネットワークです (提案論文) 。下図は原文Table 1.の抜粋で、VGG16は右から2番目の構成です。
KerasにはImageNetで事前学習 (1000クラスの分類タスク) されたモデルが組み込まれております。このモデルを転移学習させる元 (ソースドメイン) とします。
Places365
Places365も画像の分類タスクなのですが、その名の通り365種類の場所を分類します。このタスクを転移学習させる先 (ターゲットドメイン) とします。
Places: A 10 million Image Database for Scene Recognition
学習済みモデルで分類
まずはVGG16の学習済みモデルを使って、任意の画像を分類します。
- VGG16をインスタンスを生成
include_top=True
で、全結合層も含めます (このあとの転移学習ではFalseにして全結合層を削除します)weights='imagenet'
で、ImageNetで学習したモデルになります
# 学習済みモデルをロード model = VGG16(include_top=True, weights='imagenet') model.summary()
224x224x3 -> (Conv2Dx2 -> MaxPooling)x2 -> (Conv2Dx3 -> MaxPooling)x3 -> FC -> FC -> sigmoid -> 1000 というネットワークになってます (上の表のとおりです) 。
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= input_1 (InputLayer) (None, 224, 224, 3) 0 _________________________________________________________________ block1_conv1 (Conv2D) (None, 224, 224, 64) 1792 _________________________________________________________________ block1_conv2 (Conv2D) (None, 224, 224, 64) 36928 _________________________________________________________________ block1_pool (MaxPooling2D) (None, 112, 112, 64) 0 _________________________________________________________________ block2_conv1 (Conv2D) (None, 112, 112, 128) 73856 _________________________________________________________________ block2_conv2 (Conv2D) (None, 112, 112, 128) 147584 _________________________________________________________________ block2_pool (MaxPooling2D) (None, 56, 56, 128) 0 _________________________________________________________________ block3_conv1 (Conv2D) (None, 56, 56, 256) 295168 _________________________________________________________________ block3_conv2 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_conv3 (Conv2D) (None, 56, 56, 256) 590080 _________________________________________________________________ block3_pool (MaxPooling2D) (None, 28, 28, 256) 0 _________________________________________________________________ block4_conv1 (Conv2D) (None, 28, 28, 512) 1180160 _________________________________________________________________ block4_conv2 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_conv3 (Conv2D) (None, 28, 28, 512) 2359808 _________________________________________________________________ block4_pool (MaxPooling2D) (None, 14, 14, 512) 0 _________________________________________________________________ block5_conv1 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv2 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_conv3 (Conv2D) (None, 14, 14, 512) 2359808 _________________________________________________________________ block5_pool (MaxPooling2D) (None, 7, 7, 512) 0 _________________________________________________________________ flatten (Flatten) (None, 25088) 0 _________________________________________________________________ fc1 (Dense) (None, 4096) 102764544 _________________________________________________________________ fc2 (Dense) (None, 4096) 16781312 _________________________________________________________________ predictions (Dense) (None, 1000) 4097000 ================================================================= Total params: 138,357,544 Trainable params: 138,357,544 Non-trainable params: 0 _________________________________________________________________
こんな感じで予測します。
# 画像を224x224にリサイズしてnumpy形式でロード target_size = (224, 224) def load_input_image(path): x = image.img_to_array(image.load_img(path, target_size=target_size)) x = np.expand_dims(x, axis=0) return x # 予測値を算出 x = load_input_image('./images/yagi.jpg') x_predicts = model.predict(preprocess_input(x)) results = decode_predictions(x_predicts, top=5) for result in results: print(result)
まずはヤギの写真。四足の哺乳類という点は抑えてますが、ヒツジと間違えられてます。顔だけというのもあり、やむなしですね。
[('n02412080', 'ram', 0.29453668), ('n02437616', 'llama', 0.19823727), ('n01883070', 'wombat', 0.0629531), ('n01882714', 'koala', 0.062317774), ('n01877812', 'wallaby', 0.05154209)]
次は公園の写真。こちらは違和感の無い結果になってます。特定の物体ではなく、風景というニュアンスをきっちり汲み取っており、全体的な情景も表現可能なモデルとなっていることがわかります。
[('n09332890', 'lakeside', 0.71276414), ('n02859443', 'boathouse', 0.08407786), ('n09468604', 'valley', 0.05165147), ('n02951358', 'canoe', 0.049545184), ('n02980441', 'castle', 0.039445978)]
最後にビルの写真。特徴的なカーブがベンチと勘違いさせた、ように見えますね (ベンチの画像をオーグメンテーションしてみたら類似の画像を作られるかもしれません) 。それ以外にも窓の枠や反射に引きずられて、良い感じの答えになってないです。
[('n03891251', 'park_bench', 0.14174516), ('n04258138', 'solar_dish', 0.13436465), ('n04435653', 'tile_roof', 0.12912594), ('n03028079', 'church', 0.10859317), ('n02825657', 'bell_cote', 0.06655419)]
転移学習
Places365 Standardのダウンロード
予めカレントディレクトにファイルをダウンロード・展開しておきます。1つ目には正解ラベル、2つ目には画像データがそれぞれ入ってます。
今回は少数のデータでも学習できることを確認するため、validation用のデータ (ラベルごとに100枚で、計36,500枚) を使いました。また各画像は256 x 256のsmall imageです。
$ wget http://data.csail.mit.edu/places/places365/filelist_places365-standard.tar $ tar -xvf ./filelist_places365-standard.tar $ wget http://data.csail.mit.edu/places/places365/val_256.tar $ tar -xvf ./val_256.tar
2つほど画像データを見てみます。左は"/r/rock_arch"、右は"/g/golf_course"というラベルになってます。右は解釈しづらいですが、placeというドメインなのでゴルフコースというのは正しそうです。
ラベルデータのロードと整形
各ファイルの正解ラベルをDataFrameの形式にします。
# 画像と正解ラベル (ID) の対応付け labels = pd.read_csv('./places365_val.txt', sep=' ', header=None) labels = labels.rename({0: 'file_name', 1: 'category_id'}, axis=1) labels['file_name'] = labels['file_name'].astype(str) # ラベルIDとラベル名の対応付け categories = pd.read_csv('./categories_places365.txt', sep=' ', header=None) categories = categories.rename({0: 'category_name', 1: 'category_id'}, axis=1) labels = pd.merge(labels, categories).sort_values('file_name').reset_index(drop=True)
そして学習データとテストデータへ80:20で分割します。したがって学習データ数は29,200、テストデータ数は7,300となります。
このあと用いるgeneratorのための準備のため、学習用とテスト用の画像ファイルを格納するディレクトリを作成します (train_imagesとtest_images) 。さらに、正解ラベル (ここではcategory_id) と同じ名前のディレクトリをその下に作成し、ファイルをコピーします。例えば、category_id=1のテストデータはtest_images/1/の下、category_id=2の学習データはtrain_images/2/の下に、それぞれ配置します。
# 学習用データとテストデータで80:20で分割 train_labels, test_labels = train_test_split(labels, test_size=0.2, stratify=labels['category_id'], random_state=1) train_size = train_labels.shape[0] test_size = test_labels.shape[0] # (test|train)_images/{category_id}/*.jpg にコピー labels = 365 for parent in ['test_images', 'train_images']: os.mkdir(parent) for child in range(labels): os.mkdir(parent + '/' + str(child)) for i, f in zip(test_labels['category_id'], test_labels['file_name']): shutil.copy('./val_256/' + f, './test_images/' + str(i) + '/' + f) for i, f in zip(train_labels['category_id'], train_labels['file_name']): shutil.copy('./val_256/' + f, './train_images/' + str(i) + '/' + f)
モデルの生成と学習
VGG16から全結合層を除いたモデルをロードし、代わりにオリジナルの全結合層2層 (FC -> sigmoid) を追加してます。
- 学習済みモデルで
trainable=False
とすることで、その層は重みが更新されなくなります
input_shape = (224, 224, 3) classes = 365 # VGG16から全結合層を除く conv_layers = VGG16(include_top=False, weights='imagenet', input_shape=input_shape, classes=classes) conv_layers.trainable = False # 学習させない (学習済みの重みを使う) # VGG16に全結合層を追加 model = Sequential() model.add(conv_layers) model.add(Flatten()) model.add(Dense(1024, activation='relu')) model.add(Dense(365, activation='sigmoid')) model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['accuracy']) model.summary()
VGG16のパラメータはNon-trainableであることに着目してください。
_________________________________________________________________ Layer (type) Output Shape Param # ================================================================= vgg16 (Model) (None, 7, 7, 512) 14714688 _________________________________________________________________ flatten_2 (Flatten) (None, 25088) 0 _________________________________________________________________ dense_3 (Dense) (None, 1024) 25691136 _________________________________________________________________ dense_4 (Dense) (None, 365) 374125 ================================================================= Total params: 40,779,949 Trainable params: 26,065,261 Non-trainable params: 14,714,688 _________________________________________________________________
上のモデルを学習すると、20エポックでaccuracyで0.75となりました。まだサチってはいないので、エポック数を増やせば更に改善しそうです。
- ImageDataGeneratorを使うことで、0〜255 -> 0.0〜1.0にスケールされた画像が生成されます
- flow_from_directory()で、学習用のディレクトリを指定 (カテゴリ名などもパスから推定されます)
- GPUを使って約100分かかりました
batch_size = 40 target_size = (224, 224) # 学習データのジェネレータ train_generator = image.ImageDataGenerator(rescale=1.0/255).flow_from_directory( './train_images/', target_size=target_size, class_mode='categorical', batch_size=batch_size ) # Found 29200 images belonging to 365 classes. # 学習 history = model.fit_generator( generator=train_generator, steps_per_epoch=train_size/batch_size, epochs=20, verbose=2 ) # Epoch 1/20 # - 309s - loss: 5.0425 - acc: 0.0372 # Epoch 2/20 # - 308s - loss: 4.0309 - acc: 0.1147 # ... # Epoch 20/20 # - 304s - loss: 0.9108 - acc: 0.7510
テストデータでの評価
上で得られたモデルを使って、テストデータで評価しました。ところがaccuracyは0.10程度と学習データとかけ離れており、完全に過学習していました。
# テストデータのジェネレータ test_generator = image.ImageDataGenerator(rescale=1.0/255).flow_from_directory( './test_images/', target_size=target_size, class_mode='categorical', batch_size=batch_size, shuffle=False ) # Found 7300 images belonging to 365 classes. # 評価 results = model.evaluate_generator(test_generator, steps=test_size/batch_size, verbose=1) # [6.848537857891762, 0.1019178093194145]
まとめ
ImageNetで学習済みのVGG16を、Places365の分類タスクへ転移学習してみました。
学習データに対する精度はまあまあ良かったのですが、完全に過学習していました。オーグメンテーションしたり、全結合層をチューニングしたりなどはやはり必要だなあ、というところですね。