從張量到自動微分:PyTorch入門教程
作者:Sayak Paul
編譯:weakish
原文地址: ofollow,noindex"> https://www. datacamp.com/community/ tutorials/investigating-tensors-pytorch
深度學習領域有張量是資料結構的基石這一說法,Google的機器學習庫TensorFlow甚至都是以張量(tensor)命名的。張量是線性代數中用到的一種資料結構,類似向量和矩陣,你可以在張量上進行算術運算。
PyTorch 是由 Facebook 建立的python包,它提供兩個高層特性:1) 類似Numpy的基於GPU加速的張量運算 2) 在tape-based自動微分系統之上構建的深度神經網路。
本教程將介紹什麼是張量,如何在PyTorch中操作張量:
- 張量介紹
- PyTorch介紹
- PyTorch安裝步驟
- PyTorch下的一些張量操作
- 基於PyTorch實現一個簡單的神經網路
閒話少敘,讓我們開始介紹張量吧。
張量介紹
張量是向量和矩陣的推廣,可以理解為多維陣列。知名的《Deep Learning》(Goodfellow等編寫)是這樣介紹張量的:
在一般意義上,以基於可變數目的軸的規則網格組織的一組數字稱為張量。
標量是零階張量。向量是一階張量,矩陣是二階張量。
下面是張量的示意圖:

現在讓我們以更清晰易懂的方式看看什麼是張量。
張量是現代機器學習的基本構建。它是一個數據容器,大多數情況下包含數字,有時可能包含字串(不過這罕見)。所以可以把張量想象成一桶數字。
人們經常混用張量和多維陣列。不過有時需要嚴格區分兩者,如StackExchange指出:
張量和多維陣列是不同型別的物件。前者是一種函式,後者是適宜在座標系統中表示張量的一種資料結構。
在數學上,張量由多元線性函式定義。一個多元線性函式包含多個向量變數。張量域是張量值函式。更嚴謹的數學解釋,可以參考 https:// math.stackexchange.com/ q/10282
所以,張量是需要定義的函式或容器。實際上,當資料傳入時,計算才真正發生。當不需要嚴格區分陣列和張量的時候,陣列或多維陣列(1D, 2D, …, ND)一般可以視作張量。
現在我們稍微講下張量表述(Tensor notation)。
張量表述和矩陣類似,一般用大寫字母表示張量,帶整數下標的小寫字母表示張量中的標量值。

標量、向量、矩陣的許多運算同樣適用於張量。
張量和張量代數是物理和工程領域廣泛使用的工具。機器學習的許多技術,深度學習模型的訓練和操作,常常使用張量這一術語進行描述。
PyTorch介紹
PyTorch是一個非常靈活的基於Python的深度學習研究平臺。
PyTorch特性
- 提供各種張量的常規操作。
- 基於回放的自動微分系統。
- 不同於TensorFlow、Theano、Caffe、CNTK等大多數框架採用的靜態圖系統,PyTorch採用動態圖系統。
- 最小化框架開銷,可基於GPU加速。
- 相比Torch等替代品,PyTorch的記憶體使用非常高效。這讓你可以訓練比以往更大的深度學習模型。
Kirill Dubovikov 寫的 PyTorch vs TensorFlow — spotting the difference 比較了PyTorch和TensorFlow這兩個框架。如果你想了解TensorFlow,可以看看Karlijn Willems寫的教程 TensorFlow Tutorial For Beginners 。
PyTorch安裝步驟
PyTorch的安裝很簡單。如果你的顯示卡支援,可以安裝GPU版本的PyTorch。
你可以使用pip安裝torch、torchvision這兩個包,也可以使用conda安裝pytorch torchvision這兩個包。注意,Windows平臺上,PyTorch不支援Python 2.7,需要基於Python 3.5以上的版本安裝。
具體的安裝命令可以通過PyTorch官網查詢: https:// pytorch.org/get-started /locally/
好了,下面讓我們直接深入PyTorch下的一些張量算術。
PyTorch下的一些張量操作
首先,匯入所需的庫:
import torch
如果出現報錯,說明PyTorch沒有安裝成功,請參考上一節重新安裝。
現在,我們構造一個5×3的矩陣:
x = torch.rand(5, 3) print(x)
輸出:
tensor([[ 0.5991,0.9365,0.6120], [ 0.3622,0.1408,0.8811], [ 0.6248,0.4808,0.0322], [ 0.2267,0.3715,0.8430], [ 0.0145,0.0900,0.3418]])
再構造一個5×3的矩陣,不過這次用零初始化,並指定資料型別為 long
:
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])
如果你想檢驗下自己是否理解了PyTorch中的張量,那可以思考下上面的張量x是什麼類別的。
基於已有張量,可以建立新張量——新張量會複用輸入張量的屬性,比如dtype(資料型別),除非另外給出新值:
x = x.new_ones(5, 3, dtype=torch.double) 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([[-1.2174,1.1807,1.4249], [-1.1114, -0.8098,0.4003], [ 0.0780, -0.5011, -1.0985], [ 1.8160, -0.3778, -0.8610], [-0.7109, -2.0509, -1.2079]])
獲取張量的尺寸:
print(x.size())
輸出:
torch.Size([5, 3])
注意, torch.Size
事實上是一個元組,支援所有元組操作。
現在,讓我們看下張量的加法。
張量加法
兩個張量分素相加,得到維度一致的張量,結果張量中每個標量的值是相應標量的和。

y = torch.rand(5, 3) print(x) print(y) print(x + y)
輸出:
tensor([[-1.2174,1.1807,1.4249], [-1.1114, -0.8098,0.4003], [ 0.0780, -0.5011, -1.0985], [ 1.8160, -0.3778, -0.8610], [-0.7109, -2.0509, -1.2079]]) tensor([[ 0.8285,0.7619,0.1147], [ 0.1624,0.8994,0.6119], [ 0.2802,0.2950,0.7098], [ 0.8132,0.3382,0.4383], [ 0.6738,0.2022,0.3264]]) tensor([[-0.3889,1.9426,1.5396], [-0.9490,0.0897,1.0122], [ 0.3583, -0.2061, -0.3887], [ 2.6292, -0.0396, -0.4227], [-0.0371, -1.8487, -0.8815]])
除了使用 +
運算子外,也可以呼叫 torch.add
方法(兩者是等價的):
print(torch.add(x, y))
下面我們來看張量減法。
張量減法
兩個張量分素相減,得到維度一致的張量,結果張量中每個標量的值是相應標量之差。

接著我們將討論 張量相乘 。
張量乘法
假設 mat1
是一個(n×m)的張量, mat2
是一個(m×p)的張量,兩者相乘,將得到一個(n×p)的張量。
mat1 = torch.randn(2, 3) mat2 = torch.randn(3, 3) print(mat1) print(mat2) print(torch.mm(mat1, mat2))
輸出:
tensor([[ 1.9490, -0.6503, -1.9448], [-0.7126,1.0519, -0.4250]]) tensor([[ 0.0846,0.4410, -0.0625], [-1.3264, -0.5265,0.2575], [-1.3324,0.6644,0.3528]]) tensor([[ 3.6185, -0.0901, -0.9753], [-0.8892, -1.1504,0.1654]])
注意, torch.mm()
不支援廣播(broadcast)。
廣播
“廣播”這一術語用於描述如何在形狀不一的陣列上應用算術運算。在滿足特定限制的前提下,較小的陣列“廣播至”較大的陣列,使兩者形狀互相相容。廣播提供了一個向量化陣列操作的機制,這樣遍歷就發生在C層面,而不是Python層面。廣播可以避免不必要的資料複製,通常導向高效的演算法實現。不過,也存在不適用廣播的情形(可能導致拖慢計算過程的低效記憶體使用)。
可廣播的一對張量需滿足以下規則:
- 每個張量至少有一個維度。
- 迭代維度尺寸時,從尾部的維度開始,維度尺寸或者相等,或者其中一個張量的維度尺寸為一,或者其中一個張量不存在這個維度。
讓我們通過幾段程式碼來理解PyTorch的廣播機制。
x=torch.empty(5,7,3) y=torch.empty(5,7,3)
相同形狀的張量總是可廣播的,因為總能滿足以上規則。
x=torch.empty((0,)) y=torch.empty(2,2)
不可廣播(x不滿足第一條規則)。
# 為了清晰易讀,可以對齊尾部 x=torch.empty(5,3,4,1) y=torch.empty(3,1,1)
x和y可廣播:
- 倒數第一個維度:兩者的尺寸均為1
- 倒數第二個維度:y尺寸為1
- 倒數第三個維度:兩者尺寸相同
- 倒數第四個維度:y該維度不存在
但下面一對就不可廣播了:
x=torch.empty(5,2,4,1) y=torch.empty(3,1,1)
這是因為倒數第三個維度: 2 != 3
現在你對“可廣播”這一概念已經有所瞭解了,讓我們看下,廣播後的張量是什麼樣的。
如果張量x和張量y是可廣播的,那麼廣播後的張量尺寸按照如下方法計算:
如果x和y的維數不等,在維數較少的張量上新增尺寸為1的維度。結果維度尺寸是x和y相應維度尺寸的較大者。例如:
x=torch.empty(5,1,4,1) y=torch.empty(3,1,1) (x+y).size()
輸出:
torch.Size([5, 3, 4, 1])
再如:
x=torch.empty(1) y=torch.empty(3,1,7) (x+y).size()
輸出:
torch.Size([3, 1, 7])
再看一個不可廣播的例子:
x=torch.empty(5,2,4,1) y=torch.empty(3,1,1) (x+y).size()
報錯:
--------------------------------------------------------------------------- RuntimeErrorTraceback (most recent call last) <ipython-input-17-72fb34250db7> in <module>() 1 x=torch.empty(5,2,4,1) 2 y=torch.empty(3,1,1) ----> 3 (x+y).size() RuntimeError: The size of tensor a (2) must match the size of tensor b (3) at non-singleton dimension 1
你現在應該已經掌握了廣播這個概念了!
張量乘積是最常見的張量乘法,但也存在其他種類的張量乘法,例如 張量點積 和 張量縮並 。
藉助 Numpy橋 ,PyTorch張量和NumPy陣列之間的互相轉換極其迅速。下面就讓我們來了解一下這個概念。
NumPy橋
NumPy橋使得PyTorch張量和NumPy陣列共享底層記憶體地址,對其中之一的修改會反映到另一個上。
轉換PyTorch張量至NumPy陣列。
a = torch.ones(5) b = a.numpy() print(a) print(b)
輸出:
tensor([ 1.,1.,1.,1.,1.]) [1. 1. 1. 1. 1.]
在這一節中,我們討論了一些基本的張量算術,例如加法、減法、張量乘積。下一節我們將使用PyTorch實現一個基本的神經網路。
基於PyTorch實現一個簡單的神經網路
如果你想要溫習一下神經網路的概念,可以參考以下文章:
- 初窺神經網路內部機制
- 從頭開始搭建三層神經網路
- 基於Numpy實現神經網路:反向傳播、梯度下降
在實現神經網路之前,我們先來討論一下 自動微分 ,這是PyTorch下所有神經網路的核心,在進行反向傳播計算梯度時尤其有用。
PyTorch的 autograd
模組為張量的所有運算提供了自動微分。這是一個define-by-run框架,也就是說,反向傳播由程式碼如何執行定義,每個迭代都可以不一樣。
讓我們直接用程式碼展示自動微分是如何工作的。
x = torch.ones(2, 2, requires_grad=True) print(x)
輸出:
tensor([[ 1.,1.], [ 1.,1.]])
進行加法運算:
y = x + 2 print(y)
輸出:
tensor([[ 3.,3.], [ 3.,3.]])
再進行一些運算:
z = y * y * 3 out = z.mean() print(z) print(out)
輸出:
tensor([[ 27.,27.], [ 27.,27.]]) tensor(27.)
現在讓我們進行反向傳播:
out.backward() print(x.grad)
自動微分給出的梯度為:
tensor([[ 4.5000,4.5000], [ 4.5000,4.5000]])
感興趣的讀者可以手工驗證下梯度。
瞭解了PyTorch如何進行自動微分之後,讓我們使用PyTorch編碼一個簡單的神經網路。
我們將建立一個簡單的神經網路,包括一個隱藏層,一個輸出層。隱藏層使用ReLU啟用,輸出層使用sigmoid啟用。
構建神經網路需要引入 torch.nn
模組:
import torch.nn as nn
接著定義網路層尺寸和batch尺寸:
n_in, n_h, n_out, batch_size = 10, 5, 1, 10
現在生成一些輸入資料 x 和目標資料 y ,並使用PyTorch張量儲存這些資料。
x = torch.randn(batch_size, n_in) y = torch.tensor([[1.0], [0.0], [0.0], [1.0], [1.0], [1.0], [0.0], [0.0], [1.0], [1.0]])
接下來,只需一行程式碼就可以定義我們的模型:
model = nn.Sequential(nn.Linear(n_in, n_h), nn.ReLU(), nn.Linear(n_h, n_out), nn.Sigmoid())
我們建立了一個輸入 -> 線性 -> relu -> 線性 -> sigmoid的模型。對於需要更多自定義功能的更加複雜的模型,可以定義一個類,具體請參考PyTorch文件。
現在,我們需要構造損失函式。我們將使用均方誤差:
criterion = torch.nn.MSELoss()
然後定義優化器。我們將使用強大的隨機梯度下降演算法,學習率定為0.01. model.parameters()
會返回一個模型引數(權重、偏置)上的迭代器。
optimizer = torch.optim.SGD(model.parameters(), lr=0.01)
下面我們跑50個epoch,這依次包括前向傳播、損失計算、反向傳播和引數更新。
for epoch in range(50): # 前向傳播 y_pred = model(x) # 計算並列印損失 loss = criterion(y_pred, y) print('epoch: ', epoch,' loss: ', loss.item()) # 梯度歸零 optimizer.zero_grad() # 反向傳播 loss.backward() # 更新引數 optimizer.step()
輸出:
epoch:0loss:0.2399429827928543 epoch:1loss:0.23988191783428192 epoch:2loss:0.23982088267803192 epoch:3loss:0.2397598922252655 epoch:4loss:0.23969893157482147 epoch:5loss:0.23963800072669983 epoch:6loss:0.23957709968090057 epoch:7loss:0.23951618373394012 epoch:8loss:0.23945537209510803 epoch:9loss:0.23939454555511475 epoch:10loss:0.23933371901512146 epoch:11loss:0.23927298188209534 epoch:12loss:0.23921218514442444 epoch:13loss:0.23915143311023712 epoch:14loss:0.2390907108783722 epoch:15loss:0.23903003334999084 epoch:16loss:0.23896940052509308 epoch:17loss:0.23890872299671173 epoch:18loss:0.23884813487529755 epoch:19loss:0.23878750205039978 epoch:20loss:0.23872694373130798 epoch:21loss:0.2386663407087326 epoch:22loss:0.2386058121919632 epoch:23loss:0.23854532837867737 epoch:24loss:0.23848481476306915 epoch:25loss:0.23842433094978333 epoch:26loss:0.2383638620376587 epoch:27loss:0.23830339312553406 epoch:28loss:0.2382429838180542 epoch:29loss:0.23818258941173553 epoch:30loss:0.2381247729063034 epoch:31loss:0.2380656749010086 epoch:32loss:0.23800739645957947 epoch:33loss:0.2379491776227951 epoch:34loss:0.2378900945186615 epoch:35loss:0.23783239722251892 epoch:36loss:0.23777374625205994 epoch:37loss:0.23771481215953827 epoch:38loss:0.23765745759010315 epoch:39loss:0.23759838938713074 epoch:40loss:0.23753997683525085 epoch:41loss:0.2374821901321411 epoch:42loss:0.23742322623729706 epoch:43loss:0.23736533522605896 epoch:44loss:0.23730707168579102 epoch:45loss:0.23724813759326935 epoch:46loss:0.23719079792499542 epoch:47loss:0.23713204264640808 epoch:48loss:0.23707345128059387 epoch:49loss:0.2370160073041916
PyTorch的寫法很清晰,配上註釋,應該不難理解。如果仍有不解之處,可以參考下面的講解:
-
y_pred
獲取模型一次前向傳播的預測值。y_pred
和目標變數y
一起傳給criterion
以計算損失。 - 接著,
optimizer.zero_grad()
清空上一次迭代的梯度。 - 接下來的
loss.backward()
集中體現了PyTorch的神奇之處——這裡用到了PyTorch的Autograd(自動計算梯度)特性。Autograd基於動態建立的計算圖自動計算所有引數上的梯度。總的來說,這一步進行的是梯度下降和反向傳播。 - 最後,我們呼叫
optimizer.step()
,使用新的梯度更新一次所有引數。
恭喜你讀到了這篇長文的結尾。這篇文章從張量講到了自動微分,同時基於PyTorch及其張量系統實現了一個簡單的神經網路。
如果你想了解更多關於PyTorch的內容,或想進一步深入,請閱讀PyTorch的官方文件和教程,這些文件和教程寫得非常好。你可以從PyTorch官網找到這些文件和教程。
撰寫這篇教程的時候,我參考了以下內容:
- Daniel A. Fleisch的《 A Student’s Guide to Vectors and Tensors 》
- PyTorch官方文件
- Jason Brownlee寫的 A Gentle Introduction to Tensors for Machine Learning with NumPy