Amazon SageMakerで猫と犬の検出と種類の特定をしてみた

はじめに

当エントリでは、Amazon SageMakerを使って猫と犬の検出と分類をやってみた内容の紹介を行います。

背景

以前ねじの検出と分類(記事)を行ったんですが、その際に作成したスクリプト、方法を流用して他のテーマに応用できないかという考えのもと、猫と犬のデータセットで試してみることにしました。

概要

機械学習アルゴリズムはAamazon SageMakerの組み込みアルゴリズムである物体検出(ResNet-SSD)と画像分類(ResNet)を使用して、猫と犬の顔の検出と分類を行います。検出と種類の分類を分けるのは、中間層を多く出来る画像分類の方が細かい差異の抽出が可能だろうという考えからです。
教師データには、オックスフォード大学のヴィジュアルジオメトリグループが公開している犬と猫の画像データセットを使用します。

ざっくりとした作業の流れは以下の通りです。

  1. 教師データの取得
  2. ラベルデータをxml形式からlst形式に変換する
  3. 分類器用教師データの加工(増幅やデータの分離など)
  4. 分類器用教師データをRecordIO形式に変換して、S3にアップロード
  5. 分類器の学習(ハイパーパラメータのチューニング含む)
  6. 検出器用教師データ加工(データの分離など)
  7. 検出器用教師データをRecordIO形式に変換して、S3にアップロード
  8. 検出器の学習(ハイパーパラメータのチューニング含む)
  9. 分類器と検出器のモデルのデプロイ
  10. 画像から猫と犬の検出と分類
  11. エンドポイントの削除

実際には3~8までを何度も繰り返して、学習データやハイパーパラメータを調整することが必要になります。(時には加工処理のバグ潰しに追われる事も...)

やってみた

概要で説明した内容に沿って、進めていきます。所々でimgmlutilという自前のモジュールを使用しています。

IAMロールとセッションの取得

学習やエンドポイントの起動時に使用するIAMロールと、SageMakerで操作を行うときのセッションの取得を行います。

import sagemaker
from sagemaker import get_execution_role

role = get_execution_role()
sess = sagemaker.Session()

教師データの取得

import imgmlutil

imgmlutil.download_file('http://www.robots.ox.ac.uk/~vgg/data/pets/data/images.tar.gz')
imgmlutil.download_file('http://www.robots.ox.ac.uk/~vgg/data/pets/data/annotations.tar.gz')
!tar -zxvf images.tar.gz
!tar -zxvf annotations.tar.gz

ラベルデータをlst形式に変換

取得したデータセットのラベルデータはPascal VOCで使用するXML形式なので、それをlst形式に変換します。
合わせて、クラスリスト(ラベルIDと名称の対照表)、クラスマップ(ラベルの大カテゴリとそれに属する小カテゴリの関連データ)を作成し、jsonファイルに書き出します。

import imgmlutil
imgmlutil.create_lst_from_pascalvoc_dir('./annotations/xmls', './data')

画像分類モデルの作成

画像増幅器の定義

画像にノイズを加えて汎化性能を高めることを目的に画像増幅を行います。そのための増幅方法を定義します。

import imgaug as ia
from imgaug import augmenters as iaa
import random

# シードを固定
ia.seed(1)
random.seed(1)

# 画像増幅のためのaugmentorを定義
aug_templates = [
    iaa.Noop(), # 無変換
    iaa.CoarseDropout((0.03, 0.15), size_percent=(0.02, 0.25)), #ところどころ欠落させる
    iaa.CoarseDropout((0.03, 0.15), size_percent=0.02, per_channel=0.8), # ところどころ色を変える
    iaa.CoarseSaltAndPepper(0.2, size_percent=(0.05, 0.1)), # 白と黒のノイズ
    iaa.WithChannels(0, iaa.Affine(rotate=(0,10))), # 赤い値を傾ける
    iaa.FrequencyNoiseAlpha( # 決まった形のノイズを加える
        first=iaa.EdgeDetect(1),
        per_channel=0.5
    ),
    iaa.AddToHueAndSaturation(value=25), # 色調と彩度に値を追加
    iaa.Emboss(alpha=1.0, strength=1.5), # 浮き出し加工
    iaa.Superpixels(n_segments=128, p_replace=0.25) # superpixel表現にして、各セル内を一定確率でセルの平均値で上書きする
]

# 反転系のaugmentor
aug_rotate_templates=[
    iaa.Fliplr(1.0),
    iaa.Flipud(1.0)
]

# 画像増幅に使用するaugmentor
img_augmentors = [
    iaa.SomeOf(1, aug_templates),
    iaa.SomeOf(1, aug_templates),
    iaa.SomeOf(2, aug_templates),
    iaa.OneOf(aug_rotate_templates)
]

教師データの加工

画像分類モデルの教師データ用に画像を加工します。画像をリサイズ・増幅し、学習用と検証用にデータを分割します。

from os import path
import imgmlutil

# 画像サイズ(変換後の画像サイズ: img_edge_size * img_edge_size)
img_edge_size = 224

# lstファイルの入力パス
input_lst_path = './data/lst.lst'

# 入力する画像が入っているディレクトリのパス
input_img_root_path = './images'

# 出力先
output_root_path = './data/classification_auged/'

# 除外したいデータのクラス
except_class_list = []

# 処理実行(戻り値:trainとvalのそれぞれのデータ数)
cla_data_count = imgmlutil.process_image_for_classification(
    validate_ratio=0.3,
    test_ratio=0,
    validate_augment=False,
    test_augment=False,
    img_augmentors=img_augmentors,
    img_edge_size=img_edge_size,
    input_lst_path=input_lst_path,
    input_img_root_path=input_img_root_path,
    output_root_path=output_root_path,
    except_class_list=except_class_list)

教師データをRecordIO形式に加工して、S3にアップロード

import imgmlutil
from os import path
output_root_path = './data/classification_auged/'

# 保存場所
bucket_name = "bucket_name"
s3_classification_prefix = 'cat_dog_detect_and_classify/classification/'
cla_s3_train_rec = path.join(s3_classification_prefix, 'train.rec')
cla_s3_val_rec = path.join(s3_classification_prefix, 'val.rec')

# 処理実行(学習用と検証用)
imgmlutil.create_recordio_and_upload_to_s3( path.join(output_root_path, 'train'), bucket_name, cla_s3_train_rec)
imgmlutil.create_recordio_and_upload_to_s3( path.join(output_root_path, 'val'), bucket_name, cla_s3_val_rec)

学習

画像分類モデルのハイパーパラメータチューニングを行います。
ハイパーパラメータチューニングはかなり時間とお金がかかる場合があるので、注意が必要です。

import time
from time import gmtime, strftime
import sagemaker
from sagemaker.amazon.amazon_estimator import get_image_uri
from sagemaker.tuner import IntegerParameter, CategoricalParameter, ContinuousParameter, HyperparameterTuner
import boto3

# 加工処理から引き継ぐパラメータ
bucket_name = bucket_name
cla_s3_train_rec = cla_s3_train_rec
cla_s3_val_rec =  cla_s3_val_rec
s3_classification_prefix = s3_classification_prefix
train_data_count = cla_data_count['train']

# 入力データ定義
train_data = sagemaker.session.s3_input( 's3://'+path.join(bucket_name, cla_s3_train_rec), distribution='FullyReplicated',
                        content_type='application/x-recordio', s3_data_type='S3Prefix')
validation_data = sagemaker.session.s3_input( 's3://'+path.join(bucket_name, cla_s3_val_rec), distribution='FullyReplicated',
                             content_type='application/x-recordio', s3_data_type='S3Prefix')
data_channels = { 'train':train_data, 'validation':validation_data }

# モデルアーティファクトの出力先
s3_output_location = 's3://' + path.join(bucket_name, s3_classification_prefix, 'output', time.strftime('-%Y-%m-%d-%H-%M-%S', time.gmtime()))


# 学習に使用するコンテナイメージ
training_image = get_image_uri(boto3.Session().region_name, 'image-classification')

# 画像分類の学習時の設定
cl_model = sagemaker.estimator.Estimator(training_image,
                                         role,
                                         train_instance_count=1,
                                         train_instance_type='ml.p3.16xlarge',
                                         train_volume_size = 20,
                                         train_max_run = 7200,
                                         input_mode= 'File',
                                         output_path=s3_output_location,
                                         sagemaker_session=sess)

# ハイパーパラメータの設定
cl_model.set_hyperparameters(image_shape='3,224,224',
                             num_layers=152,
                             use_pretrained_model=1,
                             num_classes=len(class_list),
                             mini_batch_size=32,
                             epochs=2,
                             learning_rate=0.001,
                             lr_scheduler_step=10,
                             top_k=3,
                             optimizer='adam',
                             checkpoint_frequency=2,
                             momentum=0.9,
                             weight_decay=0.0005,
                             num_training_samples=train_data_count)

# ハイパーパラメータチューニングの探索範囲設定
hyperparameter_ranges  = {'mini_batch_size': IntegerParameter(32, 128),
                        'learning_rate': ContinuousParameter(1e-6, 0.5),
                         'optimizer': CategoricalParameter(['sgd', 'adam', 'rmsprop', 'nag']),
                        'momentum': ContinuousParameter(0, 0.999),
                        'weight_decay': ContinuousParameter(0, 0.999),
                        'beta_1': ContinuousParameter(1e-6, 0.999),
                        'beta_2': ContinuousParameter(1e-6, 0.999),
                        'eps': ContinuousParameter(1e-8, 1.0),
                        'gamma': ContinuousParameter(1e-8, 0.999)}

# ハイパーパラメータチューニングの目的関数
objective_metric_name = 'validation:accuracy'


# ハイパーパラメータチューニング用のチューナー定義
tuner = HyperparameterTuner(cl_model,
                            objective_metric_name,
                            hyperparameter_ranges,
                            max_jobs=30,
                            max_parallel_jobs=2)

# チューニング開始(稼働時間に対してお金がかかるので注意!)
tuner.fit(inputs=data_channels, logs=True, wait=False, include_cls_metadata=False)

物体検出モデルの作成

教師データの加工

物体検出モデルの教師データ用に画像を加工します。画像をリサイズ・増幅し、学習用と検証用にデータを分割します。

from os import path
import imgmlutil
import json
merged_root_path = './data/merged'

# クラスの変換表データ(subに入っているクラスのラベルを該当するlabel_idに変換するためのマップデータ)
with open('./data/class.json') as f:
    class_data = json.load(f)
class_list = class_data['class_list']
class_map = list(class_data['class_map'].values())

# 入出力先定義
input_lst_path = path.join(merged_root_path, 'lst.lst')
input_img_root_path = path.join(merged_root_path, 'img')
output_root_path = './data/detection_auged/'

# 変換したい画像サイズ(img_edg_size * img_edge_sizeの正方形に変換する)
img_edge_size = 512

# 物体検出は画像増幅しない
img_augmentors = []
# 処理を実行(戻り値:trainとvalのそれぞれのデータ数)
det_data_count = imgmlutil.process_image_for_detection(
    validate_ratio=0.3,
    test_ratio=0.3,
    validate_augment=False,
    test_augment=False,
    img_augmentors=img_augmentors,
    img_edge_size=img_edge_size,
    input_lst_path=input_lst_path,
    input_img_root_path=input_img_root_path,
    output_root_path=output_root_path,
    class_map=class_map)

教師データをRecordIO形式に加工して、S3にアップロード

import imgmlutil
from os import path

# 保存場所
bucket_name = "bucket_name"
s3_detection_prefix = 'cat_dog_detect_and_classify/detection/'
det_s3_train_rec = path.join(s3_detection_prefix, 'train.rec')
det_s3_val_rec = path.join(s3_detection_prefix, 'val.rec')


# 処理実行(学習用と検証用)
imgmlutil.create_recordio_and_upload_to_s3_from_lst(
    path.join(output_root_path, 'train'),
    path.join(output_root_path, 'train.lst'),
    bucket_name,
    det_s3_train_rec)

imgmlutil.create_recordio_and_upload_to_s3_from_lst(
    path.join(output_root_path, 'val'),
    path.join(output_root_path, 'val.lst'),
    bucket_name,
    det_s3_val_rec)

学習

画像分類モデルのハイパーパラメータチューニングを行います。
ハイパーパラメータチューニングはかなり時間とお金がかかる場合があるので、注意が必要です。

import time
from time import gmtime, strftime
import sagemaker
from sagemaker.amazon.amazon_estimator import get_image_uri
from sagemaker.tuner import IntegerParameter, CategoricalParameter, ContinuousParameter, HyperparameterTuner
import boto3

# 加工処理から引き継ぐパラメータ
bucket_name = bucket_name
cla_s3_train_rec = cla_s3_train_rec
cla_s3_val_rec =  cla_s3_val_rec
s3_classification_prefix = s3_classification_prefix
train_data_count = cla_data_count['train']

# 入力データ定義
train_data = sagemaker.session.s3_input( 's3://'+path.join(bucket_name, cla_s3_train_rec), distribution='FullyReplicated',
                        content_type='application/x-recordio', s3_data_type='S3Prefix')
validation_data = sagemaker.session.s3_input( 's3://'+path.join(bucket_name, cla_s3_val_rec), distribution='FullyReplicated',
                             content_type='application/x-recordio', s3_data_type='S3Prefix')
data_channels = { 'train':train_data, 'validation':validation_data }

# モデルアーティファクトの出力先
s3_output_location = 's3://' + path.join(bucket_name, s3_classification_prefix, 'output', time.strftime('-%Y-%m-%d-%H-%M-%S', time.gmtime()))


# 学習に使用するコンテナイメージ
training_image = get_image_uri(boto3.Session().region_name, 'image-classification')

# 画像分類の学習時の設定
cl_model = sagemaker.estimator.Estimator(training_image,
                                         role,
                                         train_instance_count=1,
                                         train_instance_type='ml.p3.16xlarge',
                                         train_volume_size = 20,
                                         train_max_run = 7200,
                                         input_mode= 'File',
                                         output_path=s3_output_location,
                                         sagemaker_session=sess)

# ハイパーパラメータの設定
cl_model.set_hyperparameters(image_shape='3,224,224',
                             num_layers=152,
                             use_pretrained_model=1,
                             num_classes=len(class_list),
                             mini_batch_size=32,
                             epochs=2,
                             learning_rate=0.001,
                             lr_scheduler_step=10,
                             top_k=3,
                             optimizer='adam',
                             checkpoint_frequency=2,
                             momentum=0.9,
                             weight_decay=0.0005,
                             num_training_samples=train_data_count)

# ハイパーパラメータチューニングの探索範囲設定
hyperparameter_ranges  = {'mini_batch_size': IntegerParameter(32, 128),
                        'learning_rate': ContinuousParameter(1e-6, 0.5),
                         'optimizer': CategoricalParameter(['sgd', 'adam', 'rmsprop', 'nag']),
                        'momentum': ContinuousParameter(0, 0.999),
                        'weight_decay': ContinuousParameter(0, 0.999),
                        'beta_1': ContinuousParameter(1e-6, 0.999),
                        'beta_2': ContinuousParameter(1e-6, 0.999),
                        'eps': ContinuousParameter(1e-8, 1.0),
                        'gamma': ContinuousParameter(1e-8, 0.999)}

# ハイパーパラメータチューニングの目的関数
objective_metric_name = 'validation:accuracy'


# ハイパーパラメータチューニング用のチューナー定義
tuner = HyperparameterTuner(cl_model,
                            objective_metric_name,
                            hyperparameter_ranges,
                            max_jobs=30,
                            max_parallel_jobs=2)

# チューニング開始(稼働時間に対してお金がかかるので注意!)
tuner.fit(inputs=data_channels, logs=True, wait=False, include_cls_metadata=False)

モデルのデプロイ

学習して得たモデルアーティファクトからモデルを作成し、エンドポイントに展開します。この操作の方法についてはこちらのエントリをご参照ください。

検出と分類

検出器と分類器の設定

検出器と分類器を作成します。先ほど立ち上げたエンドポイントの名前、モデルに入力するコンテンツタイプ(データ形式)、エンドポイントからのレスポンスのデシリアライザをそれぞれに設定します。

from sagemaker.predictor import json_deserializer
classifier_end_name = 'hoge-classifier-endpoint'
detector_end_name = 'hoge-detector-endpoint'

classifier = sagemaker.predictor.RealTimePredictor(classifier_end_name, sess, content_type='image/jpeg', deserializer=json_deserializer)
detector = sagemaker.predictor.RealTimePredictor(detector_end_name, sess, content_type='image/jpeg', deserializer=json_deserializer)

画像の読み込み

from PIL import Image
from os import path

file_name = './path/to/img'

# 画像ファイルを読み込む(ついでにjpegに変換する)
af_file_name = path.splitext(file_name)[0] + 'jpg'
Image.open(file_name).convert('RGB').save(af_file_name, 'JPEG')
file_name = af_file_name

with open(file_name, 'rb') as image:
    f = image.read()
    b = bytearray(f)

検出と分類の実行

先ほど読み込んだ画像から猫と犬を検出し、その種類を特定します。

import imgmlutil
# クラスマップデータを読み込む
with open('./data/class.json') as f:
    class_data = json.load(f)
class_list = class_data['class_list']
class_map = list(class_data['class_map'].values())


# thresholdより低い判定率のものは表示しない
threshold = 0.4

# 検出結果を描画(内部で種類の特定も行う)
detections = detector.predict(b)['predictions'][0]
imgmlutil.classify_and_visualize_detection(file_name, detections['prediction'], class_map, list(class_list.keys()), classifier, threshold)

検出分類結果

何個かの画像で試してみました。その中からいくつか紹介します。 みんなの犬図鑑みんなの猫図鑑より画像をお借りしました。

どちらも犬と猫の検出は正しく出来ています!ただし、その顔から種類を分類することは出来ていません。
分類の学習と推論に使用するのは顔だけなので特徴量を上手く取るのが難しかった可能性、毛色の変化に対応できるほどの汎化性能を持ち合わせていない可能性、過学習などなど...こういったことが上手く分類できなかった要因になってるんではないかと思っています。

さいごに

今回はAmazon SageMakerを利用して猫と犬の検出と分類を行いました。検出はかなりの精度で行えましたが、分類に関しては残念ながら上手くいきませんでした。
画像を扱う機械学習はマシンパワーが必要になるため、変な進め方をしてしまうと札束がすぐに羽ばたいていってしまいます。 SageMakerを利用することでかなり簡単に学習やハイパーパラメータチューニングができますが、その分お金もかかります。場合によっては自分で環境構築を行い、スポットインスタンス上でチューニングすることも考えた方がいいかもしれません。

画像分類や物体検出を試してみようと思っている方の参考になれば幸いです。
最後までお読みいただき、ありがとうございましたー!

参考