【Deeplearningでポケモン図鑑計画_その3】ポケモン×Fine Tuning(1)

はじめに

こんにちは、がんがんです。今回はついに転移学習を行っていきます。
一気に151種類行うのは大変だったので、まずはフシギダネからライチュウまでの26種類で実験を行いました。今回はそのまとめです。

前回記事はこちらです。
gangannikki.hatenadiary.jp


転移学習とは

大規模な画像データセットを用いて学習したモデルを利用して新しいモデルを構築していく手法です。
私はkerasを使用しており、kerasには多くの事前学習済みモデルが用意されています。
詳しい種類については、以下の日本語用ドキュメントを参照ください。

Applications - Keras Documentation


まずは、転移学習とFine Tuningについてどう違うのか気になったので調べてみました。
すると、こちらの方がまとめられていました。

qiita.com

こちらから引用させていただくと、転移学習とFine Tuningはこのような違いがあるようです。

  • 転移学習:既存の学習済モデル(出力層以外の部分)を、重みデータは変更せずに特徴量抽出機として利用する。
  • ファインチューニング:既存の学習済モデル(出力層以外の部分)を、重みデータを一部再学習して特徴量抽出機として利用する。

ということみたいです。

今回の場合はimagenetの重みをそのまま使用するため、厳密には転移学習にあたるようです。

参考記事

以下に参考記事をまとめておきます。上記にあるkerasのドキュメントは非常に参考になりますので、そちらも参考にしてみて下さい。
今回は主にkeras関連の記事を載せていますが、Chainer、PyTorchでも多くの記事があるので自身に合うものを調べてみてください。

aidiary.hatenablog.com

aidiary.hatenablog.com

elix-tech.github.io

実験

実験1

まずは現在作っているモデルにおいて実験を行いました。すると、驚くほどに精度が低くなりました。

検証結果を保存し忘れてしまいましたが、正解のクラスはe-6くらいになり、それ以外はe-10くらいなので一応分類はできている(?)ようです。
それにしても精度が驚くべき低さです。


モデルが間違っているのか、それとも学習データの質が悪いのかを調べることにしました。
まずはモデルについて検証しました。検証についてはこちらの記事にまとめています。

gangannikki.hatenadiary.jp

検証の結果、モデルがきちんと完成していなかったという結論に至りました。無事に検証が上手くいってよかったです。

実験2

実験2では、ベンチマークを用いた検証により修正したプログラムを用いて再度実験を行います。実験の結果は以下に示します。

f:id:gangannikki:20181103214419p:plainf:id:gangannikki:20181103214416p:plain
Accuracy Loss

val_lossval_accは50epochを過ぎたあたりからほぼ変化してません。実際の予測結果はこちらです。

f:id:gangannikki:20181103214312p:plain
フシギダネ
f:id:gangannikki:20181103214307p:plain
トランセル
f:id:gangannikki:20181103214318p:plain
ゼニガメ

フシギダネトランセルはしっかりと結果が出ています。ゼニガメも低いですがまあ精度が出てるのではと思います。
ちょっと予測精度が気になったので26種類すべてのポケモンで試してみました。以下は失敗してた結果です。

f:id:gangannikki:20181104214339p:plain
リザードン
f:id:gangannikki:20181104214823p:plain
ポッポ

すごいぐらいカメックスが認識されています。26種類中15種類しかきちんと認識されていませんでした…

名前 確率
フシギダネ 99%
ゼニガメ 69%
カメール 99%
カメックス 98%
キャタピー 99%
トランセル 98%
バタフリー 99%
ビードル 98%
ピジョン 87%
ピジョット 59%
コラッタ 80%
ラッタ 88%
オニドリル 96%
アーボ 99%
アーボック 99%

コード

使用言語はPython 3.5です。深層学習用のフレームワークはKeras 2.1.5(backendはTensorFlow)で使用しています。OSはUbuntu 16.04 LTSです。

Fine_Tuning_vgg16.py

#------------------------------------------------------------
#
#	Fine_Tuning_vgg16.py
#		転移学習を行うプログラム(VGG16)
#
#------------------------------------------------------------

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
#from keras.backend import

#  SGDのパラメータ
lr = 1e-4					
momentum = 0.9
decay = 1e-9
nesterov = False

"""
	モデル(VGG16の転移学習)
"""
class VGG16_Transfer(object):
	def __init__(self, args, classes):
		self.train_dir = args.train_dir
		self.validation_dir = args.validation_dir	
		self.batch_size = args.batch_size
		self.epochs = args.epochs
		self.input_shape = ( args.img_size, args.img_size, 3 )
		self.classes = classes

		#  VGG16の読み込み
		vgg16 = VGG16(include_top=False, weights='imagenet', input_shape=self.input_shape)
		#vgg16.summary()
	
		#  モデル定義
		self.model = self.build_transfer_model(vgg16)
		#  モデル構築
		self.model.compile( optimizer=SGD(lr=lr, momentum=momentum, nesterov=nesterov),
						  loss='categorical_crossentropy',
					   	  metrics=["accuracy"] )

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

		print(self.classes)
	"""
		モデル構築
	"""
	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(self.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=self.input_shape[:2],
				classes=self.classes
#				batch_size=self.batch_size
		)

		#  検証データ
		test_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	
		)	

		validation_generator = test_datagen.flow_from_directory(
				self.validation_dir,
				target_size=self.input_shape[:2],
				classes=self.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

#------------------------------------------------------------
#
#	main.py
#		転移学習を行うプログラム
#
#------------------------------------------------------------

import argparse
import pandas as pd
from pathlib import Path
from Fine_Tuning_vgg16 import VGG16_Transfer

"""
	メイン処理
"""
if __name__ == '__main__':
	#  parameter
	parser = argparse.ArgumentParser(description="Pokemon of Keras")
	parser.add_argument("--train_dir", default= "./image/train/")
	parser.add_argument("--validation_dir", default="./image/validation/")
	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()
	
	#  csvファイルを読み込み,図鑑を取り出す
	csv_path = Path(".") / "zukan" / "RG.csv"
	df = pd.read_csv(csv_path, engine="python", encoding='utf-8',
					 header=0)
	
	#  クラスを設定
	classes = list(df["name"].iloc[:26])
	
#	for i in range(len(classes)):
#		print(classes[i])
	print(classes)

	#  モデル
	md = VGG16_Transfer(args,classes)
	#  学習
	print("Training Start------------------")
	hist = md.training()
	
	#  グラフの表示
	md.plot_history( hist )

Predict.py

#------------------------------------------------------------
#
#	テスト
#
#------------------------------------------------------------
import sys
import numpy as np
import pandas as pd
from PIL import Image
from pathlib import Path
from keras.models import load_model	
from keras.preprocessing.image import load_img, img_to_array, array_to_img
from keras.applications.vgg16 import preprocess_input, decode_predictions

"""
	テスト
"""
if __name__ == '__main__':
	#  コマンドラインより入力(後にparamに変える)
	if len(sys.argv) <= 1:
		quit()				#  画像がないと終了
	
	"""
		画像の処理
	"""
	img_path = Path("./zukan_image/") / sys.argv[1]
#	img_path = Path(".") / sys.argv[1] / sys.argv[2]

	#  画像をarrayとして読み込む
	img = img_to_array( load_img(img_path, target_size=( 224, 224)) )
#	print(img)
	img_ori = array_to_img( img )
	img_ori.save( "./image/test/test_before.png" )	#  PILで保存

	#  3次元テンソルを4次元テンソルに変換
	x = np.expand_dims( img, axis=0 )
	x = x / 255.
	
	"""
#		モデル, 重みの読み込み
	"""
	#  model_pathの一番最後が最良モデル
	model_path = sorted(list(Path("./model/").glob("*.hdf5")))
	num = len(model_path) - 1
	model = load_model( model_path[num] )
	model.load_weights( model_path[num] )

	print("-------------------------------------------------")
	print("load image:{}".format(img_path))
	print("model_path:{}".format(model_path[num]))
	print("-------------------------------------------------")

	"""
#		予測
	"""
	print("Predicting Start------------------")
	pred = model.predict(x)[0]

	#  csvファイルを読み込み,図鑑を取り出す
	csv_path = Path(".") / "zukan" / "RG.csv"
	df = pd.read_csv(csv_path, engine="python", encoding='utf-8',
					 header=0)	
	#  クラスを設定
	classes = list(df["name"].iloc[:26])

	#  予測確率が高いトップ5を出力
	top = 5
	top_indices = pred.argsort()[-top:][::-1]
	result = [(classes[i], pred[i]) for i in top_indices]
	for x in result:
	    print(x)

まとめ

今回は26種類のポケモン分類を行う転移学習モデルを作成していきました。15/26種類ではありますが、きちんと分類できたようでよかったです。
今回改善できなかったエラーは学習率を変更するなどして改善していきます。pngとjpgが混じっているのが問題かな?

徐々に種類数を増やしていき、最終的には151種類の分類可能モデルを構築していきます。
また、GitHubについても登録したので徐々にプログラムを上げていきます。なかなか難しいですが慣れていきます。

いろいろ検索していたときに以下を見つけました。Androidにて図鑑の実装を試みている人の記事です。
同じように考える人がいるのはうれしいですね。
qiita.com