AWS 기술 블로그
사물인터넷(IoT) 디바이스에서 기계학습(ML)을 이용한 이미지 분류하기
사물인터넷 (IoT) 디바이스에서 이미지 분류를 하기 위해서는 효과적인 기계학습 모델이 필요합니다. 2020년 AWS re:Invent에서 소개된 AWS IoT Greengrass V2는 ResNet-50에 기초한 DLR 이미지 분류 모델 스토어를 Java 기반의 공개 컴포넌트(Public component)로 제공하고 있으므로, IoT 디바이스에서 이미지 분류를 할 때 유용하게 사용할 수 있습니다.
DLR (Deep Learning Runtime) 이미지 분류 모델은 Built-in public component인 variant.DLR.ImageClassification.ModelStore
로 제공되며, 이것을 IoT 디바이스에서 활용하기 위해서는 또 다른 공개 컴포넌트인 aws.greengrass.DLRImageClassification
을 이용하여 이미지 분류를 요청하거나, 별도의 custom component를 구현하여야 합니다.
Built-in Public Component를 이용한 이미지 분류 및 문제점
AWS IoT Greengrass에서 Built-in으로 제공하는 public component인 aws.greengrass.DLRImageClassification
을 이용하면 IoT 디바이스에서 이미지 분류를 손쉽게 구현할 수 있습니다. aws.greengrass.DLRImageClassification
을 설치하고, 활용하는 방법에 대한 상세한 설명은 [Workshop: Image Classification via Greengrass]를 참조하시기 바랍니다.
aws.greengrass.DLRImageClassification
에서는 아래와 같이 이미지 분류를 처리하는 주기 (InferenceInterval
), 이미지를 로드하는 폴더 (ImageDirectory
), 이미지의 이름 (ImageName)을 변경할 수 있지만, public component이므로 사용자의 목적에 따라 자유롭게 수정하여 사용할 수 없습니다.
{
"InferenceInterval": "60",
"ImageDirectory": "/tmp/images/",
"ImageName": "image.jpg"
}
또한, RESTful API 와 같이 Greengrass의 다른 component에서 aws.greengrass.DLRImageClassification
에 직접 요청을 보내고 결과를 얻는 방식이 아니라, IoT Core를 통해 결과를 확인하여야 합니다. 따라서, 오프라인 같은 네트워크 상황도 고려하여 IoT 디바이스에서 이미지 분류를 구현하고자 한다면 custom component에서 직접 variant.DLR.ImageClassification.ModelStore
의 DLR model을 로딩하여 활용할 수 있어야 합니다.
Custom Component를 이용한 이미지 분류
엣지(Edge)에 있는 IoT 디바이스에서 이미지 분류를 수행하는 과정을 아래 아키텍처에서 설명하고 있습니다. AWS 클라우드의 Greengrass를 이용하여 디바이스에 custom component를 배포하거나 관리할 수 있습니다.
IoT 디바이스에는 이미지 분류를 요청하는 component인 Requester (com.custom.requester
), 추론을 수행하는 component인 Classifier (com.custom.ImageClassifier
), DLR model (variant.DLR.ImageClassification.ModelStore
)이 있습니다. 아래와 같이 Requester와 Classifier는 Nucleus를 통해 IPC 통신을 수행하고, 이미지 분류 추론을 수행하는 Inference은 Classifier와 DLR model로 구성됩니다.
Requester는 “local/inference
” topic으로 추론을 요청하고, “local/result
” topic으로 결과를 얻습니다. 마찬가지로 Classifier는 “local/inference
” topic으로 추론 요청을 확인하여, DLR model로 추론을 수행하고, “local/result
” topic으로 결과를 Requester에 전달합니다.
이미지 분류를 수행하는 과정을 순서에 따라 설명하면 아래와 같습니다.
- Greengrass의 custom component인 Requester가 이미지 분류를 시작합니다.
- Requester는 Classifier가 분류를 수행할 수 있도록 “
local/inference
” topic으로 분류할 이미지에 대한 정보를 publish합니다. 이때, Nucleus가 제공하는 local PubSub Service를 이용합니다. - Classifier는 “
local/inference
” topic을 subscribe하고 있다가, Requester가 보낸 이미지 분류 추론 요청을 받습니다. - Classifier는
DLR.ImageClassification.ModelStore
의 DLR model을 이용하여 추론(inference)을 수행합니다. - DLR model을 이용해 얻은 이미지 분류 추론 결과가 Classifier에 리턴 됩니다.
- Classifier가 이미지 분류 추론 결과를 Requester에게 전달하기 위하여, “
local/result
” topic으로 publish를 수행합니다. - Requester는 “
local/result
” topic을 subscribe하고 있다가, Classifier가 전달한 이미지 분류 결과를 확인합니다.
Requester (com.custom.requester)의 구현
Requester (com.custom.requester
)는 Greengrass IPC Client V2를 이용해 아래와 같이 Classifier (com.custom.ImageClassifier
)에게 추론을 요청합니다. 이를 위해 이미지의 위치 및 파일명에 대한 정보를 JSON 포맷으로 만든 후, 바이너리로 변환하여 publish 합니다.
from awsiot.greengrasscoreipc.clientv2 import GreengrassCoreIPCClientV2
ipc_client = GreengrassCoreIPCClientV2()
message = {
'image_dir': BASE_DIR,
'fname': 'pelican.jpeg'
}
publish_binary_message_to_topic(ipc_client, topic, json.dumps(message))
def publish_binary_message_to_topic(ipc_client, topic, message):
binary_message = BinaryMessage(message=bytes(message, 'utf-8'))
publish_message = PublishMessage(binary_message=binary_message)
ipc_client.publish_to_topic(topic=topic, publish_message=publish_message)
Classifier (com.custom.ImageClassifier
)를 통해 추론을 수행한 결과는 “local/result
” topic을 이용하여 아래처럼 확인합니다.
_, operation = ipc_client.subscribe_to_topic(topic="local/result", on_stream_event=on_stream_event,
on_stream_error=on_stream_error, on_stream_closed=on_stream_closed)
def on_stream_event(event: SubscriptionResponseMessage) -> None:
try:
message = str(event.binary_message.message, 'utf-8')
print('result: %s' % (message))
except:
traceback.print_exc()
Pub/Sub IPC를 이용해 엣지에 설치된 component들 사이에 메시지를 교환하기 위해서는 recipe을 아래와 같이 설정합니다. 여기서, aws.greengrass.ipc.pubsub
은 디바이스의 local component들 사이에 메시지를 교환하기 위한 IPC 서비스 식별자입니다.
"ComponentConfiguration": {
"DefaultConfiguration": {
"accessControl": {
"aws.greengrass.ipc.pubsub": {
"com.custom.requester:pubsub:1": {
"policyDescription": "Allows access to publish/subscribe to the topics.",
"operations": [
"aws.greengrass#PublishToTopic",
"aws.greengrass#SubscribeToTopic"
],
"resources": [
"local/inference",
"local/result"
]
}
}
}
}
recipe의 “Lifecycle”에서는 awsiotsdk와 같은 라이브러리를 설치하고, requester.py
를 실행합니다.
"Manifests": [{
"Platform": {
"os": "linux"
},
"Lifecycle": {
"Install": {
"RequiresPrivilege": "false",
"Script": "pip3 install awsiotsdk"
},
"Run": {
"RequiresPrivilege": "false",
"Script": "python3 -u {artifacts:path}/requester.py"
}
}
}]
Classifier (com.custom.ImageClassifier)의 구현
interface.py
는 “local/inference
” topic을 subscribe하고 있다가, 메시지를 받으면 classifier()
를 호출합니다.
from classifier import classifier
ipc_client = GreengrassCoreIPCClientV2()
topic = 'local/inference'
def on_stream_event(event: SubscriptionResponseMessage) -> None:
try:
message = str(event.binary_message.message, 'utf-8')
event_topic = event.binary_message.context.topic
logger.info("Received new message on topic %s: %s", topic, message)
# Inference
if event_topic == topic:
json_data = json.loads(message) # json decoding
result = classifier(json_data)
publish_binary_message_to_topic(ipc_client, "local/result", result)
classifier.py
는 아래와 같이 이미지를 로드하여 실제 추론을 수행하는 inference.py
의 handler()
를 호출하여 결과를 얻고, 가장 확률이 높은 결과를 리턴합니다.
from inference import handler
def classifier(data):
image_data = load_image(os.path.join(data['image_dir'], data['fname']))
event = {
'body': image_data
}
try:
result = handler(event,"")
return result['body'][0]['Label']
inference.py
에서는 기학습된 모델을 로딩하고 전달받은 이미지 데이터 크기를 변경(resize) 한 후 추론을 수행합니다. 아래에서처럼 load_model()
은 variant.DLR.ImageClassification.ModelStore
의 DLR model을 로딩하고, handler()
는 event을 받아서 body를 추출한후 모델에 맞는 크기로 변환합니다. 마지막으로 predict_from_image()
은 로딩한 모델로 추론을 수행하고 결과를 리턴합니다.
SCORE_THRESHOLD = 0.3
MAX_NO_OF_RESULTS = 5
SHAPE = (224, 224)
MODEL_DIR = '/greengrass/v2/packages/artifacts-unarchived/variant.DLR.ImageClassification.ModelStore/2.1.9/DLR-resnet50-x86_64-cpu-ImageClassification'
def load_model(model_dir):
model = DLRModel(model_dir, dev_type='cpu', use_default_dlr=False)
return model
model = load_model(MODEL_DIR)
def handler(event, context):
image_data = event['body']
cvimage = resize(image_data, SHAPE)
if cvimage is not None:
result = predict_from_image(model, cvimage)
return {
'statusCode': 200,
'body': result
}
def predict_from_image(model, image_data):
result = []
try:
model_output = model.run(image_data)
probabilities = model_output[0][0]
sort_classes_by_probability = argsort(probabilities)[::-1]
for i in sort_classes_by_probability[: MAX_NO_OF_RESULTS]:
if probabilities[i] >= SCORE_THRESHOLD:
result.append({"Label": str(synset[i]), "Score": str(probabilities[i])})
return result
recipe의 “Lifecycle
”에서는 libgl1, opencv-python등과 같은 라이브러리를 설치하고, interface.py
를 실행합니다.
"Manifests": [{
"Lifecycle": {
"Install": {
"RequiresPrivilege": "true",
"Script": "apt-get install libgl1 -y\n pip3 install --upgrade pip \n pip3 install scikit-build wheel opencv-python==4.6.0.66 dlr\n python -m pip install dlr\n pip3 install awsiotsdk"
},
"Run": {
"RequiresPrivilege": "true",
"Script": "python3 -u {artifacts:path}/interface.py"
}
}
또한, DLR model을 가진 Built-in public component를 설치하기 위하여, ComponentDependencies에서 아래와 같이 “variant.DLR.ImageClassification.ModelStore
“를 기술합니다.
"ComponentDependencies": {
"variant.DLR.ImageClassification.ModelStore": {
"VersionRequirement": ">=2.1.0 <2.2.0",
"DependencyType": "HARD"
}
}
AWS CDK를 이용한 Component 배포 준비
AWS CDK를 이용하여 Component를 배포합니다. cdk-greengrass-stack.ts에서는 S3 bucket을 생성하고, artifact를 복사한후 deployment를 이용해 배포합니다.
const s3Bucket = new s3.Bucket(this, "gg-depolyment-storage",{
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
removalPolicy: cdk.RemovalPolicy.DESTROY,
autoDeleteObjects: true,
publicReadAccess: false,
});
// copy artifact into s3 bucket
new s3Deploy.BucketDeployment(this, "UploadArtifact", {
sources: [s3Deploy.Source.asset("../src")],
destinationBucket: s3Bucket,
});
new greengrassv2.CfnDeployment(this, 'MyCfnDeployment', {
targetArn: `arn:aws:iot:ap-northeast-2:`+accountId+`:thing/`+deviceName,
components: {
"com.custom.requester": {
componentVersion: version_requester
},
"com.custom.ImageClassifier": {
componentVersion: version_ImageClassifier
},
"aws.greengrass.Cli": {
componentVersion: "2.9.2"
}
}
}
사전 준비 사항
이 솔루션을 사용하기 위해서는 사전에 아래와 같은 준비가 되어야 합니다.
배포하기
여기에서는 AWS의 개발환경인 AWS Cloud9을 이용하여 이미지 분류가 가능한 Greengrass Component를 배포하는 일련의 과정을 설명합니다. Cloud9은 브라우저만으로 코드를 작성, 실행 및 디버깅할 수 있는 클라우드 기반 IDE(통합 개발 환경)로서 Greengrass 디바이스 동작을 테스트하기에 유용합니다.
단계1: Greengrass 사용을 위한 IAM Role 및 Policy 설정
Greegrass를 처음 설치하는 경우에 아래와 같이 IAM Role과 Policy를 설정하여야 합니다. 이미 Greengrass를 사용중이라면 단계2로 이동합니다.
Authorize core devices to interact with AWS services에 따라 IAM Console의 Policy에서 [Create policy]를 선택한 후에 JSON Tab에서 아래의 policy를 입력하여, “GreengrassV2TokenExchangeRoleAccess
” 이름을 가지는 policy를 생성합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams",
"s3:GetBucketLocation"
],
"Resource": "*"
}
]
}
마찬가지로 S3로 부터 artifact를 가져올 수 있도록 S3에 대한 접근권한을 가지는 “GGv2WorkshopS3Policy
” policy를 아래와 같이 생성합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::*"
}
]
}
IAM Console의 Roles에서 [Create role]을 선택하여 “GreengrassV2TokenExchangeRole
” 이름을 가지는 role을 생성합니다. 이때, “trusted entities
”은 아래와 같이 설정합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "credentials.iot.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
이후, “GreengrassV2TokenExchangeRole
”을 찾아서, [Add permissions]으로 “GGv2WorkshopS3Policy
”과 “GreengrassV2TokenExchangeRoleAccess
”을 추가합니다.
디바이스에서 Greengrass를 프로비저닝하기 위하여, Minimal IAM policy for installer to provision resources에 따른 policy를 account에 추가하여야 합니다. 여기서는 “aws-policy-greengrass
” 이름의 policy를 생성하려고 합니다.
먼저, policy 생성에 필요한, account-id 정보를 AWS 콘솔화면에서 확인하거나, 아래 명령어로 확인합니다.
aws sts get-caller-identity --query account-id --output text
IAM Console의 Policies로 접속하여 [Create Policy]를 선택한 후 JSON 탭에서 아래와 같은 policy를 생성합니다. 이때, 아래의 “account-id
”에는 12자리 account번호를 입력합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateTokenExchangeRole",
"Effect": "Allow",
"Action": [
"iam:AttachRolePolicy",
"iam:CreatePolicy",
"iam:CreateRole",
"iam:GetPolicy",
"iam:GetRole",
"iam:PassRole"
],
"Resource": [
"arn:aws:iam::account-id:role/GreengrassV2TokenExchangeRole",
"arn:aws:iam::account-id:policy/GreengrassV2TokenExchangeRoleAccess"
]
},
{
"Sid": "CreateIoTResources",
"Effect": "Allow",
"Action": [
"iot:AddThingToThingGroup",
"iot:AttachPolicy",
"iot:AttachThingPrincipal",
"iot:CreateKeysAndCertificate",
"iot:CreatePolicy",
"iot:CreateRoleAlias",
"iot:CreateThing",
"iot:CreateThingGroup",
"iot:DescribeEndpoint",
"iot:DescribeRoleAlias",
"iot:DescribeThingGroup",
"iot:GetPolicy"
],
"Resource": "*"
},
{
"Sid": "DeployDevTools",
"Effect": "Allow",
"Action": [
"greengrass:CreateDeployment",
"iot:CancelJob",
"iot:CreateJob",
"iot:DeleteThingShadow",
"iot:DescribeJob",
"iot:DescribeThing",
"iot:DescribeThingGroup",
"iot:GetThingShadow",
"iot:UpdateJob",
"iot:UpdateThingShadow"
],
"Resource": "*"
}
]
}
“aws-policy-greengrass
” policy가 생성되면, IAM Console의 Users에서 현재 User에 “aws-policy-greengrass
” policy를 [Add permissions]로 추가합니다.
단계2: Cloud9 생성
Cloud9 Console에서 아래와 같이 [Name]을 입력합니다. Cloud9 생성시 서울리전(ap-northeast-2)에서 진행 합니다.
Platform은 “Ubuntu Server 18.04 LTS”을 선택하고, 메모리 2GiB이상의 Instance를 선택합니다. 여기에서는 t3.small을 선택하였습니다.
아래로 이동하여 [Create]를 선택하면 수분후에 Cloud9이 생성됩니다.
단계3: Greengrass 설치하기
Cloud9을 오픈하고 터미널을 실행합니다.
아래와 같이 Greengrass를 다운로드 합니다.
curl -s https://d2s8p88vqu9w66.cloudfront.net/releases/greengrass-nucleus-latest.zip > greengrass-nucleus-latest.zip && unzip greengrass-nucleus-latest.zip -d GreengrassCore
아래와 같이 디바이스 이름은 “GreengrassCore-18163f7ac3e
“, Group은 ggc_user:ggc_group
로 설치를 진행합니다. 디바이스 이름과 Group은 용도에 맞게 수정하여 사용합니다. 또한, 서울리전이 아닌 다른 리전 사용시에는 “aws-region” 옵션을 해당 리전에 맞게 변경합니다.
sudo -E java -Droot="/greengrass/v2" -Dlog.store=FILE -jar ./GreengrassCore/lib/Greengrass.jar \
--aws-region ap-northeast-2 \
--thing-name GreengrassCore-18163f7ac3e \
--thing-group-name GreengrassGroup \
--component-default-user ggc_user:ggc_group \
--provision true \
--setup-system-service true \
--deploy-dev-tools true
설치가 다 완료가 되면, Greengrass Console에서 아래와 같이 Greengrass core device로 “GreengrassCore-18163f7ac3e
“가 등록 된 것을 알 수 있습니다. 설치 후 Console 화면에 디바이스 정보가 노출되는데 수분정도 지연 될 수 있으니 보이지 않는 경우에 몇 분 후에 새로 고침 해봅니다.
단계4: CDK Deployment
여기에서는 CDK를 이용해 기계학습 알고리즘 추론을 IoT Greengrass에 배포하는 방법에 대해 설명합니다. 먼저 Image Classification via IoT Greengrass를 아래와 같이 Cloud9으로 다운로드 합니다.
curl https://aws-korea-tech-blog-public.s3.ap-northeast-2.amazonaws.com/20230126-GreenGrass-Image-Resnet-50/gg-image-classification/image-classification-via-iot-greengrass.zip -o image-classification.zip
이후 아래와 같이 다운로드한 코드의 압축을 풉니다.
mkdir ImageClassification && cd ImageClassification && unzip ../image-classification.zip
CDK 폴더로 이동하여 필요한 라이브러리를 설치합니다. “aws-cdk-lib”는 CDK V2 라이브러리입니다.
cd cdk-greengrass && npm install aws-cdk-lib
CDK를 처음 사용하는 경우에는 아래와 같이 bootstrap을 실행하여야 합니다. 여기서 account-id은 Account Number를 의미합니다.
cdk bootstrap aws://account-id/ap-northeast-2
Component들을 여러 개의 stack으로 구성하였으므로 아래와 같이 배포를 수행합니다.
cdk deploy --all
단계5: 배포 결과 확인
Greengrass Console – Components에서 아래와 같이 생성된 component 정보를 확인합니다.
Greengrass Console – Deployment에서 아래와 같이 배포상태를 확인합니다. 아래와 같이 Status가 “Completed”가 되어야 합니다. 만약 “Active”라면, 수분정도 기다립니다.
Requester (com.custom.requester)의 로그는 아래와 같이 확인합니다.
sudo tail -f /greengrass/v2/logs/com.custom.requester.log
아래 로그와 같이 Requester 동작에 필요한 패키지를 설치하고, requester (com.custom.requester
)을 실행합니다.
결과를 얻기 위해서 “local/result
” topic을 subscribe 합니다.
2023-01-04T12:44:00.550Z [INFO] (Copier) com.custom.requester: stdout. Successfully subscribed to topic: local/result. {scriptName=services.com.custom.requester.lifecycle.Run, serviceName=com.custom.requester, currentState=RUNNING}
Requester는 JSON 포맷으로 이미지의 위치와 파일명을 Classifier에게 전달합니다.
2023-01-04T12:44:50.618Z [INFO] (Copier) com.custom.requester: stdout. request: {"image_dir": "/greengrass/v2/packages/artifacts/com.custom.requester/1.0.0", "fname": "pelican.jpeg"}. {scriptName=services.com.custom.requester.lifecycle.Run, serviceName=com.custom.requester, currentState=RUNNING}
이때, Requester에게 전달된 이미지 분류 결과는 “pelican”으로서, 정상적으로 분류가 되었음을 알 수 있습니다.
2023-01-04T12:44:51.225Z [INFO] (Copier) com.custom.requester: stdout. result: pelican. {scriptName=services.com.custom.requester.lifecycle.Run, serviceName=com.custom.requester, currentState=RUNNING}
추론을 수행하는 Classifier (com.custom.ImageClassifier.log)
의 로그는 아래와 같이 확인합니다.
sudo tail -f /greengrass/v2/logs/com.custom.ImageClassifier.log
Classifier 로그를 보면, 아래와 같이 Requester로 부터 추론 요청을 받아서 “pelican”이라는 결과를 얻고 있습니다.
2023-01-04T12:44:50.619Z [INFO] (Copier) com.custom.ImageClassifier: stdout. Received new message on topic local/inference: {"image_dir": "/greengrass/v2/packages/artifacts/com.custom.requester/1.0.0", "fname": "pelican.jpeg"}. {scriptName=services.com.custom.ImageClassifier.lifecycle.Run.Script, serviceName=com.custom.ImageClassifier, currentState=RUNNING}
2023-01-04T12:44:51.223Z [INFO] (Copier) com.custom.ImageClassifier: stdout. result: pelican. {scriptName=services.com.custom.ImageClassifier.lifecycle.Run.Script, serviceName=com.custom.ImageClassifier, currentState=RUNNING}
리소스 정리하기
배포에 사용했던 S3의 아티팩트(artifact)와 IoT Greengrass에 있는 Component, Deployment는 아래 명령어를 통해 삭제할 수 있습니다. 아래 명령어로 디바이스에 이미 배포된 Component들이 삭제되지 않습니다. 디바이스의 Component들은 Deployment에서 재배포시에 모든 Component를 리스트에서 제외하고 배포하여야 삭제가 가능합니다.
$ cdk destroy --all
결론
AWS에서 Built-in public component로 제공하는 이미지 분류 모델을 custom component에서 활용하는 방법을 설명하였습니다. 이를 통해 네트워크 상황과 관련없이 IoT 디바이스에서 이미지 분류를 수행하고, 사용자의 목적에 따라 자유롭게 커스터마이즈 할 수 있습니다. 추가적으로 AWS CDK를 이용해 Greengrass 컴포넌트들을 쉽게 배포하는 것을 예제와 함께 설명하였습니다. 이미지 분류는 IoT 디바이스에서 다양한 용도로 활용될 수 있는 유용한 기계학습 기법으로서 IoT디바이스의 활용성을 높이고 많은 비지니스에 효과적으로 이용될 수 있습니다.
참고
아래의 링크에서 실습 소스 파일 및 기계 학습(ML)과 관련된 자료를 확인하실 수 있습니다.