PyTorch 1.4 Tutorials : PyTorch モデル配備 : Flask REST API で PyTorch を配備する

PyTorch 1.4 Tutorials : PyTorch モデル配備 : Flask REST API で PyTorch を配備する (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 01/18/2020 (1.4.0)

* 本ページは、PyTorch 1.4 Tutorials の以下のページを翻訳した上で適宜、補足説明したものです:

* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。

 

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)

上のメソッドはバイトの画像データを取り、transformes のシリーズを適用し、そして 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 サーバをプロダクションで配備するために このチュートリアル を調べることができます。
  • form を持つページを作成して UI を追加することも可能です、これは画像を取り予測を表示します。類似のプロジェクトの デモ とその ソースコード を調べてください。
  • このチュートリアルでは、一度に単一の画像のための予測を返せるサービスをどのように構築するかを示しただけです。一度に複数の画像のために予測を返すことができるようにサービスを変更できるでしょう。加えて、service-streamer ライブラリは自動的にサービスへのリクエストをキューイングしてそれらをモデルに供給できるミニバッチにサンプリングします。このチュートリアル を調べることができます。
  • 最後に、ページの冒頭でリンクされている PyTorch モデルを配備することについての他のチュートリアルを調べることを勧めます。

 
以上