评价此页

使用 PyTorch C++ 前端#

创建日期:2019年1月15日 | 最后更新:2025年9月22日 | 最后验证:2024年11月5日

作者: Peter Goldsborough

您将学到什么
  • 如何构建一个利用 PyTorch C++ 前端的 C++ 应用程序

  • 如何使用 PyTorch 抽象从 C++ 定义和训练神经网络

先决条件
  • PyTorch 1.5 或更高版本

  • 对 C++ 编程的基本了解

  • 具备 CMake >= 3.5 的基本 Ubuntu Linux 环境;在 MacOS / Windows 环境下,类似的命令也适用

  • (可选)一个基于 CUDA 的 GPU,用于 GPU 训练部分

PyTorch C++ 前端是 PyTorch 机器学习框架的纯 C++ 接口。虽然 PyTorch 的主要接口自然是 Python,但这个 Python API 是建立在大量的 C++ 代码库之上的,提供了诸如张量和自动微分等基础数据结构和功能。C++ 前端公开了一个纯 C++17 API,它通过机器学习训练和推理所需的工具扩展了这个底层的 C++ 代码库。这包括一个内置的常用神经网络建模组件集合;一个扩展此集合以包含自定义模块的 API;一个流行的优化算法库,如随机梯度下降;一个具有定义和加载数据集 API 的并行数据加载器;序列化例程等等。

本教程将引导您完成一个使用 C++ 前端训练模型的端到端示例。具体来说,我们将训练一个 DCGAN ——一种生成模型——来生成 MNIST 数字的图像。虽然在概念上这是一个简单的示例,但它应该足以让您快速了解 PyTorch C++ 前端,并激发您训练更复杂模型的兴趣。我们将从一些关于您为何要使用 C++ 前端的动机开始,然后直接进入定义和训练我们的模型。

提示

观看 2018 年 CppCon 的这个闪电演讲,快速(且幽默地)了解 C++ 前端。

提示

这篇说明 提供了 C++ 前端组件和设计理念的全面概述。

提示

PyTorch C++ 生态系统的文档可在 https://pytorch.ac.cn/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 的 API。如果您熟悉 Python 前端,并且想知道“如何在 C++ 前端中完成 X?”,请以在 Python 中编写代码的方式来编写您的代码,很可能相同的函数和方法在 C++ 和 Python 中都可用(只需记住用双冒号替换点)。

编写基本应用程序#

让我们从编写一个最小的 C++ 应用程序开始,以验证我们在设置和构建环境方面是否步调一致。首先,您需要获取 LibTorch 分发版——我们预先构建的 zip 存档,其中包含了使用 C++ 前端所需的所有相关头文件、库和 CMake 构建文件。LibTorch 分发版可在 PyTorch 网站上 下载,适用于 Linux、MacOS 和 Windows。本教程的其余部分将假定您使用基本的 Ubuntu Linux 环境,但您也可以在 MacOS 或 Windows 上跟随操作。

提示

关于 安装 PyTorch 的 C++ 分发版 的说明更详细地描述了以下步骤。

提示

在 Windows 上,调试和发布版本不兼容 ABI。如果您计划以调试模式构建项目,请尝试使用 LibTorch 的调试版本。另外,请确保在下面的 cmake --build . 行中指定正确的配置。

第一步是通过从 PyTorch 网站检索的链接在本地下载 LibTorch 分发版。对于标准的 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

接下来,让我们编写一个名为 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.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 <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 一样,我们定义了一个名为 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::Lineartorch::nn::Dropouttorch::nn::Conv2d 等内置模块的完整列表。

上述代码的一个微妙之处是,为什么子模块是在构造函数的初始化列表中创建的,而参数是在构造函数体内创建的。这有一个很好的理由,我们将在下面的 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 提供了一个 named_parameters() 方法,该方法返回一个 OrderedDict,与 Python 中的一样。

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<Net> net) { }

int main() {
  auto net = std::make_shared<Net>();
  a(net);
}

根据我们的经验,来自动态语言的研究人员非常喜欢引用语义而不是值语义,尽管后者对 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_ptr,因此具体类也必须这样做。

但是等等!我没有在上面的代码中看到任何关于 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);

简而言之:该模块不称为 Linear,而称为 LinearImpl。一个宏 TORCH_MODULE 然后定义了实际的 Linear 类。这个“生成的”类实际上是 std::shared_ptr<LinearImpl> 的一个包装器。它是一个包装器而不是一个简单的 typedef,是为了使构造函数等仍然按预期工作,也就是说,您仍然可以写 torch::nn::Linear(3, 4) 而不是 std::make_shared<LinearImpl>(3, 4)。我们称由宏创建的类为模块*持有者*。与(共享)指针一样,您可以使用箭头运算符(如 model->forward(...))访问底层对象。最终结果是一个所有权模型,与 Python API 的模型非常相似。引用语义成为默认,但没有 std::shared_ptrstd::make_shared 的额外输入。对于我们的 Net,使用模块持有者 API 如下所示:

struct NetImpl : torch::nn::Module {};
TORCH_MODULE(Net);

void a(Net net) { }

int main() {
  Net net;
  a(net);
}

这里有一个值得一提的微妙问题。默认构造的 std::shared_ptr 是“空的”,即包含一个空指针。默认构造的 LinearNet 是什么?这是一个棘手的选择。我们可以说它应该是一个空的(空)std::shared_ptr<LinearImpl>。但是,请记住 Linear(3, 4) 等同于 std::make_shared<LinearImpl>(3, 4)。这意味着如果我们决定 Linear linear; 应该是一个空指针,那么就没有办法构造一个不接受任何构造函数参数或对其所有参数都使用默认值的模块。因此,在当前 API 中,默认构造的模块持有者(如 Linear())会调用底层模块(LinearImpl())的默认构造函数。如果底层模块没有默认构造函数,您会得到一个编译器错误。要改为构造空的持有者,您可以将 nullptr 传递给持有者的构造函数。

实际上,这意味着您可以使用子模块,如前面所示,模块在*初始化列表*中被注册和构造:

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;
};

或者您可以先用空指针构造持有者,然后在构造函数中对其进行赋值(对 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::savetorch::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::ConvTranspose2dnn::BatchNorm2d,遵循前面概述的结构。kNoiseSize 常量决定了输入噪声向量的大小,并设置为 100。超参数当然是通过梯度下降学生找到的。

注意

在发现超参数的过程中,没有伤害到任何研究生。他们定期食用 Soylent。

注意

关于 C++ 前端如何将选项传递给内置模块(如 Conv2d)的简要说明:每个模块都有一些必需的选项,例如 BatchNorm2d 的特征数量。如果你只需要配置必需的选项,可以直接将它们传递给模块的构造函数,例如 BatchNorm2d(128)Dropout(0.5)Conv2d(8, 4, 2)(分别对应输入通道数、输出通道数和卷积核大小)。但是,如果你需要修改其他通常是默认值的选项,例如 Conv2dbias,则需要构造并传递一个*选项*对象。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*模块只是执行函数组合。第一个子模块的输出成为第二个子模块的输入,第三个子模块的输出成为第四个子模块的输入,依此类推。

加载数据#

现在我们已经定义了生成器和判别器模型,我们需要一些数据来训练这些模型。C++ 前端与 Python 前端一样,提供了一个强大的并行数据加载器。这个数据加载器可以从(你可以自己定义的)数据集中读取数据批次,并提供许多配置选项。

注意

虽然 Python 数据加载器使用多进程,但 C++ 数据加载器是真正多线程的,并且不会启动任何新进程。

数据加载器是 C++ 前端*data* API 的一部分,包含在*torch::data::*命名空间中。这个 API 由几个不同的组件组成:

  • 数据加载器类;

  • 定义数据集的 API;

  • 定义*转换*的 API,这些转换可以应用于数据集;

  • 定义*采样器*的 API,它们生成用于索引数据集的索引;

  • 现有数据集、转换和采样器的库。

在这个教程中,我们可以使用 C++ 前端自带的*MNIST*数据集。让我们为此实例化一个*torch::data::datasets::MNIST*,并应用两个转换:首先,我们将图像归一化,使其范围在* -1* 到*+1*之间(从原始的*0*到*1*范围)。其次,我们应用*Stack* *collator*,它接收一批张量并沿着第一个维度将它们堆叠成一个单独的张量。

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) {
  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;
}

在这种情况下,数据加载器返回的类型是*torch::data::Example*。这种类型是一个简单的结构体,包含一个用于数据的*data*字段和一个用于标签的*target*字段。因为我们之前应用了*Stack* collator,数据加载器只返回一个这样的示例。如果我们没有应用 collator,数据加载器将返回*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 数据集加载数据。

编写训练循环#

现在,让我们完成示例的算法部分,并实现生成器和判别器之间的精妙协作。首先,我们将创建两个优化器,一个用于生成器,一个用于判别器。我们使用的优化器实现了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>());
  }
}

上面,我们首先在真实图像上评估判别器,它应该为此分配高概率。为此,我们使用*torch::empty(batch.data.size(0)).uniform_(0.8, 1.0)*作为目标概率。

注意

我们选择介于 0.8 和 1.0 之间的均匀分布的随机值,而不是到处都是 1.0,以使判别器训练更加稳健。这个技巧被称为*标签平滑*。

在评估判别器之前,我们清零其参数的梯度。计算损失后,通过调用*d_loss.backward()*将其反向传播通过网络以计算新梯度。我们对假图像重复此过程。我们不使用数据集中的图像,而是让生成器通过输入一批随机噪声来创建假图像。然后,我们将这些假图像输入到判别器。这次,我们希望判别器输出低概率,理想情况下为零。一旦我们计算了真实图像批次和假图像批次的判别器损失,我们就可以通过一个步骤来推进判别器的优化器,以更新其参数。

为了训练生成器,我们再次先清零其梯度,然后重新评估判别器在假图像上的表现。但是,这次我们希望判别器分配非常接近于一的概率,这将表明生成器可以生成足以欺骗判别器认为它们实际上是真的(来自数据集)的图像。为此,我们将*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 设备规范传递给我们自己分配的张量,并通过 C++ 前端中所有张量和模块都拥有的*to()*方法显式地将任何其他张量复制到 GPU。实现这两点的最简单方法是在训练脚本的顶层创建一个*torch::Device*实例,然后将该设备传递给张量工厂函数(如*torch::zeros*)以及*to()*方法。我们可以先用 CPU 设备这样做:

// 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);

我们的模型参数也应该移动到正确的设备:

generator->to(device);
discriminator->to(device);

注意

如果张量已经存在于传递给*to()*的设备上,则该调用将不起作用。不会进行额外的复制。

此时,我们只是使我们之前的 CPU 代码更加明确。但是,现在也很容易将设备更改为 CUDA 设备:

torch::Device device(torch::kCUDA)

现在所有张量都将驻留在 GPU 上,并为所有操作调用快速的 CUDA 内核,而无需我们更改任何下游代码。如果我们想指定一个特定的设备索引,可以将其作为*Device*构造函数的第二个参数传递。如果我们希望不同的张量驻留在不同的设备上,我们可以传递单独的设备实例(例如,一个在 CUDA 设备 0 上,另一个在 CUDA 设备 1 上)。我们甚至可以动态地进行这种配置,这通常有助于使我们的训练脚本更具可移植性:

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);

检查点和恢复训练状态#

我们应该对训练脚本做的最后一个增强是定期保存模型参数的状态、优化器的状态以及一些生成的图像样本。如果我们的计算机在训练过程中崩溃,前两项将允许我们恢复训练状态。对于持久的训练会话,这绝对是必不可少的。幸运的是,C++ 前端提供了一个 API 来序列化和反序列化模型和优化器状态以及单个张量。

*torch::save(thing,filename)*和*torch::load(thing,filename)*是核心 API,其中*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';
}

其中*kCheckpointEvery*是一个整数,设置为类似*100*的值,以便每*100*批次进行一次检查点,而*checkpoint_counter*是一个每次进行检查点时递增的计数器。

要恢复训练状态,你可以在创建所有模型和优化器之后,但在训练循环之前,添加如下所示的行:

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) {

检查生成的图像#

我们的训练脚本现在已完成。我们已准备好在 CPU 或 GPU 上训练我们的 GAN。要检查我们训练过程的中间输出(为此我们添加了代码以定期将图像样本保存到*“dcgan-sample-xxx.pt”*文件中),我们可以编写一个小型 Python 脚本来加载张量并使用 matplotlib 显示它们。

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

这应该看起来像这样:

digits

数字!太棒了!现在轮到你了:你能改进模型,让数字看起来更好吗?

结论#

本教程希望为您提供了 PyTorch C++ 前端的一个易于理解的摘要。像 PyTorch 这样的机器学习库必然拥有非常广泛和丰富的 API。因此,有许多概念我们没有时间或空间在这里讨论。但是,我鼓励您尝试使用该 API,并在遇到困难时查阅我们的文档,特别是库 API部分。另外,请记住,当我们可以做到时,C++ 前端将遵循 Python 前端的设计和语义,因此您可以利用这一点来提高您的学习效率。

提示

您可以在 此仓库 中找到本教程中提供的完整源代码。

一如既往,如果您遇到任何问题或有疑问,可以使用我们的论坛GitHub issues与我们联系。