評價此頁

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與我們聯絡。