程式碼例項理解PyTorch
介紹
PyTorch是增長最快的深度學習框架。PyTorch也非常具有Python風格,注重簡潔和實用。
此外,也有一些使用者說,使用PyTorch甚至可以改善健康。
動機
網上有許多PyTorch教程,它的文件非常完整和廣泛。那麼,為什麼要繼續閱讀這個循序漸進的教程呢?這份教程以一系列常見的例子為主從基本原理開始講解。從而使大家對PyTorch的理解更加直觀。本文除了這些之外,還將提供一些避免常見陷阱和錯誤的建議。這份教程內容比較多,因此,為了便於查閱,建立目錄如下:
目錄
一個簡單的迴歸問題
梯度下降法
Numpy中的線性迴歸
PyTorch
Autograd
動態計算圖
最佳化器
損失
模型
資料集
DataLoader
評價
一個簡單的迴歸問題
大多數教程都是從一些漂亮的影象分類問題開始,以說明如何使用PyTorch。但是這容易讓人偏離原來的目標即:PyTorch是如何工作的?
因此本教程從一個簡單的
迴歸問題
開始。線性迴歸模型可表示成如下形式:
很多人認為迴歸模型就是線性迴歸,但是不是這樣的,迴歸代表你的模型結果是一個或多個連續值。
資料生成
讓我們開始生成一些合成數據:我們從特徵x的100個點的向量開始,然後使用a = 1, b = 2和一些高斯噪聲建立我們的標籤。
接下來,讓我們將合成數據分解為
訓練集
和
驗證集
,打亂索引陣列並使用前80個打亂的點進行訓練。
# Data Generation
np。random。seed(42)
x = np。random。rand(100, 1)
y = 1 + 2 * x + 。1 * np。random。randn(100, 1)
# Shuffles the indices
idx = np。arange(100)
np。random。shuffle(idx)
# Uses first 80 random indices for train
train_idx = idx[:80]
# Uses the remaining indices for validation
val_idx = idx[80:]
# Generates train and validation sets
x_train, y_train = x[train_idx], y[train_idx]
x_val, y_val = x[val_idx], y[val_idx]
我們知道a = 1 b = 2,但是現在讓我們看看如何使用
梯度下降
和訓練集中的80個點來接近真實值的。
梯度下降法
關於梯度下降的內部執行機制,前面有篇文章來專門說明。這裡只簡單介紹梯度下降的四個基本步驟。
步驟1:計算損失
對於迴歸問題,損失由均方誤差(MSE)給出,即標籤(y)和預測(a + bx)之間所有平方誤差的平均值。
值得一提的是,如果我們使用訓練集(N)中的所有點來計算損失,我們是在執行
批次梯度下降
。如果我們每次都用一個點,那就是
隨機梯度下降法
。在1和n之間的任何其他(n)都是
小批次梯度下降
的特徵。
步驟2:計算梯度
梯度是偏導數,為什麼偏導數?因為它是用一個引數(w。r。t)來計算的。我們有兩個引數,a和b,所以我們必須計算兩個偏導。 導數告訴你,當你稍微改變某個量時,這個量的變化量是多少。在我們的例子中,當我們改變兩個引數中的一個時,我們的MSE損失變化了多少?
步驟3:更新引數
在最後一步,我們使用梯度來更新引數。因為我們試圖最小化我們的損失,所以我們反轉了更新的梯度符號。
還需要考慮另一個引數:學習率,用希臘字母eta表示(看起來像字母n),這是我們需要對梯度進行引數更新的乘法因子,在程式裡通常簡化為lr。
關於如何選擇合適的學習率,這是一個需要大量實踐的內容,學習率不能太大,也不能太小。
第四步:重複。
現在,我們使用更新的引數返回步驟1並重新啟動流程。
對於批次梯度下降,這是微不足道的,因為它使用所有的點來計算損失-一個輪次等於一個更新。對於隨機梯度下降,一個epoch意味著N次更新,而對於小批次(大小為N),一個epoch有N/n次更新。
簡單地說,對於許多時代來說,反覆地重複這個過程就是訓練一個模型。
Numpy中的線性迴歸
接下來就是使用Numpy用梯度下降來實驗線性迴歸模型的時候了。還沒有到PyTorch,使用Numpy的原因有兩點:
介紹任務的結松
展示主要的難點,以便能夠充分理解使用PyTorch的方便之處。
對於一個模型的訓練,有4個初始化步驟:
引數/權重的隨機初始化(我們只有兩個,a和b)——第3行和第4行;
超引數的初始化(在我們的例子中,只有學習速率和epoch的數量)——第9行和第11行; 確保始終初始化您的隨機種子,以確保您的結果的再現性。和往常一樣,隨機的種子是42,是所有隨機種子中最不隨機的:-)
每個epoch有四個訓練步驟:
計算模型的預測——這是正向傳遞——第15行;
計算損失,使用預測和標籤,以及當前任務的適當損失函式——第18行和第20行;
計算每個引數的梯度——第23行和第24行;
更新引數——第27行和第28行; 請記住,如果您不使用批次梯度下降(我們的示例使用),則必須編寫一個內部迴圈來為每個點(隨機)或n個點(迷你批次)執行四個訓練步驟。稍後我們將看到一個小型批處理示例。
# Initializes parameters “a” and “b” randomly
np。random。seed(42)
a = np。random。randn(1)
b = np。random。randn(1)
print(a, b)
# Sets learning rate
lr = 1e-1
# Defines number of epochs
n_epochs = 1000
for epoch in range(n_epochs):
# Computes our model‘s predicted output
yhat = a + b * x_train
# How wrong is our model? That’s the error!
error = (y_train - yhat)
# It is a regression, so it computes mean squared error (MSE)
loss = (error ** 2)。mean()
# Computes gradients for both “a” and “b” parameters
a_grad = -2 * error。mean()
b_grad = -2 * (x_train * error)。mean()
# Updates parameters using gradients and the learning rate
a = a - lr * a_grad
b = b - lr * b_grad
print(a, b)
# Sanity Check: do we get the same results as our gradient descent?
from sklearn。linear_model import LinearRegression
linr = LinearRegression()
linr。fit(x_train, y_train)
print(linr。intercept_, linr。coef_[0])
結果是:
# a and b after initialization
[0。49671415] [-0。1382643]
# a and b after our gradient descent
[1。02354094] [1。96896411]
# intercept and coef from Scikit-Learn
[1。02354075] [1。96896447]
以上是Numpy的做法,接下來我們看一看PyTorch的做法。
PyTorch
首先,我們需要介紹一些基本概念。
在深度學習中,張量無處不在。嗯,谷歌的框架被稱為TensorFlow是有原因的,那到底什麼是張量?
張量
張量(tensor)是多維陣列,目的是把向量、矩陣推向更高的維度。
一個標量(一個數字)有0維,一個向量有1維,一個矩陣有2維,一個張量有3維或更多。 但是,為了
簡單
起見,我們通常也稱向量和矩陣為
張量
。
載入資料,裝置和CUDA
你可能會問:“我們如何從Numpy的陣列過渡到PyTorch的張量?”這就是from_numpy的作用。它返回一個CPU張量。
如何要使用GPU,那麼它會把張量傳送到GPU上面。“如果我想讓我的程式碼回退到CPU,如果沒有可用的GPU ?”你可以使用cuda。is_available()來找出你是否有一個GPU供你使用,並相應地設定你的裝置。 當然還可以使用float()輕鬆地將其轉換為較低精度(32位浮點數)。
import torch
import torch。optim as optim
import torch。nn as nn
from torchviz import make_dot
device = ‘cuda’ if torch。cuda。is_available() else ‘cpu’
# Our data was in Numpy arrays, but we need to transform them into PyTorch‘s Tensors
# and then we send them to the chosen device
x_train_tensor = torch。from_numpy(x_train)。float()。to(device)
y_train_tensor = torch。from_numpy(y_train)。float()。to(device)
# Here we can see the difference - notice that 。type() is more useful
# since it also tells us WHERE the tensor is (device)
print(type(x_train), type(x_train_tensor), x_train_tensor。type())
如果比較這兩個變數的型別,就會得到預期的結果第一種程式碼用的是
numpy。ndarray
,第三種程式碼用的是
torch。Tensor
。
使用PyTorch的type(),它會顯示它的位置。
我們也可以反過來,使用Numpy()將張量轉換回Numpy陣列。它應該像
x_train_tensor。numpy()
一樣簡單,但是…
TypeError: can’t convert CUDA tensor to numpy。 Use Tensor。cpu() to copy the tensor to host memory first。
非常遺憾,Numpy不能處理GPU張量。
建立引數
如何區分用於資料的張量(就像我們剛剛建立的那些)和用作(可訓練的)引數/權重的張量?
後一個張量需要計算它的梯度,所以我們可以更新它們的值(即引數的值)。這就是
requires_grad=True
引數的作用。它告訴PyTorch我們想讓它為我們計算梯度。
你可能想為一個引數建立一個簡單的張量,然後把它傳送到所選擇的裝置上,就像我們處理資料一樣,對吧? 但其實沒那麼快……
# FIRST
# Initializes parameters “a” and “b” randomly, ALMOST as we did in Numpy
# since we want to apply gradient descent on these parameters, we need
# to set REQUIRES_GRAD = TRUE
a = torch。randn(1, requires_grad=True, dtype=torch。float)
b = torch。randn(1, requires_grad=True, dtype=torch。float)
print(a, b)
# SECOND
# But what if we want to run it on a GPU? We could just send them to device, right?
a = torch。randn(1, requires_grad=True, dtype=torch。float)。to(device)
b = torch。randn(1, requires_grad=True, dtype=torch。float)。to(device)
print(a, b)
# Sorry, but NO! The to(device) “shadows” the gradient。。。
# THIRD
# We can either create regular tensors and send them to the device (as we did with our data)
a = torch。randn(1, dtype=torch。float)。to(device)
b = torch。randn(1, dtype=torch。float)。to(device)
# and THEN set them as requiring gradients。。。
a。requires_grad_()
b。requires_grad_()
print(a, b)
第一個程式碼塊為我們的引數、梯度和所有東西建立了兩個很好的張量。但它們是CPU張量。
# FIRST
tensor([-0。5531], requires_grad=True)
tensor([-0。7314], requires_grad=True)
在第二段程式碼中,我們嘗試了將它們傳送到我們的GPU的簡單方法。我們成功地將它們傳送到另一個裝置上,但是我們不知怎麼地“丟失”了梯度……
# SECOND
tensor([0。5158], device=‘cuda:0’, grad_fn=
在第三塊中,我們首先將張量傳送到裝置,然後使用
requires_grad_()
方法將其
requires_grad
設定為True。
在PyTorch中,每個以下劃線(_)結尾的方法都會進行適當的更改,這意味著它們將修改底層變數。
儘管最後一種方法工作得很好,但最好在裝置建立時將張量分配給它們。
# We can specify the device at the moment of creation - RECOMMENDED!
torch。manual_seed(42)
a = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
b = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
print(a, b)
tensor([0。6226], device=‘cuda:0’, requires_grad=True) tensor([1。4505], device=‘cuda:0’, requires_grad=True)
容易多了,對吧? 現在我們知道了如何建立需要梯度的張量,讓我們看看PyTorch如何處理它們。
Autograd
Autograd是PyTorch的自動微分包。
那麼,我們如何讓PyTorch完成它的任務並計算所有的梯度呢?這就是
backward()
的好處。
還記得計算梯度的起點嗎?這是loss。因此,我們需要從相應的Python變數中呼叫
backward()
方法,比如,
loss。 backwards()
。
那麼梯度的實際值呢?我們可以透過觀察張量的grad屬性來考察它們。
如果你檢視該方法的文件,就會清楚地看到漸變是累積的。因此,每次我們使用梯度來更新引數時,我們都需要在之後將梯度歸零。這就是
zero_()
的好處。
因此,讓我們拋棄手工計算梯度的方法,同時使用
backward()
和
zero_()
方法。 就這些嗎? 嗯,差不多…但是,總是有一個陷阱,這一次它與引數的更新有關…
lr = 1e-1
n_epochs = 1000
torch。manual_seed(42)
a = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
b = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
for epoch in range(n_epochs):
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2)。mean()
# No more manual computation of gradients!
# a_grad = -2 * error。mean()
# b_grad = -2 * (x_tensor * error)。mean()
# We just tell PyTorch to work its way BACKWARDS from the specified loss!
loss。backward()
# Let‘s check the computed gradients。。。
print(a。grad)
print(b。grad)
# What about UPDATING the parameters? Not so fast。。。
# FIRST ATTEMPT
# AttributeError: ’NoneType‘ object has no attribute ’zero_‘
# a = a - lr * a。grad
# b = b - lr * b。grad
# print(a)
# SECOND ATTEMPT
# RuntimeError: a leaf Variable that requires grad has been used in an in-place operation。
# a -= lr * a。grad
# b -= lr * b。grad
# THIRD ATTEMPT
# We need to use NO_GRAD to keep the update out of the gradient computation
# Why is that? It boils down to the DYNAMIC GRAPH that PyTorch uses。。。
with torch。no_grad():
a -= lr * a。grad
b -= lr * b。grad
# PyTorch is “clingy” to its computed gradients, we need to tell it to let it go。。。
a。grad。zero_()
b。grad。zero_()
print(a, b)
在第一次嘗試中,如果我們使用相同的更新結構如Numpy程式碼,我們會得到下面的奇怪的錯誤,我們再次“失去”梯度而重新分配引數更新結果。因此,grad屬性為None,它會引發錯誤…
# FIRST ATTEMPT
tensor([0。7518], device=’cuda:0‘, grad_fn=
AttributeError: ’NoneType‘ object has no attribute ’zero_‘
然後,我們稍微更改一下,在第二次嘗試中使用熟悉的就地Python賦值。而且,PyTorch再一次抱怨它並提出一個錯誤。
# SECOND ATTEMPT
RuntimeError: a leaf Variable that requires grad has been used in an in-place operation。
為什麼? !事實證明,這是一個“好事過頭”的例子。罪魁禍首是PyTorch的能力,它能夠從每一個涉及到任何梯度計算張量或其依賴項的Python操作中構建一個動態計算圖。 在下一節中,我們將深入討論動態計算圖的內部工作方式。
那麼,我們如何告訴PyTorch“後退”並讓我們更新引數,而不打亂它的動態計算圖呢? 這就是
torch。no_grad()
。no_grad()的好處。它允許我們對張量執行常規的Python操作,與PyTorch的計算圖無關。
最後,我們成功地運行了我們的模型並獲得了結果引數。當然,它們與我們在純numpy實現中得到的那些差不多。
# THIRD ATTEMPT
tensor([1。0235], device=’cuda:0‘, requires_grad=True)
tensor([1。9690], device=’cuda:0‘, requires_grad=True)
動態計算圖
目前神經網路框架分為靜態圖框架和動態圖框架,PyTorch 和 TensorFlow、Caffe 等框架最大的區別就是他們擁有不同的計算圖表現形式。
TensorFlow 使用靜態圖,這意味著我們先定義計算圖,然後不斷使用它,而在 PyTorch 中,每次都會重新構建一個新的計算圖
。
對於使用者來說,兩種形式的計算圖有著非常大的區別,同時靜態圖和動態圖都有他們各自的優點,比如動態圖比較方便debug,使用者能夠用任何他們喜歡的方式進行debug,同時非常直觀,而靜態圖是透過先定義後執行的方式,之後再次執行的時候就不再需要重新構建計算圖,所以速度會比動態圖更快。
PyTorchViz包及其make_dot(變數)方法允許我們輕鬆地視覺化與給定Python變數關聯的圖。
torch。manual_seed(42)
a = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
b = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2)。mean()
如果我們呼叫
make_dot(yhat)
,我們將得到下面圖中最左邊的圖形:
讓我們仔細看看它的組成部分:
藍方框:這些對應於我們用作引數的張量,也就是我們要求PyTorch計算梯度的張量;
灰箱:包含梯度計算張量或其相依關係的Python操作;
綠色方框:與灰色方框相同,只是它是漸變計算的起點(假設使用reverse()方法從用於視覺化圖形的變數中呼叫)——它們是從圖形中的自底向上計算的。
如果我們為error(中間)和loss(右邊)變數繪製圖形,那麼它們與第一個變數之間的惟一區別就是中間步驟的數量(灰色框)。
現在,仔細看看最左邊的綠色方框:有兩個箭頭指向它,因為它將兩個變數a和b*x相加。
然後,看一下同一圖形的灰框:它執行的是乘法,即b*x。但是隻有一個箭頭指向它!箭頭來自於對應於引數b的藍色方框。
為什麼我們沒有資料x的方框呢?答案是:我們不為它計算梯度!因此,即使計算圖所執行的操作涉及到更多的張量,也只顯示了梯度計算張量及其依賴關係。
如果我們將引數a的
requires_grad
設為False,計算圖形會發生什麼變化?
不出所料,與引數a對應的藍色框是no more!很簡單:沒有梯度,沒有圖形。
動態計算圖最好的地方在於你可以讓它變得像你想要的那樣複雜。甚至可以使用控制流語句(例如,if語句)來控制梯度流(顯然!)
下面的圖顯示了一個示例。
最佳化器
到目前為止,我們一直在使用計算出的梯度手動更新引數。這對於兩個引數來說可能很好,但是如果我們有很多引數呢?我們使用PyTorch的一個最佳化器,比如SGD或Adam。
最佳化器獲取我們想要更新的引數、我們想要使用的學習率(可能還有許多其他超引數!)並透過其step()方法執行更新。
此外,我們也不需要一個接一個地將梯度歸零。我們只需呼叫最佳化器的
zero_grad()
方法就可以了! 在下面的程式碼中,我們建立了一個隨機梯度下降(SGD)最佳化器來更新引數a和b。
不要被最佳化器的名字所欺騙:如果我們一次使用所有的訓練資料進行更新——就像我們在程式碼中所做的那樣——最佳化器執行的是批次梯度下降,而不是它的名字。
torch。manual_seed(42)
a = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
b = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
print(a, b)
lr = 1e-1
n_epochs = 1000
# Defines a SGD optimizer to update the parameters
optimizer = optim。SGD([a, b], lr=lr)
for epoch in range(n_epochs):
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2)。mean()
loss。backward()
# No more manual update!
# with torch。no_grad():
# a -= lr * a。grad
# b -= lr * b。grad
optimizer。step()
# No more telling PyTorch to let gradients go!
# a。grad。zero_()
# b。grad。zero_()
optimizer。zero_grad()
print(a, b)
讓我們檢查一下之前和之後的兩個引數,以確保一切正常:
# BEFORE: a, b
tensor([0。6226], device=’cuda:0‘, requires_grad=True) tensor([1。4505], device=’cuda:0‘, requires_grad=True)
# AFTER: a, b
tensor([1。0235], device=’cuda:0‘, requires_grad=True) tensor([1。9690], device=’cuda:0‘, requires_grad=True)
損失
PyTorch集成了很多損失函式。在這個例子中我們使用的是MSE損失。
注意
nn。MSELoss
實際上為我們建立了一個損失函式——它不是損失函式本身。此外,你還可以指定一個要應用的reduction method,即如何聚合單個點的結果—你可以對它們進行平均(約簡= ’ mean ‘),或者簡單地對它們求和(約簡= ’ sum ‘)。
然後在第20行使用建立的損失函式,根據我們的預測和標籤計算損失。
我們的程式碼是這樣的:
torch。manual_seed(42)
a = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
b = torch。randn(1, requires_grad=True, dtype=torch。float, device=device)
print(a, b)
lr = 1e-1
n_epochs = 1000
# Defines a MSE loss function
loss_fn = nn。MSELoss(reduction=’mean‘)
optimizer = optim。SGD([a, b], lr=lr)
for epoch in range(n_epochs):
yhat = a + b * x_train_tensor
# No more manual loss!
# error = y_tensor - yhat
# loss = (error ** 2)。mean()
loss = loss_fn(y_train_tensor, yhat)
loss。backward()
optimizer。step()
optimizer。zero_grad()
print(a, b)
模型
在PyTorch中,model由一個常規的Python類表示,該類繼承自Module類。
它需要實現的最基本的方法是:
__init__(self)
定義了組成模型的兩個引數:a和b。
模型可以包含其他模型作為它的屬性,所以可以很容易實現巢狀。
forward(self, x)
:它執行了實際的計算,也就是說,給定輸入x,它輸出一個預測。
讓我們為我們的迴歸任務構建一個適當的(但簡單的)模型。它應該是這樣的:
class ManualLinearRegression(nn。Module):
def __init__(self):
super()。__init__()
# To make “a” and “b” real parameters of the model, we need to wrap them with nn。Parameter
self。a = nn。Parameter(torch。randn(1, requires_grad=True, dtype=torch。float))
self。b = nn。Parameter(torch。randn(1, requires_grad=True, dtype=torch。float))
def forward(self, x):
# Computes the outputs / predictions
return self。a + self。b * x
在
_init__
方法中,我們定義了兩個引數,a和b,使用
Parameter()
類,告訴PyTorch應該將這些張量視為它們是的屬性的模型引數。
我們為什麼要關心這個?透過這樣做,我們可以使用模型的
parameters()
方法來檢索所有模型引數的迭代器,甚至是那些巢狀模型的引數,我們可以使用它們來提供我們的最佳化器(而不是自己構建引數列表!) 此外,我們可以使用模型的
state_dict()
方法獲取所有引數的當前值。
重要提示:我們需要將模型傳送到資料所在的同一裝置。如果我們的資料是由GPU張量構成的,我們的模型也必須“活”在GPU內部。
我們可以使用所有這些方便的方法來改變我們的程式碼,應該是這樣的:
torch。manual_seed(42)
# Now we can create a model and send it at once to the device
model = ManualLinearRegression()。to(device)
# We can also inspect its parameters using its state_dict
print(model。state_dict())
lr = 1e-1
n_epochs = 1000
loss_fn = nn。MSELoss(reduction=’mean‘)
optimizer = optim。SGD(model。parameters(), lr=lr)
for epoch in range(n_epochs):
# What is this?!?
model。train()
# No more manual prediction!
# yhat = a + b * x_tensor
yhat = model(x_train_tensor)
loss = loss_fn(y_train_tensor, yhat)
loss。backward()
optimizer。step()
optimizer。zero_grad()
print(model。state_dict())
現在打印出來的語句將是這樣的——引數a和引數b的最終值仍然相同,所以一切正常。
OrderedDict([(’a‘, tensor([0。3367], device=’cuda:0‘)), (’b‘, tensor([0。1288], device=’cuda:0‘))])
OrderedDict([(’a‘, tensor([1。0235], device=’cuda:0‘)), (’b‘, tensor([1。9690], device=’cuda:0‘))])
在PyTorch中,模型有一個
train()
方法,有點令人失望的是,它沒有執行訓練步驟。其唯一目的是將模型設定為訓練模式。為什麼這很重要?有些模型可能使用Dropout機制,在訓練和評估階段有不同的行為。
巢狀模型
在我們的模型中,我們手動建立了兩個引數來執行線性迴歸。讓我們使用PyTorch的Linear模型作為我們自己的屬性,從而建立一個巢狀模型。
儘管這顯然是一個人為設計的示例,因為我們幾乎是在包裝底層模型,而沒有向其新增任何有用的東西,但它很好地說明了這個概念。
在
_init__
方法中,我們建立了一個包含巢狀線性模型的屬性。 在
forward()
方法中,我們呼叫巢狀模型本身來執行forward傳遞(注意,我們沒有呼叫
self。linear。forward(x)
)。
class LayerLinearRegression(nn。Module):
def __init__(self):
super()。__init__()
# Instead of our custom parameters, we use a Linear layer with single input and single output
self。linear = nn。Linear(1, 1)
def forward(self, x):
# Now it only takes a call to the layer to make predictions
return self。linear(x)
現在,如果我們呼叫這個模型的
parameters()
方法,PyTorch將以遞迴方式顯示其屬性的引數。您可以使用類似於
[*LayerLinearRegression()。parameters()]
的方法來獲得所有引數的列表。你還可以新增新的線性屬性,即使在前向傳遞中根本不使用它們,它們仍然會在
parameters()
下列出。
順序模型
我們的模型非常簡單……你可能會想:“為什麼要為它構建一個類呢?”“
對於使用普通層的簡單模型,其中一層的輸出按順序作為下一層的輸入,我們可以使用Sequential模型。
在我們的例子中,我們將使用單個引數構建一個序列模型,即我們用來訓練線性迴歸的線性層。模型應該是這樣的:
# Alternatively, you can use a Sequential model
model = nn。Sequential(nn。Linear(1, 1))。to(device)
非常簡單。
訓練步驟
到目前為止,我們已經定義了最佳化器、損失函式和模型。向上滾動一點,快速檢視迴圈中的程式碼。如果我們使用不同的最佳化器,或者損失,甚至模型,它會改變嗎?如果不是,我們如何使它更通用?
好吧,我想我們可以說所有這些程式碼行執行一個訓練步驟,給定這三個元素(最佳化器、損失和模型)、特性和標籤。
那麼,如何編寫一個函式來獲取這三個元素並返回另一個函式來執行一個訓練步驟,將一組特性和標籤作為引數並返回相應的損失呢?
然後,我們可以使用這個通用函式來構建一個
train_step()
函式,以便在訓練迴圈中呼叫。現在我們的程式碼應該是這樣的……看到訓練迴圈有多小?
def make_train_step(model, loss_fn, optimizer):
# Builds function that performs a step in the train loop
def train_step(x, y):
# Sets model to TRAIN mode
model。train()
# Makes predictions
yhat = model(x)
# Computes loss
loss = loss_fn(y, yhat)
# Computes gradients
loss。backward()
# Updates parameters and zeroes gradients
optimizer。step()
optimizer。zero_grad()
# Returns the loss
return loss。item()
# Returns the function that will be called inside the train loop
return train_step
# Creates the train_step function for our model, loss function and optimizer
train_step = make_train_step(model, loss_fn, optimizer)
losses = []
# For each epoch。。。
for epoch in range(n_epochs):
# Performs one train step and returns the corresponding loss
loss = train_step(x_train_tensor, y_train_tensor)
losses。append(loss)
# Checks model’s parameters
print(model。state_dict())
暫時把注意力放在我們的資料上……到目前為止,我們只是簡單地使用了由Numpy陣列轉換而來的PyTorch張量。但我們可以做得更好,我們可以建立一個Pytorch張量資料。
資料集
在PyTorch中,dataset由一個常規的Python類表示,該類繼承自dataset類。你可以將它的睦作一種Python元組列表,每個元組對應於一個數據點(特性,標籤)。
它需要實現的最基本的方法是:
__init__(self)
:它採取任何引數需要建立一個元組列表-它可能是一個名稱的CSV檔案,將載入和處理;它可以是兩個張量,一個代表特徵,另一個代表標籤;或者其他的,取決於手頭的任務。
不需要在建構函式方法中載入整個資料集。如果資料集很大(例如,成千上萬的影象檔案),立即載入它將是記憶體效率不高的。建議按需載入它們(無論何時呼叫了
_get_item__
)。
_get_item__(self, index)
:它允許資料集被索引,因此它可以像列表一樣工作(dataset)——它必須返回與請求的資料點對應的元組(特性,標籤)。我們可以返回預先載入的資料集或張量的相應切片,或者,如前所述,按需載入它們(如本例中所示)。
__len__(self)
:它應該簡單地返回整個資料集的大小,這樣,無論什麼時候取樣它,它的索引都被限制在實際大小。
讓我們構建一個簡單的自定義資料集,它接受兩個張量作為引數:一個用於特性,一個用於標籤。對於任何給定的索引,我們的資料集類將返回每個張量的對應切片。它應該是這樣的:
from torch。utils。data import Dataset, TensorDataset
class CustomDataset(Dataset):
def __init__(self, x_tensor, y_tensor):
self。x = x_tensor
self。y = y_tensor
def __getitem__(self, index):
return (self。x[index], self。y[index])
def __len__(self):
return len(self。x)
# Wait, is this a CPU tensor now? Why? Where is 。to(device)?
x_train_tensor = torch。from_numpy(x_train)。float()
y_train_tensor = torch。from_numpy(y_train)。float()
train_data = CustomDataset(x_train_tensor, y_train_tensor)
print(train_data[0])
train_data = TensorDataset(x_train_tensor, y_train_tensor)
print(train_data[0])
再一次,你可能會想“為什麼要在一個類中經歷這麼多麻煩來包裝幾個張量呢?”如果一個數據集只是兩個張量,那麼我們可以使用PyTorch的TensorDataset類,它將完成我們在上面的自定義資料集中所做的大部分工作。
你注意到我們用Numpy陣列構建了我們的訓練張量,但是我們沒有將它們傳送到裝置上嗎?所以,它們現在是CPU張量!為什麼?
我們不希望我們的全部訓練資料都被載入到GPU張量中,就像我們到目前為止的例子中所做的那樣,因為它佔用了我們寶貴的顯示卡RAM中的空間。
構建資料集的作用是因為我們想用。
DataLoader
到目前為止,我們在每個訓練步驟都使用了全部的訓練資料。一直以來都是批次梯度下降。
這對於我們的小得可笑的資料集來說當然很好,但是對於一些大的資料集,我們必須使用小批次梯度下降。因此,我們需要小批次。因此,我們需要相應地分割資料集。
因此我們使用PyTorch的DataLoader類來完成這項工作。我們告訴它使用哪個資料集(我們在前一節中剛剛構建的資料集)、所需的mini-batch處理大小,以及我們是否希望對其進行洗牌。
我們的載入器將表現得像一個迭代器,因此我們可以迴圈它並每次獲取不同的mini-batch批處理。
from torch。utils。data import DataLoader
train_loader = DataLoader(dataset=train_data, batch_size=16, shuffle=True)
要檢索一個mini-batch批處理示例,只需執行下面的命令—它將返回一個包含兩個張量的列表,一個用於特徵,另一個用於標籤。
next(iter(train_loader))
重新看一下訓練迴圈,看一下這些是如何對迴圈做出改變的,我們來看看。
losses = []
train_step = make_train_step(model, loss_fn, optimizer)
for epoch in range(n_epochs):
for x_batch, y_batch in train_loader:
# the dataset “lives” in the CPU, so do our mini-batches
# therefore, we need to send those mini-batches to the
# device where the model “lives”
x_batch = x_batch。to(device)
y_batch = y_batch。to(device)
loss = train_step(x_batch, y_batch)
losses。append(loss)
print(model。state_dict())
現在有兩件事不同了:我們不僅有一個內部迴圈來從DataLoader載入每個mini-batch批處理,而且更重要的是,我們現在只向裝置傳送一個mini-batch批處理。
對於更大的資料集,使用Dataset的
_get_item__
將一個樣本一個樣本地載入(到一個CPU張量中),然後將屬於同一小批處理的所有樣本一次性發送到你的GPU(裝置)是為了充分利用你的顯示卡RAM的方法。
此外,如果有許多gpu來訓練您的模型,那麼最好保持資料集“不可知”,並在訓練期間將這些批分配給不同的gpu。
到目前為止,我們只關注訓練資料。我們為它建立了一個數據集和一個數據載入器。我們可以對驗證資料做同樣的事情,使用我們在這篇文章開始時執行的分割…或者我們可以使用
random_split
。
隨機分割
PyTorch的
random_split()
方法是執行訓練驗證分離的一種簡單而熟悉的方法。請記住,在我們的示例中,我們需要將它應用到整個資料集(而不是我們在前兩節中構建的培訓資料集)。
然後,對於每個資料子集,我們構建一個相應的DataLoader,因此我們的程式碼如下:
from torch。utils。data。dataset import random_split
x_tensor = torch。from_numpy(x)。float()
y_tensor = torch。from_numpy(y)。float()
dataset = TensorDataset(x_tensor, y_tensor)
train_dataset, val_dataset = random_split(dataset, [80, 20])
train_loader = DataLoader(dataset=train_dataset, batch_size=16)
val_loader = DataLoader(dataset=val_dataset, batch_size=20)
現在,我們的驗證集有了一個數據載入器。
評價
我們需要更改訓練迴圈,以包括對模型的評估,即計算驗證損失。第一步是包含另一個內部迴圈來處理來自驗證載入程式的mini-batch,將它們傳送到與我們的模型相同的裝置。接下來,我們使用模型進行預測,並計算相應的損失。
差不多了,但有兩件小事需要考慮:
torch_grad()
:雖然在我們的簡單模型中沒有什麼不同,但是使用這個上下文管理器來包裝驗證內部迴圈是一個很好的實踐,這樣可以禁用您可能無意中觸發的任何梯度計算——梯度屬於訓練,而不是驗證步驟;
eval()
:它所做的唯一一件事就是將模型設定為評估模式(就像它的train()對手所做的那樣),這樣模型就可以根據某些操作(比如Dropout)調整自己的行為。
現在,我們的訓練是這種樣子的:
losses = []
val_losses = []
train_step = make_train_step(model, loss_fn, optimizer)
for epoch in range(n_epochs):
for x_batch, y_batch in train_loader:
x_batch = x_batch。to(device)
y_batch = y_batch。to(device)
loss = train_step(x_batch, y_batch)
losses。append(loss)
with torch。no_grad():
for x_val, y_val in val_loader:
x_val = x_val。to(device)
y_val = y_val。to(device)
model。eval()
yhat = model(x_val)
val_loss = loss_fn(y_val, yhat)
val_losses。append(val_loss。item())
print(model。state_dict())
總結
希望在完成本文中所有的程式碼後,你能夠更好地理解PyTorch官方教程,並更輕鬆地學習它。 歡迎關注會眾號:小金博士。
Reference:
Understanding PyTorch with an example: a step-by-step tutorial