け日記

最近はPythonでいろいろやってます

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というドメインなのでゴルフコースというのは正しそうです。

f:id:ohke:20190323215358p:plainf:id:ohke:20190323215349p:plain

ラベルデータのロードと整形

各ファイルの正解ラベルを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)

f:id:ohke:20190323215928p:plain

そして学習データとテストデータへ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の分類タスクへ転移学習してみました。

学習データに対する精度はまあまあ良かったのですが、完全に過学習していました。オーグメンテーションしたり、全結合層をチューニングしたりなどはやはり必要だなあ、というところですね。