CNNで花を分類するアプリの作成

# はじめに
道端や観光スポットで花をよくみかけるが、何の花なのかわからないことが多く、これをAIで解決することができないかと考えモデルを作成することにしました。
元々、AIには興味があったため、勉強を兼ねて実施しています。
今回は5種類の花の分類を行っています。

#目次
[1.開発環境](#開発環境)
[2.データセットの作成](#データセットの作成)
[3.シンプルなCNNでの検証](#シンプルなCNNでの検証)
[3-1.ライブラリの読み込み](#ライブラリの読み込み)
[3-2.学習データの読み込み](#学習データの読み込み)
[3-3.モデルの構築](#モデルの構築)
[3-4.モデルのコンパイル、学習](#モデルのコンパイル、学習)
[3-5.制度の評価](#制度の評価)
[3-6.層を増減させて確認](#層を増減させて確認)
[4.転移学習の導入](#転移学習の導入)
[4-1.コードの変更](#コードの変更)
[4-2.結果](#結果)
[4-3.画像サイズ変更](#画像サイズ変更)
[5.Flask形式に置き換え、Renderへアプリをデプロイ](#Flask形式に置き換え、Renderへアプリをデプロイ)
[6.完成品](#おわりに)
[7.おわりに](#おわりに)

# 開発環境
Windows 10
Visual Studio Code
Anaconda

# データセットの作成
データセットの作成にあたり、Kaggleから**5 Flower Types Classification Dataset**という5種類の花の画像がセットになったデータセットをダウンロードします。
https://www.kaggle.com/datasets/kausthubkannan/5-flower-types-classification-dataset
5 Flower Types Classification Datasetは、
* リリー
* ロータス
* ひまわり
* 蘭
* チューリップ
がそれぞれ1000画像あるデータセットです。

# シンプルなCNNでの検証
まずは基本的なCNNモデルを構築しました。
CNNとは、画像認識に広く使われるディープニューラルネットワークを用いた深層学習手法です。
このCNNモデルに5つに分類した花画像を学習させ、的確に花の種類を識別させるのが目標です。

## ライブラリの読み込み
5つの分類データの保存パスから画像データを読み込み、学習データ、検証データに分離しました。
```python
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 Dense, Dropout, Flatten, Input,Conv2D,MaxPooling2D,Activation, BatchNormalization
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers
```

## 学習データの読み込み
```python
path_lilly = os.listdir('flower_images/Lilly/')
path_lotus = os.listdir('flower_images/Lotus/')
path_orchid = os.listdir('flower_images/Orchid/')
path_sunflower = os.listdir('flower_images/Sunflower/')
path_tulip = os.listdir('flower_images/Tulip/')

img_lilly = []
img_lotus = []
img_orchid = []
img_sunflower = []
img_tulip = []

for i in range(len(path_lilly)):
    img = cv2.imread('flower_images/Lilly/' + path_lilly[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    img_lilly.append(img)

for i in range(len(path_lotus)):
    img = cv2.imread('flower_images/Lotus/' + path_lotus[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    img_lotus.append(img)

for i in range(len(path_orchid)):
    img = cv2.imread('flower_images/Orchid/' + path_orchid[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    img_orchid.append(img)

for i in range(len(path_sunflower)):
    img = cv2.imread('flower_images/Sunflower/' + path_sunflower[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    img_sunflower.append(img)

for i in range(len(path_tulip)):
    img = cv2.imread('flower_images/Tulip/' + path_tulip[i])
    b,g,r = cv2.split(img)
    img = cv2.merge([r,g,b])
    img = cv2.resize(img, (100,100))
    img_tulip.append(img)


X = np.array(img_lilly + img_lotus + img_orchid + img_sunflower + img_tulip)
y =  np.array([0]*len(img_lilly) + [1]*len(img_lotus) + [2]*len(img_orchid) + [3]*len(img_sunflower) + [4]*len(img_tulip))

rand_index = np.random.permutation(np.arange(len(X)))
X = X[rand_index]
y = y[rand_index]

y = to_categorical(y)

# データの分割
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):]
```
## モデルの構築
畳み込み層を3層と全結合層を3層に通すシンプルなモデルを作成しました。
後に全結合層の層の数を増減させて検証するために、A,B,C層と振っておきます。
```python
model = Sequential()
model.add(Conv2D(input_shape=(100,100,3),filters=32,kernel_size=(5,5),strides=(1,1),padding="same"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())
model.add(Conv2D(filters=32,kernel_size=(3,3),strides=(1,1),padding="same"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())
model.add(Conv2D(filters=32,kernel_size=(3,3),strides=(1,1),padding="same"))
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(BatchNormalization())
model.add(Flatten())
# A層
model.add(Dense(256))
model.add(BatchNormalization())
model.add(Activation('relu'))
# B層
model.add(Dense(25))
model.add(BatchNormalization())
model.add(Activation('relu'))
# C層
model.add(Dropout(0.5))
model.add(Dense(5, activation='softmax'))
```
## モデルのコンパイル、学習
```python
model.compile(loss="categorical_crossentropy",optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),metrics=["accuracy"])
model.summary() #モデルの表示
history = model.fit(X_train, y_train, batch_size=400, epochs=100, validation_data=(X_test, y_test))
```
## 制度の評価
```python
plt.figure(figsize=(10,5))
plt.subplot(1,2,1)
plt.plot(history.history["accuracy"], label="acc", ls="-", marker="o")
plt.plot(history.history["val_accuracy"], label="val_acc", ls="-", marker="x")
plt.ylabel("accuracy")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.grid(True)
plt.subplot(1,2,2)
plt.plot(history.history["loss"], label="acc", ls="-", marker="o")
plt.plot(history.history["val_loss"], label="val_acc", ls="-", marker="x")
plt.ylabel("loss")
plt.xlabel("epoch")
plt.legend(loc="best")
plt.grid(True)
plt.show()
```
![シンプルなCNN_3層.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513676/f6853357-42b5-c7cd-8dbe-29ceef6c5ed8.png)

結果としては56%程度であり、高い精度が出ていないようです。

## 層を増減させて確認
B層を削除して2層にして確認
![シンプルなCNN_2層.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513676/223e53d7-dcc9-a9d8-9917-cb3a408f5d6f.png)
結果は60%程度になりました。

A層とB層の間に下記層を挿入して4層にして確認
```python
model.add(Dense(125))
model.add(BatchNormalization())
model.add(Activation('relu'))
```
![シンプルなCNN_4層.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513676/5a09aca0-08c2-7bf2-ad98-f803b4247a10.png)
結果としては60%程度でした。

結論としては全結合層を多少増減させた程度では精度の向上は見れなさそうということになります。
今回は全結合層の1層の増減だけにしましたが、大きく増加させた場合や畳み込み層を変更した際の制度も今後検証していきたいと思います。

# 転移学習の導入
精度の向上が見込めなかったため、転移学習を導入してみることにしました。
転移学習とは、すでに大量のデータを使ってある程度学習しているモデルをベースに、自分で層を追加して学習を行う方法です。
今回はVGG16モデルを通した後、3つの全結合層を追加するモデルを構築しました。

## コードの変更
新たにライブラリの読み込みを追加し、先ほどのモデルの構築部分をVGGを使用するように差し替えます。
```python
from tensorflow.keras.applications.vgg16 import VGG16
```
```python
# vgg16のインスタンス作成 
input_tensor=Input(shape=(100,100,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))
top_model.add(BatchNormalization())
top_model.add(Activation('relu'))
top_model.add(Dense(25))
top_model.add(BatchNormalization())
top_model.add(Activation('relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(5, activation='softmax'))

# モデルの連結
model = Model(inputs=vgg16.input,outputs=top_model(vgg16.output))

# vgg16の重みの固定
for layer in model.layers[:19]:
  layer.trainable = False

# モデルのコンパイル
model.compile(loss="categorical_crossentropy",optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),metrics=["accuracy"])
```
## 結果
結果は66%程度と多少精度は上がっていましたが、劇的な効果は見られませんでした。
![vgg16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513676/63fe07e2-71a1-bc36-6ac5-f21b599dc305.png)

## 画像サイズ変更
画像サイズを100*100から200*200にして精度を検証してみました。
※batch_size=400だと「ResourceExhaustedError」がでたため、batch_size=200に変更して実施しています
![vgg16_200_200.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513676/82135716-e08c-166f-5a37-00595e65213f.png)

結果は89%と制度が向上していることがわかります。
画像サイズはある程度大きくないと特徴を抽出しきれないのかもしれません。
今後は、画像サイズを変えたことによる特徴量の抽出の変化を可視化し、何が変わったのか検証していきたいと思います。

# Flask形式に置き換え、Renderへアプリをデプロイ
Flask形式に置き換え、WEBアプリとしてRenderで公開することにしました。
事前準備として先ほど作成したプログラムに、以下の工程を追加して識別モデルをmodel.h5として書き出しておきます。
```python
#resultsディレクトリを作成
result_dir = 'results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)
# 重みを保存
model.save(os.path.join(result_dir, 'model.h5'))
```
他にはpythonのバージョンを記したruntime.txt、読み込みライブラリを記したrequirements.txt、アプリ表面上のレイアウトとなるindex.html、デザイン要素をまとめたstylesheet.cssと画像を全て１つのフォルダ内に用意しました。
![folder_image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/513676/224f2068-fc02-1473-c2f9-fca4d34e7267.png)

そしてアプリとしてメインで動くmain.pyを以下のように作成しました。
Aidemyアプリ開発講座、Flask入門のコードを流用して作成しています。
```python
import os
from flask import Flask, request, redirect, render_template, flash
from werkzeug.utils import secure_filename
from tensorflow.keras.models import Sequential, load_model
from tensorflow.keras.preprocessing import image

import numpy as np


classes = ["リリー","ロータス","欄","ひまわり","チューリップ"]
image_size = 200

UPLOAD_FOLDER = "uploads"   
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

app = Flask(__name__)

def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

path = os.getcwd()
print(path)
model = load_model('./model.h5', compile=False)
#学習済みモデルをロード


@app.route('/', methods=['GET', 'POST'])
def upload_file():
    if request.method == 'POST':
        if 'file' not in request.files:
            flash('ファイルがありません')
            return redirect(request.url)
        file = request.files['file']
        if file.filename == '':
            flash('ファイルがありません')
            return redirect(request.url)
        if file and allowed_file(file.filename):
            filename = secure_filename(file.filename)
            file.save(os.path.join(UPLOAD_FOLDER, filename))
            filepath = os.path.join(UPLOAD_FOLDER, filename)

            #受け取った画像を読み込み、np形式に変換
            img = image.load_img(filepath, grayscale=False, target_size=(image_size,image_size))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            pred_answer = "これは " + classes[predicted] + " です"

            return render_template("index.html",answer=pred_answer)

    return render_template("index.html",answer="")


if __name__ == "__main__":
    port = int(os.environ.get('PORT', 8080))
    app.run(host ='0.0.0.0',port = port)
```

Renderにデプロイして完成です。

# 完成品
出来上がったアプリは下記です。
https://flask-flower-app.onrender.com/

# おわりに
VGGを適切に使用することによって、初心者でもある程度の画像認識の精度を出せることができました。
しかし、パラメータの設定や、画像サイズによって制度は大きく変わってくることがわかりました。
ただ、全結合層を1層増減させただけでは、学習結果に大きな影響がなかったため、大きく増加させた場合や畳み込み層を変更した際の制度も今後検証していきたいと思います。
また、画像サイズを変えたことによる特徴量の抽出の変化を可視化し、何が変わったのか検証していきたいと思います。
今回の検証において、学習データが大きいと、自身のPCではエラーが発生したため、大量のデータを使用する場合は環境構築が大変だと感じました。