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

в 7:00, , рубрики: c++, open source, Алгоритмы, машинное обучение, нейросети, обработка изображений

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

Введение

Здравствуйте, уважаемыее!

Последние два года моей работы в компании 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<float> Net;
typedef Synet::View View;
typedef Synet::Shape Shape;
typedef Synet::Region<float> Region;
typedef std::vector<Region> 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 на выборке из набора открытых моделей:

Test Description i7-6700 3.4GHz 4c/8t FMA/AVX-2 i9-7900X 3.3GHz 10c/20t AVX-512
test_000 Vehicle attributes recognition (2.4 MB) 1.520 / 1.597 ms (-5%) 0.772 / 0.690 ms (+12%)
test_001 Age gender recognition (8.2 MB) 1.659 / 1.418 ms (+17%) 0.988 / 0.804 ms (+23%)
test_002 Face detection (4.0 MB) 34.26 / 43.17 ms (-21%) 26.72 / 24.57 ms (+9%)
test_003f Face detection (2.2 MB) 12.63 / 14.87 ms (-15%) 8.680 / 9.326 ms (-7%)
test_004 Licence plate recognition (4.6 MB) 4.350 / 4.871 ms (-11%) 2.838 / 2.432 ms (+17%)
test_005 Licence plate recognition (0.7 MB) 0.339 / 0.260 ms (+30%) 0.200 / 0.142 ms (+41%)
test_006 Face reidentification (4.2 MB) 11.82 / 9.052 ms (+31%) 8.200 / 4.559 ms (+80%)
test_007 Person reidentification (3.1 MB) 3.567 / 3.402 ms (+5%) 2.471 / 1.679 ms (+47%)
Average +2% +25%

Как видно из таблицы, на данных тестах на машине с поддержкой 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, которое я сделал в этой статье, я намеренно не углублялся в детали ее внутренней реализации — под капотом много вкусных алгоритмов, но детали их реализации хотелось бы раскрыть в следующих статьях цикла.

Автор: Ермолаев Игорь

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js