Выбрано из Medium, автором Ayoosh Kathuria, составлено Heart of the Machine.
Обнаружение объектов — это область, которая больше всего выиграла от недавнего развития глубокого обучения. С развитием технологий было разработано множество алгоритмов для обнаружения объектов, включая YOLO, SSD, Mask RCNN и RetinaNet. В этом руководстве мы будем использовать PyTorch для реализации детектора объектов на основе YOLO v3, который представляет собой алгоритм быстрого обнаружения объектов. Учебник состоит из пяти частей, и эта статья содержит первые три из них.
Последние несколько месяцев я работал над улучшением обнаружения объектов в своей лаборатории. Самым большим вдохновением, которое я получил от этого, было осознание того, что лучший способ научиться обнаружению объектов — это реализовать эти алгоритмы самостоятельно, к чему вас и приведет этот учебник.
В этом руководстве мы будем использовать PyTorch для реализации детектора объектов на основе YOLO v3, который представляет собой алгоритм быстрого обнаружения объектов.
Код, используемый в этом руководстве, должен работать на Python 3.5 и PyTorch 0.3. Вы можете найти весь код по следующей ссылке:
GitHub.com/Ayooo пролистните Тайху…
Этот учебник состоит из пяти частей:
1. Как работает YOLO
2. Создайте сетевую иерархию YOLO
3. Реализовать прямое распространение сети
4. Порог доверия к объектности и немаксимальное подавление
5. Проектирование входных и выходных конвейеров
Необходимые базовые знания
Прежде чем следовать этому руководству, вам необходимо знать:
- Как работают сверточные нейронные сети, включая остаточные блоки, пропуск соединений и повышение частоты дискретизации;
- Обнаружение объектов, регрессия ограничивающей рамки, IoU и немаксимальное подавление;
- Базовое использование PyTorch. Вам нужно уметь легко создавать простые нейронные сети.
Что такое ЙОЛО?
YOLO — это аббревиатура от You Only Look Once. Это детектор объектов, который использует функции, изученные глубокими свёрточными нейронными сетями, для обнаружения объектов. Прежде чем мы сможем писать код вручную, мы должны сначала понять, как работает YOLO.
Полностью сверточная нейронная сеть
YOLO использует только сверточные слои, что делает ее полностью сверточной нейронной сетью (FCN). Он имеет 75 сверточных слоев, а также пропускные соединения и слои повышения дискретизации. Он не использует какую-либо форму объединения и понижает выборку карт объектов, используя сверточные слои с шагом 2. Это помогает предотвратить потерю низкоуровневых функций, обычно вызываемую объединением.
Как FCN, YOLO не чувствителен к размеру входного изображения. Однако на практике нам могут понадобиться постоянные размеры входных данных, поскольку различные проблемы возникают только при реализации алгоритма.
Одна важная проблема заключается в том, что если мы хотим обрабатывать изображения пакетами (пакеты изображений обрабатываются графическим процессором параллельно, что увеличивает скорость), нам необходимо зафиксировать высоту и ширину всех изображений. Это требует объединения нескольких изображений в один большой пакет (объединение множества тензоров PyTorch в один).
YOLO шаг за шагом повышает разрешение изображения. Например, если шаг сети равен 32, входное изображение размером 416×416 создаст выходное изображение 13×13. В общем, произвольный шаг на сетевом уровне относится к входу слоя, деленному на вход.
Интерпретировать вывод
Обычно (как и в случае со всеми детекторами объектов) признаки, изученные сверточным слоем, передаются классификатору/регрессору для прогнозирования (координаты ограничивающих рамок, метки классов и т. д.).
В YOLO предсказание выполняется через сверточный слой (помните, это полностью сверточная нейронная сеть!), основные размеры которого:
1×1×(В×(5+С))
Теперь первое, на что следует обратить внимание, это то, что наш вывод — это карта объектов. Поскольку мы использовали свертку 1 × 1, размер карты предсказания точно равен размеру предыдущей карты признаков. В YOLO v3 (и более новых версиях) карта предсказания — это просто каждая ячейка, которая может предсказывать фиксированное количество ограничивающих прямоугольников.
Хотя правильным термином для описания ячейки на карте объектов был бы «нейрон», в этой статье мы будем называть ее ячейкой для ясности.
Что касается глубины, то на карте признаков имеется (B x (5 + C))* записей. B представляет количество ограничивающих рамок, которые может предсказать каждая ячейка. Согласно документу YOLO, каждая из этих ограничивающих рамок B может быть специализирована для обнаружения какого-либо объекта. Каждая ограничивающая рамка имеет атрибуты 5+C, которые описывают координаты центра каждой ограничивающей рамки, размеры, показатель объектности и достоверность класса C. YOLO v3 предсказывает 3 ограничивающих прямоугольника в каждой ячейке.
Если центр объекта находится в пределах рецептивного поля ячейки, вы хотели бы, чтобы каждая ячейка карты объектов предсказывала объект через одну из ограничивающих рамок. (Рецептивное поле — это область, в которой входное изображение видно клетке.)
Это связано с тем, что YOLO обучается только с одной ограничивающей рамкой, отвечающей за обнаружение любого заданного объекта. Во-первых, мы должны определить, к какой ячейке принадлежит этот ограничивающий прямоугольник.
Поэтому нам нужно разрезать входное изображение на сетку с размерами, равными конечной карте объектов.
Давайте рассмотрим следующий пример, где размер входного изображения составляет 416 × 416, а шаг сети равен 32. Как упоминалось ранее, размер карты объектов будет 13×13. Впоследствии мы делим входное изображение на сетки 13×13.
Сетка на входном изображении, которая содержит центр поля объекта истинности, используется в качестве ячейки, ответственной за предсказание объекта. На изображении это ячейка, отмеченная красным, которая содержит центр основного поля истинности (отмечено желтым).
Красная ячейка теперь седьмая в седьмом ряду сетки. Теперь мы делаем седьмую ячейку в седьмой строке на карте признаков (соответствующую ячейку на карте признаков) единицей для обнаружения собак.
Теперь эта ячейка может предсказать три ограничивающих прямоугольника. Что будет присвоено метке правды собаки? Чтобы понять это, мы должны понять концепцию якорей.
Обратите внимание, что ячейки, о которых мы здесь говорим, являются ячейками на карте прогнозируемых объектов, и мы разбиваем входное изображение на сетки, чтобы определить, какая ячейка карты прогнозируемых объектов отвечает за прогнозирование объекта.
Якорная коробка
Предсказание ширины и высоты ограничивающего прямоугольника выглядит очень разумно, но на практике обучение вводит нестабильные градиенты. Таким образом, большинство детекторов объектов в наши дни предсказывают преобразование логарифмического пространства или смещение между предсказанием и предварительно обученной ограничивающей рамкой по умолчанию (то есть точкой привязки).
Затем эти преобразования применяются к полям привязки для получения прогнозов. YOLO v3 имеет три привязки, поэтому каждая ячейка предсказывает 3 ограничивающих прямоугольника.
Возвращаясь к предыдущему вопросу, якорь, который отвечает за обнаружение ограничивающей рамки собаки, имеет самый высокий IoU и имеет наземную истинную область.
предсказывать
Приведенная ниже формула описывает, как выходные данные сети преобразуются для получения прогнозов ограничивающей рамки.
Координаты центра
Примечание. Мы используем сигмовидную функцию для прогнозирования координат центра. Это делает выходное значение между 0 и 1. Причины следующие:
Обычно YOLO не предсказывает точные координаты центра ограничивающей рамки. Он предсказывает:
- смещение относительно левого верхнего угла ячейки сетки цели предсказания;
- Смещение нормализовано с использованием измерения (1) единицы карты объектов.
Возьмем, к примеру, наше изображение. Если прогноз центра равен (0,4, 0,7), координаты центра на карте признаков 13 x 13 равны (6,4, 6,7) (верхний левый угол красной ячейки равен (6,6)).
Однако, если предсказанные координаты x,y больше 1, например (1.2, 0.7). Тогда координаты центра равны (7.2, 6.7). Обратите внимание, что центр находится в ячейке справа от красной ячейки или в 8-й ячейке в строке 7. Это нарушает теорию, лежащую в основе YOLO, потому что если мы предположим, что красный прямоугольник отвечает за предсказание целевой собаки, то центр собаки должен быть в красной ячейке, а не в ячейке сетки рядом с ней.
Поэтому, чтобы решить эту проблему, мы выполняем сигмовидную функцию на выходе, сжимая выход в интервал от 0 до 1, эффективно гарантируя, что центр находится в ячейке сетки, где выполняется прогноз.
Размеры ограничивающей рамки
Мы выполняем преобразование логарифмического пространства на выходе и умножаем опорные точки, чтобы предсказать размеры ограничивающей рамки.
Результирующие прогнозы bw и bh нормализуются с использованием высоты и ширины изображения. То есть, если предсказанные bx и by коробки, содержащей цель (собаку), равны (0,3, 0,8), то фактическая ширина и высота карты признаков 13 x 13 равны (13 x 0,3, 13 x 0,8).
Оценка объективности
Оценка объекта представляет собой вероятность того, что объект находится внутри ограничивающей рамки. Оценка объекта для красной сетки и соседних сеток должна быть близка к 1, в то время как оценка объекта для сеток в углах может быть близка к 0.
При расчете показателя объектности также используется сигмовидная функция, поэтому его можно понимать как вероятность.
доверие класса
Достоверность класса представляет собой вероятность того, что обнаруженный объект принадлежит классу (например, собака, кошка, банан, автомобиль и т. д.). До версии 3 YOLO требовалось, чтобы функция softmax работала с оценками класса.
Однако YOLO v3 отказался от этой конструкции, и автор решил использовать сигмовидную функцию. Потому что предпосылка выполнения операции softmax с оценками категорий заключается в том, что категории являются взаимоисключающими. Короче говоря, если объект принадлежит к одному классу, необходимо убедиться, что он не принадлежит к другому классу. Это верно для набора данных COCO, где мы установили детектор. Однако это предположение не работает при наличии категорий «Женщины» и «Человек». Вот почему авторы решили не использовать функцию активации Softmax.
Прогнозирование в разных масштабах
YOLO v3 делает прогнозы в 3 разных масштабах. Слой обнаружения используется для выполнения прогнозов на трех картах объектов разного размера с шагом карты объектов 32, 16 и 8 соответственно. Это означает, что при размере входного изображения 416 x 416 мы выполняем обнаружение в масштабах 13 x 13, 26 x 26 и 52 x 52.
Сеть выполняет субдискретизацию входного изображения перед первым слоем обнаружения, который выполняет обнаружение с использованием карт объектов из слоев с шагом 32. Затем, после выполнения апсемплинга с коэффициентом 2, она объединяется с картой признаков предыдущего слоя (карта признаков имеет тот же размер). Другое обнаружение выполняется в слое с шагом 16. Тот же шаг повышения частоты дискретизации повторяется, и последнее обнаружение выполняется в слое с шагом 8.
В каждом масштабе каждый модуль предсказывает 3 ограничивающих прямоугольника с использованием 3 привязок, а общее количество привязок равно 9 (разные привязки в разных масштабах).
Авторы говорят, что это помогает YOLO v3 достичь большей производительности при обнаружении объектов меньшего размера, на что часто жаловались предыдущие версии YOLO. Повышение дискретизации может помочь сети изучить детализированные функции, которые помогают обнаруживать более мелкие объекты.
обработка вывода
Для изображения размером 416 x 416 YOLO предсказывает ((52 x 52) + (26 x 26) + 13 x 13)) x 3 = 10647 ограничивающих рамок. Однако в нашем примере есть только один объект — собака. Итак, как мы можем уменьшить количество обнаружений с 10647 до 1?
Пороговое значение достоверности объекта: во-первых, мы фильтруем ограничивающие рамки на основе их оценок объектности. Как правило, ограничивающие рамки с оценками ниже порогового значения игнорируются.
Немаксимальное подавление: Немаксимальное подавление (NMS) решает проблему множественных обнаружений на одном изображении. Например, 3 ограничивающих прямоугольника красной ячейки сетки могут обнаруживать прямоугольник, или соседние сетки могут обнаруживать один и тот же объект.
выполнить
YOLO может обнаруживать только объекты, принадлежащие классам в наборе данных, используемом для обучения. Наш детектор будет использовать официальный файл весов, полученный путем обучения сети на наборе данных COCO, поэтому мы можем обнаружить 80 классов объектов.
На этом первая часть урока заканчивается. В этом разделе подробно объясняется алгоритм YOLO. Если вы хотите подробно изучить, как работает YOLO, процесс обучения и предотвращение производительности с другими детекторами, прочитайте оригинальную статью:
1. YOLO V1: You Only Look Once: Unified, Real-Time Object Detection (АР Вест V.org/PDF/1506.02…)
2. YOLO V2: YOLO9000: Better, Faster, Stronger (АР Вест V.org/PDF/1612.08…)
3. YOLO V3: An Incremental Improvement (Семья P Eddie.com/Media/files…)
4. Convolutional Neural Networks (На данный момент 231 you.GitHub.IO/convolution…)
5. Bounding Box Regression (Appendix C) (АР Вест V.org/PDF/1311.25…)
6. IoU (Woohoo.YouTube.com/watch?V=D NE…)
7. Non maximum suppresion (Woohoo.YouTube.com/watch?V=A46…)
8. PyTorch Official Tutorial (py torch.org/tutorials/ нет…)
Часть 2: Создание сетевой иерархии YOLO
Ниже приведена вторая часть руководства по реализации детектора YOLO v3 с нуля. Мы будем использовать PyTorch для реализации слоев YOLO, основных строительных блоков всей модели, на основе базовых концепций, описанных ранее.
Эта часть требует, чтобы читатель имел общее представление о том, как и как работает YOLO, а также базовые знания о PyTorch, например, о том, как создавать собственные архитектуры нейронных сетей с помощью таких классов, как nn.Module, nn.Sequential и torch.nn. .параметр.
начать путешествие
Сначала создайте папку для кода детектора, затем создайте файл Python darknet.py. Даркнет — это среда для построения базовой архитектуры YOLO, этот файл будет содержать весь код для реализации сети YOLO. Также нам нужно добавить файл с именем util.py, который будет содержать различные функции, которые нужно вызывать. После сохранения всех этих файлов в папке детектора мы можем использовать git для отслеживания их изменений.
конфигурационный файл
Официальный код (написанный на C) использует файл конфигурации для построения сети, файл cfg, который по частям описывает архитектуру сети. Если вы использовали серверную часть caffe, это файл .protxt, описывающий сеть.
Мы будем строить сеть, используя официальный файл cfg, который опубликован автором YOLO. Мы можем скачать его по следующему адресу и поместить в папку cfg в каталоге детектора.
Загрузка файла конфигурации:GitHub.com/P Family Eddie/Big…
Конечно, если вы используете Linux, вы можете сначала перейти в сетевой каталог детектора и запустить следующую командную строку.
mkdir cfg
cd cfg
wget https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg
Если вы откроете файл конфигурации, вы увидите некоторые сетевые архитектуры, подобные этой:
[convolutional]
batch_normalize=1
filters=64
size=3
stride=2
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=32
size=1
stride=1
pad=1
activation=leaky
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
[shortcut]
from=-3
activation=linear
Мы видим, что выше есть четыре блока конфигурации, 3 из которых описывают сверточные слои, а последний описывает слои ярлыков или пропущенные соединения, обычно используемые в ResNet. Вот 5 уровней, используемых в YOLO:
1. Сверточный слой
[convolutional]
batch_normalize=1
filters=64
size=3
stride=1
pad=1
activation=leaky
2. Пропустить соединение
[shortcut]
from=-3
activation=linear
Пропускные соединения аналогичны структуре, используемой в остаточных сетях, а параметр от -3 означает, что выходные данные слоя быстрого доступа будут получены путем добавления карт объектов выходных данных предыдущего слоя и выходных данных третьего слоя перед на вход модуля.
3. Повышение частоты дискретизации
[upsample]
stride=2
Билинейно повышает дискретизацию карты объектов в предыдущих слоях с помощью параметра шага.
4. Слой маршрутизации (Route)
[route]
layers = -4
[route]
layers = -1, 61
Уровень маршрутизации нуждается в некотором объяснении, а его уровни параметров имеют одно или два значения. Когда есть только одно значение, он выводит карту объектов этого слоя, индексированную этим значением. В наших экспериментах установлено значение -4, поэтому слой будет выводить карту объектов четвертого слоя перед слоем маршрутизации.
Когда иерархия имеет два значения, она возвращает объединенную карту объектов, индексированную этими двумя значениями. -1 и 61 в наших экспериментах, поэтому этот слой будет выводить карты объектов из предыдущего слоя (-1) в 61-й слой и объединять их по глубине.
5.YOLO
[yolo]
mask = 0,1,2
anchors = 10,13, 16,30, 33,23, 30,61, 62,45, 59,119, 116,90, 156,198, 373,326
classes=80
num=9
jitter=.3
ignore_thresh = .5
truth_thresh = 1
random=1
Уровень YOLO соответствует описанному выше уровню обнаружения. Параметр привязки определяет 9 наборов привязок, но это только привязки, индексированные атрибутом, используемым тегом маски. Здесь значения маски 0, 1, 2 указывают на первый, второй и третий используемые якоря. Маска указывает, что каждый блок в слое обнаружения предсказывает три блока. В целом, наш уровень обнаружения имеет шкалу 3 и собирает в общей сложности 9 якорей.
Net
[net]
# Testing
batch=1
subdivisions=1
# Training
# batch=64
# subdivisions=16
width= 320
height = 320
channels=3
momentum=0.9
decay=0.0005
angle=0
saturation = 1.5
exposure = 1.5
hue=.1
В файле конфигурации есть еще одна блочная сеть, но я не думаю, что это слой, потому что он описывает только соответствующую информацию о входных данных сети и параметрах обучения и не используется для прямого распространения YOLO. Однако он предоставляет нам такую информацию, как размер входных данных сети, которую можно использовать для настройки привязок в прямом проходе.
Разобрать файл конфигурации
Прежде чем мы начнем, давайте добавим необходимые импорты вверху файла darknet.py.
from __future__ import division
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import numpy as np
Мы определяем функцию parse_cfg, которая принимает на вход путь к файлу конфигурации.
def parse_cfg(cfgfile):
"""
Takes a configuration file
Returns a list of blocks. Each blocks describes a block in the neural
network to be built. Block is represented as a dictionary in the list
"""
Идея здесь состоит в том, чтобы разобрать cfg, сохранив каждый фрагмент в виде словаря. Свойства и значения этих блоков хранятся в словарях в виде пар ключ-значение. Во время синтаксического анализа мы добавляем эти словари (представленные блоком переменных в коде) в блоки списка. Наша функция вернет этот блок.
Начнем с сохранения содержимого файла конфигурации в виде списка строк. Следующий код выполняет предварительную обработку этого списка:
file = open(cfgfile, 'r')
lines = file.read().split('\n') # store the lines in a list
lines = [x for x in lines if len(x) > 0] # get read of the empty lines
lines = [x for x in lines if x[0] != '#'] # get rid of comments
lines = [x.rstrip().lstrip() for x in lines] # get rid of fringe whitespaces
Затем мы перебираем предварительно обработанный список и получаем фрагменты.
block = {}
blocks = []
for line in lines:
if line[0] == "[": # This marks the start of a new block
if len(block) != 0: # If block is not empty, implies it is storing values of previous block.
blocks.append(block) # add it the blocks list
block = {} # re-init the block
block["type"] = line[1:-1].rstrip()
else:
key,value = line.split("=")
block[key.rstrip()] = value.lstrip()
blocks.append(block)
return blocks
Создание строительных блоков
Теперь мы будем использовать список, возвращенный parse_cfg выше, для создания модуля PyTorch в качестве стандартного блока в файле конфигурации.
В списке 5 типов слоев. PyTorch предоставляет готовые слои для свертки и повышения частоты дискретизации. Для остальных слоев мы напишем собственные модули, расширив класс nn.Module.
Функция create_modules использует список блоков, возвращаемый функцией parse_cfg:
def create_modules(blocks):
net_info = blocks[0] #Captures the information about the input and pre-processing
module_list = nn.ModuleList()
prev_filters = 3
output_filters = []
Прежде чем перебирать список, мы определяем переменную net_info для хранения информации о сети.
nn.ModuleList
Наша функция вернет nn.ModuleList. Этот класс почти эквивалентен обычному списку, содержащему объекты nn.Module. Однако при добавлении nn.ModuleList в качестве члена объекта nn.Module (т. е. когда мы добавляем модули в нашу сеть) параметры всех объектов (модулей) nn.Module внутри nn.ModuleList также добавляются как nn. объекта Module (то есть нашей сети, которая добавляет nn.ModuleList в качестве своего члена).
Когда мы определяем новый слой свертки, мы должны определить его ядерное измерение. При этом высота и ширина ядра свертки определяются файлом cfg, а глубина ядра свертки определяется количеством ядер на одну свертка (фиг. особенность или глубина). Это означает, что нам нужно отслеживать количество примененных слоев свертки ядра свертки. Мы используем переменную prev_filter. Мы инициализировали значением 3, поскольку у изображения, соответствующего каналам RGB, есть три канала.
Слой маршрута получает карты объектов (возможно, объединенные) из предыдущих слоев. Если после слоя маршрутизации есть сверточный слой, то ядро свертки будет применено к карте признаков предыдущего слоя, а именно к карте признаков, полученной слоем маршрутизации. Поэтому нам нужно отслеживать не только количество ядер свертки в предыдущем слое, но и каждый предыдущий слой. По мере выполнения итерации мы добавляем количество выходных фильтров для каждого модуля в список output_filters.
Теперь идея состоит в том, чтобы перебрать список модулей и создать модуль PyTorch для каждого.
for index, x in enumerate(blocks[1:]):
module = nn.Sequential()
#check the type of block
#create a new module for the block
#append to module_list
Класс nn.Sequential используется для последовательного выполнения нескольких объектов nn.Module. Если вы посмотрите на файл cfg, вы обнаружите, что модуль может содержать более одного слоя. Например, модуль сверточного типа имеет слой пакетной нормализации, слой активации ReLU с утечкой и сверточный слой. Мы объединяем эти слои, используя nn.Sequential, чтобы получить функцию add_module. Например, ниже показан пример того, как мы создаем сверточные слои и слои с повышением дискретизации:
if (x["type"] == "convolutional"):
#Get the info about the layer
activation = x["activation"]
try:
batch_normalize = int(x["batch_normalize"])
bias = False
except:
batch_normalize = 0
bias = True
filters= int(x["filters"])
padding = int(x["pad"])
kernel_size = int(x["size"])
stride = int(x["stride"])
if padding:
pad = (kernel_size - 1) // 2
else:
pad = 0
#Add the convolutional layer
conv = nn.Conv2d(prev_filters, filters, kernel_size, stride, pad, bias = bias)
module.add_module("conv_{0}".format(index), conv)
#Add the Batch Norm Layer
if batch_normalize:
bn = nn.BatchNorm2d(filters)
module.add_module("batch_norm_{0}".format(index), bn)
#Check the activation.
#It is either Linear or a Leaky ReLU for YOLO
if activation == "leaky":
activn = nn.LeakyReLU(0.1, inplace = True)
module.add_module("leaky_{0}".format(index), activn)
#If it's an upsampling layer
#We use Bilinear2dUpsampling
elif (x["type"] == "upsample"):
stride = int(x["stride"])
upsample = nn.Upsample(scale_factor = 2, mode = "bilinear")
module.add_module("upsample_{}".format(index), upsample)
Слой маршрутизации/слой ярлыков
Далее давайте напишем код для создания слоя маршрута и слоя ярлыка:
#If it is a route layer
elif (x["type"] == "route"):
x["layers"] = x["layers"].split(',')
#Start of a route
start = int(x["layers"][0])
#end, if there exists one.
try:
end = int(x["layers"][1])
except:
end = 0
#Positive anotation
if start > 0:
start = start - index
if end > 0:
end = end - index
route = EmptyLayer()
module.add_module("route_{0}".format(index), route)
if end < 0:
filters = output_filters[index + start] + output_filters[index + end]
else:
filters= output_filters[index + start]
#shortcut corresponds to skip connection
elif x["type"] == "shortcut":
shortcut = EmptyLayer()
module.add_module("shortcut_{}".format(index), shortcut)
Код, создающий уровень маршрутизации, нуждается в пояснении. Сначала мы извлекаем значение атрибута слоя, представляем его как целое число и сохраняем в списке.
Затем мы получаем новый слой с именем EmptyLayer, который, как следует из названия, является пустым слоем.
route = EmptyLayer()
Он определяется следующим образом:
class EmptyLayer(nn.Module):
def __init__(self):
super(EmptyLayer, self).__init__()
Подождите, пустой слой?
Теперь пустой слой может сбивать с толку, потому что он ничего не делает. И слой маршрута, как и любой другой слой, что-то сделает (получит конкатенацию предыдущего слоя). В PyTorch, когда мы определяем новый слой, мы записываем операцию прямой функции слоя в объекте nn.Module в подклассе nn.Module.
Для проектирования слоя в модуле Route мы должны создать объект nn.Module, который инициализируется как член Layers. Затем мы можем написать код, который сшивает карты объектов в прямой функции и передает их вперед. Наконец, мы выполняем этот уровень некоторой прямой функции сети.
Но код для операции конкатенации довольно короткий и простой (вызов torch.cat на карте объектов), и разработка слоя, подобного описанному выше процессу, привела бы к ненужной абстракции и увеличению объема шаблонного кода. Вместо этого мы можем поместить фиктивный слой вместо слоя маршрутизации, предложенного ранее, и выполнить операцию конкатенации непосредственно в прямой функции объекта nn.Module, представляющего даркнет. (Если вы запутались, я предлагаю вам прочитать об использовании класса nn.Module в PyTorch).
Сверлюционный слой после того, как слой маршрутизации применит свое ядро свертывания на карту функции (возможно, объединенной) предыдущего слоя. Следующий код обновляет переменную фильтров для удержания количества вывода фильтров по слою маршрутизации.
if end < 0:
#If we are concatenating maps
filters = output_filters[index + start] + output_filters[index + end]
else:
filters= output_filters[index + start]
Слой ярлыков также использует пустой слой, потому что он также выполняет очень простую операцию (добавление). Нет необходимости обновлять переменную фильтров, потому что она просто добавляет карты объектов из предыдущего слоя в более поздние слои.
YOLO-слой
Наконец, мы напишем код для создания слоя YOLO:
#Yolo is the detection layer
elif x["type"] == "yolo":
mask = x["mask"].split(",")
mask = [int(x) for x in mask]
anchors = x["anchors"].split(",")
anchors = [int(a) for a in anchors]
anchors = [(anchors[i], anchors[i+1]) for i in range(0, len(anchors),2)]
anchors = [anchors[i] for i in mask]
detection = DetectionLayer(anchors)
module.add_module("Detection_{}".format(index), detection)
Мы определяем новый слой DetectionLayer для хранения привязок, используемых для обнаружения ограничивающих прямоугольников.
Уровень обнаружения определяется следующим образом:
class DetectionLayer(nn.Module):
def __init__(self, anchors):
super(DetectionLayer, self).__init__()
self.anchors = anchors
В конце этого цикла мы делаем некоторую статистику (бухгалтерский учет).
module_list.append(module)
prev_filters = filters
output_filters.append(filters)
Это суммирует тело этого цикла. После функции create_modules мы получаем кортеж, содержащий net_info и module_list.
return (net_info, module_list)
тестовый код
Вы можете протестировать код, введя следующую командную строку после darknet.py, запустите файл.
blocks = parse_cfg("cfg/yolov3.cfg")
print(create_modules(blocks))
Вы увидите длинный список (точнее, 106 элементов) с элементами, которые выглядят следующим образом:
(9): Sequential(
(conv_9): Conv2d (128, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(batch_norm_9): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
(leaky_9): LeakyReLU(0.1, inplace)
)
(10): Sequential(
(conv_10): Conv2d (64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(batch_norm_10): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
(leaky_10): LeakyReLU(0.1, inplace)
)
(11): Sequential(
(shortcut_11): EmptyLayer(
)
)
Часть 3: Реализация прямого распространения сети
Во второй части мы реализовали слои, используемые в архитектуре YOLO. В этой части мы планируем реализовать сетевую архитектуру YOLO в PyTorch, чтобы мы могли генерировать выходные данные для данного изображения.
Наша цель — спроектировать прямое распространение сети.
предпосылки
- Прочтите первые две части этого руководства;
- основы PyTorch, в том числе создание пользовательских схем с использованием nn.Module, nn.Sequential и torch.nn.parameter;
- Обработка изображений в PyTorch.
определить сеть
Как упоминалось ранее, мы используем nn.Module для создания пользовательских схем в PyTorch. Здесь мы можем определить сеть для детектора. В файле darknet.py мы добавили следующие категории:
class Darknet(nn.Module):
def __init__(self, cfgfile):
super(Darknet, self).__init__()
self.blocks = parse_cfg(cfgfile)
self.net_info, self.module_list = create_modules(self.blocks)
Здесь мы создаем подкласс категории nn.Module и называем нашу категорию Darknet. Мы инициализируем сеть с элементами, блоками, net_info и module_list.
реализовать прямое распространение сети
Прямое распространение этой сети достигается путем переопределения прямого метода класса nn.Module.
forward имеет две основные цели. Во-первых, вычислить вывод; во-вторых, преобразовать карту признаков обнаружения вывода ранним способом (например, после преобразования эти карты обнаружения разных масштабов можно объединить, иначе добиться объединения будет невозможно из-за разных размерностей).
def forward(self, x, CUDA):
modules = self.blocks[1:]
outputs = {} #We cache the outputs for the route layer
Прямая функция принимает три аргумента: self, вход x и CUDA (если это правда, GPU используется для ускорения прямого распространения).
Здесь мы перебираем self.block[1:] вместо self.blocks, потому что первый элемент self.blocks — это сетевой блок, который не является частью прямого прохода.
Поскольку для слоя маршрутизации и слоя ярлыков требуются выходные карты объектов предыдущих слоев, мы кэшируем выходные карты объектов каждого слоя в выходных данных словаря. Ключ лежит в индексе слоя, а значение соответствует карте признаков.
Как и в случае с функцией create_module, теперь мы перебираем список модулей, который содержит модули сети. Обратите внимание, что эти модули добавляются в файле конфигурации в том же порядке. Это означает, что мы можем просто передать ввод через каждый модуль, чтобы получить вывод.
write = 0 #This is explained a bit later
for i, module in enumerate(modules):
module_type = (module["type"])
Слои свертки и повышающей дискретизации
Если модуль представляет собой слой свертки или повышающей дискретизации, прямой проход должен работать следующим образом:
if module_type == "convolutional" or module_type == "upsample":
x = self.module_list[i](x)
Слой маршрутизации/слой ярлыков
Если вы посмотрите на код уровня маршрутизации, мы должны проиллюстрировать два случая (как описано во второй части). В первом случае мы должны объединить две карты объектов, используя функцию torch.cat со вторым параметром, равным 1. Это потому, что мы хотим каскадировать карты объектов по глубине. (В PyTorch ввод и вывод сверточного слоя имеют формат `B X C X H X W. Глубина соответствует размеру канала).
elif module_type == "route":
layers = module["layers"]
layers = [int(a) for a in layers]
if (layers[0]) > 0:
layers[0] = layers[0] - i
if len(layers) == 1:
x = outputs[i + (layers[0])]
else:
if (layers[1]) > 0:
layers[1] = layers[1] - i
map1 = outputs[i + layers[0]]
map2 = outputs[i + layers[1]]
x = torch.cat((map1, map2), 1)
elif module_type == "shortcut":
from_ = int(module["from"])
x = outputs[i-1] + outputs[i+from_]
YOLO (уровень обнаружения)
Результатом YOLO является сверточная карта объектов, содержащая атрибуты ограничительной рамки по глубине карты объектов. Свойства ограничительной рамки прогнозируются по ячейкам, расположенным друг над другом. Поэтому, если вам нужно получить доступ ко второй границе ячейки в (5,6), вам нужно проиндексировать ее с помощью map[5,6, (5+C): 2*(5+C)] . Этот формат неудобен для обработки вывода, такой как пороговое значение по достоверности цели, добавление смещения сетки к центру, применение привязок и т. д.
Другая проблема заключается в том, что, поскольку обнаружение выполняется в трех масштабах, размеры прогнозного графа будут разными. Хотя размеры трех карт объектов различны, обработка выходных данных, выполняемая для них, аналогична. Было бы неплохо иметь возможность выполнять эти операции над одним тензором вместо трех отдельных тензоров.
Чтобы решить эти проблемы, мы вводим функцию predict_transform.
Преобразование вывода
Функция predict_transform импортируется в файл util.py, когда мы используем ее в форварде категории Darknet.
Добавьте импорт вверху util.py:
from __future__ import division
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.autograd import Variable
import numpy as np
import cv2
Predict_transform принимает 5 параметров: предсказание (наш вывод), inp_dim (размер входного изображения), привязки, num_classes, флаг CUDA (необязательно).
def predict_transform(prediction, inp_dim, anchors, num_classes, CUDA = True):
Функция predict_transform преобразует карту признаков обнаружения в двумерный тензор, где каждая строка тензора соответствует атрибутам ограничивающей рамки следующим образом:
Код, используемый для приведенного выше преобразования:
batch_size = prediction.size(0)
stride = inp_dim // prediction.size(2)
grid_size = inp_dim // stride
bbox_attrs = 5 + num_classes
num_anchors = len(anchors)
prediction = prediction.view(batch_size, bbox_attrs*num_anchors, grid_size*grid_size)
prediction = prediction.transpose(1,2).contiguous()
prediction = prediction.view(batch_size, grid_size*grid_size*num_anchors, bbox_attrs)
Размер якоря соответствует свойствам высоты и ширины блока NET. Эти атрибуты описывают размеры входного изображения, превышающие размер диаграммы обнаружения (торговцы из двух — это шаги). Следовательно, мы должны использовать ступенчатую сегментацию шагов функции обнаружения.
anchors = [(a[0]/stride, a[1]/stride) for a in anchors]
Теперь нам нужно преобразовать вывод в соответствии с формулой, рассмотренной в первой части.
Выполните операцию сигмовидной функции над координатами (x, y) и оценкой объектности.
#Sigmoid the centre_X, centre_Y. and object confidencce
prediction[:,:,0] = torch.sigmoid(prediction[:,:,0])
prediction[:,:,1] = torch.sigmoid(prediction[:,:,1])
prediction[:,:,4] = torch.sigmoid(prediction[:,:,4])
Добавьте смещение сетки к предсказанию координат центра:
#Add the center offsets
grid = np.arange(grid_size)
a,b = np.meshgrid(grid, grid)
x_offset = torch.FloatTensor(a).view(-1,1)
y_offset = torch.FloatTensor(b).view(-1,1)
if CUDA:
x_offset = x_offset.cuda()
y_offset = y_offset.cuda()
x_y_offset = torch.cat((x_offset, y_offset), 1).repeat(1,num_anchors).view(-1,2).unsqueeze(0)
prediction[:,:,:2] += x_y_offset
Примените опорные точки к размерам ограничивающей рамки:
#log space transform height and the width
anchors = torch.FloatTensor(anchors)
if CUDA:
anchors = anchors.cuda()
anchors = anchors.repeat(grid_size*grid_size, 1).unsqueeze(0)
prediction[:,:,2:4] = torch.exp(prediction[:,:,2:4])*anchors
Примените сигмовидную функцию активации к оценкам класса:
prediction[:,:,5: 5 + num_classes] = torch.sigmoid((prediction[:,:, 5 : 5 + num_classes]))
Наконец, нам нужно изменить размер карты обнаружения, чтобы она соответствовала размеру входного изображения. Свойства ограничивающей рамки основаны на размере карты объектов (например, 13 x 13). Если размер входного изображения 416 x 416, то мы умножаем атрибут на 32 или на переменную шага.
prediction[:,:,:4] *= stride
Часть цикла в основном заканчивается здесь.
В конце функции возвращается результат предсказания:
return prediction
Пересмотренный слой обнаружения
Мы преобразовали выходной тензор и теперь можем объединить карты обнаружения трех разных масштабов в один большой тензор. Обратите внимание, что это необходимо сделать после преобразования, поскольку вы не можете каскадировать карты объектов с разными пространственными измерениями. После преобразования наш выходной тензор отображает таблицу ограничивающих рамок в виде строк, и конкатенация становится более осуществимой.
Одно препятствие состоит в том, что мы не можем инициализировать пустой тензор, а затем каскадировать к нему непустой тензор (другой формы). Поэтому мы откладываем инициализацию коллектора (тензора, содержащего обнаружения) до тех пор, пока не будет получена первая карта обнаружения, а затем каскадируем эти карты обнаружения.
Обратите внимание, что write = 0 стоит перед циклом в функции forward. флаг записи указывает, столкнулись ли мы с первым обнаружением. Если запись равна 0, сборщик не был инициализирован. Если запись равна 1, коллектор уже инициализирован, и нам просто нужно соединить граф обнаружения с коллектором.
Теперь, когда у нас есть функция predict_transform, мы можем написать код для обработки обнаруженных карт объектов в прямой функции.
В верхней части файла darknet.py добавьте следующие импорты:
from util import *
Затем в прямой функции определите:
elif module_type == 'yolo':
anchors = self.module_list[i][0].anchors
#Get the input dimensions
inp_dim = int (self.net_info["height"])
#Get the number of classes
num_classes = int (module["classes"])
#Transform
x = x.data
x = predict_transform(x, inp_dim, anchors, num_classes, CUDA)
if not write: #if no collector has been intialised.
detections = x
write = 1
else:
detections = torch.cat((detections, x), 1)
outputs[i] = x
Теперь просто верните результат обнаружения.
return detections
Проверка прямого распространения
Функция ниже создаст поддельный ввод, который мы можем передать в нашу сеть. Прежде чем писать функцию, мы можем сохранить это изображение в рабочий каталог, используя следующую командную строку:
wget https://github.com/ayooshkathuria/pytorch-yolo-v3/raw/master/dog-cycle-car.png
Также можно загружать изображения напрямую:GitHub.com/Ayooo пролистните Тайху…
Теперь определите следующую функцию в верхней части файла darknet.py:
def get_test_input():
img = cv2.imread("dog-cycle-car.png")
img = cv2.resize(img, (416,416)) #Resize to the input dimension
img_ = img[:,:,::-1].transpose((2,0,1)) # BGR -> RGB | H X W C -> C X H X W
img_ = img_[np.newaxis,:,:,:]/255.0 #Add a channel at 0 (for batch) | Normalise
img_ = torch.from_numpy(img_).float() #Convert to float
img_ = Variable(img_) # Convert to Variable
return img_
Нам нужно ввести следующий код:
model = Darknet("cfg/yolov3.cfg")
inp = get_test_input()
pred = model(inp)
print (pred)
Вы увидите такой вывод:
( 0 ,.,.) =
16.0962 17.0541 91.5104 ... 0.4336 0.4692 0.5279
15.1363 15.2568 166.0840 ... 0.5561 0.5414 0.5318
14.4763 18.5405 409.4371 ... 0.5908 0.5353 0.4979
⋱ ...
411.2625 412.0660 9.0127 ... 0.5054 0.4662 0.5043
412.1762 412.4936 16.0449 ... 0.4815 0.4979 0.4582
412.1629 411.4338 34.9027 ... 0.4306 0.5462 0.4138
[torch.FloatTensor of size 1x10647x85]
Форма тензора — 1×10647×85, первое измерение — это размер пакета, здесь мы используем только одно изображение. Для изображений в пакете у нас будет таблица размером 100647×85, в которой каждая строка представляет ограничивающую рамку (4 атрибута ограничивающей рамки, 1 показатель объектности и 80 оценок класса).
Прямо сейчас наша сеть имеет случайные веса и не будет выводить правильный класс. Нам нужно загрузить файл весов для сети, чтобы мы могли воспользоваться официальным файлом весов.
Скачать предварительно обученные веса
Загрузите файл весов и поместите его в каталог детектора, мы можем загрузить его напрямую с помощью командной строки:
wget https://pjreddie.com/media/files/yolov3.weights
Его также можно скачать по этому адресу:Семья P Eddie.com/Media/files…
Понимание файла весов
Официальный файл весов представляет собой двоичный файл, в котором веса нейронной сети хранятся последовательно.
Мы должны внимательно читать веса, потому что веса хранятся только в форме с плавающей запятой, и нет никакой другой информации, которая точно указывала бы нам, к какому слою они принадлежат. Поэтому, если показания неверны, то очень вероятно, что все веса загружены неправильно, и модель совершенно непригодна для использования. Таким образом, просто читая поплавок, невозможно определить, к какому слою относятся веса. Поэтому мы должны понимать, как хранятся веса.
Во-первых, веса относятся только к двум типам слоев: слоям пакетной нормы и сверточным слоям. Веса этих слоев хранятся точно в том же порядке, в котором слои определены в файле конфигурации. Таким образом, если за сверточным блоком следует сокращенный блок, а этот ярлык соединяет другой сверточный блок, можно ожидать, что файл будет содержать веса предыдущих сверточных блоков, за которыми следуют веса последних.
Когда слой пакетной нормализации появляется в сверточном модуле, он не несет члена смещения. Однако, если пакетная нормализация для модуля свертки не существует, «веса» члена смещения считываются из файла. На рисунке ниже показано, как хранятся веса.
вес груза
Мы пишем функцию для загрузки весов, которая является функцией-членом класса Darknet. Он принимает аргумент, отличный от self, в качестве пути к файлу весов.
def load_weights(self, weightfile):
Первый 160-битный файл весов содержит 5 значений int32, составляющих заголовок файла.
#Open the weights file
fp = open(weightfile, "rb")
#The first 5 values are header information
# 1. Major version number
# 2. Minor Version Number
# 3. Subversion number
# 4,5. Images seen by the network (during training)
header = np.fromfile(fp, dtype = np.int32, count = 5)
self.header = torch.from_numpy(header)
self.seen = self.header[3]
Последующие биты представляют веса в порядке, описанном выше. Веса сохраняются как float32 или 32-битные числа с плавающей запятой. Давайте загрузим оставшиеся веса в np.ndarray.
weights = np.fromfile(fp, dtype = np.float32)
Теперь мы итеративно загружаем файл весов в модули сети.
ptr = 0
for i in range(len(self.module_list)):
module_type = self.blocks[i + 1]["type"]
#If module_type is convolutional load weights
#Otherwise ignore.
Во время цикла мы сначала проверяем, имеет ли сверточный модуль значение batch_normalize(True). Исходя из этого, нагружаем веса.
if module_type == "convolutional":
model = self.module_list[i]
try:
batch_normalize = int(self.blocks[i+1]["batch_normalize"])
except:
batch_normalize = 0
conv = model[0]
Мы сохраняем переменную с именем ptr, чтобы отслеживать нашу позицию в массиве весов. Теперь, если проверка batch_normalize окажется True, мы загружаем веса следующим образом:
if (batch_normalize):
bn = model[1]
#Get the number of weights of Batch Norm Layer
num_bn_biases = bn.bias.numel()
#Load the weights
bn_biases = torch.from_numpy(weights[ptr:ptr + num_bn_biases])
ptr += num_bn_biases
bn_weights = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
ptr += num_bn_biases
bn_running_mean = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
ptr += num_bn_biases
bn_running_var = torch.from_numpy(weights[ptr: ptr + num_bn_biases])
ptr += num_bn_biases
#Cast the loaded weights into dims of model weights.
bn_biases = bn_biases.view_as(bn.bias.data)
bn_weights = bn_weights.view_as(bn.weight.data)
bn_running_mean = bn_running_mean.view_as(bn.running_mean)
bn_running_var = bn_running_var.view_as(bn.running_var)
#Copy the data to model
bn.bias.data.copy_(bn_biases)
bn.weight.data.copy_(bn_weights)
bn.running_mean.copy_(bn_running_mean)
bn.running_var.copy_(bn_running_var)
Если результат проверки batch_normalize не равен True, необходимо загрузить только элемент смещения сверточный слой.
else:
#Number of biases
num_biases = conv.bias.numel()
#Load the weights
conv_biases = torch.from_numpy(weights[ptr: ptr + num_biases])
ptr = ptr + num_biases
#reshape the loaded weights according to the dims of the model weights
conv_biases = conv_biases.view_as(conv.bias.data)
#Finally copy the data
conv.bias.data.copy_(conv_biases)
Наконец, мы загружаем веса сверточного слоя.
#Let us load the weights for the Convolutional layers
num_weights = conv.weight.numel()
#Do the same as above for weights
conv_weights = torch.from_numpy(weights[ptr:ptr+num_weights])
ptr = ptr + num_weights
conv_weights = conv_weights.view_as(conv.weight.data)
conv.weight.data.copy_(conv_weights)
На этом введение в функцию завершено, теперь вы можете загрузить веса в объект даркнета, вызвав функцию load_weights для объекта даркнета.
model = Darknet("cfg/yolov3.cfg")
model.load_weights("yolov3.weights")
С построением модели и загрузкой веса мы, наконец, можем приступить к обнаружению объектов. В будущем мы также опишем, как можно использовать пороги достоверности объектности и подавление немаксимумов для генерации окончательных обнаружений.
Оригинальная ссылка:medium.com/paper space/…