SageMaker NeoでコンパイルしたモデルをラズパイにGreenGrassを使ってデプロイして推論処理パフォーマンスを計測した:Amazon SageMaker Advent Calendar 2018

ラズパイ上での「ResNet-50」での処理時間を計測してみました。
2018.12.23

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

概要

こんにちは、データインテグレーション部のyoshimです。
この記事は「クラスメソッド Amazon SageMaker Advent Calendar」の23日目の記事となります。

今日は「SageMaker Neo」でコンパイルした「ResNet-50」モデルをGreenGrassを使って「ラズパイ 3B+」にデプロイし、エッジでの推論処理のパフォーマンスを検証してみました。かなり長くなってしまったので、「目次」から気になるところだけ選択して読んでください。

主に、下記のドキュメントを参考にしています。
モジュール 1: Greengrass の環境設定
モジュール 2: Greengrass Core ソフトウェアをインストールする
How to Configure Optimized Machine Learning Inference Using the AWS Management Console

目次

1.最初に

今回は「Amazon SageMaker Neo」でコンパイルした「ResNet-50」モデルを「raspbrry pi 3B+」にデプロイして、推論処理を100回実行して処理時間を計測してみました。
モデルは「今回のチュートリアル用に事前に用意されているもの」と「SageMakerの画像分類ビルトインアルゴリズムで学習した後にSageMaker Neoでコンパイルしたモデル」の2点で検証しています。
「SageMaker Neo」でのモデルのコンパイル方法についてはこちらをご参照ください。

また、チュートリアル用に用意されているモデルには通常の「SageMaker Neo」でラズパイ用に最適化したモデルとは一部異なる点があったので、差分について「3.今回利用するモデルについて」に記述しました。
自分でSageMakerで学習させた画像分類ビルトインアルゴリズムのモデルを、「SageMaker Neo」でコンパイルしたモデルを利用したい、といった場合も基本的に手順は同じですが、「3.今回利用するモデルについて」が参考になるかもしれません。

「ラズパイ上でのコンパイルしたモデルのパフォーマンス」が知りたい方は「6.推論時間の検証」をご参照ください。また、SageMaker推論用エンドポイント(ml.c5.xlarge)での計測結果との比較も参考になるかもしれません。

2.手順の整理

実際の構築作業の前に、一回必要な作業の全体感について整理しておきます。
全体の処理を箇条書きで記述すると下記の通りです。

1.ラズパイ上でGreenGrassを使うための設定
2.「AWS IoT Greengrass」の設定
3.ラズパイの設定
4.「Amazon SageMaker Neo deep learning runtime」をラズパイにインストール
5.Lambda関数を作成
6.GreenGrassグループにLambda関数を紐付ける
7.GreenGrassグループに「SageMaker Neo」で最適化したモデルを紐付ける
8.GreenGrassグループにラズパイのリソースを紐づける
9.GreenGrassグループにサブスクリプションを紐づける
10.GreenGrassグループをデプロイする

1~4ではラズパイで「GreenGrass」や「SageMaker Neoで最適化したモデル」を利用できるように設定しており、5でLambda関数を作成、6~9でラズパイにデプロイするリソースを紐付け、10でデプロイする、という流れです。

3.今回利用するモデルについて

具体的な手順に移る前に、今回利用するモデルについて少しだけ言及しておきます。
「SageMaker」の「画像分類ビルトインアルゴリズムで学習したモデル」を「SageMaker Neo」で最適化すると、S3に下記のようなファイルが出力されます。

model-rasp3b
    - model-shapes.json
    - compiled.so
    - compiled_model.json
    - compiled.params

一方、今回チュートリアルで用意されているファイルは下記のような構成です。

resnet50
    - synset.txt
    - model.so # compiled.so
    - model.json # compiled_model.json
    - model.params # compiled.params

デフォルトで展開されるファイル名の違いは置いておいて、内容が異なるファイルは下記の2点です。
今回私が「自分で学習したモデルをSageMaker Neoで最適化した」ファイルを利用した際の変更点は下記の4点です。
(今回は処理時間の計測が目的だったので、「sysnet.txt」は用意しませんでした)

  • 「compiled.so」のファイル名を「model.so」に変更
  • 「compiled_model.json」のファイル名を「model.json」に変更
  • 「compiled.params」のファイル名を「model.params」に変更
  • 「model-shapes.json」ファイルを削除して、推論スクリプト中で定義した
    ●チュートリアルに用意されている「sysnet.txt」について

    「分類結果が何か」ということがわかるテキストファイルです。
    今回のチュートリアルでは、このファイルを追加で用意しています。

    {0: 'tench, Tinca tinca',
     1: 'goldfish, Carassius auratus',
     2: 'great white shark, white shark, man-eater, man-eating shark, Carcharodon carcharias',
     3: 'tiger shark, Galeocerdo cuvieri',
    以下略

    推論コード中では、下記のように利用しています。

    synset_path = os.path.join(model_resource_path, 'synset.txt') # sysnetファイルを読み込み
    prediction_scores = dlr_model.run({'data' : flattened_data}).squeeze() # 推論処理(この時はまだ結果を「1」とか「27」とかで持っている)
    max_score_id = np.argmax(prediction_scores) # もっとも推論スコアが高いクラスIDを取得
    predicted_class = synset[max_score_id] # 上記のクラスIDから、「分類結果」を取得
    ●チュートリアルで使われていない「model-shapes.json」について

    こちらはINPUTデータの定義を指定するjsonファイルです。
    例えば、下記のような内容です。
    (今回利用するモデルはMXNetの物なので、inputデータのshape(NCHW format)と名前を書いてます)

    [{"shape": [1, 3, 224, 224], "name": "data"}]

    コンソール画面上からモデルをコンパイルした場合は、下記の部分に入れた値がこのファイルに記述されます。

    詳細についてはInputConfigをご参照ください。

    この定義について、今回実施するチュートリアルでは推論用スクリプト上で下記のような記述を追加することで対応しています。

    input_shape = {'data': [1, 3, 224, 224]}

    4.環境構築

    基本的に「How to Configure Optimized Machine Learning Inference Using the AWS Management Console」の通りに作業を進めていきます。
    また、ラズパイ上での操作については、手元のPCからSSH接続して実行するのが簡単です。
    以下、簡単に各手順の概要について整理します。

    4-1.ラズパイ上でGreenGrassを使うための設定

    基本的に、こちらの手順に沿って処理を進めていきます。

    手順についてはドキュメントにある通りです。
    ただ、ドキュメントでは「greengrass-dependency-checker-GGCv1.5.0」とあるのですが、今回は「greengrass-dependency-checker-GGCv1.7.0」を使う点にご注意ください。

    また、本ステップ最後の確認作業で、「Node v6.10」、「Java 8」が無いと警告が出るかもしれませんが、今回のチュートリアルに限っては無視できます。

    Important
    This tutorial uses the AWS IoT Device SDK for Python. The check_ggc_dependencies script might produce warnings about the missing optional Node v6.10 and Java 8 prerequisites. You can ignore these warnings.

    引用:Module 1: Environment Setup for Greengrass

    4-2.「AWS IoT Greengrass」の設定

    基本的にはこちらの通りに作業していけばOKです。

    ただ、「証明書」については少し躓きましたので記述しておきます。
    ここでは、X.509 証明書と AWS IoTから「RSA 2048 ビットキー: Amazon ルート CA 1」をダウンロードし、これをラズパイに転送した上で「root.ca.pem」とファイル名を変更しました。

    4-3.ラズパイ上の環境構築

    ラズパイのパッケージ更新・カメラモジュールを使えるようにする、といった設定をします。 ここも基本的にはドキュメントの通りに進めていきます。

    4-4.「Amazon SageMaker Neo deep learning runtime」をラズパイにインストール

    ラズパイ上で、「SageMaker Neo」でコンパイルしたモデルを動かすために「Amazon SageMaker Neo deep learning runtime」パッケージをインストールします。
    基本はドキュメントの通りですが、少しだけ手順を記述します。

    これをクリックして、

    ラズパイ用のパッケージをダウンロードします。

    あとはドキュメントの通りにラズパイ上にこのパッケージを展開し、下記のような表示が出れば完了です。

    $ sudo ./install-dlr.sh
    Testing dlr using python...
    All tests PASSED!
    
    dlr was installed successfully!

    4-5.Lambda関数を作成

    推論処理のためのLambda関数を作成します。この作業はラズパイ上ではなく手元の端末PCで実行します。
    この作業もドキュメントの通りに進めます。

    先ほどダウンロードしてきたファイルを解凍すると、中に「inference.py」というファイルがありますが、これが実際に推論処理に用いられる内容です。
    ちなみに、下記の部分が推論処理をしている部分です。

    prediction_scores = dlr_model.run({'data' : flattened_data}).squeeze()

    ・inference.py

    #
    # Copyright 2010-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
    #
    # Greengrass lambda function to perform Image Classification with example model
    # Resnet-50 that was compiled by DLC.
    #
    #
    import logging
    import os
    
    from dlr import DLRModel
    from PIL import Image
    import numpy as np
    
    import greengrasssdk
    import camera
    import utils
    
    # Create MQTT client
    mqtt_client = greengrasssdk.client('iot-data')
    
    # Initialize logger
    customer_logger = logging.getLogger(__name__)
    
    # Initialize example Resnet-50 model
    model_resource_path = os.environ.get('MODEL_PATH', '/ml_model')
    input_shape = {'data': [1, 3, 224, 224]}
    output_shape = [1, 1000]
    dlr_model = DLRModel(model_resource_path, input_shape, output_shape, 'cpu')
    
    # Read synset file
    synset_path = os.path.join(model_resource_path, 'synset.txt')
    with open(synset_path, 'r') as f:
        synset = eval(f.read())
    
    
    def predict(image_data):
        r"""
        Predict image with DLR. The result will be published
        to MQTT topic '/resnet-50/predictions'.
    
        :param image: numpy array of the Image inference with.
        """
        flattened_data = image_data.astype(np.float32).flatten()
    
        prediction_scores = dlr_model.run({'data' : flattened_data}).squeeze()
        max_score_id = np.argmax(prediction_scores)
        max_score = np.max(prediction_scores)
    
        # Prepare result
        predicted_class = synset[max_score_id]
        result = 'Inference result: "{}" with probability {}.'.format(predicted_class, max_score)
    
        # Send result
        send_mqtt_message(
            'Prediction Result: {}'.format(result))
    
    def predict_from_cam():
        r"""
        Predict with the photo taken from your pi camera.
        """
        send_mqtt_message("Taking a photo...")
        my_camera = camera.Camera()
        image = Image.open(my_camera.capture_image())
        image_data = utils.transform_image(image)
    
        send_mqtt_message("Start predicting...")
        predict(image_data)
    
    
    def send_mqtt_message(message):
        r"""
        Publish message to the MQTT topic:
        '/resnet-50/predictions'.
    
        :param message: message to publish
        """
        mqtt_client.publish(topic='/resnet-50/predictions',
                            payload=message)
    
    
    # The lambda to be invoked in Greengrass
    def handler(event, context):
        try:
            predict_from_cam()
        except Exception as e:
            customer_logger.exception(e)
            send_mqtt_message(
                'Exception occurred during prediction. Please check logs for troubleshooting: /greengrass/ggc/var/log.')

    今回のために新規にグループを作成して、このグループにLambdaをデプロイします。

    Gitにも書いてある通りですが、こんな感じですね。
    IAMロールは必要に応じては新規作成する等の対応をしてください。

    無事、Lambda関数がアップロードできたことを、コンソール画面上からも確認しておきます。

    4-6.Lambda関数をGreenGrassグループに紐付ける

    先ほど作成したLambda関数を、GreenGrassグループに紐付けます。
    まずは、GreenGrassの「グループ」から「Lambda」、「Lambdaの追加」とクリックし、

    「既存のLambdaの使用」をクリックします。

    先ほど作成したLambda関数を選択し「次へ」をクリックします。

    先ほど指定した「エイリアス」を指定して、「完了」をクリックします。

    続いて、推論処理にかかる負荷に耐えられるように、Lambda関数のconfigを編集します。
    GreenGrassのコンソール画面上から、今回利用するLambda関数をクリックし、

    「編集」をクリックします。

    こちらに記載の通りの値を入れます。
    基本的には上記のドキュメントに書いてある通りですが、一応実際の画面の画像を添付しておきます。
    四角で囲ってある部分が、今回修正した箇所です。

    4-7.GreenGrassグループに「SageMaker Neo」で最適化したモデルを紐付ける

    モデルをGreenGrassグループに紐付けるために、まずGreenGrassの「グループ」から「リソース」、「Machine Learning」、「機械学習リソースの追加」とクリックしていきます。

    チュートリアルで用意されているファイルをアップロードする場合はこのような形になります。

    また、ここで利用するS3バケット名には「greengrass」を含めた文字列にする、「.」を使わない、といった制約があるそうなのでご注意ください。

    Note
    Your bucket name must contain the string greengrass in order for the bucket to be accessible. Choose a unique name (such as greengrass-dlr-bucket-user-id-epoch-time). Don't use a period (.) in the bucket name.

    引用:How to Configure Optimized Machine Learning Inference Using the AWS Management Console

    4-8.GreenGrassグループにラズパイのリソースを紐づける

    続いて、今回使うラズパイをGreenGrassグループに紐付けます。
    対象のグループを選択した後、「リソース」、「ローカル」、「ローカルリソースの追加」をクリックして進めていきます。

    これは単純に、ドキュメントの通りに進めました。

    Step 6: Add Your Camera Device Resource to the Greengrass Group

    4-9.GreenGrassグループにサブスクリプションを紐づける

    「サブスクリプション」から「サブスクリプションの追加」をクリックし、追加していきます。

    手順についてはドキュメントの通りです。

    4-10.GreenGrassグループをデプロイする

    ここも例によってドキュメントの通りに進めますが、大まかに何をやっているのかについて説明しておきます。

    • ラズパイの「daemon」を起動させた
    • ラズパイを「AWS IoT」から自動検出させた後、GreenGrassグループに今まで紐づけてきたリソースをラズパイにデプロイ

    デプロイの進行中...

    デプロイ完了!

    もしデプロイがうまくいかない場合はAWS IoT Greengrass のトラブルシューティングが参考になります。

    5.テスト

    デプロイが完了したので、早速テストしてみます。
    手順はこちらの通りです。

    トピックを指定して、「トピックに発行」をクリックします。

    ラズパイの前に私のノートPCがある状態なのですが、検出できているようです。

    6.推論時間の検証

    やっとこさデプロイできたので、今回デプロイした「ResNet-50」モデルでの「推論」にかかる時間を100回計測してみました。
    下記に主要な値を記述しておきます。
    (単位は一回の推論にかかった秒数)

    ・チュートリアルのモデル

    平均:0.98274936676
    第1四分位点:0.978071212768
    中央値:0.98380148
    第3四分位点:0.987836420536
    分散:0.000219083612006
    標準偏差:0.0148014733052
    最大値:1.07441711426
    最小値:0.959302186966

    かなり処理時間は安定しており、だいたい1秒程度で処理が完了するようでした。
    一応、SageMakerのビルトインアルゴリズムから手動でコンパイルしたモデルでの計測結果も記述しておきます。
    アルゴリズムも同じなので、ほとんど変わりません。

    ・自前で用意したモデル

    平均:0.96550909757619974
    第1四分位点:0.955839812756
    中央値:0.959525108337
    第3四分位点:0.978467702866
    分散:0.00022636801939996537
    標準偏差:0.015045531542619735
    最大値:1.0539650917100001
    最小値:0.94228982925399996

    7.まとめ

    「SageMaker Neo」で最適化したResNet(50層)なら、「ラズパイ3B+」上で1秒程度で実行できることが確認できました。
    また、SageMakerのビルトインアルゴリズムでもほぼほぼ同様の手順でデプロイ&実行できることも確認しました。
    SageMaker推論用エンドポイント(ml.c5.xlarge)での計測結果と比較すると処理時間が随分と大きい(ResNet-50,ResNet-152という違いがあるにも関わらず)ですが、この辺りはラズパイ上での実行なのでしょうがないところかと思います。

    かなり長くなってしまいましたが、本エントリーでの紹介内容は以上になります。
    最後まで読んでくださりありがとうございました。