アラフォーが"ネコ"のネコ種を判別するAIアプリを作成してみた

# はじめに
<!-- 発端や概要を記載 -->
2021年、プログラミング未経験のアラフォーが一念発起しエンジニアを目指すべく、㈱Aidemy様のAIアプリ開発コースの最終成果物として画像認識アプリを作成した記録を記載していきます。

どのような画像認識アプリを作成しようか考えていたところ、自宅にネコの置物が置いてあったり最近ネコの動画を観ていたり(観て泣いたり・・・)、よく野良ネコに遭遇したりと"ネコ"に関連することが多々あるのでテーマを選定しました。
それにとにかく"かわいい"ので癒やされます。

ちなみにネコの置物はこちらになります。
![IMG_20210718_143855.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/c396e5c7-6578-72bc-6caf-9da4bd2d2e39.jpeg)



# 目次
<!-- タイトルとアンカー名を編集 -->
1. [実行環境](#1-実行環境)
1. [画像収集](#2-画像収集)
1. [画像の水増し](#3-画像の水増し)
1. [CNNモデルの作成,学習](#4-cnnモデルの作成学習)
1. [HTML,CSSのコーディング](#5-htmlcssのコーディング)
1. [FLASKのコード作成](#6-flaskのコード作成)
1. [アプリのデプロイ,動作確認,リンク先](#7-アプリのデプロイ動作確認リンク先)
1. [考察,感想](#8-考察感想)
1. [参考文献](#9-参考文献)

<!-- 各チャプター -->
#1. 実行環境
・Python 3.8.10
・MacBook Air
・Google Colaboratory
・Visual Studio Code (1.57.1)
・Annaconda Navigator

#2. 画像収集
テーマの選定ができたので、画像収集を実施していきます。
まずは学習用の画像収集です。
今回使用する画像は、特に自分の好きな猫種を集めてみました。
アメリカン・ショートヘア、スコティッシュ・フォールド、ノルウェージャン・フォレスト・キャット、ブリティッシュ・ショートヘア、ベンガル、マンチカン、ラグドール、ロシアンブルーの８種です。
混血種(MIX)も大好きなのですが、様々な混血種があり画像認識困難と判断したため、今回は対象外とさせていただきました。

画像収集についてはicrawlerを使用することにしました。
icrawlerはpythonでwebクローリングを行い、画像を集めるためのフレームワークで、非常に短いコードを記述するだけで画像を集めることができます。
BeautifulSoupやselenium、Flickr等を絡めてスクレイピングで画像収集もできますが、
Pythonで画像データを手軽に収集するならこれ！と言わんばかりに検索ででてきたので
活用してみました。
今回、icrawlerを使用してBing検索してみました。


```python:icrawler.py
# Bing用クローラーのモジュールをインポート
from icrawler.builtin import BingImageCrawler

# Bing用クローラーの生成
bing_crawler = BingImageCrawler(
    downloader_threads=4,
    storage={'root_dir': 'images'}) #imageというフォルダに画像が集まります。

# クロール（キーワード検索による画像収集）の実行
bing_crawler.crawl(
    keyword="ネコ", #""に検索ワード(猫種)を入力します。
    max_num=200) #今回は最大200枚収集します。
```

（参考サイト）
[画像データをキーワード検索で効率的に収集する方法（Python「icrawler」のBing検索）](https://www.atmarkit.co.jp/ait/articles/2010/28/news018.html)

各猫種を検索キーワードとして約100〜200枚ずつダウンロードしました。
ダウンロードした画像の一部です。

![スクリーンショット 2021-07-05 13.56.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/b8484273-9a3e-bbfb-19ab-fdf87e3648ff.png)

![スクリーンショット 2021-07-05 14.40.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/064837bf-d09a-d60a-ffec-bd2886513be5.png)

ダウンロードした画像より、一匹で写っている画像や中央に写っている画像を厳選し、100〜120枚ほど収集しました。


#3. 画像の水増し
画像の枚数が少ないため水増しを実施していきます。
今回、kerasのImageDataGeneratorを使用し、
rotation_range、horizontal_flip、shear_rangeの3種類を組み合わせて水増ししてみました。

```python:extend.py
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from keras.utils.np_utils import to_categorical
from keras.layers import Dense, Dropout, Flatten, Input, BatchNormalization
from keras.applications.vgg16 import VGG16
from keras.models import Model, Sequential
from keras import optimizers
from keras.preprocessing.image import ImageDataGenerator
from glob import glob

# パスに使用する用のネコの名前リスト
cat_breed = ['American', 'Bengal', 'British', 'Munchkin', 'Norwegian', 'Ragdoll', 'Russian', 'Scottish']

# 画像ファイルのパス作成関数
def make_path(dir):
    path_name = os.listdir('/content/drive/MyDrive/images/{}'.format(dir))
    path_name = glob('/content/drive/MyDrive/images/{}/*.jpg'.format(dir))
    return path_name

# 画像ファイルの一時保管用リスト
pre_list = [[] for i in range(len(cat_breed))]

# 画像ファイルの読み込み    
for n, breed_name in zip(range(len(cat_breed)), cat_breed):
    numbers = len(make_path(breed_name))
    for i in range(numbers):
        img = cv2.imread('/content/drive/MyDrive/images/{}/'.format(breed_name) + make_path(breed_name)[i])
        img = cv2.imread(make_path(breed_name)[i])
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        img = cv2.resize(img, (100, 100))
        pre_list[n].append(img)

# ネコの種類ごとに分けられていた画像ファイルを1つのリストに統合する
img_list = []
for i in range(len(cat_breed)):
    img_list += pre_list[i]

# 正解ラベル格納リスト
labels = []
for breed_name in cat_breed:
    numbers = len(make_path(breed_name))
    for i in range(len(cat_breed)):
        labels += [i]*numbers

# 画像データをnumpy配列に変換
X = np.array(img_list)
y = np.array(labels)

# 画像データをシャッフル
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)

# 画像を水増しする関数
def img_extend(x, y):
    X_extend = []
    y_extend = []
    i = 0

    # 水増しを20回繰り返す
    while i < 20:
        datagen = ImageDataGenerator(rotation_range = 30, horizontal_flip = True, shear_range = 0.2)
        extension = datagen.flow(X_train, y_train, shuffle = False, batch_size = len(X_train))
        X_extend.append(extension.next()[0])
        y_extend.append(extension.next()[1])
        i += 1

    # numpy配列に変換
    X_extend = np.array(X_extend).reshape(-1, 100, 100, 3)
    y_extend = np.array(y_extend).reshape(-1, len(cat_breed))

    return X_extend, y_extend

img_add = img_extend(X_train, y_train)

# 元の画像データと水増しデータを統合
X_train_add = np.concatenate([X_train, img_add[0]])
y_train_add = np.concatenate([y_train, img_add[1]])
```

各クラス20倍の枚数を水増しし、合計約20000枚増やしました。

#4. CNNモデルの作成,学習
水増しした画像を使用してCNNモデルの作成と学習を実施していきます。

ハイパーパラメータ等を変更し検証する過程で、以下の様に設定しました。

- バッチサイズについて8/16/32/64/128で検証したところ、8のときの精度が一番高かったため8とした。
- Dropoutを0.1にし、活性化関数をsigmoid関数とした。ReLU関数で検証したところ精度のブレが大きかったため、今回のモデル作成はsigmoid関数で実施した。
- epoch数について、1/3/5/10/20で検証したところ、3からほぼ精度が変わらなかっためepochを5とした。

以上のことを反映させ、最終的な全体のコードは以下のようになりました。

```python:model.py
# 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, activation = 'sigmoid'))
top_model.add(Dropout(0.1))
top_model.add(Dense(8, activation = 'softmax'))

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

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

# コンパイル
model.compile(loss = 'categorical_crossentropy', optimizer = optimizers.SGD(lr = 1e-4, momentum = 0.9), metrics = ['accuracy'])

# 学習の実行
history = model.fit(X_train_add, y_train_add, validation_data = (X_test, y_test), batch_size = 8, epochs = 5)

# 学習モデルを保存
model.save("cat_breed8.h5")

#----------------------------------------------------------
# 以下、評価指標を可視化するコード

# 正解率の可視化
plt.plot(history.history['accuracy'], label='acc', ls='-')
plt.plot(history.history['val_accuracy'], label='val_acc', ls='-')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.legend(loc='best')
plt.show()

from sklearn.metrics import classification_report

# 混同行列を計算する関数
def Report(Imgs, y_true):
    def pred_img(img):
        img = cv2.resize(img, (100, 100))
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)   
        pred = np.argmax(model.predict(img.reshape(-1, 100, 100, 3)))
        return pred

    y_pred = []
    for x in Imgs:
        y_pred.append(pred_img(x))

    y_true = np.argmax(y_true, axis = 1)
    rep = classification_report(y_true, y_pred, target_names = cat_breed, output_dict = True)
    return rep

rep_dict = Report(X_test, y_test)

# F値をリストに格納
F_vals = []
for breed_name in cat_breed:
    F = rep_dict[breed_name]['f1-score']
    F_vals.append(F)

# プロット用にSeriesを作成
F_dict = dict(zip(cat_breed, F_vals))
F_series = pd.Series(F_dict)

# matplotlibに日本語フォントを導入
import matplotlib as mpl
mpl.rcParams['font.family'] = 'AppleGothic'

# F値の可視化
F_series.plot.barh()
plt.xlim([0.4, 1.0])
plt.subplots_adjust(left = 0.22, right = 0.9, bottom = 0.15, top = 0.9)
plt.xlabel('F値')
plt.show()  
```
![グラフ1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/c8c75020-bffb-a44f-8ae9-c03e5ebd3220.jpeg)

![F値1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/b4eef6ca-9ef7-66d9-1501-720a58a430cb.jpeg)


正解率は約70%でした。
ベンガルの様に特徴的なネコ種は安定して高い数値を得られましたが、マンチカンやスコッティ・フォールド等の様々な毛色をもつネコ種だと結果にバラツキが見られました。
また、オリジナル画像の少なさやトリミングがあまりうまくできていなかったため、これらのことも結果に影響してると思われます。

#5. HTML,CSSのコーディング
次にHTML,CSSのコーディングを実施していきます。
全体のデザインとしては㈱Aidemy様のAIアプリ開発コースで作成したデザインをベースに作成していきます。

今回のテーマはするのはネコ種を判別するAIアプリなので、シンプルにネコの画像のみで作成することとしました。

フリー画像素材よりかわいいネコ画像を見つけたので、これをbackground-imageとして使用することにしました。

ヘッダーメニューにはHOMEに戻ることができるリセットボタンと、判別できるネコ種の一覧,ネコに関する小話を別ウインドウで開ける様に設定しました。
以下、HTMLとCSSのコードになります。

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ネコの種類を判別するアプリ（8種類対応）</title>
    <link rel="stylesheet" href="../static/stylesheet.css">
</head>
<body>
    <header>
        <div>
            <a class='home' href="{{url_for('upload_file')}}">
                リセット
            </a>
            <a class='variety' href="{{url_for('c_type')}}" onClick="window.open('/templates/c_type.html','windowname','scrollbars=yes,width=550,height=450'); return false;">
                判別できるネコの種類    
            </a>
            <a  class='potency' href="{{url_for('anecdote')}}" onClick="window.open('/templates/anecdote.html','windowname','scrollbars=yes,width=670,height=650'); return false;">
                ネコの小話
            </a>
        </div>

    </header>

    <div class="main">
        <img class="title" src="../static/pic/top_img.jpg">

        <h2>ネコ種を、AIがお答えします！</h2>
        <p class='img_send'>ネコの画像を送信してください</p>
        <form method="POST" enctype="multipart/form-data">
            <input class="file_choose" type="file" name="file">
            <input class="btn" value="送信" type="submit">
        </form>

        <h3 class="answer">{{answer}}</h3>
    </div>


    <footer>
        <small>&copy; 2021 K.Yoshinaga</small>
    </footer>
</body>
</html>
```

```css:stylesheet.css
body{
    max-width: 1920px;
    width: 100%;
    padding: 0px 0px;
    margin: 0 auto;
    background-color: #C5956B;
    display: flex;
    flex-direction: column;
    min-height:100vh;
}

header{
    height: 40px;
    width: 100%;
    background-color: #734e30;  
}

.home{
    width: 140px;
    color: #fff;
    line-height: 44px;
    text-align: center;
    display: block;
    transition: all 0.5s;
    text-decoration: none;
    float: left;
}

.variety{
    width: 200px;
    color: #fff;
    line-height: 44px;
    text-align: center;
    display: block;
    transition: all 0.5s;
    text-decoration: none;
    float: left;
}

.potency{
    width: 200px;
    color: #fff;
    line-height: 44px;
    text-align: center;
    display: block;
    transition: all 0.5s;
    text-decoration: none;
    float: left;
}

.home:hover {
    background-color: rgba(255, 255, 255, 0.3);
    width: 180px;
}

.variety:hover {
    background-color: rgba(255, 255, 255, 0.3);
    width: 250px;
}

.potency:hover {
    background-color: rgba(255, 255, 255, 0.3);
    width: 260px;
}

.title{
    background-size: cover;
    opacity: 0.8;
    border-bottom: 2px groove;
}

img{
    width: 100%;
    height: 500px;
}

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

.img_send{
    color: #444444;
    margin: 50px 0px 30px 0px;
    text-align: center;
    font-size: 20px;
}

.answer{
    color: #734e30;
    margin: 70px 0px 30px 0px;
    text-align: center;
    font-size: 25px;
}

form{
    text-align: center;
}

.flower_info{
    text-align: center;
    font-size: 20px;
}

.error_message{
    color :orangered;
    text-align: center;
    font-size: 25px;
}

footer{
    background-color: #734e30;
    border-top: 3px ridge;
    text-align: center;
    height: 10px;
    width: 100%;
    padding: 0px 3px 10px 3px;
    margin-top: auto;
}

small{
    color: #fff;
}
```

こちらが判定可能なネコの種類を記載したページのコードです。

```html:c_type.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>判定できるネコの種類</title>
    <link rel="stylesheet" href="../static/c_type.css">
</head>

<body>
    <h2>
        判別できるネコは、以下の8種です。
    </h2>
    <div>
        <ul>
            <li>アメリカン・ショートヘア</li>
            <li>ベンガル</li>
            <li>ブリティッシュ・ショートヘア</li>
            <li>マンチカン</li>
            <li>ノルウェージャン・フォレスト・キャット</li>
            <li>ラグドール</li>
            <li>ロシアンブルー</li>
            <li>スコティッシュ・フォールド</li>
        </ul>
    </div>    

    <a href="#" onclick="window.close()">[閉じる]</a>
```
```css:c_type.css
body{
    background-color: #C5956B;
    text-align: center;
}

h2{
    text-align: center;
}

div{
    text-align: center;
    margin-left: -40px;
    margin-right: auto;
}

li{
    list-style: none;
}
```

```html:anecdote.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ネコの小話</title>
    <link rel="stylesheet" href="../static/anecdote.css">
</head>

<body>
    <h2>
        ネコの小話
    </h2>

    <p class='subject'>
        ネコはペットして飼う動物としてイヌと並ぶ程人気の高い動物です。
        <br>
        ネコの鳴き声には癒やしの効果があり、アニマルセラピーとしても大きな癒やしの力をもっています。
        <br>
        そんなネコは元々農家の守り神と崇められており、また幸運をもたらすと言われております。
        <br>
        また、道端で遭遇するネコにも幸運や癒やしの効果があるので、ぜひネコに遭遇したら愛でてあげてください。
    </p>

    <p class='citation'>
        【参考文献】
        <br>
        <a href = "https://amaterasu49.media/archives/38524">アマテラスチャンネル49(ネコのスピリチュアル)</a>
    </p>

    <a href="#" onclick="window.close()">[閉じる]</a>
</body>
</html>
```

```css:anecdote.css
body{
    background-color: #C5956B;
    text-align: center;
}

h2{
    text-align: center;
    border-bottom: 2px groove;
}

.subject{
    font-size: 16px;
    display: block;
    text-align: left;
    margin-left: 10px;
}

.citation{
    font-size: 15px;
    margin-top: 50px;
    margin-bottom: 70px;
}
```

出来上がった画面が以下になります。

![スクリーンショット 2021-07-14 3.19.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/c4772714-4fde-2365-7d54-e1e2ec11b837.png)

#6. FLASKのコード作成

HTML、CSSのコード同様、㈱Aidemy様の数字認識アプリのコードをベースに作成していきます。

画像識別後の解答として、答えとなるネコの名前とAIが判定した確率を載せることにしました。
以下がコードになります。

```python: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 = ["アメリカン・ショートヘア","ベンガル","ブリティッシュ・ショートヘア","マンチカン","ノルウェージャン・フォレスト・キャット","ラグドール","ロシアンブルー","スコティッシュ・フォールド"]
image_size = 100

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

model = load_model('./cat_breed8.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, target_size=(image_size,image_size))
            img = image.img_to_array(img)
            data = np.array([img])
            #変換したデータをモデルに渡して予測する
            result = model.predict(data)[0]
            predicted = result.argmax()
            #確率の表示
            per = int(result[predicted]*100)
            # 解答の文章
            pred_answer = 'このネコは' + classes[predicted] + 'です（確率: {}%）'.format(per)         

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

        # 画像ファイルがない場合、あるいは異なるファイル形式の場合
        else:
            error = '画像ファイルは png, jpg, jpeg, gif のいずれかの形式にしてください'
            return render_template('index.html', error = error)

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

# 判別できるネコの種類（別ウインドウ）
@app.route('/templates/c_type.html')
def c_type():
    return render_template('c_type.html')

# ネコの小話（別ウインドウ）
@app.route('/templates/anecdote.html')
def anecdote():
    return render_template('anecdote.html')

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

#7. アプリのデプロイ,動作確認,リンク先

Herokuにアプリをデプロイしていきます。
無事にデプロイできたので、あとは動作確認です。

まず、小手調べとしてラグドールの画像を使用してみました。

![222.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/1e6cccf6-9c74-3994-5263-8e2309fa5667.jpeg)

結果

![スクリーンショット 2021-07-14 12.04.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/7d3cea43-ee52-ce31-52c6-a291a42bdd26.png)

もう一つ、F値の結果があまりよろしくなかったマンチカンを見てみます。

![3554217_s-600x400.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/1d269af4-27d3-30fc-e9d2-f24039e59e19.jpeg)


![スクリーンショット 2021-07-14 12.47.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1718802/89f380b2-78bd-9d67-ff25-d09d7ce52fe6.png)

2つとも正解でした。
何度か検証が必要ですが今回はここまでとさせていただきました。

これで動作確認は終了です。
AIが導き出した答えと確率は確認とれたのでこれでひとまず完成としました。

アプリのリンク先は下記になります。
リンク先に飛ぶまで30秒ほど掛かりますので少々お待ち下さい。
また、スマホでも動作しますが多少画面がくずれる可能性がありますのでご了承願います。

[ネコ種を判定するAIアプリ(8種)](https://catbreed.herokuapp.com/)

#8. 考察・感想

今回、初めてAIアプリを作成して
オリジナル画像の精度(中央に画像がある,1匹のみにする等)や画像の数によって画像認識の精度に大きく影響するのだと感じました。
重要な特徴(ベンガルやロシアンブルー)でも、オリジナル画像が少ないと、AIにとっては情報が少なかったのだろうと思いました。

また、今回の活性化関数はsigmoid関数で設定しましたがReLU関数やDropoutの設定変更によって精度の改良ができたかもしれませんので今後の課題として検証していきたいと思います。

HTML、CSS、FLASK、Herokuのデプロイに関してもどんどん数をこなして精進していきたいです。

様々なエラーや精度上げに直面しても自分の力で切り抜けられる様日々頑張っていきたいと思います。


#9. 参考文献

["春に咲く花の画像" を判別するAIアプリを作ってみた](https://qiita.com/KK_Ken/items/97f382c632e93eb7a239)

[Qiita Markdown 書き方 まとめ](https://qiita.com/shizuma/items/8616bbe3ebe8ab0b6ca1)

[活性化関数「Tanh」「Sigmoid」「ReLu」について数式なしでざっくり整理／ディープラーニング](https://arakan-pgm-ai.hatenablog.com/entry/2018/11/07/090000)

[PhotoAC](https://www.photo-ac.com/)