Tensorflow+Kerasの環境構築を前回やってみて、無事環境構築に成功しました。
そのときはMNISTデータセットで正常な実行を確認しましたが、実用的な面を考えると、自分で学習画像を用意して訓練するというケースが多くなると思います。
そこで、予めフォルダに分類した学習画像からクラス名(及びクラス数)を自動で読み取り、各クラスの分類学習を行うソースコードを書いてみました。
また、前回からの差分として、分類のモデルはAlexNet(畳み込み層5層、全結合層3層モデル)を用いることとしました。
AlexNetのモデル部分のコードに関しては、
参考ページ:KerasによるAlexNetを用いた犬猫分類モデル – Qiita
上記のコードを参考にさせていただきました。ありがとうございます(厳密には上記のコードはLRNがBN((Batch Normalization)に変わっているので、AlexNetの論文実装とは異なっています)。
今回の環境
・OS : Windows10(64bit)
・GPU: GeForce GTX 950
・Anaconda
・CUDA 9.0
・cuDNN v7.0.5
・Tensorflow 1.11.0
・Keras 2.2.4
上記全て環境構築済
ソースコード
先にソースコードを載せちゃいます。説明は先を読んでください。
import numpy as np | |
import keras | |
from keras.utils import np_utils | |
from keras.models import Sequential | |
from keras.layers.convolutional import Conv2D, MaxPooling2D | |
from keras.layers.core import Dense, Dropout, Activation, Flatten | |
from sklearn.model_selection import train_test_split | |
from PIL import Image | |
import glob | |
from keras.preprocessing.image import load_img, img_to_array | |
from keras.initializers import TruncatedNormal, Constant | |
from keras.layers import Input, Dropout, Flatten, Conv2D, MaxPooling2D, Dense, Activation, BatchNormalization | |
from keras.optimizers import SGD | |
# ------------------------------------------------------------------------------------- | |
# 初期設定部 | |
# ------------------------------------------------------------------------------------- | |
# GrayScaleのときに1、COLORのときに3にする | |
COLOR_CHANNEL = 3 | |
# 入力画像サイズ(画像サイズは正方形とする) | |
INPUT_IMAGE_SIZE = 224 | |
# 訓練時のバッチサイズとエポック数 | |
BATCH_SIZE = 32 | |
EPOCH_NUM = 100 | |
# 使用する訓練画像の入ったフォルダ(ルート) | |
TRAIN_PATH = "..\training_dataset\training_anime1" | |
# 使用する訓練画像の各クラスのフォルダ名 | |
folder = ["000_hatsune_miku", "001_kinomoto_sakura", "002_suzumiya_haruhi", | |
"003_fate_testarossa", "004_takamachi_nanoha", "005_lelouch_lamperouge", | |
"006_akiyama_mio", "007_nagato_yuki", "008_shana", "009_hakurei_reimu"] | |
# CLASS数を取得する | |
CLASS_NUM = len(folder) | |
print("クラス数 : " + str(CLASS_NUM)) | |
# ------------------------------------------------------------------------------------- | |
# 訓練画像入力部 | |
# ------------------------------------------------------------------------------------- | |
# 各フォルダの画像を読み込む | |
v_image = [] | |
v_label = [] | |
for index, name in enumerate(folder): | |
dir = TRAIN_PATH + "\" + name | |
files = glob.glob(dir + "\*.png") | |
print(dir) | |
for i, file in enumerate(files): | |
if COLOR_CHANNEL == 1: | |
img = load_img(file, color_mode = "grayscale", target_size=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)) | |
elif COLOR_CHANNEL == 3: | |
img = load_img(file, color_mode = "rgb", target_size=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE)) | |
array = img_to_array(img) | |
v_image.append(array) | |
v_label.append(index) | |
v_image = np.array(v_image) | |
v_label = np.array(v_label) | |
# imageの画素値をint型からfloat型にする | |
v_image = v_image.astype('float32') | |
# 画素値を[0~255]⇒[0~1]とする | |
v_image = v_image / 255.0 | |
# 正解ラベルの形式を変換 | |
v_label = np_utils.to_categorical(v_label, CLASS_NUM) | |
# 学習用データと検証用データに分割する | |
train_images, valid_images, train_labels, valid_labels = train_test_split(v_image, v_label, test_size=0.20) | |
# ------------------------------------------------------------------------------------- | |
# モデルアーキテクチャ定義部 | |
# ------------------------------------------------------------------------------------- | |
def conv2d(filters, kernel_size, strides=1, bias_init=1, **kwargs): | |
trunc = TruncatedNormal(mean=0.0, stddev=0.01) | |
cnst = Constant(value=bias_init) | |
return Conv2D( | |
filters, | |
kernel_size, | |
strides=strides, | |
padding='same', | |
activation='relu', | |
kernel_initializer=trunc, | |
bias_initializer=cnst, | |
**kwargs | |
) | |
def dense(units, **kwargs): | |
trunc = TruncatedNormal(mean=0.0, stddev=0.01) | |
cnst = Constant(value=1) | |
return Dense( | |
units, | |
activation='tanh', | |
kernel_initializer=trunc, | |
bias_initializer=cnst, | |
**kwargs | |
) | |
def AlexNet(): | |
model = Sequential() | |
# 第1畳み込み層 | |
model.add(conv2d(96, 11, strides=(4,4), bias_init=0, input_shape=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, COLOR_CHANNEL))) | |
model.add(MaxPooling2D(pool_size=(3, 3), strides=(2, 2))) | |
model.add(BatchNormalization()) | |
# 第2畳み込み層 | |
model.add(conv2d(256, 5, bias_init=1)) | |
model.add(MaxPooling2D(pool_size=(3, 3), strides=(2, 2))) | |
model.add(BatchNormalization()) | |
# 第3~5畳み込み層 | |
model.add(conv2d(384, 3, bias_init=0)) | |
model.add(conv2d(384, 3, bias_init=1)) | |
model.add(conv2d(256, 3, bias_init=1)) | |
model.add(MaxPooling2D(pool_size=(3, 3), strides=(2, 2))) | |
model.add(BatchNormalization()) | |
# 全結合層 | |
model.add(Flatten()) | |
model.add(dense(4096)) | |
model.add(Dropout(0.5)) | |
model.add(dense(4096)) | |
model.add(Dropout(0.5)) | |
# 出力層 | |
model.add(Dense(CLASS_NUM, activation='softmax')) | |
model.compile(optimizer=SGD(lr=0.01), loss='categorical_crossentropy', metrics=['accuracy']) | |
return model | |
# コンパイル | |
model = AlexNet() | |
# 訓練 | |
history = model.fit(train_images, train_labels, batch_size=BATCH_SIZE, epochs=EPOCH_NUM) | |
# ------------------------------------------------------------------------------------- | |
# 訓練実行&結果確認部 | |
# ------------------------------------------------------------------------------------- | |
# モデル構成の確認 | |
model.summary() | |
score = model.evaluate(valid_images, valid_labels, verbose=0) | |
print(len(valid_images)) | |
print('Loss:', score[0]) | |
print('Accuracy:', score[1]) | |
使用するデータセットと準備
今回は自作画像……といいつつ、自作画像を用意するのは面倒なので、JPGやPNGなどの一般的な画像形式で提供されている公開データセットを使うことにしました。
ただ、今回のソースコードに関しては、自分で学習画像を収集して、フォルダに分類し、そのフォルダ名をソースコード内に入れるだけで使っていただけるような構成になっています。
では、今回のソースコードを動かすためのデータセットの準備方法を説明します。
今回、「animeface-character-dataset」というアニメキャラの顔画像のデータセットを使用しています。以下のページからダウンロード可能です。
外部サイト:animeface-character-dataset
ダウンロードしたZIPファイルを解凍すると、以下のように各キャラクター別に分類された画像が入ったフォルダ群が展開されます。
多数のキャラクターがあるのですが、学習にあまり時間を掛けたくないという事情から、10キャラ分のフォルダだけを、訓練データ配置用のフォルダへ移動します。
訓練データ配置用のフォルダに関してはソースコードの方から変更可能ですが、私は実行ファイルからの相対パスで見て「..\training_dataset\training_anime1」のフォルダに画像を置きました。
あとは、ソースコードの29-35行目に訓練データ配置フォルダへのパスと、各クラスごとの画像が入ったフォルダ名を指定して、認識を行うことができます。
# 使用する訓練画像の入ったフォルダ(ルート)
TRAIN_PATH = "..\\training_dataset\\training_anime1"
# 使用する訓練画像の各クラスのフォルダ名
folder = ["000_hatsune_miku", "001_kinomoto_sakura", "002_suzumiya_haruhi",
"003_fate_testarossa", "004_takamachi_nanoha", "005_lelouch_lamperouge",
"006_akiyama_mio", "007_nagato_yuki", "008_shana", "009_hakurei_reimu"]
コードの説明
一部コードをかいつまんで説明します。
カラー画像とグレースケール画像への対応
今回は最初の方でCOLOR_CHANNELという変数を定義し、これを1のときは画像をグレースケールとしてネットワークへ入力、3のときはカラー画像としてネットワークに入力できるようにしました。
# GrayScaleのときに1、COLORのときに3にする
COLOR_CHANNEL = 3
以下の部分のコードにて、 グレースケール入力とカラー入力を自動で切り替えるようにしています。入力が元々グレースケールだとカラーで入れても意味がない気がしますが、元々カラー画像で用意されている場合は、グレースケールに変換して入力してくれます。
if COLOR_CHANNEL == 1:
img = load_img(file, color_mode = "grayscale", target_size=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE))
elif COLOR_CHANNEL == 3:
img = load_img(file, color_mode = "rgb", target_size=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE))
また、ネットワークの入力の部分にも、COLOR_CHANNELを変数として与えて、グレースケールとカラーの場合で入力の形が自動で変わるようにしています。
# 第1畳み込み層
model.add(conv2d(96, 11, strides=(4,4), bias_init=0, input_shape=(INPUT_IMAGE_SIZE, INPUT_IMAGE_SIZE, COLOR_CHANNEL)))
分類クラス数の自動取得
今回は10人のキャラクターの顔を分類するので、当然クラスは10になります。
フォルダごとに分類した学習画像を用意すれば、クラス数はフォルダ数と同一になるため、以下のようにフォルダ数から自動でクラス数をカウントし、
# CLASS数を取得する
CLASS_NUM = len(folder)
print("クラス数 : " + str(CLASS_NUM))
ネットワークの出口のところの数はクラス数に合わせないとエラーが起こります。
# 出力層
model.add(Dense(CLASS_NUM, activation='softmax'))
実行結果の確認
結構画像数の少ないデータセットなので、少々実験としては心許ない部分がありますが、実際に実行をしてみました。
今回はカラーチャンネルを変更し、グレースケール化した訓練画像で認識する場合と、カラー化した訓練画像で認識する場合を比較してみましょう。
エポックは100、バッチサイズは32で統一しました。
COLOR_CHANNEL = 1 の結果
COLOR_CHANNEL = 1でプログラムを回したところ、以下のような結果が出ました。
上の190は検証に用いた画像の枚数で、Lossは損失を示し、低ければ低いほど良い結果であることを示します。
そして、肝心の精度は81.6%というところでした。
今回は10クラス分類なので、ランダムで正解当てをした場合は精度は10%ですが、81.6%とモデルの訓練が成功していることがわかります。
また、以下がsummary()関数で表示したモデルの構成です。
COLOR_CHANNEL = 3 の結果
次に、COLOR_CHANNELを3に変えて実験をしました。
結果は以下となりました。
精度は88.4%と、グレースケールの場合よりも精度が高まりました。
これは、アニメのキャラクターの分類には髪の色などの色情報が非常に重要な情報であり、色があることで認識率が上がりそう……という直感的な予想にも見合う結果となりました。
まとめ
今回は自作画像から学習を行う準備を整えると共に、実際にモデルを作って精度を確認してみました。
今回はAlexNetを扱いましたが、他のネットワーク等も試してみようと思います。