転移学習を使ったCNNモデルの作成



# はじめに
このブログはAidemy Premiumのカリキュラムの一環で、受講修了条件を満たすために公開しています。
Aidemy Premiumで学んだことをもとに、オリジナルのWebアプリを作成しました。
Webアプリの内容は、アップロードした画像がピザかピザではないかを判定するものです。今回この内容にした理由は主に2点あります。

1. kaggleのDataSetから容易にデータを取得できる
2. kaggleで該当のDataSetを実際に使い、画像認識を実装しているコードが多数ある

それでは私が実際に作成したアプリを紹介したいと思います。

# 目次
1. 実行環境
1. 画像収集
1. データ前処理
1. モデルの作成
1. 転移学習
1. Webアプリの実装
1. 考察
1. まとめ


# 1\.実行環境
ローカル環境に仮想環境を構築し、Visual Studio Codeでコーディング行いました。
以下はモデル作成に使用したライブラリです。

~~~
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Activation, Conv2D, Dense, Dropout, Flatten, Input, MaxPooling2D, BatchNormalization
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers
from glob import glob
~~~

# 2\.画像収集
kaggleのDataSetからダウンロードしました。
ピザの画像と、ピザではない画像がjpg拡張子ファイルとして収集できました。

# 3\.データ前処理
以下、データ前処理のコードを解説します。
まず、データのpathを格納するリスト、モデルに入力する形式へと処理したデータを格納するリストを宣言します。

~~~
pizza_datas_path = []
not_pizza_datas_path = []
pizza_datas = []
not_pizza_datas = []
~~~

globを使いデータのpath一覧を取得します。全ての画像が、jpg拡張子だったので「*.jpg」としています。sortedを使っているのはglobで取得したファイルが名前順になっていなかったためです。

~~~
pizza_datas_path = sorted(glob("pizza_not_pizza/pizza/*.jpg"))
not_pizza_datas_path = sorted(glob("pizza_not_pizza/not_pizza/*.jpg"))
~~~

pathを画像に変換し、モデルへ入力できるよう成形する関数を定義しています。
画像データのチャンネル数を3、サイズを50、50としてリストに格納します。
今回のモデルでは入力値をチャンネル数3、サイズ50、50とします。

~~~
def to_img(path, data_list):
    img = cv2.imread(path)
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (50,50))
    data_list.append(img)
~~~

先ほど定義した関数でリストに格納されたpathを画像へと変換、成形します。
画像データは900枚ほどありますが、今回は400枚づつ使います。

~~~
for path in pizza_datas_path[:400]:
    to_img(path, pizza_datas)
for path in not_pizza_datas_path[:400]:
    to_img(path, not_pizza_datas)
~~~

ピザとピザではないデータを合わせて、nbarray型に変換します。モデルへの入力はnbarray型と決まっているためです。
正解データはピザのデータであれば0、ピザでなければ1としてnbarray型にして作成します。汎用性をもてせるため、lenを使っていますが、今回は[0]*400でも問題ありません。

~~~
X = np.array(pizza_datas + not_pizza_datas)
y = np.array([0]*len(pizza_datas) + [1]*len(not_pizza_datas))
~~~

モデルの精度を向上させるためにリストの要素をランダムに並び替えます。
そして、X、yともに学習データと検証データに分けます。

~~~
rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]
X_train = X[:int(len(X)*0.8)]
y_train = y[:int(len(y)*0.8)]
X_test = X[int(len(X)*0.8):]
y_test = y[int(len(y)*0.8):]
~~~

正解データをone-hotエンコーディングします。
以上で、データ前処理を完了とします。

~~~
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)
~~~

# 5\.モデルの作成
CNNモデルを構築します。
畳み込み層、プーリング層、データを一次元にし、全結合層へ入力するという流れです。

~~~
model = Sequential()
model.add(Conv2D(input_shape=(50, 50, 3), filters=32,
                 kernel_size=(2, 2), strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Conv2D(filters=32, kernel_size=(
    2, 2), strides=(1, 1), padding="same"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Flatten())
model.add(Dense(256))
model.add(Activation('relu'))
model.add(Dense(128))
model.add(Activation('relu'))
model.add(Dense(2))
model.add(Activation('softmax'))
~~~

モデルをコンパイルし、データを学習させます。

~~~
model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])
model.fit(X_train, y_train, batch_size=100, epochs=20, validation_data=(X_test, y_test))
~~~

正解率は0.63でした。

~~~
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])
~~~

Test loss: 1.3389208316802979
Test accuracy: 0.637499988079071

# 6\.転移学習
転移学習を使い、再度モデルを作成し正解率を向上させたいと思います。
VGG16を利用します。
VGG16からの出力を一次元に変換し、全結合層へと渡します。
19層までがVGG16なので、重みを固定します。

~~~
input_tensor = Input(shape=(50, 50, 3))
vgg16 = VGG16(include_top=False, weights='imagenet', input_tensor=input_tensor)

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(2, activation='softmax'))
model2 = Model(vgg16.inputs, top_model(vgg16.output))

for layer in model2.layers[:19]:
    layer.trainable = False
~~~

正解率は0.8でした。
先ほどのモデルよりも向上しました。
転移学習を用いて作成したモデルをアプリに使用します。

~~~
model2.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])
model2.fit(X_train, y_train, batch_size=100, epochs=20, validation_data=(X_test, y_test))
scores = model2.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])
~~~
Test loss: 1.4948604106903076
Test accuracy: 0.800000011920929

~~~
result_dir = 'results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)
model2.save(os.path.join(result_dir, 'model.h5'))
~~~

# 7\.Webアプリの実装
Webアプリの実装はAidemyのFlask入門講座をもとにFlask、HTML、CSSを用いました。

# 8\.考察、反省
転移学習を用いることで正解率が向上しましたが、損失関数の値はわずかではありますが、悪化しました。
学習の経過を見ると、転移学習を使わなければ、正解率はエポック数が増えるにつれて上がりますが、検証の正解率はほぼ変化がありません。過学習を起こしているかと考えられます。
一方の転移学習を用いたモデルは、エポック数が増えるにつれて、正解率、検証の正解率も上昇しています。
以上から、転移学習を用いることで、精度が良いモデルを作成できることがわかりました。

# 9\.まとめ
今回受講したAidemy Premiumでは画像認識モデルの実装を学びました。
これから学んでいきたいことは、モデルの理論、AIアルゴリズムの数学などです。
理論的な理解を持つことでより質が高いモデルを構築できるようこれからも学習を続けたいです。