Python × Flask × Tensorflow.Keras 猫の品種を予測するWebアプリ

# はじめに
Webアプリ作成初挑戦、Qiita初投稿ですm(_ _)m

参考にした投稿
- [【AI×ねこ】猫の品種を当ててみる](https://qiita.com/Gri_Gra/items/e5a507ebae5c4be2be6f)
- [Python × Flask × PyTorch 数字認識Webアプリのお手軽構築](https://qiita.com/oyngtmhr/items/433974829015abac753c)

# 環境
- Mac
- Visual Code
- Python3.7.5

# 作りたいもの
　猫画像を送信したら品種を予測してくれるWebアプリ
　（Tensolflowの転移学習モデルInception-v3を用いて猫の品種を予測して
　　近い品種上位3位を確率を添えて教えてくれるやつ）
　![やりたいこと3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251117/bf0c7916-e13f-71b1-93f5-571065b1f3a0.png)


# 環境構築
### ファイル構成
```bash
　├── path/to/cat/image
　│   └── (学習データ　猫画像)
　├── cat_list.txt（猫の品種リスト）
　├── image_process.py
　├── model.h5（save_model.pyの出力）
　├── save_model.py（転移モデルを再学習する関数）
　├── sever.py
　├── static
　│   └── (入力した画像が保存される)
　└── templates
    　└── index.html
```

### importしたモジュールとバージョン
- Flask                1.1.1 
- numpy                1.18.1 
- pandas               1.0.1 
- tensorflow           2.1.0  
- scikit-learn         0.22.2.post1
- opencv-python        4.2.0.32  
- Pillow               7.0.0  

# 機械学習モデル構築

###モデル構築のイメージ
![モデル.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251117/f3194653-7607-5a0e-685b-51350dc26557.png)
###猫画像データセットを用意
[The Oxford-IIIT Pet Dataset](http://www.robots.ox.ac.uk/~vgg/data/pets/)のDownloadsのDatasetから得ました
犬の画像も含まれています

###モデル構築
Inception-v3を再学習させてmodel.h5を作ります
学習データは200枚/クラス
クラスは以下の12個
- Abyssinian
- Bengal
- Birman
- Bombay
- British_Shorthair
- Egyptian_Mau
- Maine_Coon
- Persian
- Ragdoll
- Russian_Blue
- Siamese
- Sphynx

事前にモデル構築をします

```python:save_model.py
import glob
import numpy as np
import pandas as pd
#kerasのload_imgには手動でpillowのinstall必要
from tensorflow.keras.preprocessing.image import load_img, img_to_array
from tensorflow.keras.applications.inception_v3 import InceptionV3, preprocess_input
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Dense
from sklearn.model_selection import train_test_split

x_size = 299
y_size = 299
kind_label = []
cat_img = []

# 識別する品種のリスト
cat_list = ['Abyssinian','Bengal','Birman','Bombay','British_Shorthair','Egyptian_Mau','Maine_Coon','Persian'
            ,'Ragdoll','Russian_Blue','Siamese','Sphynx']
f = open('cat_list.txt', 'w')
for x in cat_list:
    f.write(str(x) + "\n")
f.close()

#データセットのロード
for cat_kind in cat_list:
    file_list = glob.glob(f'path/to/cat/images/{cat_kind}*.jpg')
    #print(f"kind: {cat_kind}, num of images: {len(file_list)}")
    for cat_file in file_list:
        # img_path = file
        img = load_img(cat_file, target_size=(x_size, y_size))
        x = img_to_array(img)
        x = preprocess_input(x)
        cat_img.append(x)
        kind_label.append(cat_kind) 

#品種ラベルをダミー化
Y_dummy = pd.get_dummies(kind_label)

X_train, X_test, y_train, y_test = train_test_split(
    cat_img, Y_dummy, test_size=0.2, random_state=42)

# model読み込み
model = InceptionV3(weights='imagenet')

# 中間層を出力するモデル
intermediate_layer_model = Model(inputs=model.input, outputs=model.layers[311].output)

# Denseレイヤーを接続
x = intermediate_layer_model.output
x = Dense(1024, activation='relu')(x)
predictions = Dense(len(cat_list), activation='softmax')(x)

# 転移学習モデル
transfer_model = Model(inputs=intermediate_layer_model.input, outputs=predictions)

# 一旦全レイヤーをフリーズ
for layer in transfer_model.layers:
    layer.trainable = False

# 最終段のDenseだけ再学習する
transfer_model.layers[312].trainable = True
transfer_model.layers[313].trainable = True

transfer_model.compile(loss='categorical_crossentropy',
                        optimizer='adam',
                        metrics=['accuracy'])

#転移学習
transfer_model.fit(np.array(X_train), np.array(y_train), epochs=10,
                    validation_data=(np.array(X_test), np.array(y_test)))

#精度確認用に出力（必要に応じて）
loss, acc = transfer_model.evaluate(np.array(X_test), np.array(y_test))
print('Loss {}, Accuracy {}'.format(loss, acc))

transfer_model.save("./model.h5")
```

#Webアプリ


①saver.py
- モデル読み込み
- 入力データ読み込み
- 品種予測（②image_process.py）
- 入力データと予測結果表示（③index.html）

```python:sever.py
from flask import Flask, render_template, request
from tensorflow.keras.preprocessing.image import load_img
from tensorflow.keras.models import load_model
import numpy as np
from image_process import examine_cat_breeds
from datetime import datetime
import os
import cv2
import pandas as pd

app = Flask(__name__)

# モデル(model.h5)とクラスのリスト(cat_list)を読み込み
model = load_model('model.h5')
cat_list = []
with open('cat_list.txt') as f:
    cat_list = [s.strip() for s in f.readlines()]
print('= = cat_list = =')
print(cat_list)


@app.route("/", methods=["GET","POST"])
def upload_file():
    if request.method == "GET":
        return render_template("index.html")

    if request.method == "POST":
        # アプロードされたファイルをいったん保存する
        f = request.files["file"]
        filepath = "./static/" + datetime.now().strftime("%Y%m%d%H%M%S") + ".png"
        f.save(filepath)
        # 画像ファイルを読み込む
        # 画像ファイルをリサイズ
        input_img = load_img(filepath, target_size=(299, 299))
        # 猫の種別を調べる関数の実行
        result = examine_cat_breeds(input_img, model, cat_list)
        print("result")
        print(result)

        no1_cat = result[0,0]
        no2_cat = result[1,0]
        no3_cat = result[2,0]

        no1_cat_pred = result[0,1]
        no2_cat_pred = result[1,1]
        no3_cat_pred = result[2,1]

        return render_template("index.html", filepath=filepath, 
        no1_cat=no1_cat, no2_cat=no2_cat, no3_cat=no3_cat,
        no1_cat_pred=no1_cat_pred, no2_cat_pred=no2_cat_pred, no3_cat_pred=no3_cat_pred)

if __name__ == '__main__':
    app.debug = True
    app.run(host='localhost', port=5000)
```

②image_process.py
引数は
- 入力データ（予測したい猫画像）
- モデル（猫モデル）
- クラスリスト（猫の品種リスト）
返り値は予測された品種上位3位と確率が格納された行列

```python:image_process.py
from tensorflow.keras.preprocessing.image import img_to_array
import numpy as np
from tensorflow.keras.applications.inception_v3 import preprocess_input


def examine_cat_breeds(image, model, cat_list):
    # 行列に変換
    img_array = img_to_array(image)
    # 3dim->4dim
    img_dims = np.expand_dims(img_array, axis=0)
    # Predict class（preds：クラスごとの確率が格納された12×1行列）
    preds = model.predict(preprocess_input(img_dims))
    preds_reshape = preds.reshape(-1,preds.shape[0])
    # cat_list(リスト)を12×1行列に変換
    cat_array = np.array(cat_list).reshape(len(cat_list),-1)
    # 確率高い順にソートする
    preds_sort = preds_reshape[np.argsort(preds_reshape[:, 0])[::-1]]
    # 確率の降順に合わせて猫の順番も変える
    cat_sort = cat_array[np.argsort(preds_reshape[:, 0])[::-1]]
    # preds_reshape と cat_arrayを結合
    set_result = np.concatenate([cat_sort, preds_sort], 1)
    return set_result[0:3, :]
```

③index.html
綺麗に実装できませんでした(^_^;)

```html:index.html
<!DOCTYPE html>
<html>
   <body>
      {% if no1_cat %}
         <img src="{{filepath}} " border="1"> <br>
         予測結果<br>
         {{no1_cat}}:{{no1_cat_pred}}<br>
         {{no2_cat}}:{{no2_cat_pred}}<br>
         {{no3_cat}}:{{no3_cat_pred}}<br>
         <hr>
      {% endif %}
         ファイルを選択して送信してください<br>
         <form action = "./" method = "POST" 
            enctype = "multipart/form-data">
            <input type = "file" name = "file" />
            <input type = "submit"/>
         </form>
   </body>
</html>
```
# 実行
`python sever.py`＋Enterで実行して
`http://localhost:5000/`にアクセスするすると以下が表示される

<img width="639" alt="スクリーンショット 2020-03-22 23.37.19.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251117/fb84e43f-d50e-3563-9c26-59a76553209b.png">

かわいい猫画像を送信すると品種予測結果が表示されます

<img width="631" alt="スクリーンショット 2020-03-22 23.31.36.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251117/f22cc925-e594-c781-38fd-70b71e31e8af.png">

#まとめ
そういえばめちゃくちゃかわいい地域猫はキジ白っぽいんですよね
今回学習したクラスにいない…(^_^;)

キジ白など日本に多い品種の学習データを集める手法を探してみようかな
スクレイピングというやつ？

スマホアプリ化したいなぁ