- PVSM.RU - https://www.pvsm.ru -

Как подружить Tensorflow и C++

Как подружить Tensorflow и C++ - 1

У Google TensorFlow есть одна замечательная особенность, оно умеет работать не только в программах на Python, а также и в программах на C++. Однако, как оказалось, в случае С++ нужно немного повозиться, чтобы правильно приготовить это блюдо. Конечно, основная часть разработчиков и исследователей, которые используют TensorFlow работают в Python. Однако, иногда бывает необходимо отказаться от этой схемы. Например вы натренировали вашу модель и хотите ее использовать в мобильном приложении или роботе. А может вы хотите интегрировать TensorFlow в существующий проект на С++. Если вам интересно как это сделать, добро пожаловать под кат.

Компиляция libtensorflow.so

Для компиляции tensorflow используется гугловая система сборки Bazel [1]. Поэтому для начала придется поставить ее. Чтобы не засорять систему, я ставлю bazel в отдельную папку:

примерно так

git clone https://github.com/bazelbuild/bazel.git ${BAZELDIR}
cd ${BAZELDIR}
./compile.sh
cp output/bazel ${INSTALLDIR}/bin

Теперь приступим к сборке TensorFlow. На всякий случай: официальная документация по установке здесь [2]. Раньше чтобы получить библиотеку приходилось делать что-то вроде этого [3].

Но теперь все немного проще

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 [4] для сериализации графа вычислений. Объекты, подлежащие сериализации, описываются на языке Protocol Buffers [5], и затем, с помощью консольной утилиты генерируется код 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). На видео они находятся в среде с довольно большим трением, поэтому двигаются медленно. На данный момент работаю над полетом в среде без трения и облетом препятствий. При получении хорошего результата планирую еще одну статью.

Автор: Parilo

Источник [6]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/algoritmy/177031

Ссылки в тексте:

[1] Bazel: http://bazel.io/

[2] здесь: https://www.tensorflow.org/versions/r0.10/get_started/os_setup.html#installing-from-sources

[3] этого: https://medium.com/jim-fleming/loading-tensorflow-graphs-via-host-languages-be10fd81876f#.vwkf80c9d

[4] protobuf: https://developers.google.com/protocol-buffers/

[5] Protocol Buffers: https://ru.wikipedia.org/wiki/Protocol_Buffers

[6] Источник: https://habrahabr.ru/post/308002/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best