Shortcuts

使用PyTorch C++前端

Created On: Jan 15, 2019 | Last Updated: Jan 23, 2025 | Last Verified: Nov 05, 2024

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

本教程将通过一个端到端的模型训练示例来引导您使用C++前端。具体来说,我们将训练一个`DCGAN <https://arxiv.org/abs/1511.06434>`_——一种生成模型——来生成MNIST数字的图像。虽然这是一个概念上简单的示例,但应该足以让您快速了解PyTorch C++前端并激发您训练更复杂模型的兴趣。我们将从一些关于使用C++前端的动机性话语开始,然后直接定义和训练我们的模型。

小技巧

观看`CppCon 2018的这个快闪演讲 <https://www.youtube.com/watch?v=auRPXMMHJzc>`_,可以快速(和幽默地)了解C++前端。

小技巧

`这个说明 <https://pytorch.org/cppdocs/frontend.html>`_提供了关于C++前端的组件和设计哲学的全面概述。

小技巧

可以在https://pytorch.org/cppdocs找到PyTorch C++生态系统的文档。在那里您可以找到高级描述以及API级文档。

动机

在我们踏上激动人心的GAN和MNIST数字之旅之前,让我们退后一步讨论为什么您想要使用C++前端而不是Python前端。我们(PyTorch团队)创建C++前端是为了促进在Python无法使用或者根本不是正确工具的环境中进行研究。这些环境的例子包括:

  • 低延迟系统: 您可能希望在具有高帧速率和低延迟要求的纯C++游戏引擎中进行强化学习研究。使用纯C++库更适合这样的环境,而不是Python库。由于Python解释器的速度缓慢,Python可能完全不可行。

  • 高度线程化的环境: 由于全局解释器锁(GIL)的存在,Python无法同时运行多个系统线程。虽然多进程是一种替代方案,但其可扩展性较差且存在显著缺点。C++则没有这样的限制,并且线程的创建和使用更加轻松。需要大量并行计算的模型,例如 Deep Neuroevolution 中使用的模型,可以从中受益。

  • 现有的C++代码库: 如果您拥有一个现有的C++应用程序,无论是用于后端服务器中提供网页服务,还是在照片编辑软件中进行3D图像渲染,并且希望将机器学习方法集成到您的系统中,C++前端允许您继续使用C++,避免在Python和C++之间来回绑定的麻烦,同时保留了传统PyTorch(Python)经验的大部分灵活性和直观性。

C++前端并非旨在与Python前端竞争,而是为其补充。我们知道研究人员和工程师们都喜欢PyTorch,因为它简单、灵活并且API直观。我们的目标是确保您可以在所有可能的环境中利用这些核心设计原则,包括上述描述的环境。如果这些场景之一与您的使用案例相符,或者您仅仅出于兴趣或好奇,请继续阅读,以下段落将详细介绍C++前端。

小技巧

C++前端试图提供一个尽可能接近Python前端的API。如果您熟悉Python前端并且想知道“如何用C++前端实现X功能?”, 按照您在Python中的编码方式编写代码,多数情况下,在C++中您会发现与Python中相同的函数和方法(只是需要将点替换为双冒号)。

编写一个基本的应用程序

让我们从编写一个最小的C++应用程序开始,以验证我们在设置和构建环境方面是否一致。首先,您需要获取一个 LibTorch 发行版– 一个预先构建的压缩档案,其中包含使用C++前端所需的所有相关头文件、库和CMake构建文件。可以从PyTorch官网上下载适用于Linux、MacOS和Windows的LibTorch发行版:PyTorch官网。本教程其余部分将以基本的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,并暂时只打印一个3x3的单位矩阵:

#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、普通的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::Linear``、torch::nn::Dropout``或``torch::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()``方法,它返回一个类似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`。引用语义的优势在于它减少了思考模块如何传递给函数以及参数如何声明的认知负担(假设你在所有地方都使用`shared_ptr`),这与Python类似。

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`模块的简化版本定义(完整版在此处:https://github.com/pytorch/pytorch/blob/master/torch/csrc/api/include/torch/nn/modules/linear.h):

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_ptr`或`std::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`是“空的”,即包含一个空指针。那么默认构造的`Linear`或`Net`是什么呢?嗯,这个选择比较难。我们可以说它应该是一个空的(null)`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::save`和`torch::load)仅支持模块持有者(或普通`shared_ptr`)。因此,模块持有者API是定义C++前端模块的推荐方式,我们将在本教程中继续使用该API。

定义DCGAN模块

现在我们已经有了定义机器学习任务模块所需的背景知识和介绍。回顾一下:我们的任务是从`MNIST数据集 <https://huggingface.co/datasets/ylecun/mnist>`_生成数字图片。我们想用一个`生成对抗网络(GAN) <https://papers.nips.cc/paper/5423-generative-adversarial-nets.pdf>`_来完成这个任务。特别是,我们将使用一个`DCGAN架构 <https://arxiv.org/abs/1511.06434>`_——这是第一种和最简单的一种,但对于该任务来说完全足够。

小技巧

本教程中展示的完整源码可以在这个仓库中找到:https://github.com/pytorch/examples/tree/master/cpp/dcgan

什么是GAN,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++前端中内置模块的简要说明:每个模块都有一些必须的选项,如`BatchNorm2d`的特征数。如果你只需要配置这些必须选项,可以直接传递给模块的构造函数,比如`BatchNorm2d(128)`或`Dropout(0.5)`或`Conv2d(8, 4, 2)`(用于输入通道计数、输出通道计数和核大小)。然而,如果你需要修改其他通常默认的选项,例如`Conv2d`中的`bias`,则需要构造并传递一个*选项对象*。C++前端的每个模块都有一个相关的选项结构体,称为`ModuleOptions`,其中`Module`是模块名称,例如`Linear`的选项为`LinearOptions`。这就是我们在上述`Conv2d`模块中所做的。

判别器模块

判别器同样是一系列卷积、批量归一化和激活构成的序列。然而,卷积现在是常规卷积而非转置卷积,我们使用alpha值为0.2的泄漏ReLU而不是普通ReLU。此外,最终激活变为Sigmoid,将值归一化到0到1的范围内。我们可以将这些归一化值解释为判别器对图像真实性的概率判断。

为了构建判别器,我们将尝试使用一种不同的方式:一个`Sequential`模块。与Python中的类似,PyTorch提供了两种模型定义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++前端``data`` 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``目录中。您可以使用`此脚本 <https://gist.github.com/goldsborough/6dd52a5e01ed73a642c1e772084bcd03>`_下载MNIST数据集。

接下来,我们创建一个数据加载器并将此数据集传递给它。要创建一个新的数据加载器,我们使用``torch::data::make_data_loader``,它返回正确类型的``std::unique_ptr``(取决于数据集类型、采样器类型和其他一些实现细节):

auto data_loader = torch::data::make_data_loader(std::move(dataset));

数据加载器确实有很多选项。您可以在`这里 <https://github.com/pytorch/pytorch/blob/master/torch/csrc/api/include/torch/data/dataloader_options.h>`_查看完整设置。例如,为了加快数据加载,我们可以增加工作线程数量。默认值为零,这意味着将使用主线程。如果我们将``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``归并,所以数据加载器只返回一个这样的例子。如果我们未应用归并,数据加载器将生成``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 <https://arxiv.org/pdf/1412.6980.pdf>`_算法:

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的优化器。`文档 <https://pytorch.org/cppdocs/api/namespace_torch__optim.html>`_包含最新列表。

接下来,我们需要更新我们的训练循环。我们将在外部添加一个循环以每个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()``对网络进行反向传播以计算新的梯度。我们对假图像重复此操作。我们不是使用数据集中的图像,而是让生成器通过输入一批随机噪声来创建假图像。然后,我们将这些假图像传递给判别器。这一次,我们希望判别器输出低概率,理想情况下全部为零。一旦我们为真实图像批次和假图像批次计算了判别器损失,我们可以通过判别器的优化器前进一步以更新其参数。

为了训练生成器,我们再次首先将其梯度归零,然后重新评估假图像上的判别器。然而,这一次我们希望判别器分配非常接近1的概率,这表明生成器可以生成让判别器认为它们实际上是真实的(来自数据集)的图像。为此,我们将``fake_labels``张量填充为全1。最后,我们对生成器的优化器也进行一步调整以更新其参数。

我们现在应该可以在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来序列化和反序列化模型和优化器状态以及单个张量。

核心API是``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';
}

其中``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

应该看起来像这样:

数字

数字!太棒了!现在机会在你手中:你能改进这个模型让数字看起来更好吗?

总结

本教程希望能够向您介绍PyTorch C++前端的基本概念。像PyTorch这样的机器学习库必然有一个非常广泛和全面的API。因此,有许多概念我们没有时间或空间在此处讨论。然而,我鼓励您尝试使用API,并在您遇到困难时查阅`我们的文档 <https://pytorch.org/cppdocs/>`_,特别是`库API <https://pytorch.org/cppdocs/api/library_root.html>`_部分。另外,请记住,C++前端尽可能遵循Python前端的设计和语义,因此您可以利用这一事实来提高您的学习速度。

小技巧

本教程中展示的完整源码可以在这个仓库中找到:https://github.com/pytorch/examples/tree/master/cpp/dcgan

如往常,如果您遇到任何问题或有疑问,可以使用论坛 https://discuss.pytorch.org/ 或 GitHub 问题 https://github.com/pytorch/pytorch/issues 与我们联系。

文档

访问 PyTorch 的详细开发者文档

查看文档

教程

获取针对初学者和高级开发人员的深入教程

查看教程

资源

查找开发资源并获得问题的解答

查看资源