Я обучил модель ResNet-50 находить мяч для пинг-понга в кадре видео. Моя модель хороша или я только что обучил детектор «белых капель»?

В течение нескольких недель после моего поста в« поисковой системе видео я взволнованно показывал свои успехи всем, кто хотел меня слушать.

В то время как друзья в сообществе машинного обучения считали это изящным, меня мучил вопрос:

Моя модель хороша?

Я только что обучил детектор белых капель?

Иногда моя модель находила мяч для пинг-понга, и я восклицал от радости. Но в других случаях модель выводила мячи для настольного тенниса на шортах игроков, лысинах и многом другом. Не очень впечатляет.

Следующим шагом на моем пути было измерение точности модели.

Точность обнаружения объектов измеряется с использованием вариантов средней средней точности (MAP). Существует бесчисленное количество сообщений, описывающих варианты этой метрики, поэтому я не буду на них останавливаться. Но стоит суммировать ключевые моменты, которые будут отражены в приведенных ниже примерах кода.

Предпосылки и методология

Средняя средняя точность (mAP) - это среднее значение средней точности (AP) каждого класса, которое измеряется как площадь под кривой Precision-Recall для каждого класса:

Предположим, детектор объектов может идентифицировать 20 различных классов. MAP вычисляет среднюю точность для каждого класса, а затем усредняет результат.

Следовательно, нам нужно рассчитать точность для заданных значений отзыва:

а затем измерьте площадь под кривой для пар «точность-отзыв». Для простоты зигзаги кривой Precision-Recall сглажены, например:

Таким образом, нам нужно рассчитать точность и отзыв. Эти формулы информативны:

где точность относится к точности (какой процент правильных прогнозов среди всех прогнозов), а отзыв относится к истинно положительному показателю (отношение истинных положительных результатов ко всей достоверности положительные).

В Испытании PASCAL VOC (который включает метрики оценки для сравнительного анализа моделей обнаружения объектов) прогноз является истинно положительным, если IoU больше или равно 0,5. IoU меньше 0,5 является ложным срабатыванием (как и дублированные ограничивающие прямоугольники).

IoU - это метрика оценки, которая вычисляет перекрытие между ограничивающей рамкой наземной истины и предсказанной ограничивающей рамкой. Этот рисунок из Джонатана Хуэя представляет собой прекрасное резюме:

IoU довольно легко вычислить:

  • В числителе мы вычисляем область перекрытия между предсказанной ограничивающей рамкой и ограничивающей рамкой наземной истины.
  • Знаменатель - это площадь объединения, охватываемая как предсказанной ограничивающей рамкой, так и ограничивающей рамкой наземной истины.
  • Разделение области перекрытия на площадь объединения дает оценку «Пересечение по объединению».

Теперь у нас есть все необходимое для расчета точности моей модели. В следующем разделе показаны фрагменты кода для реализации описанных выше шагов.

Средняя точность для моей поисковой системы видео

Начнем с импорта необходимых пакетов для этого примера:

# import the necessary packages
import json
import os
import boto3
from collections import namedtuple
import cv2
import matplotlib.image as mpimg
import pandas as pd
# set Pandas table options for viewing data on screen
pd.set_option(‘display.max_columns’, 500)
pd.set_option(‘display.width’, 1000)

Следуя примеру Адриана Роузброка, мы определяем объект обнаружения, который будет хранить три атрибута:

  • image_path: путь к нашему входному изображению, которое находится на диске.
  • gt: ограничивающий прямоугольник.
  • pred: предсказанная ограничивающая рамка из нашей модели.

Затем мы создаем функцию для вычисления пересечения по объединению:

# Define the `Detection` object
Detection = namedtuple("Detection", ["image_path", "gt", "pred"])
def bb_intersection_over_union(boxA, boxB):
  # determine the (x, y)-coordinates of the intersection rectangle
  xA = max(boxA[0], boxB[0])
  yA = max(boxA[1], boxB[1])
  xB = min(boxA[2], boxB[2])
  yB = min(boxA[3], boxB[3])
  # compute the area of intersection rectangle
  interArea = max(0, xB - xA + 1) * max(0, yB - yA + 1)
  # compute the area of both the prediction and ground-truth
  # rectangles
  boxAArea = (boxA[2] - boxA[0] + 1) * (boxA[3] - boxA[1] + 1)
  boxBArea = (boxB[2] - boxB[0] + 1) * (boxB[3] - boxB[1] + 1)
  # compute the intersection over union by taking the intersection
  # area and dividing it by the sum of prediction + ground-truth
  # areas - the interesection area
  iou = interArea / float(boxAArea + boxBArea - interArea)
  # return the intersection over union value
  return iou
def bb_intersection_over_union(boxA, boxB):
 # determine the (x, y)-coordinates of the intersection rectangle
 xA = max(boxA[0], boxB[0])
 yA = max(boxA[1], boxB[1])
 xB = min(boxA[2], boxB[2])
 yB = min(boxA[3], boxB[3])
 # compute the area of intersection rectangle
 interArea = max(0, xB - xA + 1) * max(0, yB - yA + 1)
 # compute the area of both the prediction and ground-truth
 # rectangles
 boxAArea = (boxA[2] - boxA[0] + 1) * (boxA[3] - boxA[1] + 1)
 boxBArea = (boxB[2] - boxB[0] + 1) * (boxB[3] - boxB[1] + 1)
 # compute the intersection over union by taking the intersection
 # area and dividing it by the sum of prediction + ground-truth
 # areas - the interesection area
 iou = interArea / float(boxAArea + boxBArea - interArea)
 # return the intersection over union value
 return iou

В идеале мы повторяем этот расчет для каждого изображения в нашем наборе проверки. Итак, давайте создадим массив кортежей обнаружения, (1) перебирая каждое проверочное изображение; (2) вызов конечной точки обнаружения объекта; и (3) сохранение результатов ограничивающего прямоугольника предсказания, ограничивающего прямоугольника наземной истины и пути изображения. Это делает свою работу:

for filename in os.listdir(directory):
 if filename.endswith(".png"):
  file_with_path = (os.path.join(directory, filename))
  img = mpimg.imread(file_with_path)
  height = img.shape[0]
  width = img.shape[1]
  with open(file_with_path, 'rb') as image:
   f = image.read()
   b = bytearray(f)
   ne = open('n.txt', 'wb')
   ne.write(b)
response = runtime_client.invoke_endpoint(EndpointName=endpoint_name, ContentType='image/png', Body=b)
  result = response['Body'].read().decode('ascii')
  detections = json.loads(result)
  best_detection = detections['prediction'][0] # ordered by max confidence; take the first one b/c only one ping pong ball in play ever
  print(best_detection)
(klass, score, x0, y0, x1, y1) = best_detection
  xmin = int(x0 * width)
  ymin = int(y0 * height)
  xmax = int(x1 * width)
  ymax = int(y1 * height)
  pred_pixels = [xmin, ymin, xmax, ymax]
  gt_pixels = find_gt_bbox_for_image(annotation_filename, filename)
  det = Detection(filename, gt_pixels, pred_pixels)
  det_array.append(det)
  mAP_df.loc[filename, 'Confidences'] = score
  continue
 else:
  continue

Обратите внимание, что приведенный выше фрагмент извлекает ограничивающую рамку достоверной информации, ссылаясь на файл annotations.json, используя имена файлов изображений в качестве ключей, используя это:

def find_gt_bbox_for_image(annotation_filename, image_filename):
 with open(annotation_filename) as f:
  js = json.load(f)
  images = js['images']
  categories = js['categories']
  annotations = js['annotations']
  for i in images:
   if i['file_name'] == image_filename:
    line = {}
    line['file'] = i['file_name']
    line['image_size'] = [{
     'width': int(i['width']),
     'height': int(i['height']),
     'depth': 3
    }]
    line['annotations'] = []
    line['categories'] = []
    for j in annotations:
     if j['image_id'] == i['id'] and len(j['bbox']) > 0:
      line['annotations'].append({
       'class_id': int(j['category_id']),
       'top': int(j['bbox'][1]),
       'left': int(j['bbox'][0]),
       'width': int(j['bbox'][2]),
       'height': int(j['bbox'][3])
      })
      class_name = ''
      for k in categories:
       if int(j['category_id']) == k['id']:
        class_name = str(k['name'])
      assert class_name is not ''
      line['categories'].append({
       'class_id': int(j['category_id']),
       'name': class_name
      })
    if line['annotations']:
     x0 = line['annotations'][0]['left']
     y0 = line['annotations'][0]['top']
     x1 = int(line['annotations'][0]['left'] + line['annotations'][0]['width'])
     y1 = int(line['annotations'][0]['top'] + line['annotations'][0]['height'])
     gt = [x0, y0, x1, y1]
 return gt

Теперь давайте возьмем каждый Detection кортеж в массиве обнаружения det_array и вычислим его IoU в дополнение к выводу изображения для нашей проверки:

for detection in det_array:
 # load the image
 image_with_path = (os.path.join(directory, detection.image_path))
 image = cv2.imread(image_with_path)
 # draw the ground-truth bounding box and predicted bounding box
 cv2.rectangle(image, tuple(detection.gt[:2]),
  tuple(detection.gt[2:]), (0, 255, 0), 1)
 cv2.rectangle(image, tuple(detection.pred[:2]),
  tuple(detection.pred[2:]), (0, 0, 255), 1)
 # compute the intersection over union and display it
 iou = bb_intersection_over_union(detection.gt, detection.pred)
 mAP_df.loc[detection.image_path, 'IoU'] = iou # add to df for PR-curve calc.
 cv2.putText(image, "IoU: {:.4f}".format(iou), (10, 30),
  cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)
 print("{}: {:.4f}".format(detection.image_path, iou))
 if not cv2.imwrite('./IoU/{}'.format(detection.image_path), image):
  raise Exception("Could not write image")

Вот пример изображения из приведенного выше скрипта:

Но наша работа не завершена. Нам все еще нужно рассчитать кривую Precision-Recall и измерить площадь под этой кривой. Следуя этому примеру Рен Джи Тана, я составил таблицу выводов модели Pandas, упорядоченную по соответствующим оценкам вывода. Я понимаю, что Pandas - слишком большая огневая мощь для этой простой задачи, но я хотел использовать макеты и визуализации таблиц Pandas для этого сообщения:

Давайте вычислим таблицу Precision-Recall, сохранив результат IoU в кадре данных с индексом image_path на каждой итерации в det_array выше:

mAP_df.loc[detection.image_path, 'IoU']

Затем мы можем отсортировать фрейм данных по шкале достоверности:

mAP_df = mAP_df.sort_values(by='Confidences', ascending=False)

и вычислите истинные положительные результаты, ложные срабатывания, точность и значения отзыва для каждой строки в кадре данных:

def calc_TP(row):
 iou = row['IoU']
 if iou >= 0.5:
  result = 1
 else:
  result = 0
 return result
def calc_FP(row):
 iou = row['IoU']
 if iou < 0.5:
  result = 1
 else:
  result = 0
 return result
mAP_df['TP'] = mAP_df.apply(calc_TP, axis=1)
mAP_df['FP'] = mAP_df.apply(calc_FP, axis=1)
mAP_df['Acc TP'] = mAP_df['TP'].cumsum(axis=0)
mAP_df['Acc FP'] = mAP_df['FP'].cumsum(axis=0)
def calc_Acc_Precision(row):
 precision = row['Acc TP'] / (row['Acc TP'] + row['Acc FP'])
 return precision
def calc_Acc_Recall(row):
 recall = row['Acc TP'] / (mAP_df.count()[0])
 return recall
mAP_df['Precision'] = mAP_df.apply(calc_Acc_Precision, axis=1)
mAP_df['Recall'] = mAP_df.apply(calc_Acc_Recall, axis=1)

Эта удобная функция построения графика показывает PR-кривую, которая, по общему признанию, выглядит довольно скудной, учитывая ограниченное количество примеров проверки:

import matplotlib.pyplot as plt
mAP_df.plot(kind='line',x='Recall',y='Precision',color='red')
plt.show()

Осталось только вычислить площадь под кривой. Эта функция выполняет численное интегрирование по серии координат (x, y):

def calc_PR_AUC(x, y):
   sm = 0
   for i in range(1, len(x)):
       h = x[i] - x[i-1]
       sm += h * (y[i-1] + y[i]) / 2
return sm
calc_PR_AUC(mAP_df['Recall'], mAP_df['Precision'])

Итак, какова средняя точность моей модели?

Барабанная дробь, пожалуйста ...

>>> 0.57

Ура…? Без контекста трудно восхищаться этой цифрой. Давайте сравним мою среднюю точность с другими моделями.

Контекстуализация моей средней точности

Конкурс Pascal Visual Object Classes (VOC) Challenge проводится ежегодно с 2006 года. Задача состоит из двух компонентов: (i) общедоступный набор данных изображений, полученных с веб-сайта Flickr (2013 г.), вместе с аннотацией достоверной информации и стандартизированное программное обеспечение для оценки; и (ii) ежегодный конкурс и семинар . (Эверингем М., Эслами, SMA, Ван Гул, Л. и др., Проблема классов визуальных объектов PASCAL: ретроспектива (2015).) Как здесь, одной задачей в этой статье было обнаружение объекта, в котором спрашивается где являются ли экземпляры определенного класса объектов на изображении (если есть)? (Идентификатор)

Глядя на вызов 2012 PASCAL VOC, мы получаем эту таблицу результатов нескольких команд мирового уровня:

Там каждая команда обучила детектор объектов на данных VOC2012, и в таблице указаны оценки AP для каждого класса и представления объекта. «Золотые записи в каждом столбце обозначают максимальную AP для соответствующего класса, а серебряные записи обозначают результаты, занявшие второе место». (Id.) Мы видим, что наивысшее значение AP было 65. В этом документе также показаны лучшие AP, полученные любым методом (макс.), и медианное значение AP по всем методам (медиана):

В этом контексте мой AP 57 выглядит довольно неплохо!

Но если вы похожи на меня, вы думаете,

Подождите, это слишком хорошо, чтобы быть правдой ...

Сравнение моих результатов с проблемой VOC 2012 года сделано из лучших побуждений, но сильно ограничено. Моя модель выглядит лучше, чем она есть, по разным причинам:

  • Моя модель предсказывает только один класс. Требование модели для прогнозирования многих классов, безусловно, снизит мою общую карту.
  • Учитывая мою специфичную для моей области задачу, предполагается, что в любой момент времени есть только «один мяч в игре», поэтому в моих расчетах учитывалось обнаружение наивысшего балла за кадр, что исключает дублирование ограничивающих прямоугольников. Просмотр всех прогнозов ухудшит мою карту.
  • В моей модели использовалась новая архитектура глубокого обучения, ResNet-50 (сверточная нейронная сеть с 50 слоями, занявшая 1-е место в соревнованиях ILSVRC и COCO 2015), что представляет собой значительный скачок в современном состоянии по сравнению с тем, что было раньше. доступно в испытании PASCAL VOC.
  • Моя модель использовала «трансферное обучение», используя более миллиона изображений из базы данных ImageNet для предварительного обучения модели перед передачей ей дополнительных данных, специфичных для моей задачи.
  • Методология PASCAL VOC - не самый современный способ расчета средней точности для задач обнаружения объектов. Джонатан Хуэй хорошо резюмирует это: [l] проверенные исследовательские работы, как правило, дают результаты только для набора данных COCO. . . . Для COCO AP - это среднее значение по нескольким IoU. . . . AP @ [. 5: .95] соответствует среднему AP для IoU от 0,5 до 0,95 с размером шага 0,05 . YOLOv3 (современная нейронная сеть с глубоким обучением с 2018 года) набрала 57,9% MAP на 80 классах, работая со скоростью 30 кадров в секунду на наборе данных COCO test-dev, тогда как моя модель предсказывает один класс на скорости ~ 0,3 кадра в секунду!

Тем не менее, я доволен своим расчетом AP в контексте обучения и калибровки моего понимания этой метрики. Я смиренно добавляю, что приведенные выше сравнения не делают вывод о том, что моя модель хороша или что ее характеристики сравнимы с моделями от исследователей мирового уровня.

Будущие направления

Учитывая среднюю точность модели, я начал мечтать о сборке всей системы:

  • учитывая видео о настольном теннисе,
  • определять, когда мяч находится в игре, вызывая мою модель в каждом кадре
  • сохранить отметку времени, когда мяч находится в игре
  • предоставить пользователю интерфейс веб-интерфейса и
  • позволяют пользователю просматривать выделенную катушку, когда мяч находится в игре.

Но мне было интересно, как моя модель будет распространена на различные матчи по настольному теннису. Взорвет мою модель под разными углами камеры? Повлияют ли на производительность разные цвета столешницы или одежда игроков? Я был приятно удивлен, увидев такие результаты:

Надежды оправдались, я нацарапал эту диаграмму, чтобы указать на следующий шаг в моем путешествии:

Следите за новостями об этом в ближайшее время!

Хотя в приведенном выше сообщении объясняются различные фрагменты кода, лучше иметь полную сущность, чтобы вы могли изучить код самостоятельно: