2.1 環境配置
本節簡單介紹一些必要的軟件的安裝與配置,由于不同機器軟硬件配置不同,所以不詳述,遇到問題請善用Google。
2.1.1 Anaconda
Anaconda是Python的一個開源發行版本,主要面向科學計算。我們可以簡單理解為,Anaconda是一個預裝了很多我們用的到或用不到的第三方庫的Python。而且相比于大家熟悉的pip install命令,Anaconda中增加了conda install命令。當你熟悉了Anaconda以后會發現,conda install會比pip install更方便一些。 強烈建議先去看看最省心的Python版本和第三方庫管理——初探Anaconda和初學 Python 者自學 Anaconda 的正確姿勢-猴子的回答。
總的來說,我們應該完成以下幾步:
- 根據操作系統下載并安裝Anaconda(或者mini版本Miniconda)并學會常用的幾個conda命令,例如如何管理python環境、如何安裝卸載包等;
- Anaconda安裝成功之后,我們需要修改其包管理鏡像為國內源,這樣以后安裝包時就會快一些。
2.1.2 Jupyter
在沒有notebook之前,在IT領域是這樣工作的:在普通的 Python shell 或者在IDE(集成開發環境)如Pycharm中寫代碼,然后在word中寫文檔來說明你的項目。這個過程很繁瑣,通常是寫完代碼,再寫文檔的時候我還的重頭回顧一遍代碼。最蛋疼的地方在于,有些數據分析的中間結果,還得重新跑代碼,然后把結果弄到文檔里給客戶看。有了notebook之后,世界突然美好了許多,因為notebook可以直接在代碼旁寫出敘述性文檔,而不是另外編寫單獨的文檔。也就是它可以能將代碼、文檔等這一切集中到一處,讓用戶一目了然。如下圖所示。
Jupyter Notebook 已迅速成為數據分析,機器學習的必備工具。因為它可以讓數據分析師集中精力向用戶解釋整個分析過程。
我們參考jupyter notebook-猴子的回答進行jupyter notebook及常用包(例如環境自動關聯包nb_conda)的安裝。
安裝好后,我們使用以下命令打開一個jupyter notebook:
jupyter notebook
這時在瀏覽器打開 http://localhost:8888 (通常會自動打開)位于當前目錄的jupyter服務。
2.1.3 PyTorch
由于本文需要用到PyTorch框架,所以還需要安裝PyTorch(后期必不可少地會使用GPU,所以安裝GPU版本的)。直接去PyTorch官網找到自己的軟硬件對應的安裝命令即可(這里不得不吹一下PyTorch的官方文檔,從安裝到入門,深入淺出,比tensorflow不知道高到哪里去了)。安裝好后使用以下命令可查看安裝的PyTorch及版本號。
conda list | grep torch
2.1.4 其他
此外還可以安裝python最好用的IDE PyCharm,專業版的應該是需要收費的,但學生用戶可以申請免費使用(傳送門),或者直接用免費的社區版。
如果不喜歡用IDE也可以選擇編輯器,例如VSCode等。
本節與原文有很大不同,原文傳送門
2.2 數據操作
在深度學習中,我們通常會頻繁地對數據進行操作。作為動手學深度學習的基礎,本節將介紹如何對內存中的數據進行操作。
在PyTorch中,torch.Tensor
是存儲和變換數據的主要工具。如果你之前用過NumPy,你會發現Tensor
和NumPy的多維數組非常類似。然而,Tensor
提供GPU計算和自動求梯度等更多功能,這些使Tensor
更加適合深度學習。
"tensor"這個單詞一般可譯作“張量”,張量可以看作是一個多維數組。標量可以看作是0維張量,向量可以看作1維張量,矩陣可以看作是二維張量。
2.2.1 創建Tensor
我們先介紹Tensor
的最基本功能,即Tensor
的創建。
首先導入PyTorch:
import torch
然后我們創建一個5x3的未初始化的Tensor
:
x = torch.empty(5, 3)
print(x)
輸出:
tensor([[ 0.0000e+00, 1.5846e+29, 0.0000e+00],
[ 1.5846e+29, 5.6052e-45, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 1.5846e+29, -2.4336e+02]])
創建一個5x3的隨機初始化的Tensor
:
x = torch.rand(5, 3)
print(x)
輸出:
tensor([[0.4963, 0.7682, 0.0885],
[0.1320, 0.3074, 0.6341],
[0.4901, 0.8964, 0.4556],
[0.6323, 0.3489, 0.4017],
[0.0223, 0.1689, 0.2939]])
創建一個5x3的long型全0的Tensor
:
x = torch.zeros(5, 3, dtype=torch.long)
print(x)
輸出:
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
還可以直接根據數據創建:
x = torch.tensor([5.5, 3])
print(x)
輸出:
tensor([5.5000, 3.0000])
還可以通過現有的Tensor
來創建,此方法會默認重用輸入Tensor
的一些屬性,例如數據類型,除非自定義數據類型。
x = x.new_ones(5, 3, dtype=torch.float64) # 返回的tensor默認具有相同的torch.dtype和torch.device
print(x)
x = torch.randn_like(x, dtype=torch.float) # 指定新的數據類型
print(x)
輸出:
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
tensor([[ 0.6035, 0.8110, -0.0451],
[ 0.8797, 1.0482, -0.0445],
[-0.7229, 2.8663, -0.5655],
[ 0.1604, -0.0254, 1.0739],
[ 2.2628, -0.9175, -0.2251]])
我們可以通過shape
或者size()
來獲取Tensor
的形狀:
print(x.size())
print(x.shape)
輸出:
torch.Size([5, 3])
torch.Size([5, 3])
注意:返回的torch.Size其實就是一個tuple, 支持所有tuple的操作。
還有很多函數可以創建Tensor
,去翻翻官方API就知道了,下表給了一些常用的作參考。
函數 | 功能 |
---|---|
Tensor(*sizes) | 基礎構造函數 |
tensor(data,) | 類似np.array的構造函數 |
ones(*sizes) | 全1Tensor |
zeros(*sizes) | 全0Tensor |
eye(*sizes) | 對角線為1,其他為0 |
arange(s,e,step) | 從s到e,步長為step |
linspace(s,e,steps) | 從s到e,均勻切分成steps份 |
rand/randn(*sizes) | 均勻/標準分布 |
normal(mean,std)/uniform(from,to) | 正態分布/均勻分布 |
randperm(m) | 隨機排列 |
這些創建方法都可以在創建的時候指定數據類型dtype和存放device(cpu/gpu)。
2.2.2 操作
本小節介紹Tensor
的各種操作。
算術操作
在PyTorch中,同一種操作可能有很多種形式,下面用加法作為例子。
-
加法形式一
y = torch.rand(5, 3) print(x + y)
-
加法形式二
print(torch.add(x, y))
還可指定輸出:
result = torch.empty(5, 3) torch.add(x, y, out=result) print(result)
-
加法形式三、inplace
# adds x to y y.add_(x) print(y)
注:PyTorch操作inplace版本都有后綴
_
, 例如x.copy_(y), x.t_()
以上幾種形式的輸出均為:
tensor([[ 1.3967, 1.0892, 0.4369],
[ 1.6995, 2.0453, 0.6539],
[-0.1553, 3.7016, -0.3599],
[ 0.7536, 0.0870, 1.2274],
[ 2.5046, -0.1913, 0.4760]])
索引
我們還可以使用類似NumPy的索引操作來訪問Tensor
的一部分,需要注意的是:索引出來的結果與原數據共享內存,也即修改一個,另一個會跟著修改。
y = x[0, :]
y += 1
print(y)
print(x[0, :]) # 源tensor也被改了
輸出:
tensor([1.6035, 1.8110, 0.9549])
tensor([1.6035, 1.8110, 0.9549])
除了常用的索引選擇數據之外,PyTorch還提供了一些高級的選擇函數:
函數 | 功能 |
---|---|
index_select(input, dim, index) | 在指定維度dim上選取,比如選取某些行、某些列 |
masked_select(input, mask) | 例子如上,a[a>0],使用ByteTensor進行選取 |
nonzero(input) | 非0元素的下標 |
gather(input, dim, index) | 根據index,在dim維度上選取數據,輸出的size與index一樣 |
這里不詳細介紹,用到了再查官方文檔。
改變形狀
用view()
來改變Tensor
的形狀:
y = x.view(15)
z = x.view(-1, 5) # -1所指的維度可以根據其他維度的值推出來
print(x.size(), y.size(), z.size())
輸出:
torch.Size([5, 3]) torch.Size([15]) torch.Size([3, 5])
注意view()
返回的新Tensor
與源Tensor
雖然可能有不同的size
,但是是共享data
的,也即更改其中的一個,另外一個也會跟著改變。(顧名思義,view僅僅是改變了對這個張量的觀察角度,內部數據并未改變)
x += 1
print(x)
print(y) # 也加了1
輸出:
tensor([[1.6035, 1.8110, 0.9549],
[1.8797, 2.0482, 0.9555],
[0.2771, 3.8663, 0.4345],
[1.1604, 0.9746, 2.0739],
[3.2628, 0.0825, 0.7749]])
tensor([1.6035, 1.8110, 0.9549, 1.8797, 2.0482, 0.9555, 0.2771, 3.8663, 0.4345,
1.1604, 0.9746, 2.0739, 3.2628, 0.0825, 0.7749])
所以如果我們想返回一個真正新的副本(即不共享data內存)該怎么辦呢?Pytorch還提供了一個reshape()
可以改變形狀,但是此函數并不能保證返回的是其拷貝,所以不推薦使用。推薦先用clone
創造一個副本然后再使用view
。參考此處
x_cp = x.clone().view(15)
x -= 1
print(x)
print(x_cp)
輸出:
tensor([[ 0.6035, 0.8110, -0.0451],
[ 0.8797, 1.0482, -0.0445],
[-0.7229, 2.8663, -0.5655],
[ 0.1604, -0.0254, 1.0739],
[ 2.2628, -0.9175, -0.2251]])
tensor([1.6035, 1.8110, 0.9549, 1.8797, 2.0482, 0.9555, 0.2771, 3.8663, 0.4345,
1.1604, 0.9746, 2.0739, 3.2628, 0.0825, 0.7749])
使用
clone
還有一個好處是會被記錄在計算圖中,即梯度回傳到副本時也會傳到源Tensor
。
另外一個常用的函數就是item()
, 它可以將一個標量Tensor
轉換成一個Python number:
x = torch.randn(1)
print(x)
print(x.item())
輸出:
tensor([2.3466])
2.3466382026672363
線性代數
另外,PyTorch還支持一些線性函數,這里提一下,免得用起來的時候自己造輪子,具體用法參考官方文檔。如下表所示:
函數 | 功能 |
---|---|
trace | 對角線元素之和(矩陣的跡) |
diag | 對角線元素 |
triu/tril | 矩陣的上三角/下三角,可指定偏移量 |
mm/bmm | 矩陣乘法,batch的矩陣乘法 |
addmm/addbmm/addmv/addr/baddbmm.. | 矩陣運算 |
t | 轉置 |
dot/cross | 內積/外積 |
inverse | 求逆矩陣 |
svd | 奇異值分解 |
PyTorch中的Tensor
支持超過一百種操作,包括轉置、索引、切片、數學運算、線性代數、隨機數等等,可參考官方文檔。
2.2.3 廣播機制
前面我們看到如何對兩個形狀相同的Tensor
做按元素運算。當對兩個形狀不同的Tensor
按元素運算時,可能會觸發廣播(broadcasting)機制:先適當復制元素使這兩個Tensor
形狀相同后再按元素運算。例如:
x = torch.arange(1, 3).view(1, 2)
print(x)
y = torch.arange(1, 4).view(3, 1)
print(y)
print(x + y)
輸出:
tensor([[1, 2]])
tensor([[1],
[2],
[3]])
tensor([[2, 3],
[3, 4],
[4, 5]])
由于x
和y
分別是1行2列和3行1列的矩陣,如果要計算x + y
,那么x
中第一行的2個元素被廣播(復制)到了第二行和第三行,而y
中第一列的3個元素被廣播(復制)到了第二列。如此,就可以對2個3行2列的矩陣按元素相加。
2.2.4 運算的內存開銷
前面說了,索引操作是不會開辟新內存的,而像y = x + y
這樣的運算是會新開內存的,然后將y
指向新內存。為了演示這一點,我們可以使用Python自帶的id
函數:如果兩個實例的ID一致,那么它們所對應的內存地址相同;反之則不同。
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y = y + x
print(id(y) == id_before) # False
如果想指定結果到原來的y
的內存,我們可以使用前面介紹的索引來進行替換操作。在下面的例子中,我們把x + y
的結果通過[:]
寫進y
對應的內存中。
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
y[:] = y + x
print(id(y) == id_before) # True
我們還可以使用運算符全名函數中的out
參數或者自加運算符+=
(也即add_()
)達到上述效果,例如torch.add(x, y, out=y)
和y += x
(y.add_(x)
)。
x = torch.tensor([1, 2])
y = torch.tensor([3, 4])
id_before = id(y)
torch.add(x, y, out=y) # y += x, y.add_(x)
print(id(y) == id_before) # True
注:雖然
view
返回的Tensor
與源Tensor
是共享data
的,但是依然是一個新的Tensor
(因為Tensor
除了包含data
外還有一些其他屬性),二者id(內存地址)并不一致。
2.2.5 Tensor
和NumPy相互轉換
我們很容易用numpy()
和from_numpy()
將Tensor
和NumPy中的數組相互轉換。但是需要注意的一點是: 這兩個函數所產生的的Tensor
和NumPy中的數組共享相同的內存(所以他們之間的轉換很快),改變其中一個時另一個也會改變!!!
還有一個常用的將NumPy中的array轉換成
Tensor
的方法就是torch.tensor()
, 需要注意的是,此方法總是會進行數據拷貝(就會消耗更多的時間和空間),所以返回的Tensor
和原來的數據不再共享內存。
Tensor
轉NumPy
使用numpy()
將Tensor
轉換成NumPy數組:
a = torch.ones(5)
b = a.numpy()
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)
輸出:
tensor([1., 1., 1., 1., 1.]) [1\. 1\. 1\. 1\. 1.]
tensor([2., 2., 2., 2., 2.]) [2\. 2\. 2\. 2\. 2.]
tensor([3., 3., 3., 3., 3.]) [3\. 3\. 3\. 3\. 3.]
NumPy數組轉Tensor
使用from_numpy()
將NumPy數組轉換成Tensor
:
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
print(a, b)
a += 1
print(a, b)
b += 1
print(a, b)
輸出:
[1\. 1\. 1\. 1\. 1.] tensor([1., 1., 1., 1., 1.], dtype=torch.float64)
[2\. 2\. 2\. 2\. 2.] tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
[3\. 3\. 3\. 3\. 3.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)
所有在CPU上的Tensor
(除了CharTensor
)都支持與NumPy數組相互轉換。
此外上面提到還有一個常用的方法就是直接用torch.tensor()
將NumPy數組轉換成Tensor
,需要注意的是該方法總是會進行數據拷貝,返回的Tensor
和原來的數據不再共享內存。
c = torch.tensor(a)
a += 1
print(a, c)
輸出
[4\. 4\. 4\. 4\. 4.] tensor([3., 3., 3., 3., 3.], dtype=torch.float64)
2.2.6 Tensor
on GPU
用方法to()
可以將Tensor
在CPU和GPU(需要硬件支持)之間相互移動。
# 以下代碼只有在PyTorch GPU版本上才會執行
if torch.cuda.is_available():
device = torch.device("cuda") # GPU
y = torch.ones_like(x, device=device) # 直接創建一個在GPU上的Tensor
x = x.to(device) # 等價于 .to("cuda")
z = x + y
print(z)
print(z.to("cpu", torch.double)) # to()還可以同時更改數據類型
注: 本文主要參考PyTorch官方文檔和此處,與原書同一節有很大不同。
2.3 自動求梯度
在深度學習中,我們經常需要對函數求梯度(gradient)。PyTorch提供的autograd包能夠根據輸入和前向傳播過程自動構建計算圖,并執行反向傳播。本節將介紹如何使用autograd包來進行自動求梯度的有關操作。
2.3.1 概念
上一節介紹的Tensor
是這個包的核心類,如果將其屬性.requires_grad
設置為True
,它將開始追蹤(track)在其上的所有操作(這樣就可以利用鏈式法則進行梯度傳播了)。完成計算后,可以調用.backward()
來完成所有梯度計算。此Tensor
的梯度將累積到.grad
屬性中。
注意在
y.backward()
時,如果y
是標量,則不需要為backward()
傳入任何參數;否則,需要傳入一個與y
同形的Tensor
。解釋見 2.3.2 節。
如果不想要被繼續追蹤,可以調用.detach()
將其從追蹤記錄中分離出來,這樣就可以防止將來的計算被追蹤,這樣梯度就傳不過去了。此外,還可以用with torch.no_grad()
將不想被追蹤的操作代碼塊包裹起來,這種方法在評估模型的時候很常用,因為在評估模型時,我們并不需要計算可訓練參數(requires_grad=True
)的梯度。
Function
是另外一個很重要的類。Tensor
和Function
互相結合就可以構建一個記錄有整個計算過程的有向無環圖(DAG)。每個Tensor
都有一個.grad_fn
屬性,該屬性即創建該Tensor
的Function
, 就是說該Tensor
是不是通過某些運算得到的,若是,則grad_fn
返回一個與這些運算相關的對象,否則是None。
下面通過一些例子來理解這些概念。
2.3.2 Tensor
創建一個Tensor
并設置requires_grad=True
:
x = torch.ones(2, 2, requires_grad=True)
print(x)
print(x.grad_fn)
輸出:
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
None
再做一下運算操作:
y = x + 2
print(y)
print(y.grad_fn)
輸出:
tensor([[3., 3.],
[3., 3.]], grad_fn=<AddBackward>)
<AddBackward object at 0x1100477b8>
注意x是直接創建的,所以它沒有grad_fn
, 而y是通過一個加法操作創建的,所以它有一個為<AddBackward>
的grad_fn
。
像x這種直接創建的稱為葉子節點,葉子節點對應的grad_fn
是None
。
print(x.is_leaf, y.is_leaf) # True False
再來點復雜度運算操作:
z = y * y * 3
out = z.mean()
print(z, out)
輸出:
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward>) tensor(27., grad_fn=<MeanBackward1>)
通過.requires_grad_()
來用in-place的方式改變requires_grad
屬性:
a = torch.randn(2, 2) # 缺失情況下默認 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad) # False
a.requires_grad_(True)
print(a.requires_grad) # True
b = (a * a).sum()
print(b.grad_fn)
輸出:
False
True
<SumBackward0 object at 0x118f50cc0>
2.3.3 梯度
因為out
是一個標量,所以調用backward()
時不需要指定求導變量:
out.backward() # 等價于 out.backward(torch.tensor(1.))
我們來看看out
關于x
的梯度 [Math Processing Error]<math xmlns="http://www.w3.org/1998/Math/MathML"><semantics><annotation encoding="application/x-tex">\frac{d(out)}{dx}</annotation></semantics></math>dxd(out):
print(x.grad)
輸出:
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
注意:grad在反向傳播過程中是累加的(accumulated),這意味著每一次運行反向傳播,梯度都會累加之前的梯度,所以一般在反向傳播之前需把梯度清零。
# 再來反向傳播一次,注意grad是累加的
out2 = x.sum()
out2.backward()
print(x.grad)
out3 = x.sum()
x.grad.data.zero_()
out3.backward()
print(x.grad)
輸出:
tensor([[5.5000, 5.5000],
[5.5000, 5.5000]])
tensor([[1., 1.],
[1., 1.]])
現在我們解釋2.3.1節留下的問題,為什么在
y.backward()
時,如果y
是標量,則不需要為backward()
傳入任何參數;否則,需要傳入一個與y
同形的Tensor
? 簡單來說就是為了避免向量(甚至更高維張量)對張量求導,而轉換成標量對張量求導。舉個例子,假設形狀為m x n
的矩陣 X 經過運算得到了p x q
的矩陣 Y,Y 又經過運算得到了s x t
的矩陣 Z。那么按照前面講的規則,dZ/dY 應該是一個s x t x p x q
四維張量,dY/dX 是一個p x q x m x n
的四維張量。問題來了,怎樣反向傳播?怎樣將兩個四維張量相乘???這要怎么乘???就算能解決兩個四維張量怎么乘的問題,四維和三維的張量又怎么乘?導數的導數又怎么求,這一連串的問題,感覺要瘋掉…… 為了避免這個問題,我們不允許張量對張量求導,只允許標量對張量求導,求導結果是和自變量同形的張量。所以必要時我們要把張量通過將所有張量的元素加權求和的方式轉換為標量,舉個例子,假設y
由自變量x
計算而來,w
是和y
同形的張量,則y.backward(w)
的含義是:先計算l = torch.sum(y * w)
,則l
是個標量,然后求l
對自變量x
的導數。 參考
來看一些實際例子。
x = torch.tensor([1.0, 2.0, 3.0, 4.0], requires_grad=True)
y = 2 * x
z = y.view(2, 2)
print(z)
輸出:
tensor([[2., 4.],
[6., 8.]], grad_fn=<ViewBackward>)
現在 z
不是一個標量,所以在調用backward
時需要傳入一個和z
同形的權重向量進行加權求和得到一個標量。
v = torch.tensor([[1.0, 0.1], [0.01, 0.001]], dtype=torch.float)
z.backward(v)
print(x.grad)
輸出:
tensor([2.0000, 0.2000, 0.0200, 0.0020])
注意,x.grad
是和x
同形的張量。
再來看看中斷梯度追蹤的例子:
x = torch.tensor(1.0, requires_grad=True)
y1 = x ** 2
with torch.no_grad():
y2 = x ** 3
y3 = y1 + y2
print(x.requires_grad)
print(y1, y1.requires_grad) # True
print(y2, y2.requires_grad) # False
print(y3, y3.requires_grad) # True
輸出:
True
tensor(1., grad_fn=<PowBackward0>) True
tensor(1.) False
tensor(2., grad_fn=<ThAddBackward>) True
可以看到,上面的y2
是沒有grad_fn
而且y2.requires_grad=False
的,而y3
是有grad_fn
的。如果我們將y3
對x
求梯度的話會是多少呢?
y3.backward()
print(x.grad)
輸出:
tensor(2.)
上面提到,y2.requires_grad=False
,所以不能調用 y2.backward()
,會報錯:
RuntimeError: element 0 of tensors does not require grad and does not have a grad_fn
此外,如果我們想要修改tensor
的數值,但是又不希望被autograd
記錄(即不會影響反向傳播),那么我么可以對tensor.data
進行操作。
x = torch.ones(1,requires_grad=True)
print(x.data) # 還是一個tensor
print(x.data.requires_grad) # 但是已經是獨立于計算圖之外
y = 2 * x
x.data *= 100 # 只改變了值,不會記錄在計算圖,所以不會影響梯度傳播
y.backward()
print(x) # 更改data的值也會影響tensor的值
print(x.grad)
輸出:
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])
注: 本文主要參考PyTorch官方文檔,與原書同一節有很大不同。