Как подружить Tensorflow и C++
У 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()(i, j) = value1;
}
}
std::vector> inputs = {
{ "tensor_scope/tensor_name1", inputTensor1 },
{ "tensor_scope/tensor_name2", inputTensor2 }
};
//здесь мы увидим тензоры - результаты операций
std::vector 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()(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 out;
std::vector 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(&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(&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> variablesValues;
std::vector restoreOps;
int variableCount;
input.read(reinterpret_cast(&variableCount), sizeof(int));
for (int i=0; i(&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 out;
Status status = session->Run(variablesValues, {}, restoreOps, &out);
if (!status.ok()) {
std::cout << "tf error2: " << status.ToString() << "\n";
}
return true;
};
Немножечко видео
Примерно так, как описано в статье я тренирую модель пока что двумерного квадрокоптера. Выглядит это вот так:
Задача дронов прилететь в центр крестика и находиться там, для этого они могут включать или выключать двигатели (используется алгоритм DQN). На видео они находятся в среде с довольно большим трением, поэтому двигаются медленно. На данный момент работаю над полетом в среде без трения и облетом препятствий. При получении хорошего результата планирую еще одну статью.