Вступление

Одним из основных усовершенствований недавно выпущенного PyTorch 1.5 является стабильный паритет API интерфейса C ++ с Python ¹. Интерфейс API C ++ хорошо работает с системами с низкой задержкой, многопоточными средами, существующими базами кода C ++, вы можете проверить мотивацию и варианты использования интерфейса C ++ здесь ³. Я хочу познакомиться с API внешнего интерфейса PyTorch C ++, создав небольшой пример. Итак, я взял простой пример двухслойной нейронной сети из Learning PyTorch с примерами ². В оставшейся части этого поста подробно описаны шаги по преобразованию двухуровневой нейронной сети с использованием примера API интерфейса Python для работы с API интерфейса C ++. Полный пример кода с инструкциями по запуску кода подробно описан в репозитории Github ⁴.

Обновление: график измерения производительности добавлен 05.04.2020.

В PyTorch есть три основных компонента: тензоры, модуль автоматического дифференцирования (autograd) и nn (нейронная сеть).

Тензоры

Python - это динамический язык, и ему не нужно объявлять типы данных для переменных. Где как C ++ - это статически типизированный язык с безопасностью типов для создания скомпилированного и оптимизированного кода, который помогает создавать быстрые и требовательные приложения. Итак, первый шаг, который нужно сделать при преобразовании кода Python, - это начать определение типов данных. Современный C ++ также определяет спецификатор типа auto, который помогает компилятору определить тип во время компиляции.

Начнем с создания случайных входных и выходных данных тензора для обучения нейронной сети отображению x в y, то есть для изучения функции f который определяет y = f (x).

# Python
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)

Тензоры в C ++ используют оператор разрешения области видимости :: вместо точечной нотации.

// C++
torch::Tensor x = torch::rand({N, D_in});
torch::Tensor y = torch::rand({N, D_out});

Модель двухуровневой нейронной сети

В Python нейронные сети являются производными от многоразового базового модуля torch.nn.Module. Подмодули автоматически обнаруживаются и регистрируются с помощью оболочек, когда они назначаются в качестве атрибута модуля. Вот простая двухуровневая сеть с использованием интерфейса Python.

import torch
# Python neural network model defined as a class
class TwoLayerNet(torch.nn.Module):
  def __init__(self, D_in, H, D_out):
    super(TwoLayerNet, self).__init__()
    self.linear1 = torch.nn.Linear(D_in, H)
    self.linear2 = torch.nn.Linear(H, D_out)
  def forward(self, x):
    h_relu = self.linear1(x).clamp(min=0)
    y_pred = self.linear2(h_relu)
    return y_pred

Модули нейронной сети реализованы в виде структур на C ++. Есть два способа определить модули, используя либо семантику значений, либо ссылочную семантику. Сначала мы рассмотрим пример создания модуля с семантикой значений.

#include <torch/torch.h>
// C++ neural network model defined with value semantics
struct TwoLayerNet : torch::nn::Module {
  // constructor with submodules registered in the initializer list
  TwoLayerNet(int64_t D_in, int64_t D_out, int64_t H) :
    linear1(register_module("linear1", torch::nn::Linear(D_in, H))),
    linear2(register_module("linear2", torch::nn::Linear(H, D_out)))
    {}
  torch::Tensor forward(torch::Tensor x) {
    x = torch::relu(linear1->forward(x));
    x = linear2->forward(x);
    return x;
  }
  torch::nn::Linear linear1;
  torch::nn::Linear linear2;
};
// Usage: access the object using dot . operator
TwoLayerNet model(D_in, D_out, H);
model.to(device);
model.forward(x);
model.parameters();

Для регистрации подмодулей в C ++ метод register_module(), определенный в списке инициализаторов конструкторов, позволяет рекурсивно обращаться к параметрам дерева модулей.

/// Registers a submodule with this `Module`.
///
/// This method deals with `ModuleHolder`s.
///
/// Registering a module makes it available to methods such as 
/// `modules()`, `clone()` or `to()`.
///
/// \rst
/// .. code-block:: cpp
///
///   MyModule::MyModule() {
///     submodule_ = register_module("linear", torch::nn::Linear(3, 4));
///   }
/// \endrst
template <typename ModuleType>
std::shared_ptr<ModuleType> register_module(
    std::string name,
    ModuleHolder<ModuleType> module_holder);

Рекомендуется определять модули C ++ с использованием эталонной семантики с std::shared_ptr API модульного держателя. Макрос TORCH_MODULE(TwoLayerNet) определяет класс TwoLayerNet. Этот «сгенерированный» класс фактически является оболочкой над std::shared_ptr<TwoLayerNetImpl>. Это упрощает кодирование, вместо того, чтобы писать std::make_shared<TwoLayerNetImpl>(1000, 10, 64), вы можете писатьTwoLayerNet(1000, 10, 64). Ссылочная семантика - это рекомендуемый способ определения модулей с помощью интерфейса API C ++.

// C++ neural network model defined with reference semantics
struct TwoLayerNetImpl : torch::nn::Module {
  // module holders are assigned in the constructor
  TwoLayerNetImpl(int64_t D_in, int64_t D_out, int64_t H) :    
    linear1(D_in, H), linear2(H, D_out) {
      register_module("linear1", linear1);
      register_module("linear2", linear2);
  }
  ... // forward() method
  
  torch::nn::Linear linear1{nullptr}; //construct an empty holder
  torch::nn::Linear linear2{nullptr}; //construct an empty holder
};
TORCH_MODULE(TwoLayerNet);
// Usage: access the object using arrow -> operator
TwoLayerNet model(D_in, D_out, H);
model->to(device);
model->forward(x);
model->parameters();

Оптимизатор

Код оптимизатора очень похож на Python и C ++.

# Python SGD optimizer
 
learning_rate = 1e-4
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate, momentum=0.5)
# Zero the gradients before running the backward pass
optimizer.zero_grad()
# Update weights
optimizer.step()

In C++:

// C++ SGD optimizer
float_t learning_rate = 1e-4;
torch::optim::SGD optimizer(
  model.parameters(),
  torch::optim::SGDOptions(learning_rate).momentum(0.5));
// Zero the gradients before running the backward pass
optimizer.zero_grad()
// Update the weights
optimizer.step()

2x + улучшение производительности

Я построил график времени обучения как для Python, так и для C ++, варьируя количество эпох от 500 до 5000 на компьютере Ubuntu 18.04 LTS Intel IvyBridge. Я вижу, что обучение модели C ++ происходит в 2 раза быстрее по сравнению с моделью Python.

В заключение, PyTorch поддерживает паритет между интерфейсами Python и C ++. Интерфейс C ++ в большинстве случаев следует дизайну и эргономике интерфейса Python. Код Python и C ++ для этого простого примера нейронной сети представлен в репозитории Github .

использованная литература

  1. Https://pytorch.org/blog/pytorch-1-dot-5-released-with-new-and-updated-apis/
  2. Джастин Джонсон. Изучение PyTorch на примерах.
  3. Использование внешнего интерфейса PyTorch C ++
  4. Примеры кода: https://github.com/venkatacrc/PyTorchCppFrontEnd