【VGG16にて転移学習】花の分類

はじめに

こんにちは、がんがんです。ポケモンの転移学習が上手くいかなかったため、以下の記事を参考にしてまずは花の分類をしていこうと思います。

aidiary.hatenablog.com

ベンチマークを試すことにより、学習データがきちんとしてないのか、それともモデルに問題があるのかを調査するために実験を行いました。

実験1 まずはそのまま

まずはまったくプログラムをいじらずに実験してみました。結果は以下になります。

f:id:gangannikki:20181031142157p:plain
Accuracy-Loss
f:id:gangannikki:20181031142001p:plain
実験1

Accuracyとlossは上手くいっているものの、predict結果はとんちんかんなことを出力していました。
この結果からまずデータではなく、モデルのプログラムの方に問題があることが分かりました。

kerasのドキュメントを見てみると、flow_from_directoryのclassesの入力内容が間違っていたようです。
以下、kerasのドキュメントから引用。

  • classes: クラスサブディレクトリのリスト.(例えば,['dogs', 'cats']).デフォルトはNone.与えられなければ,クラスのリスト自動的に推論されます(そして,ラベルのインデックスと対応づいたクラスの順序はalphanumericになります).クラス名からクラスインデックスへのマッピングを含む辞書はclass_indeces属性を用いて取得できます.

このようになっています。今回のプログラムではclasses = 17として、種類の数をclassesに入力していたためよく分からない結果となったようです。

実験2 classesの追加

実験2では、参考プログラムを参考にしてclassesを以下のように追加しました。

classes = ['Tulip', 'Snowdrop', 'LilyValley', 'Bluebell', 'Crocus',
           'Iris', 'Tigerlily', 'Daffodil', 'Fritillary', 'Sunflower',
           'Daisy', 'ColtsFoot', 'Dandelion', 'Cowslip', 'Buttercup',
           'Windflower', 'Pansy']

追加後の結果はこちらになります。

f:id:gangannikki:20181031154521p:plainf:id:gangannikki:20181031154524p:plainf:id:gangannikki:20181031154726p:plain
実験2の結果


実験1よりも上手くいっているものの、test_imageを用いて14%はあまりにも低いですね…
他の画像で実験すると1%程度だったりとまだまだ精度は悪かったです。

実験3 検証データのImageDataGeneratorの変更

実験3では、検証データの水増しをrescale,preprocessingに限定してみました。
実験3のAccuracy, Loss, predict結果を以下に示します。今回はepoch=200で試しました。

f:id:gangannikki:20181101150452p:plainf:id:gangannikki:20181101150506p:plainf:id:gangannikki:20181101145949p:plain
実験3の結果

こちらでもほとんど結果は変わりませんでした。別の画像で試しても同様の結果が出ました。
そこでVGG16の論文を読んでみました。
https://arxiv.org/pdf/1409.1556.pdf


すると、Activationがsigmoidではなく、softmaxとなっていました。
本を参考にして組んでいたこともあり、Activationに関しては盲点でした…
そこで実験4ではActivationを変更して実験します。

実験4 Activationの変更

Activationをsigmoidからsoftmaxへと変更して実験しました。

実験結果を以下に示します。

f:id:gangannikki:20181101161325p:plainf:id:gangannikki:20181101161328p:plainf:id:gangannikki:20181101161322p:plain
実験4の結果

しっかりと結果が出ました!
何種類か試した結果を示します。

Iris
f:id:gangannikki:20181101162640j:plain:w256
f:id:gangannikki:20181101162654p:plain

Sunflower
f:id:gangannikki:20181101162719j:plain:w256
f:id:gangannikki:20181101162720p:plain

Dandelion
f:id:gangannikki:20181101162738j:plain:w256
f:id:gangannikki:20181101162748p:plain

画像にちょうちょが写っていても80%以上の認識精度を有しています。

コード

Fine-Tuning_vgg16.py

import math
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.optimizers import SGD
from keras.applications.vgg16 import VGG16, preprocess_input
from keras.callbacks import CSVLogger, EarlyStopping, TensorBoard, ModelCheckpoint
from keras.preprocessing.image import ImageDataGenerator

#  parameter
lr = 1e-4			#  SGDのlr係数
momentum = 0.9			#  SGDのmomentum係数
input_shape = ( 224, 224, 3)	#  VGG16のinput_shape

classes = ['Tulip', 'Snowdrop', 'LilyValley', 'Bluebell', 'Crocus',
           'Iris', 'Tigerlily', 'Daffodil', 'Fritillary', 'Sunflower',
           'Daisy', 'ColtsFoot', 'Dandelion', 'Cowslip', 'Buttercup',
           'Windflower', 'Pansy']

"""
	モデル(VGG16の転移学習)
"""
class VGG16_Transfer(object):
	def __init__(self, args):
		self.train_dir = args.train_dir
		self.validation_dir = args.validation_dir
		self.batch_size = args.batch_size
		self.epochs = args.epochs
				
		#  VGG16の読み込み
		vgg16 = VGG16(include_top=False, weights='imagenet', input_shape=input_shape)
		#vgg16.summary()
	
		#  モデル定義
		self.model = self.build_transfer_model(vgg16)
		#  モデル構築
		self.model.compile( optimizer=SGD(lr=lr, momentum=momentum),
                                    loss='categorical_crossentropy',
				    metrics=["accuracy"] )

		#  モデルの表示
		print("Model Summary------------------")
		self.model.summary()

	"""
		モデル構築
	"""
	def build_transfer_model( self, vgg16 ):
		model = Sequential(vgg16.layers)
		#  再学習しないように指定
		for layer in model.layers[:15]:
			layer.trainable = False
		#  FC層を構築
		model.add(Flatten())
		model.add(Dense(256, activation="relu"))
		model.add(Dropout(0.5))
		model.add(Dense(len(classes), activation="softmax"))
		
		return model
	
	"""
		モデル学習
	"""
	def training( self ):
		#  学習データ
		train_datagen = ImageDataGenerator(
				rescale=1/255.,
				rotation_range=90,			#  画像をランダムに回転
				shear_range=0.1,			#  せん断する
				zoom_range=0.1,				#  ランダムにズーム
				horizontal_flip=True,			#  水平方向にランダム反転
				vertical_flip=True,			#  垂直方向にランダム反転
				preprocessing_function=self.preprocess_input	
		)	

		train_generator = train_datagen.flow_from_directory(
				self.train_dir,
				target_size=input_shape[:2],
				classes=classes,
				batch_size=self.batch_size
		)

		#  検証データ
		test_datagen = ImageDataGenerator(
				rescale=1/255.,
				preprocessing_function=self.preprocess_input	
		)	

		validation_generator = test_datagen.flow_from_directory(
				self.validation_dir,
				target_size=input_shape[:2],
				classes=classes,
				batch_size=self.batch_size
		)

		#  steps_per_epoch, validation_stepsの算出
		steps_per_epoch = math.ceil( train_generator.samples / self.batch_size )
		validation_steps = math.ceil( validation_generator.samples / self.batch_size )
	
		#  callbacksの設定
		csv_logger = CSVLogger("./training.log")
#		early_stop = EarlyStopping( monitor="val_loss", mode="auto" )
		Path("./logs").mkdir(parents=True, exist_ok=True)
		tensor_board = TensorBoard( "./logs",
					    histogram_freq=0,
					    write_graph=True,
					    write_images=True )
		Path("./model").mkdir(parents=True, exist_ok=True)	
		check_point = ModelCheckpoint( filepath='./model/model.{epoch:02d}-{val_loss:.4f}.hdf5',
					       monitor="val_loss",
					       save_best_only=True,
#					       save_weights_only=True,
					       mode="auto" )
						    
		cb = [ csv_logger, tensor_board, check_point ]

		#  学習
		hist = self.model.fit_generator( 
				  	train_generator,
		 	  	  	steps_per_epoch=steps_per_epoch,
		 	  	  	epochs=self.epochs,
				  	validation_data=validation_generator,
				  	validation_steps=validation_steps,				  
				  	callbacks=cb
		)
		
		return hist

	# keras.applications.imagenet_utilsのxは4Dテンソルなので
	# 3Dテンソル版を作成
	def preprocess_input(self,x):
	    """Preprocesses a tensor encoding a batch of images.
	    # Arguments
	        x: input Numpy tensor, 3D.
	    # Returns
	        Preprocessed tensor.
	    """
	    # 'RGB'->'BGR'
	    x = x[:, :, ::-1]
	    # Zero-center by mean pixel
	    x[:, :, 0] -= 103.939
	    x[:, :, 1] -= 116.779
	    x[:, :, 2] -= 123.68
	    return x
		
	"""
		損失,精度のグラフ描画
	"""
	def plot_history( self, hist ):
		#print(history.history.keys())
		
		#  損失の経過をプロット
		plt.figure()
		loss = hist.history['loss']
		val_loss = hist.history['val_loss']
		plt.plot( range(self.epochs), loss, marker='.', label='loss' )
		plt.plot( range(self.epochs), val_loss, marker='.', label='val_loss' )
		plt.legend( loc='best', fontsize=10 )
		plt.xlabel('epoch')
		plt.ylabel('loss')
		plt.title('model loss')
		plt.legend( ['loss','val_loss'], loc='upper right')
		plt.savefig( "./model_loss.png" )
	#	plt.show()
	
		#  精度の経過をプロット
		plt.figure()
		acc = hist.history['acc']
		val_acc = hist.history['val_acc']
		plt.plot( range(self.epochs), acc, marker='.', label='acc' )
		plt.plot( range(self.epochs), val_acc, marker='.', label='val_acc' )
		plt.legend( loc='best', fontsize=10 )
		plt.xlabel('epoch')
		plt.ylabel('acc')
		plt.title('model accuracy')
		plt.legend( ['acc','val_acc'], loc='lower right')
		plt.savefig( "./model_accuracy.png" )
	#	plt.show()

main.py

import argparse
import pandas as pd

"""
	メイン処理
"""
if __name__ == '__main__':
	#  parameter
	parser = argparse.ArgumentParser(description="17flowers Fine Tuning ")
	parser.add_argument("--train_dir", default= "./train_images/")
	parser.add_argument("--validation_dir", default="./test_images/")
	parser.add_argument("--batch_size", "-b", type=int, default=32,
						help="Number of images in each mini-batch")
	parser.add_argument("--epochs", "-e", type=int, default=100,
						help="Number of sweeps over the dataset to train")
	parser.add_argument("--img_size", "-s", type=int, default=224,
						help="Number of images size")
	args = parser.parse_args()
	
	#  モデル
	md = VGG16_Transfer(args)
	#  学習
	print("Training Start------------------")
	hist = md.training()
	
	#  グラフの表示
	md.plot_history( hist )

まとめ

無事に参考記事通りの結果が出てとてもうれしいです。
転移学習とかやったら簡単にできると思ってましたが、やってみると難しかったです。


今回でモデルは無事に完成したので、ポケモンの方もclassesを追加して実験していきます。
また、今度Inception-v3でも転移学習やってみようと思います。