👨‍🎓

「あの犬、何という犬種だったけ?」を解決してみた。

2023/08/05に公開

Index

  1. はじめに
  2. 本記事の概要
  3. Dataset および実行環境
  4. CNN の設計
  • 入力層(データの前処理)
  • 出力層(予測結果)
  • 隠れ層(ネットワーク、モデル)
  1. 作成したプログラム
  2. 今後の活用
  3. おわりに

はじめに

「あの犬、何という犬種だったけ?」
私たちが日常生活で主にみるのは限られたいくつの犬種であるが、実際に世の中には色んな犬種が存在している。私もシーズーの飼い主だが、犬籍登録頭数 10 位であるシーズーさえも散歩中に良く犬種を聞かれる。また犬好きな私も散歩中に分からない犬種の犬によく合い、どんな犬種か質問する場合が多い。上記のことを踏まえてこの記事では Aidemy で勉強した内容を用いて犬の写真から犬種を自動的に予測してくれるアプリケーションを設計、作成し、その結果について考察を行う。

  • 簡単な自己紹介
    • 出身:韓国、大学から日本へ留学
    • 学歴:京都大学卒業(電気電子工学科)、京都大学大学院修了(電気工学専攻)
    • 現職:半導体メーカーの Technical Sales Engineer
    • 目標:Data Scientist または Data Engineer への転職
    • 手段:Aidemy の「機械学習マスターコース」受講およびキャリアサポート

Dataset および実行環境

Stanford Dogs Dataset

Stanford Dogs Dataset には全世界 120 種、20,580 枚のイメージ及び各イメージの annotation(注釈)で構成されている。データ元は ImageNet である。

  • Contents of this dataset
    • Number of categories: 120
    • Number of images: 20,580
    • Annotations: Class labels, Bounding boxes

実行環境

  • Version 管理が容易にできる Kaggle の Notebook を使用

CNN の設計

入力層(データの前処理)

Crop & Resize & Write

  • 元イメージから犬の画像を切り取り、サイズを調整する。
    • Dataset の Annotation ファイルにある Bounding boxes を参考にして正方形で切り取り、任意のイメージサイズ(下記は 128x128)に調整する。
    • |800

出力層(予測結果)

  • 多重分類の結果を確率として出力
    • 出力の数=120:120 種の犬種に対応
    • 活性化関数 softmax:多重分類問題であるため、出力層の活性化関数として softmax を使用

隠れ層(ネットワーク、モデル)

転移学習 (モデル:VGG16)

  • モデル設計のアイデア:Filter(Kernel) を小さくする代わりに深いニューラルネットワークを作ろう
  • 上記の構造のように重みがある層は 16 層(Convolution Layer 13 層 + Fully-connected Layer 3 層)で構成されている。
  • イメージの特徴を抽出する際に特徴を抽出する全結合層の前までの Layer を用いる場合が多い。

自作 Layer

Layer Output Activation function Rate
Flatten [1:]
Fully-connected 512 sigmoid
Dropout 0.1
Fully-connected 256 sigmoid
Fully-connected(出力層) 120 softmax

作成したプログラム

Imports

from tensorflow.keras import optimizers
from tensorflow.keras.layers import Activation, Conv2D, Dense, Flatten, MaxPooling2D, Input, Dropout
from tensorflow.keras.models import Sequential, Model
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.applications.vgg16 import VGG16
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import cv2
from keras.utils import np_utils
from sklearn.model_selection import train_test_split
import os

Example of Image

  • 本節ではデータセットの中身を確認する。
  • チワワが3匹存在するイメージを例として元イメージを Annotation データによって Crop & Resize した結果を示す。

Original Image

  • 約 95% のイメージが一匹だけ含まれているが、多数存在する場合もある。
  • 今回は例としてチワワが3匹存在するイメージを選定した。
img = cv2.imread('../input/stanford-dogs-dataset/images/Images/n02085620-Chihuahua/n02085620_1152.jpg')
my_img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
print(my_img.shape)
plt.imshow(my_img)
plt.show()

Cropped & Resized Images

  • Annotation data から Binding Box の座標を読み取り、正方形になるように切り取る。
  • 入力データの Tensor を一定にするため、切り取ったイメージを任意の img_size で統一させる。

工夫した点

  • 最初は Binding box の長方形に切り取って Resize を通して正方形に形を整えた。
  • しかし、上記の通りだと画像の縦横比が変化してしまう。
  • 犬種の識別の精度を上げるためには縦横比を保持したほうがいいと思い、修正を加えた。
  • 下記のイメージが順に「Binding box」、「Binding Box通りCrop & Resize」、「原点(左下)を基準で正方形でCrop & Resize」のイメージである。

from xml.etree.ElementTree import parse

annotation_file = '../input/stanford-dogs-dataset/annotations/Annotation/n02085620-Chihuahua/n02085620_1152'

tree = parse(annotation_file)
root = tree.getroot()

img_size = 128
cropped_images = []

for object in root.iter('object'):
    xmin = int(object.find('bndbox').findtext('xmin'))
    ymin = int(object.find('bndbox').findtext('ymin'))
    xmax = int(object.find('bndbox').findtext('xmax'))
    ymax = int(object.find('bndbox').findtext('ymax'))
    
    if ymax-ymin > xmax-xmin:
        delta = ymax-ymin
    else:
        delta = xmax-xmin
    
    cropped_image = my_img[ymin:ymin+delta, xmin:xmin+delta]
    cropped_image = cv2.resize(cropped_image, (img_size,img_size))
    cropped_images.append(cropped_image)
    plt.imshow(cropped_image)
    plt.show()

Prepare X and Y

犬種のリストの作成及び編集したイメージの保存

犬種のリストの作成

  • 元データを利用して犬種のリストを作成し、総犬種及びイメージの数を出力。
breed_list = os.listdir("../input/stanford-dogs-dataset/images/Images/")

print("{} breeds".format(len(breed_list)))

total_images = 0
for breed in breed_list:
    total_images += len(os.listdir("../input/stanford-dogs-dataset/images/Images/{}".format(breed)))

print("{} images".format(total_images))

>>>OUTPUT
120 breeds
20580 images

編集したイメージの保存

  • 上で作成した犬種リストを用いて犬種毎の Directory を作成し、編集したイメージの保存を行う。
os.makedirs('data', exist_ok=True)

for breed in breed_list:
    os.makedirs('data/'+breed, exist_ok=True)

for breed in os.listdir('data'):
    for file in os.listdir('../input/stanford-dogs-dataset/annotations/Annotation/{}'.format(breed)):
        img = cv2.imread('../input/stanford-dogs-dataset/images/Images/{}/{}.jpg'.format(breed, file))
        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
        
        tree = parse('../input/stanford-dogs-dataset/annotations/Annotation/{}/{}'.format(breed, file))
        root = tree.getroot()

        i = 0
        img_size = 128

        for object in root.iter('object'):
            xmin = int(object.find('bndbox').findtext('xmin'))
            ymin = int(object.find('bndbox').findtext('ymin'))
            xmax = int(object.find('bndbox').findtext('xmax'))
            ymax = int(object.find('bndbox').findtext('ymax'))
                        
            if ymax-ymin > xmax-xmin:
                delta = ymax-ymin
            else:
                delta = xmax-xmin
            
            cropped_image = img[ymin:ymin+delta, xmin:xmin+delta]
            cropped_image = cv2.resize(cropped_image, (img_size,img_size))
            cv2.imwrite('data/'+breed+'/'+file+'_'+str(i)+'.jpg', cropped_image)
            i += 1

保存後の犬種のリストとイメージ数の確認

  • イメージ数が約 10% 増加したのがわかる。
new_breed_list = os.listdir("data/")

print("{} breeds".format(len(new_breed_list)))

new_total_images = 0
for breed in new_breed_list:
    new_total_images += len(os.listdir("data/{}".format(breed)))

print("{} images".format(new_total_images))

>>>OUTPUT
120 breeds
22126 images

Prepare X and Y

イメージのパスおよびラベルのリスト作成

  • 犬種のリストから{番号: 犬種}の Dictionary を作成する。
  • 各イメージのパスに対応したラベルリストを作成。
  • 作成した Dictionary を用いてラベルリストをターゲット(番号)リストに変換する。
label_maps = {}
label_maps_rev = {}
for i, v in enumerate(breed_list):
    label_maps.update({v: i})
    label_maps_rev.update({i : v})

num_classes = len(breed_list)
    
def paths_and_labels():
    paths = []
    labels = []
    targets = []
    for breed in breed_list:
        base_name = "./data/{}/".format(breed)
        for img_name in os.listdir(base_name):
            paths.append(base_name + img_name)
            labels.append(breed)
            targets.append(label_maps[breed])
    return paths, labels, targets

paths, labels, targets = paths_and_labels()

assert len(paths) == len(labels)
assert len(paths) == len(targets)

Prepare X and Y

  • X にイメージデータを、y は Targets List を用いて one-hot encoding を行う。
  • また、X と y を model に使えるように numpy array に変換後、訓練データと検証データに分ける。
X = []
for path in paths:
    X.append(cv2.imread(path))
y = np_utils.to_categorical(targets, num_classes=num_classes)

X = np.array(X)
y = np.array(y)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

Model(VGG16)

VGG16 のインスタンス作成

  • input_tensor を定義し、VGG16 のインスタンスを作成する。
  • 重みは imagenet のものをそのまま用いる。
input_tensor = Input(shape=(img_size, img_size, 3))

vgg16 = VGG16(include_top=False,
              input_tensor = input_tensor,
              weights = 'imagenet')

自作モデル作成

top_model = Sequential()
top_model.add(Flatten(input_shape=vgg16.output_shape[1:]))
top_model.add(Dense(512, activation='sigmoid')) # 2つまで
top_model.add(Dropout(0.1)) # 過学習防止
top_model.add(Dense(256, activation='sigmoid')) # 2つまで
top_model.add(Dense(120, activation='softmax'))

モデルの結合、レイヤーの固定、コンパイル

  • VGG16 のレイヤーは固定させ、自作モデルの部分で学習が行われるようにする。
model = Model(inputs=vgg16.input, outputs=top_model(vgg16.output))
for layer in model.layers[:19]:
    layer.trainable = False
model.compile(loss='categorical_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])

モデルの学習、予測結果

モデルの学習、可視化

  • batch_size=32, epochs=20 で学習。
  • 約 40% の精度を見せる
history = model.fit(X_train, y_train, validation_data=(X_test, y_test), batch_size=32, epochs=20)

plt.plot(history.history['accuracy'], label='acc', ls='-', marker='o')
plt.plot(history.history['val_accuracy'], label='val_acc', ls='-', marker='x')
plt.ylabel('accuracy')
plt.xlabel('epoch')
plt.suptitle('model2', fontsize=12)
plt.legend()
plt.show()

検証データの可視化と予測結果

  • 検証データの先頭の 10 枚を出力し、予測結果および正解を出力
  • 運よく 60% の正解率を見せた。
  • しかし、EntleBucher と African_hunting_dog 以外は検索してみると、人間が見てもその差が分からないくらいである。
# データの可視化(検証データの先頭の10枚)
for i in range(10):
    plt.subplot(2, 5, i+1)
    plt.imshow(X_test[i], 'gray')
plt.suptitle("10 images of test data",fontsize=20)
plt.show()

# 予測(検証データの先頭の10枚)
pred = np.argmax(model.predict(X_test[0:10]), axis=1)
ans = np.argmax(y_test[0:10], axis=1)
[print('pred:', label_maps_rev[i],'\nans:' ,label_maps_rev[j]) for i, j in zip(pred,ans)]
model.summary()

pred: n02088364-beagle
ans: n02089973-English_foxhound
pred: n02093859-Kerry_blue_terrier
ans: n02105251-briard
pred: n02106662-German_shepherd
ans: n02106662-German_shepherd
pred: n02089867-Walker_hound
ans: n02089867-Walker_hound

pred: n02110063-malamute
ans: n02110185-Siberian_husky
pred: n02097209-standard_schnauzer
ans: n02097209-standard_schnauzer
pred: n02099712-Labrador_retriever
ans: n02099712-Labrador_retriever

pred: n02108000-EntleBucher
ans: n02116738-African_hunting_dog
pred: n02092002-Scottish_deerhound
ans: n02092002-Scottish_deerhound
pred: n02100583-vizsla
ans: n02100583-vizsla

結論及び今後の課題

本プログラムの目的は「あの犬、何という犬種だったけ?」の疑問点の解決だった。モデル学習の結果は約40%の精度だった。しかし、上記予測結果の「beagleとEnglish_foxhound」のように人間でも区別しにくい犬種があることを踏まえると、人の認識能力と大きくは変わらないかもしれない。

今後の課題としては以下の項目が考えられる。

  • データセット
    • 同モデルを用いて他データセットの検証
  • データの前処理
    • 正規化、データの水増しの利用
  • モデル
    • VGG16以外のモデル利用
    • 自作レイヤーの調整

おわりに

  • まずAidemyのTutor様達、色んな質問に対応してくださり本当にありがとうございました。
  • 受講ペースの調節に失敗して三日で本記事を作り上げましたが、それなりの結果は得られて良かったと思います。
  • E資格受講のため、一か月延長しましたが、その間にこの記事の今後の課題を一つずつ解決していこうと思います。
  • またその時いろいろお世話になると思いますので、よろしくお願いします。

Discussion