使用 PyTorch C++ 前端¶
创建时间: 2019年1月15日 |上次更新时间:2024 年 12 月 18 日 |上次验证: Nov 05, 2024
PyTorch C++ 前端是 PyTorch 机器学习的纯 C++ 接口 框架。虽然 PyTorch 的主要接口自然是 Python,但此 Python API 位于提供基础数据的大量 C++ 代码库之上 结构和功能,例如张量和自动微分。这 C++ 前端公开了一个纯 C++11 API,用于扩展此底层 C++ 代码库 使用机器学习训练和推理所需的工具。这包括 用于神经网络建模的常用组件的内置集合;一个 API 用于 使用自定义模块扩展此集合;流行优化库 随机梯度下降等算法;一个并行数据加载器,具有 用于定义和加载数据集的 API;序列化例程等。
本教程将引导您完成训练模型的端到端示例 与 C++ 前端一起使用。具体来说,我们将训练一个 DCGAN(一种生成模型),以 生成 MNIST 数字的图像。虽然从概念上讲是一个简单的示例,但它应该 足以为您提供 PyTorch C++ 前端和 wet 的旋风式概述 您对训练更复杂的模型的兴趣。我们将从一些 激励您为什么首先要使用 C++ 前端, 然后直接开始定义和训练我们的模型。
提示
观看 CppCon 2018 的闪电演讲,快速(且幽默) 在 C++ 前端上的演示文稿。
提示
本说明提供了一个全面的 C++ 前端的组件和设计理念概述。
提示
有关 PyTorch C++ 生态系统的文档,请访问 https://pytorch.org/cppdocs。在那里,您可以找到高级描述 以及 API 级文档。
赋予动机¶
在我们开始激动人心的 GAN 和 MNIST 数字之旅之前,让我们先来了解一下 退后一步,讨论为什么您希望使用 C++ 前端而不是 Python 首先。我们(PyTorch 团队)创建了 C++ 前端,以 在无法使用 Python 或根本不使用 Python 的环境中进行研究 适合这项工作的工具。此类环境的示例包括:
低延迟系统:您可能希望在 具有高每秒帧数和低延迟的纯 C++ 游戏引擎 要求。使用纯 C++ 库更适合这样的 environment 而不是 Python 库。Python 可能根本无法处理,因为 Python 解释器的速度慢。
高度多线程环境:由于全局解释器锁 (GIL) 中,Python 不能一次运行多个系统线程。 多处理是一种替代方案,但可扩展性不高,并且具有重要的 缺点。C++ 没有这样的约束,线程易于使用并且 创造。需要大量并行化的模型,如 Deep 中使用的模型 神经进化,可以从中受益 这。
现有 C++ 代码库:您可能是现有 C++ 的所有者 应用程序执行任何操作,从在后端服务器中提供网页到 在照片编辑软件中渲染 3D 图形,并希望集成 机器学习方法导入到您的系统中。C++ 前端允许您 留在 C++ 中,省去在两者之间来回绑定的麻烦 Python 和 C++,同时保留了 传统的 PyTorch (Python) 体验。
C++ 前端并非旨在与 Python 前端竞争。是的 旨在补充它。我们知道研究人员和工程师都喜欢 PyTorch 它的简单性、灵活性和直观的 API。我们的目标是确保您能够 在每一个可能的环境中利用这些核心设计原则, 包括上述那些。如果以下场景之一描述了您的用途 case well,或者如果您只是感兴趣或好奇,请跟随我们 在以下段落中详细探索 C++ 前端。
提示
C++ 前端尝试提供尽可能接近 Python 前端。如果您对 Python 前端有经验并且曾经问过 自己 “how do I do X with the C++ frontend?”,按照你的方式编写你的代码 ,并且通常相同的函数和方法会 在 C++ 中可用,就像在 Python 中一样(请记住将点替换为 double 冒号)。
编写基本应用程序¶
让我们首先编写一个最小的 C++ 应用程序,以验证我们是否在 关于我们的设置和构建环境的同一页面。首先,您需要 获取 LibTorch 发行版的副本 – 我们现成的 zip 存档 打包所需的所有相关头文件、库和 CMake 构建文件 C++ 前端。LibTorch 发行版可在适用于 Linux、MacOS 的 PyTorch 网站上下载 和 Windows。本教程的其余部分将假设有一个基本的 Ubuntu Linux 环境中,但是您也可以在 MacOS 或 Windows 上自由地进行操作。
提示
有关安装 PyTorch 的 C++ 发行版的说明介绍了以下步骤 更详细地。
提示
在 Windows 上,调试和发布版本与 ABI 不兼容。如果您打算
在 Debug 模式下构建您的项目,请尝试 LibTorch 的 debug 版本。
此外,请确保在下面的行中指定正确的配置。cmake --build .
第一步是通过以下链接在本地下载 LibTorch 发行版 从 PyTorch 网站检索。对于普通的 Ubuntu Linux 环境,此 表示运行:
# If you need e.g. CUDA 9.0 support, please replace "cpu" with "cu90" in the URL below.
wget https://download.pytorch.org/libtorch/nightly/cpu/libtorch-shared-with-deps-latest.zip
unzip libtorch-shared-with-deps-latest.zip
接下来,让我们编写一个名为 include 的小型 C++ 文件,现在只需打印出一个 3 x 3 的标识
矩阵:dcgan.cpp
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 14)
注意
虽然 CMake 是 LibTorch 的推荐构建系统,但它并不难 要求。您还可以使用 Visual Studio 项目文件、QMake、plain Makefiles 或您觉得舒服的任何其他构建环境。然而 我们不提供对此的开箱即用支持。
记下上述 CMake 文件中的第 4 行:。
这会指示 CMake 查找 LibTorch 库的构建配置。
为了让 CMake 知道在哪里可以找到这些文件,我们必须在调用时设置 .在我们这样做之前,让我们就
应用程序的以下目录结构:find_package(Torch REQUIRED)
CMAKE_PREFIX_PATH
cmake
dcgan
dcgan/
CMakeLists.txt
dcgan.cpp
此外,我将解压缩的 LibTorch 发行版的路径称为 。请注意,这必须是绝对路径。在
特别是,设置为 like 会以意想不到的方式中断。相反,写入以获取
相应的绝对路径。现在,我们已准备好构建我们的应用程序:/path/to/libtorch
CMAKE_PREFIX_PATH
../../libtorch
$PWD/../../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# cmake --build . --config Release
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
在上面,我们首先在我们的目录
进入此文件夹,运行命令以生成必要的构建
(Make) 文件,最后通过运行 .我们现在都准备好执行我们的最小二进制文件了
并完成本节的 Basic Project Configuration:build
dcgan
cmake
cmake
--build . --config Release
root@fa350df05ecf:/home/build# ./dcgan
1 0 0
0 1 0
0 0 1
[ Variable[CPUFloatType]{3,3} ]
对我来说看起来像一个单位矩阵!
定义神经网络模型¶
现在我们已经配置了基本环境,我们可以深入研究 本教程中更多有趣的部分。首先,我们将讨论如何定义 并与 C++ 前端中的模块交互。我们将从 basic 开始, 小规模示例模块,然后使用 C++ 前端提供的大量内置模块库。
模块 API 基础¶
与 Python 接口一致,基于 C++ 前端的神经网络是
由称为模块的可重用构建块组成。有一个基本模块
类。在 Python 中,这个类是 ,在 C++ 中是 。除了实现模块封装的算法的方法外,还有一个
module 通常包含以下三种子对象中的任何一种:parameters、buffers
和子模块。torch.nn.Module
torch::nn::Module
forward()
参数和缓冲区以张量的形式存储状态。参数记录 gradients,而缓冲区则没有。参数通常是 您的神经网络。缓冲区的示例包括 batch 的均值和方差 正常化。为了重用特定的 logic 和 state 块, PyTorch API 允许对模块进行嵌套。嵌套模块称为子模块。
参数、缓冲区和子模块必须显式注册。一次
registered,方法(如 or 可用于
检索整个 (嵌套) 模块层次结构中所有参数的容器。
同样,像 , 这样的方法,例如 移动全部
参数和缓冲区,适用于整个模块
等级制度。parameters()
buffers()
to(...)
to(torch::kCUDA)
定义模块并注册参数¶
为了将这些单词放入代码中,让我们考虑一下这个用 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::addmm(b, input, W);
}
torch::Tensor W, b;
};
就像在 Python 中一样,我们定义了一个名为 (为简单起见,这里是 a 而不是 a ) 的类,并从模块基类派生它。
在构造函数中,我们像
在 Python 中使用。一个有趣的区别是我们如何注册
参数。在 Python 中,我们使用类包装张量,而在 C++ 中,我们必须通过方法传递张量。这样做的原因是 Python
API 可以检测到属性的类型为
自动注册此类 Tensor。在 C++ 中,反射非常有限,因此
提供了更传统(且不那么神奇)的方法。Net
struct
class
torch::randn
torch.randn
torch.nn.Parameter
register_parameter
torch.nn.Parameter
注册子模块并遍历模块层次结构¶
就像我们可以注册参数一样,我们也可以注册子模块。在 Python 时,会自动检测并注册子模块。 assigned 作为模块的属性:
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
例如,这允许使用方法递归地
访问我们的 Module 层次结构中的所有参数: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::Linear
torch::nn::Dropout
torch::nn::Conv2d
torch::nn
上述代码的一个微妙之处是,为什么在
constructor 的 initializer 列表中,而参数是在
构造函数 body 的 Body。这是有充分理由的,我们将对此进行讨论
在下面的 C++ 前端的所有权模型一节中。结束
然而,结果是我们可以递归地访问模块树的参数
就像在 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 提供了一个方法,该方法返回
就像在 Python 中一样:named_parameters()
OrderedDict
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++ 定义模块、注册参数、
register submodules,通过 methods 遍历 module 层次结构,最后运行 module 的方法。虽然在那里
在 C++ API 中还有更多方法、类和主题可供吞噬,我将参考
您到文档
完整菜单。在实现
DCGAN 模型和端到端训练管道。在我们这样做之前,
让我简要地谈谈 C++ 前端提供的所有权模型
的子类。parameters()
forward()
torch::nn::Module
在本次讨论中,所有权模型是指模块的存储方式 和 passed around —— 这决定了谁或什么拥有特定模块 实例。在 Python 中,对象总是动态分配的(在堆上),并且 具有引用语义。这非常易于使用且简单明了 理解。事实上,在 Python 中,你基本上可以忘记对象的位置 以及他们如何获得参考,并专注于完成工作。
C++ 作为一种较低级别的语言,在此领域提供了更多选择。这
增加复杂性并严重影响 C++ 的设计和人体工程学
前端。特别是,对于 C++ 前端中的模块,我们可以选择
使用值语义或引用语义。第一种情况是
最简单的 ,到目前为止的示例中显示了:模块对象在
堆栈 and 在传递给函数时,可以被复制、移动(使用 )或通过引用或指针获取: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);
}
对于第二种情况 - 引用语义 - 我们可以使用 .
引用语义的优点是,就像在 Python 中一样,它减少了
考虑如何将模块传递给函数的认知开销,以及
参数必须如何声明(假设你到处都使用)。std::shared_ptr
shared_ptr
struct Net : torch::nn::Module {};
void a(std::shared_ptr<Net> net) { }
int main() {
auto net = std::make_shared<Net>();
a(net);
}
根据我们的经验,来自动态语言的研究人员非常喜欢
reference semantics over value semantics,即使后者更
“native” 添加到 C++。还需要注意的是
设计,为了更贴近 Python API 的人体工程学,依赖于
共享所有权。例如,以我们之前(此处缩写)的定义 为例:torch::nn::Module
Net
struct Net : torch::nn::Module {
Net(int64_t N, int64_t M)
: linear(register_module("linear", torch::nn::Linear(N, M)))
{ }
torch::nn::Linear linear;
};
为了使用子模块,我们想把它直接存储在
类。但是,我们还希望 module 基类能够了解并有权访问
添加到此子模块中。为此,它必须存储对此子模块的引用。在
在这一点上,我们已经达到了共享所有权的需求。类和具体类都需要对
子模块。因此,基类将模块存储为 s,因此具体类也必须存储。linear
torch::nn::Module
Net
shared_ptr
但是等等!我在上面的代码中没有看到任何提及!为什么
那?嗯,因为打字要打字的东西太多了。自
为了提高研究人员的工作效率,我们想出了一个精心设计的方案来隐藏
提及 – 通常为值语义保留的好处 –
同时保留引用语义。为了理解它是如何工作的,我们可以采用
查看 core 中 module 的简化定义
库(完整定义在这里):shared_ptr
std::shared_ptr<MyModule>
shared_ptr
torch::nn::Linear
struct LinearImpl : torch::nn::Module {
LinearImpl(int64_t in, int64_t out);
Tensor forward(const Tensor& input);
Tensor weight, bias;
};
TORCH_MODULE(Linear);
简而言之:该模块不称为 ,而是 。然后,宏定义实际的类。这个 “生成”
class 实际上是 .它是一个
wrapper 而不是简单的 typedef,因此,除其他外,构造函数
仍然按预期工作,即您仍然可以写入 而不是 .我们将创建的类称为
宏 模块支架。与使用 (shared) 指针一样,您可以访问
使用箭头运算符的底层对象(如 )。这
最终结果是一个类似于 Python API 的所有权模型
密切。引用语义成为默认值,但没有额外的 or 键入。对于我们的 ,使用模块
holder API 如下所示:Linear
LinearImpl
TORCH_MODULE
Linear
std::shared_ptr<LinearImpl>
torch::nn::Linear(3, 4)
std::make_shared<LinearImpl>(3, 4)
model->forward(...)
std::shared_ptr
std::make_shared
Net
struct NetImpl : torch::nn::Module {};
TORCH_MODULE(Net);
void a(Net net) { }
int main() {
Net net;
a(net);
}
这里有一个微妙的问题值得一提。默认构造为 “empty”,即包含一个 null 指针。什么是默认值
constructed 还是 ?嗯,这是一个棘手的选择。我们可以这么说
应为空 (null) 。但是,回想一下,这与 相同。这
意味着如果我们决定 应该是 null 指针,
那么就没有办法构造一个不采用任何
constructor 参数,或者默认所有参数。因此,在当前的
API 中,默认构造的模块持有者(如 )会调用
底层模块 () 的 default 构造函数。如果
底层模块没有默认构造函数,则会收到编译器错误。
要构造空 holder,您可以传递给
Holder 的构造器。std::shared_ptr
Linear
Net
std::shared_ptr<LinearImpl>
Linear(3, 4)
std::make_shared<LinearImpl>(3, 4)
Linear linear;
Linear()
LinearImpl()
nullptr
在实践中,这意味着您可以使用子模块,如前所述,其中 该模块在 Initializer 列表中注册和构造:
struct Net : torch::nn::Module {
Net(int64_t N, int64_t M)
: linear(register_module("linear", torch::nn::Linear(N, M)))
{ }
torch::nn::Linear linear;
};
或者,您可以先使用 null 指针构造 holder,然后分配给它 在构造函数中(Pythonistas 更熟悉):
struct Net : torch::nn::Module {
Net(int64_t N, int64_t M) {
linear = register_module("linear", torch::nn::Linear(N, M));
}
torch::nn::Linear linear{nullptr}; // construct an empty holder
};
结论:您应该使用哪种所有权模型 – 哪种语义?这
C++ 前端的 API 最能支持模块持有者提供的所有权模型。
这种机制的唯一缺点是下面多了一行样板
模块声明。也就是说,最简单的模型仍然是值
C++ 模块简介中显示的语义模型。适用于小巧、简单的
脚本,你也可以侥幸逃脱。但你迟早会发现,因为
由于技术原因,它并不总是受支持。例如,序列化
API( 和 )仅支持模块持有者(或 plain )。因此,模块持有者 API 是推荐的方式
使用 C++ 前端定义模块,我们将在此 API 中使用
从此 tutorial 的 .torch::save
torch::load
shared_ptr
定义 DCGAN 模块¶
我们现在有了定义 我们在本文中要解决的机器学习任务。回顾一下:我们的任务是 从 MNIST 数据集生成数字图像。我们想使用生成对抗 网络 (GAN) 求解 这个任务。具体而言,我们将使用 DCGAN 架构 – 这是最早也是最简单的架构之一 kind 的,但完全足以完成这项任务。
提示
您可以在以下位置找到本教程中介绍的完整源代码 存储库。
什么是 GAN aGAN?¶
GAN 由两个不同的神经网络模型组成:生成器和判别器。发生器从噪声分布中接收样本,并且
其目的是将每个噪声样本转换为类似于
目标分布 – 在我们的例子中是 MNIST 数据集。鉴别器
turn 接收来自 MNIST 数据集的真实图像,或从
生成器。它被要求发出一个概率,以判断特定图像的真实 (更接近) 或虚假 (更接近) 的程度。来自
生成器生成的图像的真实性的判别器用于
训练生成器。关于真伪眼光如何的反馈
discriminator has 用于优化判别器。理论上,一个精致的
生成器和鉴别器之间的平衡使它们同步改进,
导致生成器生成与目标无法区分的图像
分布,欺骗判别器(到那时)出色的眼睛发出
真实图像和虚假图像的概率。对我们来说,最终结果
是一台接收噪声作为输入并生成逼真图像的机器
digits 作为其输出。1
0
0.5
生成器模块¶
我们首先定义 generator 模块,它由一系列
转置 2D 卷积、批量归一化和 ReLU 激活单元。
我们在我们自己定义的模块的方法中(以功能方式)在模块之间显式传递输入:forward()
struct DCGANGeneratorImpl : nn::Module {
DCGANGeneratorImpl(int kNoiseSize)
: conv1(nn::ConvTranspose2dOptions(kNoiseSize, 256, 4)
.bias(false)),
batch_norm1(256),
conv2(nn::ConvTranspose2dOptions(256, 128, 3)
.stride(2)
.padding(1)
.bias(false)),
batch_norm2(128),
conv3(nn::ConvTranspose2dOptions(128, 64, 4)
.stride(2)
.padding(1)
.bias(false)),
batch_norm3(64),
conv4(nn::ConvTranspose2dOptions(64, 1, 4)
.stride(2)
.padding(1)
.bias(false))
{
// register_module() is needed if we want to use the parameters() method later on
register_module("conv1", conv1);
register_module("conv2", conv2);
register_module("conv3", conv3);
register_module("conv4", conv4);
register_module("batch_norm1", batch_norm1);
register_module("batch_norm2", batch_norm2);
register_module("batch_norm3", batch_norm3);
}
torch::Tensor forward(torch::Tensor x) {
x = torch::relu(batch_norm1(conv1(x)));
x = torch::relu(batch_norm2(conv2(x)));
x = torch::relu(batch_norm3(conv3(x)));
x = torch::tanh(conv4(x));
return x;
}
nn::ConvTranspose2d conv1, conv2, conv3, conv4;
nn::BatchNorm2d batch_norm1, batch_norm2, batch_norm3;
};
TORCH_MODULE(DCGANGenerator);
DCGANGenerator generator(kNoiseSize);
现在,我们可以调用 将噪声样本映射到图像。forward()
DCGANGenerator
选择的特定模块,如 和 ,
遵循前面概述的结构。常量确定
输入噪声向量的大小,并设置为 。超参数是,
当然,是通过研究生血统找到的。nn::ConvTranspose2d
nn::BatchNorm2d
kNoiseSize
100
注意力
没有研究生在发现超参数时受到伤害。他们是 定期喂食 Soylent。
注意
关于选项传递给内置模块的方式,如 C++ 前端中的选项:每个模块都有一些必需的选项,例如数字
的特征。如果只需要配置所需的
选项,你可以将它们直接传递给模块的构造函数,比如 OR 或 (for input
通道计数、输出通道计数和内核大小)。但是,如果您需要
要修改其他选项(通常是默认的),例如 for ,您需要构造并传递一个 options 对象。每
C++前端的模块有一个关联的 options 结构,称为 where 是模块的名称,类似于 。这就是我们对上述模块所做的。Conv2d
BatchNorm2d
BatchNorm2d(128)
Dropout(0.5)
Conv2d(8, 4, 2)
bias
Conv2d
ModuleOptions
Module
LinearOptions
Linear
Conv2d
鉴别器模块¶
判别器同样是一系列卷积、批量归一化 和激活。但是,卷积现在是常规卷积,而不是 转置,我们使用 alpha 值为 0.2 的泄漏 ReLU,而不是 原版 ReLU。此外,最终激活会变成 Sigmoid,它会压扁 值设置为 0 到 1 之间的范围。然后我们可以解释这些压缩的值 作为判别器分配给图像为真实的概率。
为了构建判别器,我们将尝试一些不同的东西:一个 Sequential 模块。 与 Python 一样,PyTorch 在这里提供了两个用于模型定义的 API:一个函数式 API 其中 inputs 通过连续的函数传递(例如 generator 模块示例), 以及一个更面向对象的模块,我们构建一个 Sequential 模块,其中包含 整个模型作为子模块。使用 Sequential,判别器如下所示:
nn::Sequential discriminator(
// Layer 1
nn::Conv2d(
nn::Conv2dOptions(1, 64, 4).stride(2).padding(1).bias(false)),
nn::LeakyReLU(nn::LeakyReLUOptions().negative_slope(0.2)),
// Layer 2
nn::Conv2d(
nn::Conv2dOptions(64, 128, 4).stride(2).padding(1).bias(false)),
nn::BatchNorm2d(128),
nn::LeakyReLU(nn::LeakyReLUOptions().negative_slope(0.2)),
// Layer 3
nn::Conv2d(
nn::Conv2dOptions(128, 256, 4).stride(2).padding(1).bias(false)),
nn::BatchNorm2d(256),
nn::LeakyReLU(nn::LeakyReLUOptions().negative_slope(0.2)),
// Layer 4
nn::Conv2d(
nn::Conv2dOptions(256, 1, 3).stride(1).padding(0).bias(false)),
nn::Sigmoid());
提示
模块只执行函数组合。的
第一个子模块成为第二个子模块的输入,第三个子模块的输出
成为第四个的输入,依此类推。Sequential
加载数据¶
现在我们已经定义了生成器和判别器模型,我们需要一些 我们可以用来训练这些模型的数据。C++ 前端,就像 Python 前端一样, 附带一个强大的并行数据加载器。此数据加载器可以批量读取 来自数据集的数据(您可以自己定义)并提供许多 配置旋钮。
注意
Python 数据加载器使用多处理,而 C++ 数据加载器确实是 多线程,并且不会启动任何新进程。
数据加载器是 C++ 前端 api 的一部分,包含在命名空间中。此 API 由几个不同的组件组成:data
torch::data::
数据加载器类
用于定义数据集的 API,
用于定义转换的 API,可应用于数据集,
一个用于定义采样器的 API,用于生成用于索引数据集的索引,
现有数据集、转换和采样器的库。
在本教程中,我们可以使用 C++ 附带的数据集
前端。让我们为此实例化 a,然后
应用两个转换:首先,我们标准化图像,使它们位于
的范围 to (从原始范围 to )。
其次,我们应用排序规则,它接受一批张量和
将它们沿第一维堆叠成一个张量:MNIST
torch::data::datasets::MNIST
-1
+1
0
1
Stack
auto dataset = torch::data::datasets::MNIST("./mnist")
.map(torch::data::transforms::Normalize<>(0.5, 0.5))
.map(torch::data::transforms::Stack<>());
请注意,MNIST 数据集应位于目录
相对于您执行训练二进制文件的位置。您可以使用这个
脚本下载 MNIST 数据集。./mnist
接下来,我们创建一个数据加载器并将此数据集传递给它。创建新数据
loader 中,我们使用 ,它返回正确类型的 a(取决于
dataset、sampler 的类型和其他一些实现细节):torch::data::make_data_loader
std::unique_ptr
auto data_loader = torch::data::make_data_loader(std::move(dataset));
数据加载器确实有很多选项。您可以在此处检查完整集。
例如,为了加快数据加载速度,我们可以增加
工人。默认数字为零,这意味着将使用主线程。
如果我们设置为 ,将生成两个加载数据的线程
同时。我们还应该将 batch size 从其默认值 of 增加到更合理的值,例如 (的值 )。所以
让我们创建一个对象并设置适当的属性:workers
2
1
64
kBatchSize
DataLoaderOptions
auto data_loader = torch::data::make_data_loader(
std::move(dataset),
torch::data::DataLoaderOptions().batch_size(kBatchSize).workers(2));
现在,我们可以编写一个循环来加载批量数据,我们只会将其打印到 控制台:
for (torch::data::Example<>& batch : *data_loader) {
std::cout << "Batch size: " << batch.data.size(0) << " | Labels: ";
for (int64_t i = 0; i < batch.data.size(0); ++i) {
std::cout << batch.target[i].item<int64_t>() << " ";
}
std::cout << std::endl;
}
在这种情况下,数据加载程序返回的类型是 .
此类型是一个简单的结构,其中包含一个用于数据的字段和一个用于标签的字段。因为我们之前应用了排序规则,所以
Data Loader 仅返回一个此类示例。如果我们没有应用
collation,则数据加载器将改为生成,批处理中的每个示例都有一个元素。torch::data::Example
data
target
Stack
std::vector<torch::data::Example<>>
如果重新构建并运行此代码,您应该会看到如下内容:
root@fa350df05ecf:/home/build# make
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
root@fa350df05ecf:/home/build# make
[100%] Built target dcgan
root@fa350df05ecf:/home/build# ./dcgan
Batch size: 64 | Labels: 5 2 6 7 2 1 6 7 0 1 6 2 3 6 9 1 8 4 0 6 5 3 3 0 4 6 6 6 4 0 8 6 0 6 9 2 4 0 2 8 6 3 3 2 9 2 0 1 4 2 3 4 8 2 9 9 3 5 8 0 0 7 9 9
Batch size: 64 | Labels: 2 2 4 7 1 2 8 8 6 9 0 2 2 9 3 6 1 3 8 0 4 4 8 8 8 9 2 6 4 7 1 5 0 9 7 5 4 3 5 4 1 2 8 0 7 1 9 6 1 6 5 3 4 4 1 2 3 2 3 5 0 1 6 2
Batch size: 64 | Labels: 4 5 4 2 1 4 8 3 8 3 6 1 5 4 3 6 2 2 5 1 3 1 5 0 8 2 1 5 3 2 4 4 5 9 7 2 8 9 2 0 6 7 4 3 8 3 5 8 8 3 0 5 8 0 8 7 8 5 5 6 1 7 8 0
Batch size: 64 | Labels: 3 3 7 1 4 1 6 1 0 3 6 4 0 2 5 4 0 4 2 8 1 9 6 5 1 6 3 2 8 9 2 3 8 7 4 5 9 6 0 8 3 0 0 6 4 8 2 5 4 1 8 3 7 8 0 0 8 9 6 7 2 1 4 7
Batch size: 64 | Labels: 3 0 5 5 9 8 3 9 8 9 5 9 5 0 4 1 2 7 7 2 0 0 5 4 8 7 7 6 1 0 7 9 3 0 6 3 2 6 2 7 6 3 3 4 0 5 8 8 9 1 9 2 1 9 4 4 9 2 4 6 2 9 4 0
Batch size: 64 | Labels: 9 6 7 5 3 5 9 0 8 6 6 7 8 2 1 9 8 8 1 1 8 2 0 7 1 4 1 6 7 5 1 7 7 4 0 3 2 9 0 6 6 3 4 4 8 1 2 8 6 9 2 0 3 1 2 8 5 6 4 8 5 8 6 2
Batch size: 64 | Labels: 9 3 0 3 6 5 1 8 6 0 1 9 9 1 6 1 7 7 4 4 4 7 8 8 6 7 8 2 6 0 4 6 8 2 5 3 9 8 4 0 9 9 3 7 0 5 8 2 4 5 6 2 8 2 5 3 7 1 9 1 8 2 2 7
Batch size: 64 | Labels: 9 1 9 2 7 2 6 0 8 6 8 7 7 4 8 6 1 1 6 8 5 7 9 1 3 2 0 5 1 7 3 1 6 1 0 8 6 0 8 1 0 5 4 9 3 8 5 8 4 8 0 1 2 6 2 4 2 7 7 3 7 4 5 3
Batch size: 64 | Labels: 8 8 3 1 8 6 4 2 9 5 8 0 2 8 6 6 7 0 9 8 3 8 7 1 6 6 2 7 7 4 5 5 2 1 7 9 5 4 9 1 0 3 1 9 3 9 8 8 5 3 7 5 3 6 8 9 4 2 0 1 2 5 4 7
Batch size: 64 | Labels: 9 2 7 0 8 4 4 2 7 5 0 0 6 2 0 5 9 5 9 8 8 9 3 5 7 5 4 7 3 0 5 7 6 5 7 1 6 2 8 7 6 3 2 6 5 6 1 2 7 7 0 0 5 9 0 0 9 1 7 8 3 2 9 4
Batch size: 64 | Labels: 7 6 5 7 7 5 2 2 4 9 9 4 8 7 4 8 9 4 5 7 1 2 6 9 8 5 1 2 3 6 7 8 1 1 3 9 8 7 9 5 0 8 5 1 8 7 2 6 5 1 2 0 9 7 4 0 9 0 4 6 0 0 8 6
...
这意味着我们能够成功地从 MNIST 数据集加载数据。
编写训练循环¶
现在让我们完成示例的算法部分并实现精致的 在生成器和判别器之间跳舞。首先,我们将创建两个 优化器,一个用于生成器,一个用于判别器。优化器 我们使用 implement Adam 算法:
torch::optim::Adam generator_optimizer(
generator->parameters(), torch::optim::AdamOptions(2e-4).betas(std::make_tuple(0.5, 0.5)));
torch::optim::Adam discriminator_optimizer(
discriminator->parameters(), torch::optim::AdamOptions(5e-4).betas(std::make_tuple(0.5, 0.5)));
注意
在撰写本文时,C++ 前端提供了实现 Adagrad 的优化器, Adam、LBFGS、RMSprop 和 SGD。文档具有 最新列表。
接下来,我们需要更新我们的训练循环。我们将添加一个外部循环以排气 数据加载器每个 epoch 编写 GAN 训练代码:
for (int64_t epoch = 1; epoch <= kNumberOfEpochs; ++epoch) {
int64_t batch_index = 0;
for (torch::data::Example<>& batch : *data_loader) {
// Train discriminator with real images.
discriminator->zero_grad();
torch::Tensor real_images = batch.data;
torch::Tensor real_labels = torch::empty(batch.data.size(0)).uniform_(0.8, 1.0);
torch::Tensor real_output = discriminator->forward(real_images).reshape(real_labels.sizes());
torch::Tensor d_loss_real = torch::binary_cross_entropy(real_output, real_labels);
d_loss_real.backward();
// Train discriminator with fake images.
torch::Tensor noise = torch::randn({batch.data.size(0), kNoiseSize, 1, 1});
torch::Tensor fake_images = generator->forward(noise);
torch::Tensor fake_labels = torch::zeros(batch.data.size(0));
torch::Tensor fake_output = discriminator->forward(fake_images.detach()).reshape(fake_labels.sizes());
torch::Tensor d_loss_fake = torch::binary_cross_entropy(fake_output, fake_labels);
d_loss_fake.backward();
torch::Tensor d_loss = d_loss_real + d_loss_fake;
discriminator_optimizer.step();
// Train generator.
generator->zero_grad();
fake_labels.fill_(1);
fake_output = discriminator->forward(fake_images).reshape(fake_labels.sizes());
torch::Tensor g_loss = torch::binary_cross_entropy(fake_output, fake_labels);
g_loss.backward();
generator_optimizer.step();
std::printf(
"\r[%2ld/%2ld][%3ld/%3ld] D_loss: %.4f | G_loss: %.4f",
epoch,
kNumberOfEpochs,
++batch_index,
batches_per_epoch,
d_loss.item<float>(),
g_loss.item<float>());
}
}
上面,我们首先在真实图像上评估判别器,它应该
分配一个高概率。为此,我们使用 作为 target
概率。torch::empty(batch.data.size(0)).uniform_(0.8, 1.0)
注意
我们选择均匀分布在 0.8 和 1.0 之间而不是 1.0 之间的随机值 无处不在,以使判别器训练更加健壮。这个技巧 称为标签平滑。
在评估判别器之前,我们将其
参数。计算损失后,我们通过以下方式通过网络反向传播它
调用以计算新的梯度。我们重复这个口号:
假图片。我们没有使用数据集中的图像,而是让生成器
通过向它提供一批随机噪声来为此创建假图像。然后
将这些假图像转发给鉴别器。这一次,我们希望
discriminator 发出低概率,理想情况下全为 0。一旦我们有
计算了 BATCH 的 REAL 和 BATCH 的 fake 的判别器损失
图像,我们可以将判别器的优化器前进一步,以便
更新其参数。d_loss.backward()
为了训练生成器,我们再次首先将其梯度归零,然后重新计算
假图像的鉴别器。但是,这一次我们希望
discriminator 分配非常接近 1 的概率,这将指示
生成器可以生成欺骗判别器思考的图像
它们实际上是真实的(来自数据集)。为此,我们用所有 1 填充张量。最后,我们将生成器的优化器也用于更新
它的参数。fake_labels
我们现在应该准备好在 CPU 上训练我们的模型了。我们还没有任何代码 捕获状态或示例输出,但我们稍后会添加它。为 现在,让我们观察我们的模型正在做某事 - 我们稍后会 根据生成的图像验证此内容是否有意义。 重新构建并运行应打印如下内容:
root@3c0711f20896:/home/build# make && ./dcgan
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcga
[ 1/10][100/938] D_loss: 0.6876 | G_loss: 4.1304
[ 1/10][200/938] D_loss: 0.3776 | G_loss: 4.3101
[ 1/10][300/938] D_loss: 0.3652 | G_loss: 4.6626
[ 1/10][400/938] D_loss: 0.8057 | G_loss: 2.2795
[ 1/10][500/938] D_loss: 0.3531 | G_loss: 4.4452
[ 1/10][600/938] D_loss: 0.3501 | G_loss: 5.0811
[ 1/10][700/938] D_loss: 0.3581 | G_loss: 4.5623
[ 1/10][800/938] D_loss: 0.6423 | G_loss: 1.7385
[ 1/10][900/938] D_loss: 0.3592 | G_loss: 4.7333
[ 2/10][100/938] D_loss: 0.4660 | G_loss: 2.5242
[ 2/10][200/938] D_loss: 0.6364 | G_loss: 2.0886
[ 2/10][300/938] D_loss: 0.3717 | G_loss: 3.8103
[ 2/10][400/938] D_loss: 1.0201 | G_loss: 1.3544
[ 2/10][500/938] D_loss: 0.4522 | G_loss: 2.6545
...
迁移到 GPU¶
虽然我们当前的脚本可以在 CPU 上运行良好,但我们都知道卷积
在 GPU 上要快得多。让我们快速讨论如何将我们的培训转移到
GPU 的 GPU 。为此,我们需要做两件事:传递 GPU 设备规范
分配给 Tensor,并将任何其他 Tensor 显式复制到
GPU 通过 C++ 前端中的所有张量和模块都有的方法。
实现两者的最简单方法是在训练脚本的顶层创建一个实例,然后将该设备传递给 tensor
factory 函数以及 method.我们可以
首先使用 CPU 设备执行此操作:to()
torch::Device
torch::zeros
to()
// Place this somewhere at the top of your training script.
torch::Device device(torch::kCPU);
新的张量分配,如
torch::Tensor fake_labels = torch::zeros(batch.data.size(0));
应该更新为将 作为最后一个参数:device
torch::Tensor fake_labels = torch::zeros(batch.data.size(0), device);
对于创建不在我们手中的张量,例如来自 MNIST 的张量
数据集,我们必须插入显式调用。这意味着to()
torch::Tensor real_images = batch.data;
成为
torch::Tensor real_images = batch.data.to(device);
此外,我们的 model 参数应该移动到正确的设备:
generator->to(device);
discriminator->to(device);
注意
如果张量已存在于提供给 的设备上,则调用是
无操作。不会创建额外的副本。to()
此时,我们刚刚使以前的 CPU 驻留代码更加明确。 但是,现在将设备更改为 CUDA 设备也非常容易:
torch::Device device(torch::kCUDA)
现在,所有张量都将位于 GPU 上,为所有人调用快速 CUDA 内核
操作,而无需更改任何下游代码。如果我们想要
指定一个特定的设备索引,它可以作为第二个参数传递给
构造函数。如果我们希望不同的张量存在于不同的
devices 中,我们可以传递单独的设备实例(例如,CUDA 设备上的一个
0 和 CUDA 设备 1 上的另一个)。我们甚至可以进行此配置
dynamicly,这通常有助于使我们的训练脚本更具可移植性:Device
torch::Device device = torch::kCPU;
if (torch::cuda::is_available()) {
std::cout << "CUDA is available! Training on GPU." << std::endl;
device = torch::kCUDA;
}
甚至
torch::Device device(torch::cuda::is_available() ? torch::kCUDA : torch::kCPU);
检查点和恢复训练状态¶
我们应该对训练脚本进行的最后一次增强是定期 保存模型参数的状态、优化器的状态以及 生成的图像样本很少。如果我们的计算机在 training 过程中,前两个将允许我们恢复 Training 状态。 对于持久的训练课程,这是绝对必要的。幸运 C++ 前端提供了一个 API 来序列化和反序列化 model 和 optimizer 状态以及单个张量。
其核心 API 是 and ,其中可以是子类或优化器实例,如对象
我们的训练脚本中有。让我们更新训练循环以将
model 和 optimizer 状态的 Sample:torch::save(thing,filename)
torch::load(thing,filename)
thing
torch::nn::Module
Adam
if (batch_index % kCheckpointEvery == 0) {
// Checkpoint the model and optimizer state.
torch::save(generator, "generator-checkpoint.pt");
torch::save(generator_optimizer, "generator-optimizer-checkpoint.pt");
torch::save(discriminator, "discriminator-checkpoint.pt");
torch::save(discriminator_optimizer, "discriminator-optimizer-checkpoint.pt");
// Sample the generator and save the images.
torch::Tensor samples = generator->forward(torch::randn({8, kNoiseSize, 1, 1}, device));
torch::save((samples + 1.0) / 2.0, torch::str("dcgan-sample-", checkpoint_counter, ".pt"));
std::cout << "\n-> checkpoint " << ++checkpoint_counter << '\n';
}
其中 是一个整数,设置为 to
checkpoint 中,并且是一个计数器 bumped
每次我们做一个检查点。kCheckpointEvery
100
100
checkpoint_counter
要恢复训练状态,您可以在所有 models 和 创建优化器,但在训练循环之前:
torch::optim::Adam generator_optimizer(
generator->parameters(), torch::optim::AdamOptions(2e-4).beta1(0.5));
torch::optim::Adam discriminator_optimizer(
discriminator->parameters(), torch::optim::AdamOptions(2e-4).beta1(0.5));
if (kRestoreFromCheckpoint) {
torch::load(generator, "generator-checkpoint.pt");
torch::load(generator_optimizer, "generator-optimizer-checkpoint.pt");
torch::load(discriminator, "discriminator-checkpoint.pt");
torch::load(
discriminator_optimizer, "discriminator-optimizer-checkpoint.pt");
}
int64_t checkpoint_counter = 0;
for (int64_t epoch = 1; epoch <= kNumberOfEpochs; ++epoch) {
int64_t batch_index = 0;
for (torch::data::Example<>& batch : *data_loader) {
检查生成的图像¶
我们的训练脚本现已完成。我们已经准备好训练我们的 GAN,无论是在
CPU 或 GPU。为了检查我们的训练程序的中介输出,对于
我们添加了代码来定期将图像样本保存到文件中,我们可以编写一个小的 Python 脚本来加载
张量并使用 matplotlib 显示它们:"dcgan-sample-xxx.pt"
import argparse
import matplotlib.pyplot as plt
import torch
parser = argparse.ArgumentParser()
parser.add_argument("-i", "--sample-file", required=True)
parser.add_argument("-o", "--out-file", default="out.png")
parser.add_argument("-d", "--dimension", type=int, default=3)
options = parser.parse_args()
module = torch.jit.load(options.sample_file)
images = list(module.parameters())[0]
for index in range(options.dimension * options.dimension):
image = images[index].detach().cpu().reshape(28, 28).mul(255).to(torch.uint8)
array = image.numpy()
axis = plt.subplot(options.dimension, options.dimension, 1 + index)
plt.imshow(array, cmap="gray")
axis.get_xaxis().set_visible(False)
axis.get_yaxis().set_visible(False)
plt.savefig(options.out_file)
print("Saved ", options.out_file)
现在让我们训练大约 30 个 epoch 的模型:
root@3c0711f20896:/home/build# make && ./dcgan 10:17:57
Scanning dependencies of target dcgan
[ 50%] Building CXX object CMakeFiles/dcgan.dir/dcgan.cpp.o
[100%] Linking CXX executable dcgan
[100%] Built target dcgan
CUDA is available! Training on GPU.
[ 1/30][200/938] D_loss: 0.4953 | G_loss: 4.0195
-> checkpoint 1
[ 1/30][400/938] D_loss: 0.3610 | G_loss: 4.8148
-> checkpoint 2
[ 1/30][600/938] D_loss: 0.4072 | G_loss: 4.36760
-> checkpoint 3
[ 1/30][800/938] D_loss: 0.4444 | G_loss: 4.0250
-> checkpoint 4
[ 2/30][200/938] D_loss: 0.3761 | G_loss: 3.8790
-> checkpoint 5
[ 2/30][400/938] D_loss: 0.3977 | G_loss: 3.3315
...
-> checkpoint 120
[30/30][938/938] D_loss: 0.3610 | G_loss: 3.8084
并在绘图中显示图像:
root@3c0711f20896:/home/build# python display.py -i dcgan-sample-100.pt
Saved out.png
它应该看起来像这样:
![数字](https://pytorch.org/tutorials/_images/digits.png)
数字!万岁!现在球在你的球场上:你能改进模型来制作 数字看起来更好看?