Tensor 创建 API#
本笔记介绍了如何在 PyTorch C++ API 中创建 tensor。它重点介绍了可用的工厂函数,这些函数根据某些算法填充新 tensor,并列出了可用于配置新 tensor 的形状、数据类型、设备和其他属性的选项。
工厂函数#
工厂函数是用于生成新 tensor 的函数。PyTorch 中有许多工厂函数(包括 Python 和 C++),它们在返回新 tensor 之前初始化新 tensor 的方式上有所不同。所有工厂函数都遵循以下通用“模式”:
torch::<function-name>(<function-specific-options>, <sizes>, <tensor-options>)
让我们来分析一下这个“模式”的各个部分。
<function-name>
是您希望调用的函数名称,<functions-specific-options>
是特定工厂函数接受的任何必需或可选参数,<sizes>
是 `IntArrayRef` 类型的对象,用于指定结果 tensor 的形状,<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
torch::Tensor tensor = torch::randn({3, 4, 5});
assert(tensor.sizes() == std::vector<int64_t>{3, 4, 5});
tensor.sizes()
返回一个 `IntArrayRef`,可以将其与 `std::vectortensor.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进行联系。