Привет, Хабр! Представляю вашему вниманию перевод статьи "Implementing RoI Pooling in TensorFlow + Keras" автора Jaime Sevilla. В данный момент я прохожу курс машинного обучения. В учебном блоке "Компьютерное зрение" возникла необходимость в изучении RoI Pooling слоёв. Приведённая ниже статья мне показалась интересной, в связи с чем я решил поделиться переводом с сообществом. В этом посте мы объясним основную концепцию и общее использование RoI pooling (Region of Interest — область интересов) и предоставим реализацию с использованием слоев Keras среды TensorFlow. Целевая аудитория этого поста — люди, знакомые с базовой теорией (сверточных) нейронных сетей (CNN) и способные создавать и запускать простые модели с использованием Keras. Если вы здесь только для кода, обратитесь сюда и не забудьте поставить лайк и поделиться статьей! RoI Pooling был предложен Россом Гиршиком в статье Fast R-CNN как часть его пайплайна распознавания объектов. В общем случае использования для RoI Pooling у нас есть подобный изображению объект и несколько областей интереса (RoI — regions of interest), указанных через ограничивающие рамки. Мы хотим создать эмбеддинги (embedding (вложения) — сопоставления произвольной сущности (кусочка картинки) некоторому вектору) из каждого RoI. Например, в настройке R-CNN у нас есть изображение и механизм выделения регионов-кандидатов, который создает ограничивающие рамки для потенциально интересных частей изображения. Теперь мы хотим создать эмбеддинг для каждого предложенного кусочка изображения.  Простое обрезание каждой предложенной области не сработает, потому что мы хотим наложить полученные эмбеддинги друг на друга, и предлагаемые области не обязательно имеют одинаковую форму! Поэтому нам нужно придумать способ трансформации каждого предложенного кусочка изображения таким образом, чтобы он приводил к созданию эмбеддинга заранее определенной размерности. Как мы можем сделать это? В задачах компьютерного зрения стандартным способом уменьшения формы изображений является использование какой-либо операции объединения (pooling). Наиболее распространенным является вариант max pooling, где мы делим входное изображение на (обычно не перекрывающиеся) области одинаковой формы и формируем выход, беря максимальное значение, найденное в каждой области.  Это напрямую не решает проблему, которая у нас есть – кусочки изображения различного размера будут разделяться блоками фиксированной размерности: эмбеддинги будут переменной размерности. Но этот подход наталкивает на идею. Что если мы разделим каждую интересующую RoI на одно и то же количество областей различной формы и возьмем максимум каждой из них?  И это именно то, что делает слой ROI Pooling. RoI Pooling — довольно общий инструмент. Как правило, он выходит на свет в сочетании с механизмами выделения кандидатов RoI, устраняя разрыв между выделением кандидатов и их эмбеддингами. Мы увидим два конкретных примера, иллюстрирующих его потенциал.
Во-первых, в контексте распознавания объектов (для которого RoI Pooling был разработан), он позволяет нам разделить пайплайн задач на две части (выделение кандидатов и классификация регионов), сохраняя при этом сквозную (end-to-end) однопроходную (single-pass) дифференцируемую архитектуру.  Таким образом, в модели R-CNN у нас сначала есть компонент в модели, который выделяет на изображении фиксированное количество областей интереса (RoI). RoI Pooling позволяет нам запускать классификатор CNN для всех предложенных областей. В качестве ответа алгоритм выбирает регион с максимальной вероятностью целевого класса.
Во-вторых, вместе с выделением регионов-кадидатов, RoI Pooling также может быть использован для реализации визуального внимания (visual attention).  В качестве примера в Attentional Network для визуальной детекции обьектов мы видим, как Hara другие соавторы реализовали механизм attention, используя итеративные выделения кандидатов ROI и ROI Pooling. Сначала алгоритм генерирует первого кандидата (t = 1), которого ROI Pooling адаптирует к полносвязным слоям (Fully Connected). Результат используется в качестве входных данных для компонента Glimpse (представление) (t = 2) для генерации нового кандидата, который так же подвергается эмбеддингу с помощью ROI Pooling. Процесс повторяется Т раз. Прежде чем мы углубимся в реализацию, остановимся на минуту, чтобы подумать о типе сигнатуры слоя ROI. Он принимает два тензора: На выходе должен быть: Keras позволяет нам легко реализовывать пользовательские слои посредством наследования базового класса Layer. В документации tf.keras рекомендуется реализовать методы init, build и call для нашего самодельного слоя. Однако, поскольку целью функции build является добавление весов слоя, а наш слой не имеет весов, нам не нужно переопределять этот метод. Мы также реализуем удобный метод compute_output_shape. Мы будем писать код для каждой части отдельно, а в конце статьи объединим все куски. Конструктор класса довольно прост для понимания. Нам нужно указать целевую высоту и ширину эмбеддингов, которые мы создаём. В последней строке конструктора мы вызываем родительский конструктор для инициализации остальных атрибутов класса. compute_output_shape — это просто полезная служебная функция, которая скажет нам, каким будет выход слоя для определенного ввода. Далее мы должны реализовать вызов (call). Функция вызова — это место, где живет логика слоя. Эта функция должна принимать в качестве входных данных два тензора, которые содержат входные данные для слоя ROI Pooling, и выводить тензор с эмбеддингами. Прежде чем реализовать это, нам нужно реализовать более простую функцию, которая будет брать одно изображение и одну ROI и возвращать соответствующий эмбеддинг. Давайте сделаем это шаг за шагом. Первые шесть строк функции вычисляют, где интересующая область начинается и заканчивается в изображении. Мы выбрали в качестве соглашения, что координаты каждой ROI должны быть указаны в относительных величинах, как числа между 0 и 1. Конкретно, каждая ROI определяется 4-мерным тензором, содержащим четыре относительные координаты (x_min, y_min, x_max, y_max ).
Мы могли бы решить идентифицировать координаты каждой ROI в абсолютных значениях, но это, как правило, хуже: общей практикой считается варинт с пропусканимем входного изображения через некоторые свертки, которые изменяют размеры изображения, прежде чем подавать его в слой ROI Pooling, что заставит нас отслеживать, как изменяются размеры изображения, чтобы правильно масштабировать ограничивающие рамки ROI. Седьмая строка просто обрезает изображение до интересующей области с помощью удобного синтаксиса тензорных срезов, который предоставляет нам TensorFlow. В следующих четырех строках мы вычисляем размеры каждой области в ROI, которые будут объединены. После этого мы создаем 2D массив тензоров, где каждый компонент является кортежем, указывающим координаты начала и конца каждой из областей, в которых мы собираемся взять максимум. Код, который генерирует сетку координат сектора, кажется слишком сложным, но обратите внимание, что если мы просто разделим ROI на области с размерами (region_height // pooled_height, region_width // pooled_width), то будут некоторые пиксели ROI, которые не попадают ни в один из секторов. В результате получается 2D список ограничивающих рамок, и мы переходим к следующей части. Эти строки выше делают всю магию. Мы определяем вспомогательную функцию pool_area, которая принимает в качестве входных данных ограничивающую рамку, указанную кортежами, подобными тем, которые мы только что создали, и выводит максимум каждого канала в области.
Мы отображаем pool_area на каждую область, которую мы объявили, используя list comprehension . К этому моменту мы возвращаем тензор формы (pooled_height, pooled_width, n_channels), содержащий результат пуллинга одной RoI одного изображения. Следующий шаг — объединение множества RoI на одном изображении. Это легко реализовать с использованием вспомогательной функции и tf.map_fn для получения тензора размерами (n_rois, pooled_height, pooled_width, n_channels). Наконец, нам нужно реализовать итерацию на уровне батча. Если мы передадим в tf.map_fn последовательность тензоров (например, наш ввод x), то под капотом этой функции позаботились о том, чтобы нам зипанули батч. Обратите внимание, что мы должны указывать параметр dtype для tf.map_fn каждый раз, когда ожидаемый вывод не соответствует типу данных его ввода. Как правило, рекомендуется указывать его как можно чаще, чтобы четко указать, как типы изменяются в нашем графе вычислений Tensorflow. Давайте сложим все вместе: Давайте проверим нашу реализацию! Мы собираемся предположить, что 1-канальное изображение размером 100x200, и мы извлечем 2 RoI, используя патчи для пуллинга размером 7x3. Изображения могут иметь регионы, классифицированные максимум в 4 метках. Пример карты призноков — все 1, но одно значение 50 помещено в точку (высота-1, ширина-3). Вышеуказанные строки определяют тестовый вход для слоя, строят соответствующие тензоры и запускают сеанс TensorFlow, в ходе которого мы можем проверить работу алгоритма. Запуск скрипта приведет к следующему выводу: Мы можем проверить, что размер тензорна соответствуют нашим ожидаемым результатам. Полученные эмбеддинги — все 1, кроме той области, где мы ввели значение 50. Кажется, это работает! И это все на сегодня, ребята! Сегодня мы узнали, что делает ROI Pooling слой и как мы можем использовать его для реализации внимания (attention). Кроме того, мы узнали, как расширить Keras для реализации пользовательских слоёв без весов, и дали реализацию вышеупомянутого ROI Pooling слоя. Я надеюсь, что это было полезно для вас, не забудьте поделиться статьей и оставить комментарий, если это было! Спасибо Ari Brill, Tjark Miener и Bryan Kim за отзывы о статье. Ссылки машинное обучение