注意
點擊這裡下載完整的範例程式碼
簡介 || 張量 || Autograd || 建構模型 || TensorBoard 支援 || 訓練模型 || 模型理解
PyTorch 張量簡介¶
建立於:2021 年 11 月 30 日 | 最後更新:2025 年 1 月 29 日 | 最後驗證: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([[ 3.6678e+14, 7.0065e-45, 1.4013e-45, 0.0000e+00],
[-2.3594e+33, 3.0662e-41, 0.0000e+00, 0.0000e+00],
[ 1.6356e+27, 3.0663e-41, -4.0164e+37, 4.5852e-41]])
讓我們解開我們剛才所做的事情
我們使用附加到
torch
模組的眾多工廠方法之一建立了張量。張量本身是二維的,具有 3 列和 4 行。
傳回的物件類型為
torch.Tensor
,它是torch.FloatTensor
的別名;預設情況下,PyTorch 張量會填充 32 位元浮點數。(有關資料類型的更多資訊如下。)列印張量時,您可能會看到一些看起來很隨機的值。
torch.empty()
呼叫會為張量分配記憶體,但不會使用任何值初始化它 - 所以您看到的是分配時記憶體中的任何內容。
關於張量及其維度數量的簡要說明,以及術語
有時您會看到一個稱為向量的一維張量。
同樣地,二維張量通常稱為矩陣。
任何具有兩個以上維度的東西通常只稱為張量。
通常,您會想要使用某些值初始化您的張量。常見的情況是全為零、全為一或隨機值,並且 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()
呼叫?使用隨機值初始化張量(例如模型的學習權重)很常見,但在某些時候(尤其是在研究環境中),您會想要確保結果的可重現性。手動設定隨機數產生器的種子是執行此操作的方式。讓我們仔細看看
torch.manual_seed(1729)
random1 = torch.rand(2, 3)
print(random1)
random2 = torch.rand(2, 3)
print(random2)
torch.manual_seed(1729)
random3 = torch.rand(2, 3)
print(random3)
random4 = torch.rand(2, 3)
print(random4)
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]])
您應該在上面看到的是 random1
和 random3
攜帶相同的值,random2
和 random4
也是如此。手動設定 RNG 的種子會重設它,因此在大多數情況下,取決於隨機數的相同計算應該提供相同的結果。
如需更多資訊,請參閱 PyTorch 關於可重現性的文件。
張量形狀¶
通常,當您對兩個或多個張量執行運算時,它們需要具有相同的形狀 - 也就是說,具有相同數量的維度,並且每個維度中具有相同數量的儲存格。為此,我們有 torch.*_like()
方法
x = torch.empty(2, 2, 3)
print(x.shape)
print(x)
empty_like_x = torch.empty_like(x)
print(empty_like_x.shape)
print(empty_like_x)
zeros_like_x = torch.zeros_like(x)
print(zeros_like_x.shape)
print(zeros_like_x)
ones_like_x = torch.ones_like(x)
print(ones_like_x.shape)
print(ones_like_x)
rand_like_x = torch.rand_like(x)
print(rand_like_x.shape)
print(rand_like_x)
torch.Size([2, 2, 3])
tensor([[[ 8.5991e+21, 3.0663e-41, 1.6365e+27],
[ 3.0663e-41, -2.3594e+33, 3.0662e-41]],
[[ 0.0000e+00, 0.0000e+00, 9.6631e-07],
[ 3.0663e-41, -4.0164e+37, 4.5852e-41]]])
torch.Size([2, 2, 3])
tensor([[[1.1145e-29, 4.5853e-41, 4.8662e-06],
[3.0663e-41, 4.4842e-44, 0.0000e+00]],
[[1.5695e-43, 0.0000e+00, 3.4321e-03],
[3.0670e-41, 0.0000e+00, 1.4013e-45]]])
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
是一個三維張量,形狀為 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]])
如果您已經有 Python tuple 或 list 中的資料,則使用 torch.tensor()
是建立張量最直接的方式。如上所示,巢狀集合將產生多維張量。
注意
torch.tensor()
會建立資料的副本。
Tensor 資料型別¶
設定 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)
設定 tensor 的底層資料型別最簡單的方法是在建立時使用一個可選參數。在上方程式碼區塊的第一行,我們為 tensor a
設定了 dtype=torch.int16
。當我們印出 a
時,可以看到它充滿了 1
而不是 1.
- Python 用這個細微的提示來表明這是一個整數型別,而不是浮點數型別。
關於印出 a
,另一件需要注意的是,與我們將 dtype
保留為預設值(32 位元浮點數)時不同,印出 tensor 時也會指定它的 dtype
。
您可能也注意到,我們從將 tensor 的形狀指定為一系列整數參數,到將這些參數分組在一個元組中。這並非絕對必要 - PyTorch 會將一系列初始的、未標記的整數參數作為 tensor 的形狀 - 但在添加可選參數時,它可以使您的意圖更具可讀性。
另一種設定資料型別的方法是使用 .to()
方法。在上方程式碼區塊中,我們以通常的方式建立一個隨機浮點數 tensor b
。之後,我們透過使用 .to()
方法將 b
轉換為 32 位元整數來建立 c
。請注意,c
包含與 b
相同的所有值,但截斷為整數。
更多資訊請參閱資料型別文件。
使用 PyTorch Tensor 進行數學運算與邏輯運算¶
現在您已經知道建立 tensor 的一些方法了...您可以用它們做什麼?
讓我們首先看看基本算術,以及 tensor 如何與簡單的純量互動
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]])
如您在上面看到的,tensor 和純量之間的算術運算(例如加法、減法、乘法、除法和指數運算)會分散到 tensor 的每個元素上。由於這種運算的輸出將是一個 tensor,因此您可以按照通常的運算符優先順序規則將它們鏈接在一起,就像我們建立 threes
的那一行一樣。
兩個 tensor 之間的類似運算也表現得如您直觀期望的那樣
tensor([[ 2., 4.],
[ 8., 16.]])
tensor([[5., 5.],
[5., 5.]])
tensor([[12., 12.],
[12., 12.]])
這裡需要注意的是,前一個程式碼區塊中的所有 tensor 都具有相同的形狀。如果我們嘗試對形狀不同的 tensor 執行二元運算,會發生什麼情況?
注意
以下程式碼區塊會拋出執行階段錯誤。這是故意的。
a = torch.rand(2, 3)
b = torch.rand(3, 2)
print(a * b)
在一般情況下,您不能以這種方式對不同形狀的 tensor 進行運算,即使在上面的程式碼區塊中,tensor 具有相同數量的元素也是如此。
簡述:Tensor 廣播¶
注意
如果您熟悉 NumPy ndarray 中的廣播語義,您會發現相同的規則適用於此處。
相同形狀規則的例外是tensor 廣播。這是一個例子
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 tensor 乘以 1x4 tensor 的?
廣播是一種在形狀具有相似性的 tensor 之間執行運算的方法。在上面的例子中,單行四列的 tensor 乘以二行四列 tensor 的兩行。
這是深度學習中的一個重要運算。常見的例子是將學習權重的 tensor 乘以一批輸入 tensor,將運算單獨應用於批次中的每個實例,並返回一個形狀相同的 tensor - 就像我們上面的 (2, 4) * (1, 4) 例子返回一個形狀為 (2, 4) 的 tensor 一樣。
廣播的規則是
每個 tensor 必須至少有一個維度 - 沒有空 tensor。
比較兩個 tensor 的維度大小,從最後一個開始:
每個維度必須相等,或者
其中一個維度的大小必須為 1,或者
該維度在其中一個 tensor 中不存在
當然,形狀相同的 tensor 可以被簡單地 “廣播”,正如您之前看到的。
以下是一些符合上述規則並允許廣播的情況示例
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]]])
仔細觀察上面每個 tensor 的值
建立
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
更多關於 Tensor 的數學運算¶
PyTorch tensor 具有超過三百個可以對其執行的運算。
以下是一些主要類別運算的小樣本
# 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]]))
這只是一個小樣本運算。有關更多詳細資訊和數學函數的完整列表,請查看文件。有關更多詳細資訊和線性代數運算的完整列表,請查看此文件。
原地修改 Tensor¶
大多數對 tensor 進行的二元運算將返回第三個新 tensor。當我們說 c = a * b
(其中 a
和 b
是 tensor)時,新的 tensor c
將佔用與其他 tensor 不同的記憶體區域。
但是,有時您可能希望原地修改 tensor - 例如,如果您正在進行逐元素計算,並且可以丟棄中間值。為此,大多數數學函數都有一個附加底線 (_
) 的版本,它將原地修改 tensor。
例如
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])
對於算術運算,有類似行為的函數
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 中的任何物件一樣,將張量賦值給變數會使該變數成為張量的標籤,而不會複製它。 例如
tensor([[ 1., 561.],
[ 1., 1.]])
但是,如果您想要一個單獨的資料副本來處理呢? clone()
方法是為您準備的
tensor([[True, True],
[True, True]])
tensor([[1., 1.],
[1., 1.]])
使用``clone()``時,需要注意一件重要的事情。 如果您的來源張量啟用了 autograd,則複製的張量也會啟用。 這將在 autograd 影片中更深入地介紹, 但如果您想要簡要版本,請繼續。
在許多情況下,這會是您想要的。 例如,如果您的模型在其 forward()
方法中有多個運算路徑,並且原始張量及其複製的張量都對模型的輸出有所貢獻,那麼為了啟用模型學習,您需要為這兩個張量都開啟 autograd。 如果您的來源張量已啟用 autograd(如果它是一組學習權重或源自涉及權重的運算,則通常會啟用),那麼您將獲得您想要的結果。
另一方面,如果您正在進行的運算中,原始張量及其複製的張量都不需要追蹤梯度,那麼只要來源張量已關閉 autograd,您就可以正常使用。
但還有第三種情況: 假設您正在模型的 forward()
函數中執行運算,預設情況下所有項目的梯度都已開啟,但您想要從中途提取一些值來產生一些指標。 在這種情況下,您不希望來源張量的複製副本追蹤梯度 - 關閉 autograd 的歷史記錄追蹤可以提高效能。 為此,您可以在來源張量上使用 .detach()
方法
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)
這裡發生了什麼?
我們使用開啟的
requires_grad=True
建立a
。 我們尚未介紹這個可選參數,但會在 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 的圖片,也就是一個具有 3 個顏色通道的 226 像素正方形。當您載入並轉換它時,會得到一個形狀為 (3, 226, 226)
的張量。但是,您的模型期望輸入的形狀為 (N, 3, 226, 226)
,其中 N
是批次中的圖片數量。那麼,您該如何建立一個大小為 1 的批次呢?
torch.Size([3, 226, 226])
torch.Size([1, 3, 226, 226])
unsqueeze()
方法會新增一個長度為 1 的維度。unsqueeze(0)
會將其作為新的第零個維度新增 - 現在您就有了一個大小為 1 的批次!
如果那是unsqueezing(擴展)?那麼 squeezing(壓縮)又是什麼意思?我們利用了任何長度為 1 的維度不會改變張量中元素數量的事實。
c = torch.rand(1, 1, 1, 1, 1)
print(c)
tensor([[[[[0.2347]]]]])
繼續上面的例子,假設模型的輸出是每個輸入的 20 個元素的向量。那麼,您會期望輸出的形狀為 (N, 20)
,其中 N
是輸入批次中的實例數量。這意味著對於我們單個輸入的批次,我們會得到一個形狀為 (1, 20)
的輸出。
如果您想要使用該輸出執行一些非批次運算 - 也就是說,只是想要一個 20 個元素的向量呢?
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])
您可以從形狀中看到,我們的二維張量現在變成了一維,如果您仔細觀察上面儲存格的輸出,您會發現列印 a
會顯示一組“額外”的方括號 []
,這是因為有一個額外的維度。
您只能 squeeze()
長度為 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()
方法也有原地 (in-place) 版本: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])
有時候您需要更徹底地更改張量的形狀,同時保持元素的數量及其內容不變。這種情況的一個例子是在模型的卷積層和模型的線性層之間的介面 - 這在圖片分類模型中很常見。卷積核將產生一個形狀為 *features x width x height* 的輸出張量,但後面的線性層期望一個一維輸入。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()
將會傳回要更改的張量的檢視 (view) - 也就是說,一個單獨的張量物件,指向相同的底層記憶體區域。這很重要:這意味著對來源張量所做的任何變更都會反映在該張量的檢視中,除非您 clone()
它。
在超出本簡介範圍的條件下,reshape()
必須傳回一個攜帶資料副本的張量。 更多資訊,請參閱文件。
NumPy 橋接¶
在上面關於廣播的章節中,提到了 PyTorch 的廣播語義與 NumPy 的相容 - 但 PyTorch 和 NumPy 之間的關係比這更深入。
如果您有現有的 ML 或科學程式碼,其資料儲存在 NumPy ndarray 中,您可能希望將相同的資料表示為 PyTorch 張量,無論是為了利用 PyTorch 的 GPU 加速,還是其用於建構 ML 模型的高效抽象。在 ndarray 和 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.315 秒)