[Amazon SageMaker] イメージ分類のモデルをNeoで最適化して、Greengrass+RasPi4+OpenCV+Webカメラで使用してみました

2020.06.15

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

1 はじめに

CX事業本部の平内(SIN)です。

Amazon SageMager Neo(以下、Neo)を使用すると、既存のモデルを簡単にRaspberryPiで動作させる事ができます。 今回は、Neoで最適化したモデルをAmazon Greengrass(以下、Greengrass)で機械学習リソースとして組み込み、エッジ(RasPi)上のLambdaから利用してみました。

作業にあたっては、下記の記事を参考にさせて頂きました。

やってる事は、上記ブログと同じなのですが、少し、DLRの利用方法や、Neoの出力形式が変化していたので、改めて纏めさせて頂きました。

最初に動作している様子です。

RaspberryPiに接続したWebカメラの画像を推論にかけて、結果をGreengrassのMQTTメッセージで送っています。推論には、2秒弱かかるため、反応は少し時間がかかります。

2 Neoによる最適化

使用したモデルは、Amazon SageMaker(以下、SageMaker)で以前作成した、商品を検出するイメージ分類(SageMakerの組み込みアルゴリズム)で作成したのです。

SageMakerで作成したイメージ分類のモデルをNeoで最適化する手順は以下のとおりです。

  • SageMakeのコンソールから 推論 > コンパイルジョブ > コンパイルジョブの作成を選択し、新しくジョブを作成します。

  • アーティファクトの場所には、先に作成したイメージ分類のモデル(XXXX.tar.gz)を指定します。
  • データ入力は、イメージ分類の場合、{"data", [1, 3, 224, 224]} になります。
  • 機械学習フレームワークは、MXNetを選択します。

  • S3出力先:最適化されたモデルの出力場所を指定します。
  • 対象デバイスは、rasp3b を選択しました。(RasPi model 4は、選択リストに無かったのですが、RasPi4で動作確認できました)

  • 作成をクリックすると、最適化モデルの生成が始まり、1分程度で、ステータスはCOMPLATEDになります。

  • 出力先に指定したS3のバケットにmodel-rasp3b.tar.gzが生成されている事が確認できます。

注意:この出力先URLをGreengrassの機械学習モデルリソースの指定でそのまま使用するためには、デフォルトのパーミッションでは、バケット名にgreengrass若しくは、sagemakerが含まれている必要があります。

3 RaspberryPiの設定

(1) 使用環境

Raspberri Piは、Model4で、OSは、今年5月の最新版(Raspberry Pi OS (32-bit) with desktop and recommended software)2020-05-27-raspios-buster-full-armhf.img です

$ cat /proc/cpuinfo  | grep Revision
Revision    : c03112

$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description:    Raspbian GNU/Linux 10 (buster)
Release:    10
Codename:   buster

$ uname -a
Linux raspberrypi 4.19.118-v7l+ #1311 SMP Mon Apr 27 14:26:42 BST 2020 armv7l GNU/Linux

(2) スワップ

RasPi4はメモリが1Gで、デフォルトで設定されているswapが100Mとなっているのですが、機械学習のモデルを読み込むのにメモリ不足となってしまうため、スワップのサイズを上げました。

  • デフォルは、100Mです。
$ swapon
NAME      TYPE SIZE USED PRIO
/var/swap file 100M   0B   -2
  • 設定ファイルを編集します。
$ sudo vi /etc/dphys-swapfile
CONF_SWAPSIZE=2048
  • dphys-swapfileの再起動すると2Gになっている事を確認できます。
$ sudo /etc/init.d/dphys-swapfile restart
$ swapon
NAME      TYPE SIZE USED PRIO
/var/swap file   2G   0B   -2

(3) Greengrassのセットアップ

RaspberryPiへのGreengrassのセットアップは、クイックスタートを使用させて頂きました。
参考:【小ネタ】[AWS IoT Greengrass] クイックスタートを使用して、コマンド3行でセットアップする(RaspberryPi)

指定した内容は、以下のとおりです。

  • グループ名: RasPi_Sample_Group
  • リージョン: ap-northeast-1
  • Lambda: あり
$ wget -q -O ./gg-device-setup-latest.sh https://d1onfpft10uf5o.cloudfront.net/greengrass-device-setup/downloads/gg-device-setup-latest.sh \
&& chmod +x ./gg-device-setup-latest.sh \
&& sudo -E ./gg-device-setup-latest.sh bootstrap-greengrass \
--aws-access-key-id XXXXX \
--aws-secret-access-key XXXXX \
--region ap-northeast-1 \
--group-name My_GG_Group \
--hello-world-lambda \
--verbose
$ sudo reboot
$ sudo -E ./gg-device-setup-latest.sh bootstrap-greengrass \
--aws-access-key-id XXXXX \
--aws-secret-access-key XXXXX

クイックスタートが正常に完了している事を確認するため、AWS IoTのテスト画面で hello/world をSubscribeしてメッセージが到着して事を確認します。

また、クイックスタートで作成されたLambdaの名前も確認しておきます。 Helloで検索して、最終更新時間が新しいことで分かると思います。

(4) DLR

RasPi上でNeoのモデルを使用するためには、DLR(Compact Runtime for Machine Learning Models)が必要です。
https://neo-ai-dlr.readthedocs.io/en/latest/install.html

以前は、いくつかのビルド済みのモジュールがpipから利用可能でしたが、現在、そのリンクが無くなってしまったので、ソースからコンパイルします。

  • 最初にpip3等、必要なパッケージをインストールします。
$ sudo apt-get update
$ sudo apt-get install -y python3 python3-setuptools build-essential curl ca-certificates
$ curl https://bootstrap.pypa.io/get-pip.py -o /tmp/get-pip.py
$ sudo python3 /tmp/get-pip.py
$ rm /tmp/get-pip.py
  • ソースコードをcloneします。
$ git clone --recursive https://github.com/neo-ai/neo-ai-dlr
$ cd neo-ai-dlr
  • コンパイルします。
$ mkdir build
$ cd build
$ cmake ..
$ make -j4
$ cd ..
  • インストールします。 (--userでは、greengrassから利用できなかったの絵、rootでインストールしています)
$ cd python
$ sudo python3 setup.py install
$ cd ../..
  • 下記のコマンドでエラーが無ければ、Python3.7で利用可能になっていることが確認できます。(Greengrassからは。Python3.7が使用されます)
$ python3.7 -c "import dlr"

(5) OpenCV

今回のサンプルで利用するOprnCVもインストールします。

$ sudo apt-get upgrade
$ sudo apt-get install libhdf5-dev libhdf5-serial-dev libhdf5-103
$ sudo apt-get install libqtgui4 libqtwebkit4 libqt4-test python3-pyqt5
$ sudo apt-get install libatlas-base-dev libjasper-dev
$ sudo pip3 install opencv-python opencv-contrib-python
  • 環境変数(LD_PRELOAD)を設定することで、OpenCVが利用可能になっていることが確認できます。 (この環境変数は、後ほど、GreengrassのLambdaで設定します)
$ export LD_PRELOAD=/usr/lib/arm-linux-gnueabihf/libatomic.so.1
$ python3.7 -c "import cv2"

4 Greengrassの設定

(1) Lambdaの設定

GreengrassグループのLambdaの設定です。

実行ユーザーは、デフォルトのggc_userからpiに変更しています。Python用のライブラリなど、ユーザpiで設定したので、この方法が一番簡単だと思います。

※ piのユーザーID及び、グループIDは、1000です。

pi~$ grep pi /etc/passwd
pi:x:1000:1000:,,,:/home/pi:/bin/bash

メモリ制限は、1.5Gとしました。モデルのサイズにも依存しますが、今回のモデルは、1Gでは、不足となってしまいました。

環境変数は、numpyのためのものです。

(2) リソース(Machine Larning)

リソースのMachine Larningで、Neoで作成したモデルをLambdaで利用可能なようにデバイス側に送ります。

リソース名は任意でOKです。

S3のモデルをアップロードを選択すると、NeoのS3への出力を、そのまま指定できます。 ローカルパスは、Lambdaから見えるパスになります。(今回は、/modelsとしました)

なお、Neoで最適化されたモデルは、上記のような圧縮ファイル(tar.gz)となっていますが、こちらが、エッジ側で下記のように展開されることになります。

/models
├── compiled.meta
├── compiled.params
├── compiled.so
├── compiled_model.json
└── model-shapes.json

最後に、モデルにLambdaからの読み取り専用アクセスを設定します。

(3) リソース(ローカル)

RaspberryPiに接続されたWebカメラは、Lambdaに利用許可を与える必要があります。

任意のリソース名をつけ、デバイスパスを/dev/video0とします。

リソースを所有するLinuxグループOSグループアクセスを許可を自動的に追加するにチェックし、読み取りと書き込みアクセスでLamdaを関連付けます。

映像を取得するだけなので、読み取り専用でいいような気もしますが、OpenCVがデバイスを開く時に、設定も可能なように、Write権限を必要とするので、読み取り専用では、OpenCVで利用できません。

5 Lambdaのコード

Lambdaのコードは、以下のとおりです。

Webカメラの画像を2秒に一回、推論にかけて、レスポンスをMQTTで送信しています。 このLambdaは、Greengrass起動時にInvokeされ、ハンドラは使用されていません。

import logging
import sys
import greengrasssdk
import cv2
import numpy as np
import dlr
import time

CLASSES = ['PORIPPY(GREEN)', 'OREO', 'CUNTRY_MAM', 'PORIPPY(RED)', 'BANANA', 
           'CHEDDER_CHEESE', 'PRETZEL(YELLOW)', 'FURUGURA(BROWN)', 'NOIR', 'PRIME',
           'CRATZ(RED)', 'CRATZ(GREEN)', 'PRETZEL(BLACK)', 'CRATZ(ORANGE)', 'ASPARA',
           'FURUGURA(RED)', 'PRETZEL(GREEN)']

logger = logging.getLogger(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)
client = greengrasssdk.client("iot-data")

def publish(message):
    print("punlish:{}".format(message))
    try:
        client.publish(
            topic="hello/world",
            queueFullPolicy="AllOrException",
            payload=message
        )
    except Exception as e:
        logger.error("Failed to publish message: " + repr(e))

def main():

    print("Ver dlr:{} cv2:{}".format(dlr.__version__, cv2.__version__))

    DEVICE_ID = 0
    WIDTH = 800
    HEIGHT = 600
    FPS = 24

    cap = cv2.VideoCapture (DEVICE_ID)
    
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
    cap.set(cv2.CAP_PROP_FPS, FPS)

    width = cap.get(cv2.CAP_PROP_FRAME_WIDTH)
    height = cap.get(cv2.CAP_PROP_FRAME_HEIGHT)
    fps = cap.get(cv2.CAP_PROP_FPS)
    print("fps:{} width:{} height:{}".format(fps, width, height))


    print("DLRModel() start")
    model = dlr.DLRModel('/models','cpu')
    print("DLRModel() end")
    print("model.get_input_dtypes(): {}".format(model.get_input_dtypes()))
    print("model.get_input_names(): {}".format(model.get_input_names()))

    # 2秒ごとに推論する
    counter = 0
    span = 2 
    
    while(True):
        
        _, img = cap.read()
        if(img is None):
            continue
        counter += 1

        if(counter > fps*span):
            counter = 0
            
            img = img[0 : int(height), 0 : int(height)] # 横長の長方形 => 正方形
            img = cv2.resize(img, dsize=(224, 224)) 
            img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
            img = img.transpose((2, 0, 1))
            img = img[np.newaxis, :]
        
            print("img.shape: {}".format(img.shape))

            start = time.time()
            out = model.run({'data' : img})
            elapsed_time = time.time() - start
            probability = "{:.5f}".format(np.max(out))
            index = np.argmax(out[0])
            str = "{} - {} - {:.2f}sec".format(CLASSES[index], probability, elapsed_time)
            publish(str)

main()

def function_handler(event, context):
    return

GreengrassのLambdaは、バージョン若しくは、エリアスで識別されますが、クイックスタートで生成されたLambdaは、バージョン1になっているため、コードを更新するためには、バージョンを更新し、グループ設定でLambda及び、サブルクリプションの再設定が必要です。

なお、エリアス指定にしておくと、Lambdaの更新時は、新しいバージョンにエリアスを貼り直すだけになるので、開発が捗ると思います。

6 最後に

今回は、Neoで最適化したモデルをGreengrass経由で使用してみました。

モデルのサイズにも依存しますが、Lambdaのメモリ制限が、デフォルト値の16Mでは、まず、不足するので注意が必要です。また、メモリ制限を増やした場合、デバイス側のメモリもスワップ等で対応可能にしておく事が必要です。

Greengrass経由でモデルを利用すると、Neoがモデルを更新した際、そのまま反映(デプロイ)することが可能になるので、機械学習のサイクルを回すには、必須の形かも知れません。