转自:https://segmentfault.com/p/1210000018096964/read
PyTorch C++ 前端 是PyTorch机器学习框架的一个纯C++接口。PyTorch的主接口是Python,Python API位于一个基础的C++代码库之上,提供了基本的数据结构和功能,例如张量和自动求导。C++前端暴露了一个纯的C++11的API,在C++底层代码库之上扩展了机器学习训练和推理所需的工具扩展。这包括用于神经网络建模的内置组件集合;扩展此集合的自定义模块API;流行的优化算法库(如随机梯度下降);使用API定义和加载数据集的并行数据加载程序;序列化例行程序等等。
本教程将为您介绍一个用C++ 前端对模型进行训练的端到端示例。具体地说,我们将训练一个 DCGAN——一种生成模型——来生成 MNIST数字的图像。虽然看起来这是一个简单的例子,但它足以让你对 PyTorch C++ frontend有一个深刻的认识,并勾起你对训练更复杂模型的兴趣。我们将从设计它的原因开始,告诉你为什么你应该使用C++前端,然后直接深入解释和训练我们的模型。
小贴士
可以在 this lightning talk from CppCon 2018 网站观看有关C++前端的快速介绍。
小贴士
这份笔记提供了C++前端组件和设计理念的全面概述。
小贴士
在 https://pytorch.org/cppdocs你可以找到工作人员的API说明文档,这些PyTorch C++ 生态系统的文档是很有用的。
动机
在我们开始令人兴奋的GANs和MNIST数字的旅程之前,让我们往回看,讨论一下为什么我们一开始要使用C++前端而不是Python。我们(the PyTorch team)创建了C++前端,以便在不能使用Python的环境中或者是没有适合该作业的工具的情况下进行研究。此类环境的示例包括:
- **低延迟系统:**您可能希望在具有高帧/秒和低延迟的要求的纯C++游戏引擎中进行强化学习研究。由于Python解释器的速度慢,Python可能根本无法被跟踪,使用纯C++库这样的环境比Python库更合适。
- **高度多线程环境:**由于全局解释器锁(GIL),一次不能运行多个系统线程。多道处理是另一种选择,但它不具有可扩展性,并且有显著的缺点。C++没有这样的约束,线程易于使用和创建。需要大量并行化的模型,像那些用于深度神经进化 Deep Neuroevolution的模型,可以从中受益。
- **现有的C++代码库:**您可能是一个现有的C++应用程序的所有者,在后台服务器上为Web页面提供服务,以在照片编辑软件中绘制3D图形,并希望将机器学习方法集成到您的系统中。C++前端允许您保留在C++中,免除了在Python和C++之间来回绑定的麻烦,同时保留了传统 PyTorch(Python)体验的大部分灵活性和直观性。
C++前端不打算与Python前端竞争,它是为了补充Python前端。我们知道由于它简单、灵活和直观的API研究人员和工程师都喜欢PyTorch。我们的目标是确保您可以在每个可能的环境中利用这些核心设计原则,包括上面描述的那些。如果这些场景中的一个描述了你的用例,或者如果你只是感兴趣的话,跟着我们在下面的文章中详细探究C++前端。
小贴士
C++前端试图提供尽可能接近Python前端的API。如果你对Python前端有经验,并且想知道:“我如何用C++前端做这个东西?”你可以以Python的方式编写代码,在Python中,通常可以使用与C++相同的函数和方法(只要记住用双冒号替换点)。
编写基本应用程序
让我们开始编写一个小的C++应用程序,以验证我们在安装和构建环境上是一致的。首先,您需要获取 LibTorch分发的副本——我们已经准备好了ZIP存档,它封装了使用C++前端所需的所有相关的头文件、库和 CMake 构建文件。Libtorch发行版可在Linux, MacOS 和 Windows的PyTorch website上下载。本教程的其余部分将假设一个基本的Ubuntu Linux环境,您也可以在MacOS或Windows上继续自由地学习。
小贴士
关于安装PyTrac C++ 在 Installing C++ Distributions of PyTorch 的文档更详细地描述了以下步骤。
第一步是通过从PyTorch网站检索到的链接在本地下载 LibTorch发行版。对于普通的Ubuntu Linux环境,这意味着运行:
wget https://download.pytorch.org/libtorch/nightly/cpu/libtorch-shared-with-deps-latest.zip unzip libtorch-shared-with-deps-latest.zip
接下来,让我们编写一个名为 dcgan.cpp
的小型C++文件,它包括 torch/torch.h
,现在只需打印出三×三的身份矩阵:
#include <torch/torch.h> #include <iostream> int main() { torch::Tensor tensor = torch::eye(3); std::cout << tensor << std::endl; }
我们将使用CMakeLists.txt
文件构建这个小应用程序以及我们稍后的完整训练脚本:
cmake_minimum_required(VERSION 3.0 FATAL_ERROR) project(dcgan) find_package(Torch REQUIRED) add_executable(dcgan dcgan.cpp) target_link_libraries(dcgan "${TORCH_LIBRARIES}") set_property(TARGET dcgan PROPERTY CXX_STANDARD 11)
笔记
虽然CMake是LibTorch推荐的构建系统,但这并不是一个硬性要求。您还可以使用Visual Studio项目文件、Qmake、plain Makefiles或任何其他您觉得合适的构建环境。但是,我们不提供开箱即用的支持。
记下上述CMake文件中的第4行: find_package(Torch REQUIRED)
.。这将指示CMake查找LibTorch库的构建配置。为了让CMake知道在哪里找到这些文件,我们必须在调用 cmake
时设置 CMAKE_PREFIX_PATH
。在进行此操作之前,让我们就 dcgan
应用程序的以下目录结构达成一致:
dcgan/ CMakeLists.txt dcgan.cpp
此外,我将特别指出解压LibTorch分发的路径 /path/to/libtorch
。**请注意,这必须是绝对路径。**我们用编写 $PWD/../../libtorch
的做法获取相应的绝对路径;如果将 CMAKE_PREFIX_PATH
设置为../../libtorch
它将以意想不到的方式中断。现在,我们已经准备好构建我们的应用程序:
root@fa350df05ecf:/home# mkdir build root@fa350df05ecf:/home# cd build root@fa350df05ecf:/home/build# cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch .. -- The C compiler identification is GNU 5.4.0 -- The CXX compiler identification is GNU 5.4.0 -- Check for working C compiler: /usr/bin/cc -- Check for working C compiler: /usr/bin/cc -- works -- Detecting C compiler ABI info -- Detecting C compiler ABI info - done -- Detecting C compile features -- Detecting C compile features - done -- Check for working CXX compiler: /usr/bin/c++ -- Check for working CXX compiler: /usr/bin/c++ -- works -- Detecting CXX compiler ABI info -- Detecting CXX compiler ABI info - done -- Detecting CXX compile features -- Detecting CXX compile features - done -- Looking for pthread.h -- Looking for pthread.h - found -- Looking for pthread_create -- Looking for pthread_create - not found -- Looking for pthread_create in pthreads -- Looking for pthread_create in pthreads - not found -- Looking for pthread_create in pthread -- Looking for pthread_create in pthread - found -- Found Threads: TRUE -- Found torch: /path/to/libtorch/lib/libtorch.so -- Configuring done -- Generating done -- Build files have been written to: /home/build root@fa350df05ecf:/home/build# make -j Scanning dependencies of target dcgan [ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o [100%] Linking CXX executable dcgan [100%] Built target dcgan
在上文,我们首先在 dcgan
目录中创建了一个 build
文件夹,然后进入这个文件夹,运行 cmake
命令生成必要的build(Make)文件,最后通过运行 make -j
.成功编译了项目。现在,我们将项目设置为执行最小的二进制文件,基本项目配置这一部分就完成了:
root@fa350df05ecf:/home/build# ./dcgan 1 0 0 0 1 0 0 0 1 [ Variable[CPUFloatType]{3,3} ]
在我看来它就像一个身份矩阵!
定义神经网络模型
既然我们已经配置了基本环境,那么我们可以深入了解本教程中更有趣的部分。首先,我们将讨论如何在C++前端中定义和交互模块。我们将从基本的、小规模的示例模块开始,然后使用C++前端提供的内置模块的广泛库来实现一个成熟的GAN。
模块API基础知识
依据Python接口,基于C++前端的神经网络由可重用的模块组成,称为模块。它有一个基本模块类,从中派生所有其他模块。在Python中,这个类是 torch.nn.Module
,在C++中是 torch::nn::Module
模块。除了实现模块封装的算法的 forward()
方法外,模块通常还包含三种子对象:参数、缓冲区和子模块。
参数和缓冲区以张量的形式存储状态。参数记录,而缓冲区不记录。参数通常是神经网络的可训练权重。缓冲区的示例包括用于批处理规范化的平均值和方差。为了重用特定的逻辑块和状态块,PyTorch API允许嵌套模块。嵌套模块称为_子模块_。
必须显式注册参数、缓冲区和子模块。注册后,可以使用parameters()
or buffers()
等方法来检索整个(嵌套)模块层次结构中所有参数的容器。类似地,类似于 to(...)
的方法(例如 to(torch::kCUDA)
将所有参数和缓冲区从CPU移动到CUDA内存)在整个模块层次结构上工作。
定义模块并注册参数
为了将这些随机数放入代码中,让我们考虑一下在Python接口中编写这个简单模块:
import torch class Net(torch.nn.Module): def __init__(self, N, M): super(Net, self).__init__() self.W = torch.nn.Parameter(torch.randn(N, M)) self.b = torch.nn.Parameter(torch.randn(M)) def forward(self, input): return torch.addmm(self.b, input, self.W)
在C++中它长这样:
#include <torch/torch.h> struct Net : torch::nn::Module { Net(int64_t N, int64_t M) { W = register_parameter("W", torch::randn({N, M})); b = register_parameter("b", torch::randn(M)); } torch::Tensor forward(torch::Tensor input) { return torch::admm(b, input, W); } torch::Tensor W, b; };
就像在Python中一样,我们定义了一个类 Net
(为了简单起见,这里是 struct
而不是一个 class
)并从模块基类派生它。在构造函数内部,我们使用 torch::randn
创建张量,就像在Python中使用torch.randn
一样。一个有趣的区别是我们如何注册参数。在Python中,我们用torch.nn.Parameter
类来包装张量,而在C++中,我们必须通过 register_parameter
参数方法来传递张量。原因是Python API可以检测到属性的类型为 torch.nn.Parameter
,并自动注册这些张量。在C++中,反射是非常有限的,因此提供了一种更传统的(和不太神奇的)方法。
注册子模块并遍历模块层次结构
同样,我们可以注册参数,也可以注册子模块。在Python中,当子模块被指定为模块的属性时,将自动检测和注册子模块:
class Net(torch.nn.Module): def __init__(self, N, M): super(Net, self).__init__() # Registered as a submodule behind the scenes self.linear = torch.nn.Linear(N, M) self.another_bias = torch.nn.Parameter(torch.rand(M)) def forward(self, input): return self.linear(input) + self.another_bias
例如,这允许使用 parameters()
方法递归访问模块层次结构中的所有参数:
>>> net = Net(4, 5) >>> print(list(net.parameters())) [Parameter containing: tensor([0.0808, 0.8613, 0.2017, 0.5206, 0.5353], requires_grad=True), Parameter containing: tensor([[-0.3740, -0.0976, -0.4786, -0.4928], [-0.1434, 0.4713, 0.1735, -0.3293], [-0.3467, -0.3858, 0.1980, 0.1986], [-0.1975, 0.4278, -0.1831, -0.2709], [ 0.3730, 0.4307, 0.3236, -0.0629]], requires_grad=True), Parameter containing: tensor([ 0.2038, 0.4638, -0.2023, 0.1230, -0.0516], requires_grad=True)]
为了在C++中注册子模块,使用恰当命名的 register_module()
方法注册一个就像 torch::nn::Linear
:的模块。
struct Net : torch::nn::Module { Net(int64_t N, int64_t M) : linear(register_module("linear", torch::nn::Linear(N, M))) { another_bias = register_parameter("b", torch::randn(M)); } torch::Tensor forward(torch::Tensor input) { return linear(input) + another_bias; } torch::nn::Linear linear; torch::Tensor another_bias; };
小贴士
您可以在这里的 torch::nn
命名空间文档中找到可用内置模块的完整列表,如 torch::nn::Linear
, torch::nn::Dropout
和 torch::nn::Conv2d
。
上面代码的一个微妙之处就是,为什么我们要在构造函数的初始值设定项列表中创建子模块,而在构造函数主体中创建参数。这是一个很好的理由,我们将在下面进一步讨论C++前端的 ownership model 。最终,我们可以像在Python中那样递归地访问树的模块的参数。调用参数 parameters()
返回一个我们可以迭代的 std::vector<torch::Tensor>
:
int main() { Net net(4, 5); for (const auto& p : net.parameters()) { std::cout << p << std::endl; } }
输出的结果是:
root@fa350df05ecf:/home/build# ./dcgan 0.0345 1.4456 -0.6313 -0.3585 -0.4008 [ Variable[CPUFloatType]{5} ] -0.1647 0.2891 0.0527 -0.0354 0.3084 0.2025 0.0343 0.1824 -0.4630 -0.2862 0.2500 -0.0420 0.3679 -0.1482 -0.0460 0.1967 0.2132 -0.1992 0.4257 0.0739 [ Variable[CPUFloatType]{5,4} ] 0.01 * 3.6861 -10.1166 -45.0333 7.9983 -20.0705 [ Variable[CPUFloatType]{5} ]
就像在Python中一样这里有三个参数。为了看到这些参数的名称,C++ API提供了一个 named_parameters()
参数方法,它像Python一样返回 named_parameters()
:
Net net(4, 5); for (const auto& pair : net.named_parameters()) { std::cout << pair.key() << ": " << pair.value() << std::endl; }
我们可以再次执行来查看输出:
root@fa350df05ecf:/home/build# make && ./dcgan 11:13:48 Scanning dependencies of target dcgan [ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o [100%] Linking CXX executable dcgan [100%] Built target dcgan b: -0.1863 -0.8611 -0.1228 1.3269 0.9858 [ Variable[CPUFloatType]{5} ] linear.weight: 0.0339 0.2484 0.2035 -0.2103 -0.0715 -0.2975 -0.4350 -0.1878 -0.3616 0.1050 -0.4982 0.0335 -0.1605 0.4963 0.4099 -0.2883 0.1818 -0.3447 -0.1501 -0.0215 [ Variable[CPUFloatType]{5,4} ] linear.bias: -0.0250 0.0408 0.3756 -0.2149 -0.3636 [ Variable[CPUFloatType]{5} ]
笔记
torch::nn::Module
的文档 包含在模块层次结构上操作的方法的完整清单。
在正向模式中运行网络
为了在C++中运行网络,我们只需调用我们定义的 forward()
方法:
int main() { Net net(4, 5); std::cout << net.forward(torch::ones({2, 4})) << std::endl; }
输出内容如下:
root@fa350df05ecf:/home/build# ./dcgan 0.8559 1.1572 2.1069 -0.1247 0.8060 0.8559 1.1572 2.1069 -0.1247 0.8060 [ Variable[CPUFloatType]{2,5} ]
模块所有权
现在,我们知道如何定义C++中的模块、寄存器参数、寄存器子模块、通过参数 parameters()
等方法遍历模块层次结构,和最后运行模块的 forward()
方法。在C++ API中有更多的方法、类和主题要我们思考,但接下来我会向你介绍完整清单 文档 。我们在一秒钟内实现 DCGAN模型和端到端训练管道的同时,还将涉及更多的概念。在我们这样做之前,让我简单地介绍一下C++前端的所有权模型,它提供了 torch::nn::Module
.模块的子类。
对于这个论述,所有权模型指的是模块的存储和传递方式,它决定了谁或什么拥有一个特定的模块实例。在Python中,对象总是动态分配(在堆上)并具有引用语义。这很容易操作,也很容易理解。事实上,在Python中,您大可以忘记对象的位置以及它们是如何被引用的,而更专注于完成工作。
C++是一种这个领域提供了更多的选择的低级语言。它更加了复杂,并严重影响了C++前端的设计和人机工程学。特别地,对于C++前端中的模块,我们可以选择使用值语义或引用语义。第一种情况是最简单的,并在迄今为止的示例中显示:当传递给函数时,在堆栈上分配的模块对象,可以复制、移动(使用 std::move
))或通过引用和指针获取:
struct Net : torch::nn::Module { }; void a(Net net) { } void b(Net& net) { } void c(Net* net) { } int main() { Net net; a(net); a(std::move(net)); b(net); c(&net); }
对于第二种情况——引用语义——我们可以使用 std::shared_ptr
.。引用语义的优点在于,与Python一样,它减少了认知模块如何传递给函数以及如何声明参数(假设在任何地方都使用shared_ptr
)。