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

2020.05.11

1 はじめに

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

Amazon SageMaker の組み込みのイメージ分類アルゴリズム(image-classification)では、イメージ形式のデータセットが利用可能です。

今回は、Amazon SageMaker Ground Truth(以下、Ground Truth)で作成したデータを変換して、このイメージ形式のデータセットを作成してみました。

2 Ground Truth

最初に、プライベートなプロジェクトを作成して、ごく少数ですが、データセットを作成しました。

設定したラベルは以下の3種類です。

AHIRU:0
DOG:1
CAT:2

作業が完了すると、Output dataset locationに示されるバケットにデータが保存されます。 生成されるデータは、output.manifestというファイルです。

output.manifestは、各画像ごと1行のラベル情報となっており、1行を見やすく展開すると以下のようになります。

下の例では、ここでmy-projectが、プロジェクトにつけた名前であり、s3://sagemaker-working-bucket/my-project/AHIRU-10001.jpgは、ラベルが0 ( AHIRU ) であると言う事になります。

{
    "source-ref": "s3://sagemaker-working-bucket/my-project/AHIRU-10001.jpg",
    "my-project": 0,
    "my-project-metadata": {
        "confidence": 0.87,
        "job-name": "labeling-job/my-project",
        "class-name": "AHIRU",
        "human-annotated": "yes",
        "creation-date": "2020-05-10T04:35:07.616040",
        "type": "groundtruth/image-classification"
    }
}

3 イメージ形式のデータ

SageMakerのイメージ分類で使用されるイメージ形式のデータは、画像と.lstファイルで構成されています。 下記は、.lstファイルの一例です。

1行が1画像の情報となっており、3カラム目が、画像のファイル名で、2カラム目が、そのラベルです。 1カラム目は、インデックスであり、1つのファイルの中で1から始まる連番です。(順序は問わない)

なお、ファイル名は、S3に配置する際に、パラメータで指定したパスからの相対パスです。

1   0   AHIRU-1589071571245159.jpg
2   0   AHIRU-1589071564049605.jpg
3   0   AHIRU-1589071562074867.jpg
25  2   CAT-1589071643325546.jpg
26  2   CAT-1589071641738042.jpg
27  2   CAT-1589071640123267.jpg

4 学習データと検証データ

教師あり学習のアルゴリズムである、「イメージ分類」では、学習用のデータと、検証用のデータが必要です。

データを単純に分割すると、特定のラベルのデータが、全て学習用に分割されてしまうような事が起きる可能性があるため、変換対象となる全てのデータから、含まれるラベルの数をカウントし、サンプル数の少ないラベルから順に、分割するようにしました。

# ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
def getLabel(dataList):
    labels = {}
    for data in dataList:
        if(data.label in labels):
            labels[data.label] += 1
        else:
            labels[data.label] =1
    # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
    labels = sorted(labels.items(), key=lambda x:x[1])
    return labels

# ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
labels = getLabel(dataList)

作成されたデータは、以下のプレフィックスでS3に保存します。

  • train (学習用の画像)
  • train_lst (学習用のlstファイル)
  • validation (検証用の画像)
  • validation_lst (検証用のlstファイル)

5 コード

変換を行う、すべてコードは、以下のとおりです。以下を定義を設定して利用可能です。

  • inputPath 元データ(画像ファイルとoutput.manifestを置く)
  • outputPath 出力先
  • ratio 学習データと検証データの分割比率
import json
import glob
import os
import shutil
import copy

# 定義
inputPath = '/tmp/input'
outputPath = '/tmp/output'
manifest = 'output.manifest'
# 分割の比率
ratio = 0.8 # 8:2

# 1件のデータを表現するクラス
class Data():
    def __init__(self, src):
        # プロジェクト名の取得
        for key in src.keys():
            index = key.rfind("-metadata")
            if(index!=-1):
                projectName = key[0:index]

        # 画像名の取得
        self.__imgFileName = os.path.basename(src["source-ref"])
        # クラスID
        self.__clsId = src[projectName]
        # ラベル名
        metadata = src[projectName + '-metadata']
        self.__label = metadata["class-name"]

    @property
    def label(self):
        return self.__label

    @property
    def imgFileName(self):
        return self.__imgFileName

    @property
    def clsId(self):
        return self.__clsId

    # 指定されたラベルを含むかどうか
    def exsists(self, label):
        for annotation in self.annotations:
            if(annotation["label"] == label):
                return True
        return False

# dataListをラベルを含むものと、含まないものに分割する
def deviedDataList(dataList, label):
    targetList = []
    unTargetList = []
    for data in dataList:
        if(data.label == label):
            targetList.append(data)
        else:
            unTargetList.append(data)
    return (targetList, unTargetList)

# ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
def getLabel(dataList):
    labels = {}
    for data in dataList:
        if(data.label in labels):
            labels[data.label] += 1
        else:
            labels[data.label] =1
    # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
    labels = sorted(labels.items(), key=lambda x:x[1])
    return labels

# output.manifestを読み込む
def getDataList(inputPath, manifest):
    dataList = []
    with open("{}/{}".format(inputPath, manifest), 'r') as f:
        srcList = f.read().split('\n')
        for src in srcList:
            if(src != ''):
                json_src = json.loads(src)
                dataList.append(Data(json.loads(src)))
    return dataList

# 出力ファルダに保存
def save(inputPath, dataPath, lstPath, dataList):
    # 画像のコピー
    for i,data in enumerate(dataList):
        shutil.copyfile("{}/{}".format(inputPath, data.imgFileName),"{}/{}".format(dataPath, data.imgFileName))

    with open(lstPath, mode='w') as f:
        for i,data in enumerate(dataList):
            f.write("{}\t{}\t{}\n".format(i+1, data.clsId, data.imgFileName))
def main():

    # 出力先フォルダ生成
    train = "{}/train".format(outputPath)
    validation = "{}/validation".format(outputPath)
    train_lst = "{}/train_lst".format(outputPath)
    validation_lst = "{}/validation_lst".format(outputPath)
    os.makedirs(outputPath, exist_ok=True)
    os.makedirs(train, exist_ok=True)
    os.makedirs(validation, exist_ok=True)
    os.makedirs(train_lst, exist_ok=True)
    os.makedirs(validation_lst, exist_ok=True)

    # 全てのJSONデータを読み込む
    dataList = getDataList(inputPath, manifest)
    print("全データ: {}件 ".format(len(dataList)))

    # ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
    labels = getLabel(dataList)

    # 保存済みリスト
    storedList = [] 
    # 分割先
    trainList = []
    validationList = []

    # ラベルの数の少ないものから優先して分割する
    for i,label in enumerate(labels):
        # dataListをラベルが含まれるものと、含まないものに分割する
        (targetList, unTargetList) = deviedDataList(dataList, label[0])
        # 保存済みリストから、当該ラベルで既に保存済の件数をカウントする
        (include, notInclude) = deviedDataList(storedList, label[0])
        storedCounst = len(include)
        # train用に必要な件数
        count = int(label[1] * ratio) - storedCounst
        # train側への保存
        for i in range(count):
            data = targetList.pop()
            trainList.append(data)
            storedList.append(data)
        # validation側への保存
        for data in targetList:
            validationList.append(data)
            storedList.append(data)
        print("{} ({}件) => {}:{}".format(label[0], label[1], format(count), len(targetList)))
        dataList = unTargetList
    # 出力ファルダに保存
    save(inputPath, train, train_lst + "/train.lst", trainList)
    save(inputPath, validation, validation_lst + "/validation.lst", validationList)

    print("\ntrain:{} validation:{}".format(len(trainList), len(validationList)))

main()

例えば、各ラベル(ARUHI、DOG及び、CAT)が、30件で、合計90件のデータを変換すると、以下のようなログが出力されます。

全データ: 90件 
AHIRU (30件) => 24:6
CAT (30件) => 24:6
DOG (30件) => 24:6

train:72 validation:18

6 イメージ分類

学習の終わったモデルは、下記の記事で利用したコードで動作確認してみました。「画面の一部」を推論の対象にしています。
参考:[Amazon SageMaker] イメージ分類で画面の一部に写っているものを検出してみました

7 最後に

今回は、Ground Truthで作成されたデータをイメージ分類で利用するためのデータに変換する作業を行ってみました。

イメージ形式は、簡単にデータが作成できますが、train_lst及び、validation_lstの中に、「.lstの拡張子のファイルが1つだけ」という 形式に、注意が必要そうです。