14.4. Якорные рамки

Алгоритмы обнаружения объектов обычно выбирают множество примеров областей на входном изображении, определяют, содержат ли эти области искомые объекты, и корректируют границы областей, чтобы точнее предсказать истинные ограничивающие рамки объектов. Различные модели могут использовать разные схемы выборки областей. Здесь мы рассмотрим один из таких методов: он генерирует несколько ограничивающих рамок с различными масштабами и соотношениями сторон, центрированных на каждом пикселе. Эти ограничивающие рамки называются якорными рамками. Мы разработаем модель обнаружения объектов на основе якорных рамок в Section 14.7.

Сначала изменим точность вывода, чтобы сделать его более компактным.

%matplotlib inline
import torch
from d2l import torch as d2l

torch.set_printoptions(2)  # Simplify printing accuracy

14.4.1. Генерация нескольких якорных коробок

Предположим, что входное изображение имеет высоту \(h\) и ширину \(w\). Мы генерируем якорные рамки различных форм, центрированные на каждом пикселе изображения. Пусть масштаб равен \(s\in (0, 1]\), а соотношение сторон (отношение ширины к высоте) равно \(r > 0\). Тогда ширина и высота якорной рамки равны \(ws\sqrt{r}\) и \(hs/\sqrt{r}\) соответственно. Обратите внимание, что при заданном центральном положении якорная рамка с известными шириной и высотой определяется однозначно.

Для генерации нескольких якорных прямоугольников различных форм зададим ряд масштабов \(s_1,\ldots, s_n\) и ряд соотношений сторон \(r_1,\ldots, r_m\). При использовании всех комбинаций этих масштабов и соотношений сторон с каждым пикселем в качестве центра на входном изображении будет сгенерировано всего \(whnm\) якорных прямоугольников. Хотя эти якорные прямоугольники могут покрывать все истинные ограничивающие рамки, вычислительная сложность оказывается слишком высокой. На практике мы можем рассматривать только те комбинации, которые содержат \(s_1\) или \(r_1\):

(14.4.1)\[(s_1, r_1), (s_1, r_2), \ldots, (s_1, r_m), (s_2, r_1), (s_3, r_1), \ldots, (s_n, r_1).\]

То есть количество якорных боксов, центрированных на одном и том же пикселе, равно \(n+m-1\). Для всего входного изображения мы сгенерируем в общей сложности \(wh(n+m-1)\) якорных боксов.

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

#@save
def multibox_prior(data, sizes, ratios):
    """Generate anchor boxes with different shapes centered on each pixel."""
    in_height, in_width = data.shape[-2:]
    device, num_sizes, num_ratios = data.device, len(sizes), len(ratios)
    boxes_per_pixel = (num_sizes + num_ratios - 1)
    size_tensor = torch.tensor(sizes, device=device)
    ratio_tensor = torch.tensor(ratios, device=device)
    # Offsets are required to move the anchor to the center of a pixel. Since
    # a pixel has height=1 and width=1, we choose to offset our centers by 0.5
    offset_h, offset_w = 0.5, 0.5
    steps_h = 1.0 / in_height  # Scaled steps in y axis
    steps_w = 1.0 / in_width  # Scaled steps in x axis

    # Generate all center points for the anchor boxes
    center_h = (torch.arange(in_height, device=device) + offset_h) * steps_h
    center_w = (torch.arange(in_width, device=device) + offset_w) * steps_w
    shift_y, shift_x = torch.meshgrid(center_h, center_w, indexing='ij')
    shift_y, shift_x = shift_y.reshape(-1), shift_x.reshape(-1)

    # Generate `boxes_per_pixel` number of heights and widths that are later
    # used to create anchor box corner coordinates (xmin, xmax, ymin, ymax)
    w = torch.cat((size_tensor * torch.sqrt(ratio_tensor[0]),
                   sizes[0] * torch.sqrt(ratio_tensor[1:])))\
                   * in_height / in_width  # Handle rectangular inputs
    h = torch.cat((size_tensor / torch.sqrt(ratio_tensor[0]),
                   sizes[0] / torch.sqrt(ratio_tensor[1:])))
    # Divide by 2 to get half height and half width
    anchor_manipulations = torch.stack((-w, -h, w, h)).T.repeat(
                                        in_height * in_width, 1) / 2

    # Each center point will have `boxes_per_pixel` number of anchor boxes, so
    # generate a grid of all anchor box centers with `boxes_per_pixel` repeats
    out_grid = torch.stack([shift_x, shift_y, shift_x, shift_y],
                dim=1).repeat_interleave(boxes_per_pixel, dim=0)
    output = out_grid + anchor_manipulations
    return output.unsqueeze(0)

Мы видим, что форма возвращаемой переменной якорных рамок ``Y`` равна (размер батча, количество якорных рамок, 4).

img = d2l.plt.imread('../img/catdog.jpg')
h, w = img.shape[:2]

print(h, w)
X = torch.rand(size=(1, 3, h, w))  # Construct input data
Y = multibox_prior(X, sizes=[0.75, 0.5, 0.25], ratios=[1, 2, 0.5])
Y.shape

После изменения формы переменной якорных рамок Y на (высота изображения, ширина изображения, количество якорных рамок, центрированных на одном пикселе, 4) мы можем получить все якорные рамки, центрированные в заданной позиции пикселя. В дальнейшем мы получим доступ к первой якорной рамке, центрированной на (250, 250). Она содержит четыре элемента: координаты по оси \((x, y)\) в левом верхнем углу и координаты по оси \((x, y)\) в правом нижнем углу якорной рамки. Значения координат по обеим осям разделены на ширину и высоту изображения соответственно.

boxes = Y.reshape(h, w, 5, 4)
boxes[250, 250, 0, :]

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

#@save
def show_bboxes(axes, bboxes, labels=None, colors=None):
    """Show bounding boxes."""

    def make_list(obj, default_values=None):
        if obj is None:
            obj = default_values
        elif not isinstance(obj, (list, tuple)):
            obj = [obj]
        return obj

    labels = make_list(labels)
    colors = make_list(colors, ['b', 'g', 'r', 'm', 'c'])
    for i, bbox in enumerate(bboxes):
        color = colors[i % len(colors)]
        rect = d2l.bbox_to_rect(bbox.detach().numpy(), color)
        axes.add_patch(rect)
        if labels and len(labels) > i:
            text_color = 'k' if color == 'w' else 'w'
            axes.text(rect.xy[0], rect.xy[1], labels[i],
                      va='center', ha='center', fontsize=9, color=text_color,
                      bbox=dict(facecolor=color, lw=0))

Как мы только что видели, значения координат по осям \(x\) и \(y\) в переменной boxes были разделены на ширину и высоту изображения соответственно. При отрисовке якорных рамок нам необходимо восстановить их исходные значения координат; поэтому ниже мы определяем переменную bbox_scale. Теперь мы можем отрисовать все якорные рамки, центрированные в точке (250, 250) на изображении. Как видно, синяя якорная рамка с масштабом 0,75 и соотношением сторон 1 хорошо охватывает собаку на изображении.

d2l.set_figsize()
bbox_scale = torch.tensor((w, h, w, h))
fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, boxes[250, 250, :, :] * bbox_scale,
            ['s=0.75, r=1', 's=0.5, r=1', 's=0.25, r=1', 's=0.75, r=2',
             's=0.75, r=0.5'])

14.4.2. Отношение площади пересечения к площади объединения (IoU)

Мы уже упоминали, что якорная рамка «хорошо» охватывает собаку на изображении. Если известна ограничивающая рамка объекта, заданная вручную (ground-truth), как можно количественно оценить, насколько хорошо это выполняется? Интуитивно мы можем измерить сходство между якорной рамкой и ограничивающей рамкой, заданной вручную. Известно, что индекс Жаккара позволяет измерять сходство между двумя множествами. Для множеств \(\mathcal{A}\) и \(\mathcal{B}\) индекс Жаккара определяется как отношение размера их пересечения к размеру их объединения:

(14.4.2)\[J(\mathcal{A},\mathcal{B}) = \frac{\left|\mathcal{A} \cap \mathcal{B}\right|}{\left| \mathcal{A} \cup \mathcal{B}\right|}.\]

На самом деле, область пикселей любой ограничивающей рамки можно рассматривать как множество пикселей. Таким образом, сходство двух ограничивающих рамок можно измерить с помощью индекса Жаккара их множеств пикселей. Для двух ограничивающих рамок индекс Жаккара обычно называют пересечением над объединением (IoU), который представляет собой отношение площади их пересечения к площади объединения, как показано на рисунке Fig. 14.4.1. Значение IoU находится в диапазоне от 0 до 1: 0 означает, что ограничивающие рамки вообще не пересекаются, а 1 указывает на то, что они совпадают.

../_images/iou.svg

Fig. 14.4.1 Отношение площади пересечения к площади объединения двух ограничивающих рамок называется IoU.

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

#@save
def box_iou(boxes1, boxes2):
    """Compute pairwise IoU across two lists of anchor or bounding boxes."""
    box_area = lambda boxes: ((boxes[:, 2] - boxes[:, 0]) *
                              (boxes[:, 3] - boxes[:, 1]))
    # Shape of `boxes1`, `boxes2`, `areas1`, `areas2`: (no. of boxes1, 4),
    # (no. of boxes2, 4), (no. of boxes1,), (no. of boxes2,)
    areas1 = box_area(boxes1)
    areas2 = box_area(boxes2)
    # Shape of `inter_upperlefts`, `inter_lowerrights`, `inters`: (no. of
    # boxes1, no. of boxes2, 2)
    inter_upperlefts = torch.max(boxes1[:, None, :2], boxes2[:, :2])
    inter_lowerrights = torch.min(boxes1[:, None, 2:], boxes2[:, 2:])
    inters = (inter_lowerrights - inter_upperlefts).clamp(min=0)
    # Shape of `inter_areas` and `union_areas`: (no. of boxes1, no. of boxes2)
    inter_areas = inters[:, :, 0] * inters[:, :, 1]
    union_areas = areas1[:, None] + areas2 - inter_areas
    return inter_areas / union_areas

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

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

Как известно, обучающая выборка для задачи обнаружения объектов содержит метки координат ограничивающих рамок истинных объектов и классов объектов, которые они охватывают. Чтобы присвоить метки любой сгенерированной якорной рамке, мы обращаемся к меткам координат и класса той присвоенной ограничивающей рамки истинного объекта, которая находится ближе всего к данной якорной рамке. В дальнейшем мы опишем алгоритм присвоения ближайших ограничивающих рамок истинных объектов якорным рамкам.

14.4.3.1. Присвоение якорным блокам истинных ограничивающих рамок

Рассмотрим изображение, для которого заданы якорные рамки \(A_1, A_2, \ldots, A_{n_a}\) и истинные ограничивающие рамки \(B_1, B_2, \ldots, B_{n_b}\), где \(n_a \geq n_b\). Определим матрицу \(\mathbf{X} \in \mathbb{R}^{n_a \times n_b}\), элемент \(x_{ij}\) которой в \(i^\textrm{th}\)-й строке и \(j^\textrm{th}\)-м столбце равен значению IoU между якорной рамкой \(A_i\) и истинной ограничивающей рамкой \(B_j\). Алгоритм состоит из следующих шагов:

  1. Находим наибольший элемент в матрице \(\mathbf{X}\) и обозначаем индексы его строки и столбца как \(i_1\) и \(j_1\) соответственно. Затем ограничивающая рамка \(B_{j_1}\) назначается якорной рамке \(A_{i_1}\). Это вполне интуитивно, поскольку \(A_{i_1}\) и \(B_{j_1}\) являются наиболее близкими среди всех пар якорных рамок и ограничивающих рамок. После первого назначения исключаем все элементы в строке \({i_1}^\textrm{th}\) и столбце \({j_1}^\textrm{th}\) матрицы \(\mathbf{X}\).

  2. Находим наибольший из оставшихся элементов в матрице \(\mathbf{X}\) и обозначаем индексы её строки и столбца как \(i_2\) и \(j_2\) соответственно. Назначаем ограничивающую рамку истинных значений \(B_{j_2}\) якорной рамке \(A_{i_2}\) и исключаем все элементы в строке \({i_2}^\textrm{th}\) и столбце \({j_2}^\textrm{th}\) матрицы \(\mathbf{X}\).

  3. На этом этапе элементы двух строк и двух столбцов в матрице \(\mathbf{X}\) были отброшены. Мы продолжаем этот процесс до тех пор, пока не будут отброшены все элементы в \(n_b\) столбцах матрицы \(\mathbf{X}\). К этому моменту мы присвоим истинную ограничивающую рамку каждой из \(n_b\) якорных рамок.

  4. Перебираем только оставшиеся \(n_a - n_b\) якорные рамки. Например, для любой якорной рамки \(A_i\) находим ограничивающую рамку истинных значений \(B_j\), имеющую наибольшее значение IoU с \(A_i\) во всей строке \(i^\textrm{th}\) матрицы \(\mathbf{X}\), и присваиваем \(B_j\) рамке \(A_i\) только в том случае, если это значение IoU превышает заранее заданный порог.

Проиллюстрируем описанный выше алгоритм на конкретном примере. Как показано на рисунке Fig. 14.4.2 (слева), предположим, что максимальное значение в матрице \(\mathbf{X}\) равно \(x_{23}\). Тогда мы назначаем истинную ограничивающую рамку \(B_3\) якорной рамке \(A_2\). Затем исключаем все элементы второй строки и третьего столбца матрицы, находим наибольшее \(x_{71}\) среди оставшихся элементов (заштрихованная область) и назначаем истинную ограничивающую рамку \(B_1\) якорной рамке \(A_7\). Далее, как показано на рисунке Fig. 14.4.2 (в центре), исключаем все элементы седьмой строки и первого столбца матрицы, находим наибольшее \(x_{54}\) среди оставшихся элементов (заштрихованная область) и назначаем истинную ограничивающую рамку \(B_4\) якорной рамке \(A_5\). Наконец, как показано на рисунке Fig. 14.4.2 (справа), исключаем все элементы пятой строки и четвертого столбца матрицы, находим наибольшее \(x_{92}\) среди оставшихся элементов (заштрихованная область) и назначаем истинную ограничивающую рамку \(B_2\) якорной рамке \(A_9\). После этого остается только пройти по оставшимся якорным рамкам \(A_1, A_3, A_4, A_6, A_8\) и определить, следует ли назначать им истинные ограничивающие рамки в соответствии с пороговым значением.

../_images/anchor-label.svg

Fig. 14.4.2 Присвоение эталонных ограничивающих рамок якорным рамкам.

Этот алгоритм реализован в следующей функции assign_anchor_to_bbox.

#@save
def assign_anchor_to_bbox(ground_truth, anchors, device, iou_threshold=0.5):
    """Assign closest ground-truth bounding boxes to anchor boxes."""
    num_anchors, num_gt_boxes = anchors.shape[0], ground_truth.shape[0]
    # Element x_ij in the i-th row and j-th column is the IoU of the anchor
    # box i and the ground-truth bounding box j
    jaccard = box_iou(anchors, ground_truth)
    # Initialize the tensor to hold the assigned ground-truth bounding box for
    # each anchor
    anchors_bbox_map = torch.full((num_anchors,), -1, dtype=torch.long,
                                  device=device)
    # Assign ground-truth bounding boxes according to the threshold
    max_ious, indices = torch.max(jaccard, dim=1)
    anc_i = torch.nonzero(max_ious >= iou_threshold).reshape(-1)
    box_j = indices[max_ious >= iou_threshold]
    anchors_bbox_map[anc_i] = box_j
    col_discard = torch.full((num_anchors,), -1)
    row_discard = torch.full((num_gt_boxes,), -1)
    for _ in range(num_gt_boxes):
        max_idx = torch.argmax(jaccard)  # Find the largest IoU
        box_idx = (max_idx % num_gt_boxes).long()
        anc_idx = (max_idx / num_gt_boxes).long()
        anchors_bbox_map[anc_idx] = box_idx
        jaccard[:, box_idx] = col_discard
        jaccard[anc_idx, :] = row_discard
    return anchors_bbox_map

14.4.3.2. Разметка классов и смещений

Теперь мы можем задать метки классов и смещений для каждой якорной рамки. Предположим, что якорной рамке \(A\) сопоставлена истинная ограничивающая рамка \(B\). С одной стороны, класс якорной рамки \(A\) будет помечен так же, как и класс \(B\). С другой стороны, смещение якорной рамки \(A\) будет задано на основе относительного положения между центральными координатами \(B\) и \(A\), а также относительного размера этих двух рамок. Учитывая различное положение и размеры разных рамок в наборе данных, мы можем применить преобразования к этим относительным положениям и размерам, что позволит получить более равномерно распределённые смещения, которые легче аппроксимировать. Ниже описано одно из распространённых преобразований. Пусть центральные координаты \(A\) и \(B\) равны \((x_a, y_a)\) и \((x_b, y_b)\), их ширины — \(w_a\) и \(w_b\), а высоты — \(h_a\) и \(h_b\) соответственно. Тогда смещение \(A\) можно задать как

(14.4.3)\[\left( \frac{ \frac{x_b - x_a}{w_a} - \mu_x }{\sigma_x}, \frac{ \frac{y_b - y_a}{h_a} - \mu_y }{\sigma_y}, \frac{ \log \frac{w_b}{w_a} - \mu_w }{\sigma_w}, \frac{ \log \frac{h_b}{h_a} - \mu_h }{\sigma_h}\right),\]

где значения констант по умолчанию равны \(\mu_x = \mu_y = \mu_w = \mu_h = 0, \sigma_x=\sigma_y=0.1\) и \(\sigma_w=\sigma_h=0.2\). Эта трансформация реализована в функции offset_boxes ниже.**

#@save
def offset_boxes(anchors, assigned_bb, eps=1e-6):
    """Transform for anchor box offsets."""
    c_anc = d2l.box_corner_to_center(anchors)
    c_assigned_bb = d2l.box_corner_to_center(assigned_bb)
    offset_xy = 10 * (c_assigned_bb[:, :2] - c_anc[:, :2]) / c_anc[:, 2:]
    offset_wh = 5 * torch.log(eps + c_assigned_bb[:, 2:] / c_anc[:, 2:])
    offset = torch.cat([offset_xy, offset_wh], axis=1)
    return offset

Если якорной рамке не сопоставлена истинная ограничивающая рамка, мы просто помечаем класс этой якорной рамки как «фон». Якорные рамки, отнесённые к классу «фон», часто называют отрицательными якорными рамками, а остальные — положительными якорными рамками. Мы реализуем следующую multibox_target функцию для присвоения меток классов и смещений якорным рамкам (аргумент anchors) с использованием истинных ограничивающих рамок (аргумент labels). В этой функции классу «фон» присваивается значение ноль, а целочисленный индекс нового класса увеличивается на единицу.

#@save
def multibox_target(anchors, labels):
    """Label anchor boxes using ground-truth bounding boxes."""
    batch_size, anchors = labels.shape[0], anchors.squeeze(0)
    batch_offset, batch_mask, batch_class_labels = [], [], []
    device, num_anchors = anchors.device, anchors.shape[0]
    for i in range(batch_size):
        label = labels[i, :, :]
        anchors_bbox_map = assign_anchor_to_bbox(
            label[:, 1:], anchors, device)
        bbox_mask = ((anchors_bbox_map >= 0).float().unsqueeze(-1)).repeat(
            1, 4)
        # Initialize class labels and assigned bounding box coordinates with
        # zeros
        class_labels = torch.zeros(num_anchors, dtype=torch.long,
                                   device=device)
        assigned_bb = torch.zeros((num_anchors, 4), dtype=torch.float32,
                                  device=device)
        # Label classes of anchor boxes using their assigned ground-truth
        # bounding boxes. If an anchor box is not assigned any, we label its
        # class as background (the value remains zero)
        indices_true = torch.nonzero(anchors_bbox_map >= 0)
        bb_idx = anchors_bbox_map[indices_true]
        class_labels[indices_true] = label[bb_idx, 0].long() + 1
        assigned_bb[indices_true] = label[bb_idx, 1:]
        # Offset transformation
        offset = offset_boxes(anchors, assigned_bb) * bbox_mask
        batch_offset.append(offset.reshape(-1))
        batch_mask.append(bbox_mask.reshape(-1))
        batch_class_labels.append(class_labels)
    bbox_offset = torch.stack(batch_offset)
    bbox_mask = torch.stack(batch_mask)
    class_labels = torch.stack(batch_class_labels)
    return (bbox_offset, bbox_mask, class_labels)

14.4.3.3. Пример

Проиллюстрируем процесс разметки якорных рамок на конкретном примере. Определим истинные ограничивающие рамки для собаки и кошки на загруженном изображении, где первый элемент — это класс (0 для собаки и 1 для кошки), а остальные четыре элемента — это координаты по оси \((x, y)\) верхнего левого и нижнего правого углов (диапазон значений от 0 до 1). Также создадим пять якорных рамок для разметки, используя координаты верхнего левого и нижнего правого углов: \(A_0, \ldots, A_4\) (индексация начинается с 0). Затем отобразим эти истинные ограничивающие рамки и якорные рамки на изображении.

ground_truth = torch.tensor([[0, 0.1, 0.08, 0.52, 0.92],
                         [1, 0.55, 0.2, 0.9, 0.88]])
anchors = torch.tensor([[0, 0.1, 0.2, 0.3], [0.15, 0.2, 0.4, 0.4],
                    [0.63, 0.05, 0.88, 0.98], [0.66, 0.45, 0.8, 0.8],
                    [0.57, 0.3, 0.92, 0.9]])

fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, ground_truth[:, 1:] * bbox_scale, ['dog', 'cat'], 'k')
show_bboxes(fig.axes, anchors * bbox_scale, ['0', '1', '2', '3', '4']);

Используя функцию multibox_target, определённую выше, мы можем присвоить метки классов и смещения этим якорным прямоугольникам на основе истинных ограничивающих прямоугольников для собаки и кошки. В этом примере индексы классов фона, собаки и кошки равны 0, 1 и 2 соответственно. Ниже мы добавим размерность для примеров якорных прямоугольников и истинных ограничивающих прямоугольников.

labels = multibox_target(anchors.unsqueeze(dim=0),
                         ground_truth.unsqueeze(dim=0))

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

Рассмотрим возвращенные метки классов, опираясь на позиции якорных рамок и ограничивающих рамок истинных объектов на изображении. Сначала среди всех пар якорных рамок и ограничивающих рамок истинных объектов наибольшее значение IoU имеет пара, состоящая из якорной рамки \(A_4\) и ограничивающей рамки истинного объекта «кошка». Следовательно, класс для \(A_4\) помечается как «кошка». Исключив пары, содержащие якорную рамку \(A_4\) или ограничивающую рамку истинного объекта «кошка», среди оставшихся находим пару якорной рамки \(A_1\) и ограничивающей рамки истинного объекта «собака», которая имеет наибольшее значение IoU. Таким образом, класс для \(A_1\) помечается как «собака». Далее нам нужно перебрать оставшиеся три непомеченные якорные рамки: \(A_0\), \(A_2\) и \(A_3\). Для \(A_0\) класс ограничивающей рамки истинного объекта с наибольшим IoU — это «собака», однако значение IoU ниже заданного порога (0,5), поэтому класс помечается как «фон». Для \(A_2\) класс ограничивающей рамки истинного объекта с наибольшим IoU — это «кошка», и значение IoU превышает порог, поэтому класс помечается как «кошка». Для \(A_3\) класс ограничивающей рамки истинного объекта с наибольшим IoU — это «кошка», но значение IoU ниже порога, поэтому класс помечается как «фон».

labels[2]

Вторым возвращаемым значением является переменная маски формы (размер батча, четыре умножить на количество якорных рамок). Каждые четыре элемента в переменной маски соответствуют четырем значениям смещения для каждой якорной рамки. Поскольку нас не интересует обнаружение фона, смещения этого отрицательного класса не должны влиять на целевую функцию. С помощью поэлементного умножения нули в переменной маски исключают смещения отрицательного класса перед вычислением целевой функции.

labels[1]

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

labels[0]

14.4.4. Предсказание ограничивающих рамок с помощью подавления немаксимумов

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

#@save
def offset_inverse(anchors, offset_preds):
    """Predict bounding boxes based on anchor boxes with predicted offsets."""
    anc = d2l.box_corner_to_center(anchors)
    pred_bbox_xy = (offset_preds[:, :2] * anc[:, 2:] / 10) + anc[:, :2]
    pred_bbox_wh = torch.exp(offset_preds[:, 2:] / 5) * anc[:, 2:]
    pred_bbox = torch.cat((pred_bbox_xy, pred_bbox_wh), axis=1)
    predicted_bbox = d2l.box_center_to_corner(pred_bbox)
    return predicted_bbox

Когда якорных коробок много, для одного и того же объекта может быть предсказано множество похожих ограничивающих рамок (с существенным перекрытием). Чтобы упростить вывод, мы можем объединять похожие предсказанные ограничивающие рамки, относящиеся к одному и тому же объекту, с помощью немаксимальной подавления (NMS).

Вот как работает подавление немаксимумов. Для предсказанной ограничивающей рамки \(B\) модель обнаружения объектов вычисляет предсказанное правдоподобие для каждого класса. Обозначая через \(p\) наибольшее предсказанное правдоподобие, класс, соответствующий этой вероятности, является предсказанным классом для \(B\). Конкретно, значение \(p\) называют уверенностью (score) предсказанной ограничивающей рамки \(B\). На одном и том же изображении все предсказанные ограничивающие рамки, не относящиеся к фону, сортируются по убыванию уверенности, чтобы сформировать список \(L\). Затем мы обрабатываем отсортированный список \(L\) следующим образом:

  1. Выбираем предсказанную ограничивающую рамку \(B_1\) с наивысшей уверенностью из \(L\) в качестве базовой и удаляем из \(L\) все остальные предсказанные ограничивающие рамки, у которых IoU с \(B_1\) превышает заранее заданный порог \(\epsilon\). Таким образом, \(L\) сохраняет предсказанную ограничивающую рамку с наивысшей уверенностью, но отбрасывает остальные, которые слишком похожи на неё. Иными словами, рамки с ненемаксимальными оценками уверенности подавляются.

  2. Выберем предсказанную ограничивающую рамку \(B_2\) со вторым по величине значением уверенности из \(L\) в качестве другой базовой и удалим из \(L\) все предсказанные ограничивающие рамки, не являющиеся базовыми, IoU которых с \(B_2\) превышает \(\epsilon\).

  3. Повторяйте описанную выше процедуру до тех пор, пока все предсказанные ограничивающие рамки в \(L\) не будут использованы в качестве базовых. К этому моменту значение IoU для любой пары предсказанных ограничивающих рамок в \(L\) окажется ниже порога \(\epsilon\); следовательно, ни одна пара не будет слишком похожей друг на друга.

  4. Вывести все предсказанные ограничивающие рамки из списка \(L\).

Следующая nms функция сортирует оценки уверенности по убыванию и возвращает их индексы.

#@save
def nms(boxes, scores, iou_threshold):
    """Sort confidence scores of predicted bounding boxes."""
    B = torch.argsort(scores, dim=-1, descending=True)
    keep = []  # Indices of predicted bounding boxes that will be kept
    while B.numel() > 0:
        i = B[0]
        keep.append(i)
        if B.numel() == 1: break
        iou = box_iou(boxes[i, :].reshape(-1, 4),
                      boxes[B[1:], :].reshape(-1, 4)).reshape(-1)
        inds = torch.nonzero(iou <= iou_threshold).reshape(-1)
        B = B[inds + 1]
    return torch.tensor(keep, device=boxes.device)

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

#@save
def multibox_detection(cls_probs, offset_preds, anchors, nms_threshold=0.5,
                       pos_threshold=0.009999999):
    """Predict bounding boxes using non-maximum suppression."""
    device, batch_size = cls_probs.device, cls_probs.shape[0]
    anchors = anchors.squeeze(0)
    num_classes, num_anchors = cls_probs.shape[1], cls_probs.shape[2]
    out = []
    for i in range(batch_size):
        cls_prob, offset_pred = cls_probs[i], offset_preds[i].reshape(-1, 4)
        conf, class_id = torch.max(cls_prob[1:], 0)
        predicted_bb = offset_inverse(anchors, offset_pred)
        keep = nms(predicted_bb, conf, nms_threshold)
        # Find all non-`keep` indices and set the class to background
        all_idx = torch.arange(num_anchors, dtype=torch.long, device=device)
        combined = torch.cat((keep, all_idx))
        uniques, counts = combined.unique(return_counts=True)
        non_keep = uniques[counts == 1]
        all_id_sorted = torch.cat((keep, non_keep))
        class_id[non_keep] = -1
        class_id = class_id[all_id_sorted]
        conf, predicted_bb = conf[all_id_sorted], predicted_bb[all_id_sorted]
        # Here `pos_threshold` is a threshold for positive (non-background)
        # predictions
        below_min_idx = (conf < pos_threshold)
        class_id[below_min_idx] = -1
        conf[below_min_idx] = 1 - conf[below_min_idx]
        pred_info = torch.cat((class_id.unsqueeze(1),
                               conf.unsqueeze(1),
                               predicted_bb), dim=1)
        out.append(pred_info)
    return torch.stack(out)

Теперь применим приведённые выше реализации к конкретному примеру с четырьмя якорными рамками. Для простоты предположим, что предсказанные смещения равны нулю. Это означает, что предсказанные ограничивающие рамки совпадают с якорными рамками. Для каждого из классов — фона, собаки и кошки — мы также определим его предсказанное правдоподобие.

anchors = torch.tensor([[0.1, 0.08, 0.52, 0.92], [0.08, 0.2, 0.56, 0.95],
                      [0.15, 0.3, 0.62, 0.91], [0.55, 0.2, 0.9, 0.88]])
offset_preds = torch.tensor([0] * anchors.numel())
cls_probs = torch.tensor([[0] * 4,  # Predicted background likelihood
                      [0.9, 0.8, 0.7, 0.1],  # Predicted dog likelihood
                      [0.1, 0.2, 0.3, 0.9]])  # Predicted cat likelihood

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

fig = d2l.plt.imshow(img)
show_bboxes(fig.axes, anchors * bbox_scale,
            ['dog=0.9', 'dog=0.8', 'dog=0.7', 'cat=0.9'])

Теперь мы можем вызвать функцию multibox_detection для выполнения подавления немаксимумов, установив порог равным 0,5. Обратите внимание, что мы добавляем размерность для примеров во входном тензоре.

Мы видим, что форма возвращаемого результата равна (размер батча, количество якорных рамок, 6). Шесть элементов во внутренней размерности содержат информацию об одной и той же предсказанной ограничивающей рамке. Первый элемент — это предсказанный индекс класса, который начинается с 0 (0 — собака, 1 — кошка). Значение -1 указывает на фон или удаление при подавлении немаксимумов. Второй элемент — это уверенность предсказанной ограничивающей рамки. Оставшиеся четыре элемента — это координаты по оси \((x, y)\) левого верхнего и правого нижнего углов предсказанной ограничивающей рамки соответственно (диапазон значений от 0 до 1).

output = multibox_detection(cls_probs.unsqueeze(dim=0),
                            offset_preds.unsqueeze(dim=0),
                            anchors.unsqueeze(dim=0),
                            nms_threshold=0.5)
output

После удаления предсказанных ограничивающих рамок класса -1 мы можем вывести финальную предсказанную ограничивающую рамку, оставленную алгоритмом non-maximum suppression.

fig = d2l.plt.imshow(img)
for i in output[0].detach().numpy():
    if i[0] == -1:
        continue
    label = ('dog=', 'cat=')[int(i[0])] + str(i[1])
    show_bboxes(fig.axes, [torch.tensor(i[2:]) * bbox_scale], label)

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

14.4.5. Итог

  • Мы генерируем якорные рамки различных форм, центрированные на каждом пикселе изображения.

  • Пересечение над объединением (IoU), также известное как индекс Жаккара, измеряет сходство двух ограничивающих рамок. Это отношение площади их пересечения к площади их объединения.

  • В обучающей выборке для каждой якорной рамки требуются два типа меток. Одна из них — класс объекта, соответствующего якорной рамке, а другая — смещение ограничивающей рамки истинного положения относительно якорной рамки.

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

14.4.6. Упражнения

  1. Измените значения sizes и ratios в функции multibox_prior. Как изменятся генерируемые якорные рамки?

  2. Постройте и визуализируйте две ограничивающие рамки с IoU, равным 0,5. Как они пересекаются друг с другом?

  3. Измените переменную anchors в функциях Section 14.4.3 и Section 14.4.4. Как изменятся результаты?

  4. Немаксимальное подавление — это жадный алгоритм, который устраняет предсказанные ограничивающие рамки, удаляя их. Возможно ли, что некоторые из этих удалённых рамок на самом деле полезны? Как можно модифицировать этот алгоритм для мягкого подавления? См. Soft-NMS (Bodla et al., 2017).

  5. Можно ли обучить алгоритм подавления немаксимумов, а не создавать его вручную?

Обсуждения