https://kindpark.jpn.org/dogemotion/

こんなウェブアプリを作りました。犬の写真から表情を見て、感情を推測するAI(convolutional neural network)です。データサイエンスの観点からは、簡単に機械学習モデルを使えるインターフェイスを作ることはとても意義のあるものだと思います。獣医学的には、あまり意義…。とはいえ、できるまでとても大変だったので、その過程をメモしておきます。

概念図

デプロイする前

デプロイした後

FastAPI

pythonでサーバーを扱うフレームワークには、django、flask、fastAPIがあります。flaskも扱ったことありますが、fastAPIはデバッグがしやすいですね。uvicorn server立ち上げたあとに、サーバーのアドレスの後ろにdocsを付けてアクセスすると簡易的なインターフェイスが立ち上がります。curl使ってpostとかやらなくても、簡易的なインターフェイスでget、postを試すことができるのでとてもありがたいです。

Heroku

無料でAPIを公開させてくれるサーバーです。もちろん制限はありますが、無料というのがありがたいです。pythonは、基本のサーバー用のソフトapacheやnginxに加えてuvicornやgunicornといったソフトが必要になります。uvicornやgunicornを常に起動させておく必要があるのですが、それを許してくれるレンタルサーバーにはお金を積まないといけないようです。最低価格帯の共同レンタルサーバーでもできることはできるそうなのですが、かなり大変みたいです。

機械学習モデルをherokuにデプロイするに当たって大変なことは、容量制限です。tensorflowでneural networkのモデルを作ってherokuにアップロードすると工夫しない限り Compiled slug size: 700.5M is too large (max is 500M) とか言われてエラーになっていまいます。そこを乗り越えた方法もメモっておきます。

私のモデルはAutokerasで作成したモデルで、autokerasとtensorflowを両方読み込ませないとh5モデルを読み込めませんでした。なぜかtensorflow-lite用のモデルに変換すると、tensorflow単独で読み込めるようになるのでそれを利用してautokerasをそぎ落としました。

レンタルサーバー

Herokuにデプロイして使えるようになったAPI。しかしAPIを叩くインターフェイス(ウェブページ)を用意しないと、プログラマー以外はAPIにアクセスできないでしょう。そのため、レンタルサーバーでホームページを作り、そこからjavascriptを使ってAPIに対してpostします。

デプロイ前の壁(主にfastAPIの使い方)

  • HTMLからfastAPIへの画像をPOSTするやり方は?
  • fastAPIで画像をどうやって受け取ってtensorflowに読ませる?
  • fastAPIで画像解析してその結果をどうやってHTMLが受け取る?

デプロイ時の壁(Herokuの使い方)

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

デプロイ後の壁(FastAPIの設定の仕方)

  • No ‘Access-Control-Allow-Origin’ header is present on the requested resource
  • 30分APIを叩かないとAPIが寝てしまい、次にPOSTした際の初回は反応がない。

基本的なFastAPIの使い方

仮想環境に入り、以下のコマンド。

pip install fastapi
pip install uvicorn[standard]

pythonファイル内に、以下のテンプレートを使ってかく。まずFastAPI()でアプリオブジェクトを作成。その後、アプリオブジェクトに関数を追加。@app.get(“/”) ルートディレクトリにGETのリクエストが来た場合の処理を@の下に関数として書き込む。

from fastapi import FastAPI
app = FastAPI()

@app.get("/")
async def root():
    return {"message": "Hello World"}

コマンドラインから仮想環境内に入り、main.pyがあるフォルダ内で以下のコマンドを実行することでuvicornサーバーを立てることができる。mainは、pythonのファイル名。–reloadは、ファイルに変更があるときに、変更がリアルタイムでされる。これがないと、ファイル変更するたびにサーバーを再起動しないといけないのでめんどくさいです。

uvicorn main:app --reload

以下のアドレスでサーバーが立ち上がります。

http://127.0.0.1:8000

たぶんエラー的なものがでていると思いますが、気にしないでください。getしてないので当然です。

以下のアドレスで、fastAPIが自動的に作成するインターフェイス(swagger UI)がでてきます。ここでget・postを試したりできるのでデバッグできます。特に、pythonファイル内のアルゴリズムがおかしいのか、それとも通信系がおかしいのかの切り分けに役に立ちます。ここがfastAPIの良い機能です!

http://127.0.0.1:8000/docs
HTMLからfastAPIへの画像をPOSTするやり方は?

いくつかあるみたいですが、jQueryを使いました。まず、index.htmlのインターフェイスのtagたちです。ファイルのアップロードを可能にします。

<form method="post" action="" enctype="multipart/form-data" class="mycss">
	<input type="file" id="file" name="file" accept="image/*" />
	<input type="button" class="button" value="Upload" id="but_upload">
</form>

上記のHTMLが操作されたときにJavascriptです。アップロードされた画像を宛先のアドレスにpostします。

$(document).ready(function () {

    $("#but_upload").click(function () {

        var fd = new FormData();
        var files = $('#file')[0].files;

        // Check file selected or not
        if (files.length > 0) {
            fd.append('file', files[0]);

            $.ajax({
                url: 'https://~~宛先のアドレス',
                type: 'post',
                data: fd,
                contentType: false,
                processData: false,
                success: function (response) {
                    if (response != 0) {
                        $("#prediction").text(response) 
                    } 
                },
            });
        }
    });
});

上記のファイルを作っていざ送信だと思っても、以下のようなエラーが起こりました。

Access to XMLHttpRequest at ‘http://127.0.0.1:8000/’ from origin ‘null’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

これは、htmlとjavascriptのサーバーを立ててから、uvicorn(fastAPI)に送らないといけません。つまり、uvicornでサーバー立てて、htmlとjavascriptのサーバー立ててと、2つのサーバーを立てて通信させることになります。

htmlとjavascriptのサーバーを立てるのは、visual studio codeならlive serverというエクステンションで一発です。vs codeでフォルダ開いて、F1押してstart live serverです。

fastAPIで画像を受け取ってどうやってtensorflowに読ませる?

画像を読み込ませるためには、追加のパッケージが必要です。

pip install python-multipart

pythonファイルに書き込みます。ファイルを受け取るときの関数の書き方は、async def create_upload_file(file: UploadFile = File(…)): です。

ファイルを受け取ったら、contents = await file.read() でファイルを読み込みます。

BytesIOというので受け取り、それをpillowで読み込むとうまくいきます。

from fastapi import FastAPI, File, UploadFile
app = FastAPI()

from io import BytesIO
from PIL import Image 
from tensorflow.keras.preprocessing.image import img_to_array
import numpy as np
from tensorflow.keras.models import load_model

@app.post("/")
async def create_upload_file(file: UploadFile = File(...)):

    contents = await file.read()
    im = Image.open(BytesIO(contents))
    img = im.resize((224,224))
    img = img_to_array(img)
    img = img.reshape(224, 224, 3)
    img = img.astype('float32')/255
    
    X_trial = []
    X_trial.append(img)
    X_trial = np.array(X_trial, dtype='float') 

    model = load_model("model.h5")
    pred = model.predict(X_trial)   

    classlist = ["Happy","Angry","Neutral","No energy"]
    result = classlist[np.argmax(pred)]

    return result

fastAPIで画像解析してその結果をどうやってHTMLが受け取る?

さきほどのpostリクエストの下のほうに、successという項目があります。ここにresponseが来た時の処理を書き込みます。

$(document).ready(function () {

    $("#but_upload").click(function () {

        var fd = new FormData();
        var files = $('#file')[0].files;

        // Check file selected or not
        if (files.length > 0) {
            fd.append('file', files[0]);

            $.ajax({
                url: 'https://~~宛先のアドレス',
                type: 'post',
                data: fd,
                contentType: false,
                processData: false,

/* ここにresponseが来た時の処理を記載する */
                success: function (response) {
                    if (response != 0) {
                        $("#prediction").text(response) 
                    } 
                },
            });
        }
    });
});

Herokuのデプロイの仕方

gitを使えるようにしておきます。

フォルダ内に、main.pyとtensorflowのモデルh5ファイルの2つしかない状態にします。

仮想環境にいる状態で以下のコマンドを実行してください。3ファイル作ります。計5ファイルできます。

pip freeze > requirements.txt

echo web: uvicorn main:app --host=0.0.0.0 --port=${PORT:-5000} > Procfile

echo python-3.8.11 > runtime.txt

1つめは、仮想環境で使っているライブラリを記録します。このファイルをつかってherokuがpipしてパッケージをインストールするそうです。

2つめは、サーバーの設定です。

3つめは、pythonのバージョンを記載します。2021年7月現在以下のランタイムが選べるようです。

python-3.9.6
python-3.8.11
python-3.7.11
python-3.6.14

herokuにアカウントを作成して、CLIツールをダウンロードします。さきほどの5ファイルあるフォルダ内に移動して、コマンドラインから以下のコマンドを実行します。

git init
git add .
git commit -m "first"

heroku login
heroku create
heroku apps:rename myappname
git push --set-upstream https://git.heroku.com/〇〇.git master
heroku git:remote -a myappname
heroku open

デプロイ時の壁(Herokuの使い方)

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

上記のようなエラーがでませんでしたか?パッケージがでかすぎると。私の場合は、autokerasのモデルを使っていたのでautokerasとtensorflowの両方入りのどでかいパッケージになっていました。

Tensorflow-cpu & Tensorflow-Lite

tensorflowを普通にpipするとCPUだけでなくGPUを使うライブラリもついてきます。herokuではGPUを使わないので、CPUのライブラリだけでいいです。pip install tensorflow-cpuとするとGPUライブラリがついてこないのでかなりサイズが小さくなります。

それでもサイズがでかい場合には、tensorflow-Liteをお勧めします。これは、モバイル端末等で使うことを想定されているものです。ただ、pip install “URL”でインストールするのでherokuで使えるか分かりません。その上、h5ファイルをtflite方式にすればモデルのサイズが少し小さくなります。(200Mから190Mくらい)

autokerasのモデルは通常、読み込む際にautokerasのロードを必要としますがtensorflow-liteのモデルに変換しておけばtensorflow単独で読み込めるようになりました。

tensorflow-liteの使い方は、通常版と異なります。以下の記事の最後でh5ファイルからtfliteモデルに変更する方法、それからtfliteでpredictする方法を記載しています。

分かりにくかったかもしれないのでまとめると、基本的にはpip install tensorflowのかわりにpip install tensorflow-cpuにするだけでいいです。それでもだめなら、h5モデルを小さくします。

h5モデルをtflite形式で小さくした場合でも、pip install tensorflow-cpuだけで使えます。tensorflow-cpuの中にtensorflow-liteが入っています。

tensorflow-liteのみでデプロイできるか確認してません。

デプロイ後の壁(FastAPIの設定の仕方)

No ‘Access-Control-Allow-Origin’ header is present on the requested resource

このエラーはあるあるらしくて、色々なところに色々な回答がありますが、なかなかうまくいきません。これはfastAPIのmain.pyファイルに問題があるようです。どうやらpostする側の問題ではないようです。私の場合は以下のようにしたらうまくいきました。

最初は、allow_originsに許可する受信先を限定するように書き方をしていたのですが、どうにもうまくいかずに全許可(アスタリスク)に切り替えました。どこからでもAPIを叩けるようになるのでsecurity上良くないそうです。ただ、いろんな人に使ってほしい場合はこうしないとダメでしょう。allow_originsをアスタリスクにした場合、allow_credentialsをfalseにすることを忘れないでください。公式チュートリアルではtrueになっていてハマりポイントです。

from fastapi import FastAPI, File, UploadFile
app = FastAPI()

from fastapi.middleware.cors import CORSMiddleware

# origins = [
# "https://〇〇〇〇/"
# ]

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=False,
    allow_methods=["*"],
    allow_headers=["*"],
)

30分APIを叩かないとAPIが寝てしまい、次にPOSTした際の初回は反応がない

30分毎に叩き起こすしかないです。そんなことをやってくれるサイトがあります。

https://kaffeine.herokuapp.com/

自分のAPIを記入するだけです。やめたいときは、右下のremove your upp?のところから再度APIを記入すればやめれるようです。

無料版だと、APIの継続時間・起動時間に制限があるので気を付けて下さい!

category