ファインチューニングを使ったCNNの実装

#はじめに
本記事では，ファインチューニングを使ったCNNのコード（train）を掲載し，簡単な解説をします．
仮想環境の構築やライブラリのインストール方法に関しては割愛しますので，ご了承ください．

#本記事を書いた経緯
人工知能がTVや各種メディアに取り上げられることからも，AIがより身近なものになってきています．
大学の研究でAI(画像認識)をすることになる学生もますます増えていることかと思います．しかしながら，周りに頼れる人や先輩がおらず，苦労する大学生や大学院生が発生しているのではないかと考え，本記事を書きました．
そういった方々（もちろんそうでない方々も）の助けになれば幸いです．

#ファインチューニングについて
ファインチューニングとは，すでに学習され，重みが与えられている既存のモデルをベースにして新たなモデルを構築することで，学習に使用するデータ数が少なくても，適切な学習が行えるというものです．CNNの場合，通常，学習に使用するデータ数は数千，数万のデータが必要であるといわれています．一方，ファインチューニングをすることで学習に使用するデータ数を減らすことができ，数百個のデータで学習を行っている研究もあります．

#開発環境
- Windows10
- Python 3.7.7
- TensorFlow 1.14.0
- keras 2.3.1
- numpy 1.16.4
- matplotlib 3.1.3
- hdf5 1.8.20

Anacondaでroot環境をコピーして，tensorflow,kerasあたりを入れて作りました．
root環境だとnumpyのverが1.17とかなので，バージョンをダウンさせてます．
一応TensorFlowとKeras両方を入れています．
※仮想環境の構築は以下の記事が参考になるかと
https://qiita.com/ozaki_physics/items/13466d6d1954a0afeb3b

#CNNのコード
~~~python
from PIL import Image
import os, glob
import numpy as np
import random, math

#画像が保存されているルートディレクトリのパス
#root_dir = "パス"
root_dir = "./image"
#分類する種別ごとにフォルダーを分ける。モデルの最終出力もここで設定したフォルダーの数に合わせる
categories = ["dog","rabbit","marmot"]

# 画像データ用配列
X = []
# ラベルデータ用配列
Y = []

#画像データごとにadd_sample()を呼び出し、X,Yの配列を返す関数
def make_sample(files):
    global X, Y
    X = []
    Y = []
    for cat, fname in files:
        add_sample(cat, fname)
    return np.array(X), np.array(Y)

#渡された画像データを読み込んでXに格納し、また、
#画像データに対応するcategoriesのidxをY格納する関数
def add_sample(cat, fname):
    img = Image.open(fname)
    img = img.convert("RGB")
    img = img.resize((224, 224))
    data = np.asarray(img)
    X.append(data)
    Y.append(cat)

#全データ格納用配列
allfiles = []

#カテゴリ配列の各値と、それに対応するidxを認識し、全データをallfilesにまとめる
for idx, cat in enumerate(categories):
    image_dir = root_dir + "/" + cat
    files = glob.glob(image_dir + "/*.jpg")
    for f in files:
        allfiles.append((idx, f))

#シャッフル後、学習データと検証データに分ける
random.shuffle(allfiles)
th = math.floor(len(allfiles) * 0.9)
train = allfiles[0:th]
test  = allfiles[th:]
X_train, y_train = make_sample(train)
X_test, y_test = make_sample(test)
xy = (X_train, X_test, y_train, y_test)

np.save("image_data", xy)

#モデルの構築
from keras import layers, models
from keras.applications import VGG16

#転移元のモデルとしてVGG16を使用
conv_base = VGG16(weights='imagenet',
                     include_top=False,
                     input_shape=(224, 224, 3))

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
#model.add(layers.Dense(3,activation="sigmoid")) #分類先の種類分設定,分類数に応じて数字変更
model.add(layers.Dense(3, activation='softmax'))

#モデル構成の確認
model.summary()

#モデルのコンパイル
from keras import optimizers
#model.compile(loss="binary_crossentropy",バイナリは二値分類用らしい
model.compile(loss="categorical_crossentropy",
              optimizer=optimizers.RMSprop(lr=1e-4),
              metrics=["acc"])

#データの準備
from keras.utils import np_utils
import numpy as np

categories = ["dog","rabbit","marmot"]
nb_classes = len(categories)

#X_train, X_test, y_train, y_test = np.load("保存した学習データ・テストデータのパス")
X_train, X_test, y_train, y_test = np.load("image_data.npy",allow_pickle=True)

#データの正規化
X_train = X_train.astype("float") / 255
X_test  = X_test.astype("float")  / 255

#kerasで扱えるようにcategoriesをベクトルに変換
y_train = np_utils.to_categorical(y_train, nb_classes)
y_test  = np_utils.to_categorical(y_test, nb_classes)

#モデルの学習

model = model.fit(X_train,
                  y_train,
                  epochs=50,
                  batch_size=16,
                  validation_data=(X_test,y_test))

#学習結果を表示

import matplotlib.pyplot as plt

acc = model.history['acc']
val_acc = model.history['val_acc']
loss = model.history['loss']
val_loss = model.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
#plt.savefig('精度を示すグラフのファイル名')
plt.savefig('cnn_accuracy')

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
#plt.savefig('損失値を示すグラフのファイル名')
plt.savefig('cnn_loss')

#モデルの保存

json_string = model.model.to_json()

open('animal_model.json', 'w').write(json_string)

#重みの保存
hdf5_file = "animal_weight.hdf5"
model.model.save_weights(hdf5_file)

print('Finish!')
~~~

#コードの解説
ちょっと長くなりますが，上から順に簡単な説明をしていきたいと思います．
コードがわかっている方は読み飛ばしてしまって全く問題ないです！

~~~python
from PIL import Image
import os, glob
import numpy as np
import random, math

#画像が保存されているルートディレクトリのパス
#root_dir = "パス"
root_dir = "./image"
#分類する種別ごとにフォルダーを分ける。モデルの最終出力もここで設定したフォルダーの数に合わせる
categories = ["dog","rabbit","marmot"]
~~~
importでは，本コードにて使用するライブラリーの指定をしています．できるだけimportはまとめた方が見やすいです（このコードではまとめてません）．
root_dirは画像を読み込む場所を指定しています．categoriesは分類するものを指定してます．ここではdogとrabbitとmarmotの画像を分類するようにしてます．

~~~python
# 画像データ用配列
X = []
# ラベルデータ用配列
Y = []

#画像データごとにadd_sample()を呼び出し、X,Yの配列を返す関数
def make_sample(files):
    global X, Y
    X = []
    Y = []
    for cat, fname in files:
        add_sample(cat, fname)
    return np.array(X), np.array(Y)

#渡された画像データを読み込んでXに格納し、また、
#画像データに対応するcategoriesのidxをY格納する関数
def add_sample(cat, fname):
    img = Image.open(fname)
    img = img.convert("RGB")
    img = img.resize((224, 224))
    data = np.asarray(img)
    X.append(data)
    Y.append(cat)

#全データ格納用配列
allfiles = []

#カテゴリ配列の各値と、それに対応するidxを認識し、全データをallfilesにまとめる
for idx, cat in enumerate(categories):
    image_dir = root_dir + "/" + cat
    files = glob.glob(image_dir + "/*.jpg")
    for f in files:
        allfiles.append((idx, f))

#シャッフル後、学習データと検証データに分ける
random.shuffle(allfiles)
th = math.floor(len(allfiles) * 0.9)
train = allfiles[0:th]
test  = allfiles[th:]
X_train, y_train = make_sample(train)
X_test, y_test = make_sample(test)
xy = (X_train, X_test, y_train, y_test)

np.save("image_data", xy)
~~~
ここの処理については，簡単に言うと，画像データをXに格納し，教師値をYに格納，そのあと学習データと検証データに分けてます．
教師値の与え方は各フォルダーごとに与えています．dogは[1,0,0]，rabbitは[0,1,0]，marmotは[0,0,1]といった具合です．同じフォルダーに入れた画像には，同じ教師値が与えられます．フォルダーの場所等に関しては後述の”使い方”で説明します．
学習データと検証データの比は9:1です．コード内の数値"0.9"を変えることで，好きな比にできます．
その後，image_dataという名前で保存しています．このファイルは後で読み込みます．

~~~python
#モデルの構築
from keras import layers, models
from keras.applications import VGG16

#転移元のモデルとしてVGG16を使用
conv_base = VGG16(weights='imagenet',
                     include_top=False,
                     input_shape=(224, 224, 3))

model = models.Sequential()
model.add(conv_base)
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
#model.add(layers.Dense(3,activation="sigmoid")) #分類先の種類分設定,分類数に応じて数字変更
model.add(layers.Dense(3, activation='softmax'))

#モデル構成の確認
model.summary()

#モデルのコンパイル
from keras import optimizers
#model.compile(loss="binary_crossentropy",バイナリは二値分類用らしい
model.compile(loss="categorical_crossentropy",
              optimizer=optimizers.RMSprop(lr=1e-4),
              metrics=["acc"])
~~~
続いてモデルの構築＋コンパイルです．
今回はImageNetで学習をしたVGG16を既存モデルとして使用してます．
活性化関数がsoftmaxの層は出力層です．出力する数は分類の個数と同じにする必要があります．
今回はdog, rabbit, marmotの3分類なので3です．

モデルのコンパイルでは損失関数などを指定します．今回は3分類なので損失関数はcategolical…です．
最適化関数は適当です．

~~~python
#データの準備
from keras.utils import np_utils
import numpy as np

categories = ["dog","rabbit","marmot"]
nb_classes = len(categories)

#X_train, X_test, y_train, y_test = np.load("保存した学習データ・テストデータのパス")
X_train, X_test, y_train, y_test = np.load("image_data.npy",allow_pickle=True)

#データの正規化
X_train = X_train.astype("float") / 255
X_test  = X_test.astype("float")  / 255

#kerasで扱えるようにcategoriesをベクトルに変換
y_train = np_utils.to_categorical(y_train, nb_classes)
y_test  = np_utils.to_categorical(y_test, nb_classes)

#モデルの学習

model = model.fit(X_train,
                  y_train,
                  epochs=50,
                  batch_size=16,
                  validation_data=(X_test,y_test))
~~~
データの準備では，さっき保存したimage_dataを読み込みます．
その後model.fitでモデルの学習を行います．エポック数やバッチサイズは変更可能です．
学習しているときは，その経過が表示されます（〇〇%まで学習中みたいな）．

~~~python
#学習結果を表示

import matplotlib.pyplot as plt

acc = model.history['acc']
val_acc = model.history['val_acc']
loss = model.history['loss']
val_loss = model.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Training acc')
plt.plot(epochs, val_acc, 'b', label='Validation acc')
plt.title('Training and validation accuracy')
plt.legend()
#plt.savefig('精度を示すグラフのファイル名')
plt.savefig('cnn_accuracy')

plt.figure()

plt.plot(epochs, loss, 'bo', label='Training loss')
plt.plot(epochs, val_loss, 'b', label='Validation loss')
plt.title('Training and validation loss')
plt.legend()
#plt.savefig('損失値を示すグラフのファイル名')
plt.savefig('cnn_loss')

#モデルの保存

json_string = model.model.to_json()

open('animal_model.json', 'w').write(json_string)

#重みの保存
hdf5_file = "animal_weight.hdf5"
model.model.save_weights(hdf5_file)

print('Finish!')
~~~
最後に正解率の変動がcnn_accuracyというファイル名，損失関数の値の変動がcnn_lossというファイル名で保存され，表示されます．
不要なら消してしまっても構いません．
学習が完了したモデルがanimal_model.json，重みがanimal_weight.hdf5というファイル名で保存されます．
こちらはpredictする際に読み込むことになるかと思います．

#使い方
下の図のようにファイルを配置すればオーケーです．
<img width="600" alt="mae - コピー.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1177046/4e3ce134-3bd4-0961-fec1-36e796791fd7.png">

本記事に示したコードを入れたファイルと同じフォルダーに"image"という名前のフォルダーを置き，その中にdog, rabbit, marmotのフォルダーを入れてさらにその中に画像を入れるといった具合です．

学習を終えると以下の図のようになります．

<img width="600" alt="ato - コピー.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1177046/53790240-9fc0-a05f-e4e2-23d5c13bfa6a.png">

コードのファイルがあるフォルダー内に，image_dataやれweightやれいろいろなファイルが保存されます．
ファイルがこういう配置になっているのは，ファイルの保存・読み込む場所のパスをほとんど指定してないからです（学習に使う画像のみファイルを指定してます）．理由は私がパスの書き方について"./"ぐらいしかわかってないからです．
普通はこんなにごちゃごちゃにはならないです！

#おわりに
CNNでファインチューニングするコードについて，書かせていただきました．
少しでも参考になったのなら幸いです．
気が向いたらpredictのコードも書きたいと思います．
といっても，predictはモデルと重みと画像を読み込むだけですが…

#参考にしたもの
画像認識で「綾鷹を選ばせる」AIを作る
https://qiita.com/tomo_20180402/items/e8c55bdca648f4877188
データの読み込みなどコードのほとんどの部分において，この記事を参考にしました．

Keras documentation
https://keras.io/ja/
kerasの公式(?)サイトです．大変お世話になっております．