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 нам все таки не обойтись, так как на данный момент функционал по построению графа недоступен из С++. Поэтому наш план таков:
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)
BelBES
22.08.2016 10:38+1О, спасибо за пост) Python'нутость TensorFlow была одной из немногих причин, почему я все еще использую caffe вместо TF, теперь можно потихоньку начинать мигрировать)
з.ы. а как в этом фреймворке с производительностью на GPU? А то когда я последний раз интересовался, былижалобы с низким перформансом по сравнению с caffe. И там еще не появился централизованный зоопарк моделей?
Parilo
22.08.2016 11:22Я тоже видел жалобы на тормознутость на GPU. Но с другими фреймворками не сравнивал, пользуюсь только TensorFlow на данный момент. Было бы интересно посмотреть на какие-нибудь сравнительные тесты. Про централизованый зоопарк моделей тоже не слышал, но идея здравая. Может к версии 1.0 появится.
BelBES
22.08.2016 11:28+1Про централизованый зоопарк моделей тоже не слышал, но идея здравая. Может к версии 1.0 появится.
в caffe есть вот такая штука, а разработчикам TF уже и говорили про необходимость такого зоопарка, но что-то видно все никак не сделают. А то там глянешь, даже самых необходимых VGG16/ResNet/Inception etc. нету, все нужно из caffe конвертировать руками.
supersonic_snail
22.08.2016 15:59Вы врядли сейчас найдете что-то, что будет быстрее caffe для convolutional сетей. Просто потому, что с++ и боттленков практически нет, по сравнению с тем же питоном, где банальный цикл sgd выполняется медленнее, чем в с++.
Сам tf работает примерно так же, как и theano. Хотя тут тоже не без нюансов — ощущение, что tf будет лучше, если у вас большая инфрасктруктура и в целом нормально оттестированная и рабочая сеть. Если наоборот, то theano лучше. Ну и theano не сильно медленнее, чем caffe — я обучал первую версию Inception сети, получалось где-то на 10-15% медленнее, чем в caffe. С учетом того, что обучается оно с неделю, лишние 8-10 часов погоду не делают.UndeadDragon
26.08.2016 21:08Вы врядли сейчас найдете что-то, что будет быстрее caffe для convolutional сетей
Почему? «Потому, что С++»? А Торч вообще Си.
veydlin
22.08.2016 11:24-1Было бы здорово, если бы вы дополнили статью запуском на С++ в VS
Parilo
22.08.2016 11:33С VS, вероятно, так просто не получится, так как я не видел инструкций по сборке для Windows. Но, если возможно будет собрать Bazel, я думаю, что шанс будет. Однако хлебнуть приятных минут ковыряния глубоко в исходниках наверное придется. Я не пользуюсь ни Windows, ни VS. Так, что ничем, кроме как расплывчатыми советами помочь не могу.
SKolotienko
22.08.2016 13:42+1Насколько я знаю, сейчас есть большие сложности с TensorFlow под Windows. Что чуть ли не единственный вариант — запускать в каком-то виде эмулированном *nix-окружении и без возможности вычислений на GPU.
Parilo
22.08.2016 13:48Еще недавно не было возможности использовать GPU на маке, сейчас это уже есть. Может и для винды сделают какой-то готовый пакет. Хотя верится в это с трудом, конечно.
makondo
22.08.2016 14:49Я был бы счастлив откомпилировать TensorFlow под виндой. С докером у меня что-то не получилось. Больше времени разбираться не оказалось.
Если есть какая-нибудь информация — поделитесь, пожалуйста.
Наверное, я бы мог поставить его под вируталку ubuntu на VM Box, но есть ли какие-нибудь известия насчет проброса использования GPU в гостевую систему?Parilo
22.08.2016 16:22В VM Box скорее всего GPU не пробросить, а вот в виртуалке от Parallels раньше была функция проброса GPU, может быть стоит посмотреть в этом направлении
qwerty135
Почему бы двумя фразами в начале статьи не описать, что такое Google TensorFlow? И так многие делают, просто не объясняют во вступлении, о чём идёт речь. Тем самым сильно снижая эффективность публикаций.
thelongrunsmoke
Было бы странно видеть это в подобной статье на данном ресурсе.