Не так давно в свет вышло устройство Movidius Neural Compute Stick (NCS), представляющее собой аппаратный ускоритель для нейронных сетей с USB интерфейсом. Меня заинтересовала потенциальная возможность применения устройства в области робототехники, поэтому я приобрел его и задумал запустить какую-нибудь нейросеть. Однако большинство существующих примеров для NCS решают задачу классификации изображений, а мне хотелось попробовать кое-что другое, а именно обнаружение лиц. В этой публикации я хотел бы поделиться опытом, полученным в ходе такого эксперимента.

Весь код можно найти на GitHub.

image


Подробнее об NCS


Neural Compute Stick — это устройство, предназначенное для ускорения нейронных сетей (преимущественно свёрточных) на этапе применения (inference). Идея заключается в том, что NCS можно присоединить к роботу или дрону и запускать нейросети там, где для этого не хватает вычислительных ресурсов. К примеру, NCS можно подключить к Raspberry Pi.

Фреймфорк для этого устройства, он же NCSDK, включает API для Python и C++, а также несколько полезных утилит, позволяющих скомпилировать нейронную сеть в формат, который понимает NCS, измерить время, которое занимают вычисления на каждом слое и проверить работоспособность сети. В качестве исходных данных могут выступать предобученные нейронные сети в формате Caffe или TensorFlow.

Выбор модели


Отлично, мы хотим решить задачу обнаружения лиц (face detection). Есть две довольно популярные архитектуры нейросетей для задач обнаружения: это Fast-RCNN/Faster-RCNN и YOLO. Мне не хотелось на данном этапе обучать свою модель, поэтому решил поискать готовую.

Трудность заключается в том, что NCSDK поддерживает далеко не все возможности, доступные в Caffe и TensorFlow, поэтому произвольная архитектура может просто не скомпилироваться. Например, далеко не все типы слоев поддерживаются, и при этом архитектура должна иметь ровно один входной слой (полный список ограничений и поддерживаемых слоев для Caffe можно увидеть здесь). Первая модель для обнаружения лиц, которую мне удалось найти (Faster-RCNN), не удовлетворяла обоим требованиям.

Затем я наткнулся на обученную модель архитектуры YOLO. Проблема была лишь в том, что нейросеть была в формате Darknet, хотя сама архитектура выглядела подходящей для NCS, поэтому появилась идея конвертировать нейросеть в формат Caffe.

Конвертация модели


Для конвертации модели я решил использовать вот этот проект, позволяющий переходить между форматами Darknet, Pytorch и Caffe.

Я запускаю конвертер в контейнере Docker — это уловка, которая появилась из-за того, что версия Caffe, установленная NCSDK, не понравилась конвертеру, а трогать конфигурацию системы мне не хотелось:

sudo docker run -v `pwd`:/workspace/data 	-u `id -u` -ti dlconverter:latest bash -c 	"python ./pytorch-caffe-darknet-convert/darknet2caffe.py 	./data/yolo-face.cfg ./data/yolo-face_final.weights 	./data/yolo-face.prototxt ./data/yolo-face.caffemodel"

Получается кое-что интересное: модель конвертируется, но выдается предупреждение о том, что слои типа Crop, Dropout и Detection распознать не удалось, из-за чего конвертер их пропустил. Можно ли обойтись без этих слоев? Оказывается, можно. Если внимательно посмотреть на код Darknet, можно заметить следующее:

Слой типа Crop нужен только на этапе обучения. Он занимается тем, что расширяет выборку, поворачивая изображение на случайные углы и вырезая из него случайные фрагменты. На этапе применения он не потребуется.

С Dropout немного интереснее. Dropout слой тоже нужен в основном на этапе обучения (Dropout слой обнуляет выходы нейронов с вероятностью $p$) для того, чтобы избежать переобучения и повысить способности модели к обобщению. На этапе применения от него можно избавиться, но при этом необходимо масштабировать выходы нейронов так, чтобы матожидание значений на входах следующего слоя не изменилось, чтобы поведение модели сохранилось (разделить на $1-p$). Если вглядеться в код Darknet, то можно заметить, что Dropout слой не только обнуляет некоторые выходы, но и масштабирует все остальные, поэтому Dropout слой можно безболезненно удалить.

Что касается Detection слоя, то он находится последним и занимается тем, что переводит в более читаемый вид выходы с предпоследнего слоя, а также считает функцию потерь на этапе обучения. Функцию потерь нам считать не нужно, а вот перевод результата в читаемый вид пригодится. В итоге я решил просто использовать функцию для последнего слоя прямиком из Darknet (немного подредактировав ее), оттуда же взял функцию для NMS (Non maximum suppression — удаление избыточных ограничивающих рамок). Они находятся в файлах detection_layer.c и detection_layer.h.

Тут стоит сделать замечание о том, что делает предпоследний слой. В архитектуре YOLO (You only look once) изображение разбивается на блоки сеткой размера $n\times n$ (в данном случае $n=11$), и для каждого блока предсказывается $5m+c$ значений, где $m=2$ — число ограничивающих рамок для каждого блока, а $c=1$ — число классов. Сами значения представляют собой: координаты, ширину, высоту и значение уверенности для каждой из $m$ рамок (то есть, всего пять значений), а также вероятность нахождения объекта в этом блоке для каждого класса. Итого получается $n \times n \times (5m+c)=1331$ значений. Слой Detection только разделяет разные типы данных и приводит их в структурированный вид.

Осталась одна проблема: конвертер генерирует .prototxt файл с форматом входного слоя, который NCSDK не может разобрать. Различия исключительно декоративные: конвертер записывает размер входного слоя в формате:

input_dim: x
input_dim: y 
input_dim: z 
input_dim: w

А утилита mvNCCompile, которая должна компилировать нейронную сеть в понятный NCS файл, хочет видеть формат:

input_shape {
dim: x
dim: y
dim: z
dim: w
}

Python скрипт utils/fix_proto_input_format.py призван решить эту проблему (не самому же это делать).

Компиляция модели


Теперь, когда модель переведена в формат Caffe, можно ее скомпилировать. Делается это довольно просто:

mvNCCompile -s 12 -o graph -w yolo-face.caffemodel yolo-face-fix.prototxt

Эта команда должна породить бинарный файл graph, который представляет собой граф вычислений в формате, понятном NCS.

Предобработка изображений


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

Судя по коду демо для Darknet, перед загрузкой изображения его нужно сжать до размера $448\times 448$ (причем не заботясь о пропорциях), нормировать на отрезок $[0,1]$ каждый пиксель и инвертировать порядок каналов с BGR на RGB. Вообще, в Caffe и OpenCV стандартным считается вариант BGR, а в Darknet — RGB, однако конвертер ничего об этом не знает, и в итоге каналы все равно нужно инвертировать.

Обращение к NCS и загрузка данных


Тут стоит заметить, что я использую C++, а не Python, поскольку ориентируюсь на применение устройства в робототехнике и верю, что на C++ можно добиться большего быстродействия. Именно из-за этого появляются дополнительные сложности: граф вычислений получает на вход и выдает на выходе данные в формате fp16 (16-битные числа с плавающей точкой), реализации которых нет в C++ по умолчанию. В примерах NCSDK эта проблема решается использованием функций floattofp16 и fp16tofloat, выдранных из Numpy, поэтому я использую такое же решение.

Для того, чтобы начать взаимодействие с NCS, нужно выполнить целый ряд действий:

  • Вызвать mvncGetDeviceName, чтобы получить имя NCS
  • Открыть устройство по имени с помощью mvncOpenDevice
  • Загрузить содержимое файла graph в буфер (специальной функции для этого нет, нужно использовать свою)
  • Разместить граф вычислений с помощью mvncAllocateGraph

Для загрузки данных и получения результата используются функции mvncLoadTensor и mvncGetResult соответственно. При этом нужно помнить про конвертацию данных и результата в fp16 и обратно.

Для прекращения работы с NCS нужно освободить ресурсы, отведенные под граф вычислений (mvncDeallocateGraph) и закрыть устройство (mvncCloseDevice).

Поскольку для взаимодействия с NCS нужно довольно много действий, я написал класс-обертку, у которого (помимо конструктора и деструктора) есть всего две функции: load_file для инициализации устройства и графа и load_tensor для загрузки данных и получения результата.

Профилировщик


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

mvNCProfile yolo-face-fix.prototxt -w yolo-face.caffemodel -s 12

Что получилось в итоге


Итоговый результат выглядит следующим образом:

Исходная модель (.cfg и .weights) с помощью конвертера преобразуется в формат Caffe (.prototxt и .caffemodel), в файле .prototxt формат входов исправляется с помощью Python скрипта, после чего модель компилируется в файл graph — это цели convert и graph в Makefile.

В самой программе для каждого полученного кадра производится предобработка, перевод в формат fp16 и загрузка в граф вычислений. Полученный результат переводится из fp16 в формат float и передается в функцию, которая имитирует работу последнего Detection слоя. Затем применяется Non maximum suppression.

Демо гордо выдает 4.5 кадра в секунду — это маловато. Проблема, видимо, в том, что эта архитектура относится к тому типу, который затачивается больше на точность, а не на быстродействие. Скорость работы можно значительно повысить, если использовать «мобильные» архитектуры вроде Tiny YOLO — для этого придется искать новую модель или обучать свою. Тем не менее, этот пример показывает, что нейросеть в формате Darknet можно скомпилировать и запустить на Neural Compute Stick.

Комментарии (12)


  1. old_bear
    24.01.2018 16:32

    А на «софте» сколько fps получается? Или нет возможности эту сетку запустить на обычном процессоре?


    1. BeloborodovDS Автор
      24.01.2018 16:50

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


    1. BeloborodovDS Автор
      24.01.2018 23:16

      Проверил. Без GPU и CUDNN получается 0.9 fps на core i3 (это в формате Darknet). То есть, ускорение вполне приличное для такого небольшого устройства. Как я и написал в статье, эта архитектура нейросети предполагает использование видеокарты даже на этапе применения.


      1. old_bear
        24.01.2018 23:18

        Спасибо. Будет теперь в голове хоть какая-то зацепка на тему относительной производительности этого устройства.


      1. olegator99
        25.01.2018 15:52

        Странно — очень маленькие цифры у вас получились.
        Мы запускали детектор лиц на базе OpenCV на hi3516 (ARM v7, 600MHZ), hard float point. Получили примерно 2-5 fps.


        Думаю, на RPi, можно достичь похожих или даже лучших цифр без внешнего железа.


        1. BeloborodovDS Автор
          25.01.2018 16:51

          Если я правильно вас понял, то детектор на базе OpenCV — это каскадный Haar или LBP. Я пробовал запускать их на RPi, и получил похожую производительность (порядка 5-7 вроде). Проблема в том, что большей скорости можно добиться только за счет сильного падения качества. У меня есть подозрения, что с помощью нейросетки можно добиться порядка 15 fps или больше, если выбрать более «легковесную» архитектуру, но это еще проверить нужно. С нейросеткой, которую я запускал, есть одна проблема — она явно рассчитана на использование видеокарты. Сейчас есть много архитектур, которые позволяют значительно повысить скорость за счет небольшого падения качества (те же Mobile Net, например).


  1. sshmakov
    25.01.2018 08:38

    Насчет маловато ли 4.5 fps — подумайте о том, что лица в видео не меняются с каждым кадром. На мой взгляд вполне достаточно по 4 кадрам из 25 находить лица.


  1. habraperson
    25.01.2018 16:52

    Если важна производительность (fps), то настоятельно рекомендую отказаться от использования нейронных сетей а взять готовую библиотеку OpenCV. У них в документации готовый пример с использованием каскадов Хаара. Поверьте сьевшему на этом собаку будет на порядок быстрее.

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


    1. BeloborodovDS Автор
      25.01.2018 17:12

      В этом есть смысл, если 5-7 fps достаточно (примерно столько выдает Haar на RPi), а я хотел бы выжать больше, и у меня есть подозрения, что это сделать вполне можно (я проверю).

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

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


  1. habraperson
    25.01.2018 17:33

    Будет интересно если у вас получиться быстрее. В системах что я строил fps был повыше (60 — 120) и разрешение камеры не уменьшалось до таких маленьких размеров. Если взять стандартное на сегодня FullHD и сжать до 448х448 очень много информации потеряется.
    Кстати, как вы обучаете сеть? Размечать кадр за кадром довольно муторное занятие.


    1. BeloborodovDS Автор
      25.01.2018 17:57

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


  1. habraperson
    25.01.2018 18:20

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