評價此頁

介紹 || 張量 || Autograd || 構建模型 || TensorBoard 支援 || 訓練模型 || 模型理解

PyTorch 張量入門#

建立日期:2021 年 11 月 30 日 | 最後更新:2025 年 9 月 22 日 | 最後驗證:2024 年 11 月 05 日

請觀看下面的影片,或在 youtube 上觀看。

張量是 PyTorch 中的核心資料抽象。本互動式 Notebook 提供了對 torch.Tensor 類的深入介紹。

首先,讓我們匯入 PyTorch 模組。我們還將新增 Python 的 math 模組以方便某些示例。

import torch
import math

建立張量#

建立張量的最簡單方法是呼叫 torch.empty()

x = torch.empty(3, 4)
print(type(x))
print(x)
<class 'torch.Tensor'>
tensor([[ 1.9192e-04,  4.5804e-41,  2.2958e-32,  3.0645e-41],
        [-4.3165e+09,  3.0644e-41, -1.9917e+27,  4.5801e-41],
        [-1.9896e+27,  4.5801e-41, -1.9917e+27,  4.5801e-41]])

讓我們解析一下我們剛才所做的。

  • 我們使用 torch 模組附帶的眾多工廠方法之一建立了一個張量。

  • 張量本身是 2 維的,有 3 行 4 列。

  • 返回物件的型別是 torch.Tensor,它是 torch.FloatTensor 的別名;預設情況下,PyTorch 張量填充 32 位浮點數。(關於資料型別的更多資訊見下文。)

  • 列印張量時,您可能會看到一些看起來隨機的值。 torch.empty() 呼叫為張量分配記憶體,但並未用任何值對其進行初始化 - 因此您看到的是分配時記憶體中存在的任何內容。

關於張量及其維數和術語的簡要說明。

  • 您有時會看到 1 維張量被稱為向量

  • 同樣,2 維張量通常被稱為矩陣

  • 任何超過兩維的通常都只稱為張量。

十有八九,您會想用某個值來初始化張量。常見的情況是全零、全一或隨機值,並且 torch 模組為所有這些提供了工廠方法。

zeros = torch.zeros(2, 3)
print(zeros)

ones = torch.ones(2, 3)
print(ones)

torch.manual_seed(1729)
random = torch.rand(2, 3)
print(random)
tensor([[0., 0., 0.],
        [0., 0., 0.]])
tensor([[1., 1., 1.],
        [1., 1., 1.]])
tensor([[0.3126, 0.3791, 0.3087],
        [0.0736, 0.4216, 0.0691]])

工廠方法都如您所料 - 我們有一個全零的張量,一個全一的張量,以及一個包含 0 到 1 之間隨機值的張量。

隨機張量和種子#

說到隨機張量,您注意到它前面緊跟著 torch.manual_seed() 的呼叫了嗎?用隨機值初始化張量(例如模型的學習權重)很常見,但在某些情況下(尤其是在研究環境中),您會希望確保結果的可復現性。手動設定隨機數生成器的種子是做到這一點的方法。讓我們仔細看看。

tensor([[0.3126, 0.3791, 0.3087],
        [0.0736, 0.4216, 0.0691]])
tensor([[0.2332, 0.4047, 0.2162],
        [0.9927, 0.4128, 0.5938]])
tensor([[0.3126, 0.3791, 0.3087],
        [0.0736, 0.4216, 0.0691]])
tensor([[0.2332, 0.4047, 0.2162],
        [0.9927, 0.4128, 0.5938]])

您應該在上面看到 random1random3 具有相同的值,random2random4 也一樣。手動設定 RNG 的種子會重置它,因此在大多數情況下,依賴隨機數的相同計算應提供相同的結果。

有關更多資訊,請參閱 PyTorch 關於可復現性的文件

張量形狀#

通常,在對兩個或多個張量執行操作時,它們需要具有相同的形狀 - 即,具有相同的維數並在每個維數中具有相同的單元數。為此,我們有 torch.*_like() 方法。

torch.Size([2, 2, 3])
tensor([[[1.8992e-34, 3.0645e-41, 6.3185e-16],
         [4.5803e-41, 1.4013e-45, 0.0000e+00]],

        [[8.4078e-45, 0.0000e+00, 0.0000e+00],
         [0.0000e+00, 0.0000e+00, 0.0000e+00]]])
torch.Size([2, 2, 3])
tensor([[[2.2792e-32, 3.0645e-41, 9.1477e-41],
         [0.0000e+00, 1.6816e-44, 0.0000e+00]],

        [[3.5873e-43, 0.0000e+00, 6.6220e-06],
         [4.5804e-41, 0.0000e+00, 0.0000e+00]]])
torch.Size([2, 2, 3])
tensor([[[0., 0., 0.],
         [0., 0., 0.]],

        [[0., 0., 0.],
         [0., 0., 0.]]])
torch.Size([2, 2, 3])
tensor([[[1., 1., 1.],
         [1., 1., 1.]],

        [[1., 1., 1.],
         [1., 1., 1.]]])
torch.Size([2, 2, 3])
tensor([[[0.6128, 0.1519, 0.0453],
         [0.5035, 0.9978, 0.3884]],

        [[0.6929, 0.1703, 0.1384],
         [0.4759, 0.7481, 0.0361]]])

程式碼單元格中的第一個新內容是使用張量上的 .shape 屬性。此屬性包含張量每個維度的範圍列表 - 在我們的例子中,x 是一個 3 維張量,形狀為 2 x 2 x 3。

在此下方,我們呼叫了 .empty_like().zeros_like().ones_like().rand_like() 方法。使用 .shape 屬性,我們可以驗證這些方法中的每一個都返回具有相同維度和範圍的張量。

我們將介紹的最後一種建立張量的方法是從 PyTorch 集合直接指定其資料。

some_constants = torch.tensor([[3.1415926, 2.71828], [1.61803, 0.0072897]])
print(some_constants)

some_integers = torch.tensor((2, 3, 5, 7, 11, 13, 17, 19))
print(some_integers)

more_integers = torch.tensor(((2, 4, 6), [3, 6, 9]))
print(more_integers)
tensor([[3.1416, 2.7183],
        [1.6180, 0.0073]])
tensor([ 2,  3,  5,  7, 11, 13, 17, 19])
tensor([[2, 4, 6],
        [3, 6, 9]])

使用 torch.tensor() 是建立張量的最直接方法,如果您已經擁有 Python 元組或列表中的資料。如上所示,巢狀集合將導致多維張量。

注意

torch.tensor() 會建立資料的副本。

張量資料型別#

設定張量的資料型別可以通過幾種方式實現。

a = torch.ones((2, 3), dtype=torch.int16)
print(a)

b = torch.rand((2, 3), dtype=torch.float64) * 20.
print(b)

c = b.to(torch.int32)
print(c)
tensor([[1, 1, 1],
        [1, 1, 1]], dtype=torch.int16)
tensor([[ 0.9956,  1.4148,  5.8364],
        [11.2406, 11.2083, 11.6692]], dtype=torch.float64)
tensor([[ 0,  1,  5],
        [11, 11, 11]], dtype=torch.int32)

設定張量的底層資料型別的最簡單方法是在建立時使用一個可選引數。在上面的單元格的第一行,我們為張量 a 設定了 dtype=torch.int16。當我們列印 a 時,我們可以看到它填充的是 1 而不是 1. - 這是 Python 中表示整數型別而不是浮點數的細微提示。

列印 a 時還應注意的另一件事是,與我們保留 dtype 為預設值(32 位浮點數)時不同,列印張量時還會指定其 dtype

您可能還注意到,我們從指定張量的形狀作為一系列整數引數,轉變為將這些引數分組到元組中。這並非嚴格必要 - PyTorch 會將一系列初始的、未標記的整數引數視為張量形狀 - 但在新增可選引數時,可以使您的意圖更具可讀性。

設定資料型別的另一種方法是使用 .to() 方法。在上面的單元格中,我們以通常方式建立了一個隨機浮點張量 b。之後,我們使用 .to() 方法將 b 轉換為 32 位整數,從而建立了 c。請注意,c 包含與 b 相同的值,但已截斷為整數。

有關更多資訊,請參閱 資料型別文件

PyTorch 張量的數學與邏輯運算#

既然您已經瞭解了一些建立張量的方法... 您可以用它們做什麼?

讓我們先看看基本算術,以及張量如何與簡單的標量互動。

ones = torch.zeros(2, 2) + 1
twos = torch.ones(2, 2) * 2
threes = (torch.ones(2, 2) * 7 - 1) / 2
fours = twos ** 2
sqrt2s = twos ** 0.5

print(ones)
print(twos)
print(threes)
print(fours)
print(sqrt2s)
tensor([[1., 1.],
        [1., 1.]])
tensor([[2., 2.],
        [2., 2.]])
tensor([[3., 3.],
        [3., 3.]])
tensor([[4., 4.],
        [4., 4.]])
tensor([[1.4142, 1.4142],
        [1.4142, 1.4142]])

如上所示,張量與標量之間的算術運算(如加法、減法、乘法、除法和指數運算)會應用於張量的每個元素。因為此類運算的輸出將是一個張量,所以您可以按照通常的運算子優先順序規則將它們連結在一起,就像我們在建立 threes 的行中所做的那樣。

兩個張量之間的類似運算也如您直觀預期那樣工作。

powers2 = twos ** torch.tensor([[1, 2], [3, 4]])
print(powers2)

fives = ones + fours
print(fives)

dozens = threes * fours
print(dozens)
tensor([[ 2.,  4.],
        [ 8., 16.]])
tensor([[5., 5.],
        [5., 5.]])
tensor([[12., 12.],
        [12., 12.]])

這裡需要注意的是,前一個程式碼單元格中的所有張量都具有相同的形狀。當嘗試對不同形狀的張量執行二元運算時會發生什麼?

注意

下面的單元格會引發執行時錯誤。這是故意的。

a = torch.rand(2, 3)
b = torch.rand(3, 2)

print(a * b)

在一般情況下,您不能以這種方式操作不同形狀的張量,即使在像上面的單元格那樣張量具有相同數量元素的情況下也不行。

簡而言之:張量廣播#

注意

如果您熟悉 NumPy ndarray 中的廣播語義,您會發現這裡也適用相同的規則。

相同形狀規則的例外是張量廣播。以下是一個示例。

rand = torch.rand(2, 4)
doubled = rand * (torch.ones(1, 4) * 2)

print(rand)
print(doubled)
tensor([[0.6146, 0.5999, 0.5013, 0.9397],
        [0.8656, 0.5207, 0.6865, 0.3614]])
tensor([[1.2291, 1.1998, 1.0026, 1.8793],
        [1.7312, 1.0413, 1.3730, 0.7228]])

這裡的訣竅是什麼?我們如何將一個 2x4 的張量乘以一個 1x4 的張量?

廣播是一種對形狀相似的張量執行運算的方法。在上面的示例中,一行四列的張量被乘以兩行四列張量的兩行

這是深度學習中的一項重要運算。常見示例是將學習權重的張量乘以輸入張量的批次,分別對批次中的每個例項應用該運算,並返回一個形狀相同的張量 - 就像我們上面的 (2, 4) * (1, 4) 示例返回一個形狀為 (2, 4) 的張量一樣。

廣播規則是:

  • 每個張量至少必須有一個維度 - 沒有空張量。

  • 比較兩個張量的維度大小,從最後一個到第一個:

    • 每個維度必須相等,或者

    • 其中一個維度的大小必須是 1,或者

    • 其中一個張量中不存在該維度。

形狀相同的張量當然是“可廣播的”,正如您之前所見。

以下是一些符合上述規則並允許廣播的情況示例:

a =     torch.ones(4, 3, 2)

b = a * torch.rand(   3, 2) # 3rd & 2nd dims identical to a, dim 1 absent
print(b)

c = a * torch.rand(   3, 1) # 3rd dim = 1, 2nd dim identical to a
print(c)

d = a * torch.rand(   1, 2) # 3rd dim identical to a, 2nd dim = 1
print(d)
tensor([[[0.6493, 0.2633],
         [0.4762, 0.0548],
         [0.2024, 0.5731]],

        [[0.6493, 0.2633],
         [0.4762, 0.0548],
         [0.2024, 0.5731]],

        [[0.6493, 0.2633],
         [0.4762, 0.0548],
         [0.2024, 0.5731]],

        [[0.6493, 0.2633],
         [0.4762, 0.0548],
         [0.2024, 0.5731]]])
tensor([[[0.7191, 0.7191],
         [0.4067, 0.4067],
         [0.7301, 0.7301]],

        [[0.7191, 0.7191],
         [0.4067, 0.4067],
         [0.7301, 0.7301]],

        [[0.7191, 0.7191],
         [0.4067, 0.4067],
         [0.7301, 0.7301]],

        [[0.7191, 0.7191],
         [0.4067, 0.4067],
         [0.7301, 0.7301]]])
tensor([[[0.6276, 0.7357],
         [0.6276, 0.7357],
         [0.6276, 0.7357]],

        [[0.6276, 0.7357],
         [0.6276, 0.7357],
         [0.6276, 0.7357]],

        [[0.6276, 0.7357],
         [0.6276, 0.7357],
         [0.6276, 0.7357]],

        [[0.6276, 0.7357],
         [0.6276, 0.7357],
         [0.6276, 0.7357]]])

仔細檢視上面每個張量的值。

  • 建立 b 的乘法運算被廣播到 a 的每個“層”。

  • 對於 c,該運算被廣播到 a 的每個層和行 - 每個 3 元素列都相同。

  • 對於 d,我們顛倒了過來 - 現在每都相同,跨越層和列。

有關廣播的更多資訊,請參閱 PyTorch 文件

以下是一些嘗試廣播但會失敗的示例:

注意

下面的單元格會引發執行時錯誤。這是故意的。

a =     torch.ones(4, 3, 2)

b = a * torch.rand(4, 3)    # dimensions must match last-to-first

c = a * torch.rand(   2, 3) # both 3rd & 2nd dims different

d = a * torch.rand((0, ))   # can't broadcast with an empty tensor

張量的更多數學運算#

PyTorch 張量有三百多種可對其執行的操作。

以下是來自主要操作類別的一些小樣本。

# common functions
a = torch.rand(2, 4) * 2 - 1
print('Common functions:')
print(torch.abs(a))
print(torch.ceil(a))
print(torch.floor(a))
print(torch.clamp(a, -0.5, 0.5))

# trigonometric functions and their inverses
angles = torch.tensor([0, math.pi / 4, math.pi / 2, 3 * math.pi / 4])
sines = torch.sin(angles)
inverses = torch.asin(sines)
print('\nSine and arcsine:')
print(angles)
print(sines)
print(inverses)

# bitwise operations
print('\nBitwise XOR:')
b = torch.tensor([1, 5, 11])
c = torch.tensor([2, 7, 10])
print(torch.bitwise_xor(b, c))

# comparisons:
print('\nBroadcasted, element-wise equality comparison:')
d = torch.tensor([[1., 2.], [3., 4.]])
e = torch.ones(1, 2)  # many comparison ops support broadcasting!
print(torch.eq(d, e)) # returns a tensor of type bool

# reductions:
print('\nReduction ops:')
print(torch.max(d))        # returns a single-element tensor
print(torch.max(d).item()) # extracts the value from the returned tensor
print(torch.mean(d))       # average
print(torch.std(d))        # standard deviation
print(torch.prod(d))       # product of all numbers
print(torch.unique(torch.tensor([1, 2, 1, 2, 1, 2]))) # filter unique elements

# vector and linear algebra operations
v1 = torch.tensor([1., 0., 0.])         # x unit vector
v2 = torch.tensor([0., 1., 0.])         # y unit vector
m1 = torch.rand(2, 2)                   # random matrix
m2 = torch.tensor([[3., 0.], [0., 3.]]) # three times identity matrix

print('\nVectors & Matrices:')
print(torch.linalg.cross(v2, v1)) # negative of z unit vector (v1 x v2 == -v2 x v1)
print(m1)
m3 = torch.linalg.matmul(m1, m2)
print(m3)                  # 3 times m1
print(torch.linalg.svd(m3))       # singular value decomposition
Common functions:
tensor([[0.9238, 0.5724, 0.0791, 0.2629],
        [0.1986, 0.4439, 0.6434, 0.4776]])
tensor([[-0., -0., 1., -0.],
        [-0., 1., 1., -0.]])
tensor([[-1., -1.,  0., -1.],
        [-1.,  0.,  0., -1.]])
tensor([[-0.5000, -0.5000,  0.0791, -0.2629],
        [-0.1986,  0.4439,  0.5000, -0.4776]])

Sine and arcsine:
tensor([0.0000, 0.7854, 1.5708, 2.3562])
tensor([0.0000, 0.7071, 1.0000, 0.7071])
tensor([0.0000, 0.7854, 1.5708, 0.7854])

Bitwise XOR:
tensor([3, 2, 1])

Broadcasted, element-wise equality comparison:
tensor([[ True, False],
        [False, False]])

Reduction ops:
tensor(4.)
4.0
tensor(2.5000)
tensor(1.2910)
tensor(24.)
tensor([1, 2])

Vectors & Matrices:
tensor([ 0.,  0., -1.])
tensor([[0.7375, 0.8328],
        [0.8444, 0.2941]])
tensor([[2.2125, 2.4985],
        [2.5332, 0.8822]])
torch.return_types.linalg_svd(
U=tensor([[-0.7889, -0.6145],
        [-0.6145,  0.7889]]),
S=tensor([4.1498, 1.0548]),
Vh=tensor([[-0.7957, -0.6056],
        [ 0.6056, -0.7957]]))

這是一個小樣本操作。有關更多詳細資訊和完整的數學函式列表,請檢視 文件。有關更多詳細資訊和完整的線性代數運算列表,請檢視此 文件

原地修改張量#

大多數張量的二元運算會返回第三個新張量。當我們說 c = a * b(其中 ab 是張量)時,新張量 c 將佔據與其他張量不同的記憶體區域。

但是,有時您可能希望原地修改張量 - 例如,如果您正在進行一項元素級計算,可以丟棄中間值。為此,大多數數學函式都有一個帶有附加下劃線(_)的版本,該版本會原地修改張量。

例如

a = torch.tensor([0, math.pi / 4, math.pi / 2, 3 * math.pi / 4])
print('a:')
print(a)
print(torch.sin(a))   # this operation creates a new tensor in memory
print(a)              # a has not changed

b = torch.tensor([0, math.pi / 4, math.pi / 2, 3 * math.pi / 4])
print('\nb:')
print(b)
print(torch.sin_(b))  # note the underscore
print(b)              # b has changed
a:
tensor([0.0000, 0.7854, 1.5708, 2.3562])
tensor([0.0000, 0.7071, 1.0000, 0.7071])
tensor([0.0000, 0.7854, 1.5708, 2.3562])

b:
tensor([0.0000, 0.7854, 1.5708, 2.3562])
tensor([0.0000, 0.7071, 1.0000, 0.7071])
tensor([0.0000, 0.7071, 1.0000, 0.7071])

對於算術運算,有一些功能行為類似。

a = torch.ones(2, 2)
b = torch.rand(2, 2)

print('Before:')
print(a)
print(b)
print('\nAfter adding:')
print(a.add_(b))
print(a)
print(b)
print('\nAfter multiplying')
print(b.mul_(b))
print(b)
Before:
tensor([[1., 1.],
        [1., 1.]])
tensor([[0.3788, 0.4567],
        [0.0649, 0.6677]])

After adding:
tensor([[1.3788, 1.4567],
        [1.0649, 1.6677]])
tensor([[1.3788, 1.4567],
        [1.0649, 1.6677]])
tensor([[0.3788, 0.4567],
        [0.0649, 0.6677]])

After multiplying
tensor([[0.1435, 0.2086],
        [0.0042, 0.4459]])
tensor([[0.1435, 0.2086],
        [0.0042, 0.4459]])

請注意,這些原地算術函式是 torch.Tensor 物件上的方法,而不是像許多其他函式(例如 torch.sin())那樣附加到 torch 模組。從 a.add_(b) 可以看出,呼叫張量是被原地更改的那個。

還有另一種選擇可以將計算結果放置在現有的、已分配的張量中。我們到目前為止看到的許多方法和函式 - 包括建立方法!- 都有一個 out 引數,允許您指定一個張量來接收輸出。如果 out 張量的形狀和 dtype 正確,則可以在不進行新記憶體分配的情況下完成。

a = torch.rand(2, 2)
b = torch.rand(2, 2)
c = torch.zeros(2, 2)
old_id = id(c)

print(c)
d = torch.matmul(a, b, out=c)
print(c)                # contents of c have changed

assert c is d           # test c & d are same object, not just containing equal values
assert id(c) == old_id  # make sure that our new c is the same object as the old one

torch.rand(2, 2, out=c) # works for creation too!
print(c)                # c has changed again
assert id(c) == old_id  # still the same object!
tensor([[0., 0.],
        [0., 0.]])
tensor([[0.3653, 0.8699],
        [0.2364, 0.3604]])
tensor([[0.0776, 0.4004],
        [0.9877, 0.0352]])

複製張量#

與 Python 中的任何物件一樣,將張量分配給變數會使該變數成為張量的標籤,而不會複製它。例如:

a = torch.ones(2, 2)
b = a

a[0][1] = 561  # we change a...
print(b)       # ...and b is also altered
tensor([[  1., 561.],
        [  1.,   1.]])

但是,如果您想有一個單獨的資料副本進行操作怎麼辦?clone() 方法可供您使用。

a = torch.ones(2, 2)
b = a.clone()

assert b is not a      # different objects in memory...
print(torch.eq(a, b))  # ...but still with the same contents!

a[0][1] = 561          # a changes...
print(b)               # ...but b is still all ones
tensor([[True, True],
        [True, True]])
tensor([[1., 1.],
        [1., 1.]])

在使用 ``clone()`` 時有一個重要事項需要注意。 如果您的源張量啟用了 autograd,那麼克隆的張量也會啟用。這將在關於 autograd 的影片中更深入地討論, 但如果您想要輕量級的詳細資訊,請繼續。

在許多情況下,這將是您想要的。 例如,如果您的模型在其 forward() 方法中有多個計算路徑,並且原始張量及其克隆都貢獻於模型的輸出,那麼為了啟用模型學習,您希望為兩個張量啟用 autograd。如果您的源張量啟用了 autograd(如果它是學習權重集合或源自涉及權重的計算,通常會如此),那麼您將獲得想要的結果。

另一方面,如果您正在進行一項計算,其中原始張量和它的克隆都不需要跟蹤梯度,那麼只要源張量停用了 autograd,您就可以進行。

還有第三種情況: 想象一下您正在模型 forward() 函式中執行一項計算,其中預設情況下所有項的梯度都已開啟,但您想在過程中提取一些值來生成一些指標。在這種情況下,您希望源張量的克隆副本跟蹤梯度 - autograd 的歷史跟蹤關閉可以提高效能。為此,您可以使用源張量上的 .detach() 方法。

a = torch.rand(2, 2, requires_grad=True) # turn on autograd
print(a)

b = a.clone()
print(b)

c = a.detach().clone()
print(c)

print(a)
tensor([[0.0905, 0.4485],
        [0.8740, 0.2526]], requires_grad=True)
tensor([[0.0905, 0.4485],
        [0.8740, 0.2526]], grad_fn=<CloneBackward0>)
tensor([[0.0905, 0.4485],
        [0.8740, 0.2526]])
tensor([[0.0905, 0.4485],
        [0.8740, 0.2526]], requires_grad=True)

這裡發生了什麼?

  • 我們建立了 a,並將 requires_grad=True 開啟。我們還沒有介紹這個可選引數,但將在關於 autograd 的單元中介紹。

  • 當我們列印 a 時,它通知我們屬性 requires_grad=True - 這意味著 autograd 和計算歷史跟蹤已開啟。

  • 我們克隆了 a 並將其標記為 b。當我們列印 b 時,我們可以看到它正在跟蹤其計算歷史 - 它繼承了 a 的 autograd 設定,並新增到計算歷史記錄中。

  • 我們將 a 克隆到 c,但在此之前呼叫了 detach()

  • 列印 c 時,我們看不到計算歷史,也沒有 requires_grad=True

detach() 方法將張量與計算歷史分離。 它表示“像 autograd 關閉一樣繼續執行接下來的操作。” 它這樣做不會改變 a - 當我們在最後再次列印 a 時,您可以看到它保留了 requires_grad=True 屬性。

移動到加速器#

PyTorch 的主要優勢之一是其在 CUDA、MPS、MTIA 或 XPU 等加速器上的強大加速功能。到目前為止,我們所做的一切都是在 CPU 上完成的。如何移動到更快的硬體?

首先,我們應該使用 is_available() 方法檢查加速器是否可用。

注意

如果您沒有加速器,本節中的可執行單元將不會執行任何與加速器相關的程式碼。

if torch.accelerator.is_available():
    print('We have an accelerator!')
else:
    print('Sorry, CPU only.')
We have an accelerator!

一旦我們確定一個或多個加速器可用,我們就需要將資料放在加速器可以看到的地方。您的 CPU 在您計算機的 RAM 中對資料進行計算。您的加速器有專用的記憶體與之連線。每當您想在裝置上執行計算時,都必須將該計算所需的所有資料移動到該裝置可訪問的記憶體中。(俗稱,“將資料移動到 GPU 可訪問記憶體”簡稱為“將資料移動到 GPU”。)

有多種方法可以將資料載入到目標裝置。您可以在建立時執行此操作。

if torch.accelerator.is_available():
    gpu_rand = torch.rand(2, 2, device=torch.accelerator.current_accelerator())
    print(gpu_rand)
else:
    print('Sorry, CPU only.')
tensor([[0.3344, 0.2640],
        [0.2119, 0.0582]], device='cuda:0')

預設情況下,新張量在 CPU 上建立,因此當我們需要在加速器上建立張量時,必須使用可選的 device 引數進行指定。您可以從列印新張量時看到,PyTorch 會告知我們它在哪一個裝置上(如果它不在 CPU 上)。

您可以使用 torch.accelerator.device_count() 查詢加速器數量。如果您有多個加速器,則可以透過索引指定它們,以 CUDA 為例:device='cuda:0'device='cuda:1' 等。

作為編碼實踐,使用字串常量在所有地方指定我們的裝置非常脆弱。在理想情況下,無論是在 CPU 還是加速器硬體上,您的程式碼都能穩健執行。您可以透過建立一個裝置控制代碼來實現這一點,該控制代碼可以傳遞給您的張量而不是字串。

my_device = torch.accelerator.current_accelerator() if torch.accelerator.is_available() else torch.device('cpu')
print('Device: {}'.format(my_device))

x = torch.rand(2, 2, device=my_device)
print(x)
Device: cuda
tensor([[0.0024, 0.6778],
        [0.2441, 0.6812]], device='cuda:0')

如果您有一個現有張量位於某個裝置上,您可以使用 to() 方法將其移動到另一個裝置。下面一行程式碼在 CPU 上建立一個張量,並將其移動到您在上一個單元中獲取的任何裝置控制代碼。

y = torch.rand(2, 2)
y = y.to(my_device)

重要的是要知道,為了進行涉及兩個或多個張量的計算,所有張量必須位於同一裝置上。下面的程式碼將引發執行時錯誤,無論您是否有可用的加速器裝置,以 CUDA 為例:

x = torch.rand(2, 2)
y = torch.rand(2, 2, device='cuda')
z = x + y  # exception will be thrown

操作張量形狀#

有時,您需要更改張量的形狀。下面,我們將介紹一些常見情況以及如何處理它們。

更改維數#

您可能需要更改維數的一種情況是向模型傳遞單個輸入例項。PyTorch 模型通常期望輸入批次

例如,假設有一個模型處理 3 x 226 x 226 的影像 - 一個 226 畫素的正方形,帶有 3 個顏色通道。當您載入並轉換它時,您將得到一個形狀為 (3, 226, 226) 的張量。但是,您的模型期望輸入形狀為 (N, 3, 226, 226),其中 N 是批次中的影像數量。那麼如何建立一個包含一個的批次呢?

a = torch.rand(3, 226, 226)
b = a.unsqueeze(0)

print(a.shape)
print(b.shape)
torch.Size([3, 226, 226])
torch.Size([1, 3, 226, 226])

unsqueeze() 方法新增一個範圍為 1 的維度。 unsqueeze(0) 將其新增為新的零維 - 現在您有一個包含一個的批次!

那麼,如果這是壓縮(unsqueezing)?我們說的壓縮(squeezing)是什麼意思?我們利用了任何範圍為 1 的維度改變張量中元素數量這一事實。

c = torch.rand(1, 1, 1, 1, 1)
print(c)
tensor([[[[[0.2347]]]]])

繼續上面的示例,假設模型的輸出是每個輸入的 20 元素向量。那麼您期望輸出的形狀為 (N, 20),其中 N 是輸入批次中的例項數量。這意味著對於我們只有一個輸入的批次,我們將得到一個形狀為 (1, 20) 的輸出。

如果您想使用該輸出進行一些非批處理的計算 - 某種只需要 20 元素向量的東西?

a = torch.rand(1, 20)
print(a.shape)
print(a)

b = a.squeeze(0)
print(b.shape)
print(b)

c = torch.rand(2, 2)
print(c.shape)

d = c.squeeze(0)
print(d.shape)
torch.Size([1, 20])
tensor([[0.1899, 0.4067, 0.1519, 0.1506, 0.9585, 0.7756, 0.8973, 0.4929, 0.2367,
         0.8194, 0.4509, 0.2690, 0.8381, 0.8207, 0.6818, 0.5057, 0.9335, 0.9769,
         0.2792, 0.3277]])
torch.Size([20])
tensor([0.1899, 0.4067, 0.1519, 0.1506, 0.9585, 0.7756, 0.8973, 0.4929, 0.2367,
        0.8194, 0.4509, 0.2690, 0.8381, 0.8207, 0.6818, 0.5057, 0.9335, 0.9769,
        0.2792, 0.3277])
torch.Size([2, 2])
torch.Size([2, 2])

您可以從形狀中看出,我們的 2 維張量現在是 1 維的,如果您仔細檢視上面單元格的輸出,您會看到 a 的列印顯示了一個“額外的”方括號 [],這是由於有一個額外的維度。

您只能壓縮範圍為 1 的維度。請參見上面,我們嘗試壓縮 c 中大小為 2 的維度,但返回了與開始時相同的形狀。對 squeeze()unsqueeze() 的呼叫只能作用於範圍為 1 的維度,因為否則會改變張量中的元素數量。

您也可以使用 unsqueeze() 來簡化廣播。回想一下上面我們有以下程式碼的示例:

a = torch.ones(4, 3, 2)

c = a * torch.rand(   3, 1) # 3rd dim = 1, 2nd dim identical to a
print(c)

其最終效果是將運算廣播到維度 0 和 2,導致隨機的 3 x 1 張量與 a 中每個 3 元素列進行逐元素乘法。

如果隨機向量只是一個 3 元素向量呢?我們將失去進行廣播的能力,因為最後一個維度將不匹配廣播規則。 unsqueeze() 來救援。

a = torch.ones(4, 3, 2)
b = torch.rand(   3)     # trying to multiply a * b will give a runtime error
c = b.unsqueeze(1)       # change to a 2-dimensional tensor, adding new dim at the end
print(c.shape)
print(a * c)             # broadcasting works again!
torch.Size([3, 1])
tensor([[[0.1891, 0.1891],
         [0.3952, 0.3952],
         [0.9176, 0.9176]],

        [[0.1891, 0.1891],
         [0.3952, 0.3952],
         [0.9176, 0.9176]],

        [[0.1891, 0.1891],
         [0.3952, 0.3952],
         [0.9176, 0.9176]],

        [[0.1891, 0.1891],
         [0.3952, 0.3952],
         [0.9176, 0.9176]]])

squeeze()unsqueeze() 方法也有原地版本,squeeze_()unsqueeze_()

batch_me = torch.rand(3, 226, 226)
print(batch_me.shape)
batch_me.unsqueeze_(0)
print(batch_me.shape)
torch.Size([3, 226, 226])
torch.Size([1, 3, 226, 226])

有時您會想更徹底地改變張量的形狀,同時保留元素數量及其內容。這種情況發生在一個模型中的卷積層和線性層之間的介面處 - 在影像分類模型中很常見。卷積核會產生形狀為特徵 x 寬度 x 高度的輸出張量,但接下來的線性層需要一個 1 維輸入。 reshape() 會為您完成此操作,前提是您請求的維度產生的元素數量與輸入張量相同。

output3d = torch.rand(6, 20, 20)
print(output3d.shape)

input1d = output3d.reshape(6 * 20 * 20)
print(input1d.shape)

# can also call it as a method on the torch module:
print(torch.reshape(output3d, (6 * 20 * 20,)).shape)
torch.Size([6, 20, 20])
torch.Size([2400])
torch.Size([2400])

注意

單元格最後一行中的 (6 * 20 * 20,) 引數是因為 PyTorch 在指定張量形狀時期望一個**元組** - 但當形狀是方法的第一個引數時,它允許我們作弊,只使用一系列整數。在這裡,我們必須新增括號和逗號,以說服該方法這實際上是一個單元素元組。

reshape() 能夠做到時,它會返回一個對要更改的張量的檢視 - 即,一個檢視相同底層記憶體區域的獨立張量物件。這一點很重要: 這意味著對源張量的任何更改都將反映在該張量的檢視上,除非您對其進行 clone()

存在一些超出本入門範圍的條件,在這些條件下 reshape() 必須返回一個包含資料副本的張量。有關更多資訊,請參閱 文件

NumPy 橋接#

在上面關於廣播的部分,我們提到 PyTorch 的廣播語義與 NumPy 的相容 - 但 PyTorch 和 NumPy 之間的聯絡比這更深。

如果您有現有的 ML 或科學程式碼,其中資料儲存在 NumPy ndarrays 中,您可能希望將相同的資料表示為 PyTorch 張量,無論是為了利用 PyTorch 的 GPU 加速,還是它用於構建 ML 模型的高效抽象。在 ndarrays 和 PyTorch 張量之間進行切換很容易。

import numpy as np

numpy_array = np.ones((2, 3))
print(numpy_array)

pytorch_tensor = torch.from_numpy(numpy_array)
print(pytorch_tensor)
[[1. 1. 1.]
 [1. 1. 1.]]
tensor([[1., 1., 1.],
        [1., 1., 1.]], dtype=torch.float64)

PyTorch 建立一個與 NumPy 陣列形狀相同且包含相同資料的張量,甚至保留 NumPy 的預設 64 位浮點資料型別。

轉換也可以同樣輕鬆地反向進行。

pytorch_rand = torch.rand(2, 3)
print(pytorch_rand)

numpy_rand = pytorch_rand.numpy()
print(numpy_rand)
tensor([[0.8716, 0.2459, 0.3499],
        [0.2853, 0.9091, 0.5695]])
[[0.87163675 0.2458961  0.34993553]
 [0.2853077  0.90905803 0.5695162 ]]

重要的是要了解,這些轉換後的物件使用的是與其源物件相同的底層記憶體,這意味著對其中一個的更改會反映在另一箇中。

numpy_array[1, 1] = 23
print(pytorch_tensor)

pytorch_rand[1, 1] = 17
print(numpy_rand)
tensor([[ 1.,  1.,  1.],
        [ 1., 23.,  1.]], dtype=torch.float64)
[[ 0.87163675  0.2458961   0.34993553]
 [ 0.2853077  17.          0.5695162 ]]

指令碼總執行時間: (0 分鐘 0.443 秒)