[Amazon SageMaker] よく似たパッケージの商品をビルトインアルゴリズムのイメージ分類で判別してみました

2020.06.28

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

1 はじめに

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

前回、よく似たパッケージをAmazon Rekognition(以下、Rekognition)で分類してみました。

各商品50枚の画像(全部で250枚)で、あまりに綺麗に分類できていたので、同じ画像を使って、Amazon SageMaker(以下、SageMaker)のイメージ分類(組み込みアルゴリズム)で同じようなモデルが作成できるかを試してみました。

結論、実力不足で難しかったです。

以下は、今回作成したモデルで推論している様子です。実は、カメラをこれ以上離すと、結構、誤検知が発生してしまい、Rekognitionで作成したモデルより、ぜんぜん性能悪いです。

2 データ

(1) データ

データは、前回のものと同じです。動画(800 * 600 24fps)から切り出した各商品50枚の画像です。


参考:[Amazon SageMaker] 回転台を使って撮影した動画で、Amazon SageMaker Ground Truth形式のデータセット(Image Classification)を作ってみました

(2) 増幅

各データ50件で学習すると、簡単に収束するのですが、精度が良くなかったため、データの増幅を行いました。

下記のパラメータで、各画像50件から200件に増幅しました。

convertList = [
    {"function": saturation, "param": 0.6}, # 彩度
    {"function": contrast, "param": 0.8}, # コントラスト
    {"function": noise, "param": 0.01}, # ごま塩ノイズ
]
全データ: 250件 
増幅後データ: 1000件


参考:[Amazon SageMaker] Amazon SageMaker Ground Truth で作成したデータをOpenCVで増幅してみました

(3) イメージ形式データセット

Ground Truth形式となっている上記のデータを、イメージ形式のデータセットに変換してS3にアップロードしています。

全データ: 1000件 
EGG (200件) => 160:40
SABA (200件) => 160:40
MEET (200件) => 160:40
HAMBURGER (200件) => 160:40
POTATO (200件) => 160:40

train:800 validation:200

学習用のデータ数は、800件となっています。


参考:[Amazon SageMaker] Amazon SageMaker Ground Truth で作成したデータをイメージ分類で利用可能なイメージ形式に変換してみました

3 学習

学習は、ml.p3.2xlarge(50G)で397秒でした。

設定したパラメータは、以下のとおりです。

early_stopping  false
epochs  30
learning_rate   0.001
mini_batch_size 32
multi_label 0
num_classes 5
num_layers  152
num_training_samples    800
optimizer   sgd
precision_dtype float32
use_pretrained_model    1
use_weighted_loss   0

進行の状況です。

epoch   Train-accuracy  Validation-accuracy
-----------------------------------------
0       0.104   0.208
5       0.826   0.917
10      0.954   0.917
15      1.0     0.951
20      0.998   0.943
25      0.969   0.906
29      1.0     0.979

出来上がったモデルは、208.3MByteとなっていました。

4 確認

デプロイして、動作確認したコードは、以下のとおりです。

import json
import datetime
import cv2
import numpy as np
from boto3.session import Session
from PIL import ImageFont, ImageDraw, Image

PROFILE = 'developer'
END_POINT = 'sampleEndPoint'

CLASSES = ['EGG','SABA','MEET','HAMBURGER','POTATO']
NAMES = ['たまごポテトサラダ','さばの塩焼','ミートボール','デミグラスハンバーグ','明太ポテトサラダ']

DEVICE_ID = 0 # Webカメラ
HEIGHT = 600
WIDTH = 800

class SageMaker():
    def __init__(self, profile, endPoint):
        self.__end_point = endPoint
        self.__client = Session(profile_name=profile).client('sagemaker-runtime')

    def invoke(self, image):
        data = self.__client.invoke_endpoint(
            EndpointName=self.__end_point,
            Body=image,
            ContentType='image/jpeg'
        )
        return json.loads(data['Body'].read())

def createArea(width, height, w, h, bias):
    x_1 = int(width/2-w/2)
    x_2 = int(width/2+w/2)
    y_1 = int(height/2-h/2) - bias 
    y_2 = int(height/2+h/2) - bias
    return (x_1, y_1, x_2, y_2)

def putText(image, text, point, size, color):
    font = ImageFont.truetype("./GenShinGothic-Bold.ttf", size)
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
    image = Image.fromarray(image)
    draw = ImageDraw.Draw(image)
    draw.text(point, text, color, font)
    image = np.asarray(image)
    return cv2.cvtColor(image, cv2.COLOR_RGB2BGR)

def main():

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

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

    (x_1, y_1, x_2, y_2) = createArea(width, height, 400, 400, 50)

    sageMake = SageMaker(PROFILE, END_POINT)

    while True:

        # カメラ画像取得
        _, frame = cap.read()
        if(frame is None):
            continue

        img = frame[y_1: y_2, x_1: x_2]

        # 推論
        _, jpg = cv2.imencode('.jpg', img)
        results = sageMake.invoke(jpg.tostring())
        probabilitys = {}
        for i, result in enumerate(results):
            probabilitys[CLASSES[i]] = result
        probabilitys = sorted(probabilitys.items(), key = lambda x:x[1], reverse=True)
        (name, probability) = probabilitys[0]

        # 結果表示
        text = "{} {}".format(NAMES[CLASSES.index(name)], probability)
        frame = putText(frame, text, (20, int(height) - 120), 60, (255, 255, 255))
        # 対象範囲の枠表示
        frame = cv2.rectangle(frame, (x_1, y_1), (x_2, y_2), (155, 155, 155), 1)

        # フレーム表示
        cv2.imshow('frame', frame)
        cv2.waitKey(1)

    cap.release()
    cv2.destroyAllWindows()

main()

5 最後に

今回は、よく似たパッケージの商品でイメージ分類モデルを作成してみました。

正直、Rekognitionの精度には、全然追いついてませんが、Rekognitionの内部で、どのように処理されているのか(画像増幅やパラメータの調整など)に思いを馳せながら、1日色々試していました。

体感として、ある程度の精度までは、簡単に出るのですが、ある一線を超えると精度を上げるのが非常に難しい印象です。頑張って勉強します。