张量创建 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 风格”简写,例如使用 kF32
代替 kFloat32
。请参阅 此处 获取完整列表。
TensorOptions
的实例为每个轴存储一个具体的值。以下是如何创建一个表示 64 位浮点数、strided 张量、需要梯度并在 CUDA 设备 1 上的 TensorOptions
对象的示例:
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);