评价此页

Tensor 创建 API#

本笔记描述了如何在 PyTorch C++ API 中创建 tensors。它重点介绍了可用的工厂函数(这些函数根据某种算法填充新 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——工厂函数的 size 参数的类型——是通过在花括号中指定每个维度的值来构造的。例如,{2, 3} 表示一个具有两行三列的 tensor(在这种情况下是矩阵),{3, 4, 5} 表示一个三维 tensor,{2} 表示一个具有两个分量的一维 tensor。在一维情况下,您可以省略花括号,直接传递单个整数,就像我们上面所做的那样。请注意,花括号只是构造 IntArrayRef 的一种方式。您还可以传递一个 std::vector<int64_t> 以及其他一些类型。无论哪种方式,这意味着我们可以通过编写以下代码来构造一个填充了来自单位正态分布的随机值的 3 维 tensor:

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

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

传递特定于函数的参数#

Neither ones nor 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 的数据类型(例如 float32int64),或者它是否驻留在 CPU 或 GPU 内存中。这些属性的指定留给了每个工厂函数的最后一个参数:一个 TensorOptions 对象,下文将讨论。

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

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

  • layout,即 strided(密集)或 sparse,

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

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

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

  • 对于 dtypekUInt8kInt8kInt16kInt32kInt64kFloat32kFloat64

  • 对于 layoutkStridedkSparse

  • 对于 devicekCPUkCUDA(后者接受可选的设备索引),

  • 对于 requires_gradtruefalse

提示

对于 dtype,存在“Rust 风格”的简写,例如 kF32 而不是 kFloat32。请在此处查看完整列表:here

TensorOptions 的一个实例存储了每个轴的具体值。例如,创建一个 TensorOptions 对象,该对象表示一个 64 位浮点、strided、需要梯度的 tensor,并且驻留在 CUDA 设备 1 上:

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 对象,但 dtypelayout 已设为默认值:

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::kFloat32torch::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,它们允许使用与 Python 同样方便的方式在 C++ 中创建 tensor。比较 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,与源 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 issue与我们联系。