物体検知の最近の記事

はじめに

このブログは前回、ドーナツの無人レジ化に向け機械学習をどのように用いるかを紹介しました。今回は、その中で出てきたドーナツ検出器の中身について紹介します。


目次

  • はじめに
  • 検出器を作るために必要なもの
  • どのような流れで作るか
  • 実際に作る
  • まとめ


必要なもの

ドーナツ検出器を作るために、ドーナツの画像データを訓練とテストを用意します。
今回は、「6種類のドーナツを検出し、合計金額を出す」ことが目標として、6種類のドーナツそれぞれの写真を50枚ずつ撮影しました。また、ドーナツが裏返っていても判断できるよう、裏返ったドーナツも同様に撮影しています(訓練データ)。テストデータは複数のドーナツが写る写真を撮りました。このドーナツの中には裏返ったものや写真からきれてしまっているドーナツも含んでいます。

スクリーンショット 2020-03-12 16.04.54.png


作成手順

ドーナツ検出器をどのような流れで作るかを紹介します。物体検出のためにYOLO3を用います。

 大まかな流れとして、このような作業をしています。

  1. 学習用の画像を整える、画像の水増し
  2. YOLO3でテスト用データのドーナツ一つ一つを検出する
  3. 学習モデルを用いてドーナツ名を予測する
  4. 合計金額などを表示する


1.ではYOLO3を用いて学習用の画像データからドーナツの部分だけを検出します。検出後、画像のアスペクト比(縦横比)を変えずにリサイズをします。画像は一つのドーナツに対して50枚しかないので回転や反転を用いて画像を水増しましょう。一つのドーナツに対して1500枚まで水増ししました。
3.では2.で検出したドーナツ画像から学習モデルを作成します。VGG16を用いて作成しました。


実際に作る

訓練データからドーナツの部分だけ検出する

スクリーンショット 2020-03-12 17.19.50.png

必要なライブラリをインポートします。

import logging
logging.disable(logging.WARNING)
import os
import glob

import numpy as np
from keras import backend as K
from keras.layers import Input

from PIL import Image

from yolo import YOLO
from PIL import Image
from yolo3.utils import letterbox_image

from matplotlib import pyplot as plt

keras-yolo3のYOLOクラスを継承してdetect_imageメソッドを修正します。

class CustomYOLO(YOLO):
  _defaults = {
        "model_path": 'model_data/yolo.h5',
        "anchors_path": 'model_data/yolo_anchors.txt',
        "classes_path": 'model_data/coco_classes.txt',
        "model_image_size" : (640, 640),
        "gpu_num" : 1,
    }

  def __init__(self, score=0.3, iou=0.45):
    self.__dict__.update(self._defaults)
    self.score = score
    self.iou = iou
    self.class_names = self._get_class()
    self.anchors = self._get_anchors()
    self.sess = K.get_session()
    self.boxes, self.scores, self.classes = self.generate()

  def detect_image(self, image):
        if self.model_image_size != (None, None):
            assert self.model_image_size[0]%32 == 0, 'Multiples of 32 required'
            assert self.model_image_size[1]%32 == 0, 'Multiples of 32 required'
            boxed_image = letterbox_image(image, tuple(reversed(self.model_image_size)))
        else:
            new_image_size = (image.width - (image.width % 32),
                              image.height - (image.height % 32))
            boxed_image = letterbox_image(image, new_image_size)
        image_data = np.array(boxed_image, dtype='float32')

        image_data /= 255.
        image_data = np.expand_dims(image_data, 0)  # Add batch dimension.

        out_boxes, out_scores, out_classes = self.sess.run(
            [self.boxes, self.scores, self.classes],
            feed_dict={
                self.yolo_model.input: image_data,
                self.input_image_shape: [image.size[1], image.size[0]],
                K.learning_phase(): 0
            })

        donut_out_boxes = []

        for i, c in reversed(list(enumerate(out_classes))):
          if self.class_names[c] == 'donut':
            box = out_boxes[i]
            top, left, bottom, right = box
            top = max(0, np.floor(top + 0.5).astype('int32') - 100)
            left = max(0, np.floor(left + 0.5).astype('int32') - 100)
            bottom = min(image.size[1], np.floor(bottom + 0.5).astype('int32') + 100)
            right = min(image.size[0], np.floor(right + 0.5).astype('int32') + 100)
            donut_out_boxes.append([left, top, right, bottom])

        return donut_out_boxes

YOLOで訓練画像からドーナツ部分を切り取って画像を保存します。

def crop_donuts(front_or_back):
  for dir_name in label_dict.keys():
    print(dir_name)
    output_path = '../data/cropped_donut_images/' + front_or_back + '/' + dir_name

    if not os.path.exists(output_path):
      os.makedirs(output_path)

    for file_path in glob.glob('../data/donut_images/' + front_or_back + '/' + dir_name + '/*.jpg'):
      file_name = file_path[-12:-4] 
      image = Image.open(file_path)
      out_boxes = yolo.detect_image(image.copy())

      if len(out_boxes) == 1:
        cropped_img = image.crop(out_boxes[0])
        cropped_img.save(output_path + '/' + str(file_name) + '.jpg')

訓練画像内にはドーナツが1つしかないはずなので、2つ以上ドーナツを検出した場合は保存せずその画像はスキップします。保存した画像を確認し、検出したがうまく切り取れていない画像はフォルダから削除してください。

YOLOでの検出がうまくいかなかった画像、検出したが切り出しがうまくいかなかった画像を抽出します。

def copy_undetected_images(front_or_back):
  for dir_name in label_dict.keys():
    print(dir_name)
    output_path = '../data/not_detected_images/' + front_or_back + '/' + dir_name

    if not os.path.exists(output_path):
      os.makedirs(output_path)

    for file_path in glob.glob('../data/donut_images/' + front_or_back + '/' + dir_name + '/*.jpg'):
      file_name = file_path[-12:-4] 
      path = '../data/cropped_donut_images/' + front_or_back + '/' + dir_name + '/' + file_name + '.jpg'

      if not os.path.exists(path):
        image = Image.open(file_path)
        image.save(output_path + '/' + str(file_name) + '.jpg')

ここで、抽出されたものは、手動でドーナツ部分を切り取りました。

1つ前の関数で抽出した画像をcroppeddonutimagesフォルダ下の適切なフォルダにコピーします。

def copy_images_cropped_by_hands(front_or_back):
  for dir_name in label_dict.keys():
    print(dir_name)
    output_path = '../data/cropped_donut_images/' + front_or_back + '/' + dir_name

    if not os.path.exists(output_path):
      os.makedirs(output_path)

    for file_path in glob.glob('../data/images_cropped_by_hands/' + front_or_back + '/' + dir_name + '/*.jpg'):
      file_name = file_path[-12:-4] 
      path = output_path + '/' + file_name + '.jpg'

      if not os.path.exists(path):
        image = Image.open(file_path)
        image.save(path)

ドーナツの画像分類のモデル作成

スクリーンショット 2020-03-12 17.25.30.png

アスペクト比を変えずに設定した大きさに画像をリサイズします。

def resize(img, base_w, base_h):
  base_ratio = base_w / base_h
  img_h, img_w = img.shape[:2]
  img_ratio = img_w / img_h

  white_img = np.zeros((base_h, base_w, 3), np.uint8)
  white_img[:, :] = [220, 220, 220]

  if img_ratio > base_ratio:
    h = int(base_w / img_ratio)
    w = base_w
    resize_img = cv2.resize(img, (w, h))
  else:
    h = base_h
    w = int(base_h * img_ratio)
    resize_img = cv2.resize(img, (w, h))

  white_img[int(base_h/2-h/2):int(base_h/2+h/2),int(base_w/2-w/2):int(base_w/2+w/2)] = resize_img
  resize_img = white_img

  return resize_img

正解ラベルのエンコード用に辞書を定義します。

label_dict = {
    'エンゼルクリーム': 0,
    'エンゼルフレンチ': 1,
    'オールドファッションハニー': 2,
    'ダブルチョコレート': 3,
    'チョコファッション': 4,
    'ポンデリング': 5,
}

データセットを作成します。

front_path = './drive/My Drive/donuts_detection/data/cropped_donut_images/front/'
back_path = './drive/My Drive/donuts_detection/data/cropped_donut_images/back/'

IMG_SIZE = 256
X = []
y = []

print("== DONUTS FRONT ==")
for dir_name in label_dict.keys():
  print(dir_name)
  label = label_dict[str(dir_name)]

  X_tmp = []
  y_tmp = []

  for file_path in glob.glob(front_path + dir_name + '/*.jpg'):
    img = image.load_img(file_path)
    # img = rotate(img)
    img = image.img_to_array(img)
    img = resize(img, IMG_SIZE, IMG_SIZE)

    X_tmp.append(img)
    y_tmp.append(label)

  for x in datagen.flow(np.array(X_tmp), batch_size=1):
    new_image = x[0]

    X_tmp.append(new_image)
    y_tmp.append(label)

    if len(X_tmp) % 1000 == 0:
      break

  X.extend(X_tmp)
  y.extend(y_tmp)

del X_tmp, y_tmp

print("== DONUTS BACK ==")
for dir_name in label_dict.keys():
  print(dir_name)
  label = label_dict[str(dir_name)]

  X_tmp = []
  y_tmp = []

  for file_path in glob.glob(back_path + dir_name + '/*.jpg'):
    img = image.load_img(file_path)
    # img = rotate(img)
    img = image.img_to_array(img)
    img = resize(img, IMG_SIZE, IMG_SIZE)

    X_tmp.append(img)
    y_tmp.append(label)

  for x in datagen.flow(np.array(X_tmp), batch_size=1):
    new_image = x[0]

    X_tmp.append(new_image)
    y_tmp.append(label)

    if len(X_tmp) % 1000 == 0:
      break

  X.extend(X_tmp)
  y.extend(y_tmp)

X = np.array(X, dtype='float32')
X /= 255.0
y = np.array(y)
y = np_utils.to_categorical(y, len(label_dict))

データの水増しを行いながらデータセットを作成します。 ドーナツの表面だけ使用するなら、それぞれのラベルにつき1500枚までメモリ不足にならずに水増しできました。表面・裏面とも使用する場合はメモリ容量の関係で各ラベル1000枚まで水増ししました。

モデルを作成します。

def create_model():
  base_model=VGG16(weights='imagenet',include_top=False, input_tensor=Input(shape=(IMG_SIZE, IMG_SIZE, 3)))
  x = base_model.output
  x = GlobalAveragePooling2D()(x)
  x = Dense(1024, activation='relu')(x)
  prediction = Dense(len(label_dict), activation='softmax')(x)

  model=Model(inputs=base_model.input, outputs=prediction)

  for layer in base_model.layers:
      layer.trainable = False

  model.compile(loss='categorical_crossentropy', optimizer='adam', metrics=['acc'])

  return model

VGG16の事前学習モデルを利用しました。モデルにVGG16の全結合層は含まず、分類用に独自に全結合層を追加しています。また、VGG16の層は学習しないようにしています。

モデルを学習して保存します。

EPOCHS = 20
BATCH_SIZE = 64
MODEL_PATH = './drive/My Drive/donuts_detection/models/donuts_front_and_back_cropped_model_2.h5'

X_train, X_valid, y_train, y_valid = train_test_split(X, y, test_size=0.30)

model = create_model()

history = model.fit(
    X_train, y_train,
    validation_data=[X_valid, y_valid],
    epochs=EPOCHS,
    batch_size=BATCH_SIZE
)

model.save(MODEL_PATH)

エポック数とバッチサイズは適切なものを選んで下さい。

画像内のドーナツをすべて検出し分類する

スクリーンショット 2020-03-12 17.30.46.png

必要な辞書を定義します。

label_dict = {

    'エンゼルクリーム': 0,
    'エンゼルフレンチ': 1,
    'オールドファッションハニー': 2,
    'ダブルチョコレート': 3,
    'チョコファッション': 4,
    'ポンデリング': 5,
}

inverse_dict = dict([(v, k) for k, v in label_dict.items()])

'''
{
   商品名: [値段, 熱量(kcal), たんぱく質(g), 脂質(g), 炭水化物(g), 食塩相当量(g)]
}
'''
menu_dict = {
    'エンゼルクリーム': [120, 219, 1.3, 12.0, 26.1, 0.6],
    'エンゼルフレンチ': [130, 200, 2.0, 15.5, 12.6, 0.3],
    'オールドファッションハニー': [110, 355, 3.4, 18.0, 44.4, 0.8],
    'ダブルチョコレート': [130, 265, 3.6, 16.8, 24.3, 0.5],
    'チョコファッション': [130, 330, 3.7, 20.8, 31.3, 0.8],
    'ポンデリング': [110, 219, 1.3, 12.0, 26.1, 0.6],
}

YOLOで取得したバウンディングボックスを元に画像からドーナツ部分をトリミングして分類します。

image = Image.open("../data/donut_images/multiple_front_and_back/IMG_2367.jpg")
r_image, out_boxes = yolo.detect_image(image.copy())

plt.figure(figsize=(6, 8))
plt.imshow(r_image)

X = []
for box in out_boxes:
  cropped_img = image.crop(box)
  cropped_img = np.asarray(cropped_img)
  cropped_img = resize(cropped_img, IMG_SIZE, IMG_SIZE)
  X.append(cropped_img)

X = np.array(X, dtype='float32')
X /= 255.0

preds = model.predict(X)

pred_labels = [inverse_dict[pred] for pred in np.argmax(preds, axis=1)]
result = np.zeros(6)

for label in pred_labels:
  result += np.array(menu_dict[label])

print()
print(pred_labels)

print()
print(f"値段:         {int(result[0])}")
print(f"熱量(kcal):   {result[1]:.1f}")
print(f"たんぱく質(g): {result[2]:.1f}")
print(f"脂質(g):      {result[3]:.1f}")
print(f"炭水化物(g):   {result[4]:.1f}")
print(f"食塩相当量(g): {result[5]:.1f}")
print()
image

まとめ

最後にどれだけ正確に判定できているか、分類のパフォーマンスを計算してみましょう。

def extract_labels(row):
  labels = []
  for key in label_dict.keys():
    if row[key] == '1':
      labels.append(key)
  return labels

df = pd.read_csv('../multiple_front_and_back.csv')
df['labels'] = df.apply(extract_labels, axis=1)

donuts_num = 0
detected_donuts_num = 0
corrected_classified_donuts_num = 0

print('== INCORRECTLY DETECTED ==')

for file_path in glob.glob('../data/donut_images/multiple_front_and_back/*.jpg'):
  file_name = file_path[-12:-4]
  correct_labels = df[df['file_name'] == file_name]['labels'].values[0]
  donuts_num += len(correct_labels)

  image = Image.open(file_path)
  r_image, out_boxes = yolo.detect_image(image.copy())

  X = []
  for box in out_boxes:
    cropped_img = image.crop(box)
    cropped_img = np.asarray(cropped_img)
    cropped_img = resize(cropped_img, IMG_SIZE, IMG_SIZE)
    X.append(cropped_img)

  X = np.array(X, dtype='float32')
  X /= 255.0

  preds = model.predict(X)
  pred_labels = [inverse_dict[pred] for pred in np.argmax(preds, axis=1)]

  if len(pred_labels) != len(correct_labels):
    print(file_name)

  for label in pred_labels:
    if label in correct_labels:
      corrected_classified_donuts_num += 1
      correct_labels.remove(label)

  detected_donuts_num += len(pred_labels)

print('==========================')

print()
print('The number of all donuts: {:d}'.format(donuts_num))
print('The number of donuts detected by YOLO: {:d}'.format(detected_donuts_num))
print('Percentage of donuts detected by YOLO: {:.2f}'.format(detected_donuts_num / donuts_num))
print('Percentage of donuts classified correctly out of all dontuts: {:.2f}'.format(corrected_classified_donuts_num / donuts_num))
print('Percentage of donuts classified correctly out of detected dontuts: {:.2f}'.format(corrected_classified_donuts_num / detected_donuts_num))

出力結果は以下のようになりました。

== INCORRECTLY DETECTED ==
IMG_2331
IMG_2344
IMG_2357
IMG_2356
==========================

The number of all donuts: 195
The number of donuts detected by YOLO: 195
Percentage of donuts detected by YOLO: 1.00
Percentage of donuts classified correctly out of all dontuts: 0.96
Percentage of donuts classified correctly out of detected dontuts: 0.96

つまり、ドーナツはYOLOによって100%検出され、すべてのドーナツ(検出されたドーナツ)を96%の割合で正しく分類できています。
このシステムにより、今まで手動で行っていたドーナツのレジ計算を自動で行うことができます。

もちろん、ドーナツだけではなく、お弁当詰めにミスがないかチェックしたり、工場の生産ラインで不良品を弾くこともできます。


Twitter・Facebookで定期的に情報発信しています!

概要

先日の勉強会にてインターン生の1人が物体検出について発表してくれました。これまで物体検出は学習済みのモデルを使うことが多く、仕組みを知る機会がなかったのでとても良い機会になりました。今回の記事では発表してくれた内容をシェアしていきたいと思います。
あくまで物体検出の入門ということで理論の深堀りや実装までは扱いませんが悪しからず。


物体検出とは

ディープラーニングによる画像タスクといえば画像の分類タスクがよく挙げられます。例としては以下の犬の画像から犬種を識別するタスクなどです。

ディープラーニングで識別してみると

  • コーギー: 75%
  • ポメラニアン: 11%
  • チワワ: 6%
  • ...


のようにどの犬種か、確率としては出てくるものの画像内に犬が2匹以上いた場合は対応できなくなってしまいます。

example of image classification


この問題を解決するために物体検出のアルゴリズムが開発されました。物体検出の技術を使えば画像中の複数の物体の位置を特定して矩形(バウンディングボックス)で囲み、更にそれぞれの矩形について物体の識別を行うことが可能になります。

物体検出の例が以下になります。犬と猫がバウンディングボックスで囲まれ、それぞれ犬か猫か識別されていることがわかります。

example of image detection

物体検出モデルの歴史は深くR-CNNから始まりFast R-CNN、Faster R-CNNと精度と処理速度が改善されてきました。これらの手法は基本的に以下の動画のようにバウンディングボックスを画像内で色々と動かして物体が検出される良い場所を見つけ出そうというものでした。

example of image detection

物体検出についての歴史まとめより引用

その後、精度と処理速度とともにFaster R-CNNを上回るSSD(Single Shot Multibox Detector)が提案されました。
今回の勉強会ではこのSSDを解説してくれましたので、復習がてらこちらで私が解説させていただきます。


SSD (Single Shot Multibox Detector)

R-CNNではバウンディングボックスを色々と動かしてそのたびにCNNによる演算を行っていたので、1枚の画像から物体検出を行うのにかなりの処理時間がかかっていました。一方でSSDでは"Single Shot"という名前が暗示しているように、1度のCNN演算で物体の「領域候補検出」と「クラス分類」の両方を行います。これにより物体検出処理の高速化を可能にしました。


全体の構造

structure of SSD

SSD: Single Shot MultiBox Detectorより引用

SSDのネットワークは最初のレイヤー(ベースネットワーク)に画像分類に使用されるモデルを用いています。論文ではVGG-16をベースネットワークとしています。ベースネットワークの全結合層を切り取り、上のように畳み込み層を追加したものがSSDの構造になります。

予測の際はそれぞれのレイヤーから特徴マップを抽出して物体検出を行います。具体的にはそれぞれの特徴レイヤーに3×3の畳み込みフィルタを適用してクラス特徴と位置特徴を抽出します


検出の仕組み

SSD framework

SSD: Single Shot MultiBox Detectorより引用

(a)が入力画像と各物体の正解ボックスです。(b)と(c)のマス目は特徴マップの位置を表しており、各位置においてデフォルトボックスと呼ばれる異なるアスペクト比の矩形を複数設定します。各位置の各デフォルトボックスについてスコア(confidence)の高いクラスを検出します。

訓練時には各クラスの誤差と、デフォルトボックスと正解ボックスの位置の誤差を元にモデルの学習を行います。

勉強会のスライドがわかりやすかったのでこちらも参考にしていただければと思います。

a slide about how to detect objects using SSD


様々なスケールの物体を検出する仕組み

検出の仕組みを示した上の図で(b)と(c)を見ると4×4の特徴マップの方がデフォルトボックスが大きく、各特徴マップではデフォルトボックスの大きさが異なることがわかります。
SSDでは、サイズの違う畳み込み層をベースネットワークの後に追加することで様々なスケールで物体を検出することができます。

how to detect objects with various size


以下のように各特徴マップにおいてクラス特徴量と位置特徴量を算出します。

feature map at each layer


そして各畳み込み層で違うスケールで物体検出を行います。

result of detection as each layer


これにより各層からの検出結果が得られるので次の図のように重複が生じてしまいます。

result of detection with duplicate


そこで次のような手順を踏んで重複を除去します。

  1. スコアが高いデフォルトボックスのみ抽出。
  2. 各クラスごとにデフォルトボックスの重なり率IoUを計算
  3. IoUが高い場合はスコアの低いデフォルトボックスを除去


この処理の結果、1つの物体に複数のバウンディングボックスが付与されることを回避できます。

result of post-processing


ちなみに重なり率IoUは次のように計算されます。

how to calculate IoU

学習の仕組み

各特徴マップについて各クラスのスコアの誤差と、デフォルトボックスの位置誤差との合成関数から正解データとの誤差を計算します。クラス誤差と位置誤差それぞれの具体的な計算は論文を参照していただきたいですが、最終的な損失関数は2つの損失関数を重み付けしたものになります。

$$ L(x, c, l, g) = \frac{1}{N}(L_{CLS}(x, c) + \alpha L_{LOC}(x, l, g)) $$

この計算結果を元に誤差逆伝播法によりモデルの重みを更新します。

training SSD


まとめ

個人的に勉強会が行われた時期に物体検出の技術を使っていたので非常に勉強になりました。やはり使っている技術の中身を知ることは大切ですので、SSD以前の物体検出の手法についても勉強していきたいですね。次回はSSDと並んでよく利用されるYOLOの解説と実装をしてくれるとのことで楽しみです。


参考文献

  • Liu, W., Anguelov, D., Erhan, D., Szegedy, C., Reed, S., Fu, C. Y., & Berg, A. C. (2016, October). Ssd: Single shot multibox detector. In European conference on computer vision (pp. 21-37). Springer, Cham..
  • その他特に言及のない画像は勉強会のスライドより引用


Twitter・Facebookで定期的に情報発信しています!

概要

物体検知の分野ではCOCOと呼ばれるデータセットを使って、検知手法の精度に関して数値的な評価が行われます。2020年1月現在、トップの正解率を示しているのが、2019年9月に発表されたCBNetを用いた手法です。

今回は物体検知に関して全くの初心者の方でも理解できるように、この論文を解説していきたいと思います。(原著論文はこちら)

目次

前提知識

CBNetを理解する上で必要な物体検知についての知識をまとめます。以下の3つを押さえればCBNetだけでなく、物体検知全体の概要もつかめると思います。

Backbone

CNNの構造は以下のようになっているのでした。
※図はここから引用

cnn.jpg

CNNの特徴は、上の図のFEATURE LEARNINGのように、ConvolutionとPoolingを繰り返して画像から特徴量を抽出することです。ここで得られた特徴量をもとにして、ニューラルネットワークで学習・予測を行うので、これをいかにうまく行うかが認識精度に直結します。その意味で、この特徴抽出の部分を論文に従って「Backbone」と呼ぶことにします。

CNNベースの物体検知

物体検知をするには、位置の検出と、物体の分類という、2つのタスクを行う必要があります。物体検知の手法はたくさんありますが、この2つのタスクを同時に行うか、別々に行うかで大きく2つに分かれます。

今回はこのうちで、後者の方法に注目します。例えば、最初期に使われていたR-CNNという手法では、まず物体がありそうな場所(これをRoIと呼びます)を見つけ、その後各RoIについてCNNを実行し分類を行います。ただ、これではすごく時間がかかってしまうので、これ以降の手法では様々な工夫をしていますが、基本的なアイディアは同じです。

従って、当然ですがCNNの精度を上げることが、物体検知の精度を上げることにつながります。

detection_ex.png

モデルの評価

モデルの性能を評価するには、どの程度正確に物体検知ができているかという精度の面と、どれくらいの早さで物体検知ができるのかというスピードの面の、両方を考慮する必要があります。例えば自動運転に使うモデルでは、いくら精度が高くても処理スピードが遅いと使い物になりませんよね。

スピードの指標は簡単で、1秒当たり何枚の画像を処理できるか、で決まります。例えば、高速で知られるYOLOというモデルは、条件にもよりますが、1秒で30枚ほど処理できますので、スピードは 30 fps ということになります。

では、精度についてはどのように測るかというと、位置の精度と分類の精度に分けて考えます。まず、位置の精度を求めるには以下のように計算します。iou.png

※図はここから引用

モデルが予測した位置と実際の位置の重なり具合を評価するわけです。完全に一致すると IoU = 1、全く違っている場合はIoU = 0 となります。

分類の精度については、AP や mAP といった数値が用いられます。これらを求めるのは少し複雑なのでこちらを参照してください。「単純にいくつ正解したか、じゃダメなの?」と思うかもしれませんが、それは正確ではありません。例えば画像の中に、リンゴが3つと梨が1つあったとします。いくつ正解したか、を精度の指標にしたとすると、全ての物体を「リンゴ」と予測するモデルが仮にあったとしても 0.75 という高い精度が出てしまうからです。

APもmAPも数字が大きいほど分類精度が高いことを意味します。

実際の評価では、位置の精度と分類の精度を組み合わせて、例えば \(AP_{50}\) のように表します。これは IoU が 0.5 以上の場合のみを考える、という意味です。

CBNetの構造

これまでの議論を踏まえた上で、いよいよ最新の手法CBNetについて解説したいと思います。

CBNetは"Composite Backbone Network"の略です。Compositeは「複合された」という意味なので、複数のBackboneを合わせた構造をしている、ということです。CBNetの新しいところはCNNによって得られた特徴マップを用いて物体検知を行う部分ではなく、CNNそれ自体に階層的な構造を導入したことです。(物体検知部分は他のモデルを使います。)

AHLC

CBNetの本質は以下の図に集約されているので、これを説明します。


cbnet.PNG

普通のCNNではBackboneは一つだけです。図でいうと、右端の一列しかありません。一方のCBNetでは複数のBackboneを用意し(通常2つか3つ)、一つのBackboneの出力を次のBackboneへと入力するという方法をとっています。最終的に右端のBackbone(Lead Backbone)に左にあるBackbone(Assistant Backbone)の効果がすべて反映されます。そしてLead Backbone の出力を最終的な特徴マップとして使い、物体検知を行います。

言葉では分かりにくいので数式で表現します。まず、通常のCNNの場合

\[ x^l = F^l \left(x^{l-1}\right),\,l\geq 2 \]

と表現できます。左辺の \(x^l\) は \(l\) 層目の出力を表しています。\(x^l\) はその前層の入力 \(x^{l-1}\) に何らかの処理(畳み込みやプーリング) \(F^l\) を施すことで得られる、ということを意味します。

続いてCBNetの場合は、次のように表現できます。

\[ x_k^l = F_k^l \left(x_k^{l-1} + g\left(x_{k-1}^l\right)\right),\,l\geq 2 \]

となります。\(x\) や \(F\) の右下に新しい添字 \(k\) が付きましたが、これは \(k\) 番目のBackboneであることを表します。また、新たに \(g\left(x_{k-1}^l\right)\) という項が加わりましたが、これは前の( \(k-1\) 番目の)Backboneの \(l\) 番目の出力に、(サイズを変えるなどの)処理 \(g\) を施したものも入力として加味することを意味します。このタイプを論文では AHLC (Adjacent Higher-Level Composition = 隣の上位層との合成)と呼んでいます。

CBNetの本質はこれで尽くされていますが、一つ前のBackboneの出力をどのように次のBackboneに入力するかによっていくつかのパターンがあります。順に見ていきましょう。

SLC (Same Level Composition = 同じ階層同士の合成)

以下の図を見て先程のAHLCと比べると分かりやすいです。

slc.png

数式で表現すると次のようになります。

\[ x_k^l = F_k^l \left(x_k^{l-1} + g\left(x_{k-1}^{l-1}\right)\right),\,l\geq 2 \]

ALLC (Adjacent Lower-Level Composition = 隣の下位層との合成)

allc.png

数式で表現すると、

\[ x_k^l = F_k^l \left(x_k^{l-1} + g(x_{k-1}^{l-2})\right),\,l\geq 2 \]

※論文中では \(g(x_{k-1}^{l+1})\)となっていますが、誤植だと思われます。

DHLC (Dense Higher-Level Composition)

dhlc.png

数式で表現すると、

\[ x_k^l = F_k^l \left(x_k^{l-1} + \sum_{i=l}^L g_i\left(x_{k-1}^i\right)\right),\,l\geq 2 \]

ここで \(L\) は各Backboneの階層数です。

まとめとして、以上の4つのモデルを並べてみます。

cbnet.png

結果

最後に色々なパラメータを変えたときのCBNetの結果について示します。実行環境などの条件は論文を参照してください。

変えるパラメータは以下の3つです。

  1. CBNetの種類

  2. 得られた特徴マップから物体検知を行うモデル

  3. Backboneの数

1. CBNetの種類を変えた場合

compare_cbnet.png

これを見ると、一番最初に示したAHLCが最も高い精度を示しているといえます。

ADLCとAHLC(DHLCと同じです)との比較から、単にパラメータを増やせば精度が上がる、という問題ではないことが言えます。

2. 物体検知を行うモデルを変えた場合

compare_model.png

Singleは普通のCNN、DBは2層のBackbone、TBは3層のBackboneを表しています。これを見ると、どのモデルを使ってもCBNetが有効であることがわかります。これはCBNetによって画像の特徴がより良く抽出されていることを示唆しています。

3. Backboneの数を変えた場合

compare_backbone.png

Backboneの数を増やすほど精度が上がっているのが見て取れます。計算量とのトレードオフも考慮すると、2層か3層にするのが良さそうです。

最後に、CBNetを用いた物体検知モデルと、他の有力な物体検知モデルとの比較です。

compare_all.png

Cascade Mask R-CNNとCBNetを組み合わせたモデルが、精度の面では最高の成績を残しています。とはいっても未だに50%強の成績であり、改善の余地は十二分にあります。また、肝心な点である、なぜこうすると精度が上がるのか、ということは分かりません。現在の機械学習のモデルでありがちですが、よく分からないけどこうすると精度が上がったよ〜、というのが現状です。


Twitter・Facebookで定期的に情報発信しています!

このアーカイブについて

このページには、過去に書かれた記事のうち物体検知カテゴリに属しているものが含まれています。

前のカテゴリはコラムです。

次のカテゴリは機械学習です。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。