第5章 使用 PyTorch C++ 前端
使用 PyTorch C++ 前端 创建时间:2019 年 1 月 15 日 | 最后更新:2025 年 9 月 22 日 | 最后验证:2024 年 11 月 5 日 作者:Peter Goldsborough
本章内容
* 如何使用 PyTorch C++ 前端构建 C++ 应用 * 如何在 C++ 中使用 PyTorch 抽象来定义和训练神经网络
先决条件
* PyTorch 1.5 或更高版本 * 基础的 C++ 编程知识 * 带有 CMake >= 3.5 的基础 Ubuntu Linux 环境;类似的命令在 MacOS / Windows 环境中也能正常工作 * (可选)用于 GPU 训练章节的基于 CUDA 的 GPU
PyTorch C++ 前端是 PyTorch 机器学习框架的纯 C++ 接口。虽然 PyTorch 的主要接口自然是 Python,但这个 Python API 建立在庞大的 C++ 代码库之上,该代码库提供了张量和自动微分等基础数据结构和功能。C++ 前端暴露了一个纯 C++17 API,扩展了底层 C++ 代码库,增加了机器学习和推理所需的工具。这包括用于神经网络建模的常见组件内置集合;用于扩展该集合的自定义模块 API;流行优化算法(如随机梯度下降)的库;带有定义和加载数据集 API 的并行数据加载器;序列化例程等。
本教程将通过端到端示例展示如何使用 C++ 前端训练模型。具体而言,我们将训练一个 DCGAN(一种生成模型)来生成 MNIST 数字图像。虽然概念上是个简单示例,但这足以让你对 PyTorch C++ 前端有一个概览,并激发你训练更复杂模型的欲望。我们首先介绍为什么要使用 C++ 前端,然后直接进入模型定义和训练。
提示 观看这个 CppCon 2018 的闪电演讲,快速(且幽默地)了解 C++ 前端。
提示 此说明提供了 C++ 前端组件和设计哲学的全面概述。
提示 PyTorch C++ 生态系统的文档可在 https://pytorch.org/cppdocs 找到。那里可以找到高层描述以及 API 级文档。
动机 在我们踏上 GAN 和 MNIST 数字的激动人心旅程之前,让我们退后一步讨论一下为什么一开始要使用 C++ 前端而不是 Python 前端。我们(PyTorch 团队)创建 C++ 前端是为了在 Python 无法使用或不是正确工具的环境中进行研究。此类环境的示例包括:
低延迟系统:你可能希望在具有高帧率和低延迟要求的纯 C++ 游戏引擎中进行强化学习研究。在这种环境下,纯 C++ 库比 Python 库更适合。由于 Python 解释器的缓慢,Python 可能完全不可行。 高度多线程环境:由于全局解释器锁(GIL),Python 不能同时运行多个系统线程。多进程是一种替代方案,但扩展性较差且存在显著缺点。C++ 没有这种限制,线程易于使用和创建。需要大量并行化的模型(如深度神经进化中使用的模型)可以从中受益。 现有的 C++ 代码库:你可能拥有一个现有的 C++ 应用程序,从在后台服务器中提供网页到在照片编辑软件中渲染 3D 图形,并希望将机器学习方法集成到系统中。C++ 前端允许你保持在 C++ 中,避免在 Python 和 C++ 之间来回绑定的麻烦,同时保留传统 PyTorch(Python)体验的大部分灵活性和直观性。
C++ 前端不旨在与 Python 前端竞争,而是旨在补充它。我们知道研究人员和工程师都喜欢 PyTorch 的简洁性、灵活性和直观的 API。我们的目标是确保你能够在每种可能的环境中利用这些核心设计原则,包括上述环境。如果其中一个场景很好地描述了你的用例,或者你只是感兴趣或好奇,请继续阅读,我们将在以下段落中详细探索 C++ 前端。
提示 C++ 前端试图提供尽可能接近 Python 前端的 API。如果你有 Python 前端的经验,并曾问自己“我如何用 C++ 前端做 X?”,请像你在 Python 中那样写代码,通常相同的函数和方法在 C++ 中也可用(只需记住将点号替换为双冒号)。
编写基本应用程序 让我们从一个最小的 C++ 应用开始,以验证我们在设置和构建环境上是否保持一致。首先,你需要获取 LibTorch 发行版的副本——这是一个预先构建的 zip 归档,打包了使用 C++ 前端所需的所有相关头文件、库和 CMake 构建文件。LibTorch 发行版可在 PyTorch 网站上下载,支持 Linux、MacOS 和 Windows。本教程的其余部分假设使用基础的 Ubuntu Linux 环境,但你也可以在 MacOS 或 Windows 上跟进。
提示 “安装 PyTorch C++ 发行版”的说明更详细地描述了以下步骤。
提示 在 Windows 上,调试版本和发布版本在 ABI 上不兼容。如果你计划在调试模式下构建项目,请尝试 LibTorch 的调试版本。此外,确保在下面的 `cmake --build .` 行中指定正确的配置。
第一步是在本地下载 LibTorch 发行版,通过从 PyTorch 网站获取链接。对于标准的 Ubuntu Linux 环境,这意味着运行: # 如果你需要例如 CUDA 9.0 支持,请将下面的 URL 中的 "cpu" 替换为 "cu90"。 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`,目前只是打印出一个 3x3 的单位矩阵:
#include
int main() { torch::Tensor tensor = torch::eye(3); std::cout << tensor << std::endl; }
为了构建这个小应用以及我们稍后的完整训练脚本,我们将使用这个 CMakeLists.txt 文件: cmake_minimum_required(VERSION 3.5 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 17)
注意 虽然 CMake 是 LibTorch 的推荐构建系统,但这不是硬性要求。你也可以使用 Visual Studio 项目文件、QMake、纯 Makefile 或你熟悉的任何其他构建环境。然而,我们不提供即插即用的支持。
注意上面 CMake 文件中的第 4 行:`find_package(Torch REQUIRED)`。这指示 CMake 查找 LibTorch 库的构建配置。为了让 CMake 知道在哪里找到这些文件,我们在调用 cmake 时必须设置 `CMAKE_PREFIX_PATH`。在此之前,让我们同意 dcgan 应用的以下目录结构: dcgan/ CMakeLists.txt dcgan.cpp
此外,我将把解压后的 LibTorch 发行版的路径称为 `/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
在上面,我们首先在 dcgan 目录内创建一个 build 文件夹,进入该文件夹,运行 cmake 命令生成必要的构建(Make)文件,最后通过运行 `cmake --build . --config Release` 成功编译了项目。现在我们可以运行这个最小二进制文件,完成基本项目配置部分: 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()` 或 `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
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 中一样,我们定义了一个名为 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; };
提示 你可以在 https://pytorch.org/cppdocs 的 `torch::nn` 命名空间文档中找到像 `torch::nn::Linear`、`torch::nn::Dropout` 或 `torch::nn::Conv2d` 这样的可用内置模块的完整列表。
上述代码的一个微妙之处在于为什么子模块在构造函数的初始化列表中创建,而参数在构造函数体内创建。对此有充分理由,我们将在下面关于 C++ 前端所有权模型的章节中探讨。然而,最终结果是我们可以像 Python 一样递归访问模块树的参数。调用 `parameters()` 返回一个 `std::vector
这将打印: 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 中的 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++ 中定义模块、注册参数、注册子模块、通过像 `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)。 struct Net : torch::nn::Module {};
void a(std::shared_ptr
int main() {
auto net = std::make_shared
根据我们的经验,来自动态语言的研究人员强烈偏好引用语义而非值语义,尽管后者更“原生”于 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; };
为了使用 linear 子模块,我们希望直接将其存储在我们的类中。然而,我们也希望模块基类知道并访问这个子模块。为此,它必须存储对该子模块的引用。此时,我们已经遇到了共享所有权的需求。`torch::nn::Module` 类和具体的 Net 类都需要对子模块的引用。因此,基类将模块存储为 shared_ptrs,因此具体类也必须如此。
但等等!我在上面的代码中没有看到任何 shared_ptr 的提及!为什么?因为 `std::shared_ptr
Tensor forward(const Tensor& input);
Tensor weight, bias; };
TORCH_MODULE(Linear);
简而言之:该模块不称为 Linear,而是 LinearImpl。一个宏 `TORCH_MODULE` 定义了实际的 Linear 类。这个“生成”的类实际上是 `std::shared_ptr
void a(Net net) { }
int main() { Net net; a(net); }
这里有一个微妙的问题值得提及。默认构造的 `std::shared_ptr` 是“空”的,即包含空指针。默认构造的 Linear 或 Net 是什么?嗯,这是一个艰难的选择。我们可以说它应该是空的(null)`std::shared_ptr
或者你可以先用空指针构造持有者,然后在构造函数中分配给它(对 Python 程序员来说更熟悉): 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(`torch::save` 和 `torch::load`)仅支持模块持有者(或纯 shared_ptr)。因此,模块持有者 API 是使用 C++ 前端定义模块的推荐方式,我们将在此教程中之后使用此 API。
定义 DCGAN 模块 现在我们有了必要的背景和介绍,可以定义我们要在本帖中解决的机器学习任务的模块。回顾一下:我们的任务是从 MNIST 数据集生成数字图像。我们要使用生成对抗网络(GAN)来解决此任务。具体来说,我们将使用 DCGAN 架构——它是该类中最早和最简单的之一,但完全足以胜任此任务。
提示 你可以在这个仓库中找到本教程中展示的完整源代码。
GAN 是什么? GAN 由两个不同的神经网络模型组成:生成器和判别器。生成器接收来自噪声分布的样本,其目标是将每个噪声样本转换为类似于目标分布的图像——在我们的情况下是 MNIST 数据集。判别器接收来自 MNIST 数据集的真实图像或来自生成器的假图像。它被要求发出一个概率,判断特定图像有多真实(接近 1)或有多虚假(接近 0)。来自判别器的关于生成器生成的图像有多真实的反馈用于训练生成器。关于判别器对真实性识别能力的反馈用于优化判别器。理论上,生成器和判别器之间的微妙平衡使它们协同改进,导致生成器产生与目标分布无法区分的图像,欺骗判别器(此时)出色的眼睛对真实和虚假图像都发出 0.5 的概率。对我们来说,最终结果是一台接收噪声作为输入并生成逼真数字图像作为输出的机器。
生成器模块 我们首先定义生成器模块,它由一系列转置 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);
现在我们可以在 DCGANGenerator 上调用 `forward()` 将噪声样本映射到图像。选择的特定模块,如 `nn::ConvTranspose2d` 和 `nn::BatchNorm2d`,遵循之前概述的结构。`kNoiseSize` 常量决定输入噪声向量的大小,设置为 100。超参数当然是通过研究生梯度下降找到的。
注意 在发现超参数的过程中,没有研究生受到伤害。他们定期服用 Soylent。
注意 简要说明一下在 C++ 前端中向像 `Conv2d` 这样的内置模块传递选项的方式:每个模块都有一些必需的选项,如 `BatchNorm2d` 的特征数量。如果你只需要配置必需选项,可以直接将它们传递给模块的构造函数,如 `BatchNorm2d(128)` 或 `Dropout(0.5)` 或 `Conv2d(8, 4, 2)`(对于输入通道数、输出通道数和核大小)。但是,如果你需要修改其他通常默认为默认值的选项,如 `Conv2d` 的 `bias`,你需要构造并传递一个选项对象。C++ 前端中的每个模块都有一个相关的选项结构,称为 `ModuleOptions`,其中 Module 是模块的名称,如 `Linear` 的 `LinearOptions`。这就是我们为上面的 `Conv2d` 模块所做的。
判别器模块 判别器类似地是卷积、批量归一化和激活的序列。然而,卷积现在是普通的而不是转置的,我们使用 alpha 值为 0.2 的 Leaky ReLU 而不是普通 ReLU。此外,最后一个激活变为 Sigmoid,将值压缩到 0 到 1 之间的范围内。然后我们可以将这些压缩值解释为判别器分配给图像为真的概率。 为了构建判别器,我们将尝试不同的方法:Sequential 模块。像 Python 一样,PyTorch 这里提供两种模型定义 API:一种函数式 API,其中输入通过连续函数传递(例如生成器模块示例),另一种更面向对象的 API,其中我们构建包含整个模型作为子模块的 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 模块仅执行函数组合。第一个子模块的输出成为第二个的输入,第三个的输出成为第四个的输入,等等。
加载数据 现在我们已经定义了生成器和判别器模型,我们需要一些数据来训练这些模型。与 Python 一样,C++ 前端附带一个强大的并行数据加载器。此数据加载器可以从数据集(你可以自己定义)读取数据批次,并提供许多配置选项。
注意 虽然 Python 数据加载器使用多进程,但 C++ 数据加载器真正是多线程的,不会启动任何新进程。
数据加载器是 C++ 前端数据 API 的一部分,包含在 `torch::data::` 命名空间中。此 API 由几个不同组件组成:
* 数据加载器类 * 用于定义数据集的 API * 用于定义可应用于数据集的转换的 API * 用于定义采样器的 API,采样器生成用于索引数据集的索引 * 现有数据集、转换和采样器的库
对于本教程,我们可以使用 C++ 前端附带的 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 数据集。 接下来,我们创建一个数据加载器并传递此数据集。要创建新的数据加载器,我们使用 `torch::data::make_data_loader`,它返回正确类型的 `std::unique_ptr`(取决于数据集类型、采样器类型和一些其他实现细节): auto data_loader = torch::data::make_data_loader(std::move(dataset));
数据加载器确实有很多选项。你可以在这里检查完整集合。 例如,为了加快数据加载速度,我们可以增加工作线程数。默认数为零,这意味着将使用主线程。如果我们设置 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) {