Понимание и внедрение Faster RCNN с нуля.

Вступление

Faster R-CNN - один из первых фреймворков, который полностью работает с глубоким обучением. Он основан на знании Fast RCNN, который действительно основан на идеях RCNN и SPP-Net. Хотя мы привносим некоторые идеи Fast RCNN при создании инфраструктуры Faster RCNN, мы не будем подробно обсуждать эти структуры. Одна из причин этого заключается в том, что Faster R-CNN работает очень хорошо и не использует традиционные методы компьютерного зрения, такие как выборочный поиск и т. Д. На очень высоком уровне Fast RCNN и Faster RCNN работают, как показано на приведенной ниже блок-схеме. .

Мы уже писали подробный пост в блоге о фреймворках для обнаружения объектов здесь. Это будет служить руководством для тех людей, которые хотели бы понять Faster RCNN путем написания кода самостоятельно.

Единственное отличие, которое вы можете заметить на приведенной выше диаграмме, заключается в том, что Faster RCNN заменил выборочный поиск сетью RPN (сеть предложений региона). Алгоритм выборочного поиска использует дескрипторы SIFT и HOG для генерации предложений объектов, и это занимает 2 секунды на каждое изображение на ЦП. Это дорогостоящий процесс, и Fast RCNN занимает в общей сложности 2,3 секунды для создания прогнозов для одного изображения, тогда как Faster RCNN работает со скоростью 5 FPS (кадров в секунду) даже при использовании очень глубоких классификаторов изображений, таких как VGGnet (ResNet и ResNext теперь также используются. ) в серверной части.

Итак, чтобы построить Faster RCNN с нуля, нам необходимо четко понимать следующие четыре темы:

[Поток]

  1. Сеть предложений региона (RPN)
  2. Функции потерь RPN
  3. Объединение областей интересов (ROI)
  4. Функции потери рентабельности инвестиций

Сеть предложений региона также представила новую концепцию под названием Якорные блоки, которая впоследствии стала золотым стандартом при построении конвейеров обнаружения объектов. Давайте углубимся и посмотрим, как различные этапы конвейеров работают вместе в Faster RCNN.

Обычный поток данных в Faster R-CNN при обучении сети описан ниже.

  1. Особенности извлечения из изображения.
  2. Создание якорных целей.
  3. Прогнозирование местоположения и оценки объектности из сети RPN.
  4. Взять верхние N местоположений и их оценки объектности, также известные как уровень предложения
  5. Передача этих N верхних местоположений через сеть Fast R-CNN и создание прогнозов местоположений и cls для каждого местоположения предлагается в пункте 4.
  6. создание целей предложения для каждого местоположения, предложенного в 4
  7. Использование 2 и 3 для вычисления rpn_cls_loss и rpn_reg_loss.
  8. используя 5 и 6 для вычисления roi_cls_loss и roi_reg_loss.

Мы настроим VGG16 и будем использовать его как серверную часть в этом эксперименте. Обратите внимание, что мы можем использовать любую стандартную классификационную сеть аналогичным образом.

Извлечение функций

Мы начинаем с изображения и набора ограничивающих рамок вместе с его меткой, как определено ниже.

import torch
image = torch.zeros((1, 3, 800, 800)).float()

bbox = torch.FloatTensor([[20, 30, 400, 500], [300, 400, 500, 600]]) # [y1, x1, y2, x2] format
labels = torch.LongTensor([6, 8]) # 0 represents background
sub_sample = 16

Сеть VGG16 используется здесь как модуль извлечения признаков. Он действует как магистраль как для сети RPN, так и для сети Fast_R-CNN. Нам нужно внести несколько изменений в сеть VGG, чтобы это работало. Поскольку вход сети равен 800, выходные данные модуля извлечения признаков должны иметь размер карты признаков (800 // 16). Поэтому нам нужно проверить, где модуль VGG16 достигает этого размера карты функций, и обрезать сеть до der. Сделать это можно следующим образом.

  • Создайте фиктивный образ и установите для volatile значение False
  • Перечислите все слои vgg16
  • Пропустите изображение через слои и подмножество списка, когда output_size изображения (карта характеристик) ниже требуемого уровня (800 // 16)
  • Преобразуйте этот список в последовательный модуль.

Давайте посмотрим, как пройти каждый шаг

  1. Создайте фиктивный образ и установите для volatile значение False.
import torchvision
dummy_img = torch.zeros((1, 3, 800, 800)).float()
print(dummy_img)
#Out: torch.Size([1, 3, 800, 800])

2. Перечислите все слои VGG16.

model = torchvision.models.vgg16(pretrained=True)
fe = list(model.features)
print(fe) # length is 15
# [Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1), ceil_mode=False),
#  Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1), ceil_mode=False),
#  Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1), ceil_mode=False),
#  Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1), ceil_mode=False),
#  Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1)),
#  ReLU(inplace),
#  MaxPool2d(kernel_size=(2, 2), stride=(2, 2), dilation=(1, 1), ceil_mode=False)]

3. Пропустите изображение по слоям и проверьте, откуда у вас получился этот размер.

req_features = []
k = dummy_img.clone()
for i in fe:
    k = i(k)
    if k.size()[2] < 800//16:
        break
    fee.append(i)
    out_channels = k.size()[1]
print(len(req_features)) #30
print(out_channels) # 512

4. Преобразуйте этот список в последовательный модуль.

faster_rcnn_fe_extractor = nn.Sequential(*req_features)

Теперь этот fast_rcnn_fe_extractor можно использовать в качестве нашей серверной части. Давайте вычислим особенности

out_map = faster_rcnn_fe_extractor(image)
print(out_map.size())
#Out: torch.Size([1, 512, 50, 50])

Якорные ящики

Это наша первая встреча с якорными ящиками. Детальное понимание якорных ящиков позволит нам очень легко понять обнаружение объектов. Итак, давайте подробно поговорим о том, как это делается.

  1. Создать привязку в месте на карте объектов
  2. Сгенерируйте привязку во всех местах на карте объектов.
  3. Назначьте метки и расположение объектов (относительно привязки) для каждой привязки.
  4. Создать привязку в месте на карте объектов
  • Мы будем использовать anchor_scales из 8, 16, 32, соотношение 0,5, 1, 2 и субдискретизацию 16 (поскольку мы объединили наше изображение от 800 до 50 пикселей). Теперь каждый пиксель в выходной карте функций отображается на соответствующие 16 * 16 пикселей изображения. Это показано на изображении ниже.

  • Сначала нам нужно сгенерировать якорные блоки поверх этих 16 * 16 пикселей и аналогичным образом проделать то же самое по осям x и y, чтобы получить все якорные блоки. Это делается на шаге 2.
  • В каждом пикселе на карте функций нам нужно сгенерировать 9 якорных ящиков (количество якорных_масштабов и количество соотношений), и у каждого якорного ящика будут «y1», «x1», «y2», «x2». Таким образом, в каждом месте привязка будет иметь форму (9, 4). Начнем с пустого массива, заполненного нулевыми значениями.
import numpy as np
ratio = [0.5, 1, 2]
anchor_scales = [8, 16, 32]

anchor_base = np.zeros((len(ratios) * len(scales), 4), dtype=np.float32)

print(anchor_base)

#Out:
# array([[0., 0., 0., 0.],
#        [0., 0., 0., 0.],
#        [0., 0., 0., 0.],
#        [0., 0., 0., 0.],
#        [0., 0., 0., 0.],
#        [0., 0., 0., 0.],
#        [0., 0., 0., 0.],
#        [0., 0., 0., 0.],
#        [0., 0., 0., 0.]], dtype=float32)

Давайте заполним эти значения соответствующими y1, x1, y2, x2 на каждой шкале привязки и соотношениях. Наш центр для этого базового якоря будет по адресу

ctr_y = sub_sample / 2.
ctr_x = sub_sample / 2.

print(ctr_y, ctr_x)
# Out: (8, 8)
for i in range(len(ratios)):
  for j in range(len(anchor_scales)):
    h = sub_sample * anchor_scales[j] * np.sqrt(ratios[i])
    w = sub_sample * anchor_scales[j] * np.sqrt(1./ ratios[i])

    index = i * len(anchor_scales) + j

    anchor_base[index, 0] = ctr_y - h / 2.
    anchor_base[index, 1] = ctr_x - w / 2.
    anchor_base[index, 2] = ctr_y + h / 2.
    anchor_base[index, 3] = ctr_x + w / 2.

#Out:
# array([[ -37.254833,  -82.50967 ,   53.254833,   98.50967 ],
#        [ -82.50967 , -173.01933 ,   98.50967 ,  189.01933 ],
#        [-173.01933 , -354.03867 ,  189.01933 ,  370.03867 ],
#        [ -56.      ,  -56.      ,   72.      ,   72.      ],
#        [-120.      , -120.      ,  136.      ,  136.      ],
#        [-248.      , -248.      ,  264.      ,  264.      ],
#        [ -82.50967 ,  -37.254833,   98.50967 ,   53.254833],
#        [-173.01933 ,  -82.50967 ,  189.01933 ,   98.50967 ],
#        [-354.03867 , -173.01933 ,  370.03867 ,  189.01933 ]],
#       dtype=float32)

Это точки привязки в первом пикселе карты функций, теперь мы должны сгенерировать эти точки привязки во всех местах карты функций. Также обратите внимание, что отрицательные значения означают, что блоки привязки находятся за пределами измерения изображения. В следующем разделе мы помечаем их -1 и удалим их при вычислении потерь функций и создании предложений для якорных ящиков. Кроме того, поскольку у нас есть 9 якорей в каждом месте и 50 * 50 таких местоположений внутри изображения, мы получим в общей сложности 17500 (50 * 50 * 9) якорей. Давайте теперь сгенерируем другие якоря,

2. Сгенерируйте привязку во всех местах на карте объектов.

Для этого нам нужно сначала сгенерировать центры для каждого пикселя карты функций.

fe_size = (800//16)
ctr_x = np.arange(16, (fe_size+1) * 16, 16)
ctr_y = np.arange(16, (fe_size+1) * 16, 16)

Прохождение через ctr_x и ctr_y даст нам центры в каждом месте. Код sudo приведен ниже

For x in shift_x:
  For y in shift_y:
    Generate anchors at (x, y) locations

То же самое можно увидеть визуально ниже.

Давайте сгенерируем эти центры с помощью python

index = 0
for x in range(len(ctr_x)):
    for y in range(len(ctr_y)):
        ctr[index, 1] = ctr_x[x] - 8
        ctr[index, 0] = ctr_y[y] - 8
        index +=1
  • Результатом будет значение (x, y) в каждом месте, как показано на изображении выше. Вместе у нас есть 2500 якорных центров. Теперь в каждом центре нам нужно создать якорные блоки. Это можно сделать с помощью кода, который мы использовали для создания привязки в одном месте, добавив отрывка для цикла для задания центров каждой привязки. Посмотрим, как это делается
anchors = np.zeros((fe_size * fe_size * 9), 4)
index = 0
for c in ctr:
  ctr_y, ctr_x = c
  for i in range(len(ratios)):
    for j in range(len(anchor_scales)):
      h = sub_sample * anchor_scales[j] * np.sqrt(ratios[i])
      w = sub_sample * anchor_scales[j] * np.sqrt(1./ ratios[i])
      anchors[index, 0] = ctr_y - h / 2.
      anchors[index, 1] = ctr_x - w / 2.
      anchors[index, 2] = ctr_y + h / 2.
      anchors[index, 3] = ctr_x + w / 2.
      index += 1
print(anchors.shape)
#Out: [22500, 4]

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

Это будут последние привязки изображения, которые мы будем использовать в дальнейшем. Давайте наглядно увидим, как эти якоря раскладываются на изображении.

  1. Назначьте метки и расположение объектов (относительно привязки) для каждой привязки.

Теперь, когда мы сгенерировали все якорные блоки, нам нужно посмотреть на объекты внутри изображения и назначить их конкретным якорным блокам, которые их содержат. В Faster_R-CNN есть несколько рекомендаций по назначению меток якорным блокам.

Мы присваиваем положительную метку двум типам якорей: а) Якорь / якоря с наивысшим пересечением по объединению (IoU) перекрываются с блоком истинности земли или b) Якорь, у которого IoU перекрытие с землей превышает 0,7. - ящик правды.

Обратите внимание, что один объект наземной истины может назначать положительные метки нескольким привязкам.

c) Мы присваиваем отрицательную метку неположительному якорю, если его отношение IoU ниже 0,3 для всех блоков наземной истины. г) Якоря, которые не являются ни положительными, ни отрицательными, не способствуют достижению цели обучения.

Посмотрим, как это делается.

bbox = np.asarray([[20, 30, 400, 500], [300, 400, 500, 600]], dtype=np.float32) # [y1, x1, y2, x2] format
labels = np.asarray([6, 8], dtype=np.int8) # 0 represents background

Мы назначим метки и места для якорных ящиков следующими способами.

  • Найдите индексы допустимых якорных ящиков и создайте массив с этими индексами. создать массив меток с массивом индексов формы, заполненным -1.
  • проверьте погоду, удовлетворяет ли одно из вышеуказанных условий a, b, c, и заполните этикетку соответствующим образом. В случае положительного якорного поля (метка 1), обратите внимание, какой объект наземной истины привел к этому.
  • вычислить местоположения (loc) наземной истины, связанной с блоком привязки по отношению к блоку привязки.
  • Реорганизуйте все якорные блоки, заполнив -1 для всех недействительных якорных ящиков и значений, которые мы вычислили для всех допустимых якорных ящиков.
  • Выходы должны быть метками с массивом (N, 1) и местами с массивом (N, 4).
  • Найдите индекс всех допустимых якорных ящиков
index_inside = np.where(
        (anchors[:, 0] >= 0) &
        (anchors[:, 1] >= 0) &
        (anchors[:, 2] <= 800) &
        (anchors[:, 3] <= 800)
    )[0]
print(index_inside.shape)
#Out: (8940,)
  • создать пустой массив меток с формой inside_index и заполнить -1. По умолчанию установлено значение (d)
label = np.empty((len(inside_index), ), dtype=np.int32)
 label.fill(-1)
 print(label.shape)
#Out = (8940, )
  • создать массив с допустимыми якорными блоками
valid_anchor_boxes = anchors[inside_index]
print(valid_anchor_boxes.shape)
#Out = (8940, 4)
  • Для каждого допустимого якорного блока вычислите iou с каждым наземным объектом истинности. Поскольку у нас есть 8940 якорных ящиков и 2 наземных объекта истинности, мы должны получить массив с (8490, 2) в качестве выходных данных. Код sudo для вычисления iou между двумя полями будет
- Find the max of x1 and y1 in both the boxes (xn1, yn1)
- Find the min of x2 and y2 in both the boxes (xn2, yn2)
- Now both the boxes are intersecting only
 if (xn1 < xn2) and (yn2 < yn1)
      - iou_area will be (xn2 - xn1) * (yn2 - yn1)
 else
      - iuo_area will be 0
- similarly calculate area for anchor box and ground truth object
- iou = iou_area/(anchor_box_area + ground_truth_area - iou_area)

Код python для вычисления ious выглядит следующим образом

ious = np.empty((len(valid_anchors), 2), dtype=np.float32)
ious.fill(0)
print(bbox)
for num1, i in enumerate(valid_anchors):
    ya1, xa1, ya2, xa2 = i  
    anchor_area = (ya2 - ya1) * (xa2 - xa1)
    for num2, j in enumerate(bbox):
        yb1, xb1, yb2, xb2 = j
        box_area = (yb2- yb1) * (xb2 - xb1)
        inter_x1 = max([xb1, xa1])
        inter_y1 = max([yb1, ya1])
        inter_x2 = min([xb2, xa2])
        inter_y2 = min([yb2, ya2])
        if (inter_x1 < inter_x2) and (inter_y1 < inter_y2):
            iter_area = (inter_y2 - inter_y1) * \
(inter_x2 - inter_x1)
            iou = iter_area / \
(anchor_area+ box_area - iter_area)            
        else:
            iou = 0.
        ious[num1, num2] = iou
print(ious.shape)
#Out: [22500, 2]

Примечание. Используя массивы numpy, эти вычисления можно выполнять гораздо эффективнее и с меньшими подробностями. Однако я стараюсь оставаться здесь таким образом, чтобы люди без сильной алгебры также могли понять.

Рассматривая сценарии a и b, нам нужно найти здесь две вещи

  • наивысший iou для каждого gt_box и соответствующего ему якоря
  • наивысшее значение iou для каждого якорного ящика и соответствующего ему наземного ящика истинности

case-1

gt_argmax_ious = ious.argmax(axis=0)
print(gt_argmax_ious)
gt_max_ious = ious[gt_argmax_ious, np.arange(ious.shape[1])]
print(gt_max_ious)
# Out:
# [2262 5620]
# [0.68130493 0.61035156]

case-2

argmax_ious = ious.argmax(axis=1)
print(argmax_ious.shape)
print(argmax_ious)
max_ious = ious[np.arange(len(inside_index)), argmax_ious]
print(max_ious)
# Out:
# (22500,)
# [0, 1, 0, ..., 1, 0, 0]
# [0.06811669 0.07083762 0.07083762 ... 0.         0.         0.        ]

Найдите якорные_боксы с этим max_ious (gt_max_ious)

gt_argmax_ious = np.where(ious == gt_max_ious)[0]
print(gt_argmax_ious)
# Out:
# [2262, 2508, 5620, 5628, 5636, 5644, 5866, 5874, 5882, 5890, 6112,
#        6120, 6128, 6136, 6358, 6366, 6374, 6382]

Теперь у нас есть три массива

  • argmax_ious - сообщает, какой объект наземной истины имеет максимальное значение iou с каждым якорем.
  • max_ious - сообщает max_iou с помощью наземного объекта истинности для каждого якоря.
  • gt_argmax_ious - сообщает якорям с наивысшим перекрытием Intersection-over-Union (IoU) с блоком наземной истины.

Используя argmax_ious и max_ious, мы можем назначать метки и местоположения якорным блокам, которые удовлетворяют [b] и [c]. Используя gt_argmax_ious, мы можем назначать метки и местоположения якорным блокам, которые удовлетворяют [a].

Давайте установим пороговые значения для некоторых переменных

pos_iou_threshold  = 0.7
neg_iou_threshold = 0.3
  • Присвойте отрицательную метку (0) всем блокам привязки, у которых max_iou меньше отрицательного порога [c]
label[max_ious < neg_iou_threshold] = 0
  • Присвойте положительную метку (1) всем блокам привязки, которые имеют наибольшее перекрытие IoU с блоком наземной истины [a]
label[gt_argmax_ious] = 1
  • Присвойте положительную метку (1) всем блокам привязки, у которых max_iou больше положительного порога [b]
label[max_ious >= pos_iou_threshold] = 1
  • Обучающий RPN В бумажном тексте Faster_R-CNN используются следующие фразы Each mini-batch arises from a single image that contains many positive and negitive example anchors, but this will bias towards negitive samples as they are dominate. Instead, we randomly sample 256 anchors in an image to compute the loss function of a mini-batch, where the sampled positive and negative anchors have a ratio of up to 1:1. If there are fewer than 128 positive samples in an image, we pad the mini-batch with negitive ones.. Отсюда мы можем получить две переменные следующим образом
pos_ratio = 0.5
n_sample = 256

Всего положительных образцов

n_pos = pos_ratio * n_sample

Теперь нам нужно случайным образом выбрать n_pos образцов из положительных меток и игнорировать (-1) оставшиеся. В некоторых случаях мы получаем меньше, чем n_pos выборок, в этом случае мы произвольно выбираем (n_sample - n_pos) отрицательных выборок (0) и назначаем метку игнорирования оставшимся якорным блокам. Это делается с помощью следующего кода.

  • положительные образцы
pos_index = np.where(label == 1)[0]
if len(pos_index) > n_pos:
    disable_index = np.random.choice(pos_index, size=(len(pos_index) - n_pos), replace=False)
    label[disable_index] = -1
  • отрицательные образцы
n_neg = n_sample * np.sum(label == 1)
neg_index = np.where(label == 0)[0]
if len(neg_index) > n_neg:
    disable_index = np.random.choice(neg_index, size=(len(neg_index) - n_neg), replace = False)
    label[disable_index] = -1

Присвоение местоположений якорным блокам
Теперь давайте назначим местоположения каждому якорному блоку с помощью наземного объекта истинности, имеющего максимальное значение iou. Обратите внимание: мы назначим точки привязки всем допустимым блокам привязки, независимо от их метки, позже, когда мы будем вычислять потери, мы сможем удалить их с помощью простых фильтров.

Мы уже знаем, какой объект наземной истины имеет высокое значение iou с каждым якорем привязки. Теперь нам нужно найти местоположения наземной истины по отношению к местоположению якорного ящика. Faster_R-CNN использует для этого следующую параметризацию

t_{x} = (x - x_{a})/w_{a}
t_{y} = (y - y_{a})/h_{a}
t_{w} = log(w/ w_a)
t_{h} = log(h/ h_a)

x, y, w, h - координаты центра основного окна правды, ширина и высота. x_a, y_a, h_a и w_a, а также центральные координаты, ширина и высота якорных боксов.

  • Для каждого якорного ящика найдите объект Groundtruth, у которого есть max_iou
max_iou_bbox = bbox[argmax_ious]
print(max_iou_bbox)
#Out
# [[ 20.,  30., 400., 500.],
#  [ 20.,  30., 400., 500.],
#  [ 20.,  30., 400., 500.],
#  ...,
#  [ 20.,  30., 400., 500.],
#  [ 20.,  30., 400., 500.],
#  [ 20.,  30., 400., 500.]]
  • Чтобы найти t_ {x}, t_ {y}, t_ {w}, t_ {h}, нам нужно преобразовать формат y1, x1, y2, x2 допустимых якорных ящиков и связанных наземных ящиков истинности с max iou в ctr_y , ctr_x, h, w формат.
height = valid_anchors[:, 2] - valid_anchors[:, 0]
width = valid_anchors[:, 3] - valid_anchors[:, 1]
ctr_y = valid_anchors[:, 0] + 0.5 * height
ctr_x = valid_anchors[:, 1] + 0.5 * width
base_height = max_iou_bbox[:, 2] - max_iou_bbox[:, 0]
base_width = max_iou_bbox[:, 3] - max_iou_bbox[:, 1]
base_ctr_y = max_iou_bbox[:, 0] + 0.5 * base_height
base_ctr_x = max_iou_bbox[:, 1] + 0.5 * base_width
  • Используйте приведенные выше формулы, чтобы найти loc
eps = np.finfo(height.dtype).eps
height = np.maximum(height, eps)
width = np.maximum(width, eps)
dy = (base_ctr_y - ctr_y) / height
dx = (base_ctr_x - ctr_x) / width
dh = np.log(base_height / height)
dw = np.log(base_width / width)
anchor_locs = np.vstack((dy, dx, dh, dw)).transpose()
print(anchor_locs)
#Out:
# [[ 0.5855727   2.3091455   0.7415673   1.647276  ]
#  [ 0.49718437  2.3091455   0.7415673   1.647276  ]
#  [ 0.40879607  2.3091455   0.7415673   1.647276  ]
#  ...
#  [-2.50802    -5.292254    0.7415677   1.6472763 ]
#  [-2.5964084  -5.292254    0.7415677   1.6472763 ]
#  [-2.6847968  -5.292254    0.7415677   1.6472763 ]]
  • Теперь у нас есть anchor_locs и метка, связанная с каждым допустимым якорным блоком.

Давайте сопоставим их с исходными якорями, используя переменную inside_index. Заполните недопустимые метки якорных ящиков значением -1 (игнорировать), а местоположения - 0.

  • Окончательные метки:
anchor_labels = np.empty((len(anchors),), dtype=label.dtype)
anchor_labels.fill(-1)
anchor_labels[inside_index] = label
  • Конечные локации
anchor_locations = np.empty((len(anchors),) + anchors.shape[1:], dtype=anchor_locs.dtype)
anchor_locations.fill(0)
anchor_locations[inside_index, :] = anchor_locs

Последние две матрицы:

  • anchor_locations [N, 4] - [22500, 4]
  • anchor_labels [N,] - [22500]

Они используются как цели для сети RPN. В следующем разделе мы увидим, как устроена эта сеть RPN.

Сеть предложений региона.

Как мы уже обсуждали ранее, до этой работы предложения регионов для сети создавались с использованием выборочного поиска, CPMC, MCG, Edgeboxes и т. Д. Faster_R-CNN - первая работа, демонстрирующая создание предложений регионов с использованием глубокого обучения.

Сеть содержит модуль свертки, поверх которого будет один регрессионный слой, который предсказывает расположение бокса внутри якоря.

Чтобы создать предложения по регионам, мы перемещаем небольшую сеть по выходным данным сверточной карты признаков, которые мы получили в модуле извлечения признаков. Эта небольшая сеть принимает в качестве входных данных пространственное окно размером n x n входной сверточной карты признаков. Каждое скользящее окно отображается на более низкоразмерный объект [512 объектов]. Эта функция передается в два одноуровневых полностью связанных слоя.

  • Слой регрессии бокса
  • Слой классификации ящиков

мы используем n = 3, как указано в статье Faster_R-CNN. Мы можем реализовать эту архитектуру, используя сверточный слой n x n, за которым следуют два одноуровневых сверточных слоя 1 x 1.

import torch.nn as nn
mid_channels = 512
in_channels = 512 # depends on the output feature map. in vgg 16 it is equal to 512
n_anchor = 9 # Number of anchors at each location
conv1 = nn.Conv2d(in_channels, mid_channels, 3, 1, 1)
reg_layer = nn.Conv2d(mid_channels, n_anchor *4, 1, 1, 0)
cls_layer = nn.Conv2d(mid_channels, n_anchor *2, 1, 1, 0) ## I will be going to use softmax here. you can equally use sigmoid if u replace 2 with 1.

В документе говорится, что они инициализировали эти слои с нулевым средним значением и стандартным отклонением 0,01 для весов и нулями для базы. Давайте сделаем это

# conv sliding layer
conv1.weight.data.normal_(0, 0.01)
conv1.bias.data.zero_()
# Regression layer
reg_layer.weight.data.normal_(0, 0.01)
reg_layer.bias.data.zero_()
# classification layer
cls_layer.weight.data.normal_(0, 0.01)
cls_layer.bias.data.zero_()

Теперь выходные данные, которые мы получили в состоянии извлечения признаков, должны быть отправлены в эту сеть для прогнозирования местоположения объектов относительно привязки и связанной с ней оценки объектности.

x = conv1(out_map) # out_map is obtained in section 1
pred_anchor_locs = reg_layer(x)
pred_cls_scores = cls_layer(x)
print(pred_cls_scores.shape, pred_anchor_locs.shape)
#Out:
#torch.Size([1, 18, 50, 50]) torch.Size([1, 36, 50, 50])

Давайте немного переформатируем их и приведем в соответствие с нашими целями привязки, которые мы разработали ранее. Мы также найдем оценки объектности для каждого якорного блока, поскольку это используется для уровня предложения, который мы обсудим в следующем разделе.

pred_anchor_locs = pred_anchor_locs.permute(0, 2, 3, 1).contiguous().view(1, -1, 4)
print(pred_anchor_locs.shape)
#Out: torch.Size([1, 22500, 4])
pred_cls_scores = pred_cls_scores.permute(0, 2, 3, 1).contiguous()
print(pred_cls_scores)
#Out torch.Size([1, 50, 50, 18])

objectness_score = pred_cls_scores.view(1, 50, 50, 9, 2)[:, :, :, :, 1].contiguous().view(1, -1)
print(objectness_score.shape)
#Out torch.Size([1, 22500])
pred_cls_scores  = pred_cls_scores.view(1, -1, 2)
print(pred_cls_scores.shape)
# Out torch.size([1, 22500, 2])

мы закончили с разделом

  • pred_cls_scores и pred_anchor_locs - это выходные данные сети RPN и потери для обновления весов
  • pred_cls_scores и objectness_scores используются в качестве входных данных для уровня предложения, который генерирует набор предложений, которые в дальнейшем используются сетью RoI. Мы увидим это в следующем разделе.

Генерация предложений по питанию сети Fast R-CNN

Функция предложения будет принимать следующие параметры

  • Weather training_mode или режим тестирования
  • nms_thresh
  • n_train_pre_nms - количество bbox-ов перед nms во время тренировки
  • n_train_post_nms - количество bbox-ов после nms во время обучения
  • n_test_pre_nms - количество bbox до nms во время тестирования
  • n_test_post_nms - количество bbox-ов после nms во время тестирования
  • min_size - минимальная высота объекта, необходимая для создания предложения.

Как сообщает Faster R_CNN, предложения RPN сильно пересекаются друг с другом. Чтобы уменьшить избыточность, мы применяем не максимальное подавление (NMS) для регионов предложения на основе их оценок cls. Мы фиксируем порог IoU для NMS на 0,7, что оставляет нам около 2000 регионов предложения на изображение. После исследования абляции авторы показывают, что NMS не вредит предельной точности обнаружения, но существенно сокращает количество предложений. После NMS мы используем для обнаружения первые N ранжированных регионов предложения. Далее мы обучаем Fast R-CNN, используя 2000 предложений RPN. Во время тестирования они оценивают только 300 предложений, они протестировали это с разными числами и получили это.

nms_thresh = 0.7
n_train_pre_nms = 12000
n_train_post_nms = 2000
n_test_pre_nms = 6000
n_test_post_nms = 300
min_size = 16

Нам нужно сделать следующие вещи, чтобы создать в сети предложения по региону интересов.

  1. преобразовать предсказания местоположения из сети rpn в формат bbox [y1, x1, y2, x2].
  2. закрепить предсказанные блоки на изображении
  3. Удалите предсказанные блоки с высотой или шириной ‹порог (min_size).
  4. Отсортируйте все пары (предложение, оценка) по количеству баллов от наивысшего к наименьшему.
  5. Возьмите верхний pre_nms_topN (например, 12000 при обучении и 300 при тестировании).
  6. Применить порог НМС ›0,7
  7. Возьмите верхний pos_nms_topN (например, 2000 при обучении и 300 при тестировании)

Мы рассмотрим каждый из этапов в оставшейся части этого раздела.

  1. преобразовать предсказания местоположения из сети rpn в формат bbox [y1, x1, y2, x2].

Это операции, обратные тому, что мы сделали при назначении наземной истины якорным блокам. Эта операция декодирует предсказания, не параметризуя их и смещая их к изображению. формулы следующие

x = (w_{a} * ctr_x_{p}) + ctr_x_{a}
y = (h_{a} * ctr_x_{p}) + ctr_x_{a}
h = np.exp(h_{p}) * h_{a}
w = np.exp(w_{p}) * w_{a}
and later convert to y1, x1, y2, x2 format
  • Преобразование формата якорей из y1, x1, y2, x2 в ctr_x, ctr_y, h, w
anc_height = anchors[:, 2] - anchors[:, 0]
anc_width = anchors[:, 3] - anchors[:, 1]
anc_ctr_y = anchors[:, 0] + 0.5 * anc_height
anc_ctr_x = anchors[:, 1] + 0.5 * anc_width
  • Преобразуйте местоположения прогнозов, используя приведенные выше формулы. перед этим преобразуйте pred_anchor_locs и objectness_score в массив numpy
pred_anchor_locs_numpy = pred_anchor_locs[0].data.numpy()
objectness_score_numpy = objectness_score[0].data.numpy()
dy = pred_anchor_locs_numpy[:, 0::4]
dx = pred_anchor_locs_numpy[:, 1::4]
dh = pred_anchor_locs_numpy[: 2::4]
dw = pred_anchor_locs_numpy[: 3::4]
ctr_y = dy * anc_height[:, np.newaxis] + anc_ctr_y[:, np.newaxis]
ctr_x = dx * anc_width[:, np.newaxis] + anc_ctr_x[:, np.newaxis]
h = np.exp(dh) * anc_height[:, np.newaxis]
w = np.exp(dw) * anc_width[:, np.newaxis]
  • преобразовать [ctr_x, ctr_y, h, w] в формат [y1, x1, y2, x2]
roi = np.zeros(pred_anchor_locs_numpy.shape, dtype=loc.dtype)
roi[:, 0::4] = ctr_y - 0.5 * h
roi[:, 1::4] = ctr_x - 0.5 * w
roi[:, 2::4] = ctr_y + 0.5 * h
roi[:, 3::4] = ctr_x + 0.5 * w
#Out:
# [[ -36.897102,  -80.29519 ,   54.09939 ,  100.40507 ],
#  [ -83.12463 , -165.74298 ,   98.67854 ,  188.6116  ],
#  [-170.7821  , -378.22214 ,  196.20844 ,  349.81198 ],
#  ...,
#  [ 696.17816 ,  747.13306 ,  883.4582  ,  836.77747 ],
#  [ 621.42114 ,  703.0614  ,  973.04626 ,  885.31226 ],
#  [ 432.86267 ,  622.48926 , 1146.7059  ,  982.9209  ]]
  • закрепить предсказанные блоки на изображении
img_size = (800, 800) #Image size
roi[:, slice(0, 4, 2)] = np.clip(
            roi[:, slice(0, 4, 2)], 0, img_size[0])
roi[:, slice(1, 4, 2)] = np.clip(
    roi[:, slice(1, 4, 2)], 0, img_size[1])
print(roi)
#Out:
# [[  0.     ,   0.     ,  54.09939, 100.40507],
#  [  0.     ,   0.     ,  98.67854, 188.6116 ],
#  [  0.     ,   0.     , 196.20844, 349.81198],
#  ...,
#  [696.17816, 747.13306, 800.     , 800.     ],
#  [621.42114, 703.0614 , 800.     , 800.     ],
#  [432.86267, 622.48926, 800.     , 800.     ]]
  • Удалите предсказанные блоки с порогом высоты или ширины ‹.
hs = roi[:, 2] - roi[:, 0]
ws = roi[:, 3] - roi[:, 1]
keep = np.where((hs >= min_size) & (ws >= min_size))[0]
roi = roi[keep, :]
score = objectness_score_numpy[keep]
print(score.shape)
#Out:
##(22500, ) all the boxes have minimum size of 16
  • Отсортируйте все пары (предложение, оценка) по количеству баллов от наивысшего к наименьшему.
order = score.ravel().argsort()[::-1]
print(order)
#Out:
#[ 889,  929, 1316, ...,  462,  454,    4]
  • Возьмите верхний pre_nms_topN (например, 12000 при обучении и 300 при тестировании)
order = order[:n_train_pre_nms]
roi = roi[order, :]
print(roi.shape)
print(roi)
#Out
# (12000, 4)
# [[607.93866,   0.     , 800.     , 113.38187],
#  [  0.     ,   0.     , 235.29704, 369.64795],
#  [572.177  ,   0.     , 800.     , 373.0086 ],
#  ...,
#  [250.07968, 186.61633, 434.6356 , 276.70615],
#  [490.07974, 154.6163 , 674.6356 , 244.70615],
#  [266.07968, 602.61633, 450.6356 , 692.7062 ]]
  • Применить не максимальный порог подавления ›0,7 Первый вопрос: что такое не максимальное подавление? Это процесс, в котором мы удаляем / объединяем очень сильно перекрывающиеся ограничительные рамки. Если мы посмотрим на диаграмму ниже, то увидим много перекрывающихся ограничивающих рамок, и нам нужно несколько ограничивающих рамок, которые уникальны и не сильно перекрываются. Мы держим порог 0,7. Порог определяет минимальную площадь перекрытия, необходимую для объединения / удаления перекрывающихся ограничивающих рамок

Код sudo для NMS работает следующим образом

- Take all the roi boxes [roi_array]
- Find the areas of all the boxes [roi_area]
- Take the indexes of order the probability score in descending order [order_array]
keep = []
while order_array.size > 0:
  - take the first element in order_array and append that to keep  
  - Find the area with all other boxes
  - Find the index of all the boxes which have high overlap with this box
  - Remove them from order array
  - Iterate this till we get the order_size to zero (while loop)
- Ouput the keep variable which tells what indexes to consider.
  • Возьмите верхний pos_nms_topN (например, 2000 при обучении и 300 при тестировании)
y1 = roi[:, 0]
x1 = roi[:, 1]
y2 = roi[:, 2]
x2 = roi[:, 3]
area = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0
    i = order[0]
    xx1 = np.maximum(x1[i], x1[order[1:]])
    yy1 = np.maximum(y1[i], y1[order[1:]])
    xx2 = np.minimum(x2[i], x2[order[1:]])
    yy2 = np.minimum(y2[i], y2[order[1:]])
    w = np.maximum(0.0, xx2 - xx1 + 1)
    h = np.maximum(0.0, yy2 - yy1 + 1)
    inter = w * h
    ovr = inter / (areas[i] + areas[order[1:]] - inter)
    inds = np.where(ovr <= thresh)[0]
    order = order[inds + 1]
keep = keep[:n_train_post_nms] # while training/testing , use accordingly
roi = roi[keep] # the final region proposals

Были получены окончательные предложения по регионам. Это используется в качестве входных данных для объекта Fast_R-CNN, который, наконец, пытается предсказать местоположения объекта (относительно предложенного блока) и класс объекта (классификация каждого предложения). Сначала мы рассмотрим, как создать цели для этих предложений по обучению этой сети. После этого мы рассмотрим, как реализована эта быстрая сеть r-cnn, и передадим эти предложения в сеть для получения прогнозируемых результатов. Затем мы определим потери. Мы рассчитаем как потери rpn, так и быстрые потери r-cnn.

Цели предложения

Сеть Fast R-CNN принимает региональные предложения (полученные из уровня предложений в предыдущем разделе), наземные блоки истинности и их соответствующие метки в качестве входных данных. Потребуются следующие параметры

  • n_sample: количество выборок для выборки из roi, значение по умолчанию - 128.
  • pos_ratio: количество положительных примеров из n_samples. Значения по умолчанию - 0,25.
  • pos_iou_thesh: минимальное перекрытие предложения региона с любым объектом наземной истины, чтобы рассматривать его как положительную метку.
  • [neg_iou_threshold_lo, neg_iou_threshold_hi]: [0,0, 0,5], ограничение значения перекрытия, необходимое для рассмотрения предложения региона как отрицательного [фоновый объект].
n_sample = 128
pos_ratio = 0.25
pos_iou_thresh = 0.5
neg_iou_thresh_hi = 0.5
neg_iou_thresh_lo = 0.0

Используя эти параметры, давайте посмотрим, как создаются цели предложения. Сначала давайте напишем код sudo.

- For each roi, find the IoU with all other ground truth object [N, n]
    - where N is the number of region proposal boxes
    - n is the number of ground truth boxes
- Find which ground truth object has highest iou with the roi [N], these are the labels for each and every region proposal
- If the highest IoU is greater than pos_iou_thesh[0.5], then we assign the label.
- pos_samples:
      - We randomly samply [n_sample x pos_ratio] region proposals and consider these only as positive labels
- If the IoU is between [0.1, 0.5], we assign a negitive label[0] to the region proposal
- neg_samples:
      - We randomly sample [128- number of pos region proposals on this image] and assign 0 to these region proposals
- We collect the pos_samples and neg_samples  and remove all other region proposals
- convert the locations of groundtruth objects for each region proposal to the required format (Described in Fast R-CNN)
- Ouput labels and locations for the sampled_rois

Теперь посмотрим, как это делается с помощью Python.

  • Найдите iou каждого наземного объекта истинности с предложениями по региону. Мы будем использовать тот же код, который мы использовали в якорных блоках, для вычисления IOS.
ious = np.empty((len(roi), 2), dtype=np.float32)
ious.fill(0)
for num1, i in enumerate(roi):
    ya1, xa1, ya2, xa2 = i  
    anchor_area = (ya2 - ya1) * (xa2 - xa1)
    for num2, j in enumerate(bbox):
        yb1, xb1, yb2, xb2 = j
        box_area = (yb2- yb1) * (xb2 - xb1)
        inter_x1 = max([xb1, xa1])
        inter_y1 = max([yb1, ya1])
        inter_x2 = min([xb2, xa2])
        inter_y2 = min([yb2, ya2])
        if (inter_x1 < inter_x2) and (inter_y1 < inter_y2):
            iter_area = (inter_y2 - inter_y1) * \
(inter_x2 - inter_x1)
            iou = iter_area / (anchor_area+ \
box_area - iter_area)            
        else:
            iou = 0.
        ious[num1, num2] = iou
print(ious.shape)
#Out:
#[1535, 2]
  • Выясните, у какой основной истины высокий IoU для каждого предложения по региону, а также найдите максимальное IoU
gt_assignment = iou.argmax(axis=1)
max_iou = iou.max(axis=1)
print(gt_assignment)
print(max_iou)
#Out:
# [0, 0, 0 ... 1, 1, 0]
# [0.016, 0., 0. ... 0.08034518, 0.10739268, 0.]
  • Присвойте ярлыки каждому предложению
gt_roi_label = labels[gt_assignment]
print(gt_roi_label)
#Out:
#[6, 6, 6, ..., 8, 8, 6]

Примечание: если вы не приняли фоновый объект за 0, добавьте +1 ко всем меткам.

  • Выберите цвет переднего плана согласно pos_iou_thesh. Нам также нужно только n_sample x pos_ratio (128 x 0,25 = 32) сэмплов переднего плана. Так что, если мы получим менее 32 положительных образцов, мы оставим все как есть. Если мы получим более 32 образцов переднего плана, мы возьмем 32 образца из положительных образцов. Это делается с помощью следующего кода.
pos_index = np.where(max_iou >= pos_iou_thresh)[0]
pos_roi_per_this_image = int(min(pos_roi_per_image, pos_index.size))
if pos_index.size > 0:
    pos_index = np.random.choice(
        pos_index, size=pos_roi_per_this_image, replace=False)
print(pos_roi_per_this_image)
print(pos_index)
#Out
# 18
# [ 257  296  317 1075 1077 1169 1213 1258 1322 1325 1351 1378 1380 1425
#  1472 1482 1489 1495]
  • То же самое мы делаем и для отрицательных (фоновых) предложений региона. Если у нас есть предложения регионов с IoU между neg_iou_thresh_lo и neg_iou_thresh_hi для основного объекта истинности, назначенного ему ранее, мы присваиваем метку 0 предложению региона. Мы выберем n (n_sample-pos_samples, 128–32 = 96) предложений регионов из этих отрицательных образцов.
neg_index = np.where((max_iou < neg_iou_thresh_hi) &
                             (max_iou >= neg_iou_thresh_lo))[0]
neg_roi_per_this_image = n_sample - pos_roi_per_this_image
neg_roi_per_this_image = int(min(neg_roi_per_this_image,
                                 neg_index.size))
if  neg_index.size > 0 :
    neg_index = np.random.choice(
        neg_index, size=neg_roi_per_this_image, replace=False)
print(neg_roi_per_this_image)
print(neg_index)
#Out:
#110
# [  79  688  160  ...  376  712 1235  148 1001]
  • Теперь мы собираем индекс положительных образцов и индекс отрицательных образцов, их соответствующие метки и предложения по регионам.
keep_index = np.append(pos_index, neg_index)
gt_roi_labels = gt_roi_label[keep_index]
gt_roi_labels[pos_roi_per_this_image:] = 0  # negative labels --> 0
sample_roi = roi[keep_index]
print(sample_roi.shape)
#Out:
#(128, 4)
  • Выберите наземные объекты для этих sample_roi, а затем параметризуйте, как мы это делали при назначении местоположений якорным блокам в разделе 2.
bbox_for_sampled_roi = bbox[gt_assignment[keep_index]]
print(bbox_for_sampled_roi.shape)
#Out
#(128, 4)
height = sample_roi[:, 2] - sample_roi[:, 0]
width = sample_roi[:, 3] - sample_roi[:, 1]
ctr_y = sample_roi[:, 0] + 0.5 * height
ctr_x = sample_roi[:, 1] + 0.5 * width
base_height = bbox_for_sampled_roi[:, 2] - bbox_for_sampled_roi[:, 0]
base_width = bbox_for_sampled_roi[:, 3] - bbox_for_sampled_roi[:, 1]
base_ctr_y = bbox_for_sampled_roi[:, 0] + 0.5 * base_height
base_ctr_x = bbox_for_sampled_roi[:, 1] + 0.5 * base_width
  • Мы будем использовать следующую формулировку
t_{x} = (x - x_{a})/w_{a}
t_{y} = (y - y_{a})/h_{a}
t_{w} = log(w/ w_a)
t_{h} = log(h/ h_a)
eps = np.finfo(height.dtype).eps
height = np.maximum(height, eps)
width = np.maximum(width, eps)
dy = (base_ctr_y - ctr_y) / height
dx = (base_ctr_x - ctr_x) / width
dh = np.log(base_height / height)
dw = np.log(base_width / width)
gt_roi_locs = np.vstack((dy, dx, dh, dw)).transpose()
print(gt_roi_locs)
#Out:
# [[-0.08075945, -0.14638858, -0.23822695, -0.23150307],
#  [ 0.04865225,  0.15570255,  0.08902431, -0.5969549 ],
#  [ 0.17411101,  0.2244332 ,  0.19870323,  0.25063717],
#  .....
#  [-0.13976236,  0.121031  ,  0.03863466,  0.09662855],
#  [-0.59361845, -2.5121436 ,  0.04558792,  0.9731178 ],
#  [ 0.1041566 , -0.7840459 ,  1.4283055 ,  0.95092565]]

Итак, теперь у нас есть gt_roi_locs и gt_roi_labels для выбранных значений rois. Теперь нам нужно спроектировать сеть Fast rcnn и спрогнозировать местоположения и метки, что мы и сделаем в следующем разделе.

Быстрый R-CNN

Fast R-CNN использовал объединение ROI для извлечения функций для каждого предложения, предложенного с помощью выборочного поиска (Fast RCNN) или сети региональных предложений (RPN в Faster R-CNN). Мы увидим, как работает этот пул ROI, и позже передадим предложения rpn, которые мы вычислили в разделе 4, на этот уровень. Далее мы увидим, как этот слой связан со слоем классификации и регрессии для вычисления вероятностей классов и координат ограничивающих прямоугольников соответственно.

Цель объединения областей интересов (также известная как объединение RoI) - выполнить максимальное объединение входных данных неоднородных размеров для получения карт функций фиксированного размера (например, 7 × 7). Этот слой принимает два входа

  • Карта признаков фиксированного размера, полученная из глубокой сверточной сети с несколькими свертками и слоями максимального объединения.
  • Матрица Nx5, представляющая список областей интереса, где N - количество RoI. Первый столбец представляет индекс изображения, а остальные четыре - координаты левого верхнего и правого нижнего углов области.

Что на самом деле делает пул RoI? Для каждой интересующей области из входного списка он берет часть входной карты функций, которая ему соответствует, и масштабирует ее до некоторого предопределенного размера (например, 7 × 7). Масштабирование выполняется:

  • Разделение предложения региона на секции равного размера (количество которых совпадает с размером выходных данных)
  • Нахождение наибольшего значения в каждом разделе
  • Копирование этих максимальных значений в выходной буфер

В результате из списка прямоугольников разных размеров мы можем быстро получить список соответствующих карт функций с фиксированным размером. Обратите внимание, что размер выходных данных объединения RoI на самом деле не зависит ни от размера входной карты функций, ни от размера предложений по регионам. Это определяется исключительно количеством разделов, на которые мы делим предложение. В чем преимущество пула рентабельности инвестиций? Один из них - скорость обработки. Если на фрейме есть несколько предложений объектов (а их обычно много), мы все равно можем использовать одну и ту же карту входных объектов для всех из них. Поскольку вычисление сверток на ранних этапах обработки очень дорого, этот подход может сэкономить нам много времени. На диаграмме ниже показана работа ROI pooling.

Из предыдущих разделов мы получили gt_roi_locs, gt_roi_labels и sample_rois. Мы будем использовать sample_rois в качестве входных данных для слоя roi_pooling. Обратите внимание, что sample_rois имеет размерность [N, 4], а формат каждой строки - yxhw [y, x, h, w]. Нам нужно внести два изменения в этот массив,

  • Добавление индекса изображения [Здесь у нас только одно изображение]
  • изменение формата на xywh.

Поскольку sample_rois является массивом numpy, мы преобразуем его в Pytorch Tensor. создать тензор roi_indices.

rois = torch.from_numpy(sample_rois).float()
roi_indices = 0 * np.ones((len(rois),), dtype=np.int32)
roi_indices = torch.from_numpy(roi_indices).float()
print(rois.shape, roi_indices.shape)
#Out:
#torch.Size([128, 4]) torch.Size([128])

concat rois и roi_indices, так что мы получаем тензор с формой [N, 5] (index, x, y, h, w)

indices_and_rois = torch.cat([roi_indices[:, None], rois], dim=1)
xy_indices_and_rois = indices_and_rois[:, [0, 2, 1, 4, 3]]
indices_and_rois = xy_indices_and_rois.contiguous()
print(xy_indices_and_rois.shape)
#Out:
#torch.Size([128, 5])

Теперь нам нужно передать этот массив на уровень roi_pooling. Мы кратко обсудим, как это работает. Код sudo выглядит следующим образом

- Multiply the dimensions of rois with the sub_sampling ratio (16 in this case)
- Empty output Tensor
- Take each roi
    - subset the feature map based on the roi dimension
    - Apply AdaptiveMaxPool2d to this subset Tensor.
    - Add the outputs to the output Tensor
- Empty output Tensor goes to the network

Мы определим размер как 7 x 7 и определим adaptive_max_pool

size = (7, 7)
adaptive_max_pool = AdaptiveMaxPool2d(size[0], size[1])
output = []
rois = indices_and_rois.data.float()
rois[:, 1:].mul_(1/16.0) # Subsampling ratio
rois = rois.long()
num_rois = rois.size(0)
for i in range(num_rois):
    roi = rois[i]
    im_idx = roi[0]
    im = out_map.narrow(0, im_idx, 1)[..., roi[2]:(roi[4]+1), roi[1]:(roi[3]+1)]
    output.append(adaptive_max_pool(im))
output = torch.cat(output, 0)
print(output.size())
#Out:
# torch.Size([128, 512, 7, 7])
# Reshape the tensor so that we can pass it through the feed forward layer.
k = output.view(output.size(0), -1)
print(k.shape)
#Out:
# torch.Size([128, 25088])

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

roi_head_classifier = nn.Sequential(*[nn.Linear(25088, 4096),
                                      nn.Linear(4096, 4096)])
cls_loc = nn.Linear(4096, 21 * 4) # (VOC 20 classes + 1 background. Each will have 4 co-ordinates)
cls_loc.weight.data.normal_(0, 0.01)
cls_loc.bias.data.zero_()

score = nn.Linear(4096, 21) # (VOC 20 classes + 1 background)

передавая результат roi-pooling в сеть, определенную выше, мы получаем

k = roi_head_classifier(k)
roi_cls_loc = cls_loc(k)
roi_cls_score = score(k)
print(roi_cls_loc.shape, roi_cls_score.shape)
#Out:
# torch.Size([128, 84]), torch.Size([128, 21])

roi_cls_loc и roi_cls_score - два выходных тензора, из которых мы можем получить фактические ограничивающие рамки. Мы увидим этот раздел 8. В разделе 7 мы вычислим потери для сетей RPN и Fast RCNN. Это завершит реализацию Faster R-CNN.

Функции потерь

У нас есть две сети, RPN и Fast-RCNN, каждая из которых имеет по два выхода (головка регрессии и головка классификации). Функция потерь для обеих сетей определяется как

РПН Убыток

где p_{i} - прогнозируемая метка класса, а p_{i}^* - фактическая оценка класса. t_{i} и t_{i}^* - это прогнозируемые координаты и фактические координаты. Метка наземной истины p_{i}^* равна 1, если привязка положительна, и 0, если привязка отрицательна. Посмотрим, как это делается в Pytorch.

В разделе 2 мы вычислили цели якорных ящиков, а в разделе 3 мы вычислили выходы сети RPN. Разница между ними даст нам потерю RPN. Посмотрим, как это рассчитывается сейчас.

print(pred_anchor_locs.shape)
print(pred_cls_scores.shape)
print(anchor_locations.shape)
print(anchor_labels.shape)
#Out:
# torch.Size([1, 12321, 4])
# torch.Size([1, 12321, 2])
# (12321, 4)
# (12321,)

Мы немного переделаем так, чтобы входы и выходы были выровнены

rpn_loc = pred_anchor_locs[0]
rpn_score = pred_cls_scores[0]
gt_rpn_loc = torch.from_numpy(anchor_locations)
gt_rpn_score = torch.from_numpy(anchor_labels)
print(rpn_loc.shape, rpn_score.shape, gt_rpn_loc.shape, gt_rpn_score.shape)
#Out
# torch.Size([12321, 4]) torch.Size([12321, 2]) torch.Size([12321, 4]) torch.Size([12321])

pred_cls_scores и anchor_labels - это прогнозируемая оценка объектности и фактическая оценка объектности сети RPN. Мы будем использовать следующие функции потерь для регрессии и классификации соответственно.

Для классификации мы используем кросс-энтропийную потерю

используя Pytorch, мы можем рассчитать потери, используя,

import torch.nn.functional as F
rpn_cls_loss = F.cross_entropy(rpn_score, gt_rpn_score.long(), ignore_index = -1)
print(rpn_cls_loss)
#Out:
# Variable containing:
#  0.6940
# [torch.FloatTensor of size 1]

Для регрессии мы используем плавные потери L1, как определено в статье Fast RCNN,

Они использовали потерю L1 вместо потери L2, потому что значения прогнозируемой главы регрессии RPN не ограничены. Потеря регрессии также применяется к ограничивающим прямоугольникам с положительной меткой.

pos = gt_rpn_score > 0
mask = pos.unsqueeze(1).expand_as(rpn_loc)
print(mask.shape)
#Out:
# torch.Size(12321, 4)

Теперь возьмем те ограничивающие рамки, на которых есть положительные метки.

mask_loc_preds = rpn_loc[mask].view(-1, 4)
mask_loc_targets = gt_rpn_loc[mask].view(-1, 4)
print(mask_loc_preds.shape, mask_loc_preds.shape)
#Out:
# torch.Size([6, 4]) torch.Size([6, 4])

Потери регрессии применяются следующим образом

x = torch.abs(mask_loc_targets - mask_loc_preds)
rpn_loc_loss = ((x < 1).float() * 0.5 * x**2) + ((x >= 1).float() * (x-0.5))
print(rpn_loc_loss.sum())
#Out:
# Variable containing:
#  0.3826
# [torch.FloatTensor of size 1]

Сочетание rpn_cls_loss и rpn_reg_loss, поскольку потеря класса применяется ко всем ограничивающим прямоугольникам, а потеря регрессии применяется только к положительному ограничивающему прямоугольнику, авторы ввели {$$} \ lambda {$$} как гиперпараметр. Они также нормализовали потерю местоположения rpn с количеством ограничивающего прямоугольника N_{reg}. Функция кросс-энтропии в Pytorch уже нормализует потери, поэтому нам не нужно снова применять N_{cls}.

rpn_lambda = 10.
N_reg = (gt_rpn_score >0).float().sum()
rpn_loc_loss = rpn_loc_loss.sum() / N_reg
rpn_loss = rpn_cls_loss + (rpn_lambda * rpn_loc_loss)
print(rpn_loss)
#Out:0.00248

Быстрая потеря R-CNN

Функции потерь Fast R-CNN также реализованы таким же образом с небольшими настройками.

У нас есть следующие переменные

  • предсказанный
print(roi_cls_loc.shape)
print(roi_cls_score.shape)
#Out:
# torch.Size([128, 84])
# torch.Size([128, 21])
  • Действительный
print(gt_roi_locs.shape)
print(gt_roi_labels.shape)
#Out:
#(128, 4)
#(128, )
  • Преобразование наземной истины в переменную факела
gt_roi_loc = torch.from_numpy(gt_roi_locs)
gt_roi_label = torch.from_numpy(np.float32(gt_roi_labels)).long()
print(gt_roi_loc.shape, gt_roi_label.shape)
#Out:
#torch.Size([128, 4]) torch.Size([128])
  • Потеря классификации
roi_clss_loss = F.cross_entropy(roi_cls_score, rt_roi_label, ignore_index=-1)
print(roi_cls_loss.shape)
#Out:
#Variable containing:
#  3.0458
# [torch.FloatTensor of size 1]
  • Потеря регрессии Для потери регрессии каждое местоположение ROI имеет 21 прогнозируемую ограничивающую рамку (num_classes + background). Для расчета потерь мы будем использовать только ограничивающую рамку с положительной меткой (p_{i}^*).
n_sample = roi_cls_loc.shape[0]
roi_loc = roi_cls_loc.view(n_sample, -1, 4)
print(roi_loc.shape)
#Out:
#torch.Size([128, 21, 4])
roi_loc = roi_loc[torch.arange(0, n_sample).long(), gt_roi_label]
print(roi_loc.shape)
#Out:
#torch.Size([128, 4])

вычисляя потери регрессии так же, как мы вычисляли потери регрессии для сети RPN, мы получаем

roi_loc_loss = REGLoss(roi_loc, gt_roi_loc)
print(roi_loc_loss)
#Out:
#Variable containing:
#  0.1895
# [torch.FloatTensor of size 1]

Обратите внимание, что здесь мы не писали никакой функции RegLoss. Читатель может обернуть все методы, обсуждаемые в RPN reg loss, и реализовать эту функцию.

  • общая потеря рентабельности инвестиций
roi_lambda = 10.
roi_loss = roi_cls_loss + (roi_lambda * roi_loc_loss)
print(roi_loss)
#Out:
#Variable containing:
#  4.2353
# [torch.FloatTensor of size 1]

Полная потеря

Теперь нам нужно объединить потерю RPN и потерю Fast-RCNN, чтобы вычислить общие потери для 1 итерации. это простое дополнение

total_loss = rpn_loss + roi_loss

Вот и все, мы должны повторить это для нескольких итераций, делая одно изображение за другим во время обучения.

Это оно. В документе Faster RCNN обсуждаются различные способы обучения этой нейронной сети. См. Статью в разделе ссылок.

На заметку:

  • Более быстрая RCNN обновляется сетями пирамиды функций, а количество якорных ящиков примерно равно ~ 100000 и более точным при обнаружении небольших объектов.
  • Более быстрый RCNN теперь обучается с использованием более популярных бэкендов, таких как Resnet и ResNext.
  • Более быстрый RCNN является основой для mask-rcnn, который представляет собой современную единую модель, например сегментацию.

использованная литература

Автор Пракашджай. Вклады Сурадж Амонкар, Сачин Чандра, Раджниш Кумар и Викаш Чалла.

Спасибо и удачного обучения.