Amazon SageMakerでネジ画像の分類をやってみた_データを増やしてみた

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

概要

こんにちは、yoshimです。 先日は「ネジ画像」の分類に挑戦したのですが、イマイチな結果でしたので、再挑戦してみようと思います。

先日の敗因は、「データサイズが小さかったこと」が一番の原因かと思ったので、今回はデータサイズを少し増やしてみました。
(まだ少ないかもしれないけど...)
また、上記のデータだけではまだ不足していると思ったので、学習時のハイパーパラメータで指定できる「augmentation_type」も使ってみました。
(データ拡張のオプションです。)
「データ拡張」を使うことで良い結果を得られるといいなぁ...。

目次

1.最初に

今回は先日ご紹介したチュートリアルに沿って、ネジ画像の分類をしています。
また、先日ご紹介したブログの続きになります。

今回も、「ネジの画像」を投入したら「なべネジ」、「皿ネジ」、「蝶ネジ」の3種類のどのネジなのか、という分類をするモデルを作成してみようと思います。 また、細かいことですが「num_layers」は50にしています。

・なべネジ

・皿ネジ

・蝶ネジ

前回はデータサイズが114と少なかったのですが、今回は検証用含めて合計280のデータを用意して実施してみました。

trainig:180
validation:100

また、最初に結果を述べてしまうと、「分類結果は一見良さそう」な結果となりましたが、それはあくまでも今回用意した「小規模なデータセット」に対してです。
なので、今後の課題として「汎化性能の向上」が挙げられます。
(「加工したデータでのval精度比較」とかで検証してみるかなぁ)

2.データの準備

実際に学習する前に、今回実施したデータの準備についてさらっとご説明します。
基本的にはチュートリアルそのままなのですが、いざ自分のデータでやってみようという方の参考になれば幸いです。

2-1.データをSageMaker上に用意

まずはいつも通り権限を取得します。

%%time
import boto3
from sagemaker import get_execution_role
from sagemaker.amazon.amazon_estimator import get_image_uri

role = get_execution_role()

bucket='hogehoge' # customize to your bucket

training_image = get_image_uri(boto3.Session().region_name, 'image-classification')

続いて、画像から「.lst」ファイルを生成するツールをダウンロードします。

import os
import urllib.request


def download(url):
    filename = url.split("/")[-1]
    if not os.path.exists(filename):
        urllib.request.urlretrieve(url, filename)

# Tool for creating lst file
download('https://raw.githubusercontent.com/apache/incubator-mxnet/master/tools/im2rec.py')

下記のようなzipファイルをローカルで用意してSageMakerにアップロードします。
(実際はもっといっぱいデータが入っています。)

|--data
|  |--test.zip
|  |  |--001.saraneji
|  |  |  |--001_0331.jpg
|  |  |  |--001_0332.jpg
|  |  |--002.tyouneji
|  |  |  |--.DS_Store
|  |  |  |--002_0241.jpg
|  |  |  |--002_0242.jpg
|  |  |--003.nabeneji
|  |  |  |--003_0351.jpg
|  |  |  |--003_0352.jpg
|  |--train_val.zip
|  |  |--001.saraneji
|  |  |  |--001_0001.jpg
|  |  |  |--001_0002.jpg
|  |  |--002.tyouneji
|  |  |  |--002_0011.jpg
|  |  |  |--002_0012.jpg
|  |  |--003.nabeneji
|  |  |  |--003_0031.jpg
|  |  |  |--003_0032.jpg

ここですね。

続いて、上記zipファイルを「unzip」します。

・Jupyterの中でやるならこう。(適宜自分のディレクトリに合わせてください)

!unzip /home/ec2-user/SageMaker/screw_clf/data/train_val.zip
!unzip /home/ec2-user/SageMaker/screw_clf/data/test.zip

ターミナルからやるなら最初の「!」を外してください。
ターミナルからの実行はここからできます。

Jupyter,ターミナルのいずれから実行するにしても、下記の通りデータが用意できました。

2-2.ちょっと前処理

続いて、学習に利用できるようにちょっと前処理をしてからS3にアップロードします。
まず、用意した画像のサイズがまちまちなので、(3,224,224)に統一します。

'''画像のリサイズ
必要なら実行する。
imagenetのINPUTの(3,224,224)にする。

'''

import glob
from PIL import Image

# トレーニング、検証用データ画像をリサイズ
for train_or_test in ('train_val','test'):
    list_images = []
    list_images = glob.glob("/home/ec2-user/SageMaker/screw_clf/data/{}/*/*.jpg".format(train_or_test), recursive=True)
    for image_for_resize in list_images:
        img = Image.open(image_for_resize)
        img_resized = img.resize((224, 224), Image.LANCZOS)
        print(img.format, img.size, img.mode)
        img_resized.save(image_for_resize, quality=95)
        print(img_resized.format, img_resized.size, img_resized.mode)

各ネジ画像ごとに60枚ずつトレーニング用にします。 また、それ以外を検証用データとして「.lst」ファイルを生成します。

%%bash

mkdir -p screw_train_60
for i in data/train_val/*; do
    c=`basename $i`
    mkdir -p screw_train_60/$c
    for j in `ls $i/*.jpg | shuf | head -n 60`; do
        mv $j screw_train_60/$c/
    done
done


python im2rec.py --list --recursive screw-train screw_train_60/
python im2rec.py --list --recursive screw-val data/train_val/

そして、画像、「.lst」ファイルをS3にアップロードします。

# Four channels: train, validation, train_lst, and validation_lst
prefix = 'screw_clf'
s3train = 's3://{0}/{1}/train/'.format(bucket,prefix)
s3validation = 's3://{0}/{1}/validation/'.format(bucket,prefix)
s3train_lst = 's3://{0}/{1}/train_lst/'.format(bucket,prefix)
s3validation_lst = 's3://{0}/{1}/validation_lst/'.format(bucket,prefix)

# upload the image files to train and validation channels
!aws s3 cp screw_train_60 $s3train --recursive --quiet
!aws s3 cp data/train_val $s3validation --recursive --quiet

# upload the lst files to train_lst and validation_lst channels
!aws s3 cp screw-train.lst $s3train_lst --quiet
!aws s3 cp screw-val.lst $s3validation_lst --quiet

以上で学習前の準備は終わりです。

3.実際にやってみた(「Data Augmentation」なし)

さて、お待ちかねの学習フェーズです。 まずは、「Data Augmentation」なしでやってみます。
下記のようなハイパーパラメータを指定したところ「p2.xlarge」インスタンスで15分ほどで完了しました。

・ハイパーパラメータ

# The algorithm supports multiple network depth (number of layers). They are 18, 34, 50, 101, 152 and 200
# For this training, we will use 18 layers
num_layers = 50
# we need to specify the input image shape for the training data
image_shape = "3,224,224"
# we also need to specify the number of training samples in the training set
num_training_samples = 180
# specify the number of output classes
num_classes = 3
# batch size for training
mini_batch_size = 30
# number of epochs
epochs = 50
# learning rate
learning_rate = 0.01
# report top_5 accuracy
top_k = 1
# period to store model parameters (in number of epochs), in this case, we will save parameters from epoch 2, 4, and 6
checkpoint_frequency = 2
# Since we are using transfer learning, we set use_pretrained_model to 1 so that weights can be 
# initialized with pre-trained weights
use_pretrained_model = 1

ログを確認してみます。

最後の10エポックほどは学習、検証用データセットでの精度が常に「1」の状態でした...。

とりあえず、混同行列を見てみようと思います。
(折角用意したので使いたかった) まず、今回作成する混同行列について説明します。
こちらが混同行列なのですが、「横軸が正解ラベル、縦軸が予測ラベル」を表します。
なので、この混同行列の場合は下記のように読み取れます。

・「なべネジ」は30個中30個全て適切に分類できている。
・「皿ネジ」は40個全て「なべネジ」と誤分類している。
・「蝶ネジ」は28個を「なべネジ」と誤分類、2個を「蝶ネジ」と正しく推測できている。

なので、対角線上に全てのデータが収まるのが理想的と言えます。
では、早速検証用データセットの混同行列を見ていきましょう。

ログでも確認した通り、精度が「1」だったので全部正しく推測できている状態のようです。

混同行列は下記のようなコードで計算しました。

'''
結果を評価するために混同行列を作る。
valデータを検証対象とする。
(合計100個)
'''

# まずは、リストに「正解」と「予測値」を格納する。
# (list_true,list_prd)

import json
import numpy as np
import glob

object_categories = ['saraneji','tyouneji','nabeneji']
list_true = []
list_prd = []


for cls in ['001.saraneji','002.tyouneji','003.nabeneji']:
    list_files = glob.glob("/home/ec2-user/SageMaker/screw_clf/data/train_val/{0}/*.jpg".format(cls), recursive=True)
    for file in list_files:
        list_true.append(cls[4:])
        with open(file, 'rb') as f:
            payload = f.read()
            payload = bytearray(payload)
            response = runtime.invoke_endpoint(EndpointName=endpoint_name, 
                                               ContentType='application/x-image', 
                                               Body=payload)
            result = response['Body'].read()
            # result will be in json format and convert it to ndarray
            result = json.loads(result)
            # the result will output the probabilities for all classes
            # find the class with maximum probability and print the class index
            index = np.argmax(result)
            list_prd.append(object_categories[index])


# confusion matrixを作成。

import pandas as pd
import seaborn as sns
from sklearn.metrics import confusion_matrix
import matplotlib.pyplot as plt

# ラベル(重複なし)を取得
labels = sorted(list(set(list_true)))

# データを格納
cmx_data = confusion_matrix(list_true, list_prd, labels=labels)    
df_cmx = pd.DataFrame(cmx_data, index=labels, columns=labels)

# 見せ方
plt.figure(figsize = (10,7))
sns.heatmap(df_cmx, cbar=True, annot=True,annot_kws={'size': 30},fmt='d',
            linewidths=5,linecolor='black',
            cmap="Greens",center=None,xticklabels = 'auto',yticklabels = 'auto')

# 表示(横軸が正解、縦軸が予測)
plt.show()

流石にうまくできすぎている気がしたので、学習にも検証にも利用していないデータセット(各クラス10個ずつ、以降「テスト用データ」)を用意して、同様に検証してみます。
コードは上記の「list_files」変数のパスを変更するだけです。

こちらも全て正確に推測できていそうです。
あまり意味はないのですが、折角なので実際に画像を投入してみようと思います。

・皿ネジ

Result: label - saraneji, probability - 0.9998642206192017

・蝶ネジ

Result: label - tyouneji, probability - 0.9999997615814209

・なべネジ

Result: label - nabeneji, probability - 0.9999179840087891

まあ、こんな感じですよね。
ちょっと気になったので、各画像の推定値のMINを見てみようと思いました。
具体的には、「テスト用データを投入した際」に「どのクラスの確率が○%」と返り値を取得できるわけですが、この○の部分のMINを取ってみようというものです。(以降、「推測した確率のリスト」)

'''
完全に推測できてしまったので、「どのくらいの確率」で分類しているのかを少し見てみる。
具体的には、上記「probability」のMINを取得する。
'''

import json
import numpy as np
import glob

object_categories = ['saraneji','tyouneji','nabeneji']
list_prob = []
pred_prob = 1

for cls in ['001.saraneji','002.tyouneji','003.nabeneji']:
    list_files = glob.glob("/home/ec2-user/SageMaker/screw_clf/data/test/{0}/*.jpg".format(cls), recursive=True)
    for file in list_files:
        with open(file, 'rb') as f:
            payload = f.read()
            payload = bytearray(payload)
            response = runtime.invoke_endpoint(EndpointName=endpoint_name, 
                                               ContentType='application/x-image', 
                                               Body=payload)
            result = response['Body'].read()
            # result will be in json format and convert it to ndarray
            result = json.loads(result)
            # the result will output the probabilities for all classes
            # find the class with maximum probability and print the class index
            index = np.argmax(result)
            pred_prob_tmp = result[index]
            print(pred_prob_tmp)
            if pred_prob_tmp < pred_prob:
                pred_prob = pred_prob_tmp


print('1番自信が無かった時:{}'.format(pred_prob))

一番自信が無い時でも「99.7%」の確率で推測できていたようです。
単純にここまでの結果をみると十分なモデルのように見えるのですが、今回の検証で利用したデータセットが小さいので「汎化性能」の面では不安が残ります。できることなら、もっとデータサイズを大きくして学習を進めていきたいところです。しかしながら、この後も手動で画像を増やしていくのも辛いので、「Data Augmentation」機能を使って学習を進めていこうと思います。

なので、一旦「Data Augmentation」機能を使うだけ使って「検証用データでの精度」等を確認してみようと思います。

4.実際にやってみた(「Data Augmentation」あり)

「Data Augmentation(データ拡張)」は元の画像データを「回転させる」、「ノイズを入れる」等の「元画像になんらかの加工を施した画像データ」を用意し、学習に利用する考え方です。
小さなデータセットで画像分類をする際は、おそらく「transfer-learning or fine-tuning」、「Data Augmentation」のいずれか、もしくは両方を利用することが多いのではないでしょうか?

この「データ拡張」ですが、keras,tensorflow等のフレームワークには関数が用意されていたりします。
(chainerにもあると思うのですが、わかりませんでした...)
kerasのImageDataGenerator
tensorflowのimage

上記のモジュールを使って画像を用意することもできそうなのですが、今回はモデルの学習時に指定できるハイパーパラメータである「augmentation_type」を指定することで「データ拡張」機能を利用してみようと思います。
「augmentation_type」は「crop」、「crop_color」、「crop_color_transform」の3種類が選べるので、一応3種類共試してみました。
ハイパーパラメータ

「Data Augmentation」(augmentation_type)以外は「3.実際にやってみた(「Data Augmentation」なし)」と同じハイパーパラメータを指定しました。
それぞれの混同行列は下記の通りでした。

4-1.augmentation_type = 'crop'

学習は「p2.xlarge」で15分ほどで完了しました。
ログを見ていると、30エポック目以降の「Validation-accuracy」がずっと「1」でした。
なので、当然検証用データセットでの混同行列は下記のようになります。

テスト用データでの混同行列はこちら。

推測した確率のリストはこちら。

こちらもいい結果ですね。

4-2.augmentation_type = 'crop_color'

学習は「p2.xlarge」で15分ほどで完了しました。
こちらも、ログを見ていると30エポック目くらいから「Validation-accuracy」がずっと「1」でした。

・検証用データセットでの混同行列

・テスト用データでの混同行列

・推測した確率のリスト

こちらもいい結果ですね。

4-3.augmentation_type = 'crop_color_transform'

学習は「p2.xlarge」で15分ほどで終わりました。
こちらも同じような結果でした。

・検証用データセットでの混同行列

・テスト用データでの混同行列

・推測した確率のリスト

5.まとめ

今回もネジ画像の分類にチャレンジしてみました。
前回は駆け足でのブログ執筆になってしまっていたのですが、今回は混同行列も確認し、かつデータ拡張も若干ですが試してみました。
(当初の予定では、「augmentation_type」を指定することで精度が向上しました、といった流れを想定していたのですが...)

一見いい結果なのですが、今回利用したデータサイズがまだ小さいので「汎化性能がどれだけあるのか」、という点がまだ気がかりです。
(今回、学習&検証に利用したデータはこちらで用意したものなのですが、「ある程度整っているデータ」と言いますか、綺麗な画像データばかりでしたし...)
本来なら、加工した画像データセットでの検証もこの後するべきかと思ったのですが、本エントリーが既にかなり長くなってしまったのでまた次回にしようと思います。
(その検証をしないと「augmentation_type」の便利さが実感できなさそう)

具体的には下記のような検証パターンで、「今回利用したデータセットを加工した画像」で推論をして検証してみようと思っています。
(なんとなく汎化性能が確認できないかな、と思っています。)

検証パターン 利用するデータセット augmentation_type
1 今回と同じ -
2 今回と同じ
3 今回利用したデータ+拡張したデータセット -

また、ハイパーパラメータのチューニングを碌にしていなかったので、ハイパーパラメータの自動チューニング機能を使ってみたいな、と思っていたのですがこちらは一旦保留にしようかと思います。

もし「ここをこうした方がいいのでは」といったご指摘がございましたら是非よろしくお願いいたします。

結果

検証用100個のデータセットでの分類結果を整理しました。
(どれも同じような結果ですが...)

augmentation_type なべネジ 蝶ネジ 皿ネジ
なし 30(100%) 40(100%) 30(100%)
crop 30(100%) 40(100%) 30(100%)
crop_color 30(100%) 40(100%) 30(100%)
crop_color_transform 30(100%) 40(100%) 30(100%)

6.引用

先日挑戦したブログ
SageMakerのチュートリアル
im2recツール
kerasのImageDataGenerator
tensorflowのimage
image classficationのハイパーパラメータ
ハイパーパラメータの自動チューニング機能