Amazon SageMaker で画像からオブジェクト（道路標識）を検出する

[**Amazon SageMaker**](https://aws.amazon.com/jp/sagemaker/) は機械学習のワークフロー全体をカバーする AWS の完全マネージド型サービスであるが、実際に何ができてどのように使えばいいのか分からなかったため、今回は Amazon SageMaker の [**オブジェクト検出アルゴリズム**](https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/object-detection.html) を使用して、画像から下図のような日本の道路に広く見られる道路標識（方面及び方向の予告）を検出してみた。
![content_108.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176935/8ca8d9da-ab3d-3db8-eaa2-7fc8cff70626.png)

## 教師データの作成

### 映像からスクリーンショットを抽出

まずは、学習用に道路標識を含む大量の画像を用意する必要がある。そこで、走行中の車内から前方を撮影した映像を用意し、[FFmpeg](https://ffmpeg.org/) により 4 秒おきにスクリーンショットを取得することで、大量の画像を得た（以下のコマンドでは yadif フィルターによるインターレース解除処理を同時に行っている）。

```bash
ffmpeg -loglevel warning -i input.mp4 -vf yadif=0:-1:1 -r 0.25 output_%04d.jpg
```

### 画像のアノテーション（タグ付け）

抽出した大量の画像をもとに教師データを作成する。すなわち、画像内の道路標識の部分を手作業で指定してやる必要がある。[Amazon SageMaker Ground Truth](https://aws.amazon.com/jp/sagemaker/groundtruth/) というサービスを利用して、この作業を外部のパブリックチームや社内のプライベートチームなどに委託することもできるが、今回は小規模のため自力で行うこととする。

このような、画像のタグ付け（アノテーション）を少しでもラクにするためにさまざまなツールが存在しているが、今回は Microsoft が開発している [**VoTT** (Visual Object Tagging Tool)](https://github.com/microsoft/VoTT) を使用した。VoTT によるアノテーションについては [こちら](https://qiita.com/clerk67/items/8e7207284037dcf1f9ec) の別記事を参照されたい。
![Screen Shot 2019-06-02 at 20.56.25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176935/bd34da42-24b5-c4f6-1d20-b4a706a1ab54.png)

### RecordIO 形式のファイルを作成

Amazon SageMaker のオブジェクト検出アルゴリズムでは入力ファイル形式として [Apache MXNet RecordIO](https://mxnet.incubator.apache.org/versions/master/architecture/note_data_loading.html) 形式が推奨されているため、VoTT により作成したアノテーションデータ（JSON 形式）を RecordIO 形式に変換する必要がある。変換の手順については [こちら](https://qiita.com/clerk67/items/faecbbbcbe26537cb42c) の別記事を参照されたい。

### Amazon S3 に教師データをアップロード

RecordIO 形式のファイルを下記の通り Amazon S3 にアップロードした。なお、機械学習においては一般的に、過学習を防ぐ目的で、学習用データと検定用データを別々に用意することが多い。今回は、タグ付けした 240 枚の画像から無作為に抽出した 160 枚の画像を学習用データ、残りの 80 枚を検定用データとして、それぞれ RecordIO 形式のファイルを作成した。

| S3 URI                                        | 説明
|:----------------------------------------------|:-----
| s3://example-bucket/train/train.rec           | 学習用データ (RecordIO 形式)
| s3://example-bucket/validation/validation.rec | 検定用データ (RecordIO 形式)

## ノートブックインスタンスの作成

機械学習を始めるにあたり、データの前処理や可視化、アルゴリズムの検討などを行うために [Jupyter Notebook](https://jupyter.org/) というツールを使う。これはブラウザ上で Python のコードを対話的に実行できるようなもので、機械学習やデータ分析の分野では一般的に使われている。

通常は Jupyter Notebook をローカル PC にインストールしたり Docker コンテナとして入手して使用したりするが、Amazon SageMaker ノートブックインスタンスは、いわば AWS のマネージド Jupyter Notebook のようなもので、インスタンスを作成するとすぐに Jupyter Notebook が使用できるようになる。

ノートブックインスタンスは Amazon SageMaker マネジメントコンソールから簡単に作成できる。
参考： https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/gs-setup-working-env.html

## トレーニングジョブの作成

ここからはノートブックインスタンスで稼働する Jupyter Notebook 上で Python コードを実行していく。Jupyter Notebook の使い方についてここでは説明しないが、基本的には「セル」と呼ばれるエリアにコードを記述し、<kbd>Shift</kbd> + <kbd>Enter</kbd> キーを押してコードを実行するという作業の繰り返しになる。

以下では https://github.com/awslabs/amazon-sagemaker-examples/blob/master/introduction_to_amazon_algorithms/object_detection_pascalvoc_coco/object_detection_recordio_format.ipynb を参考にトレーニングジョブの作成を行う。

```python
import sagemaker
from sagemaker import get_execution_role

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

Amazon SageMaker の組み込みアルゴリズムの中から「オブジェクト検出アルゴリズム」の最新のイメージを読み込む。

```python
from sagemaker.amazon.amazon_estimator import get_image_uri

training_image = get_image_uri(sess.boto_region_name, 'object-detection', repo_version='latest')
print(training_image)
```

トレーニングを行うインスタンスを設定する。第 1 引数にアルゴリズムのイメージ、第 2 引数に IAM ロールを指定する。`train_instance_count` はインスタンスの台数、`train_instance_type` はインスタンスタイプを表す。なお、オブジェクト検出アルゴリズムでは GPU を搭載した P2 または P3 インスタンスファミリーを選択する必要があることに注意。また、GPU インスタンスは高額なので使いすぎには十分注意したい。`train_volume_size` はインスタンスに与える追加のボリュームサイズ（GB 単位）。`train_max_run` はトレーニングの最大実行時間（秒）で、最初は短めに設定しておくことを推奨する。`output_path` にはトレーニング済みのモデルを出力する Amazon S3 の URI を指定する。
参考： https://sagemaker.readthedocs.io/en/stable/estimators.html

```python
od_model = sagemaker.estimator.Estimator(training_image,
                                         role,
                                         train_instance_count=1,
                                         train_instance_type='ml.p2.xlarge',
                                         train_volume_size=8,
                                         train_max_run=600,
                                         input_mode='File',
                                         output_path='s3://example-bucket/sagemaker/output',
                                         sagemaker_session=sess)
```

続いてハイパーパラメータと呼ばれるトレーニングに関連する各種パラメータを調整する。`num_classes` には教師データに何種類のタグが含まれるかを指定する。`num_training_samples` は教師データに含まれる画像の枚数を指定する。トレーニング中にメモリ不足が発生した場合は `mini_batch_size` を小さくする（ml.p2.xlarge インスタンスで `mini_batch_size = 32` の場合、メモリ不足が発生した）。その他の項目は基本的にデフォルト値のままとしている。
参考： https://docs.aws.amazon.com/ja_jp/sagemaker/latest/dg/object-detection-api-config.html

```python
od_model.set_hyperparameters(base_network='vgg-16',
                             use_pretrained_model=1,
                             num_classes=4,
                             mini_batch_size=8,
                             epochs=30,
                             learning_rate=0.001,
                             lr_scheduler_factor=0.1,
                             optimizer='sgd',
                             momentum=0.9,
                             weight_decay=0.0005,
                             overlap_threshold=0.5,
                             nms_threshold=0.45,
                             image_shape=512,
                             label_width=350,
                             num_training_samples=240)
```

Amazon S3 にアップロードした教師データ（学習用と検定用）の情報を指定する。第 1 引数の S3 URI は RecordIO 形式のファイルへのフルパス、または同ファイルが格納されているディレクトリ名を指定する。ディレクトリ名を指定する場合は、ディレクトリ内に RecordIO 形式のファイルが複数存在してはならないことに注意。

```python
train_data = sagemaker.session.s3_input('s3://example-bucket/train',
                                        distribution='FullyReplicated',
                                        content_type='application/x-recordio',
                                        s3_data_type='S3Prefix')

validation_data = sagemaker.session.s3_input('s3://example-bucket/validation',
                                             distribution='FullyReplicated',
                                             content_type='application/x-recordio',
                                             s3_data_type='S3Prefix')

data_channels = {'train': train_data, 'validation': validation_data}
```

次のコードを実行すると、トレーニングジョブが開始される。トレーニングが終了するか、`train_max_run` で指定した最大実行時間が経過すると、Amazon S3 の指定した場所にモデルが .tar.gz 形式で出力される。

```python
od_model.fit(inputs=data_channels, logs=True)
```

なお、トレーニングインスタンスの料金については、実際にトレーニングを行っていた時間に対してのみ秒単位で課金される。すなわち、トレーニングインスタンスが起動しても、教師データの不備や設定ミスによって実際にトレーニングが始まる前にジョブが終了した場合は、課金されないということになる。

## エンドポイントの作成

トレーニングにより出力されたモデルを使用して実際に未知データの推論を行うため、エンドポイントを作成する。エンドポイントのインスタンスは、トレーニングインスタンスとは異なるインスタンスタイプを選択することができる。

```python
object_detector = od_model.deploy(initial_instance_count=1, instance_type='ml.m5.large')
```

## 未知データの推論

Jupyter Notebook に画像ファイルをアップロードし、作成したエンドポイントで道路標識の検出を試みた。

```python
import json

file_name = 'test01.jpg'
with open(file_name, 'rb') as image:
    f = image.read()
    b = bytearray(f)
    ne = open('n.txt', 'wb')
    ne.write(b)

object_detector.content_type = 'image/jpeg'
results = object_detector.predict(b)
detections = json.loads(results)
print(detections)
```

検出結果を画像に重ね合わせて可視化するための関数 `visualize_detection` を用意する。

```python
def visualize_detection(img_file, dets, classes=[], thresh=0.6):
        """
        visualize detections in one image
        Parameters:
        ----------
        img : numpy.array
            image, in bgr format
        dets : numpy.array
            ssd detections, numpy.array([[id, score, x1, y1, x2, y2]...])
            each row is one object
        classes : tuple or list of str
            class names
        thresh : float
            score threshold
        """
        import random
        import matplotlib.pyplot as plt
        import matplotlib.image as mpimg

        img=mpimg.imread(img_file)
        plt.imshow(img)
        height = img.shape[0]
        width = img.shape[1]
        colors = dict()
        for det in dets:
            (klass, score, x0, y0, x1, y1) = det
            if score < thresh:
                continue
            cls_id = int(klass)
            if cls_id not in colors:
                colors[cls_id] = (random.random(), random.random(), random.random())
            xmin = int(x0 * width)
            ymin = int(y0 * height)
            xmax = int(x1 * width)
            ymax = int(y1 * height)
            rect = plt.Rectangle((xmin, ymin), xmax - xmin,
                                 ymax - ymin, fill=False,
                                 edgecolor=colors[cls_id],
                                 linewidth=3.5)
            plt.gca().add_patch(rect)
            class_name = str(cls_id)
            if classes and len(classes) > cls_id:
                class_name = classes[cls_id]
            plt.gca().text(xmin, ymin - 2,
                            '{:s} {:.3f}'.format(class_name, score),
                            bbox=dict(facecolor=colors[cls_id], alpha=0.5),
                                    fontsize=12, color='white')
        plt.show()
```

最後に結果を出力する。`visualize_detection` の第 3 引数に渡す配列は、教師データをタグ付けする際に、検知したい各物体に割り当てた番号との対応関係を表すものである（今回は青色の標識を `0` , 緑色の標識を `1` , 白色の標識を `2` としてタグ付けを行なっている）。

```python
object_categories = ['blue-sign', 'green-sign', 'white-sign']
threshold = 0.20
visualize_detection(file_name, detections['prediction'], object_categories, threshold)
```
### 実行結果

教師データの量が少なくトレーニングの時間も不十分だったため確度は低いものの、道路標識の検出には成功した。教師データを増やしたり、トレーニングの時間を十分に確保したりすることで、さらに検出精度が向上することが期待できる。

![download (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176935/cb920fca-4a0e-c1b2-4298-2c1620b1c46e.png)
![download.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176935/ffa09afb-a5a5-a9c5-116f-e8229d9db4b3.png)
![download (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/176935/d681da8b-27d4-2bb2-a1a6-c1984fc4b633.png)

## エンドポイントの削除

エンドポイントはデプロイされている時間に応じて課金されるため、不要になったら必ず削除する。

```python
sagemaker.Session().delete_endpoint(object_detector.endpoint)
```