Как я Keras на C++ запускал

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


Тем, кому не терпится – вот тут репозитарий на github, с подробным описанием использования. Ну а всех остальных прошу под кат…


Постановка проблемы.


В процессе работы мне понадобилась запустить обученную модель на C++ приложении (Unreal Engune 4). Но вот незадача: на сегодняшний день нет практически никакой возможности запустить модель Keras на C++.


Вариант с вызовом Python из C++ не представлялся мне хорошим. Еще одним вариантом было конвертация модели Keras в модель TensorFlow и потом сборка TensoFflow под кресты и вызов API TF уже из C++ кода.


Сей процесс метаморфозов хорошо описан в этой статье. Но с этим также возникают трудности. Во-первых, TensorFlow собирается через Bzzel. А сам безель штука капризная и отказался собираться под UE4. Во-вторых, сам TF довольно большая и громоздкая штуковина, а мне хотелось чего-то более легкого и производительного. Могу лишь сказать, что на просторах github был найден полупабочий проект, с нужным мне функционалом. Но, он не поддерживал актуальные версии Python и Keras. А попытки переделать его, не увенчались успехом: С++ приложение валилась с ошибкой Core Dump. Было принято писать свою реализацию…


Пишем свою библиотеку!


Включив рок потяжелее, закинувшись бутылкой пиваса энергетика, я сел за код. Во многом в реализации этой библиотеки мне помог код TensorFlow, попытки реабилитации найденного на гит кода, некоторые знания об алгоритмах и структурах данных (спасибо ИТМО за его курсы) и хорошая музыка в ушах. Так или иначе библиотека была написана за одну ночь.


И так встречайте: Keras2cpp!


Первая чать библиотеки – это Python модуль для сохранения обученной модели в собственный бинарный формат.


Ничего сложного в этой операции нет. Мы просто читаем модель Keras и записываем побитово в файл: сначала тип слоя, потом размерность, потом матрицу весов в формате float.


Теперь перейдем к самому вкусному – C++ реализации.


Пользователю доступны 2 сущности tensor и model.


Tensorпеределяет собой данные с которыми работает нейросеть и является компьютерной реализацией тензора. На данный момент поддерживается максимальная размерность в 4 измерения. Размерность каждого измерения хранится в поле std::vector<int> dims_; а вес каждого элемента тензора – в std::vector<int> data_;. Из доступных методов можно выделить void Print() и Tensor Select(int row). Остальные операции вы можете посмотреть в исходном коде. После того как математика для тензоров была написана я приступил к реализации моделей.


Modelпредставляет собой набор слоев в каждом из которых прописаны операции над тензорами и матрица весов. Для пользователя доступны 2 функции virtual bool LoadModel(const std::string& filename); и virtual bool Apply(Tensor* in, Tensor* out);.


Вот полный пример кода.


python_model.py:


import numpy as np
from keras import Sequential
from keras.layers import Dense

#create random data
test_x = np.random.rand(10, 10).astype('f')
test_y = np.random.rand(10).astype('f')
model = Sequential([
    Dense(1, input_dim=10)
])
model.compile(loss='mse', optimizer='adam')

#train model by 1 iteration
model.fit(test_x, test_y, epochs=1, verbose=False)

#predict
data = np.array([[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]])
prediction = model.predict(data)
print(prediction)

#save model
from keras2cpp import export_model
export_model(model, 'example.model')

cpp_mpdel.cc:


#include "keras_model.h"

int main() 
{
    // Initialize model.
    KerasModel model;
    model.LoadModel("example.model");

    // Create a 1D Tensor on length 10 for input data.
    Tensor in(10);
    in.data_ = {{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}};

    // Run prediction.
    Tensor out;
    model.Apply(&in, &out);
    out.Print();
    return 0;
}

На этом я думаю все. Приятного использования, а я пойду к любимому C# и Python писать нейросети дальше.


P.S.


Мне понравилось писать эту библиотеку. Когда пишешь все сам с нуля – больше понимаешь, а как оно работает… В планах добавить поддержку других архитектур и GPU…


github репозиторий
Источник

Share post
AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 15

    0
    аплодисменты стоя :). Мне постоянно кажется, что чинить ломающиеся зависимости в питоне, — это подольше будет, чем просто все перезапилить.
      0
      Много опечаток. Рекомендую вычитать статью.
      В остальном — супер, реально крутая идея! Единственное, что меня волнует — а при сериализации/десериализации ничего не поломается? Ну, в случае, например, разных архитектур (x86 vs ARM vs что-то еще)? Я уж не говорю, что я хапнул проблем с пиклами разных версий (в рамках одной версии — все ОК)…
        +1
        Какая производительность получилась? В сравнении с «обычным порошком» на CPU.
          +5
          Столкнулся с такой же задачкой, посмотрел в интернете похожие проекты (из минусов оказалось то, что их нужно дописывать), решил остановиться на чистом Си для tensorflow. Сетка в формате keras портируется в граф pb, скачивается готовая библиотека dll или so и заголовочный файл c_api.h и можно использовать api для Си (gpu поддерживается) (https://www.tensorflow.org/install/lang_c).
            0

            Тоже пока не очень понял проблему… HDF5 переписывается в .pb на ура скриптом на коленке (ну или тем же keras_to_tensorflow.py если лень).


            Дальше оно прекрасно грузится в плюсах, как-нибудь так:


            if (!( status = ReadBinaryProto(env, "graph.pb", &grdef) ).ok()) goto boom;
            if (!( status = session->Create(grdef) ).ok()) goto boom;
            ...
            if (!( status = session->Run(inputs, ..., &outputs) ).ok()) goto boom;
            ...
            return 0;
            boom:
              printf("BOOM: %s \n", status.ToString());
              return -1;

            Многослойные и сильно-мудреные вещи делаются вполне себе аналогично.

            0
            у меня была похожая идея, но руки так и не дошли сделать)
              +1
              Это не слишком большое упрощение? ЕМНИП, у Керас в каждом типе слоя есть несколько параметров, влияющих на поведение.
              И какая у этого кода производительность по сравнению с оригиналом? В питоновом варианте же сеть компилируется в формат бэкенда (по умолчанию ТензорФлоу) и далее для каждого слоя подбирается соответствующее ядро, тоже написанное на С.
                0
                А сам безель штука капризная и отказался собираться под UE4
                Поясните пожалуйста, что Вы имеете ввиду под сборкой bazel под UE4?
                TF довольно большая и громоздкая штуковина, а мне хотелось чего-то более легкого и производительного
                Сравнение производительности в студию. Вы вероятно спутали что-то, не уж то Вам удалось написать более производительный инфиренс (на CPU?) нежели у ребят из Google?
                  +1
                  Кстати, если говорить про CPU, то может быть и можно. По крайней мере, если чистую С-реализацию написать.
                  Я недавно, в неких исследовательских целях, написал на коленке инференс ResNet-34 для типа float (да, я знаю что в этой фразе несколько извращений подряд) и получил производительность где-то в 6 раз ниже, чем у аналогичного инференса на питоне. Но при этом мой вариант был чисто однопоточный и без какой-либо оптимизации, а питон бегал на трёх ядрах и уж родная библиотека явно оптимизирована по полной. Т.е. вовсе не исключено, что если добавить в мой код потоков и SIMD-ов, то он побежит бодрее оригинальной версии.
                  P.S. Собственно, эти соображения и побудили меня задать автору вопрос по производительности немного выше.
                    0

                    Есть Intel DAAL, который наверняка используется в вашем питоне, если это anaconda. Так что вряд-ли получится что-то ускорить если написать SIMDов самому.

                      0
                      Не совсем вас понял, если честно. Тот код, который я написал — он на чистом С, без использования ещё чего-либо, тем более питона.
                        0
                        > получил производительность где-то в 6 раз ниже, чем у аналогичного инференса на питоне

                        Я про то что код на питоне внутри тоже может использовать низкоуровневые библиотеки с оптимизациями вплоть до SIMD :).
                          0
                          Вы, вероятно, не очень внимательно прочитали мой первоначальный комментарий.
                          Код на питоне не просто может использовать SIMD-ы, очевидно, что он их использует. Я брал для сравнения обычный TensorFlow, который внизу оптимизирован весьма неплохо и весь из себя многопоточный.
                          Но мой код ничего этого не использует (разве что gcc что-нить без спроса векторизовал на -O3), так что, вероятно, есть задел, чтобы преодолеть эту 6-кратную разницу между моим неоптимизированным С-кодом и оптимизированным питоном.
                            0
                            Ок, всё верно, меня смутило про «родную библиотеку», прочитал как «стандартную» :).
                              0
                              Единственное, что всё-таки стоит отметить что TF можно собрать как с оптимизациями, так и без. И не известно с какой сборкой вы сравнивали свой код на Си.

                Only users with full accounts can post comments. Log in, please.