catch-img

馬とシマウマの判別器をWebアプリにしてみた!

お久しぶりです!アイデミーの渡部です!

前回は、画像認識の技術を使った馬とシマウマの判別器を作成しました。

今回は、その際に作ったモデルを利用して、簡単なWebアプリを作ってみようと思います! 具体的には、ユーザーが画像をアップロードしたら、それに対して「AIが馬かシマウマどっちなのか判別してくれる」というような機能をWebアプリとして実装します。

さらに、Webアプリらしく外観の装飾も行い、最終的には作成したアプリをデプロイしたいと思います!

アプリを作成するのが初めての経験なので、どうか今回もお手柔らかに見ていただけたら嬉しいです!

目次[非表示]

  1. 1.開発環境
  2. 2.ディレクトリ構成
  3. 3.Flaskサイド制作
  4. 4.動作確認テスト
  5. 5.デプロイ
  6. 6.Herokuの設定
  7. 7.エラーにハマった話
    1. 7.1.エラー1(Slug Sizeエラー)
    2. 7.2.エラー2(tensorflowのバージョン変化によるエラー)
  8. 8.最終テスト
    1. 8.1.テスト1(馬の画像)
    2. 8.2.テスト2(シマウマの画像)
    3. 8.3.テスト3(馬の画像)
  9. 9.まとめ

今回、PythonでWebアプリを作成するにあたって、Flaskを使いました。Flaskとは、Pythonを使ってWebアプリを作成するための軽量ウェブアプリケーションフレームワークで、小規模なWebアプリケーションを作るのに適しています。

こういったウェブアプリケーションフレームワークを使うことで、使わない時よりも容易にWebアプリケーションを制作できるという大きなメリットがあります。

開発環境

今回、開発を行った環境は以下です。

  • MacOS Catalina 10.15.7
  • Visual Studio Code 1.52.1
  • Python 3 3.6.3
  • Flask 1.0.2

ディレクトリ構成

最終的なディレクトリ構成は以下のようになります。

(animal_app)
├── main.py
├── uploads
├── templates
│   └── index.html
├── static
│   └── stylesheet.css   
├── Procfile  
├── requirements.txt
├── runtime.txt   
└── animal_cnn.h5

「main.py」はメインのプログラムです。ここに、ユーザーが画像を送信したり、それに対してAIが判別したりするといったような、今回のWebアプリのメインとなる機能を実装します。

「uploads」フォルダには、ユーザーがアップロードした画像が保存されます。

「templates」フォルダには「index.html」ファイルを、「static」フォルダには「stylesheet.css」ファイルを配置します。これらは、Webアプリの外観を作るためにあります。

「Procfile」は、Herokuにて、Webアプリを実行するためのコマンドを記述しています。

「requirements.txt」は、動作に必要なライブラリを記述しています。これにより、Herokuの自分のアプリ内にそのライブラリがインストールされます。

「runtime.txt」には使用するPythonのバージョンを記述しています。

最後に、「animal_cnn.h5」は、前回作成した学習済みモデルです。

Flaskサイド制作

さて、Flaskサイドの制作です。ここでは、ユーザーがアップロードした画像を受け取り、それに対して前回作成済みの学習モデルが馬かシマウマかを識別し、その結果を表示するという機能を実装しました。

▽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 = ['horse', 'zebra']
img_size = 64

#アップロードされた画像を保存するフォルダ名とアップロードを許可する拡張子
UPLOAD_FOLDER = "uploads"
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

#Flaskクラスのインスタンスの作成
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=True, target_size=(img_size,img_size))
            img = img.convert('RGB')
            #画像データを64 x 64に変換
            img = img.resize((img_size, img_size))
            # 画像データをnumpy配列に変換
            img = np.asarray(img)
            img = img / 255.0
            result = model.predict(np.array([img]))
            predicted = result.argmax()
            pred_answer = "これは " + classes[predicted] + " です"

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

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


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

HTML&CSSサイド制作

「index.html」、「stylesheet.css」は、共にWebアプリの外観を作成するためにあります。

ここで、htmlとcssの役割について簡単に説明します。htmlは、Webページに表示される実際のテキストや画像を記述するという役割です。そして、cssは、htmlで記述したテキストや画像の見栄えを良くするための装飾をする役割を持ちます。

▽index.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <link rel="icon" type="img/x-icon" href="../static/favicon.jpeg">
    <title>Animal Classifier</title>
    <link rel="stylesheet" href="../static/stylesheet.css">
</head>
<body>
    <header>
        <p class="header-title">Animal Classifier|馬かシマウマかAIが判定!</p>
    </header>

    <div class="wrapper">
        <h2 class="wrapper-title"> AIが送信された画像を馬かシマウマか判別します</h2>
        <p class="wrapper-comment">画像を送信してください</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 class="up_img">
            {% if filepath %}
                <img class="filepath" src={{filepath}} height="200">
            {% endif %}
        </div>
    </div>

    <footer>
        <small class="last-text">『馬とシマウマの判別器をWebアプリにしてみた!』より</small>
    </footer>
</body>
</html>

▽stylesheet.css


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

.header-title {
    color: #F7F7F7;
    font-size: 25px;
    margin: 15px 25px;
    position: absolute;
    left: 0;
}

.header_img {
    height: 25px;
    margin: 15px 25px;
}

.wrapper {
    min-height: 400px;
}
.wrapper-title {
    color: #444444;
    margin: 90px 0px;
    text-align: center;
}

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

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

.filepath {
    margin: 0 auto;
    display: block;
}

form {
    text-align: center;
}

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

.last-text {
    margin: 15px auto;
    display: block;
    width: fit-content;
    line-height: 80px;
}

今回アプリを作る際に、外観に少しこだわってみたかったのですが、デザインを考えるということと、それを反映させるということが難しく、最終的なデザインはとてもシンプルになりました。

▽Webアプリの機能と外観がひとまず完成!


動作確認テスト

さて、Flaskサイドとアプリの外観の制作が完了したので、Webアプリが正しく動作するかというテストを行ってみます。あくまで動作の確認のためのテストなので、以下の画像を1枚用意してアップロードしてみます。そして、判別を行って結果を表示してくれるのか確認します!

テストする画像は馬ですが、ここでは、正しく馬と表示されなくても良いです。

では、以下の優しそうな馬の画像でテストしてみます!

▽テスト画像

▽テスト結果

テストをしてみた結果、正しく動作することがわかりました!また、AIも正しく判別できているのが嬉しいですね!

デプロイ

正しく動作することが分かったので、作成したアプリをデプロイをしたいと思います!

コードの一部を書き換え

デプロイの手順として、まずは、animal.pyのコードの一部を書き換えます!

main.pyの末尾の

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

を以下に書き換えます。サーバーを外部からも利用可能にするためにhost='0.0.0.0'と指定します。

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

Herokuの設定

今回、開発したアプリを公開するために、Herokuというサービスを利用します。本来、Webアプリを公開するためには、アプリの開発以外にも、サーバーやデータベースなどの準備をする必要がありますが、このHerokuというサービスを使えば、10分ほどで簡単に公開することができます。さらに、制約はありますが、1サービスであれば無料で公開することができるので、とてもおすすめです。

利用するには、Herokuの会員登録、ログイン、Heroku Toolbelt (CLI)のインストールなど事前に各種設定が必要です。

以下で、Herokuへのデプロイ方法を解説します。

cd animal_app

まず、アプリのファイルのあるディレクトリに移動します。

heroku login

つぎに、このコマンドを実行し、指示された通りにメールアドレスやパスワードを入力します。

その後、Herokuに移動し、Create New App をクリックし、 App name(私はanimal--classifierにしました)を入力し、国をUnited Statesで登録します。

アプリのページに遷移しSettingsに移動、Add buildpackをクリックしてPythonを追加します。

git init
heroku git:remote -a animal--classifier
git add .
git commit -m “変更内容について記述”
git push heroku master
heroku open

その後、このコマンドを入力するとwebアプリが起動しているURLに移動できます。

動作が確認できればデプロイ成功です!

エラーにハマった話

今回、デプロイのところまではスムーズに進められていたのですが、デプロイのところでエラーにハマってしまいました!大きくハマったエラーは次の2つです。

エラー1(Slug Sizeエラー)

まず、pushした時点で以下のようなエラーが発生しました。

▽Slug Sizeエラー

先ほど、Herokuには、1サービスを無料で公開できるが制約があるとお伝えしました。このエラーは、その制約の一つが関係しているもので、Slug Sizeを500MB以内に抑えることで解決できます。

Slug Sizeは、使用している言語とフレームワーク、追加した依存関係の数、およびアプリ固有のその他の要因によって大きく異なります。

こちらのサイトを参考にさせていただいたところ、tensorflowのバージョンが2.x.xだとSlugSizeが飛躍的に大きくなってしまうようでした。

自分の「requirements.txt」を確認したところ、確かに以下のように指定していました。

tensorflow==2.4.0

そのため、tensorflowのバージョンを以下のように指定し直すことで対応をしました。

tensorflow==1.13.1

結果的に、Slug Sizeは592.9Mから305Mまで下げることができ、エラー1を解消することができました!

エラー2(tensorflowのバージョン変化によるエラー)

このエラーは、上記のエラー1を解消した後、すぐに起こりました。

pushすると正常にページが表示されたのですが、画像をアップデートして別のページに遷移する際に、Internal Server Errorが出てしまいました。

heroku logs --tail

上記のコマンドにて、エラー内容を確認したところ、以下のエラーが確認できました。

ValueError: Tensor Tensor("activation_24/Softmax:0", shape=(?, 2), dtype=float32) is not an element of this graph.

こちらのエラーは、結果的にtensorflowのバージョンを2.x.xから1.x.xに下げたことによるものだとわかりました。具体的には、main.pyのupload_file関数における処理が原因のようです。

そのため、main.pyを先ほどのコードから以下のように変更したところ、エラー2も解消することができました! tensorflowのバージョンによって、upload_file関数の処理の書き方を変える必要があるのですね。

▽main.py

import os
from flask import Flask, request, redirect, url_for, render_template, flash
from werkzeug.utils import secure_filename
from keras.models import Sequential, load_model
from keras.preprocessing import image
import tensorflow as tf
import numpy as np

#分類したいクラスと学習に用いた画像のサイズ
classes = ['horse', 'zebra']
img_size = 64

#アップロードされた画像を保存するフォルダ名とアップロードを許可する拡張子
# UPLOAD_FOLDER = "uploads"
UPLOAD_FOLDER = "static"
ALLOWED_EXTENSIONS = set(['png', 'jpg', 'jpeg', 'gif'])

#Flaskクラスのインスタンスの作成
app = Flask(__name__)

#アップロードされたファイルの拡張子のチェックをする関数
def allowed_file(filename):
    return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

#学習済みモデルをロード
model = load_model('./animal_cnn.h5')


# こちらのコメントアウトしているところがエラーの原因になっていたところです。
# @app.route('/', methods=['GET', 'POST'])
# def upload_file():
#     if request.method == 'POST':

# こちらに変更しました。
graph = tf.get_default_graph()
@app.route('/', methods=['GET', 'POST'])
def upload_file():
    global graph
    with graph.as_default():
        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)


                img = image.load_img(filepath, grayscale=False, target_size=(img_size,img_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,filepath=filepath)

    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)
# if __name__ == "__main__":
#     app.run()

最終テスト

途中、エラーでつまずいてしまいましたが、無事にデプロイまで成功したので、実際にデプロイしたアプリに馬とシマウマの画像をアップロードしてみて、どんな判別をしてくれるのかテストしてみようと思います!

テスト1(馬の画像)

まずは、馬の画像から!

残念ながら、テスト1はハズレてしまいました!

テスト2(シマウマの画像)

次にシマウマの画像でテストしてみます!



ちゃんと判別できました!

テスト3(馬の画像)

それでは、もう一度、馬の画像でテストしてみます!

よかったです!ちゃんと判別してくれました!

まとめ

今回、初めてWebアプリを制作してみましたが、とても良い経験になりました! 本来ならサーバーなどの知識がないと作れないであろうWebアプリも、Herokuを利用することで、途中、エラーでつまずきはしましたが、最後まで完成させることができました!Herokuの背景でどんなことが行われているかなど、いつか勉強してみたいです! プログラミングは本当に奥が深いですね!

▽今回制作したWebアプリ

https://animal---classifier.herokuapp.com/


PythonやAIプログラミングを学ぶなら、オンライン学習サービスのAidemy Premium Plan

「機械学習・ディープラーニングに興味がある」

「AIをどのように活用するのだろう?」

「文系の私でもプログラミング学習を続けられるだろうか?」

少しでも気になることがございましたら、ぜひお気軽にAidemy Premium Planのオンライン無料相談会にご参加いただき、お悩みをお聞かせください!

このほかにも、Aidemy MagazineとTwitter(@AidemyMagazine)ではたくさんのAI活用事例をご紹介しています。どちらも要チェック!