LoginSignup
4
3

More than 3 years have passed since last update.

"春に咲く花の画像" を判別するAIアプリを作ってみた

Posted at

はじめに

この記事はプログラミング初心者の私がAidemyさんのAIアプリ開発コースの最終成果物として、AIによる画像認識アプリを作成する過程を記録したものです。

どんなジャンルにしようか悩みましたが、今回は春に咲く花のうち、10種類の花を判別するAIアプリを作成することにしました。

ちなみに花を選んだ理由としては、時期的にちょうど春の花が綺麗に咲いている時期であることと、"花の画像を見ることで癒し効果があることが科学的に示された" という論文を読んだことがきっかけです。(被験者数少ないんですけどね)

目次

1. 実行環境
2. BingのAPIを利用した画像収集
3. CNNモデルの作成・学習(初期)
4. 画像の水増し
5. CNNモデルの作成・学習(最終)
6. HTML、CSSのコード作成
7. FLASKのコード作成
8. アプリのデプロイ、動作確認
9. 考察、感想
10. 参考文献

1. 実行環境

  • Python 3.8.7
  • MacBook Pro (16-inch, 2019)(11.2.3)
  • Google Colaboratory
  • Visual Studio Code (1.55.1)

2. BingのAPIを利用した画像収集

まずは学習用の画像収集です。
今回使用する花は、アネモネ、スイートアリッサム、スズラン、チューリップ、ナデシコ、ハナニラ、ハナミズキ、ヒヤシンス、ラナンキュラス、サクラの10種類としました。

最初はBeautifulSoupseleniumを絡ませてスクレイピングで画像を集めようと思ったのですが、手っ取り早くBingのAPIを使うことにしました。
Azure登録後30日間は実質無料で利用できるようなので、これを活用しない手はないなと。

BingのAPIを使った画像の大量ダウンロードの方法は、以下のサイトを参考にしました。

(参考サイト)
PythonでBing Image Search API v7を使って画像収集する

各花の名前を検索キーワードとして200〜300枚ずつダウンロードしました。
ダウンロードした画像の一部です。
Ranunculus_origin.png

次に、明らかに関係のない画像などを除外し、花が中心になるよう正方形にトリミングし、各花約150枚ずつにまとめました。
Ranunculus_trimming.png

3. CNNモデルの作成・学習(初期)

kerasを使ってモデルを作成します。
各クラスの画像の枚数が多いとは言えないため、VGG16を使った転移学習で画像の少なさをカバーしたいと思います。
まずは以下のようなモデルで様子を見てみました。

model_1.py
# 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 = 'sigmoid'))
top_model.add(Dropout(0.5))
top_model.add(Dense(128, activation = 'sigmoid'))
top_model.add(Dropout(0.5))
top_model.add(Dense(12, 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, y_train, validation_data = (X_test, y_test), batch_size = 32, epochs = 50)

結果はこのようになりました。
水増し前のモデル精度.png

ちょっと小さくて見にくいですが、正解率は約68%です。
非常に心許ないので、画像データを水増しして正解率の上昇を試みることにします。

ちなみに何故正解率をプロットしたグラフを出さずにこんなに見にくい形でデータを示したかというと、単に可視化するためのコードを書き忘れただけです:smirk:

4. 画像の水増し

ImageDataGeneratorを使用して画像を水増しします。
引数で指定できる項目は複数ありますが、今回はrotation_rangehorizontal_flipshear_rangeの3種類を組み合わせて使いました。

ちなみに垂直方向の反転を使用しなかったのは、「スズランの花を逆さまに見るとチューリップに似ているため、判別しにくくなるのでは?」と考えたからです。

データを残していなかったのでここでは示せないのですが、何回か検証する中でスズランをチューリップに誤認識する傾向がありました。
この仮説については後日改めて検証したいと思っています。

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

    # 水増しを20回繰り返す
    while i < 20:
        datagen = ImageDataGenerator(rotation_range = 30, horizontal_flip = True, zoom_range = [0.9, 0.9], channel_shift_range= 70)
        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, 12)

    return X_extend, y_extend

img_add = img_extend(X_train, y_train)

各クラス20倍の枚数を水増しし、合計約25000枚としました。
汎化性能低下防止のため、トレーニングデータにのみ水増し用の関数を適用しています。

5. CNNモデルの作成・学習(最終)

オリジナル画像は各約150枚程度とはいえ、ある程度は水増ししたので最初の頃に比べたら精度が上がると思います。

ハイパーパラメータ等をいじって検証する過程で、以下のことがわかりました。

  • バッチサイズを8〜128の範囲で試したところ、8と16にした場合の精度がほぼ同程度に高く、学習時間等を考慮すれば16が最も効率が良い
  • 入力サイズを(50, 50)から(100, 100)にすると精度が高くなる
  • 入力サイズを(200, 200)にすると更に精度が高くなるが、学習にかなり時間がかかる
  • Dropoutを0.1にし、活性化関数をReLUにすると、sigmoid関数よりも精度のブレが小さくなる
  • 活性化関数をReLUにした上で、バッチ正規化を追加すると精度がやや上昇する
  • 上記を適用すると、Epochが20以内で精度がほぼ頭打ちになる

今回収集したデータに関しては、このような特徴があるようです。
以上のことを反映させ、最終的な全体のコードは以下のようになりました。

model_2.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

# パスに使用する用の花の名前リスト
flowers = ['アネモネ', 'スイートアリッサム', 'スズラン', 'チューリップ', 'ナデシコ', 'ハナニラ', 'ハナミズキ', 'ヒヤシンス',\
    'ラナンキュラス', 'サクラ']

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

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

# 画像ファイルの読み込み    
for n, flower_name in zip(range(10), flowers):
    numbers = len(make_path(flower_name))
    for i in range(numbers):
        img = cv2.imread('/content/drive/MyDrive/Spring_flower/{}/'.format(flower_name) + make_path(flower_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(10):
    img_list += pre_list[i]

# 正解ラベル格納リスト
labels = []
for flower_name in flowers:
    numbers = len(make_path(flower_name))
    for i in range(10):
        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, 10)

    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]])

# 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(512, activation = 'relu'))
top_model.add(Dropout(0.1))
top_model.add(BatchNormalization())
top_model.add(Dense(512, activation = 'relu'))
top_model.add(Dropout(0.1))
top_model.add(BatchNormalization())
top_model.add(Dense(10, 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 = 16, epochs = 20)

# 学習モデルを保存
model.save("flowers_10.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 = flowers, output_dict = True)
    return rep

rep_dict = Report(X_test, y_test)

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

# プロット用にSeriesを作成
F_dict = dict(zip(flowers, 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()  

val.png
F.png
正解率は約86%でした。
できれば90%を超えて欲しかったのですが、それでも最初の正解率と比べたら悪くはないと思います。

ただ、F値のバラつきが気になります。
本来は原因を追究して必要に応じて数値の改善を図りたいところですが、時間も限られているので今回はこのまま続行します。

6. HTML、CSSのコード作成

ウェブアプリのアプリ側は出来上がったので、これからウェブサイト側を作っていきます。
全体のデザインとしてはAidemyさんのAIアプリ開発コースで練習として作成した数字認識アプリのデザインをベースにし、少しアレンジを加えていこうと思います。

今回作成するのは春の花の種類を判定するアプリなので、桜っぽいイメージのカラーでシンプルなデザインにすることにします。

フリー画像素材をダウンロードできるpixabayで桜っぽい画像を探したところ、綺麗な画像を見つけたので、これをbackground-imageとして使用することにします。

このトップ画像に文字でタイトルを入れてもいいのですが、どうにも気に入らなかったので、Photoshopで画像に文字を埋め込み、手頃なサイズに成形しました。
top_img.jpg

ヘッダーメニューにはHOMEに戻ることができるリセットボタンと、今回のアプリで識別可能な花の種類を記載したページ、花の画像に関する豆知識的な情報を紹介したページが別ウインドウで開くリンクをそれぞれ載せます。

AIが判定した答えは文字色をヘッダー・フッターのbackground-colorと同じ色にし、ファイル選択ボタンや送信ボタンの下に配置しました。

エラー判定時は赤っぽい目立つ文字色で、同じくボタンの下に配置しました。

こちらがトップページのコードです。

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>春に咲く花を判定するAI(10種類対応)</title>
    <link rel="stylesheet" href="/static/index.css">
</head>
<body>
    <header>
        <div>
            <a class='home' href="{{url_for('upload_file')}}">
                リセット
            </a>
            <a class='variety' href="{{url_for('f_type')}}" onClick="window.open('./templates/f_type.html','windowname','scrollbars=yes,width=550,height=450'); return false;">
                判別できる花の種類    
            </a>
            <a  class='potency' href="{{url_for('potency')}}" onClick="window.open('./templates/f_type.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.Ohshimo</small>
    </footer>
</body>
</html>
index.CSS
body{
    max-width: 1920px;
    width: 100%;
    padding: 0px 0px;
    margin: 0 auto;
    background-color: rgb(242, 231, 243);
    display: flex;
    flex-direction: column;
    min-height:100vh;
}

header{
    height: 40px;
    width: 100%;
    background-color: rgba(138, 36, 92, 0.9);    
}

.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: auto;
}

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: rgba(138, 36, 92, 0.9);
    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: rgba(138, 36, 92, 0.9);
    border-top: 3px ridge;
    text-align: center;
    height: 10px;
    width: 100%;
    padding: 0px 3px 10px 3px;
    margin-top: auto;
}

small{
    color: #fff;
}

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

f_type.index
<!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/f_type.css">
</head>

<body>
    <h2>
        判別できる花は、以下の10種類です。
        <br>
        すべて春に咲く花です。
    </h2>
    <div>
        <ul>
            <li>アネモネ</li>
            <li>スイートアリッサム</li>
            <li>スズラン</li>
            <li>チューリップ</li>
            <li>ナデシコ</li>
            <li>ハナニラ</li>
            <li>ハナミズキ</li>
            <li>ヒヤシンス</li>
            <li>ラナンキュラス</li>
            <li>サクラ</li>
        </ul>
    </div>    

    <a href="#" onclick="window.close()">[閉じる]</a>
</body>
</html>
f_type.CSS
body{
    background-color: rgb(245, 250, 230);
    text-align: center;
}

h2{
    text-align: center;
}

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

li{
    list-style: none;
}

こちらが豆知識的情報を記載したページのコードです。

potency.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/potency.css">
</head>

<body>
    <h2>
        花の画像に秘められた可能性
    </h2>

    <p class='subject'>
        農業・食品産業技術総合研究機構の研究者らが2020年に発表した研究によると、
        <br>
        花の画像を見るだけでコルチゾール(ストレスホルモン)の濃度を低下させたり、
        <br>
        ネガティブな情動を抑制する効果が期待できるようです。
        <br>
        <br>
        このメカニズムとして、花の画像を見ることでディストラクション効果(気そらし効果)というものが働き、その結果としてストレス軽減につながると考察されています。
        <br>
        身近に咲いている春の花をスマートフォンなどの待ち受けにし、癒し効果を感じてみてはどうでしょうか?
    </p>

    <p class='citation'>
        【参考文献】
        <br>
        Hiroko Mochizuki-Kawai, et al., Journal of Environmental Psychology 2020 70;101445.  
    </p>

    <a href="#" onclick="window.close()">[閉じる]</a>
</body>
</html>
potency.CSS
body{
    background-color: rgb(242, 231, 243);
    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;
}

おそらくもっと良いコードの書き方があるのだと思いますが、ローカル環境で確認した限りでは問題なさそうなので、今回はこれでいきます。

トップページはこんな感じです。
トップページ.png

7. FLASKのコード作成

HTML、CSSのコード同様、基本的にはAidemyさんのコースで練習として作成した数字認識アプリのコードをベースにし、必要な箇所を書き換えていきます。

画像識別後の解答として、答えとなる花の名前AIが判定した確率を載せることにします。
ついでに花の説明文があるとおもしろいと思ったので、解答の下に説明文を載せることにしました。
説明文はWikipediaを使うことにします。
答えとなる花のページに直接飛んで、解答の下にそれを表示するように設定することにしました。
それに伴いHTMLとCSSの方にも手を加えました。

main.py
import os
from flask import Flask, request, render_template
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('./flowers_10.h5')

@app.route('/', methods = ['GET', 'POST'])
def upload_file():
    error = None
    if request.method == 'POST':
        file = request.files['file']

        # 正しく画像ファイルが送信された場合 
        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)

            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()

            # 確率の表示
            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 = '', pred = '')

# 判別できる花の種類(別ウインドウ)
@app.route('/templates/f_type.html')
def f_type():
    return render_template('f_type.html')

# 花の画像が持つ可能性(別ウインドウ)
@app.route('/templates/potency.html')
def potency():
    return render_template('potency.html')


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

index.htmlmainクラス内に追加

        {% if pred == 0 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/アネモネ" width="900" height="600"></iframe>
        {% elif pred == 1 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/スイートアリッサム" width="900" height="600"></iframe>
        {% elif pred == 2 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/スズラン" width="900" height="600"></iframe>
        {% elif pred == 3 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/チューリップ" width="900" height="600"></iframe>
        {% elif pred == 4 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/ナデシコ" width="900" height="600"></iframe>
        {% elif pred == 5 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/ハナニラ" width="900" height="600"></iframe>
        {% elif pred == 6 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/ハナミズキ" width="900" height="600"></iframe>
        {% elif pred == 7 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/ヒヤシンス" width="900" height="600"></iframe>
        {% elif pred == 8 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/ラナンキュラス" width="900" height="600"></iframe>
        {% elif pred == 9 %}
            <iframe class='wiki' style="display: block; margin: 0px auto;" src="https://ja.wikipedia.org/wiki/サクラ" width="900" height="600"></iframe>
        {% elif error %}
            <p class='error_message'>Error: {{ error }}</p>
        {% endif %}

index.CSSに追加

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

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

8. アプリのデプロイ、動作確認

herokuにアプリをデプロイします。

このherokuへのデプロイが個人的には鬼門です。
何故かというと、デプロイしようとすると毎回必ず何かしらのエラーが出るからです。

今回まず最初に苦労したのは以下のエラーです。

Compiled slug size: 528.5M is too large (max is 500M)

サイズが大き過ぎるようです。
どうにかサイズを下げようと、ちょこまかと色々やってみましたがラチがあかず、結局以下の情報を頼りにtensorflow-cpu 2.4.1をインストールし、サイズを下げることができました。

(参考サイト)
Herokuでデプロイする際に出たSlug Sizeエラーの解決方法

無事にデプロイできたので、あとは動作確認です。
(手順には記載していませんが、ローカル環境で一度確認はしています。)

2種類の別ウインドウの表示、エラーログの表示は動作確認できました。
しかしここで重大な問題が。

肝心な画像識別ができないのです。
画像を選んで送信ボタンを押すと、結果が表示されるどころか、
internal server error.png
となり、しばらく悪戦苦闘するも解決策が見つからず、絶望の縁に立たされます。
これがheroku絡みで嵌まったもう1つのエラーです。

かなり長いこと悩んだ末、ふとheroku logs --tailを確認すると、

No such file or directory: '/uploads/xxx.jpg

のエラーログが。(xxxは動作確認でアプリに送信した画像ファイル名です)
つまり、Gitで追加したはずのuploadsディレクトリが存在していないということになります。

確認のため一度アプリを削除し、改めてデプロイし直すことにしたところ、commitの段階で気付きました。
確かにuploadsディレクトリがない。
Git_commit.png

これには驚きました。
アプリ用のファイルをすべて格納したディレクトリ全体を扱っているため、こんなことがあるとは思ってもいませんでしたから。

原因が判明したため、いつもここぞという場面で力を貸してくれるAidemyのチューターさんの助言もあり、uploadsディレクトリに適当なファイルを入れたところ、無事にGitに認識されました。

というわけで、ようやく画像認識の動作確認です。

まず、F値が最も低かったラナンキュラスの画像を使ってみたいと思います。
6.jpg

結果
ラナンキュラスを認識.png

もう1つ見てみます。
1.jpg

結果
ラナンキュラスを認識2.png

2つとも正解しました。
あくまでも推測ですが、もしかすると適合率はそこそこではあるものの再現率が低く、その結果としてF値が低くなっているのかもしれません。

ラナンキュラスの次にF値が低かったハナミズキの画像も見てみたいと思います。
スズランと間違えられたハナミズキ.jpg

結果
スズランに誤認.png

なんでだよ!
見て分かれ!!

と言いたいところですが、スズランは白い花が複数ぶら下がっている画像をたくさん使いました。
おそらく白いハナミズキの花が2つ並んでいて、花びら1つ1つをそれぞれスズランの花だと誤認したのでしょう。

2つある花のうち、1つをトリミングしたところ、ちゃんとハナミズキと認識しました。
ハナミズキと認識した.jpg

結果
トリミングで認識.png

最後に桜の花を使って、動作確認を終えたいと思います。
春の花といえば何といっても桜です。
この記事を書いている時にはもうほとんど散ってしまっていましたが、このために予め画像を撮っておきました。

桜の画像については花というより桜の全体像を写した画像が多かったため、この画像をどう判定するか楽しみです。
IMG_4757.JPG

結果
桜を認識.png

ちゃんと桜と認識してくれました。

これで動作確認は終了です。
AIが導き出した答えと確率が表示され、Wikipediaの該当ページも無事に表示されたため、これで完成とします。

(アプリのリンク)
春に咲く花を判定するAI(10種類対応)

9. 考察、感想

やはりオリジナル画像が少なかった影響か、雰囲気的に別の花に似ている画像を誤認することが割と多かったように思います。
ハナミズキ、アネモネ、ラナンキュラス、ハナニラ、ナデシコは極めて大雑把に見れば花の形が似ており、サクラとスイートアリッサムは塊としてのフォルムが似ています。

ヒヤシンスは全体のフォルムに特徴がありますが、花1つをとってみれば前述の5種類の花と間違えても不思議ではありません。
塊という点で考えれば、スイートアリッサムの1つの塊に似ているとも言えます。

重要な特徴でも、それが細かい部分だとすると、オリジナル画像が少ないとなかなか掴み切れないと思うので、微妙な雰囲気の花を識別するのに苦戦したのかもしれません。

人間の目で見れば細かい特徴がパッと目に付きますが、AIにとっては情報が少なかったのだろうと思いました。

また、混同行列を計算する関数を書いてF値を評価指標の1つとして使用しましたが、再現率と適合率も確認すべきだったと思います。
それぞれの画像について取りこぼしが多いのか、それとも過剰に検知してしまうことが多いのかがわかれば、精度の限界となっている部分をもう少し詳しく推測できたと思います。
今回はこれでいったん完成としましたが、より精度を高めるために改良する際には参考になるはずなので、これから新たなモデルを作る際には混同行列をもっと有効活用していきます。

画像の水増しの際に選んだ変換の方法、活性化関数、ハイパーパラメータやモデルの層の深さなど、この辺りの組み合わせが精度にどのぐらい影響を与えたかも非常に気になるところです。
今後は既存のモデルを参考にしつつ自分で新たに検証することで、知見を深めていきたいと思います。

HTML、CSS、FLASKについては最低限のことしかやっていないため、更に勉強して改善していきたいですね。

もっと高い精度を出したかったとはいえ、ある程度の精度は出せたので良い練習になりました。
イラつくエラーがたくさんありましたが、やはり "作る" ことは楽しいです。
今後も絶対に続けていきたいと思います。
大変ではありますが、自分にとってこんなに楽しいことはなかなかないですから:smile:

10. 参考文献

今回のウェブアプリ作成の際に参考にしたサイトで、本文内でまだ紹介していないサイトです。
本当に助かりました。
ありがとうございました。

ページの中に他のHTMLを読み込んで表示する
【Flask】Jinja2の制御構文(if, for in)でクライアントサイドを柔軟に簡素化する
Flaskでクライアントサイドに変数(値・リスト・辞書型配列)を渡して表示させる
【sklearn】Classification_reportの使い方を丁寧に
Macにおけるmatplotlibの日本語表示
【二郎系か家系か!?】ラーメンを判別するAIを作ってみた!🍥【画像認識】

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3