Synet — фреймворк для запуска предварительно обученных нейронных сетей на CPU

мой велосипед

Введение


Здравствуйте, уважаемые хабровчане!

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

Ответы на вопросы


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

Первый вопрос, который обычно возникает в таких случаях: Да кто сейчас запускает сети на обычных процессорах, когда есть графические ускорители и тензорные (матричные) ускорители?
Отвечу, что да — обучение нейросетей действительно не целесообразно выполнять на CPU, но вот запускать уже готовые нейросети — вполне себе востребованная задача, особенно если сеть достаточно небольшого размера. Причины для этого могут быть разные, но основные:

  1. CPU более широко распространены. GPU есть далеко не на всех машинах, особенно это касается серверов.
  2. На небольших нейросетях выигрыш от использования GPU мал, а иногда полностью отсутствует.
  3. Эффективное задействование GPU для ускорения нейросетей, как правило, требует существенно более сложной структуры приложения.


Следующий возможный вопрос: A зачем использовать для запуска специализированное решение, когда есть Tensorflow, Caffe или MXNet?
Ответить на это можно следующее:

  1. Разнообразие фреймворков не всегда хорошо — так если в проекте есть несколько моделей, обученных разными фреймворками, то потом придется их всех встраивать в готовое решение, что очень неудобно.
  2. Классические фреймворки разрабатывались для обучения моделей на GPU — и в этом они безусловно хороши! Но для запуска обученных моделей на CPU их функционал избыточен и не оптимален.
  3. Подтверждением необходимости специализированного решения служит популярность OpenVINO — фреймворка от Интел, который выполняет ту же функцию.


Тут сразу логично возникает вопрос про изобретение велосипеда: Зачем использовать свою поделку, когда есть вполне профессиональное решение от признанного мирового лидера?
Отвечу так:

  1. На момент начала работ над Synet, OpenVINO был еще в зачаточном состоянии. И по правде говоря, если бы в то время OpenVINO был в его нынешнем состоянии, то я с высокой долей вероятности не стал бы ввязываться в свой собственный проект.
  2. Собственный фреймворк можно адаптировать под свои нужды. Так в моем случае, основным требованием была максимальная однопоточная производительность.
  3. Можно максимально быстро обеспечить поддержку новой функциональности, если она вдруг потребовалась (например, добавить новый слой и ли устранить ошибку с производительностью).
  4. Легкость встраивания в готовое решение.
  5. Функционирование библиотеки на платформах, отличных от x86/x86_64 — например на ARM.


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

Краткое описание Synet


Synet написан на С++ и содержит только заголовочные файлы. Низкоуровневые платформозависимые оптимизации реализованы в Simd — другом open-source проекте, посвященном ускорению обработки изображений на CPU. И это единственная внешняя зависимость Synet (такая схема выбрана с целью облегчения интеграции библиотеки в сторонние проекты). Для запуска нейросетей используются модели собственного внутреннего формата.
image
Конвертация предварительно обученных моделей во внутренний формат осуществляется по двухшаговой схеме: 1) Сначала конвертируем модель в формат Inference Engine (благо
в OpenVINO есть для этого все необходимые инструменты). 2) Затем из этого промежуточного представления конвертируем непосредственно во внутренний формат Synet.
image
Модель Synet содержит два файла: 1) *.XML — файл с описанием структуры модели. 2) *.BIN — файл с обученными весами.
image

Пример использования Synet


Ниже приведен пример, использования Synet для детектирования лиц. Оригинальная модель Inference Engine взята здесь.

#define SYNET_SIMD_LIBRARY_ENABLE
#include "Synet/Network.h"
#include "Synet/Converters/InferenceEngine.h"
#include "Simd/SimdDrawing.hpp"

typedef Synet::Network Net;
typedef Synet::View View;
typedef Synet::Shape Shape;
typedef Synet::Region Region;
typedef std::vector Regions;

int main(int argc, char* argv[])
{
    Synet::ConvertInferenceEngineToSynet("ie_fd.xml", "ie_fd.bin", 
		true, "synet.xml", "synet.bin");

    Net net;
    net.Load("synet.xml", "synet.bin");

    net.Reshape(256, 256, 1);

    Shape shape = net.NchwShape();

    View original;
    original.Load("faces_0.ppm");

    View resized(shape[3], shape[2], original.format);
    Simd::Resize(original, resized, ::SimdResizeMethodArea);

    net.SetInput(resized, 0.0f, 255.0f);

    net.Forward();

    Regions faces = net.GetRegions(original.width, original.height, 0.5f, 0.5f);
    uint32_t white = 0xFFFFFFFF;
    for (size_t i = 0; i < faces.size(); ++i)
    {
        const Region & face = faces[i];
        ptrdiff_t l = ptrdiff_t(face.x - face.w / 2);
        ptrdiff_t t = ptrdiff_t(face.y - face.h / 2);
        ptrdiff_t r = ptrdiff_t(face.x + face.w / 2);
        ptrdiff_t b = ptrdiff_t(face.y + face.h / 2);
        Simd::DrawRectangle(original, l, t, r, b, white);
    }
    original.Save("annotated_faces_0.ppm");

    return 0;
}


В результате работы примера должна появится картинка с аннотированными лицами:
image
Теперь давайте разберем пример по шагам:

  1. В начале идет преобразование модели из формата Inference Engine в Synet:
    Synet::ConvertInferenceEngineToSynet("ie_fd.xml", "ie_fd.bin",  true, "synet.xml", "synet.bin");
    

    В реальности это шаг делается один раз, а потом везде используется уже сконвертированная модель.
  2. Загрузка сконвертированной модели:
    Net net;
    net.Load("synet.xml", "synet.bin");
    
  3. Необязятельный шаг по изменению размера входного изображения и батча (естественно, что модель должна поддерживать изменение входного размера):
    net.Reshape(256, 256, 1);
    
  4. Загрузка картинки и ее приведение в входному размеру модели:
    View original;
    original.Load("faces_0.ppm");
    View resized(net.NchwShape()[3], net.NchwShape()[2], original.format);
    Simd::Resize(original, resized, ::SimdResizeMethodArea);
    
  5. Загрузка изображения в модель:
    net.SetInput(resized, 0.0f, 255.0f);
    
  6. Запуск прямого распространения сигнала в сети:
    net.Forward();
    
  7. Получение набора регионов с найденными лицами:
    Regions faces = net.GetRegions(original.width, original.height, 0.5f, 0.5f);
    

Сравнение производительности


Наверное было бы не совсем корректно сравнивать Synet с классическими фреймворками для машинного обучения, например Inference Engine на ряде тестов их обходит в разы.
Потому ниже приведен пример сравнения однопоточной производительности Inference Engine (продукта схожей функциональности) и Synet на выборке из набора открытых моделей:
Как видно из таблицы, на данных тестах на машине с поддержкой AVX2 (i7–6700) производительность Synet в целом соответствует производительности Inference Engine (хотя и сильно варьируется от модели к модели). На машине с поддержкой AVX-512 (i9–7900X) производительность Synet в среднем на 25% превышает таковую у Inference Engine.

Все измерения проводились тестовым приложением, которое есть в Synet. Так что при желании, читатели смогут воспроизвести тесты у себя:

git clone -b master --recurse-submodules -v https://github.com/ermig1979/Synet.git synet
cd synet
./build.sh inference_engine
./test.sh

Достоинства и недостатки


Начну с плюсов:

  1. Проект небольшой по размеру, легко внедряется в сторонние проекты.
  2. Показывает высокую однопоточную производительность.
  3. Работает на мобильных процессорах (поддерживает ARM-NEON).


Ну и минусы, куда же без них:

  1. Нет поддержки GPU и других специальных ускорителей.
  2. Плохое распараллеливание одной задачи на многоядерных CPU.
  3. Нет поддержки INT8 (квантизации весов).

Заключение


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

  1. Поддержку новых моделей, слоев, алгоритмов.
  2. Поддержкy целочисленных вычислений в формате INT8 (квантизированных весов).
  3. Поддержкy вычислений на GPU.
  4. Конвертация из формата ONNX.


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

© Habrahabr.ru