
Введение
Когда пытаешься разобраться в работе YOLO по статьям в интернете, постоянно натыкаешься на примерно такое объяснение: «Алгоритм делит изображение сеткой SxS, где каждому элементу этой сетки соответствует N ббоксов с координатами, предсказаниями классов и тд...». Но лично мне становилось только непонятнее от такого высокоуровнего описания.. Ведь в исследованиях часто всё происходит примерно так: перебирают гипотезы, пока не получат приемлемый результат, а потом уже придумывают красивое описание. Поэтому для ясности хочется рассказать, как вообще приходили к идеям, которые ложились в основу YOLOv1 и последующих версий.
Немного про особенности Feature Map
Сначала поговорим про Feature Map, чтобы потом стало понятно откуда взялась эта непонятная сетка SxS)) Итак, давайте представим, что у нас есть маленькое трехканальное (RGB) изображение 10×10 пикселей. Мы прогоняем его через две свёртки: с ядром 5×5 и ядром 3×3. Соответствующий код на Torch выглядит так:
import torch
import torch.nn as nn
image = torch.rand(3,10,10) # Типа изображение
conv1 = nn.Conv2d(3,1,5) # Свертка с ядром 5
conv2 = nn.Conv2d(1,1,3) # Свертка с ядром 3
feature_map1 = conv1(image) # Результат после первой свёрки
print(f'FM1 shape: {feature_map1.shape}') # FM1 shape: torch.Size([1, 6, 6])
feature_map2 = conv2(feature_map1) # Результат после второй всёртки
print(f'FM2 shape: {feature_map2.shape}') # FM2 shape: torch.Size([1, 4, 4])Визуализация этого кода примерно такая:

На изображении видно, что левая верхняя ячейка Feature map 2 является результатом свертки выделенной области из Feature map 1. В то же время, каждая ячейка из выделенной области на Feature map 1 является результатом сверки соответствующей области исходного изображения. Получается, что выделенный элемент на Feature map 2 отображает признаки выделенной области на исходном изображении Input image. Поскольку Feature map 2 имеет размер 4×4 — можно сказать, что он как бы делит исходное изображение сеткой 4×4, так как каждый его элемент смотрит на 1 из 16 частей изображения.
Вдобавок становится понятно, что чем глубже находится Feature Map, тем более высокоуровневые признаки на изображении он описывает. Вот довольно популярная картинка, которая показывает это свойство:

Low‑Level Feature описывает всякие непонятные линии и закарючки, так как ядро сворачивает очень маленькую часть изображения. Mid‑Level Feature описывает уже что‑то более осмысленное, так как отображает часть побольше. И наконец High‑Level Feature уже содержит информацию, которую можно как‑то интерпртировать (наличие колеса, фар, решетки радиатора и тп)
YOLOv1
В 2015 году Joseph Redmon опубликовал статью You Only Look Once: Unified, Real‑Time Object Detection. Гипотеза, которую ему удалось проверить и получить хороший результат заключалась в следующем: «А что если взять какую‑то предобученную модель для классификации картинок и просто заменить последнии слои так, чтобы она предсказывала не вероятность классов, а какой‑то тензор, в котором будет содержаться информация о ббоксах и классах?».
Архитектура
В статье сначала строится архитектура для классификации изображений, вдохновленная GoogLeNet:

Эта архитектура училась классифицировать изображения на ImageNet (1000 classes) и на валидации вошла в top-5 Accuracy с 88%. После того, как мы обучили классификатор, замораживаем первые 20 слоёв. Т.е. их веса больше не будут меняться в ходе дальнейшего обучения (ничего волшебного - это обычный Fine-Tuning)
Теперь у нас есть предобученные 20 слоёв, которые извлекают информацию из изображения. Добавим к ним 4 необученных сверточных слоя + полносвязный слой. В самом конце возвращается тензор, в котором будет информация о ббоксах и соответствующих классах.
Вот финальная архитектура (Изменились только последние слои 😳👍🏻):

Пока что не обращаем внимание на размерность выходного тензора
Давайте подумаем, а как вообще записать информацию о ббоксах в выходной тензор?
Размер выходного тензора
В прошлой статье я говорил, что для описания бокса нам нужно 5 чисел:
Для того, чтоб предсказать класс объекта, находящегося в боксе, нам нужен вектор длины , где
— это количество классов (каждая компонента соответствует вероятности какого‑то класса). В датасете Pascal Voc 20 классов, так что
. Следовательно для информации об одном ббоксе нам нужен вектор длины
Ббоксы должны соответстовать какой‑то области изображения, следовательно, выходной тензор будет иметь размер, где кажд��й элемент отражает область на исходном изображении. Автор взял
, то есть выходной тензор как бы разбивает исходное изображение на 7 * 7 частей.
Далее, пусть каждой области соответствует ббоксов (автор выбрал
)
Т.е. каждому элементу выходного тензора должен быть сопоставлен вектор размера
. В статье класс решили предсказывать только для ббокса с самым большим
, поэтому формула немного упростится и станет
Общая формула размера выходного тензора выглядит так:
Отсюда мы и получаем 🤯😳🥳
Лосс и обучение
Вот и вся архитектура YOLOv1. Теперь остается только придумать функцию потерь, состоящую из ошибки предсказания ббоксов и классов. Далее в процессе обучения ошибка будет минимизироваться и в выходном тензоре будут получаться всё более и более осмысленные числа.

YOLO предсказывает несколько ббоксов для каждой области изображения. Во время обучения, мы хотим, чтобы только один ббокс был ответственным за каждый объект. Ответственным выбирается тот, у кого самый большой IoU c Ground Truth (истинным ббоксом из разметки)
Далее в статье показываются метрики, по которым видно, насколько YOLO быстрее, чем Fast R‑CNN и какая у неё хорошая метрика mAP на валидационном датасете VOC 2007. Интересно заметить, что в таблицах показаны метрики для комбинированной модели YOLO + Fast R‑CNN, которые дают хороший результат по качеству.
YOLOv2 (YOLO9000)
На волне хайпа уже в следующем году Joseph Redmon публикует улучшение YOLOv1 в этой статье. Название 9000 говорит о том, что модель способна отличить аж 9к классов! При этом оставаясь достаточно качественной и быстрой.
Главным недостатком предыдущей модели были ошибки локализации + маленький recall, по сравнению с двухстадийными детекторами. Относительно маленький recall значит, что модель часто вообще не видит объект там, где его видит, например, Fast R‑CNN. Поэтому основная задача — это улучшить геометрическую точность предсказания ббоксов и recall.
Anchor Boxes
Помимо всяких улучшений за счет батч норма, увеличения разрешения и тп, вводится очень важное архитектурное изменение, заключающееся в добавлении Anchor Boxes (я не знаю как это перевести на русский). Ранее YOLO предсказывала координаты с нуля, прямо из полносвязного слоя. Как мы помним, двухстадийные детекторы предварительно имели целый набор гипотез для ббоксов. Как показывает практика, модели гораздо проще предсказывать поправки к наперед заданным ббоксам, чем с нуля их строить. Поэтому в YOLOv2 решили взять хорошую идею из двухстадийных детекторов и использовать Anchor Boxes! (Возможно такая идея пришла как раз после работы с комбинированной моделью YOLOv1 + Fast R‑CNN). Такое нововведение также позволило предсказывать гораздо больше ббоксов.
После добавления Anchor Boxes в YOLO появилась проблема, заключающаяся в том, что размеры боксов подбираются вручную. Решением проблемы стало использование k‑means clustering для автоматической генерации гипотез. По сути дефолтные боксы (Anchor Boxes) в YOLO генерируются на основе конкретного датасета, на котором вы хотите обучать модель.
Другая проблема заключалась в том, что на ранних итерациях обучения, нейронке довольно сложно предсказывать (x, y), поскольку изначально её веса рандомные, и смещение дефолт бокса может быть произвольным, т. е. он может вообще уплыть в любую часть изображения. Чтобы этого избежать, YOLOv2 вместо предсказания поправ��к, предсказывает относительные координаты внутри grid cell (элемент сетки, которая "делит" изображение), и коэффициенты для поправок к ширине и высоте.
Сеть выдаёт по прежнему 5 чисел для каждого ббокса:
Пусть — это смещение координат левого верхнего угла grid cell (это надо для расчета абсолютных координат, тк ранее мы говорили, что предсказывать будем относительные координаты)
Пусть — это ширина и высота дефолт бокса
Тогда финальные координаты предсказанного бокса считаются следующим образом:
Выглядит сложно на первый взгляд, но по сути модель просто КАК‑ТО предсказала свои 5 чисел, потом для координат мы используем функцию активации , у которой область значений
, а далее прибавили координаты левого угла grid cell, чтобы перейти к абсолютным координатам. Ширину и высоту дефолт бокса мы просто домножили на коэффициенты, зависящие от параметров модели. Важно помнить, что это мы сами так ввели и определили вычисление координат ббоксов, а дальше оно само уже под капотом обучится.
Детекция объектов на маленьких участках изображения
Чтобы находить объекты разного масштаба, нам надо иметь несколько Feature Map. Как я уже говорил в начале этой статьи: есть Low-Level Features и High-Level Features. Так вот, чтобы находить мелкие объекты - нам нужен Low-Level Feature Map. В YOLOv2 используется passthrough layer, который конкатенирует разные Feature Map, предварительно приводя их к одному размеру.
Немного конкретнее: в YOLOv2 после первых сверток есть Feature Map размера , который содержит в себе низкоуровневую информацию. После последних сверток у нас есть Feature Map размера
. Чтобы их объединить, нам надо сделать reshape для низкоуровневых фичей:
и теперь можно объединять с High-Level Features
Новый Feature extractor
В YOLOv2 поменяли backbone, теперь в основе лежит сеть Darknet-19. Она чем-то похожа на VGG, тк в основном там фильтры 3x3, но она не такая толстая и ресурсозатратная. Думаю про Darknet-19 читатель может отдельно почитать, если возник такой интерес🤓
В целом, это все основные изменения в архитектуре YOLOv2
YOLOv3
Свою третью последнюю статью про YOLO Joseph Redmon начинает многообещающе со следующих слов:
I didn’t do a whole lot of research this year. Spent a lot of time on Twitter. Played around with GANs a little. I had a little momentum left over from last year. I managed to make some improvements to YOLO. But, honestly, nothing like super interesting, just a bunch of small changes that make it better
Во-первых, YOLOv3 теперь может работать уже на ТРЕХ разных скейлах. Это значит, что информация извлекается из 3-х разных Feature Map. Вдобавок в статье уже ссылаются на Feature Pyramid Networks. По сути эти "пирамиды" описывают способ, которым можно объединить Feature Map с разных уровней сверток. Вот пример того, как он работает:
import torch
import torch.nn as nn
# 3 Feature Map'ы, которые мы хотим объединить
high_feature_map = torch.rand(1, 512, 13, 13) # high level features
mid_feature_map = torch.rand(1, 256, 26, 26) # middle level features
low_feature_map = torch.rand(1, 128, 52, 52) # low level featuresНужно, чтобы у всех тензоров было одинаковое число каналов, например, 64 (это 2-я компонента тензора). Сделаем это при помощи сверток с единичным ядром:
# Сделаем для каждого тензора 64 канала
h_conv = nn.Conv2d(512, 64, 1) # Свертка для high level feature map
m_conv = nn.Conv2d(256, 64, 1) # Свертка для middle level feature map
l_conv = nn.Conv2d(128, 64, 1) # Свертка для low level feature map
high_feature_map = h_conv(high_feature_map)
mid_feature_map = m_conv(mid_feature_map)
low_feature_map = l_conv(low_feature_map)
print(high_feature_map.shape) # torch.Size([1, 64, 13, 13])
print(high_feature_map.shape) # torch.Size([1, 64, 26, 26])
print(high_feature_map.shape) # torch.Size([1, 64, 52, 52])Отлично! Теперь надо сложить high_feature_map и mid_feature_map. Для этого сделаем high_feature_map такого же размера при помощи nn.Upsample
upsample = nn.Upsample(scale_factor=2, mode='nearest')
high_feature_map = upsample(high_feature_map)
print(high_feature_map.shape) # torch.Size([1, 64, 26, 26])
high_mid_feature_map = torch.add(high_feature_map, mid_feature_map)Теперь аналогично делаем Upsampling для high_mid_feature_map, складываем его с low_feature_map и получаем тензор, который содержит информацию c 3-х разных scale. Таким образом YOLOv3 может видеть ещё более мелкие объекты на изображении.
Darknet-19 ---> Darknet-53 🤯
В YOLOv3 поменялся Feature extractor, теперь там 53 сверточных слоя, отсюда и название)) Cеть стала значительно глубже, что позволило улучшить точность. В Darknet-53 встречаются Residual блоки, аналогичные тем, что используются в сети ResNet. Эти блоки позволяют эффективно передавать информацию через слои, что способствует обучению более глубоких нейронных сетей. Модель обычно предварительно обучается на больших наборах данных (например, ImageNet), а затем дообучается на конкретной задаче обнаружения объектов. Она является ключевым компонентом YOLOv3, обрабатывая входные изображения и извлекая признаки, которые затем используются для обнаружения объектов на изображениях.
Заключение
Сам создатель YOLO Joseph Redmon опубликовал 3 статьи, которые тут и описываются. Дальнейшие улучшения уже делали его последователи в YOLOv4, YOLOv5, YOLOv6 и тд. Тем не менее основные идеи и дальнейший вектор развития был заложен именно в первых трех версиях. YOLO сейчас широко используется для задач обнаружения объектов в реальном времени. Например, обработка кадров с видеопотока мобильного устройства, умных камер и тд. Следующие статьи думаю будут больше про практику и применение / внедрение моделей :)
