Как реализовать свой собственный Tensorflow на C++

искусственный интеллект TensorFlow C++ Нейронные сети

оригинал:How To Write Your Own Tensorflow in C++
Автор: Рэй Чжан.
Перевод: Нет помех моему полету

Аннотация: TensorFlow — система обучения искусственного интеллекта второго поколения, разработанная Google на основе DistBelief. Название происходит от собственного принципа работы. Это полностью открытый исходный код. Автор объяснил, как использовать C++ для реализации собственного TensorFlow в рамках небольшого проекта. Эта статья может показаться немного неясной, и вам нужно иметь о ней некоторое представление. Ниже перевод.

Прежде чем мы начнем, вот код:

  1. Branch with Eigen backend
  2. Branch that only supports scalars

я иMinh Leсделали этот проект вместе.

Зачем?

Если вы специализируетесь на CS, вы, вероятно, слышали фразу «не ввязывайся в _» бесчисленное количество раз. В CS есть шифрование, стандартная библиотека, парсер и т.д. Я чувствую, что библиотека ML также должна быть включена.

Независимо от фактов, это все еще удивительный урок, который нужно усвоить. Теперь люди воспринимают TensorFlow и подобные библиотеки как нечто само собой разумеющееся, рассматривая их как черный ящик, и пусть он работает. Мало кто знает, что происходит за кулисами. Это действительно невыпуклая задача оптимизации! Не переставайте перемешивать кучу, пока она не будет выглядеть правильно (понятно с диаграммой ниже и знанием системы машинного обучения).

这里写图片描述

Tensorflow

TensorFlow — это библиотека глубокого обучения с открытым исходным кодом от Google. В основе TensorFlow лежит большой компонент, объединяющий операции в строку.граф операторовс вещами. Этот операторный граф является ориентированным графом G =(V ,E ) , в некоторых узлах u 1 ,u 2 ,…,un ,v ∈ V и e 1 ,e 2 ,… ,en ∈E ,e i =( ui ,v ) Существуют определенные операторы, которые u 1 ,… ,un отображается в в.

Например, если мы имеем x + y = z, то (x,z),(y,z)∈E.

Это полезно для вычисления арифметических выражений. Мы можем сделать это, ищаsinksчтобы получить результат.Sinksтакие вершины. Другими словами, эти вершины не имеют направленных ребер к другим вершинам. такой же,sourcesда.

Для нас,всегдаПоместите значение в источники, и оно будет передано в приемники.

Вывод обратного режима

Если вы считаете, что мое объяснение недостаточно хорошо, вот несколькослайд-шоу.

Вывод является основным требованием многих моделей, требуемых TensorFlow, потому что он необходим для запускаАлгоритм градиентного спуска. Все, кто закончил среднюю школу, знают, что такое вывод; он просто берет производную функции, и если функция представляет собой сложную комбинацию основных функций, тоПравило цепи.

Супер простой обзор

Если есть такая функция:

f(x,y) = x * y

Тогда вывод относительно X даст:

Вывод по Y дает:

Другой пример:

Эта производная:

Итак, градиент:

Цепное правило, например, применяемое к сложным функциям:

Обратный режим за 5 минут

Теперь вспомните структуру DAG графа операторов и цепное правило из предыдущего примера. Если бы мы должны были оценить, мы могли бы увидеть:

x -> h -> g -> f

как график. даст ответ ф. Однако мы можем принять и обратное решение:

dx <- dh <- dg <- df

Это похоже на цепное правило! Производные нужно перемножить, чтобы получить окончательный результат.

На следующем рисунке показан пример графа оператора:

这里写图片描述

Таким образом, это в основном вырождается в задачу обхода графа.Кто-нибудь обнаружил топологическую сортировку и DFS/BFS?

Таким образом, для поддержки двунаправленной топологической сортировки вам необходимо включить набор родительских узлов и набор дочерних узлов, приемники являются источниками в другом направлении,наоборот.

воплощать в жизнь

Перед началом занятий мы с Минь Ле начали разрабатывать проект. Мы решили использовать бэкенд библиотеки Eigen для линейной алгебры. У них есть матричный класс под названием MatrixXd. Мы используем его здесь.

Каждый узел переменной представлен классом var:

class var {
// Forward declaration
struct impl;

public:
// For initialization of new vars by ptr
var(std::shared_ptr<impl>);

var(double);
var(const MatrixXd&);
var(op_type, const std::vector<var>&);    
...

// Access/Modify the current node value
MatrixXd getValue() const;
void setValue(const MatrixXd&);
op_type getOp() const;
void setOp(op_type);

// Access internals (no modify)
std::vector<var>& getChildren() const;
std::vector<var> getParents() const;
...
private: 
// PImpl idiom requires forward declaration of the     class:
std::shared_ptr<impl> pimpl;
};

struct var::impl{
public:
impl(const MatrixXd&);
impl(op_type, const std::vector<var>&);
MatrixXd val;
op_type op; 
std::vector<var> children;
std::vector<std::weak_ptr<impl>> parents;
};

Здесь мы принимаемpImplНа идиоме это означает «через указатель». Это очень хорошо во многих отношениях, например, реализация развязки интерфейса,Разрешить создание экземпляров в стеке, если в стеке есть собственный интерфейс оболочки. Побочным эффектом pImpl является то, что время выполнения немного медленнее, но время компиляции намного короче. Это позволяет нам сохранять структуру данных неизменной с помощью нескольких вызовов/возвратов функций. Такая древовидная структура данных должна быть постоянной.

есть несколькоперечислить, который сообщает нам, какие операции выполняются в данный момент:

enum class op_type {
plus,
minus,
multiply,
divide,
exponent,
log,
polynomial,
dot,
...
none // no operators. leaf.
};

Фактический класс, который выполняет эту оценку дерева, называется выражением:

class expression {
public:
expression(var);
...
// Recursively evaluates the tree.
double propagate();
...
// Computes the derivative for the entire graph.
// Performs a top-down evaluation of the tree.
void backpropagate(std::unordered_map<var, double>& leaves);
...    
private:
var root;
};

существуетобратное распространениеВнутри есть код, похожий на этот:

backpropagate(node, dprev):
derivative = differentiate(node)*dprev
for child in node.children:
    backpropagate(child, derivative)    

Это эквивалентно выполнению DFS, вы это видите?

Почему стоит выбрать С++?

На самом деле язык C++ для этого не особо подходит. мы можем потратитьменьше времениРазработан на функциональных языках, таких как OCaml. Теперь я понимаю, почему Scala используется для машинного обучения, вам решать, нравится ли это ;).

Тем не менее, C++ имеет явные преимущества:

Eigen

Например, вы можете напрямую использовать библиотеку линейной алгебры tensorflow под названием Eigen. Это библиотека линейной алгебры для ленивых вычислений с несколькими шаблонами. Подобно тому, как выглядит дерево выражения, выражение строится, и выражение вычисляется только тогда, когда это необходимо. Однако для ЭйгенаОпределите, когда шаблоны используются во время компиляции, что означает меньшее время выполнения. Я особенно ценю того, кто написал Eigen, потому что, глядя на ошибки шаблона, мои глаза наливались кровью.

Код Эйгена выглядит так:

Matrix A(...), B(...);
auto lazy_multiply = A.dot(B);
typeid(lazy_multiply).name(); // the class name is something like Dot_Matrix_Matrix.
Matrix(lazy_multiply); // functional-style casting forces evaluation of this matrix.

Библиотека Eigen очень мощная, поэтому она является основным бэкэндом, используемым самим tensorflow. Это означает, что помимо этой техники ленивых вычислений есть и другие аспекты оптимизации.

перегрузка оператора

Было бы здорово разработать эти библиотеки на Java - нетshared_ptrs, unique_ptrs, weak_ptrsкод; мы можем взятьПрактичный, компетентный алгоритм GC. Разработка на Java экономит много времени, не говоря уже о том, что выполнение становится быстрее. Однако Java не допускает перегрузку операторов, поэтому они не могут:

// These 3 lines code up an entire neural network!
var sigm1 = 1 / (1 + exp(-1 * dot(X, w1)));
var sigm2 = 1 / (1 + exp(-1 * dot(sigm1, w2)));
var loss = sum(-1 * (y * log(sigm2) + (1-y) * log(1-sigm2)));

Кстати, выше приведен фактический код. Разве это не красиво? я думаюЭто красивее, чем оболочка python для TensorFlow.. Просто хотел, чтобы вы знали, что это тоже матрицы.

В Java это было бы крайне некрасиво, с кучейadd(), divide()... и так далее по коду. Важнее,Пользователь неявно вынужден использовать PEMDAS (круглые скобки, возведение в степень, умножение, деление, сложение, вычитание), и операторы C++ хорошо для этого подходят.

Производительность, а не ошибки

Есть несколько вещей, которые вы можете указать в этой библиотеке, для которых TensorFlow не имеет явного API или о которых я не знаю. Например, если вы хотите тренировать веса для определенного подмножества, вы можете использовать обратную поддержку только для определенных источников, представляющих интерес. Это для сверточной нейронной сетиТрансферное обучение очень полезно, некоторые крупные сети, такие как сеть VGG19, легко реализуются с помощью TensorFlow с несколькими дополнительными слоями, веса которых обучаются на новых образцах предметной области.

ориентир

Используя библиотеку Python Tensorflow, обучение классификации на наборе данных Iris по 10 000 исторических эпох с теми же гиперпараметрами приводит к следующим результатам:

  1. Нейронные сети в Tensorflow23812.5 ms
  2. Библиотека нейронной сети Scikit:22412.2 ms
  3. Нейронная сеть Autodiff, итерация, оптимизация:25397.2 ms
  4. Нейронная сеть Autodiff, с итерацией, без оптимизации:29052.4 ms
  5. Нейронная сеть Autodiff, с рекурсией, без оптимизации:28121.5 ms

В этом свете удивительно, что Scikit работает быстрее всех. Это, вероятно, потому, что мы не делаем много матричного умножения. Это также может быть связано с тем, что tensorflown должен выполнить дополнительный шаг компиляции через инициализацию переменных. Или, возможно, вам придется запустить цикл в python, а не в C (цикл pythonдействительно плохо! ). Я сам не уверен, почему это так.

Я полностью осознаю, что это ни в коем случае не всеобъемлющий тест, поскольку он применяется только к одной точке данных в конкретной ситуации. Однако производительность этой библиотеки не самая современная, потому что мы не хотим заворачиваться в TensorFlow.