テクノロジー戦略本部の村上です。以前Object Detectionを試した結果を紹介させて頂きましたが、 今回はその方法で生成したモデルを使って、TensorFlow Servingを用いてAPIを作成したのでその紹介をしたいと思います。 なお、現在検証中でリリース前ですので、紹介した内容から変更が入るかもしれないのでご了承下さい。
TensorFlow Servingとは
TensorFlow Servingは、SavedModelというフォーマットで書き出したモデルを読み込んで、RESTとgRPCの両方で推論APIを提供出来るシステムです。フォーマットさえ合っていればモデルの内容に関係なく使用出来るという汎用性が特徴で、全く実装をせずにモデルさえ与えればAPIサーバーとして動きます。Googleの記事を見ますと、Googleでは2017年時点で800以上のプロジェクトで本番環境で使われているようです。
使用する上での注意点としては、
- モデルを与えるだけなので自前でモデルを実装した場合、入力データの前処理などもモデルに含めた方が良い
- 汎用化するためにリクエストとレスポンスが汎用的なフォーマットなので、APIを呼び出すときとレスポンスを使う際に変換が必要となる
があります。1は例えば画像を入力にしたい場合はモデルに前処理などを含めないとAPIとしては使いづらいものになってしまいます。 以下のTensorFlowの公式にも同様のことが書かれています。
Benefits of doing preprocessing inside the model at inference time
2のデータフォーマットですが、たとえばpredictionのAPIのレスポンスのProtocolBufferの定義を見ますと以下のようになっています。
message PredictResponse { ModelSpec model_spec = 2; map<string, TensorProto> outputs = 1; }
outputsフィールドはmapになっているのでここでまず汎用的になっています。
さらにmapのvalueの型であるTensorProto
の定義を見てみると、このようになっています。
message TensorProto { DataType dtype = 1; TensorShapeProto tensor_shape = 2; int32 version_number = 3; bytes tensor_content = 4; repeated int32 half_val = 13 [packed = true]; repeated float float_val = 5 [packed = true]; repeated double double_val = 6 [packed = true]; // 省略 repeated uint64 uint64_val = 17 [packed = true]; }
このように、型の種類を指定して、その型に該当するフィールドに値を入れるという形になっています。 ですのでどんな型でも対応出来る代わりに、自分で定義する場合と違いフィールド名に意味を持たせたり階層構造を作ったりが出来ないというデメリットがあります。
システム実装
構成
上で述べたように汎用的であり、特にObject Detectionはレスポンスの内容が多いのでTensorFlow Servingをそのまま使うのは難しいです。そこで、以下のように構成にしました。
図のKubernetesに載せている部分が今回開発した部分です。APIとしては画像を入力にしてDetection結果を出力としたいので、インピーダンスマッチングをするアプリケーションを用意しました。 どの矢印もgRPCで通信しています。gRPCを使うためのIstioが導入されたKubernetesは、既に弊社で運用しているものを使用しています。
Tensorflow Servingのデプロイ
まずモデル生成ですが、Object Detectionの学習済みモデルは、以下のコマンドでSavedModelフォーマットに変換出来ます。
python ./exporter_main_v2.py --input_type image_tensor --pipeline_config_path ./models/my_efficientdet_d1/pipeline.config --trained_checkpoint_dir ./models/my_efficientdet_d1/ --output_directory ./exported-models/my_model
そのモデルをどう組み込むかは何通りか考えられますが、今回はGitHub ActionsでモデルをGCSから取得し、
それをDockerイメージに組み込む方法を取りました。ですので以下のようなDockerfileを用意してデプロイすることとなります。
COPYしているのがモデルとなります。app_name
という名前はあくまでこの記事用の例ですが、環境変数のMODEL_NAME
と合わせる必要があります。
FROM tensorflow/serving:2.3.0 COPY app_name /models/app_name ENV MODEL_NAME=app_name
これをKubernetesにデプロイする際は、以下のKubeflowのページに掲載されているマニフェストを参考にすれば良いです。
API実装
まずAPIのメインの役割であるインピーダンスマッチングの部分を説明します。前述したようにTensorFlow Servingの入出力が汎用化されているので、それぞれを表すキー名をまず把握する必要があります。これは実装を見るしかないです。 それぞれこのようになっていました。
- 入力:
input_tensor
- 出力:
detection_boxes
,detection_scores
,detection_classes
,num_detections
キー名がわかったので、あとはその形式で入出力を変換するだけです。
入力の変換は以下のようになります。input_tensorの準備は公式の例のrun_inference_for_single_image
というメソッドを参考にしました。
def generate_request(image_np): tensor = tf.convert_to_tensor(image_np) input_tensor = tensor[tf.newaxis, ...] request = predict_pb2.PredictRequest() request.model_spec.name = 'app_name' tensor_proto = tf.make_tensor_proto(input_tensor, shape=input_tensor.shape) request.inputs['input_tensor'].CopyFrom(tensor_proto) return request
出力の変換は以下のようになります。スコアが良い順に格納されているので、今回は一番良いスコアの結果を抽出しました。
class Result(typing.NamedTuple): class_index: int score: float rect: Rect class Rect(typing.NamedTuple): x_min: int y_min: int x_max: int y_max: int @classmethod def init(cls, x, y, box): return cls(y_min=int(y * box[0]), x_min=int(x * box[1]), y_max=int(y * box[2]), x_max=int(x * box[3])) detection_classes = np.array(result.outputs['detection_classes'].float_val) detection_scores = np.array(result.outputs['detection_scores'].float_val) detection_boxes = np.array(result.outputs['detection_boxes'].float_val).reshape([100, 4]) box = detection_boxes[0] rect = Rect.init(x, y, box) Result(class_index=int(detection_classes[0]), score=detection_scores[0], rect=rect)
リクエストが準備できればあとはTensorFlow ServingのgRPC APIを呼び出すだけです。呼び出す時の注意点としては、単にホストを設定するだけでは以下のIssueに書かれているようなエラーが出るという点です。
gRPC: Received message larger than max (32243844 vs. 4194304)" #1382
そこで書かれているようにmax_send_message_length
とmax_receive_message_length
を設定すると良いです。このようになります。
from tensorflow_serving.apis import prediction_service_pb2_grpc channel_opt = [('grpc.max_send_message_length', 512 * 1024 * 1024), ('grpc.max_receive_message_length', 512 * 1024 * 1024)] channel = grpc.insecure_channel('localhost:8500', options=channel_opt) stub = prediction_service_pb2_grpc.PredictionServiceStub(channel) result = stub.Predict(generate_request(image_np), RPC_TIMEOUT)
これで画像のndarrayを変換してTensorFlow ServingのAPIを呼び出し、その結果から必要な部分の抽出が出来たこととなります。 あとはそれをクライアントに返すだけです。
まとめ
いかがでしたでしょうか。記事で書いたようにモデルさえ作成できればTensorFlow Servingで簡単にAPIを実装することが出来ます。 公式リポジトリに色々とサンプルはあるものの、あまりまとまった記事は見当たらなかった記憶があるので、少しでも参考になれば幸いです。
参考資料
- TensorFlow Serving 1.0
- Googleでの使用状況が書かれた紹介記事
- Exporting a Trained Model
- Object DetectionのモデルをSavedModel形式に変換するチュートリアル
- Object Detection API Demo
- 本文中で参考にした
run_inference_for_single_image
が書かれています
- 本文中で参考にした