【基礎篇】PyTorch入門指南

本文是對 Neural Network Programming - Deep Learning with PyTorch 系列博客的翻譯與整理,英語基礎比較好的同學推薦閱讀原汁原味的博客。

PyTorch是一個深度學習框架和一個科學計算包,這是PyTorch核心團隊對PyTorch的描述,PyTorch的科學計算方面主要是PyTorch張量(tensor)庫和相關張量運算的結果。A tensor is an n-dimensional array (ndarray)。PyTorch的torch.Tensor對象就是由Numpy的ndarray對象創建來的,兩者之間的轉換十分高效。PyTorch中內置了對GPU的支持,如果我們在系統上安裝了GPU,那麼使用PyTorch將張量在GPU之間來回移動是非常容易的一件事情。

1. PyTorch簡介

  PyTorch的首次發佈是在2016年10月,在PyTorch創建之前,還有一個叫做Torch(火炬)的框架。Torch是一個已經存在了很長時間的機器學習框架,它基於Lua編程語言。PyTorch和這個Lua版本(稱爲Torch)之間的聯繫是存在的,因爲許多維護Lua版本的開發人員也參與了PyTorch的開發工作。你可能聽說過PyTorch是由Facebook創建和維護的,這是因爲PyTorch在創建時,Soumith Chintala(創始人)在Facebook AI Research工作。

  下表列出了PyTorch包及其相應的說明。這些是我們在本系列中構建神經網絡時將學習和使用的主要PyTorch組件。

Package Description
torch 頂層的PyTorch包和tensor庫
torch.nn 一個子包,用於構建神經網絡的模塊和可擴展類
torch.autograd 一個子包,支持PyTorch中所有微分張量運算
torch.nn.functional 一種函數接口,包含構建神經網絡的操作,如損失函數、激活函數和卷積操作。
torch.optim 一個子包,包含標準優化操作,如SGD和Adam
torch.utils 一個子包,包含數據集和數據加載器等實用工具類,使數據預處理更容易
torchvision 提供對流行數據集、模型體系結構和圖像轉換的訪問的包

  爲了優化神經網絡,我們需要計算導數,爲了進行計算,深度學習框架使用所謂的計算圖(computational graphs),計算圖用於描述神經網絡內部張量上發生的函數運算操作。

  PyTorch使用一個稱爲動態計算圖的計算圖,這意味着計算圖是在創建操作時動態生成的,這與在實際操作發生之前就已完全確定的靜態圖形成對比。正因爲如此,許多深度學習領域的前沿研究課題都需要動態圖,或者從動態圖中獲益良多。

2. GPU相關介紹

  GPU是一種擅長處理特定計算(specialized computations)的處理器。這與中央處理器(CPU)形成對比,中央處理器是一種善於處理一般計算(general computations)的處理器。CPU是在我們的電子設備上支持大多數典型計算的處理器。

  GPU的計算速度可能比CPU快得多。 然而,這並非總是如此。 GPU相對於CPU的速度取決於所執行的計算類型。最適合GPU的計算類型是可以並行完成的計算。

  並行計算(paraller computing)是一種將特定計算分解成可以同時進行的獨立的較小計算的計算方式,然後重新組合或同步計算結果,以形成原來較大計算的結果。

  一個較大的任務可以分解成的任務數量取決於特定硬件上包含的內核數量。核心是在給定處理器中實際執行計算的單元,CPU通常有4個、8個或16個核心,而GPU可能有數千個。

  有了這些工作知識,我們可以得出結論,並行計算是使用GPU完成的,我們還可以得出結論,最適合使用GPU解決的任務是可以並行完成的任務。如果計算可以並行完成,我們可以使用並行編程方法和GPU加速計算。

  現在我們把目光轉移到神經網絡上,看看爲什麼GPU在深度學習中被大量使用。 我們剛剛看到GPU非常適合並行計算,而關於GPU的事實就是深度學習使用GPU的原因。

   Neural networks are embarrassingly parallel.指的是一個任務分解爲幾個子任務之後,在不同處理器上執行該子任務,而這些子任務之間不會相互依賴,也就說明該任務十分適合於並行計算,也被稱爲embarrassingly parallel.

  我們用神經網絡所做的許多計算可以很容易地分解成更小的計算,這樣一組更小的計算就不會相互依賴了。卷積操作就是這樣一個例子。

  • 藍色區域(底部): Input channel
  • 陰影區域(底部): Filter
  • 綠色區域(頂部): Output channel

  對於藍色輸入通道上的每個位置,3 x 3過濾器都會進行計算,將藍色輸入通道的陰影部分映射到綠色輸出通道的相應陰影部分。在動畫中,這些計算一個接一個地依次進行。但是,每個計算都是獨立於其他計算的,這意味着任何計算都不依賴於任何其他計算的結果。因此,所有這些獨立的計算都可以在GPU上並行進行,從而產生整個輸出通道,加速我們的卷積過程。

3. CUDA相關介紹

  Nvidia是一家設計GPU的技術公司,他們創建了CUDA作爲一個軟件平臺,與GPU硬件適配,使開發人員更容易使用Nvidia GPU的並行處理能力來構建加速計算的軟件。Nvidia GPU是支持並行計算的硬件,而CUDA是爲開發人員提供API的軟件層。

  開發人員通過下載CUDA工具包來使用CUDA,伴隨工具包一起的是專門的庫,如 cuDNN, CUDA Deep Neural Network library.

  在PyTorch中利用CUDA非常簡單。如果我們希望在GPU上執行特定的計算,我們可以通過在數據結構(tensors)上調用cuda()來指示PyTorch這樣做。

  假設我們有以下代碼:

> t = torch.tensor([1,2,3])
> t
tensor([1, 2, 3])

  默認情況下,以這種方式創建的tensor對象在CPU上。因此,我們使用這個張量對象所做的任何操作都將在CPU上執行。

  現在,要把張量移到GPU上,我們只需要寫:

> t = t.cuda()
> t
tensor([1, 2, 3], device='cuda:0')

  由於可以在CPU或GPU上有選擇地進行計算,因此PyTorch的用途非常廣泛。

  GPU是不是總是比CPU更好呢?答案是否定的。

  GPU只對特定的(專門的)任務更快。它也會遇到某些瓶頸,例如,將數據從CPU移動到GPU的成本很高(耗時),因此在這種情況下,如果計算任務本身就很簡單,還把它轉移到GPU上進行計算,那麼總體性能可能會降低。

  把一些相對較小的計算任務轉移到GPU上不會使我們的速度大大加快,而且可能確實會減慢我們的速度。GPU對於可以分解爲許多較小任務的任務非常有效,如果計算任務已經很小,那麼將任務移到GPU上就不會有太多收穫。

4. 張量定義

  神經網絡中的輸入、輸出和變換都是用tensor表示的,在神經網絡編程中大量使用了tensor

  張量的概念是其他更具體概念的數學概括,讓我們看看張量的一些具體實例:

  • number
  • scalar
  • array
  • vector
  • 2d-array
  • matrix

  我們來把上面的張量實例分成兩組:

  • number, array, 2d-array
  • scalar, vector, matrix

  第一組中的三個術語(數字、數組、二維數組)是計算機科學中常用的術語,而第二組(標量、矢量、矩陣)是數學中常用的術語。

  我們經常看到這種情況,不同的研究領域對同一概念使用不同的詞。在深度學習中,我們通常把這些都稱爲tensor

Indexes requried Computer science Mathematics
0 number scala
1 array vector
2 2d-array matrix
n nd-array nd-tensor

5. 張量的秩、軸和形狀

  在深度學習中,秩、軸和形狀是我們最關心的tensor屬性,這些概念建立在一個又一個的基礎上,從秩開始,然後是軸,再到形狀,請注意這三者之間的關係。

  我們在這裏引入rank這個詞,是因爲它在深度學習中經常被用到,它指的是給定張量中的維數,一個張量的秩告訴我們需要多少索引來引用張量中的某一個特定元素。

  如果我們有一個張量,想要表示某一個特定的維度,那麼在深度學習中使用軸(Axis)這個詞。

  每個軸的長度告訴我們每個軸上有多少索引可用,假設我們有一個張量 t,我們知道第一個軸的長度爲3,而第二個軸的長度爲4。

  我們可以索引第一個軸的每一個元素像這樣:

t[0]
t[1]
t[2]

  由於第二軸的長度爲4,所以我們可以沿着第二軸標出4個位置。這對於第一軸的每個索引都是成立的,所以我們有:

t[0][0]
t[1][0]
t[2][0]

t[0][1]
t[1][1]
t[2][1]

t[0][2]
t[1][2]
t[2][2]

t[0][3]
t[1][3]
t[2][3]

  張量的形狀是由每個軸的長度決定的,所以如果我們知道給定張量的形狀,那麼我們知道每個軸的長度,這告訴我們每個軸有多少個索引可用。

  我們結合一個實例來看看形狀(shape)是如何計算的。

> a = torch.tensor([[[[1]],[[2]],[[3]]]])
> print(a)
tensor([[[[1]],

         [[2]],

         [[3]]]])

  如何來計算它的形狀呢,首先我們數一下中括號的個數,得知這是一個四維張量,然後由外而內,去掉最外層的中括號之後,得到 tensor([[[1]], [[2]], [[3]]]),此時只有一個最外層的中括號,那我們的shape變爲 (1,  ,  ,)(1, \;,\; ,);同理,我們繼續去除最外層的中括號,得到 tensor([[1]], [[2]], [[3]]),此時有三個最外層的中括號,那我們的shape則變爲 (1,3,  ,)(1, 3,\; ,),因爲每一個維度的形狀是相同的,於是我們繼續來只需要看其中一個維度即可,即 tensor([[1]]);去除最外層的中括號,得到 tensor([1]),此時只有一個最外層的中括號,shape變爲 (1,3,1,)(1, 3,1 ,);最後再去除一箇中括號,得到 tensor(1),shape變爲 (1,3,1,1)(1, 3,1 ,1),以上就是得到張量形狀的全部過程。

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

6. CNN中的張量

  CNN輸入的形狀,通常有4個維度,也就是說我們有一個秩爲4的四階張量,張量中的每一個索引對應着一個軸,每一個軸都代表着輸入數據的某種實際特徵,我們從右到左,來理解CNN輸入的張量中,每個維度的含義。

  原始圖像數據以像素的形式出現,用數字表示,並使用高度和寬度兩個維度進行佈局,所以我們需要 width 和 height 兩個軸。

  下一個軸表示圖像的顏色通道數,灰度圖的通道數爲1,RGB圖的通道數爲3,這種顏色通道的解釋僅適用於輸入張量,後續 feature map 中的通道都不是代表顏色。

  也就是說,張量中的最後三個軸,表示着一個完整的圖像數據。在神經網絡中,我們通常處理成批的樣本,而不是單個樣本,所以最左邊的軸的長度,告訴我們一批中有多少個樣本。

  假設給定張量的形狀爲 [3,1,28,28] ,那麼我們可以確定,一個批次中有三幅圖像,每張圖像的顏色通道數爲3,寬和高都是28。 [Batch, Channels, Height, Width]

  我們接下來看看張量被卷積層變換後,顏色通道軸的解釋是如何變化的。

  假設我們有一個 tensor 的形狀爲 [1,1,28,28],當它經過一個卷積層之後,張量的寬和高,以及通道數量都會發生改變,輸出的通道數即對應着卷積層中卷積核的個數。

  輸出的通道不再解釋爲顏色通道,而是 feature map 的修改通道(modified channels),使用 feature 一詞是因爲卷積層的輸出,代表圖像中的特定特徵,例如邊緣,這些映射隨着網絡在訓練過程中的學習而出現,並且隨着我們深入網絡而變得更加複雜。

7. torch.Tensor類

  我們可以用下面的方式,構建一個 torch.Tensor 類的實例:

> t = torch.Tensor()
> type(t)
torch.Tensor

  每一個 torch.Tensor 對象都有3個屬性:

> print(t.dtype)
> print(t.device)
> print(t.layout)
torch.float32
cpu
torch.strided

  我們來詳細看一下dtype有哪些屬性:

Data type dtype CPU tensor GPU tensor
32-bit floating point torch.float32 torch.FloatTensor torch.cuda.FloatTensor
64-bit floating point torch.float64 torch.DoubleTensor torch.cuda.DoubleTensor
16-bit floating point torch.float16 torch.HalfTensor torch.cuda.HalfTensor
8-bit integer (unsigned) torch.uint8 torch.ByteTensor torch.cuda.ByteTensor
8-bit integer (signed) torch.int8 torch.CharTensor torch.cuda.CharTensor
16-bit integer (signed) torch.int16 torch.ShortTensor torch.cuda.ShortTensor
32-bit integer (signed) torch.int32 torch.IntTensor torch.cuda.IntTensor
64-bit integer (signed) torch.int64 torch.LongTensor torch.cuda.LongTensor

  注意,每種類型有一個CPU和GPU版本。關於張量數據類型,需要記住的一點是,張量之間的張量運算必鬚髮生在具有相同類型數據的張量之間

  device用來表示張量的數據是存儲在 CPU 上還是 GPU 上。它決定來張量計算的位置在哪裏。我們可以用 索引 的方式來指定設備的編號:

> device = torch.device('cuda:0')
> device
device(type='cuda', index=0)

  使用多個設備時,需要記住的一點是張量之間的張量運算必鬚髮生在同一設備上的張量之間

  layout屬性用來指定張量在內存中是如何存儲的。

  在 PyTorch 中,可以通過以下四種方式將一個 array-like 的對象轉成 torch.Torch 的對象。

torch.Tensor(data)
torch.tensor(data)
torch.as_tensor(data)
torch.from_numpy(data)

  我們可以直接創建一個 Python list 類型的 data,不過 numpy.ndarray 是一個更加常見的選擇,如下所示:

> data = np.array([1,2,3])
> type(data)
numpy.ndarray

  然後再用上面的四種方式來創建一個 torch.Torch 的對象:

> o1 = torch.Tensor(data)
> o2 = torch.tensor(data)
> o3 = torch.as_tensor(data)
> o4 = torch.from_numpy(data)

> print(o1)
> print(o2)
> print(o3)
> print(o4)
tensor([1., 2., 3.])
tensor([1, 2, 3], dtype=torch.int32)
tensor([1, 2, 3], dtype=torch.int32)
tensor([1, 2, 3], dtype=torch.int32)

  除了第一個之外,其他的輸出(o2、o3、o4)似乎都產生了相同的張量,第一個輸出(o1)在數字後面有圓點,表示數字是浮點數,而後面三個選項的類型是int32。

> type(2.)
float
> type(2)
int

  當然,PyTorch 也內置了一些不需要通過數據轉換,直接構成張量的方式。

> print(torch.eye(2))
tensor([
    [1., 0.],
    [0., 1.]
])

> print(torch.zeros([2,2]))
tensor([
    [0., 0.],
    [0., 0.]
])

> print(torch.ones([2,2]))
tensor([
    [1., 1.],
    [1., 1.]
])

> print(torch.rand([2,2]))
tensor([
    [0.0465, 0.4557],
    [0.6596, 0.0941]
])

8. 創建Tensor的方法對比

  先來看看 torch.tensor()torch.Tensor() 這兩種方法的區別:

> data = np.array([1,2,3])
> type(data)
numpy.ndarray

> o1 = torch.Tensor(data)
> o2 = torch.tensor(data)

> print(o1)
> print(o2)

tensor([1., 2., 3.])
tensor([1, 2, 3], dtype=torch.int32)

  torch.Tensor() 是 torch.Tensor 類的構造函數,torch.tensor()是工廠函數,它構造 torch.Tensor 對象並將它們返回給調用方,這是一種創建對象的軟件設計模式,另一個區別就是前者默認的數據類型是浮點型,而後者是整型。數據類型可以顯示地指定,不指定的話,可以通過傳入的數據類型來推斷。四種構造方法中只有 torch.Tensor() 函數不可以顯示指定dtype

> torch.tensor(data, dtype=torch.float32)
> torch.as_tensor(data, dtype=torch.float32)

  我們再來看看幾種方法創建 tensor 時,對傳入的 data 採取的是拷貝還是共享的方式。

> print('old:', data)
old: [1 2 3]

> data[0] = 0

> print('new:', data)
new: [0 2 3]

> print(o1)
> print(o2)
> print(o3)
> print(o4)

tensor([1., 2., 3.])
tensor([1, 2, 3], dtype=torch.int32)
tensor([0, 2, 3], dtype=torch.int32)
tensor([0, 2, 3], dtype=torch.int32)

  可以發現,torch.Tensor()torch.tensor() 這兩個函數都是對輸入數據進行了拷貝,而 torch.as_tensor()torch.from_numpy() 這兩個函數則是對輸入數據進行了共享的方式。與複製數據相比,共享數據效率更高,佔用的內存更少,因爲數據不會寫入內存中的兩個位置。

  如果我們想把 torch.Tensor 對象轉成 ndarray 類型的話,採取下面這種方式:

> print(type(o3.numpy()))
> print(type(o4.numpy()))
<class 'numpy.ndarray'>
<class 'numpy.ndarray'>

  不過這個方法只適用於 torch.as_tensor()torch.from_numpy() 這兩種方式創建的 torch.Tensor 對象,我們再來進一步比較一下這兩個方法的區別。

  torch.from_numpy() 函數只接受 numpy.ndarray 類型的輸入,而torch.as_tensor() 函數可以接受各種類似Python數組的對象,包括其他PyTorch張量。

  綜上所述,我們更推薦 torch.tensor()torch.as_tensor() 這兩個函數,前者是一種直接調用的方式,後者是在需要調參的時候採用的方式。

  關於內存共享的機制,還有一些需要提的點:

  • 由於numpy.ndarray對象是在CPU上分配的,因此當使用GPU時,torch.as_tensor() 函數必須將數據從CPU複製到GPU。
  • torch.as_tensor() 的內存共享不適用於內置的Python數據結構,如列表。
  • torch.as_tensor() 的調用要求開發人員瞭解共享特性。這是必要的,這樣我們就不會在沒有意識到變更會影響多個對象的情況下無意中對底層數據進行不必要的更改。
  • 當 numpy.ndarray 對象和張量對象之間有許多來回操作時,torch.as_tensor() 性能的優越性會更大。

9. tensor的reshape、squeeze和cat操作

  假設我們現在有一個秩爲 2 、形狀爲 3 * 4 的張量:

> t = torch.tensor([
    [1,1,1,1],
    [2,2,2,2],
    [3,3,3,3]
], dtype=torch.float32)

  在 PyTorch 中,我們有兩種獲取張量形狀的方法:

> t.size()
torch.Size([3, 4])

> t.shape
torch.Size([3, 4])

  我們還可以採用下面的方式來獲取張量的元素數:

> torch.tensor(t.shape).prod()
tensor(12)

> t.numel()
12

  於是我們可以進行 reshape 操作:

> t.reshape([1,12])
tensor([[1., 1., 1., 1., 2., 2., 2., 2., 3., 3., 3., 3.]])

> t.reshape([2,6])
tensor([[1., 1., 1., 1., 2., 2.],
        [2., 2., 3., 3., 3., 3.]])

> t.reshape([3,4])
tensor([[1., 1., 1., 1.],
        [2., 2., 2., 2.],
        [3., 3., 3., 3.]])
        
> t.reshape(6,2)
tensor([[1., 1.],
        [1., 1.],
        [2., 2.],
        [2., 2.],
        [3., 3.],
        [3., 3.]])
        
> t.reshape(12,1)
tensor([[1.],
        [1.],
        [1.],
        [1.],
        [2.],
        [2.],
        [2.],
        [2.],
        [3.],
        [3.],
        [3.],
        [3.]])

  我們傳入的參數有 ([2, 6])(2, 6) 兩種方式,這個都是可行的,但是我們要保證 傳入參數的乘積和原始張量的元素個數要相等。

> t.reshape(2,2,3)
tensor(
[
    [
        [1., 1., 1.],
        [1., 2., 2.]
    ],

    [
        [2., 2., 3.],
        [3., 3., 3.]
    ]
])

   在 PyTorch 中,還有一個 view() 函數,功能和 reshape() 函數是一樣的,都可以改變張量的形狀。

> t.view(3,4)
tensor([[1., 1., 1., 1.],
        [2., 2., 2., 2.],
        [3., 3., 3., 3.]])
        
> t.view([2,6])
tensor([[1., 1., 1., 1., 2., 2.],
        [2., 2., 3., 3., 3., 3.]])

  我們還可以通過 squeeze()unsqueeze() 這兩個函數來改變輸入張量的形狀。

  • 壓縮(squeeze)一個張量可以去掉長度爲1的維度。

  • 解壓縮(unsqueeze)一個張量可以增加一個長度爲1的維度。

  從實際的例子來理解:

> a = torch.tensor([[[1]],[[2]]])
> print(a)
tensor([[[1]],

        [[2]]])
        
> print(a.squeeze())
tensor([1, 2])

> print(a.squeeze().unsqueeze(dim=0))
tensor([[1, 2]])

> print(a.squeeze().unsqueeze(dim=1))
tensor([[1],
        [2]])

  squeeze()函數還有一個廣泛的用途就是作爲flatten()函數的一個子函數:

def flatten(t):
    t = t.reshape(1, -1)
    t = t.squeeze()
    return t
> a = torch.tensor([[[[1]],[[2]],[[3]]]])
> print(a)
tensor([[[[1]],

         [[2]],

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

> print(a.reshape(1, -1)) 
tensor([[1, 2, 3]])  # torch.Size([1, 3])

> print(a.reshape(1, -1).squeeze()) # 可以看到實現了 flatten 的函數功能
tensor([1, 2, 3])

> print(a.reshape(1, -1).squeeze().shape)
torch.Size([3])

  PyTorch 還提供了一個 cat() 函數,來實現張量的拼接(concatenate)。假設我們有如下兩個張量:

> t1 = torch.tensor([
    [1,2],
    [3,4]
])
> t2 = torch.tensor([
    [5,6],
    [7,8]
])

  我們可以將它們按行(axis=0)拼接:

> torch.cat((t1, t2), dim=0)
tensor([[1, 2],
        [3, 4],
        [5, 6],
        [7, 8]])

  也可以將它們按列(axis=1)拼接:

torch.cat((t1, t2), dim=1)
tensor([[1, 2, 5, 6],
        [3, 4, 7, 8]])

  拼接之後的shape,可以通過所選拼接維度的值進行求和得到:

> torch.cat((t1, t2), dim=0).shape
torch.Size([4, 2])

> torch.cat((t1, t2), dim=1).shape
torch.Size([2, 4])

10. flatten操作

  我們知道,CNN中的全連接層接收的是一個一維向量輸入,而前面卷積層的輸出都是 feature map 的形式,於是在全連接層之前,一定存在一個 flatten 操作,我們來看看,如何 flatten 張量中的某一個軸。

  首先創建三個張量,代表一個Batch中的三張圖像:

> a = torch.ones(4,4)
> print(a)
tensor([[1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.],
        [1., 1., 1., 1.]])


> b = torch.full((4,4),2)  # 此處的full()和numpy中的full()的作用類似
> print(b)
tensor([[2., 2., 2., 2.],
        [2., 2., 2., 2.],
        [2., 2., 2., 2.],
        [2., 2., 2., 2.]])

> c = torch.full((4,4),3)
> print(c)
tensor([[3., 3., 3., 3.],
        [3., 3., 3., 3.],
        [3., 3., 3., 3.],
        [3., 3., 3., 3.]])

  CNN 中的 Batch 也是由單個張量表示的,於是我們需要將這三張圖像連接起來,這裏要用到 stack() 函數,後面會細講。

> t = torch.stack((a,b,c))
> t
tensor([[[1., 1., 1., 1.],
         [1., 1., 1., 1.],
         [1., 1., 1., 1.],
         [1., 1., 1., 1.]],

        [[2., 2., 2., 2.],
         [2., 2., 2., 2.],
         [2., 2., 2., 2.],
         [2., 2., 2., 2.]],

        [[3., 3., 3., 3.],
         [3., 3., 3., 3.],
         [3., 3., 3., 3.],
         [3., 3., 3., 3.]]])
> t.shape
torch.Size([3, 4, 4])

  我們這裏默認的圖像是灰度圖,即通道數爲1,於是我們可以通過 reshapeunsqueeze 這兩種方式,進一步修改我們的 tensor.

> t.reshape(3,1,4,4)
tensor([[[[1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.]]],
          
        [[[2., 2., 2., 2.],
          [2., 2., 2., 2.],
          [2., 2., 2., 2.],
          [2., 2., 2., 2.]]],

        [[[3., 3., 3., 3.],
          [3., 3., 3., 3.],
          [3., 3., 3., 3.],
          [3., 3., 3., 3.]]]])

> t.unsqueeze(dim=1)
tensor([[[[1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.]]],

        [[[2., 2., 2., 2.],
          [2., 2., 2., 2.],
          [2., 2., 2., 2.],
          [2., 2., 2., 2.]]],

        [[[3., 3., 3., 3.],
          [3., 3., 3., 3.],
          [3., 3., 3., 3.],
          [3., 3., 3., 3.]]]])

  現在我們得到了一個四維張量,我們需要 flatten 裏面的每一個圖像張量,而不是整個張量,不妨先看看有哪些 flatten 整個張量的方法。

> t.reshape(1,-1)[0]
tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 2., 2.,
        2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 3., 3., 3., 3.,
        3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.])
> t.reshape(-1)
tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 2., 2.,
        2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 3., 3., 3., 3.,
        3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.])
> t.view(t.numel())
tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 2., 2.,
        2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 3., 3., 3., 3.,
        3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.])
> t.flatten()
tensor([1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 2., 2.,
        2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 3., 3., 3., 3.,
        3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.])

  在 CNN 中我們需要對同一個Batch中的每一張圖片進行預測,因此上面對整個 tensor 進行 flatten 的做法是不可取的,我們需要對 tensor 中的(channels, width, height) 這三個維度進行展開,而 flatten() 函數中提供了對指定維度展開的方法。

>> print(t)
tensor([[[[1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.],
          [1., 1., 1., 1.]]],

        [[[2., 2., 2., 2.],
          [2., 2., 2., 2.],
          [2., 2., 2., 2.],
          [2., 2., 2., 2.]]],

        [[[3., 3., 3., 3.],
          [3., 3., 3., 3.],
          [3., 3., 3., 3.],
          [3., 3., 3., 3.]]]])
          
> t.flatten(start_dim=1)
tensor([[1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.],
        [2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.],
        [3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.]])

> t.flatten(start_dim=2)
tensor([[[1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]],

        [[2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2., 2.]],

        [[3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3., 3.]]])

  注意,這裏的 start_dim 參數,用來指定從哪一個軸開始進行 flatten 操作,默認是0。我們原本張量 t 的shape爲(3,1,4,4)(3,1,4,4),如果 start_dim=1,那麼我們最後得到的shape爲(3,16)(3,16);如果start_dim=2,那麼我們最後得到的shape爲(3,1,16)(3,1,16)

11. element-wise操作

  顧名思義,element-wise 類型操作表示兩個張量中對應元素之間的操作,這要求兩個張量的形狀一致,假設我們有如下兩個張量:

> t1 = torch.tensor([
    [1,2],
    [3,4]], dtype=torch.float32)

> t2 = torch.tensor([
    [9,8],
    [7,6]], dtype=torch.float32)

   加減乘除 都屬於 element-wise 類型的操作:

> t1 + t2
tensor([[10., 10.],
        [10., 10.]])
        
> t1 - t2
tensor([[-8., -6.],
        [-4., -2.]])

> t1 * t2
tensor([[ 9., 16.],
        [21., 24.]])
        
> t1 / t2
tensor([[0.1111, 0.2500],
        [0.4286, 0.6667]])

   除此之外,張量與某個數值之間的加減乘除也是屬於 element-wise 類型的操作:

> print(t + 2)
tensor([[3., 4.],
        [5., 6.]])

> print(t - 2)
tensor([[-1.,  0.],
        [ 1.,  2.]])

> print(t * 2)
tensor([[2., 4.],
        [6., 8.]])

> print(t / 2)
tensor([[0.5000, 1.0000],
        [1.5000, 2.0000]])

> print(t1.add(2))
tensor([[3., 4.],
        [5., 6.]])

> print(t1.sub(2))
tensor([[-1.,  0.],
        [ 1.,  2.]])

> print(t1.mul(2))
tensor([[2., 4.],
        [6., 8.]])

> print(t1.div(2))
tensor([[0.5000, 1.0000],
        [1.5000, 2.0000]])   

  這個看起來跟我們剛剛定義的 element-wise 運算有點衝突,我們剛討論的是兩個張量之間的運算,這裏明明是一個標量和一個張量的運算。爲什麼也被視作是 element-wise 的運算呢?我們需要了解 PyTorch 中的 broadcast 機制。

  以 t1 + 2 爲例,我們首先將標量 2 變換成 t1 的形狀,然後再執行 element-wise 的操作,有點類似於 numpy 中的 broadcast_to() 函數。

> np.broadcast_to(2, t1.shape)
array([[2, 2],
        [2, 2]])

  所以,對於 t1 + 2 這個運算,實際上的過程爲:

> t1 + torch.tensor(
    np.broadcast_to(2, t1.shape)
    ,dtype=torch.float32
)
tensor([[3., 4.],
        [5., 6.]])

  broadcast 機制還可以進一步推廣,低秩張量和高秩張量之間也可以進行 element-wise 的運算。

> t1 = torch.tensor([
    [1, 1],
    [1, 1]
], dtype=torch.float32)

> t2 = torch.tensor([2, 4], dtype=torch.float32)

> np.broadcast_to(t2.numpy(), t1.shape)
array([[2., 4.],
       [2., 4.]], dtype=float32)

> t1 + t2
tensor([[3., 5.],
        [3., 5.]])

   低秩張量的每一個軸的元素個數要麼和對應高秩張量軸的元素個數相等,要麼元素數爲1,才能順利進行 broadcast 操作。

> t3 = torch.ones(3,3,2) # 高秩張量形狀爲(3,3,2)
> t3
tensor([[[1., 1.],
         [1., 1.],
         [1., 1.]],

        [[1., 1.],
         [1., 1.],
         [1., 1.]],

        [[1., 1.],
         [1., 1.],
         [1., 1.]]])

> t4 = torch.tensor([2,4],dtype=torch.float32) # 此時低秩張量形狀爲(2),滿足條件
> t3 + t4
tensor([[[3., 5.],
         [3., 5.],
         [3., 5.]],

        [[3., 5.],
         [3., 5.],
         [3., 5.]],

        [[3., 5.],
         [3., 5.],
         [3., 5.]]])

> t5 = torch.tensor([[2, 4]],dtype=torch.float32) # 此時張量形狀爲(1,2),滿足條件
> t3 + t5
tensor([[[3., 5.],
         [3., 5.],
         [3., 5.]],

        [[3., 5.],
         [3., 5.],
         [3., 5.]],
         
        [[3., 5.],
         [3., 5.],
         [3., 5.]]])

> t6 = torch.tensor([[2, 4],[1, 3]],dtype=torch.float32) # 張量形狀爲(2,2),不符條件
> t3 + t6 # 輸出報錯
RuntimeError: The size of tensor a (3) must match the size of tensor b (2) at non-singleton dimension 1

> t7 = torch.tensor([[2, 4],[1, 3],[0, 5]],dtype=torch.float32) # 張量形狀爲(3,2),滿足條件
> t3 + t7
tensor([[[3., 5.],
         [2., 4.],
         [1., 6.]],

        [[3., 5.],
         [2., 4.],
         [1., 6.]],

        [[3., 5.],
         [2., 4.],
         [1., 6.]]])

> t8 = torch.tensor([[[2, 4],[1, 3],[0, 5]]],dtype=torch.float32) # 張量形狀爲(1,3,2),滿足條件
> t3 + t8
tensor([[[3., 5.],
         [2., 4.],
         [1., 6.]],

        [[3., 5.],
         [2., 4.],
         [1., 6.]],

        [[3., 5.],
         [2., 4.],
         [1., 6.]]])

  比較大小的操作也是 element-wise類型的,一個張量與某一個數值進行比較,返回的是一個和原始張量形狀相同、取值爲bool類型的張量。

> t = torch.tensor([[0,5,0],[6,0,7],[0,8,0]], dtype=torch.float32)

> t.eq(0)
tensor([[ True, False,  True],
        [False,  True, False],
        [ True, False,  True]])

> t.eq(0).dtype
torch.bool

> t.ge(0)
tensor([[True, True, True],
        [True, True, True],
        [True, True, True]])

> t.gt(0)
tensor([[False,  True, False],
        [ True, False,  True],
        [False,  True, False]])

> t.le(0)
tensor([[ True, False,  True],
        [False,  True, False],
        [ True, False,  True]])

> t.lt(0)
tensor([[False, False, False],
        [False, False, False],
        [False, False, False]])

  PyTorch中內置的 element-wise 類型的函數:

> t = torch.tensor([[1,-2,3],[-4,5,-6],[7,-8,9]],dtype=torch.float32)
> t
tensor([[ 1., -2.,  3.],
        [-4.,  5., -6.],
        [ 7., -8.,  9.]])
        
> t.abs()
tensor([[1., 2., 3.],
        [4., 5., 6.],
        [7., 8., 9.]])
        
> t.abs().sqrt()
tensor([[1.0000, 1.4142, 1.7321],
        [2.0000, 2.2361, 2.4495],
        [2.6458, 2.8284, 3.0000]])

> t.neg()
tensor([[-1.,  2., -3.],
        [ 4., -5.,  6.],
        [-7.,  8., -9.]])

  最後說一下,以下詞語名稱不同,但是都代表着 element-wise 類型的操作:

  • element-wise
  • component-wise
  • point-wise

12. reduction操作

  reduction 操作指的是減少某個張量中元素個數的操作,我們前面提過的 reshapeelement-wise 操作都不會改變張量中的元素個數,我們通過示例來看:

t = torch.tensor([[0,1,0],[2,0,2],[0,3,0]],dtype=torch.float32)
>>> t
tensor([[0., 1., 0.],
        [2., 0., 2.],
        [0., 3., 0.]])

> t.sum() # 輸出張量元素個數爲1
tensor(8.)

> t.numel() # num of element的縮寫
9
>>> type(t.numel())
<class 'int'>

> t.prod()
tensor(0.)

> t.mean()
tensor(0.8889)

> t.std()
tensor(1.1667)

  並不是只把元素個數縮減爲1的操作,叫做 reduction ops,下面的操作也是:

> t = torch.tensor([[1,1,1,1],[2,2,2,2],[3,3,3,3]],dtype=torch.float32)
> t
tensor([[1., 1., 1., 1.],
        [2., 2., 2., 2.],
        [3., 3., 3., 3.]])
> t.shape
torch.Size([3, 4])

> t.sum(dim=0)
tensor([6., 6., 6., 6.])

> t.sum(dim=1)
tensor([ 4.,  8., 12.])

  如何理解 dim=0dim=1 所得的兩種不同的結果呢?

> t.sum(dim=0) 
> 可以理解爲其他軸的索引保持不變,只改變第一個軸的元素索引值,如下所示
t[0][0] + t[1][0] + t[2][0] 
t[0][1] + t[1][1] + t[2][1] 
t[0][2] + t[1][2] + t[2][2]
t[0][3] + t[1][3] + t[2][3]

> t.sum(dim=1) 
> 可以理解爲其他軸的索引保持不變,只改變第二個軸的元素索引值,如下所示
t[0][0] + t[0][1] + t[0][2] + t[0][3]
t[1][0] + t[1][1] + t[1][2] + t[1][3]
t[2][0] + t[2][1] + t[2][2] + t[2][3]

  另外一種常見的 reduction 操作是 argmax(),作用是返回張量中最大元素值的索引,我們舉例說明:

> t = torch.tensor([
    [1,0,0,2],
    [0,3,3,0],
    [4,0,0,5]], dtype=torch.float32)

> t.max()
tensor(5.)

> t.argmax()
tensor(11)

> t.flatten()
tensor([1., 0., 0., 2., 0., 3., 3., 0., 4., 0., 0., 5.])

  我們還可以指定軸,返回軸上每個張量的最值。

> t.max(dim=0)
torch.return_types.max(values=tensor([4., 3., 3., 5.]),indices=tensor([2, 1, 1, 2]))

> t.argmax(dim=0)
tensor([2, 1, 1, 2])

> t.max(dim=1)
torch.return_types.max(values=tensor([2., 3., 5.]),indices=tensor([3, 2, 3]))

> t.argmax(dim=1)
tensor([3, 2, 3])

  這裏 dim=0dim=1的作用和上面討論的 sum() 是一致的,不再贅述。

  最後我們再來聊聊如何訪問一個張量中的內部元素:

> t = torch.tensor([
    [1,2,3],
    [4,5,6],
    [7,8,9]], dtype=torch.float32)

> t.mean()
tensor(5.)

> t.mean().item() # item() 適用於標量
5.0

> t.mean(dim=0).tolist() # 返回 Python list 類型
[4.0, 5.0, 6.0]

> t.mean(dim=0).numpy() # 返回 ndarray 類型
array([4., 5., 6.], dtype=float32)

  到現在爲止,我們已經對 PyTorch 的張量有一個基礎的認識,我們現在對張量的使用還僅處於一個非常原始的階段,在一個篇章,我們將結合 Fashion Mnist 數據集來進一步挖掘 PyTorch 的強大之處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章