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). На видео они находятся в среде с довольно большим трением, поэтому двигаются медленно. На данный момент работаю над полетом в среде без трения и облетом препятствий. При получении хорошего результата планирую еще одну статью.
Поделиться с друзьями
-->

Комментарии (14)


  1. qwerty135
    22.08.2016 09:32

    Почему бы двумя фразами в начале статьи не описать, что такое Google TensorFlow? И так многие делают, просто не объясняют во вступлении, о чём идёт речь. Тем самым сильно снижая эффективность публикаций.


    1. thelongrunsmoke
      22.08.2016 11:15
      +1

      Было бы странно видеть это в подобной статье на данном ресурсе.


  1. BelBES
    22.08.2016 10:38
    +1

    О, спасибо за пост) Python'нутость TensorFlow была одной из немногих причин, почему я все еще использую caffe вместо TF, теперь можно потихоньку начинать мигрировать)


    з.ы. а как в этом фреймворке с производительностью на GPU? А то когда я последний раз интересовался, былижалобы с низким перформансом по сравнению с caffe. И там еще не появился централизованный зоопарк моделей?


    1. Parilo
      22.08.2016 11:22

      Я тоже видел жалобы на тормознутость на GPU. Но с другими фреймворками не сравнивал, пользуюсь только TensorFlow на данный момент. Было бы интересно посмотреть на какие-нибудь сравнительные тесты. Про централизованый зоопарк моделей тоже не слышал, но идея здравая. Может к версии 1.0 появится.


      1. BelBES
        22.08.2016 11:28
        +1

        Про централизованый зоопарк моделей тоже не слышал, но идея здравая. Может к версии 1.0 появится.

        в caffe есть вот такая штука, а разработчикам TF уже и говорили про необходимость такого зоопарка, но что-то видно все никак не сделают. А то там глянешь, даже самых необходимых VGG16/ResNet/Inception etc. нету, все нужно из caffe конвертировать руками.


        1. Parilo
          22.08.2016 11:40

          Я пока что смотрю в сторону Keras. Это надстройка над Theano и TensorFlow. Но пока еще толком не юзал, вероятно оно упрощает построение моделей.


    1. supersonic_snail
      22.08.2016 15:59

      Вы врядли сейчас найдете что-то, что будет быстрее caffe для convolutional сетей. Просто потому, что с++ и боттленков практически нет, по сравнению с тем же питоном, где банальный цикл sgd выполняется медленнее, чем в с++.

      Сам tf работает примерно так же, как и theano. Хотя тут тоже не без нюансов — ощущение, что tf будет лучше, если у вас большая инфрасктруктура и в целом нормально оттестированная и рабочая сеть. Если наоборот, то theano лучше. Ну и theano не сильно медленнее, чем caffe — я обучал первую версию Inception сети, получалось где-то на 10-15% медленнее, чем в caffe. С учетом того, что обучается оно с неделю, лишние 8-10 часов погоду не делают.


      1. UndeadDragon
        26.08.2016 21:08

        Вы врядли сейчас найдете что-то, что будет быстрее caffe для convolutional сетей

        Почему? «Потому, что С++»? А Торч вообще Си.


  1. veydlin
    22.08.2016 11:24
    -1

    Было бы здорово, если бы вы дополнили статью запуском на С++ в VS


    1. Parilo
      22.08.2016 11:33

      С VS, вероятно, так просто не получится, так как я не видел инструкций по сборке для Windows. Но, если возможно будет собрать Bazel, я думаю, что шанс будет. Однако хлебнуть приятных минут ковыряния глубоко в исходниках наверное придется. Я не пользуюсь ни Windows, ни VS. Так, что ничем, кроме как расплывчатыми советами помочь не могу.


      1. SKolotienko
        22.08.2016 13:42
        +1

        Насколько я знаю, сейчас есть большие сложности с TensorFlow под Windows. Что чуть ли не единственный вариант — запускать в каком-то виде эмулированном *nix-окружении и без возможности вычислений на GPU.


        1. Parilo
          22.08.2016 13:48

          Еще недавно не было возможности использовать GPU на маке, сейчас это уже есть. Может и для винды сделают какой-то готовый пакет. Хотя верится в это с трудом, конечно.


          1. makondo
            22.08.2016 14:49

            Я был бы счастлив откомпилировать TensorFlow под виндой. С докером у меня что-то не получилось. Больше времени разбираться не оказалось.
            Если есть какая-нибудь информация — поделитесь, пожалуйста.
            Наверное, я бы мог поставить его под вируталку ubuntu на VM Box, но есть ли какие-нибудь известия насчет проброса использования GPU в гостевую систему?


            1. Parilo
              22.08.2016 16:22

              В VM Box скорее всего GPU не пробросить, а вот в виртуалке от Parallels раньше была функция проброса GPU, может быть стоит посмотреть в этом направлении