天天看點

一文了解PyTorch:附代碼執行個體

前言

一文了解PyTorch:附代碼執行個體

最近在學習Pytorch,對于每個部分有大緻了解,但沒有整體的邏輯架構,這篇文章雖然是翻譯的,但有條理的帶大家認識了Pytorch構模組化型并進行訓練的一般步驟和流程,一步一步的将用Numpy搭建的邏輯回歸模型來通過Pytorch進行高效實作并訓練,其中不乏介紹一些基本子產品,比如資料加載器,模型建構基類,優化器等知識,值得一看。

一文了解PyTorch:附代碼執行個體

介紹

一文了解PyTorch:附代碼執行個體

PyTorch是增長最快的深度學習架構。PyTorch也非常具有Python風格,注重簡潔和實用。

此外,也有一些使用者說,使用PyTorch甚至可以改善健康。

一文了解PyTorch:附代碼執行個體
一文了解PyTorch:附代碼執行個體

動機

一文了解PyTorch:附代碼執行個體

網上有許多PyTorch教程,它的文檔非常完整和廣泛。那麼,為什麼要繼續閱讀這個循序漸進的教程呢?這份教程以一系列常見的例子為主從基本原理開始講解。進而使大家對PyTorch的了解更加直覺。本文除了這些之外,還将提供一些避免常見陷阱和錯誤的建議。這份教程内容比較多,是以,為了便于查閱,建立目錄如下:

一文了解PyTorch:附代碼執行個體

目錄

一文了解PyTorch:附代碼執行個體
  • 一個簡單的回歸問題
  • 梯度下降法
  • Numpy中的線性回歸
  • PyTorch
  • Autograd
  • 動态計算圖
  • 優化器
  • 損失
  • 模型
  • 資料集
  • DataLoader
  • 評價
一文了解PyTorch:附代碼執行個體

一個簡單的回歸問題

一文了解PyTorch:附代碼執行個體

大多數教程都是從一些漂亮的圖像分類問題開始,以說明如何使用PyTorch。但是這容易讓人偏離原來的目标。

是以本教程從一個簡單的回歸問題開始。線性回歸模型可表示成如下形式:

一文了解PyTorch:附代碼執行個體

很多人認為回歸模型就是線性回歸,但是不是這樣的,回歸代表你的模型結果是一個或多個連續值。

一文了解PyTorch:附代碼執行個體

資料生成

一文了解PyTorch:附代碼執行個體

讓我們開始生成一些合成資料:我們從特征x的100個點的向量開始,然後使用a = 1, b = 2和一些高斯噪聲建立我們的标簽。

接下來,讓我們将合成資料分解為訓練集和驗證集,打亂索引數組并使用前80個打亂的點進行訓練。

# Data Generationnp.random.seed(42)x = np.random.rand(100, 1)y = 1 + 2 * x + .1 * np.random.randn(100, 1)# Shuffles the indicesidx = np.arange(100)np.random.shuffle(idx)# Uses first 80 random indices for traintrain_idx = idx[:80]# Uses the remaining indices for validationval_idx = idx[80:]# Generates train and validation setsx_train, y_train = x[train_idx], y[train_idx]x_val, y_val = x[val_idx], y[val_idx           

複制

一文了解PyTorch:附代碼執行個體

我們知道a = 1 b = 2,但是現在讓我們看看如何使用梯度下降和訓練集中的80個點來接近真實值的。

一文了解PyTorch:附代碼執行個體

梯度下降法

一文了解PyTorch:附代碼執行個體

關于梯度下降的内部運作機制,前面有篇文章來專門說明。這裡隻簡單介紹梯度下降的四個基本步驟。

步驟1:計算損失

對于回歸問題,損失由均方誤差(MSE)給出,即标簽(y)和預測(a + bx)之間所有平方誤差的平均值。

值得一提的是,如果我們使用訓練集(N)中的所有點來計算損失,我們是在執行批量梯度下降。如果我們每次都用一個點,那就是随機梯度下降法。在1和n之間的任何其他(n)都是小批量梯度下降的特征。

一文了解PyTorch:附代碼執行個體

步驟2:計算梯度

梯度是多元函數的所有偏導數構成的向量,我們有兩個參數,a和b,是以我們必須計算兩個偏導。導數告訴你,當你稍微改變某個量時,這個量的變化量是多少。在我們的例子中,當我們改變兩個參數中的一個時,我們的MSE損失變化了多少?

一文了解PyTorch:附代碼執行個體

步驟3:更新參數

在最後一步,我們使用梯度來更新參數。因為我們試圖最小化我們的損失,是以我們反轉了更新的梯度符号。

還需要考慮另一個參數:學習率,用希臘字母eta表示(看起來像字母n),這是我們需要對梯度進行參數更新的乘法因子,在程式裡通常簡化為lr.

一文了解PyTorch:附代碼執行個體

關于如何選擇合适的學習率,這是一個需要大量實踐的内容,學習率不能太大,也不能太小。

一文了解PyTorch:附代碼執行個體

第四步:重複。

現在,我們使用更新的參數傳回步驟1并重新啟動流程。

對于批量梯度下降,這是微不足道的,因為它使用所有的點來計算損失-一個輪次等于一個更新。對于随機梯度下降,一個epoch意味着N次更新,而對于小批量(大小為N),一個epoch有N/n次更新。

簡單地說,反複地重複這個過程就是在訓練一個模型。

一文了解PyTorch:附代碼執行個體

Numpy中的線性回歸

一文了解PyTorch:附代碼執行個體

接下來就是使用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" randomlynp.random.seed(42)a = np.random.randn(1)b = np.random.randn(1)
print(a, b)
# Sets learning ratelr = 1e-1# Defines number of epochsn_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 LinearRegressionlinr = 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:附代碼執行個體

PyTorch

一文了解PyTorch:附代碼執行個體

首先,我們需要介紹一些基本概念。

在深度學習中,張量無處不在。嗯,谷歌的架構被稱為TensorFlow是有原因的,那到底什麼是張量?

一文了解PyTorch:附代碼執行個體

張量

一文了解PyTorch:附代碼執行個體

張量(tensor)是多元數組,目的是把向量、矩陣推向更高的次元。

一文了解PyTorch:附代碼執行個體

一個标量(一個數字)有0維,一個向量有1維,一個矩陣有2維,一個張量有3維或更多。但是,為了簡單起見,我們通常也稱向量和矩陣為張量。

一文了解PyTorch:附代碼執行個體
一文了解PyTorch:附代碼執行個體

加載資料,裝置和CUDA

一文了解PyTorch:附代碼執行個體

你可能會問:“我們如何從Numpy的數組過渡到PyTorch的張量?”這就是from_numpy的作用。它傳回一個CPU張量。

如何要使用GPU,那麼它會把張量發送到GPU上面。“如果我想讓我的代碼回退到CPU,如果沒有可用的GPU ?”你可以使用cuda.is_available()來找出你是否有一個GPU供你使用,并相應地設定你的裝置。當然還可以使用float()輕松地将其轉換為較低精度(32位浮點數)。

import torchimport torch.optim as optimimport torch.nn as nnfrom 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 devicex_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張量。

一文了解PyTorch:附代碼執行個體

建立參數

一文了解PyTorch:附代碼執行個體

如何區分用于資料的張量(就像我們剛剛建立的那些)和用作(可訓練的)參數/權重的張量?

後一個張量需要計算它的梯度,是以我們可以更新它們的值(即參數的值)。這就是

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 = TRUEa = 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張量。

# FIRSTtensor([-0.5531], requires_grad=True)tensor([-0.7314], requires_grad=True)           

複制

在第二段代碼中,我們嘗試了将它們發送到我們的GPU的簡單方法。我們成功地将它們發送到另一個裝置上,但是我們不知怎麼地“丢失”了梯度……

# SECONDtensor([0.5158], device='cuda:0', grad_fn=<CopyBackwards>) tensor([0.0246], device='cuda:0', grad_fn=<CopyBackwards>)           

複制

在第三塊中,我們首先将張量發送到裝置,然後使用

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如何處理它們。

一文了解PyTorch:附代碼執行個體

Autograd

一文了解PyTorch:附代碼執行個體

Autograd是PyTorch的自動微分包。

那麼,我們如何讓PyTorch完成它的任務并計算所有的梯度呢?這就是

backward()

的好處。

還記得計算梯度的起點嗎?這是loss。是以,我們需要從相應的Python變量中調用

backward()

方法,比如,

loss. backwards()

那麼梯度的實際值呢?我們可以通過觀察張量的grad屬性來考察它們。

如果你檢視該方法的文檔,就會清楚地看到漸變是累積的。是以,每次我們使用梯度來更新參數時,我們都需要在之後将梯度歸零。這就是

zero_()

的好處。

是以,讓我們抛棄手工計算梯度的方法,同時使用

backward()

zero_()

方法。就這些嗎? 嗯,差不多…但是,總是有一個陷阱,這一次它與參數的更新有關…

lr = 1e-1n_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 ATTEMPTtensor([0.7518], device='cuda:0', grad_fn=<SubBackward0>)AttributeError: 'NoneType' object has no attribute 'zero_'           

複制

然後,我們稍微更改一下,在第二次嘗試中使用熟悉的就地Python指派。而且,PyTorch再一次抱怨它并提出一個錯誤。

# SECOND ATTEMPTRuntimeError: 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 ATTEMPTtensor([1.0235], device='cuda:0', requires_grad=True)tensor([1.9690], device='cuda:0', requires_grad=True)           

複制

一文了解PyTorch:附代碼執行個體

動态計算圖

一文了解PyTorch:附代碼執行個體

目前神經網絡架構分為靜态圖架構和動态圖架構,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_tensorerror = y_train_tensor - yhatloss = (error ** 2).mean()           

複制

如果我們調用

make_dot(yhat)

,我們将得到下面圖中最左邊的圖形:

一文了解PyTorch:附代碼執行個體

讓我們仔細看看它的組成部分:

  • 藍方框:這些對應于我們用作參數的張量,也就是我們要求PyTorch計算梯度的張量;
  • 灰箱:包含梯度計算張量或其相依關系的Python操作;
  • 綠色方框:與灰色方框相同,隻是它是漸變計算的起點(假設使用reverse()方法從用于可視化圖形的變量中調用)——它們是從圖形中的自底向上計算的。

如果我們為error(中間)和loss(右邊)變量繪制圖形,那麼它們與第一個變量之間的惟一差別就是中間步驟的數量(灰色框)。

現在,仔細看看最左邊的綠色方框:有兩個箭頭指向它,因為它将兩個變量a和b*x相加。

然後,看一下同一圖形的灰框:它執行的是乘法,即b*x。但是隻有一個箭頭指向它!箭頭來自于對應于參數b的藍色方框。

為什麼我們沒有資料x的方框呢?答案是:我們不為它計算梯度!是以,即使計算圖所執行的操作涉及到更多的張量,也隻顯示了梯度計算張量及其依賴關系。

如果我們将參數a的

requires_grad

設為False,計算圖形會發生什麼變化?

一文了解PyTorch:附代碼執行個體

不出所料,與參數a對應的藍色框是no more!很簡單:沒有梯度,沒有圖形。

動态計算圖最好的地方在于你可以讓它變得像你想要的那樣複雜。甚至可以使用控制流語句(例如,if語句)來控制梯度流(顯然!)

下面的圖顯示了一個示例。

一文了解PyTorch:附代碼執行個體
一文了解PyTorch:附代碼執行個體

優化器

一文了解PyTorch:附代碼執行個體

到目前為止,我們一直在使用計算出的梯度手動更新參數。這對于兩個參數來說可能很好,但是如果我們有很多參數呢?我們使用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-1n_epochs = 1000
# Defines a SGD optimizer to update the parametersoptimizer = 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, btensor([0.6226], device='cuda:0', requires_grad=True) tensor([1.4505], device='cuda:0', requires_grad=True)# AFTER: a, btensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)           

複制

一文了解PyTorch:附代碼執行個體

損失

一文了解PyTorch:附代碼執行個體

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-1n_epochs = 1000
# Defines a MSE loss functionloss_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:附代碼執行個體

模型

一文了解PyTorch:附代碼執行個體

在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 devicemodel = ManualLinearRegression().to(device)# We can also inspect its parameters using its state_dictprint(model.state_dict())
lr = 1e-1n_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:附代碼執行個體

嵌套模型

一文了解PyTorch:附代碼執行個體

在我們的模型中,我們手動建立了兩個參數來執行線性回歸。讓我們使用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()

下列出。

一文了解PyTorch:附代碼執行個體

順序模型

一文了解PyTorch:附代碼執行個體

我們的模型非常簡單……你可能會想:“為什麼要為它建構一個類呢?”“

對于使用普通層的簡單模型,其中一層的輸出按順序作為下一層的輸入,我們可以使用Sequential模型。

在我們的例子中,我們将使用單個參數建構一個序列模型,即我們用來訓練線性回歸的線性層。模型應該是這樣的:

# Alternatively, you can use a Sequential modelmodel = nn.Sequential(nn.Linear(1, 1)).to(device)           

複制

非常簡單。

一文了解PyTorch:附代碼執行個體

訓練步驟

一文了解PyTorch:附代碼執行個體

到目前為止,我們已經定義了優化器、損失函數和模型。向上滾動一點,快速檢視循環中的代碼。如果我們使用不同的優化器,或者損失,甚至模型,它會改變嗎?如果不是,我們如何使它更通用?

好吧,我想我們可以說所有這些代碼行執行一個訓練步驟,給定這三個元素(優化器、損失和模型)、特性和标簽。

那麼,如何編寫一個函數來擷取這三個元素并傳回另一個函數來執行一個訓練步驟,将一組特性和标簽作為參數并傳回相應的損失呢?

然後,我們可以使用這個通用函數來建構一個

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 optimizertrain_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 parametersprint(model.state_dict())           

複制

暫時把注意力放在我們的資料上……到目前為止,我們隻是簡單地使用了由Numpy數組轉換而來的PyTorch張量。但我們可以做得更好,我們可以建立一個Pytorch張量資料。

一文了解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中的空間。

建構資料集的作用是因為我們想用。

一文了解PyTorch:附代碼執行個體

DataLoader

一文了解PyTorch:附代碼執行個體

到目前為止,我們在每個訓練步驟都使用了全部的訓練資料。一直以來都是批量梯度下降。

這對于我們的小得可笑的資料集來說當然很好,但是對于一些大的資料集,我們必須使用小批量梯度下降。是以,我們需要小批量。是以,我們需要相應地分割資料集。

是以我們使用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:附代碼執行個體

随機分割

一文了解PyTorch:附代碼執行個體

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)           

複制

現在,我們的驗證集有了一個資料加載器。

一文了解PyTorch:附代碼執行個體

評價

一文了解PyTorch:附代碼執行個體

我們需要更改訓練循環,以包括對模型的評估,即計算驗證損失。第一步是包含另一個内部循環來處理來自驗證加載程式的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:附代碼執行個體

總結

一文了解PyTorch:附代碼執行個體

希望在完成本文中所有的代碼後,你能夠更好地了解PyTorch官方教程,并更輕松地學習它。

參考連結:

Understanding PyTorch with an example: a step-by-step tutorial

一文了解PyTorch:附代碼執行個體