Як подружити 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<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). На відео вони знаходяться в середовищі з досить великим тертям, тому рухаються повільно. На даний момент працюю над польотом в середовищі без тертя і обльотом перешкод. При отриманні позитивного результату планую ще одну статтю.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.