Tensor 创建 API¶
本注释描述了如何在 PyTorch C++ API 中创建张量。它重点介绍了可用的工厂函数,这些函数根据某种算法填充新的张量,并列出了可用于配置新张量的形状、数据类型、设备和其他属性的选项。
工厂函数¶
工厂函数是生成新张量的函数。 PyTorch 中有许多可用的工厂函数(Python 和 C++ 中都有),它们在新张量返回之前以不同的方式初始化新张量。所有工厂函数都遵循以下通用“模式”
torch::<function-name>(<function-specific-options>, <sizes>, <tensor-options>)
让我们剖析一下这个“模式”的各个部分
<function-name>
是您想要调用的函数的名称,<functions-specific-options>
是特定工厂函数接受的任何必需或可选参数,<sizes>
是IntArrayRef
类型的对象,用于指定结果张量的形状,<tensor-options>
是TensorOptions
的实例,用于配置结果张量的数据类型、设备、布局和其他属性。
选择工厂函数¶
在撰写本文时,以下工厂函数可用(超链接指向相应的 Python 函数,因为它们通常具有更简洁的文档——C++ 中的选项是相同的)
arange:返回一个包含整数序列的张量,
empty:返回一个包含未初始化值的张量,
eye:返回一个单位矩阵,
full:返回一个用单个值填充的张量,
linspace:返回一个张量,其值在某个区间内线性间隔,
logspace:返回一个张量,其值在某个区间内对数间隔,
ones:返回一个用全 1 填充的张量,
rand:返回一个张量,其值从
[0, 1)
上的均匀分布中抽取。randint:返回一个张量,其整数值从一个区间内随机抽取,
randn:返回一个张量,其值从单位正态分布中抽取,
randperm:返回一个张量,其中填充了某个区间内整数的随机排列,
zeros:返回一个用全 0 填充的张量。
指定大小¶
对于那些本质上不需要特定参数来填充张量的函数,只需指定大小即可调用。例如,以下行创建一个具有 5 个分量的向量,初始值全部设置为 1
torch::Tensor tensor = torch::ones(5);
如果我们想创建一个 3 x 5
矩阵,或者一个 2 x 3 x 4
张量呢?通常,IntArrayRef
(工厂函数的大小参数的类型)是通过在花括号中指定每个维度的大小来构造的。例如,{2, 3}
表示具有两行三列的张量(在本例中为矩阵),{3, 4, 5}
表示三维张量,{2}
表示具有两个分量的一维张量。在一维情况下,您可以省略花括号,就像我们上面所做的那样,只传递单个整数。请注意,花括号只是构造 IntArrayRef
的一种方式。您也可以传递 std::vector<int64_t>
和其他一些类型。无论哪种方式,这意味着我们可以通过编写以下代码来构造一个用单位正态分布中的值填充的三维张量
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.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 中的关键字参数一样,有助于提高可读性。
提示
主要要点是大小始终跟随函数特定的参数。
注意
有时函数根本不需要大小。例如,arange
返回的张量的大小完全由其函数特定的参数(整数范围的下限和上限)指定。在这种情况下,该函数不接受 size
参数。
配置张量的属性¶
上一节讨论了函数特定的参数。函数特定的参数只能更改张量填充的值,有时还可以更改张量的大小。它们永远不会更改数据类型(例如 float32
或 int64
)或它是在 CPU 还是 GPU 内存中。这些属性的规范留给每个工厂函数的最后一个参数:TensorOptions
对象,如下所述。
TensorOptions
是一个类,它封装了张量的构造轴。 构造轴是指张量的特定属性,可以在构造之前配置(有时在之后更改)。这些构造轴是
dtype
(以前称为“标量类型”),它控制张量中存储的元素的数据类型,layout
,它可以是 strided(密集)或 sparse(稀疏),device
,它表示存储张量的计算设备(如 CPU 或 CUDA GPU),requires_grad
布尔值,用于启用或禁用张量的梯度记录,
如果您习惯了 Python 中的 PyTorch,这些轴听起来会非常熟悉。目前这些轴允许的值为
对于
dtype
:kUInt8
、kInt8
、kInt16
、kInt32
、kInt64
、kFloat32
和kFloat64
,对于
layout
:kStrided
和kSparse
,对于
device
:kCPU
或kCUDA
(接受可选的设备索引),对于
requires_grad
:true
或false
。
提示
存在“Rust 风格”的 dtype 简写形式,例如 kF32
而不是 kFloat32
。有关完整列表,请参阅此处。
TensorOptions
的实例为每个轴存储一个具体值。这是一个创建 TensorOptions
对象的示例,该对象表示 64 位浮点数、需要梯度的 strided 张量,并位于 CUDA 设备 1 上
auto options =
torch::TensorOptions()
.dtype(torch::kFloat32)
.layout(torch::kStrided)
.device(torch::kCUDA, 1)
.requires_grad(true);
请注意我们如何使用 TensorOptions
的“构建器”样式方法来逐步构建对象。如果我们将此对象作为最后一个参数传递给工厂函数,则新创建的张量将具有这些属性
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());
现在,您可能会想:我真的需要为我创建的每个新张量指定每个轴吗?幸运的是,答案是“否”,因为每个轴都有一个默认值。这些默认值为
kFloat32
对于 dtype,kStrided
对于 layout,kCPU
对于 device,false
对于requires_grad
。
这意味着在构建 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
对象可以完全从任何张量工厂调用中省略
// 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++ 中实现与 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
来配置应如何创建新张量一样,我们也可以使用 TensorOptions
将张量从一组属性转换为一组新属性。这种转换通常会创建一个新张量,而不是就地发生。例如,如果我们有一个使用以下代码创建的 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
是一个指向新内存的新张量,与源 source_tensor
无关。
然后我们可以将其从 CPU 内存移动到 GPU 内存
torch::Tensor gpu_tensor = float_tensor.to(torch::kCUDA);
如果您有多个可用的 CUDA 设备,则上述代码会将张量复制到默认 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);