こんにちは、R&Dチームの宮崎です。 DeepLearningの有名どころの画像分類モデルを用いて、CPU環境での推論時の処理速度を計測しましたので、共有したいと思います。
背景
DeepLearningモデルをサービスに適用する際、精度と同様に推論時の処理速度も重要になってきます。AWSなど時間課金の基盤上でモデルを動かす場合、処理速度が2倍になると運用にかかるコストは1/2になるためです。 しかし、論文などでは精度やパラメータ数、FLOPSなどについては言及されているものの、処理速度についてはあまり記述がありません。あったとしてもGPU上での値だったりします。これらの値はCPUで動かした際の処理速度と比例しないため、モデル開発後にデプロイして評価してみると、想定より遅く、こんなはずじゃなかったとなったりします。
一方で、DeepLearningのモデルは、同じネットワーク構造と実行環境であれば、推論するラベルや重みが異なっても処理速度はほぼ変わらないことが期待されます。そこで今回、よく使われる画像分類モデルについて、CPU環境での処理速度を計測しました。皆様のモデル選択のご参考にしていただけると幸いです。
計測方法
今回の計測は以下の設定の環境で、画像100枚をシーケンシャルに推論し、1枚あたりの平均処理時間を求めました。
- 基盤: AWS ECS FargateSpot
- CPU: 1 vCPU
- Memory: 8GB (通常は2GBで事足りると思いますが、今回はコンテナ内に複数モデル格納したため、多く使用しています)
- 入力画像: COCO valデータセットの内100枚
- モデル
- 重み: ImageNetの事前学習済みモデル
- バッチサイズ: 8
- 推論タイプ:
- ローカルモデル: ローカルディスクのモデルを読み込んで推論
- TensorFlow Serving: TensorFlow Servingに推論リクエストを送信
- ソースコード: ページ末尾に添付
計測結果
計測結果のバブルチャートです。縦軸がImageNetのTop-1 Accuracy、横軸がローカルモデルを用いた画像一枚あたりの処理時間(対数スケール)、バブルの大きさがモデルのパラメータ数です。ImageNetのTop-1 Accuracyは今回計測せず、各モデルのページから引用しました。チャートの左上にあるほど、高速で精度が良いモデルということになります。
詳細な値は以下となります。
モデル名 | 処理速度 [秒/枚] (ローカル) |
処理速度 [秒/枚] (TF Serving) |
ImageNet Top-1 Accuracy |
パラメータ数 |
---|---|---|---|---|
VGG16 | 0.990 | 0.987 | 0.713 | 138,357,544 |
MobileNetV2 | 0.064 | 0.075 | 0.713 | 3,538,984 |
ResNet50 | 0.286 | 0.288 | 0.749 | 25,636,712 |
InceptionV3 | 0.382 | 0.390 | 0.779 | 23,851,784 |
Xception | 0.743 | 0.754 | 0.790 | 22,910,480 |
EfficientNetB0 | 0.115 | 0.120 | 0.772 | 5,330,564 |
EfficientNetB1 | 0.188 | 0.199 | 0.791 | 7,856,232 |
EfficientNetB2 | 0.255 | 0.264 | 0.802 | 9,177,562 |
EfficientNetB3 | 0.472 | 0.494 | 0.816 | 12,320,528 |
EfficientNetB4 | 1.487 | 1.536 | 0.830 | 19,466,816 |
EfficientNetB5 | 3.501 | 3.511 | 0.837 | 30,562,520 |
EfficientNetB6 | 6.078 | 6.197 | 0.841 | 43,265,136 |
EfficientNetB7 | 12.226 | 12.250 | 0.844 | 66,658,680 |
考察
例えば、InceptionV3とXceptionを比べると、パラメータ数はInceptionV3の方が多いですが、処理時間は半分ほどで、パラメータ数と処理速度が比例しないことがわかります。Xceptionに対し、InceptionV3の精度は約1.1%下がる程度のため、コストパフォーマンスはInceptionV3の方が高く感じます。また、EfficientNetシリーズは総じて高精度ですが、EfficientNetB4以降は画像1枚あたりに1秒以上かかってしまうため、選択する際はコスト面をよく考慮する必要があります。
ローカルモデルとTensorFlow Servingを比べると画像の転送時間もあり、TensorFlow Servingの方が処理時間が長いですが、その差は平均約0.02秒/枚と、影響は小さいです。また、これは画像サイズが大きいEfficientNetB7も同様であることがわかります。
まとめ
よく使われるDeepLearning画像分類モデルのCPU環境における処理速度を計測しました。モデルによって、その処理速度には大きく差があり、コスト面を考慮した上で選択する必要があることがわかりました。DeepLearningモデル選択時の参考にしていただけると幸いです。
付録(ソースコード)
実験に用いたソースコードを添付します。
モデル保存
モデル保存用のコードです。
import os import tensorflow as tf from absl import logging import tensorflow.keras.applications as app import efficientnet.tfkeras as efn MODELS = [ app.VGG16, app.ResNet50, app.InceptionV3, app.Xception, app.MobileNetV2, efn.EfficientNetB0, efn.EfficientNetB1, efn.EfficientNetB2, efn.EfficientNetB3, efn.EfficientNetB4, efn.EfficientNetB5, efn.EfficientNetB6, efn.EfficientNetB7, ] def build_serve_fn(model): @tf.function( input_signature=[tf.TensorSpec(model.input.shape, dtype=tf.uint8)]) def serve(images): images = tf.cast(images, tf.float32) / 255 probabilities = model(images) classes = tf.argmax(probabilities, axis=-1) return {'classes': classes} return serve def export_model(model, export_path): logging.info(f'Export {model.name}, input_shape: {model.input.shape}') serve_fn = build_serve_fn(model) tf.keras.backend.set_learning_phase(0) tf.saved_model.save(model, export_path, signatures={ 'serving_default': serve_fn}) def export_models(output_dir: str): for model_cls in MODELS: model = model_cls() export_path = os.path.join(output_dir, model.name, '1') export_model(model, export_path) if __name__ == '__main__': logging.set_verbosity(logging.INFO) export_models(output_dir='models')
計測
計測用のコードです。事前に、上記で出力したモデルを持たせたTensorFlow Servingコンテナを立てる必要があります。
import os import io import time import argparse import functools import grpc import numpy as np import tensorflow as tf import boto3 from PIL import Image from tensorflow_serving.apis import predict_pb2 from tensorflow_serving.apis import prediction_service_pb2_grpc from absl import logging MODEL_INPUT_SHAPES = { 'vgg16': 224, 'resnet50': 224, 'inception_v3': 299, 'xception': 299, 'mobilenetv2_1.00_224': 224, 'efficientnet-b0': 224, 'efficientnet-b1': 240, 'efficientnet-b2': 260, 'efficientnet-b3': 300, 'efficientnet-b4': 380, 'efficientnet-b5': 456, 'efficientnet-b6': 528, 'efficientnet-b7': 600, } BATCH_SIZE = 8 NUM_IMAGES = 100 MODEL_DIR = 'models' class S3Client: def __init__(self, bucket): self.s3_client = boto3.Session().client('s3') self.bucket = bucket def get_s3_keys(self, directory='', white_list_formats=('jpg', 'jpeg')): keys = [] paginator = self.s3_client.get_paginator('list_objects') for result in \ paginator.paginate(Bucket=self.bucket, Delimiter='/', Prefix=directory): if result.get('CommonPrefixes') is not None: for subdir in result.get('CommonPrefixes'): keys += self.get_s3_keys(subdir.get('Prefix'), white_list_formats) if result.get('Contents') is not None: for file in result.get('Contents'): key = file.get('Key') if key.lower().endswith(white_list_formats): keys.append(key) return keys def download_image(self, s3_key, target_size=None): s3_object = self.s3_client.get_object(Bucket=self.bucket, Key=s3_key) image_data = io.BytesIO(s3_object['Body'].read()) pil_image = Image.open(image_data).convert('RGB') if target_size: pil_image = pil_image.resize((target_size, target_size), Image.NEAREST) image = np.asarray(pil_image) return image def download_images(self: str, s3_keys, target_size=None): images = [] for key in s3_keys: image = self.download_image(key, target_size) images.append(image) images = np.asarray(images) return images def predict_on_remote(server, model_name, timeout, images): with grpc.insecure_channel(server) as channel: stub = prediction_service_pb2_grpc.PredictionServiceStub( channel) request = predict_pb2.PredictRequest() request.model_spec.name = model_name request.model_spec.signature_name = 'serving_default' tensor = tf.make_tensor_proto(images, dtype=images.dtype.type, shape=images.shape) request.inputs['images'].CopyFrom(tensor) response = stub.Predict(request, timeout) result = {} for key, value in response.outputs.items(): result[key] = tf.make_ndarray(value).reshape( [dim.size for dim in value.tensor_shape.dim]) return result def predict_on_local(model, images): f = model.signatures["serving_default"] result = f(images=tf.constant(images)) return result def run_predict(predict_fn, input_shape, num_images, batch_size, bucket, s3_dir): s3 = S3Client(bucket) s3_keys = s3.get_s3_keys(s3_dir) total_sec = 0 for start in range(0, num_images, batch_size): end = min(start+batch_size, num_images) images = s3.download_images(s3_keys[start:end], input_shape) pred_start = time.time() predict_fn(images=images) pred_end = time.time() total_sec += (pred_end - pred_start) return total_sec def main(args): logging.set_verbosity(logging.INFO) server = args.server num_images = args.num_images batch_size = args.batch_size model_dir = args.model_dir timeout = args.timeout bucket = args.bucket s3_dir = args.s3_dir models = os.listdir(model_dir) logging.info(f'models: {models}') logging.info(f'server: {server}') logging.info(f'timeout: {timeout}') # Remote prediction for model_name in models: input_shape = MODEL_INPUT_SHAPES[model_name] predict_fn = functools.partial(predict_on_remote, server=server, model_name=model_name, timeout=timeout) processing_sec = run_predict(predict_fn, input_shape, num_images, batch_size, bucket, s3_dir) logging.info(f'[Remote] model_name: {model_name}, ' f'processing_sec: {processing_sec:.3f}, ' f'processing_sec/image: {processing_sec/num_images:.3f}') # Local prediction for model_name in models: input_shape = MODEL_INPUT_SHAPES[model_name] model = tf.saved_model.load(os.path.join(model_dir, model_name, '1')) predict_fn = functools.partial(predict_on_local, model=model) processing_sec = run_predict(predict_fn, input_shape, num_images, batch_size, bucket, s3_dir) logging.info(f'[Local] model_name: {model_name}, ' f'processing_sec: {processing_sec:.3f}, ' f'processing_sec/image: {processing_sec/num_images:.3f}') if __name__ == '__main__': parser = argparse.ArgumentParser() parser.add.argument('--bucket', type=str, required=True) parser.add.argument('--s3_dir', type=str, required=True) parser.add_argument( '--server', type=str, default='localhost:8500') parser.add_argument( '--num_images', type=int, default=NUM_IMAGES) parser.add_argument( '--batch_size', type=int, default=BATCH_SIZE) parser.add_argument( '--model_dir', type=str, default=MODEL_DIR) parser.add_argument( '--timeout', type=int, default=80) args = parser.parse_args() main(args)