PyTorch 1.8 チュートリアル : PyTorch モデル配備 : Flask REST API で PyTorch を配備する (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 04/29/2021 (1.8.1+cu102)
* 本ページは、PyTorch 1.8 Tutorials の以下のページを翻訳した上で適宜、補足説明したものです:
- Deploying PyTorch Models in Production : Deploying PyTorch in Python via a REST API with Flask
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
スケジュールは弊社 公式 Web サイト でご確認頂けます。
- お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。
- ウェビナー運用には弊社製品「ClassCat® Webinar」を利用しています。
人工知能研究開発支援 | 人工知能研修サービス | テレワーク & オンライン授業を支援 |
PoC(概念実証)を失敗させないための支援 (本支援はセミナーに参加しアンケートに回答した方を対象としています。) |
◆ お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。
株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション |
E-Mail:sales-info@classcat.com ; WebSite: https://www.classcat.com/ ; Facebook |
PyTorch モデル配備 : Flask REST API で PyTorch を配備する
このチュートリアルでは、Flask を使用して PyTorch モデルを配備してモデル推論のための REST API を公開します。特に、事前訓練された DenseNet 121 モデルを配備します、これは画像を検出します。
TIP: ここで使用される総てのコードは MIT ライセンスの元でリリースされ Github 上で利用可能です。
これは PyTorch モデルをプロダクションで配備する上でのチュートリアルのシリーズの最初のものを説明します。このように Flask を使用することは貴方の PyTorch モデルのサービスを開始するための最も容易な方法ですが、それは高パフォーマンスな要請を伴うユースケースのためには役立ちません。そのためには :
- もし貴方が既に TorchScript に馴染んでいるのであれば、Loading a TorchScript Model in C++ チュートリアルに一直線に飛び込むことができます。
- 最初に TorchScript 上の復習を必要とする場合、Intro a TorchScript チュートリアルを調べてください。
API 定義
最初に API エンドポイント、リクエストとレスポンス型を定義します。私達の API エンドポイントは /predict にあり、これは画像を含むファイル・パラメータを伴う HTTP POST リクエストを取ります。レスポンスは予測を含む JSON レスポンスになります :
{"class_id": "n02124075", "class_name": "Egyptian_cat"}
依存性
次のコマンドを実行することにより必要な依存性をインストールします :
$ pip install Flask==1.0.3 torchvision-0.3.0
単純な Web サーバ
次は単純な webserver です、Flask のドキュメントから取られました :
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello World!'
上のスニペットを app.py という名前のファイルにセーブすれば今では次のようにタイプして Flask 開発サーバを実行できます :
$ FLASK_ENV=development FLASK_APP=app.py flask run
web ブラウザで http://localhost:5000/ を閲覧すれば、”Hello World!” テキストで迎えられるでしょう。
上のスニペットに僅かな変更を行ないます、その結果それは私達の API 定義に適合します。最初に、メソッドを predict に名前変更します。エンドポイント・パスを /predict に更新します。画像ファイルは HTTP POST リクエストを通して送られますので、それが POST リクエストだけを受け取るように更新もします :
@app.route('/predict', methods=['POST'])
def predict():
return 'Hello World!'
レスポンス型もまた変更します、それが ImageNet クラス id と名前を含む JSON レスポンスを返すように。更新された app.py ファイルは今では次のようになります :
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/predict', methods=['POST'])
def predict():
return jsonify({'class_id': 'IMAGE_NET_XXX', 'class_name': 'Cat'})
推論
次のセクションで推論コードを書くことにフォーカスします。これは 2 つのパートを必要とします、一つは DenseNet に供給できるように画像を準備します、そして次は、モデルから実際の予測を得るためのコードを書きます。
画像を準備する
DenseNet モデルは画像がサイズ 224 x 224 の 3 チャネル RGB 画像であることを要求します。画像 tensor をまた必要な平均と標準偏差値で正規化します。それについては ここ で更に読むことができます。
torchvision ライブラリからの transforms を使用して変換パイプラインを構築します、これは画像を要求されるように変換します。transforms については ここ で更に読むことができます。
import io
import torchvision.transforms as transforms
from PIL import Image
def transform_image(image_bytes):
my_transforms = transforms.Compose([transforms.Resize(255),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
[0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
image = Image.open(io.BytesIO(image_bytes))
return my_transforms(image).unsqueeze(0)
上のメソッドは画像データをバイトで取り、transforms のシリーズを適用し、そして tensor を返します。上のメソッドをテストするには、画像ファイルをバイトモードで読み (最初に ../_static/img/sample_file.jpeg を貴方のコンピュータ上のファイルへの実際のパスで置き換えます)、そして tensor が返されるかを見ます :
with open("../_static/img/sample_file.jpeg", 'rb') as f:
image_bytes = f.read()
tensor = transform_image(image_bytes=image_bytes)
print(tensor)
tensor([[[[ 0.4508, 0.4166, 0.3994, ..., -1.3473, -1.3302, -1.3473], [ 0.5364, 0.4851, 0.4508, ..., -1.2959, -1.3130, -1.3302], [ 0.7077, 0.6392, 0.6049, ..., -1.2959, -1.3302, -1.3644], ..., [ 1.3755, 1.3927, 1.4098, ..., 1.1700, 1.3584, 1.6667], [ 1.8893, 1.7694, 1.4440, ..., 1.2899, 1.4783, 1.5468], [ 1.6324, 1.8379, 1.8379, ..., 1.4783, 1.7352, 1.4612]], [[ 0.5728, 0.5378, 0.5203, ..., -1.3704, -1.3529, -1.3529], [ 0.6604, 0.6078, 0.5728, ..., -1.3004, -1.3179, -1.3354], [ 0.8529, 0.7654, 0.7304, ..., -1.3004, -1.3354, -1.3704], ..., [ 1.4657, 1.4657, 1.4832, ..., 1.3256, 1.5357, 1.8508], [ 2.0084, 1.8683, 1.5182, ..., 1.4657, 1.6583, 1.7283], [ 1.7458, 1.9384, 1.9209, ..., 1.6583, 1.9209, 1.6408]], [[ 0.7228, 0.6879, 0.6531, ..., -1.6476, -1.6302, -1.6476], [ 0.8099, 0.7576, 0.7228, ..., -1.6476, -1.6476, -1.6650], [ 1.0017, 0.9145, 0.8797, ..., -1.6476, -1.6650, -1.6999], ..., [ 1.6291, 1.6291, 1.6465, ..., 1.6291, 1.8208, 2.1346], [ 2.1868, 2.0300, 1.6814, ..., 1.7685, 1.9428, 2.0125], [ 1.9254, 2.0997, 2.0823, ..., 1.9428, 2.2043, 1.9080]]]])
予測
今は画像クラスを予測するために事前訓練された DenseNet 121 モデルを利用します。torchvision ライブラリからの一つを利用し、モデルをロードして推論を得ます。このサンプルで事前訓練されたモデルを使用している一方で、貴方自身のモデルのために同じアプローチを利用できます。貴方のモデルをロードすることについてこの チュートリアル で更に見てください。
from torchvision import models
# Make sure to pass `pretrained` as `True` to use the pretrained weights:
model = models.densenet121(pretrained=True)
# Since we are using our model only for inference, switch to `eval` mode:
model.eval()
def get_prediction(image_bytes):
tensor = transform_image(image_bytes=image_bytes)
outputs = model.forward(tensor)
_, y_hat = outputs.max(1)
return y_hat
tensor y_hat は予測されたクラス id のインデックスを含みます。けれども、私達は可読なクラス名を必要とします。そのためクラス id から名前へのマッピングを必要とします。このファイル を imagenet_class_index.json としてダウンロードしてそれをどこにセーブしたか覚えておいてください (あるいは、このチュートリアルの正確なステップに従っている場合、それを tutorials/_static にセーブしてください)。このファイルは ImageNet クラス id の ImageNet クラス名へのマッピングを含みます。この JSON ファイルをロードして予測されたインデックスのクラス名を得ます。
import json
imagenet_class_index = json.load(open('../_static/imagenet_class_index.json'))
def get_prediction(image_bytes):
tensor = transform_image(image_bytes=image_bytes)
outputs = model.forward(tensor)
_, y_hat = outputs.max(1)
predicted_idx = str(y_hat.item())
return imagenet_class_index[predicted_idx]
imagenet_class_index 辞書を使用する前に、最初に tensor 値を文字列値に変換します、何故ならば imagenet_class_index 辞書のキーは文字列であるからです。上のメソッドをテストします :
with open("../_static/img/sample_file.jpeg", 'rb') as f:
image_bytes = f.read()
print(get_prediction(image_bytes=image_bytes))
['n02124075', 'Egyptian_cat']
このようなレスポンスを得るはずです :
['n02124075', 'Egyptian_cat']
配列の最初の項目は ImageNet クラス id でそして 2 番目の項目は可読な名前です。
Note: model 変数が get_prediction メソッドの一部ではないことに気が付いたでしょうか?あるいは model は何故グローバル変数なのでしょう?モデルのロードはメモリと計算の観点からは高価な演算であり得ます。モデルを get_prediction メソッドでロードした場合、メソッドが呼び出されるたびにそれは不必要にロードされるでしょう。私達は web サーバを構築していますので、毎秒数千のリクエストがあるかもしれませんので、総ての推論のためにモデルを余剰にロードする時間を浪費するべきではありません。そこで、モデルを一度だけメモリにロードして保持します。プロダクション・システムでは、大規模にリクエストにサービス提供できるためには計算の使用について効率的である必要がありますので、一般的にはリクエストにサービス提供する前にモデルをロードするべきです。
API サーバでモデルを統合する
この最後のパートでモデルを Flask API サーバに追加します。API サーバは画像ファイルを取るものと想定されていますので、リクエストからファイルを読むように predict メソッドを更新します :
from flask import request
@app.route('/predict', methods=['POST'])
def predict():
if request.method == 'POST':
# we will get the file from the request
file = request.files['file']
# convert that to bytes
img_bytes = file.read()
class_id, class_name = get_prediction(image_bytes=img_bytes)
return jsonify({'class_id': class_id, 'class_name': class_name})
app.py ファイルは今では完全です。次がフル・バージョンです ; パスを貴方がファイルをセーブしたパスと置き換えてください、そしてそれは動作するはずです :
import io
import json
from torchvision import models
import torchvision.transforms as transforms
from PIL import Image
from flask import Flask, jsonify, request
app = Flask(__name__)
imagenet_class_index = json.load(open('/imagenet_class_index.json'))
model = models.densenet121(pretrained=True)
model.eval()
def transform_image(image_bytes):
my_transforms = transforms.Compose([transforms.Resize(255),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(
[0.485, 0.456, 0.406],
[0.229, 0.224, 0.225])])
image = Image.open(io.BytesIO(image_bytes))
return my_transforms(image).unsqueeze(0)
def get_prediction(image_bytes):
tensor = transform_image(image_bytes=image_bytes)
outputs = model.forward(tensor)
_, y_hat = outputs.max(1)
predicted_idx = str(y_hat.item())
return imagenet_class_index[predicted_idx]
@app.route('/predict', methods=['POST'])
def predict():
if request.method == 'POST':
file = request.files['file']
img_bytes = file.read()
class_id, class_name = get_prediction(image_bytes=img_bytes)
return jsonify({'class_id': class_id, 'class_name': class_name})
if __name__ == '__main__':
app.run()
web サーバをテストしましょう! 次を実行します :
$ FLASK_ENV=development FLASK_APP=app.py flask run
app に POST リクエストを送るために requests ライブラリを利用できます :
import requests
resp = requests.post("http://localhost:5000/predict",
files={"file": open('/cat.jpg','rb')})
resp.json() のプリントは今では次を表示します :
{"class_id": "n02124075", "class_name": "Egyptian_cat"}
Next steps
私達が書いたサーバは非常に自明なものです、そしてプロダクション・アプリケーションのために貴方が必要とするもの総ては行なわないかもしれません。そこで、それをより良くするために行える幾つかのことがここにあります :
- エンドポイント/predict はリクエストに画像ファイルが常にあることを仮定しています。これは総てのリクエストについて事実ではないかもしれません。ユーザは異なるパラメータを伴う画像を送るか、画像を全く送らないかもしれません。
- ユーザはまた非画像型ファイルを送るかもしれません。私達はエラーを処理していませんので、これはサーバを壊すでしょう。例外を投げる明示的なエラー処理パスの追加は悪い入力をより良く処理することを可能にするでしょう。
- モデルは画像の巨大な数のクラスを認識できますが、それは総ての画像を認識することはできないかもしれません。モデルが画像内で何も認識できないときのケースを処理するために実装を拡張します。
- Flask サーバを開発モードで実行しますが、これはプロダクションで配備するためにはふさわしくありません。Flask サーバをプロダクションで配備するためには このチュートリアル を調べることができます。
- フォームを持つページを作成して UI を追加することも可能です、これは画像を取り予測を表示します。類似のプロジェクトの デモ とその ソースコード を調べてください。
- このチュートリアルでは、一度に単一の画像のための予測を返せるサービスをどのように構築するかを示しただけです。一度に複数の画像のために予測を返すことができるようにサービスを変更できるでしょう。加えて、service-streamer ライブラリは自動的にサービスへのリクエストをキューイングしてそれらをモデルに供給できるミニバッチにサンプリングします。このチュートリアル を調べることができます。
- 最後に、ページの冒頭でリンクされている PyTorch モデルを配備することについての他のチュートリアルを調べることを勧めます。
以上