テリア犬6種を区別するAIアプリを作成してみた

# はじめに
テリアはイギリスなどが原産で３２種類が知られています。小型犬から大型犬までサイズも豊富で、短毛・長毛と被毛の長さも様々です。
この中から容姿の似たオーストラリアン・シルキー・テリア、ノーリッチ・テリア、アイリッシュ・テリア、チベタン・テリア、スコティッシュ・テリア、ヨークシャー・テリアを見分けるAIアプリを作ってみました。

# 実行環境
・Python 3.8.5
・Visual Studio Code 1.52.1
・Google colaboratory

# 画像データセットの作成
[スタンフォード 犬のデータセット](http://vision.stanford.edu/aditya86/ImageNetDogs/)からテリア犬6種の画像をダウンロードしました。
データは全部で1065画像あり、250×250のサイズに変更し、以下のコードでdataset.pklというファイルを作成しローカル環境に保存しました。


```ruby:dataset.py
import os
import cv2
import numpy as np
import pickle

path = './images'
 
list1 = os.listdir(path)

print(list1)

# 画像サイズの横と縦
width = 250
height = 250

# 犬種の画像を格納する配列
dog_imgs = []

# 犬種の画像に対応した犬種のindexを格納する配列
dog_breeds = []

# 犬種とindexを対応させた配列
dog_breeds_list = []

# 各犬種の画像リストを作成する
# 各犬種の空リストを作成し、犬種ごとのフォルダから画像を１枚ずつ追加
for folder_name in os.listdir(path):
    path_folder_name = os.listdir(path+ "/" + folder_name)
    dog_breeds_list.append(folder_name[10:])
    for i in range(len(path_folder_name)):
        img = cv2.imread(path + "/" + folder_name+ "/" + path_folder_name[i])
        img = cv2.resize(img, (width, height))
        dog_imgs.append(img)
        dog_breeds.append(len(dog_breeds_list)-1)

print(len(dog_imgs))
print(len(dog_breeds))

# データセットの作成・保存
X = np.array(dog_imgs)
y = np.array(dog_breeds)
z = np.array(dog_breeds_list)

with open("./dataset.pkl", "wb") as f:
 pickle.dump((X, y, z), f)
```

# モデルを構築する/学習する
ローカル環境に保存したdataset.pklをGoogle colaboratoryにアップロードします。
Google colaboratoryで下記のコードを実行し、VGG16を使って転移学習を行いモデルを構築しました。テストデータでの正答率は81.6%でした。
作成したモデルをダウンロードしローカル環境に保存します。

```ruby:model.py
import matplotlib.pyplot as plt
import pickle
import cv2
import numpy as np
from sklearn.model_selection import train_test_split
from tensorflow.keras.applications.vgg16 import VGG16
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.layers import Dense, Dropout, Flatten, Input
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras import optimizers

# モデルの保存
import os
from google.colab import files

# データセットのload
with open("./dataset.pkl", "rb") as f:
 X, y, z= pickle.load(f)

# 画像サイズの横と縦
width = 250
height = 250

# トレーニングデータとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)

X_train = X_train.reshape(-1, height, width, 3)
X_test = X_test.reshape(-1, height, width, 3)
y_train = to_categorical(y_train)
y_test = to_categorical(y_test)

# VGG16のload
input_tensor = Input(shape=(height, width, 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(512, activation='relu'))
top_model.add(Dropout(0.2))
top_model.add(Dense(256, activation = 'relu'))
top_model.add(Dropout(0.2))
top_model.add(Dense(128, activation = 'relu'))
top_model.add(Dropout(0.2))
top_model.add(Dense(6, activation='softmax'))


model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))

# modelの19層目までがvggのモデル
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'])

model.fit(X_train, y_train, batch_size=64, epochs=10, verbose=1, validation_data=(X_test, y_test))

# 精度の評価
scores = model.evaluate(X_test, y_test, verbose=1)
print('Test loss:', scores[0])
print('Test accuracy:', scores[1])

for i in range(5):
    x = X_test[i]
    x = cv2.cvtColor(x, cv2.COLOR_BGR2RGB)
    plt.imshow(x)
    plt.show()
    pred = np.argmax(model.predict(x.reshape(1,height,width,3)))
    print(z[pred])
    print(pred)
    print(y_test[i])

# resultsディレクトリを作成

result_dir='results'
if not os.path.exists(result_dir):
    os.mkdir(result_dir)
# 重みを保存
model.save(os.path.join(result_dir,'model.h5'))

files.download('/content/results/model.h5')
```

# テリア犬判別アプリ
ローカルに保存したモデルを用いて、HTML、CSSコードを記述しテリア犬6種を識別するアプリを作成します。AIが判断した確率も載せることにしました。

```ruby:main.py
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 = ["オーストラリアン・シルキー・テリア","ノーリッチ・テリア","アイリッシュ・テリア","チベタン・テリア","スコティッシュ・テリア","ヨークシャー・テリア"]
# 画像サイズの横と縦
width = 250
height = 250

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

app = Flask(__name__)

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

model = load_model('./model.h5')

@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=(height,width))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            per = result[predicted] * 100
            pred_answer = "これは {} です（確率：{:.2f}％）".format(classes[predicted], per)

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

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

if __name__ == "__main__":
    app.run()
```

```ruby:index.html
<!DOCTYPE html>
<html lang='ja'>
<head>
    <meta charset='UTF-8'>
    <meta name='viewport' content="device-width, initial-scale=1.0">
    <meta http-equiv='X-UA-Compatible' content="ie=edge">
    <title>Terrier Classifier</title>
    <link rel='stylesheet' href="./static/stylesheet.css">
</head>
<body>
    <header> 
        <div style="text-align: left;"></div><a class='header-logo' href="#">Terrier Classifier</a></div>
    </header>

    <div class='main'>    
        <h2> AIがテリア犬を見分けます！</h2>
        <p>画像を送信してください</p>
        <form method='POST' enctype="multipart/form-data">
            <input class='file_choose' type="file" name="file">
            <input class='btn' value="submit!" type="submit">
        </form>
        <div class='answer'>{{answer}}</div>
    </div>

    <footer>
    </footer>
</body>
</html>
```

```ruby:index.css
header {
    background-color: #76B55B;
    height: 60px;
    margin: -8px;
    display: flex;
    flex-direction: row-reverse;
    justify-content: space-between;
}

.header-logo {
    color: #fff;
    font-size: 25px;
    margin: 15px 25px;
}

.main {
    height: 370px;
}

h2 {
    color: #444444;
    margin: 90px 0px;
    text-align: center;
}

p {
    color: #444444;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

.answer {
    color: #444444;
    margin: 70px 0px 30px 0px;
    text-align: center;
}

form {
    text-align: center;
}

footer {
    background-color: #F7F7F7;
    height: 110px;
    margin: -8px;
    position: relative;
}
```

# 検証
テリア犬6種のWikipediaのトップ画像を認識できるか確認します。
![Terrier Classifier.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1679665/58576d4d-867a-5f00-6ab2-ff8e290cf810.jpeg)

オーストラリアン・シルキー・テリア：判定×　ヨークシャー・テリア（確率：86.26％）
ノーリッチ・テリア：判定×　スコティッシュ・テリア（確率：73.09％）
アイリッシュ・テリア：判定×　チベタン・テリア（確率：89.32％）
チベタン・テリア：判定○　チベタン・テリア（確率：99.99％）
スコティッシュ・テリア：判定○ スコティッシュ・テリア（確率：57.38％）
ヨークシャー・テリア：判定○　ヨークシャー・テリア（確率：99.99％）

Wikipediaは判定しやすい画像を載せていると思われるので検証に用いましたが、3勝3敗でした。
ランダム回答での正解数の期待値は1/6 × 6 =  1であり、ランダム予測は上回っています。
犬の大きさや向きが様々であったり、学習用・検証用とも縦長、横長と様々のサイズの画像をすべて正方形に成形しているため識別率が低下していると考えられました。