開始深度學習框架PyTorch的學習。這個系列的學習筆記是完全面向新手的,因為我也是PyTorch和DL的新手。但是,如果你想要跟隨本系列的文章一同學習,那麼你至少需要:
- 掌握Python。這是基本的要求。筆者不會在這個系列的文章中過多地解釋Python和程式設計的基本概念。筆記面向PyTorch新手,但不是面向程式設計新手。
- 在你的電腦上已經安裝Conda(或Python)環境,並瞭解程式虛擬環境(Virtual Environment)的基本概念,懂得如何建立虛擬環境。
- 有一定的英文閱讀能力。在筆記中我偶爾會使用全英文或中英文混合來描述,因為我遵照的教學影片是英文的,這很容易對我產生影響。
- 不需要有相應的數學基礎,本筆記側重於學習框架應用,而不是其背後的數學原理。
此外,因為平時比較忙,且在學習中間可能會有其他的事情,因此對於框架的學習並不是連續的,並且這個過程可能會相當長。因此這不是一個速成的完整教程,更像是一個成長過程的記錄。
專案建立
官方推薦可以在Google Colab裡面創建一個Jupyter專案,但是我並不推薦這種做法。我更希望你能夠locally運行它,這樣對於之後大型專案的開發有重要意義。所以,請遵循PyTorch官方指導來在你的電腦上安裝PyTorch。記得,把它安裝到Virtual Env中可能是一個更好的主意。
我使用一台M3Pro晶片的MacBook Pro。首先,在PyCharm中建立一個Pure Python Project,你可以選擇自動生成一個Welcome main.py檔案,無關痛癢。
而後,我在啟用了venv的終端機中執行:
1 | pip3 install torch torchvision torchaudio |
如果你使用的是Windows或Linux電腦,搭載了NVIDIA GPU,請首先安裝Cuda Toolkit,然後follow官網的steps去安裝適用於CPU或NVIDIA GPU的PyTorch。
Using GPUs to run your project
如果你恰好有NVIDIA GPU inside your computer,或者你租用了高密度計算伺服器,你可以切換到GPU去提升你的運算速度。
驗證GPU可用性:
1 | torch.cuda.is_available() |
然後設定運算硬體:
1 | device = 'cuda' if torch.cuda.is_available() else 'cpu' |
有時候我們需要把tensor移回CPU,比如GPU的tensor無法用於NumPy計算。Let’s move it back to cpu.
1 | tensor_on_gpu.cpu() |
Tensor
So what is a tensor? Tensor是PyTorch計算的基本單位,就像R Lang的基本計算單位是Vector一樣。(相信我,你很快就會再見到它的)
在數學中,tensor代表一個代數對象,描述了與向量空間相關的代數對象集之間的多重線性映射。在這裡,它的概念基本相同。
Tensor可以有很多種類型,比如Scalar,比如Vector,比如Matrix。下面這張圖片形象展示了tensor家族:
我們不妨實作一下:
1 | ''' |
Random tensors
Random tensor會按照要求生成一個隨機的tensor。這在提供廢資料方面非常有用。
1 | ## Create a random tensor of size (3, 4) |
要創建Reproducible random tensors,我們需要使用random seed。
1 | RANDOM_SEED = 42 |
Zeros and ones
1 | ## Create a tensor full of zeros |
A range of tensors and tensors-like
range可以產生一個遞增的數列tensor:
1 | tensor_range = torch.arange(1, 13) |
如果我們想按照某個tensor的shape創建一個full of zeros或full of ones的tensor,我們可以:
1 | random_tensor = torch.rand(3, 4) |
Data Types of tensor
Tensor中的資料是有特定的資料類型的。透過dtype
參數控制。
具體的data types可以到這裡來查找。
關於data types,torch.tensor()
有三個引數和它有關:dtype
、device
和requires_grad
。
dtype
:tensor中引數的類型。device
:你的tensor運行在哪個裝置上。可以以cpu的方式運行,也可以運行在cuda硬體上。requires_grad
:是否透過此tensor梯度追蹤參數。
轉換data types:
1 | float_16_tensor = float_32_tensor.type(torch.float16) |
Tensor operations
Tensor operations包括加減乘除和矩陣乘法。
1 | ## Attention: If we want to do some simple operations for two tensors, the shapes of them must be the same, or one of them should be a scalar. |
Matrix multiplication
筆者作為醫學生,沒有修習過線性代數,所以也是第一次接觸到矩陣乘法。那麼我們首先來一點數學基礎。
黨一個matrix乘一個scalar,當然很簡單,只需要把矩陣中的每一個元素乘這個scalar就可以得到結果:
比較困難的是兩個矩陣的乘法。兩個矩陣的乘法要求:第一個矩陣的欄數必須等於第二個矩陣的列數。
對中國大陸朋友的解釋:台灣和大陸的行列剛好是相反的。在台灣,我們習慣將column稱為“欄”或“行”,把row稱為“列”。
兩個矩陣的乘法:
Dot product的運算規則如下:
然後繼續產生第二個數字:
第三個、第四個。最後的結果是:
我們可以看到,一個
接下來我們使用tensor實作它:
1 | tensor_1 = torch.tensor([[1, 2, 3], |
我們還可以用transpose對矩陣進行欄列轉換:
1 | tensor_A = torch.tensor([[1, 2, 3], |
Tensor aggregation
Tensor aggregation幫助我們尋找tensor中的min、max,計算mean,sum等等。
1 | tensor = torch.arange(1, 10, dtype=torch.long) |
Reshaping
We can reshape a tensor to other shapes if numbers of the new tensor’s elements equals to the old one.
torch.reshape()
and torch.view()
method can both change the shape of a tensor.
1 | tensor = torch.arange(1, 36, dtype=torch.long, step=2) |
OK, now let’s change the old tensor and see what will happen:
1 | tensor[0] = 5 |
Oh, my god! all of the three tensors have been changed! So, why? Let’s get a view on the storage mechanism of tensor.
當我們創建了一個tensor,事實上會佔用我們記憶體中的兩塊空間——是的,一個變數兩塊空間。第一塊空間稱為頭資料區,儲存這個tensor的諸如size
、stride
等基本資料,第二塊空間則是儲存tensor的真實資料。
而這時候,如果我們將tensor A進行reshape或view,再使用=
交給tensor B,事實上A和B僅有頭資料區不同,而真實資料區是共享的。
好,我們接下來理解torch.view()
和torch.reshape()
到底有什麼區別。要理解之,必須先理解兩個properties:stride
(步長)和storage_offset
(偏移量)。
首先我們來解釋storage_offset
,它表示了“從原tensor誕生的新tensor首個元素在記憶體中的位置相對於原tensor首個元素在記憶體中的位置之偏移量”。我們直接來看一個例子:
1 | tensor_rand = torch.rand(3, 3) |
比較好理解。
接下來我們再來看stride
,它表示的是“在指定dimension下,從一個element跳到下一個element所需要的步長”。我們繼續來看例子:
1 | tensor = torch.rand(2, 3, 4) |
我們來解釋一下這個(12, 4, 1)是如何得出的:
- 12是指從第一層跳到第二層,需要跨越12個元素。比如在上面的例子中,第一層是:
1 | [[0.5037, 0.0922, 0.4752, 0.8638], |
第二層是:
1 | [[0.8771, 0.1222, 0.8956, 0.6742], |
其中有12個元素。
- 4是指同一層中,從第一列跳到第二列需要跨越4個元素。比如上面的例子第一層的第一列是:
1 | [0.5037, 0.0922, 0.4752, 0.8638] |
第二列是:
1 | [0.8847, 0.9163, 0.7370, 0.8694] |
可知需要跨越4個元素。
1表示在同一層的同一列中,從第一個元素跳到第二個元素需要跨越1個元素。比如上面的例子中,第一層,第一列的第一個元素是:
1 | 0.5037 |
第一列的第二個元素是:
1 | 0.8847 |
需要跨越1個元素。終此,釋畢。
接下來是另一個概念——連續性。
何為連續性?通俗來講,就是tensor在記憶體的真實資料區中,某個元素的下一個元素應該是tensor本身中該元素的下一個元素。啊好吧,也不怎麼通俗。我們舉例說明。
假如我們創建了一個tensor:
1 | tensor = torch.rand(2, 3) |
這時候,這個tensor在記憶體中的儲存空間是這樣的:
Index Value [0][0] 0.1753 [0][1] 0.6297 [0][2] 0.7549 [0][3] 0.3359 [0][4] 0.2473 [0][5] 0.4115
我們可以看到,記憶體中的Location index和Value是一一對應的。元素0.1753
在tensor中的下一個應該是0.6297
,元素0.6297
在記憶體中的位置也剛好是緊鄰0.1753
的位置。這就叫做連續。
那什麼情況下會產生不連續呢?比如我們透過transpose改變了tensor,這個時候改變後的tensor仍然和原tensor共享真實資料區,也就是說,真實資料區中元素的位置沒有發生任何改變,還是上表所示的樣子。但是新tensor和舊tensor卻不相同。
1 | tensor_t = tensor.t() |
在記憶體中,0.1753
的下一個位置的元素是0.6297
,而在tensor中卻變成了0.3359
,這樣子,tensor中的元素和記憶體儲存位置無法對應,這叫做不連續。
那麼torch.view()
和torch.reshape()
到底有什麼區別?理解了上面的內容,就很容易解釋了:torch.view()
只適用於連續性tensor,而torch.reshape()
同時適用於連續性和非連續性tensor。
Stack
torch.stack()
函式用於將現有的tensor堆疊起來,形成一個新的tensor。它的基本用法如下:
1 | torch.stack(tensors, dim=0) |
tensors
引數接受一個tensor tuple的輸入,表示用於堆疊的tensors,dim
表示堆疊在第幾個維度發生。我們舉例說明:
1 | tensor1 = torch.tensor([[1, 2, 3, 4], |
要實現堆疊,必須滿足如下條件:
- 原tensor的shape應該完全相同。
- dim不超過原tensor的dimension。
Squeeze and unsqueeze
torch.squeeze()
method is used to remove all extra single dimensions from a tensor.
1 | tensor2 = torch.tensor([[[1, 2, 3, 4, 5]]]) |
torch.unsqueeze()
will add a single dimension for a existing tensor.
1 | tensor3 = torch.tensor([1, 2, 3, 4, 5]) |
We can see that value of the dim
para can’t be bigger than the old dimension.
Permute
torch.permute()
函式可以根據specific order改變現有tensor的axis或dimension。
1 | tensor = torch.rand(size = (224, 224, 3)) |
Indexing
基本情況是,我們可以使用tensor[i][j][k]
或者tensor[i, j, k]
這樣的形式來拿到一個tensor裡面的任何東西。
需要注意的一點是,我們可以透過:
來表示specific dimension中的所有元素。比如:
1 | tensor = torch.tensor([[1, 2, 3, 4], |
With NumPy
PyTorch提供了和NumPy交流的通道。我們可以透過torch.from_numpy()
來從NumPy的ndarray創建tensor。
1 | import torch |
需要注意的是,ndarray中的資料預設為float64,因此轉換到tensor,預設的類型是torch.float64
而不是tensor預設的torch.float32
。
當然,你也可以使用dtype引數來設定它。
使用tensor.numpy()
將tensor轉換成ndarray:
1 | tensor = torch.ones(7) |