この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
1 はじめに
CX事業本部の平内(SIN)です。
Amazon SageMaker の組み込みの物体検出アルゴリズム(object-detection)では、RecordIO形式のデータセットが利用可能です。
今回は、Amazon SageMaker Ground Truth(以下、Ground Truth)で作成したデータを変換して、このRecordIO形式のデータセットを作成してみました。
実は、物体検出アルゴリズム(object-detection)では、RecordIO形式の他にも、イメージ形式、拡張形式という3種類のデータセットが利用可能であり、拡張形式は、Ground Truthの出力を指しているので、わざわざ変換しなくても利用可能です。(fit()の際に、RecordIO形式に自動的に変換されている模様)
しかし、増加学習は、イメージ形式と、RecordIO形式でしか行うことが出来ない事と、学習のスピードは、イメージ形式よりRecordIOの方が高速でオススメとの事なので、今回、RecordIO形式への変換作業も確認しておくことにしました。
2 Ground Truth
Ground Truthのデータセットは、前回作成した、プライベートプロジェクトのごく少数のものです。
[Amazon SageMaker] Amazon SageMaker Ground Truth で作成したデータをオブジェクト検出で利用可能なイメージ形式に変換してみました
作業が完了すると、各画像ごと1行のアノテーション情報となっているoutput.manifestが生成されますが、このファイルと画像ファイルが今回の変換元です。
3 RecordIO形式のデータ
RecordIO形式のデータは、MXNetで配布される im2rec.py を使用して作成することができます。
im2rec.pyでは、アノテーション情報として.lst形式の入力を必要とするため、Ground Truthの出力であるoutput.manifestから生成しました。
以下は、生成した.lstファイルと、その当該画像の一例です。
1 4 5 800 600 1 0.141 0.225 0.454 0.695 0 0.534 0.392 0.772 0.632 AHIRU-1586397390447333.jpg
(略)
9 4 5 800 600 1 0.352 0.363 0.614 0.805 AHIRU-1586397276514928.jpg
(略)
.lstファイルの構造は、以下のとおりです。
- 連番 (1から始まる連番)
- ヘッダ数 (ヘッダを構成するカラム数 「ヘッダ数、データ数、画像サイズ幅、画像サイズ高さ」で4となる)
- データ数(1つのアノテーションデータを構成するカラム数「ラベル、左上X座標、左上Y座標、右下X座標、右下Y座標」で5となる)
- 画像サイズ(幅)
- 画像サイズ(高さ)
- 1つ目アノテーションデータ(ラベル、左上X座標、左上Y座標、右下X座標、右下Y座標)座標は、0.0〜1.0で表現される
- 2つ目のアノテーションデータ (2つ目以降がある場合は、順番に配置される)
- 画像ファイル名 (画像ファイル名、相対的なパスで指定される)
参考:https://github.com/leocvml/mxnet-im2rec_tutorial
im2rec.pyの使用例です。your_image_folderには、画像が配置されているディレクトリが指定されます。
$ python3 im2rec.py --pack-label
4 学習データと検証データ
教師あり学習のアルゴリズムである、オブジェクト検出では、学習用のデータと、検証用のデータが必要です。
学習用と検証用として、元データを、一定の比率で分割する場合、単純に分割することができません。全てのデータに均等に全てのラベルが含まれているわけでは無いからです。
たとえば、先頭から40件目まで、Aというラベルが指定されており、41〜50件目まで、Bが指定されているとします。そして、これを単純に4:1に分割すると、1〜40と41〜50となるため、学習用データにBのデータが存在しないことになってしまいます。
この問題に対応するため、変換対象となる全てのデータから、含まれるラベルの数をカウントし、サンプル数の少ないラベルから順に、分割するようにしました。
# ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
def getLabel(dataList):
labels = {}
for data in dataList:
for annotation in data.annotations:
label = annotation["label"]
if(label in labels):
labels[label] += 1
else:
labels[label] = 1
# ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
labels = sorted(labels.items(), key=lambda x:x[1])
return labels
labels = getLabel(dataList)
# ラベルの数の少ないものから優先して分割する
for i,label in enumerate(labels):
# ・・・分割処理
5 コード
RecordIOを生成するコードは、以下のとおりです。以下を定義を設定して利用可能です。
- inputPath 元データ(画像データとoutput.manifestを置く)
- outputPath 出力先
- ratio 学習データと検証データの分割比率
内部で使用しているため、im2rec.pyをカレントに配置する必要があります。
import json
import os
import subprocess
# 定義
inputPath = '/tmp/AHIRU-DOG'
outputPath = '/tmp/AHIRU-DOG-SageMaker'
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]
# メタデータの取得
metadata = src[projectName + '-metadata']
class_map = metadata["class-map"]
# 画像名の取得
self.__imgFileName = os.path.basename(src["source-ref"])
# 画像サイズの取得
project = src[projectName]
image_size = project["image_size"]
self.__img_width = image_size[0]["width"]
self.__img_height = image_size[0]["height"]
self.__annotations = []
# アノテーションの取得
for annotation in project["annotations"]:
class_id = annotation["class_id"]
top = annotation["top"]
left = annotation["left"]
width = annotation["width"]
height = annotation["height"]
self.__annotations.append({
"label": class_map[str(class_id)],
"width": width,
"top": top,
"height": height,
"left": left
})
@property
def annotations(self):
return self.__annotations
# 指定されたラベルを含むかどうか
def exsists(self, label):
for annotation in self.__annotations:
if(annotation["label"] == label):
return True
return False
# .lstを生成して追加する
def appendLst(self, lst, labels):
cls_list = []
for label in labels:
cls_list.append(label[0])
index = len(lst.split('\n'))
headerSize = 4 # hederSize,dataSize,imageWidth,imageHeight
dataSize = 5
str = "{}\t{}\t{}\t{}\t{}".format(index, headerSize, dataSize, self.__img_width, self.__img_height)
for annotation in self.__annotations:
cls_id = cls_list.index(annotation["label"])
left = annotation["left"]
right = left + annotation["width"]
top = annotation["top"]
bottom = top + annotation["height"]
left = round(left / self.__img_width, 3)
right = round(right / self.__img_width, 3)
top = round(top / self.__img_height, 3)
bottom = round(bottom / self.__img_height, 3)
str += "\t{}\t{}\t{}\t{}\t{}".format(cls_id, left, top, right, bottom)
fileName = self.__imgFileName
str += "\t{}".format(fileName)
lst += str + "\n"
return lst
# dataListをラベルを含むものと、含まないものに分割する
def deviedDataList(dataList, label):
targetList = []
unTargetList = []
for data in dataList:
if(data.exsists(label)):
targetList.append(data)
else:
unTargetList.append(data)
return (targetList, unTargetList)
# ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
def getLabel(dataList):
labels = {}
for data in dataList:
for annotation in data.annotations:
label = annotation["label"]
if(label in labels):
labels[label] += 1
else:
labels[label] = 1
# ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
labels = sorted(labels.items(), key=lambda x:x[1])
return labels
# 全てのJSONデータを読み込む
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 main():
# 出力先フォルダ生成
os.makedirs(outputPath, exist_ok=True)
# 全てのJSONデータを読み込む
dataList = getDataList(inputPath, manifest)
log = "全データ: {}件 ".format(len(dataList))
# ラベルの件数の少ない順に並べ替える(配列のインデックスが、クラスIDとなる)
labels = getLabel(dataList)
for i,label in enumerate(labels):
log += "[{}]{}: {}件 ".format(i, label[0], label[1])
print(log)
# 保存済みリスト
storedList = []
log = ''
# 学習数
trainCount = 0
# .lst形式
train = ''
validation = ''
# ラベルの数の少ないものから優先して分割する
for i,label in enumerate(labels):
log = ''
log += "{} => ".format(label[0])
# dataListをラベルが含まれるものと、含まないものに分割する
(targetList, unTargetList) = deviedDataList(dataList, label[0])
# 保存済みリストから、当該ラベルで既に保存済の件数をカウントする
(include, notInclude) = deviedDataList(storedList, label[0])
storedCounst = len(include)
# train用に必要な件数
count = int(label[1] * ratio) - storedCounst
log += "{}:".format(count)
# train側への保存
for i in range(count):
data = targetList.pop()
train = data.appendLst(train, labels)
trainCount+=1
storedList.append(data)
# validation側への保存
log += "{} ".format(len(targetList))
for data in targetList:
validation = data.appendLst(validation, labels)
storedList.append(data)
dataList = unTargetList
log += "残り:{}件".format(len(dataList))
print(log)
print("Train: {}件".format(trainCount))
# .lstファイルの生成
trainLst = "{}/train.lst".format(outputPath)
validationLst = "{}/validation.lst".format(outputPath)
with open(trainLst, mode='w') as f:
f.write(train)
with open(validationLst, mode='w') as f:
f.write(validation)
# im2rec.pyによるRecordIOファイル生成
# python im2rec.py --pack-label
im2rec = "{}/im2rec.py".format(os.getcwd())
cmd = ["python3", im2rec, "--pack-label", "validation.lst", inputPath]
result = subprocess.run(cmd, cwd=outputPath)
print(result)
cmd = ["python3", im2rec, "--pack-label", "train.lst", inputPath]
result = subprocess.run(cmd, cwd=outputPath)
print(result)
main()
例えば、画像ファイルが50件で、ラベルDOGが10件、ラベルAHIRUが50件アノテーションされているデータをを変換すると、以下のようなログが出力されます。
全データ: 50件 [0]DOG: 10件 [1]AHIRU: 50件
DOG => 8:2 残り:40件
AHIRU => 30:10 残り:0件
Train: 38件
そして、出力先に指定したディレクトリで、RecordIOファイル(train.rec及び、varidation.rec)が生成されていることを確認できます。 この2つのファイルを、S3にアップロードして使用します。
$ ls -la
total 12632
drwxr-xr-x 8 sin staff 256 4 26 15:12 .
drwxr-xr-x 20 sin staff 640 4 26 13:29 ..
-rw-r--r-- 1 sin staff 395 4 26 15:12 train.idx
-rw-r--r-- 1 sin staff 2114 4 26 15:12 train.lst
-rw-r--r-- 1 sin staff 4105152 4 26 15:12 train.rec
-rw-r--r-- 1 sin staff 108 4 26 15:12 varidation.idx
-rw-r--r-- 1 sin staff 652 4 26 15:12 varidation.lst
-rw-r--r-- 1 sin staff 1297368 4 26 15:12 varidation.rec
6 オブジェクト検出
非常に限られたサンプル(データ)数ですが、とりあえず、モデルを作成してみました。DOGは、10件という事で、無理ですが、AHIRUの方は、限定的ですが検出できている感じがします。
7 最後に
今回は、Ground Truthで作成されたデータをSageMakerで利用するためのデータに変換する作業を行ってみました。
Ground Truthのデータを中心とした、データセット作成の作業では、今回のものが、図中の③になっています。