PyTorch 03: 訓練神經網絡

我們在上個部分構建的神經網絡其實不太成熟,它還不能識別任何數字。具有非線性激活函數的神經網絡就像通用函數逼近器一樣。某些函數會將輸入映射到輸出。例如,將手寫數字圖像映射到類別概率。神經網絡的強大之處是我們可以訓練網絡以逼近這個函數,基本上只要提供充足的數據和計算時間,任何函數都可以逼近。
在這裏插入圖片描述

一開始網絡很樸素,不知道將輸入映射到輸出的函數。我們通過向網絡展示實際數據樣本訓練網絡,然後調整網絡參數,使其逼近此函數。

要得出這些參數,我們需要了解網絡預測真實輸出的效果如何。爲此,我們將計算損失函數(也稱爲成本),一種衡量預測錯誤的指標。例如,迴歸問題和二元分類問題經常使用均方損失

=12nin(yiy^i)2 \large \ell = \frac{1}{2n}\sum_i^n{\left(y_i - \hat{y}_i\right)^2}

其中 nn 是訓練樣本的數量,yiy_i 是真實標籤,
y^i\hat{y}_i 是預測標籤。

通過儘量減小相對於網絡參數的這一損失,我們可以找到損失最低且網絡能夠以很高的準確率預測正確標籤的配置。我們使用叫做梯度下降法的流程來尋找這一最低值。梯度是損失函數的斜率,指向變化最快的方向。.要以最短的時間找到最低值,我們需要沿着梯度(向下)前進。可以將這一過程看做沿着最陡的路線下山。
在這裏插入圖片描述

反向傳播

對於單層網絡,梯度下降法實現起來很簡單。但是,對於更深、層級更多的神經網絡(例如我們構建的網絡),梯度下降法實現起來更復雜,以至於研究人員花費了30年才弄明白如何訓練多層網絡。

我們通過反向傳播來實現,實際上是採用的微積分中的鏈式法則。最簡單的理解方法是將兩層網絡轉換爲圖形表示法。
在這裏插入圖片描述

在網絡的前向傳遞過程中,數據和運算從下到上執行。我們使輸入 xx 經過線性轉換 L1L_1,權重爲 W1W_1,偏差爲 b1b_1。然後,輸出經過 S 型運算 SS 和另一個線性轉換 L2L_2。最後計算損失 \ell。我們使用損失來衡量網絡預測的成熟程度。我們的目標是通過調整權重和偏差,使損失最小化。

要用梯度下降法訓練權重,我們使損失梯度在網絡中反向傳播。每個運算在輸入和輸出之間都具有某個梯度。當我們反向傳播梯度時,我們用傳入梯度乘以運算的梯度。從數學角度來講,其實就是使用鏈式法則計算相對於權重的損失梯度。

W1=L1W1SL1L2SL2 \large \frac{\partial \ell}{\partial W_1} = \frac{\partial L_1}{\partial W_1} \frac{\partial S}{\partial L_1} \frac{\partial L_2}{\partial S} \frac{\partial \ell}{\partial L_2}

**注意:**要充分掌握這部分內容,你需要懂一些向量微積分。

我們使用此梯度和學習速率 α\alpha 更新權重。

W1=W1αW1 \large W^\prime_1 = W_1 - \alpha \frac{\partial \ell}{\partial W_1}

設置學習速率 α\alpha ,讓讓權重更新的步長降爲很小的值
,使迭代方法能達到最小值。

損失

我們首先看看如何用 PyTorch 計算損失。PyTorch 通過 nn 模塊提供了損失函數,例如交叉熵損失 (nn.CrossEntropyLoss)。通常損失賦值給 criterion。正如在上一部分提到的,對於 MNIST 等分類問題,我們使用 softmax 函數預測類別概率。對於 softmax 輸出,你需要使用交叉熵損失函數。要實際計算損失,首先需要定義條件,然後傳入網絡輸出和正確標籤。

來,劃重點! nn.CrossEntropyLoss 的文檔 裏寫道:

This criterion combines nn.LogSoftmax() and nn.NLLLoss() in one single class.

The input is expected to contain scores for each class.

這就是說,我們需要將網絡的原始輸出(而不是 softmax 函數的輸出)傳入損失函數中。這個原始輸出通常稱爲對數分數。之所以使用對數,是因爲 softmax 生成的概率通常很接近 0 或 1,但是浮點數不能準確地表示接近 0 或 1 的值(詳情請參閱此處)。通常建議不要對概率進行運算,我們一般使用對數概率。

import torch
from torch import nn
import torch.nn.functional as F
from torchvision import datasets, transforms

# Define a transform to normalize the data
transform = transforms.Compose([transforms.ToTensor(),
                                transforms.Normalize((0.5,), (0.5,)),
                              ])
# Download and load the training data
trainset = datasets.MNIST('~/.pytorch/MNIST_data/', download=True, train=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=64, shuffle=True)

注意

如果你不熟悉 nn.Sequential ,請先完成 Part 2 notebook。

# Get our data
images, labels = next(iter(trainloader))
# Build a feed-forward network
model = nn.Sequential(nn.Linear(784, 128),
                      nn.ReLU(),
                      nn.Linear(128, 64),
                      nn.ReLU(),
                      nn.Linear(64, 10))

# Define the loss
criterion = nn.CrossEntropyLoss()

# Get our data
images, labels = next(iter(trainloader))
print(images.shape)
# Flatten images
images = images.view(images.shape[0], -1)

# Forward pass, get our logits
logits = model(images)
print("images.shape:{},labels.shape:{},logits.shape:{}".format(images.shape,labels.shape,logits.shape))
# Calculate the loss with the logits and the labels
loss = criterion(logits, labels)

print(loss)
torch.Size([64, 1, 28, 28])
images.shape:torch.Size([64, 784]),labels.shape:torch.Size([64]),logits.shape:torch.Size([64, 10])
tensor(2.3260, grad_fn=<NllLossBackward>)

在我看來,使用 nn.LogSoftmaxF.log_softmax文檔)構建具有 log-softmax 輸出的模型更方便。然後我們可以通過計算指數 torch.exp(output) 獲得實際概率。對於 log-softmax 輸出,你需要使用負對數似然損失 nn.NLLLoss文檔)。

**練習:**請構建一個返回 log-softmax 輸出結果並使用負對數似然損失計算損失的模型。注意,對於 nn.LogSoftmaxF.log_softmax,你需要相應地設置 dim 關鍵字參數。dim=0 會計算各行的 softmax,使每列的和爲 1,而 dim=1 會計算各列的 softmax,使每行的和爲 1。思考下你希望輸出是什麼,並選擇恰當的 dim

# Build a feed-forward network
model = nn.Sequential(nn.Linear(784, 128),
                      nn.ReLU(),
                      nn.Linear(128, 64),
                      nn.ReLU(),
                      nn.Linear(64, 10))

# Define the loss,交叉熵損失函數
criterion = nn.CrossEntropyLoss()

# Get our data
images, labels = next(iter(trainloader))

# Flatten images
images = images.view(images.shape[0], -1)

# Forward pass, get our logits
logits = model(images)
print("images.shape:{},labels.shape:{},logits.shape:{}".format(images.shape,labels.shape,logits.shape))

# Calculate the loss with the logits and the labels
loss = criterion(logits, labels)

print(loss)
images.shape:torch.Size([64, 784]),labels.shape:torch.Size([64]),logits.shape:torch.Size([64, 10])
tensor(2.3040, grad_fn=<NllLossBackward>)

Autograd 自動計算梯度

我們已經知道如何計算損失,那麼如何使用損失進行反向傳播呢?Torch 提供了模塊 autograd,用於自動計算張量的梯度。我們可以使用它計算所有參數相對於損失的梯度。Autograd 的計算方式是跟蹤對張量執行的運算,然後反向經過這些運算並一路計算梯度。爲了確保 PyTorch 能跟蹤對張量執行的運算並計算梯度,你需要在張量上設置 requires_grad = True。你可以在創建時使用 requires_grad 關鍵字或隨時使用 x.requires_grad_(True)

你可以使用 torch.no_grad() 關閉某段代碼的梯度:

x = torch.zeros(1, requires_grad=True)
>>> with torch.no_grad():
...     y = x * 2
>>> y.requires_grad
False
False

此外,還可以使用 torch.set_grad_enabled(True|False) 關閉全部梯度。

我們通過 z.backward() 計算相對於某個變量 z 的梯度。這樣會反向經過創建 z 的運算。

x = torch.randn(2,2, requires_grad=True)
print(x)
tensor([[-1.3505,  1.3856],
        [-1.1188, -0.0365]], requires_grad=True)
y = x**2
print(y)
tensor([[1.8238e+00, 1.9199e+00],
        [1.2517e+00, 1.3356e-03]], grad_fn=<PowBackward0>)

下面是創建 y 的運算,它是一個冪運算 PowBackward0

## grad_fn shows the function that generated this variable
print(y.grad_fn)
<PowBackward0 object at 0x7f1a390d03c8>

autgrad 模塊會跟蹤這些運算,並知道如何計算每個運算的梯度。這樣的話,它就能夠計算一系列運算相對於任何一個張量的梯度。我們將張量 y 縮減爲一個標量值 - 均值。

z = y.mean()
print(z)
tensor(1.2492, grad_fn=<MeanBackward0>)

你可以查看 xy 的梯度,但是現在它們是空的。

print(x.grad)
None

要計算梯度,你需要對變量 z 等運行 .backward 方法。這樣會計算 z 相對於 x 的梯度

zx=x[1ninxi2]=x2 \frac{\partial z}{\partial x} = \frac{\partial}{\partial x}\left[\frac{1}{n}\sum_i^n x_i^2\right] = \frac{x}{2}

z.backward()
print(x.grad)
print(x/2)
tensor([[-0.6752,  0.6928],
        [-0.5594, -0.0183]])
tensor([[-0.6752,  0.6928],
        [-0.5594, -0.0183]], grad_fn=<DivBackward0>)

這些梯度計算對於神經網絡來說特別有用。在訓練過程中,我們需要計算權重相對於成本的梯度。對於 PyTorch,我們通過網絡向前運行數據來計算損失,然後向後計算與成本相關的梯度。算出梯度後,我們可以執行梯度下降步驟。

損失和 Autograd

使用 PyTorch 創建網絡時,所有參數都通過 requires_grad = True 初始化。這意味着,當我們計算損失和調用 loss.backward() 時,會計算參數的梯度。這些梯度用於在梯度下降步驟中更新權重。下面是使用反向傳播計算梯度的示例。

# Build a feed-forward network
model = nn.Sequential(nn.Linear(784, 128),
                      nn.ReLU(),
                      nn.Linear(128, 64),
                      nn.ReLU(),
                      nn.Linear(64, 10),
                      nn.LogSoftmax(dim=1))

criterion = nn.NLLLoss()
images, labels = next(iter(trainloader))
images = images.view(images.shape[0], -1)

logits = model(images)
loss = criterion(logits, labels)
print('Before backward pass: \n', model[0].weight.grad)

loss.backward()

print('After backward pass: \n', model[0].weight.grad)
Before backward pass: 
 None
After backward pass: 
 tensor([[-0.0014, -0.0014, -0.0014,  ..., -0.0014, -0.0014, -0.0014],
        [ 0.0023,  0.0023,  0.0023,  ...,  0.0023,  0.0023,  0.0023],
        [-0.0002, -0.0002, -0.0002,  ..., -0.0002, -0.0002, -0.0002],
        ...,
        [ 0.0014,  0.0014,  0.0014,  ...,  0.0014,  0.0014,  0.0014],
        [-0.0005, -0.0005, -0.0005,  ..., -0.0005, -0.0005, -0.0005],
        [ 0.0003,  0.0003,  0.0003,  ...,  0.0003,  0.0003,  0.0003]])

訓練網絡

在開始訓練之前,最後還要定義優化器,優化器可以用於更新權重和梯度。我們將使用 PyTorch 的 optim 軟件包。例如,我們可以通過 optim.SGD 使用隨機梯度下降法。下面演示瞭如何定義優化器。

from torch import optim

# Optimizers require the parameters to optimize and a learning rate
optimizer = optim.SGD(model.parameters(), lr=0.01)

首先,在循環遍歷所有數據之前,我們只考慮一個學習步驟。PyTorch 的一般流程是:

  • 通過網絡進行正向傳遞以獲取logits
  • 使用 logits 計算損失
  • 通過 loss.backward() 對網絡進行反向傳遞以計算梯度
  • 使用優化器更新權重

下面我將完成一個訓練步驟並打印出權重和梯度注意我有一行代碼 optimizer.zero_grad()。使用相同的參數多次反向傳播時,梯度會累積。這意味着,你需要在每個訓練流程中使梯度歸零,否則會保留之前訓練批次的梯度。

print('Initial weights - ', model[0].weight)

images, labels = next(iter(trainloader))
images.resize_(64, 784)

# Clear the gradients, do this because gradients are accumulated
optimizer.zero_grad()

# Forward pass, then backward pass, then update weights
output = model.forward(images)
loss = criterion(output, labels)
loss.backward()
print('Gradient -', model[0].weight.grad)
Initial weights -  Parameter containing:
tensor([[-8.3243e-03,  2.2264e-02,  3.3838e-02,  ..., -2.3544e-03,
         -2.5620e-02,  2.5944e-02],
        [-3.4294e-03,  2.7606e-02, -2.6783e-02,  ..., -1.3735e-02,
         -2.4606e-04, -2.8678e-02],
        [ 3.3992e-02, -2.0662e-02,  1.5647e-03,  ..., -1.9074e-02,
          2.5691e-02,  2.6051e-02],
        ...,
        [ 1.2866e-02, -1.4100e-02, -6.5886e-03,  ..., -6.3746e-03,
          7.8963e-03,  2.7776e-02],
        [ 3.0193e-05, -1.4614e-02, -3.1672e-02,  ...,  3.2751e-02,
         -1.9365e-02,  2.7052e-02],
        [ 3.5314e-02, -1.0650e-02, -1.7924e-02,  ..., -5.1507e-03,
         -1.5575e-02,  1.1492e-02]], requires_grad=True)
Gradient - tensor([[-0.0010, -0.0010, -0.0010,  ..., -0.0010, -0.0010, -0.0010],
        [ 0.0034,  0.0034,  0.0034,  ...,  0.0034,  0.0034,  0.0034],
        [-0.0005, -0.0005, -0.0005,  ..., -0.0005, -0.0005, -0.0005],
        ...,
        [ 0.0018,  0.0018,  0.0018,  ...,  0.0018,  0.0018,  0.0018],
        [ 0.0010,  0.0010,  0.0010,  ...,  0.0010,  0.0010,  0.0010],
        [-0.0010, -0.0010, -0.0010,  ..., -0.0010, -0.0010, -0.0010]])
# Take an update step and few the new weights
optimizer.step()
print('Updated weights - ', model[0].weight)
Updated weights -  Parameter containing:
tensor([[-8.3146e-03,  2.2273e-02,  3.3848e-02,  ..., -2.3447e-03,
         -2.5610e-02,  2.5953e-02],
        [-3.4637e-03,  2.7571e-02, -2.6817e-02,  ..., -1.3769e-02,
         -2.8030e-04, -2.8712e-02],
        [ 3.3997e-02, -2.0657e-02,  1.5701e-03,  ..., -1.9069e-02,
          2.5697e-02,  2.6056e-02],
        ...,
        [ 1.2849e-02, -1.4117e-02, -6.6063e-03,  ..., -6.3923e-03,
          7.8785e-03,  2.7759e-02],
        [ 2.0352e-05, -1.4624e-02, -3.1682e-02,  ...,  3.2741e-02,
         -1.9375e-02,  2.7042e-02],
        [ 3.5324e-02, -1.0641e-02, -1.7914e-02,  ..., -5.1409e-03,
         -1.5565e-02,  1.1502e-02]], requires_grad=True)

實際訓練

現在,我們將此算法用於循環中,去訪問所有圖像。這裏介紹一個術語,循環訪問整個數據集一次稱爲 1 個週期。我們將循環 trainloader 來獲得訓練批次。對於每個批次,我們將進行一次訓練:計算損失、進行反向傳播並更新權重。

**練習:**請按照所說的訓練網絡。如果進展順利,你應該會看到每個週期結束後,訓練損失都下降了。

## Your solution here

model = nn.Sequential(nn.Linear(784, 128),
                      nn.ReLU(),
                      nn.Linear(128, 64),
                      nn.ReLU(),
                      nn.Linear(64, 10),
                      nn.LogSoftmax(dim=1))

criterion = nn.NLLLoss()
optimizer = optim.SGD(model.parameters(), lr=0.003)

epochs = 5
for e in range(epochs):
    running_loss = 0
    for images, labels in trainloader:
        # Flatten MNIST images into a 784 long vector
        images = images.view(images.shape[0], -1)
    
        # TODO: Training pass
        optimizer.zero_grad()
        output = model(images)
        loss = criterion(output, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
    else:
        print(f"Training loss: {running_loss/len(trainloader)}")
Training loss: 1.9747026377141095
Training loss: 0.8884982707213237
Training loss: 0.5204462490674021
Training loss: 0.42427545167934666
Training loss: 0.3813415475364433

訓練完網絡後,我們可以試試它是否能做出正確預測。

%matplotlib inline
import helper

images, labels = next(iter(trainloader))

img = images[0].view(1, 784)
# Turn off gradients to speed up this part
with torch.no_grad():
    logits = model.forward(img)

# Output of the network are logits, need to take softmax for probabilities
ps = F.softmax(logits, dim=1)
helper.view_classify(img.view(1, 28, 28), ps)

在這裏插入圖片描述
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-scBVLFa0-1589537329629)(output_31_0.png)]

太棒了!它能夠預測出圖像中的數字。接下來,我們會學習編寫代碼,用更復雜的數據集訓練神經網絡。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章