
Google TensorFlow — набирающая популярность библиотека машинного обучения с акцентом на нейросетях. У нее есть одна замечательная особенность, она умеет работать не только в программах на Python, а также и в программах на C++. Однако, как оказалось, в случае С++ нужно немного повозиться, чтобы правильно приготовить это блюдо. Конечно, основная часть разработчиков и исследователей, которые используют TensorFlow работают в Python. Однако, иногда бывает необходимо отказаться от этой схемы. Например вы натренировали вашу модель и хотите ее использовать в мобильном приложении или роботе. А может вы хотите интегрировать TensorFlow в существующий проект на С++. Если вам интересно как это сделать, добро пожаловать под кат.
Компиляция libtensorflow.so
Для компиляции tensorflow используется гугловая система сборки Bazel. Поэтому для начала придется поставить ее. Чтобы не засорять систему, я ставлю bazel в отдельную папку:
примерно так
git clone https://github.com/bazelbuild/bazel.git ${BAZELDIR} cd ${BAZELDIR} ./compile.sh cp output/bazel ${INSTALLDIR}/bin
Теперь приступим к сборке TensorFlow. На всякий случай: официальная документация по установке здесь. Раньше чтобы получить библиотеку приходилось делать что-то вроде этого.
Но теперь все немного проще
git clone -b r0.10 https://github.com/tensorflow/tensorflow Tensorflow cd Tensorflow ./configure bazel build :libtensorflow_cc.so
Идем пить чай. Результат нас будет ждать здесь
bazel-bin/tensorflow/libtensorflow_сс.so
Получение заголовочных файлов
Мы получили библиотеку, но чтобы ей воспользоваться нужны еще заголовочные файлы. Но не все хедеры легко дост��пны. Tensorflow использует библиотеку protobuf для сериализации графа вычислений. Объекты, подлежащие сериализации, описываются на языке Protocol Buffers, и затем, с помощью консольной утилиты генерируется код C++ самих объектов. Для нас это значит, что нам придется сгенерировать хедеры из .proto файлов самостоятельно (возможно я просто не нашел в исходниках эти хедеры и их можно не генерить, если кто знает где они лежат, напишите в комментах). Я генерю эти хедеры
Таким вот скриптом
#!/bin/bash mkdir protobuf-generated/ DIRS="" FILES="" for i in `find tensorflow | grep .proto$` do FILES+=" ${i}" done echo $FILES ./bazel-out/host/bin/google/protobuf/protoc --proto_path=./bazel-Tensorflow/external/protobuf/src --proto_path=. --cpp_out=protobuf-generated/ $FILES
Полный список папок, которые нужно указать компилятору, как содержащие заголовочные файлы
Tensorflow Tensorflow/bazel-Tensorflow/external/protobuf/src Tensorflow/protobuf-generated Tensorflow/bazel-Tensorflow Tensorflow/bazel-Tensorflow/external/eigen_archive
От версии к версии список с папок меняется, так как меняется структура исходников tensorflow.
Загрузка графа
Теперь когда у нас есть хедеры и библиотека мы можем подключить TensorFlow к нашей С++ программе. Однако, нас ждет небольшое разочарование, без Python нам все таки не обойтись, так как на данный момент функционал по построению графа недоступен из С++. Поэтому наш план таков:
Создаем граф в Python и сохраняем его в .pb файл
import numpy as np import tempfile import tensorflow as tf session = tf.Session() #ваш код генерации графа вычислений tf.train.write_graph(session.graph_def, 'models/', 'graph.pb', as_text=False)
Загружаем сохраненный граф в С++
#include "tensorflow/core/public/session.h" using namespace tensorflow; void init () { tensorflow::GraphDef graph_def; tensorflow::Session* session; Status status = NewSession(SessionOptions(), &session); if (!status.ok()) { std::cerr << "tf error: " << status.ToString() << "\n"; } // Читаем граф status = ReadBinaryProto(Env::Default(), "models/graph.pb", &graph_def); if (!status.ok()) { std::cerr << "tf error: " << status.ToString() << "\n"; } // Добавляем граф в сессию TensorFlow status = session->Create(graph_def); if (!status.ok()) { std::cerr << "tf error: " << status.ToString() << "\n"; } }
Вычисление значений операций графа в С++ выглядит примерно так:
void calc () { Tensor inputTensor1 (DT_FLOAT, TensorShape({size1, size2})); Tensor inputTensor2 (DT_FLOAT, TensorShape({size3, size3})); //заполнение тензоров-входных данных for (int i...) { for (int j...) { inputTensor1.matrix<float>()(i, j) = value1; } } std::vector<std::pair<string, tensorflow::Tensor>> inputs = { { "tensor_scope/tensor_name1", inputTensor1 }, { "tensor_scope/tensor_name2", inputTensor2 } }; //здесь мы увидим тензоры - результаты операций std::vector<tensorflow::Tensor> outputTensors; //операции возвращающие значения и не возвращающие передаются в разных параметрах auto status = session->Run(inputs, { "op_scope/op_with_outputs_name" //имя операции, возвращающей значение }, { "op_scope/op_without_outputs_name", //имя операции не возвращающей значение }, &outputTensors); if (!status.ok()) { std::cerr << "tf error: " << status.ToString() << "\n"; return 0; } //доступ к тензорам-результатам for (int i...) { outputs [0].matrix<float>()(0, i++); } }
Сохранение и загрузка состояния графа
Иногда хочется прервать тренировку модели и продолжить ее на другом устройстве или просто позже. Или, например, просто сохранить состояние предобученного графа для последующего использования. В С++ нет какого-то стандартного пути. Но, оказывается, довольно несложно организовать этот функционал самостоятельно.
Для начала надо добавить в граф операции считывания и загрузки значений переменных
import numpy as np import tempfile import tensorflow as tf session = tf.Session() #ваш код генерации графа вычислений session.run(tf.initialize_all_variables()) #добавление операций считывания и загрузки значений переменных всего графа for variable in tf.trainable_variables(): tf.identity (variable, name="readVariable") tf.assign (variable, tf.placeholder(tf.float32, variable.get_shape(), name="variableValue"), name="resoreVariable") tf.train.write_graph(session.graph_def, 'models/', 'graph.pb', as_text=False)
В С++ операции сохранения и загрузки состояния графа выглядят примерно вот так
// Сохранение состояния void saveGraphState (const std::string fileSuffix) { std::vector<tensorflow::Tensor> out; std::vector<string> vNames; // извлекаем операции считывания переменных int node_count = graph_def.node_size(); for (int i = 0; i < node_count; i++) { auto n = graph_def.node(i); if ( n.name().find("readVariable") != std::string::npos ) { vNames.push_back(n.name()); } } // запускаем операции считывания переменных Status status = session->Run({}, vNames, {}, &out); if (!status.ok()) { std::cout << "tf error1: " << status.ToString() << "\n"; } // сохраняем значения переменных в файл int variableCount = out.size (); std::string dir ("graph-states-dir"); std::fstream output(dir + "/graph-state-" + fileSuffix, std::ios::out | std::ios::binary); output.write (reinterpret_cast<const char *>(&variableCount), sizeof(int)); for (auto& tensor : out) { int tensorSize = tensor.TotalBytes(); //Используем тот самый protobuf TensorProto p; tensor.AsProtoField (&p); std::string pStr; p.SerializeToString(&pStr); int serializedTensorSize = pStr.size(); output.write (reinterpret_cast<const char *>(&serializedTensorSize), sizeof(int)); output.write (pStr.c_str(), serializedTensorSize); } output.close (); } //Загрузка состояния bool loadGraphState () { std::string dir ("graph-states-dir"); std::fstream input(dir + "/graph-state", std::ios::in | std::ios::binary); if (!input.good ()) return false; std::vector<std::pair<string, tensorflow::Tensor>> variablesValues; std::vector<string> restoreOps; int variableCount; input.read(reinterpret_cast<char *>(&variableCount), sizeof(int)); for (int i=0; i<variableCount; i++) { int serializedTensorSize; input.read(reinterpret_cast<char *>(&serializedTensorSize), sizeof(int)); std::string pStr; pStr.resize(serializedTensorSize); char* begin = &*pStr.begin(); input.read(begin, serializedTensorSize); TensorProto p; p.ParseFromString (pStr); std::string variableSuffix = (i==0?"":"_"+std::to_string(i)); variablesValues.push_back ({"variableValue" + variableSuffix, Tensor ()}); Tensor& t (variablesValues.back ().second); t.FromProto (p); restoreOps.emplace_back ("resoreVariable" + variableSuffix); } input.close (); std::vector<tensorflow::Tensor> out; Status status = session->Run(variablesValues, {}, restoreOps, &out); if (!status.ok()) { std::cout << "tf error2: " << status.ToString() << "\n"; } return true; };
Немножечко видео
Примерно так, как описано в статье я тренирую модель пока что двумерного квадрокоптера. Выглядит это вот так:
Задача дронов прилететь в центр крестика и находиться там, для этого они могут включать или выключать двигатели (используется алгоритм DQN). На видео они находятся в среде с довольно большим трением, поэтому двигаются медленно. На данный момент работаю над полетом в среде без трения и облетом препятствий. При получении хорошего результата планирую еще одну статью.