評價此頁

張量建立 API#

本說明介紹如何在 PyTorch C++ API 中建立張量。它重點介紹了可用的工廠函式,這些函式根據某種演算法填充新張量,並列出了可用於配置新張量的形狀、資料型別、裝置及其他屬性的選項。

工廠函式#

工廠函式是一種生成新張量的函式。PyTorch(包括 Python 和 C++)中有許多可用的工廠函式,它們在返回新張量之前初始化張量的方式各不相同。所有工廠函式都遵循以下通用“模式”

torch::<function-name>(<function-specific-options>, <sizes>, <tensor-options>)

讓我們分解此“模式”的各個部分

  1. <function-name> 是您想要呼叫的函式名稱,

  2. <functions-specific-options> 是特定工廠函式接受的任何必需或可選引數,

  3. <sizes>IntArrayRef 型別的物件,用於指定結果張量的形狀,

  4. <tensor-options>TensorOptions 的一個例項,用於配置結果張量的資料型別、裝置、佈局及其他屬性。

選擇工廠函式#

在撰寫本文時,以下工廠函式可用(超連結指向相應的 Python 函式,因為它們的文件通常更詳細——C++ 中的選項是相同的)

  • arange: 返回一個包含整數序列的張量,

  • empty: 返回一個帶有未初始化值的張量,

  • eye: 返回一個單位矩陣,

  • full: 返回一個填充有單個值的張量,

  • linspace: 返回一個在某個區間內線性分佈值的張量,

  • logspace: 返回一個在某個區間內對數分佈值的張量,

  • ones: 返回一個填充有全一值的張量,

  • rand: 返回一個從 [0, 1) 上的均勻分佈中抽取值填充的張量。

  • randint: 返回一個包含從某個區間隨機抽取的整數的張量,

  • randn: 返回一個填充有從單位正態分佈中抽取值的張量,

  • randperm: 返回一個填充有某個區間內的整數隨機排列的張量,

  • zeros: 返回一個填充有全零值的張量。

指定尺寸#

按照張量填充方式的性質,不需要特定引數的函式可以直接透過尺寸呼叫。例如,以下行建立一個包含 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] 等價,但更推薦使用前者。

傳遞函式特定引數#

onesrandn 都不接受任何額外的引數來改變其行為。需要進一步配置的一個函式是 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 引數。

配置張量屬性#

前面一節討論了函式特定引數。函式特定引數只能更改填充張量的值,有時也會更改張量的尺寸。它們永遠不會更改諸如正在建立的張量的資料型別(例如 float32int64),或者張量是否儲存在 CPU 或 GPU 記憶體中。這些屬性的指定留給了每個工廠函式的最後一個引數:一個 TensorOptions 物件,下面將進行討論。

TensorOptions 是一個封裝張量構造軸的類。這裡的構造軸是指張量在構造之前(有時也可在構造之後)可以配置的特定屬性。這些構造軸包括:

  • dtype(以前稱為“標量型別”),它控制張量中儲存元素的資料型別,

  • layout,可以是跨步式(密集)或稀疏式,

  • device,表示儲存張量的計算裝置(例如 CPU 或 CUDA GPU),

  • requires_grad 布林值,用於啟用或停用張量的梯度記錄,

如果您習慣使用 Python 中的 PyTorch,這些軸會聽起來非常熟悉。目前這些軸的允許值包括:

  • 對於 dtypekUInt8, kInt8, kInt16, kInt32, kInt64, kFloat32kFloat64

  • 對於 layoutkStridedkSparse

  • 對於 devicekCPUkCUDA(接受可選的裝置索引),

  • 對於 requires_gradtruefalse

提示

存在“Rust風格”的 dtype 縮寫,例如 kF32 代替 kFloat32。請參閱此處檢視完整列表。

TensorOptions 的一個例項儲存了這些軸中每個軸的具體值。下面是一個建立 TensorOptions 物件的示例,該物件表示一個需要梯度計算的 64 位浮點型、跨步式張量,並且位於 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::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,如何使 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);

結論#

希望本說明能讓您很好地理解如何使用 PyTorch C++ API 以慣用方式建立和轉換張量。如果您有任何進一步的問題或建議,請使用我們的論壇GitHub issues 與我們聯絡。