评价此页

Tensor 创建 API#

本笔记介绍了如何在 PyTorch C++ API 中创建 tensor。它重点介绍了可用的工厂函数,这些函数根据某些算法填充新 tensor,并列出了可用于配置新 tensor 的形状、数据类型、设备和其他属性的选项。

工厂函数#

工厂函数是用于生成新 tensor 的函数。PyTorch 中有许多工厂函数(包括 Python 和 C++),它们在返回新 tensor 之前初始化新 tensor 的方式上有所不同。所有工厂函数都遵循以下通用“模式”:

torch::<function-name>(<function-specific-options>, <sizes>, <tensor-options>)

让我们来分析一下这个“模式”的各个部分。

  1. <function-name> 是您希望调用的函数名称,

  2. <functions-specific-options> 是特定工厂函数接受的任何必需或可选参数,

  3. <sizes> 是 `IntArrayRef` 类型的对象,用于指定结果 tensor 的形状,

  4. <tensor-options> 是 `TensorOptions` 的实例,用于配置结果 tensor 的数据类型、设备、布局和其他属性。

选择工厂函数#

在撰写本文时,以下工厂函数可用(链接指向相应的 Python 函数,因为它们的文档通常更清晰——C++ 中的选项相同):

  • arange:返回一个包含整数序列的 tensor,

  • empty:返回一个包含未初始化值的 tensor,

  • eye:返回一个单位矩阵,

  • full:返回一个填充了单个值的 tensor,

  • linspace:返回一个在某个区间内线性间隔值的 tensor,

  • logspace:返回一个在某个区间内对数间隔值的 tensor,

  • ones:返回一个填充了所有 1 的 tensor,

  • rand:返回一个填充了从 [0, 1) 上均匀分布中抽取的值的 tensor。

  • randint:返回一个从区间中随机抽取整数的 tensor,

  • randn:返回一个填充了从单位正态分布中抽取的值的 tensor,

  • randperm:返回一个填充了某个区间内整数的随机排列的 tensor,

  • zeros:返回一个填充了所有 0 的 tensor。

指定大小#

由于其填充 tensor 的性质,不需要特定参数的函数可以仅使用大小进行调用。例如,以下行创建了一个具有 5 个分量的向量,最初所有分量都设置为 1。

torch::Tensor tensor = torch::ones(5);

如果我们想创建一个 3 x 5 的矩阵,或者一个 2 x 3 x 4 的 tensor 怎么办?通常,`IntArrayRef`——工厂函数大小参数的类型——是通过在花括号中指定每个维度的尺寸来构建的。例如,对于一个有两行三列的 tensor(在此情况下为矩阵),使用 `{2, 3}`;对于一个三维 tensor,使用 `{3, 4, 5}`;对于一个具有两个分量的、一维的 tensor,使用 `{2}`。在一维情况下,可以省略花括号,就像我们上面所做的那样,只传递单个整数。请注意,花括号只是构建 `IntArrayRef` 的一种方式。您也可以传递 `std::vector` 以及其他几种类型。无论哪种方式,这意味着我们可以通过编写以下内容来创建一个填充了单位正态分布值的、三维的 tensor:

torch::Tensor tensor = torch::randn({3, 4, 5});
assert(tensor.sizes() == std::vector<int64_t>{3, 4, 5});

tensor.sizes() 返回一个 `IntArrayRef`,可以将其与 `std::vector` 进行比较,并且我们可以看到它包含我们传递给 tensor 的大小。您也可以编写 tensor.size(i) 来访问单个维度,这等效于 tensor.sizes()[i],但更受推荐。

传递特定于函数的参数#

无论是 `ones` 还是 `randn`,它们都不接受任何额外的参数来改变其行为。一个需要进一步配置的函数是 `randint`,它接受一个生成整数的上限,以及一个可选的下限,默认值为零。这里我们创建一个 5 x 5 的方形矩阵,其中包含 0 到 10 之间的整数。

torch::Tensor tensor = torch::randint(/*high=*/10, {5, 5});

这里我们将下限提高到 3。

torch::Tensor tensor = torch::randint(/*low=*/3, /*high=*/10, {5, 5});

当然,内联注释 /*low=*//*high=*/ 不是必需的,但就像 Python 中的关键字参数一样,它们有助于提高可读性。

提示

主要 takeaway 是,大小始终遵循特定于函数的参数。

注意

有时函数根本不需要大小。例如,由 `arange` 返回的 tensor 的大小由其特定于函数的参数完全指定——整数范围的下限和上限。在这种情况下,函数不接受 `size` 参数。

配置 Tensor 的属性#

上一节讨论了特定于函数的参数。特定于函数的参数只能更改填充 tensor 的值,有时还可以更改 tensor 的大小。它们从不更改正在创建的 tensor 的数据类型(例如 `float32` 或 `int64`)或它是否位于 CPU 或 GPU 内存中。这些属性的规范留给了每个工厂函数的最后一个参数:`TensorOptions` 对象,如下所述。

`TensorOptions` 是一个封装 Tensor 构建轴的类。我们所说的构建轴是 Tensor 的一个特定属性,可以在其构建之前进行配置(有时也可以之后更改)。这些构建轴是:

  • `dtype`(以前称为“标量类型”),它控制 tensor 中存储的元素的类型,

  • `layout`,它是 strided(密集)或 sparse(稀疏),

  • `device`,它代表存储 tensor 的计算设备(例如 CPU 或 CUDA GPU),

  • `requires_grad` 布尔值,用于启用或禁用 tensor 的梯度记录,

如果您熟悉 Python 中的 PyTorch,这些轴听起来会很熟悉。目前这些轴的允许值是:

  • `dtype`:`kUInt8`、`kInt8`、`kInt16`、`kInt32`、`kInt64`、`kFloat32` 和 `kFloat64`,

  • `layout`:`kStrided` 和 `kSparse`,

  • `device`:`kCPU` 或 `kCUDA`(可接受可选的设备索引),

  • `requires_grad`:`true` 或 `false`。

提示

存在 `dtype` 的“Rust 风格”简写,例如 `kF32` 而不是 `kFloat32`。有关完整列表,请参见此处

`TensorOptions` 的实例存储每个轴的具体值。这是一个创建 `TensorOptions` 对象的示例,该对象表示一个 64 位浮点、strided、需要梯度且位于 CUDA 设备 1 上的 tensor。

auto options =
  torch::TensorOptions()
    .dtype(torch::kFloat32)
    .layout(torch::kStrided)
    .device(torch::kCUDA, 1)
    .requires_grad(true);

请注意,我们如何使用 `TensorOptions` 的“构建器样式”方法逐块构建对象。如果我们将其作为工厂函数的最后一个参数传递,新创建的 tensor 将具有这些属性。

torch::Tensor tensor = torch::full({3, 4}, /*value=*/123, options);

assert(tensor.dtype() == torch::kFloat32);
assert(tensor.layout() == torch::kStrided);
assert(tensor.device().type() == torch::kCUDA); // or device().is_cuda()
assert(tensor.device().index() == 1);
assert(tensor.requires_grad());

现在,您可能会想:我真的需要为我创建的每个新 tensor 指定每个轴吗?幸运的是,答案是“不”,因为**每个轴都有一个默认值**。这些默认值是:

  • `dtype` 为 `kFloat32`,

  • `layout` 为 `kStrided`,

  • `device` 为 `kCPU`,

  • `requires_grad` 为 `false`。

这意味着,在创建 `TensorOptions` 对象时,您省略的任何轴都将采用其默认值。例如,这是我们之前谈论的 `TensorOptions` 对象,但 `dtype` 和 `layout` 已默认。

auto options = torch::TensorOptions().device(torch::kCUDA, 1).requires_grad(true);

事实上,我们甚至可以省略所有轴,以获得一个完全默认的 `TensorOptions` 对象。

auto options = torch::TensorOptions(); // or `torch::TensorOptions options;`

一个很好的结果是,我们可以完全省略我们一直在大力讨论的 `TensorOptions` 对象,无需调用任何 tensor 工厂。

// A 32-bit float, strided, CPU tensor that does not require a gradient.
torch::Tensor tensor = torch::randn({3, 4});
torch::Tensor range = torch::arange(5, 10);

但是,更方便的是:在目前为止介绍的 API 中,您可能已经注意到 `torch::TensorOptions()` 相当冗长。好消息是,对于每个构造轴(dtype、layout、device 和 `requires_grad`),在 `torch::` 命名空间中都有一个*免费函数*,您可以为其传递一个值。每个函数然后返回一个已预配置了该轴的 `TensorOptions` 对象,同时还允许通过上面显示的构建器样式方法进行进一步修改。例如:

torch::ones(10, torch::TensorOptions().dtype(torch::kFloat32))

等同于

torch::ones(10, torch::dtype(torch::kFloat32))

并且进一步,而不是

torch::ones(10, torch::TensorOptions().dtype(torch::kFloat32).layout(torch::kStrided))

我们可以只写

torch::ones(10, torch::dtype(torch::kFloat32).layout(torch::kStrided))

这为我们节省了大量的打字时间。这意味着在实践中,您应该几乎永远不必写出 `torch::TensorOptions`。而是使用 `torch::dtype()`、`torch::device()`、`torch::layout()` 和 `torch::requires_grad()` 函数。

最后一点便利性是 `TensorOptions` 可以从单个值隐式构造。这意味着,当一个函数有一个 `TensorOptions` 类型的参数时,就像所有工厂函数一样,我们可以直接传递一个值,例如 `torch::kFloat32` 或 `torch::kStrided`,而不是完整的对象。因此,当只有一个轴我们需要相对于其默认值进行更改时,我们可以只传递该值。因此,以前的

torch::ones(10, torch::TensorOptions().dtype(torch::kFloat32))

变成了

torch::ones(10, torch::dtype(torch::kFloat32))

并且最终可以缩短为

torch::ones(10, torch::kFloat32)

当然,使用这种短语法不可能进一步修改 `TensorOptions` 实例的属性,但如果我们需要做的只是更改一个属性,这非常实用。

总之,我们现在可以比较 `TensorOptions` 的默认值,以及使用免费函数创建 `TensorOptions` 的简写 API,如何使 C++ 中的 tensor 创建与 Python 中的一样方便。比较 Python 中的此调用:

torch.randn(3, 4, dtype=torch.float32, device=torch.device('cuda', 1), requires_grad=True)

与 C++ 中等效的调用:

torch::randn({3, 4}, torch::dtype(torch::kFloat32).device(torch::kCUDA, 1).requires_grad(true))

非常接近!

转换#

就像我们可以使用 `TensorOptions` 来配置新 tensor 的创建方式一样,我们也可以使用 `TensorOptions` 将一个 tensor 从一套属性转换为另一套属性。此类转换通常会创建一个新的 tensor,并且不会就地发生。例如,如果我们有一个 `source_tensor`,其创建方式如下:

torch::Tensor source_tensor = torch::randn({2, 3}, torch::kInt64);

我们可以将其从 `int64` 转换为 `float32`。

torch::Tensor float_tensor = source_tensor.to(torch::kFloat32);

注意

转换结果 `float_tensor` 是一个指向新内存的新 tensor,与源 `source_tensor` 无关。

然后我们可以将其从 CPU 内存移动到 GPU 内存。

torch::Tensor gpu_tensor = float_tensor.to(torch::kCUDA);

如果您有多个可用的 CUDA 设备,上面的代码会将 tensor 复制到*默认* CUDA 设备,您可以通过 `torch::DeviceGuard` 进行配置。如果没有 `DeviceGuard`,这将是 GPU 1。如果您想指定不同的 GPU 索引,可以在 `Device` 构造函数中传递它。

torch::Tensor gpu_two_tensor = float_tensor.to(torch::Device(torch::kCUDA, 1));

在 CPU 到 GPU 复制及其反向操作的情况下,我们还可以通过将 `/*non_blocking=*/false` 作为最后一个参数传递给 `to()` 来将内存复制配置为*异步*。

torch::Tensor async_cpu_tensor = gpu_tensor.to(torch::kCPU, /*non_blocking=*/true);

结论#

本笔记希望您对如何使用 PyTorch C++ API 以惯用的方式创建和转换 tensor 有了很好的理解。如果您有任何进一步的问题或建议,请使用我们的论坛GitHub issues进行联系。