【實戰篇】PyTorch入門指南

本文是對 Neural Network Programming - Deep Learning with PyTorch 系列博客的翻譯與整理,英語基礎比較好的同學推薦閱讀原汁原味的博客。

計算機程序通常由兩個主要部分組成:代碼和數據。在傳統的編程中,程序員的工作是直接編寫軟件或代碼,但是在深度學習和神經網絡中,可以說軟件就是網絡本身,特別是在訓練過程中自動產生的網絡權值。數據是深度學習的主要組成部分,儘管讓我們的神經網絡從數據中學習是我們作爲神經網絡程序員的任務,但我們仍然有責任瞭解我們實際用於訓練的數據的性質和歷史。

1. 什麼是MNIST數據集

  MNIST數據集,全稱是 Modified National Institute of Standards and Technology database,它是一個著名的手寫數字數據集,通常用於訓練機器學習的圖像處理系統。NIST是國家標準與技術協會的縮寫,M 代表修改過的,這是因爲有一個原始的NIST數據集被修改爲MNIST。

  MNIST因其被使用的頻率而聞名,常見的原因有兩個:

  • 初學者使用它很容易上手

  • 研究人員使用它來基準化(比較)不同的模型。

  這個數據集包含 70,000 張手寫體圖片,並進行如下分割:

  • 60,000 張訓練圖片
  • 10,000 張測試圖片

  由於 MNIST 數據集對於深度學習來說,有點太簡單,所以後面有人創建了 Fashion-MNIST 數據集。

2. 什麼是Fashion-MNIST數據集

  顧名思義,Fashion-MNIST是一個關於時尚產品的數據集。具體來說,該數據集有以下十類時尚項目:

Index Label
0 T-shirt/top
1 Trouser
2 Pullover
3 Dress
4 Coat
5 Sandal
6 Shirt
7 Sneaker
8 Bag
9 Ankle boot

  數據集中的部分圖片如下所示:

  Fashion-MNIST 數據集來源於Zalando,該公司內部員工創建了此數據集,之所以名字中帶MNIST,是因爲他們想用Fashion-MNIST來代替MNIST,出於此原因,Fashion-MNIST 數據集被設計成儘可能接近原始MNIST數據集(60,000張訓練圖片,10,000張測試圖片,28 * 28的灰度圖),但是由於擁有比手寫圖像更復雜的數據而在訓練中引入更高的難度。

  該數據集被設計爲原始MNIST的完全替代,通過使Fashion-MNIST數據集規格與原始MNIST規格相匹配,可以順利地實現從舊規範到新規範的轉換。該論文聲稱,切換數據集所需的唯一更改是通過指向Fashion數據集來更改MNIST數據集的獲取位置的URL。

  PyTorch 提供的 torchvision 包,可以使我們更方便地導入 Fashion-MNIST數據集。

3. Extract, Transform, and Load (ETL) data

  機器學習/深度學習工程的第一步是準備數據,我們將遵循以下的 ETL 流程:

  • 從數據源提取(extract)數據
  • 將數據轉換(transform)爲期望格式
  • 把數據加載(load)到合適的結構中

  在我們的項目中,該過程分別對應爲:

  • Extract – 從數據源中獲取Fashion-MNIST圖像
  • Transform – 把數據轉換爲 tensor 的格式
  • Load – 將我們的數據放在DataLoader類的實例對象中,以便於訪問

  基於這些目的,PyTorch 提供了以下兩個類:

描述
torch.utils.data.Dataset 用於表示數據集的抽象類
torch.utils.data.DataLoader 包裝數據集並提供對基礎數據的訪問

  抽象類 是一個Python類,它裏面的方法我們必須要實現,我們可以通過創建一個子類來擴展Dataset類的功能,從而創建一個自定義數據集類,這個新的子類可以被傳遞到PyTorch的 DataLoader對象。

  我們將使用 torchvision 包內置的Fashion-MNIST數據集,因此我們的項目不必再重新創建一個新的子類,只需知道時尚MNIST內置的dataset類是在幕後完成這項工作的。

  torchvision 包允許我們訪問以下資源:

  • Datasets (like MNIST and Fashion-MNIST)
  • Models (like VGG16)
  • Transforms
  • Utils

  我們用下面代碼來獲取 Fashion-MNIST 數據集:

> train_set = torchvision.datasets.FashionMNIST(
    root='./data' # 數據集保持在硬盤中的路徑
    ,train=True # 是否爲訓練集
    ,download=True
    ,transform=transforms.Compose([transforms.ToTensor()]) # 轉換操作
)

  要爲我們的訓練集創建一個DataLoader包裝器,我們這樣做:

train_loader = torch.utils.data.DataLoader(
	train_set
    ,batch_size=1000
    ,shuffle=True
)

4. Dataset 和 DataLoader 的工作機制

  • PyTorch Dataset: Working with the training set

  我們先看一下,Dataset 的實例 train_set,有哪些可以執行的操作,來探索我們的數據。

> len(train_set) # 數據集的大小
60000

# Before torchvision 0.2.2
> train_set.train_labels 
tensor([9, 0, 0, ..., 3, 0, 5])

# Starting with torchvision 0.2.2
> train_set.targets 
tensor([9, 0, 0, ..., 3, 0, 5])

  如果我們想知道,數據集中每個標籤對應的樣本數量,調用bincount()方法:

# Before torchvision 0.2.2
> train_set.train_labels.bincount()
tensor([6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000])

# Starting with torchvision 0.2.2
> train_set.targets.bincount()
tensor([6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000, 6000])

  要訪問訓練集中的單個元素,我們首先將 train_set 對象傳遞給Python內置的iter()函數,該函數返回一個數據流對象,然後再用Python內置的next()函數來獲取數據流中的下一個元素。

> sample = next(iter(train_set))
> len(sample)
2

  我們看到返回的 sample 的長度爲2,那是因爲一個 sample 對象包含一個 <image, label> 對。

> type(image)
torch.Tensor

# Before torchvision 0.2.2
> type(label)
torch.Tensor

# Starting at torchvision 0.2.2
> type(label)
int

  我們可以再看看 image 和 label 的形狀:

> image.shape
torch.Size([1, 28, 28]) 

> torch.tensor(label).shape
torch.Size([])

> image.squeeze().shape
torch.Size([28, 28])
  • PyTorch DataLoader: Working with batches of data

  我們將開始創建一個批處理大小爲10的數據加載器:

> display_loader = torch.utils.data.DataLoader(
    train_set, batch_size=10
  )

  和前面從 train_set 中獲取一個數據實例一樣,我們從 display_loader 中獲取一個 batch 數據,也是通過調用 iter()next() 函數。

# note that each batch will be different when shuffle=True
> batch = next(iter(display_loader))
> print('len:', len(batch))
len: 2

  這裏 batch 的長度爲2是因爲 batch 由兩個張量組成:

> images, labels = batch

> print('types:', type(images), type(labels))
> print('shapes:', images.shape, labels.shape)
types: <class 'torch.Tensor'> <class 'torch.Tensor'>
shapes: torch.Size([10, 1, 28, 28]) torch.Size([10])

  如果想要繪製一個 batch 中的所有圖像,可以採用torchvision.utils.make_grid()函數,具體如下:

> grid = torchvision.utils.make_grid(images, nrow=10)

> plt.figure(figsize=(15,15))
> plt.imshow(np.transpose(grid, (1,2,0)))
> # plt.imshow(grid.permute(1,2,0)) # 和上面效果一樣

> print('labels:', labels)
labels: tensor([9, 0, 0, 3, 0, 2, 7, 2, 5, 5])

  現在我們瞭解了一些 prepare the data 的方法,接下來開始第二步
build the model

5. torch.nn 包

  在PyTorch中構建神經網絡,需要使用torch.nn包,這是PyTorch的神經網絡(nn)庫,我們通常是這樣導入包的:

import torch.nn as nn

  構建神經網絡所需的主要組件是layer,而PyTorch的神經網絡庫torch.nn 中包含一些類,可以幫助我們構建層。而神經網絡中的layer,主要包含兩個組件:

  • 轉換操作 (code)
  • 權重參數的集合 (data)

  在torch.nn包中,有一個類叫做Module,它是所有神經網絡模塊的基類,包括layer。這意味着PyTorch中的所有layer都擴展了nn.Module類,並繼承了PyTorch在nn.Module類中的所有內置功能。在OOP(面向對象編程)中,這個理念被稱爲繼承。

  當我們將一個張量作爲輸入傳遞給網絡時,張量通過每一層轉換向前流動,直到張量到達輸出層,張量通過網絡向前流動的過程稱爲向前傳遞,也因此, nn.module類中提供了一個forward()方法,每個繼承它的類,都必須實現這個方法,它其實也就是我們前面提到的轉換操作。

  當我們在具體實現 forward() 方法時,一般需要調用 nn.functional 包中提供的函數,這個包爲我們提供了許多可以用於構建層的神經網絡操作。

6. 構建一個神經網絡

  基於前面的學習,我們知道了構建一個網絡主要分爲下面幾步:

  • 創建一個繼承了 nn.Module 類的神經網絡類
  • 在該類的構造函數中,用torch.nn中預構的層來定義網絡層,作爲類屬性
  • 使用網絡層和nn.functional中的函數來定義 forward() 函數

  我們首先來看第一步,創建一個簡單的類來表示神經網絡:

class Network:
    def __init__(self):
        self.layer = None

    def forward(self, t):
        t = self.layer(t)
        return t

  我們的類要繼承 nn.Module,所以我們還要再做兩件事情:

class Network(nn.Module): # 1. 指定nn.Module類
    def __init__(self):
        super().__init__() # 2. 對父類構造函數的調用
        self.layer = None

    def forward(self, t):
        t = self.layer(t)
        return t

  這兩點小改變將我們簡單的神經網絡轉換爲PyTorch神經網絡,使得我們的 Network 類有了 nn.Module 類的所有函數。

  我們再來看第二步,定義網絡層作爲類屬性:

class Network(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
        
        self.fc1 = nn.Linear(in_features=12 * 4 * 4, out_features=120)
        self.fc2 = nn.Linear(in_features=120, out_features=60)
        self.out = nn.Linear(in_features=60, out_features=10)
        
    def forward(self, t):
        return t

   可以看到,在我們的 Network 類中,有五個層被定義爲屬性。我們有兩個卷積層self.conv1和self.conv2,以及三個線性層self.fc1、self.fc2和self.out。

  我們在fc1和fc2中使用縮寫fc,因爲linear layers也稱爲fully connected layers。它們還有第三個名字,叫做ldense layers。 這三種叫法都是指的同一類型的層,PyTorch使用單詞 linear,因此命名爲 nn.linear

7. 卷積神經網絡超參數

  我們的每一層都擴展了PyTorch的nn.Module類,所以每一層中都封裝了兩個部分,前向傳播函數和權重向量,例如下面的卷積層nn.Conv2d

self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)

  爲了更好的理解我們定義的層,我們來看看層的構造函數中所包含的參數值。當我們構造一個層時,我們需要將參數值傳遞給層的構造函數,在我們的卷積層中有三個參數,線性層中有兩個參數。

  • Convolutional layers
    • in_channels
    • out_channels
    • kernel_size
  • Linear layers
    • in_features
    • out_features

  我們先來看看需要程序員手動設定的超參數:

Paremeter Description
kernel_size 設置filter大小(filter和kernel含義相同)
out_channels 設置filter個數
out_features 設置輸出張量的大小

  還有一些超參數,它的設定依賴於我們的數據流。在self.conv1層中的超參數in_channels,它的值應該等於輸入圖像的顏色通道數;在其後的幾個卷積層的in_channels的值則需等於它上一層的out_channels;當我們從卷積層切換到全連接層時,我們需要 flatten 我們的 tensor,於是第一個全連接層的in_features的值爲124412 * 4 *4;最後到輸出層時,我們的數據集總共有10個類別,因此我們的輸出層的out_features的值應該爲10。

8. 卷積神經網絡可學習的參數

  可學習參數指的是在訓練過程中會不斷更新的參數,我們會給它們隨機初始化一些值,然後在每一輪的迭代中,更新這些值。那麼在我們前面設計的網絡中,這些可學習參數在哪呢?

class Network(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
        
        self.fc1 = nn.Linear(in_features=12*4*4, out_features=120)
        self.fc2 = nn.Linear(in_features=120, out_features=60)
        self.out = nn.Linear(in_features=60, out_features=10)
        
    def forward(self, t):
        # implement the forward pass
        return t

  我們的可學習參數,實際上就是神經網絡中的權重,而它們就位於我們定義的網絡層之中,我們先獲取一個Network類的實例,再來觀察我們的權重:

> network = Network()                                    

  當這段代碼執行時,類構造函數__init__(self)中的代碼將會被調用,我們定義的網絡層會被初始化,然後再返回一個網絡類的實例,在我們開始使用我們的 network 實例之前,我們先看看打印它會輸出什麼:

> print(network)
Network(
    (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
    (conv2): Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))
    (fc1): Linear(in_features=192, out_features=120, bias=True)
    (fc2): Linear(in_features=120, out_features=60, bias=True)
    (out): Linear(in_features=60, out_features=10, bias=True)
)

  我們接下來看看如何獲取我們定義的網絡層:

> network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))

> network.conv2
Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))

> network.fc1
Linear(in_features=192, out_features=120, bias=True)

> network.fc2                                    
Linear(in_features=120, out_features=60, bias=True)

> network.out
Linear(in_features=60, out_features=10, bias=True)

  下一步我們可以獲取網絡層的權重,通過 network.conv1.weight 這行代碼,它輸出的是一個 tensor.

> network.conv1.weight
Parameter containing:
tensor([[[[ 0.0692,  0.1029, -0.1793,  0.0495,  0.0619],
            [ 0.1860,  0.0503, -0.1270, -0.1240, -0.0872],
            [-0.1924, -0.0684, -0.0028,  0.1031, -0.1053],
            [-0.0607,  0.1332,  0.0191,  0.1069, -0.0977],
            [ 0.0095, -0.1570,  0.1730,  0.0674, -0.1589]]],

        [[[-0.1392,  0.1141, -0.0658,  0.1015,  0.0060],
            [-0.0519,  0.0341,  0.1161,  0.1492, -0.0370],
            [ 0.1077,  0.1146,  0.0707,  0.0927,  0.0192],
            [-0.0656,  0.0929, -0.1735,  0.1019, -0.0546],
            [ 0.0647, -0.0521, -0.0687,  0.1053, -0.0613]]],

        [[[-0.1066, -0.0885,  0.1483, -0.0563,  0.0517],
            [ 0.0266,  0.0752, -0.1901, -0.0931, -0.0657],
            [ 0.0502, -0.0652,  0.0523, -0.0789, -0.0471],
            [-0.0800,  0.1297, -0.0205,  0.0450, -0.1029],
            [-0.1542,  0.1634, -0.0448,  0.0998, -0.1385]]],

        [[[-0.0943,  0.0256,  0.1632, -0.0361, -0.0557],
            [ 0.1083, -0.1647,  0.0846, -0.0163,  0.0068],
            [-0.1241,  0.1761,  0.1914,  0.1492,  0.1270],
            [ 0.1583,  0.0905,  0.1406,  0.1439,  0.1804],
            [-0.1651,  0.1374,  0.0018,  0.0846, -0.1203]]],

        [[[ 0.1786, -0.0800, -0.0995,  0.1690, -0.0529],
            [ 0.0685,  0.1399,  0.0270,  0.1684,  0.1544],
            [ 0.1581, -0.0099, -0.0796,  0.0823, -0.1598],
            [ 0.1534, -0.1373, -0.0740, -0.0897,  0.1325],
            [ 0.1487, -0.0583, -0.0900,  0.1606,  0.0140]]],

        [[[ 0.0919,  0.0575,  0.0830, -0.1042, -0.1347],
            [-0.1615,  0.0451,  0.1563, -0.0577, -0.1096],
            [-0.0667, -0.1979,  0.0458,  0.1971, -0.1380],
            [-0.1279,  0.1753, -0.1063,  0.1230, -0.0475],
            [-0.0608, -0.0046, -0.0043, -0.1543,  0.1919]]]], 
            requires_grad=True
)

9. 權重張量的形狀

  站在卷積層的角度,權重張量就在我們設定的filter之中,而在代碼中,filter實際上就是權重張量自身。

  層內的卷積運算是指該層的所有輸入通道的feature map與該層的filter之間的運算,這意味着我們實際上進行的是兩個張量之間的運算。(一次卷積是某個卷積覈對所有輸入通道的同一個區域進行卷積,而不是單個輸入通道

  對於第一個卷積層,我們有1個顏色通道,用6個大小爲555*5的卷積核進行卷積,所以最後輸出的通道數也有6個。在PyTorch中,我們不會用6個權重張量來表示每個的filter,而是集中用一個權重張量來表示,注意每一個維度所代表的含義。

> network.conv1
Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))

> network.conv1.weight.shape
torch.Size([6, 1, 5, 5])

> network.conv2
Conv2d(6, 12, kernel_size=(5, 5), stride=(1, 1))

> network.conv2.weight.shape
torch.Size([12, 6, 5, 5])

  我們需要記住兩點:

  • 所有的filter都只用一個張量來表示
  • 每個filter都具有depth維,該值對應輸入的通道數

  在全連接層中,我們的輸入、輸出都是一階張量,所以我們只需要一個二階張量對它們進行轉換即可,二階張量也常被稱爲權重矩陣。

> network.fc1.shape
torch.Size([120, 192])

> network.fc2.shape                                    
torch.Size([60, 120])

> network.out.shape
torch.Size([10, 60])

  由於我們的輸入輸出都是列向量,我們所進行的運算爲:
zl=Wlal1z^l=W^la^{l-1}

  於是我們權重矩陣的shape,第一個元素值對應的是out_features,第二個元素值對應的是in_features,這一點我們需要理解。在PyTorch中,矩陣乘法用tensor.matmul()函數來表示:

> weight_matrix.matmul(in_features)
tensor([30., 40., 50.])

  最後一個問題是,如果想要一次性獲取到網絡中的所有參數,應該怎麼做呢?可以通過下面的代碼

for name, param in network.named_parameters():
    print(name, '\t\t', param.shape)

conv1.weight 	 torch.Size([6, 1, 5, 5])
conv1.bias 		 torch.Size([6])
conv2.weight 	 torch.Size([12, 6, 5, 5])
conv2.bias 		 torch.Size([12])
fc1.weight 		 torch.Size([120, 192])
fc1.bias 		 torch.Size([120])
fc2.weight 		 torch.Size([60, 120])
fc2.bias 		 torch.Size([60])
out.weight 		 torch.Size([10, 60])
out.bias 		 torch.Size([10])

10. torch.nn.Linear源碼分析

  我們先來看看如何用矩陣乘法,把輸入的特徵向量轉換爲輸出的特徵向量:

in_features = torch.tensor([1,2,3,4], dtype=torch.float32)

weight_matrix = torch.tensor([
    [1,2,3,4],
    [2,3,4,5],
    [3,4,5,6]
], dtype=torch.float32)

> weight_matrix.matmul(in_features)
tensor([30., 40., 50.])

  再來看如何用 nn.Linear類來實現上面的轉換:

> fc = nn.Linear(in_features=4, out_features=3, bias=False)

> fc.weight  # 隨機初始化的權重
Parameter containing:
tensor([[ 0.2845,  0.4056,  0.0574, -0.2942],
        [-0.1213, -0.2582, -0.1599,  0.3142],
        [-0.0050,  0.1562,  0.3690, -0.4962]], requires_grad=True)

  那權重矩陣是在哪裏生成的呢?不急,我們來看源碼進行分析:

# torch/nn/modules/linear.py (version 1.0.1)

def __init__(self, in_features, out_features, bias=True):
    super(Linear, self).__init__()
    self.in_features = in_features
    self.out_features = out_features
    self.weight = Parameter(torch.Tensor(out_features, in_features)) # 權重矩陣
    if bias:
        self.bias = Parameter(torch.Tensor(out_features))
    else:
        self.register_parameter('bias', None)
    self.reset_parameters()

  傳入一個特徵向量,查看輸出:

> in_features = torch.tensor([1,2,3,4], dtype=torch.float32)

> fc(in_features)
tensor([ 0.0912,  0.1394, -0.5704], grad_fn=<SqueezeBackward3>)

  我們發現一件事:PyTorch的神經網絡模塊是可以調用的 Python 對象! 關於這一點,我們稍後詳細說明。現在的問題是,線性層的輸出和我們上面的例子的輸出還是有差別的,這是因爲我們的權重矩陣是隨機初始化的,我們可以顯示地指定線性層的權重矩陣。

> fc.weight = nn.Parameter(weight_matrix)  
> fc(in_features)
tensor([30., 40., 50.], grad_fn=<SqueezeBackward3>)

  現在我們的輸出和矩陣乘法的結果是一致的了,接下來我們來分析,爲什麼PyTorch的神經網絡模塊像個函數一樣可以被調用(如 fc(in_features)),這是因爲 PyTorch 的模塊類,實現了 Python 中的另一個特殊函數__call__(),如果一個類實現了該方法,則只要調用對象實例,就會執行特殊的調用方法,我們再來看看源代碼:

# torch/nn/modules/module.py (version 1.0.1)

def __call__(self, *input, **kwargs):
    for hook in self._forward_pre_hooks.values():
        hook(self, input)
    if torch._C._get_tracing_state():
        result = self._slow_forward(*input, **kwargs)
    else:
        result = self.forward(*input, **kwargs)
    for hook in self._forward_hooks.values():
        hook_result = hook(self, input, result)
        if hook_result is not None:
            raise RuntimeError(
                "forward hooks should never return any values, but '{}'"
                "didn't return None".format(hook))
    if len(self._backward_hooks) > 0:
        var = result
        while not isinstance(var, torch.Tensor):
            if isinstance(var, dict):
                var = next((v for v in var.values() if isinstance(v, torch.Tensor)))
            else:
                var = var[0]
        grad_fn = var.grad_fn
        if grad_fn is not None:
            for hook in self._backward_hooks.values():
                wrapper = functools.partial(hook, self)
                functools.update_wrapper(wrapper, hook)
                grad_fn.register_hook(wrapper)
    return result

11. 實現前向傳播算法

  我們先回顧一下前面定義的Network類:

class Network(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels=1, out_channels=6, kernel_size=5)
        self.conv2 = nn.Conv2d(in_channels=6, out_channels=12, kernel_size=5)
        
        self.fc1 = nn.Linear(in_features=12*4*4, out_features=120)
        self.fc2 = nn.Linear(in_features=120, out_features=60)
        self.out = nn.Linear(in_features=60, out_features=10)
        
    def forward(self, t):
        # implement the forward pass
        return t

  神經網絡的第一層是輸入層,輸入層返回的就是我們輸入的向量:

# (1) input layer
t = t

  第二、三層是卷積層,需要進行卷積、激活和池化等操作,激活和池化是nn.functional包中的函數:

# (2) hidden conv layer
t = self.conv1(t)
t = F.relu(t) # import nn.functional as F
t = F.max_pool2d(t, kernel_size=2, stride=2)

# (3) hidden conv layer
t = self.conv2(t)
t = F.relu(t)
t = F.max_pool2d(t, kernel_size=2, stride=2)

  第四、五層是全連接層,其中第一個全連接層要進行flatten()操作,經過前向傳播之後,還要進行激活操作。

# (4) hidden linear layer
t = t.reshape(-1, 12 * 4 * 4)
t = self.fc1(t)
t = F.relu(t)

# (5) hidden linear layer
t = self.fc2(t)
t = F.relu(t)

   第六層(最後一層)是輸出層,它輸出的向量長度,對應我們的類別數。

# (6) output layer
t = self.out(t)
# t = F.softmax(t, dim=1) # 可選

  在神經網絡內部,我們通常使用relu()作爲我們的非線性激活函數,但是對於輸出層,當我們試圖預測一個類別時,我們使用softmax()。此函數可以爲每個預測類返回一個概率值,總和爲1。

  但是在我們的例子中,我們不需要使用softmax(),因爲我們將使用的cross_entropy()損失函數,已經隱式地對其輸入執行softmax()操作,所以我們只返回上次線性轉換的結果。

  將前面代碼進行彙總,即可得到我們的前向傳播算法。

def forward(self, t):
    # (1) input layer
    t = t

    # (2) hidden conv layer
    t = self.conv1(t)
    t = F.relu(t)
    t = F.max_pool2d(t, kernel_size=2, stride=2)

    # (3) hidden conv layer
    t = self.conv2(t)
    t = F.relu(t)
    t = F.max_pool2d(t, kernel_size=2, stride=2)

    # (4) hidden linear layer
    t = t.reshape(-1, 12 * 4 * 4)
    t = self.fc1(t)
    t = F.relu(t)

    # (5) hidden linear layer
    t = self.fc2(t)
    t = F.relu(t)

    # (6) output layer
    t = self.out(t)
    # t = F.softmax(t, dim=1)

    return t

12. 預測單張圖片的類別

  注意,我們現在的網絡還是未經訓練過的。 目前只是測試如何預測單張圖片,檢查我們的網絡是否爲暢通的。所以我們先關閉PyTorch的梯度計算功能,避免它在有張量流過網絡時自動建立計算圖,這個圖主要用於計算損失函數的梯度,幫助我們後面更新網絡參數。

> torch.set_grad_enabled(False)
<torch.autograd.grad_mode.set_grad_enabled object at 0x7f6bb575fcf8>

  接下來我們創建一個Network類的實例,並從訓練集中獲取一個 image 對象。

> network = Network()

> sample = next(iter(train_set)) 
> image, label = sample 
> image.shape 
torch.Size([1, 28, 28]) 

  需要注意的是,我們的網絡期望的輸入是一個batch,於是我們只需要再增加一個維度:

> image.unsqueeze(0).shape
torch.Size([1, 1, 28, 28])

  於是再將它輸入到網絡中,進行預測:

> pred = network(image.unsqueeze(0)) # image shape needs to be 
  									 # (batch_size × in_channels × H × W)

> pred
tensor([[0.0991, 0.0916, 0.0907, 0.0949, 0.1013, 0.0922,
		 0.0990, 0.1130, 0.1107, 0.1074]])

> pred.shape
torch.Size([1, 10])

> pred.argmax(dim=1)
tensor([7])

> label
9

  注意到我們的network也是一個可以調用的對象,原因和前面的網絡層一樣。另外,我們發現pred.shape的值爲[1,10][1,10],這是因爲我們輸入的batch中,圖像的個數只有一個,第一個軸中的元素個數等於batch size,如果我們希望輸出的值代表圖像屬於每一類的概率,採用下面的代碼:

> F.softmax(pred, dim=1)
tensor([[0.1096, 0.1018, 0.0867, 0.0936, 0.1102, 0.0929, 0.1083, 0.0998, 0.0943, 0.1030]])

> F.softmax(pred, dim=1).sum()
tensor(1.)

13. 預測批量圖片的類別

  整個過程和預測單張圖片類別是類似的,區別在於我們需要用到DataLoader.

> data_loader = torch.utils.data.DataLoader(
     train_set, batch_size=10)

> batch = next(iter(data_loader))
> images, labels = batch

> images.shape
torch.Size([10, 1, 28, 28])

> labels.shape
torch.Size([10])

   然後將images喂到我們的網絡中,輸出預測結果:

> preds = network(images)

> preds.shape
torch.Size([10, 10])

> preds
tensor(
    [
        [ 0.1072, -0.1255, -0.0782, -0.1073,  0.1048,  0.1142, -0.0804, -0.0087,  0.0082,  0.0180],
        [ 0.1070, -0.1233, -0.0798, -0.1060,  0.1065,  0.1163, -0.0689, -0.0142,  0.0085,  0.0134],
        [ 0.0985, -0.1287, -0.0979, -0.1001,  0.1092,  0.1129, -0.0605, -0.0248,  0.0290,  0.0066],
        [ 0.0989, -0.1295, -0.0944, -0.1054,  0.1071,  0.1146, -0.0596, -0.0249,  0.0273,  0.0059],
        [ 0.1004, -0.1273, -0.0843, -0.1127,  0.1072,  0.1183, -0.0670, -0.0162,  0.0129,  0.0101],
        [ 0.1036, -0.1245, -0.0842, -0.1047,  0.1097,  0.1176, -0.0682, -0.0126,  0.0128,  0.0147],
        [ 0.1093, -0.1292, -0.0961, -0.1006,  0.1106,  0.1096, -0.0633, -0.0163,  0.0215,  0.0046],
        [ 0.1026, -0.1204, -0.0799, -0.1060,  0.1077,  0.1207, -0.0741, -0.0124,  0.0098,  0.0202],
        [ 0.0991, -0.1275, -0.0911, -0.0980,  0.1109,  0.1134, -0.0625, -0.0391,  0.0318,  0.0104],
        [ 0.1007, -0.1212, -0.0918, -0.0962,  0.1168,  0.1105, -0.0719, -0.0265,  0.0207,  0.0157]
    ]
)

  查看每一個圖像預測結果最大值對應的類別:

> preds.argmax(dim=1)
tensor([5, 5, 5, 5, 5, 5, 4, 5, 5, 4])

> F.softmax(preds).argmax(dim=1) #  發現softmax之後的結果和原張量的結果一致
tensor([5, 5, 5, 5, 5, 5, 4, 5, 5, 4])

> labels
tensor([9, 0, 0, 3, 0, 2, 7, 2, 5, 5])

  如果我們想要判斷 preds 的預測結果的準確性,採用下面的做法:

> preds.argmax(dim=1).eq(labels)
tensor([False, False, False, False, False, False, False, False,  True, False])

> preds.argmax(dim=1).eq(labels).sum() # 在Python中,True用1表示,False用0表示
tensor(1)

  所以我們可以自定義一個返回預測正確的圖片個數的函數,如下所示:

def get_num_correct(preds, labels):
    return preds.argmax(dim=1).eq(labels).sum().item()

14 訓練一個神經網絡

  前面我們花了很大的篇幅介紹如何 build the model,主要涉及到的是網絡層和前向傳播算法。現在我們開始學習如何 train the model,我們將訓練過程主要分爲以下步驟:

  • 從 train_set 獲取批量圖像
  • 把批量圖像輸入到神經網絡中
  • 計算模型損失(預測值與真實值之間的誤差)
  • 計算損失關於權重參數的梯度值
  • 用梯度值來更新權重參數
  • 重複前面1-5步,直到一個epoch處理完畢
  • 重複前面1-6步,直到模型損失逼近最小值

  在進行訓練過程時,我們需要打開PyTorch的梯度跟蹤功能(它默認是開啓的,我們在前面進行測試的時候將前關閉了)。

> torch.set_grad_enabled(True)
<torch.autograd.grad_mode.set_grad_enabled at 0x15b22d012b0>

  獲取一個batch的數據:

> network = Network()

> train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
> batch = next(iter(train_loader)) # Getting a batch
> images, labels = batch

  前向傳播並計算batch的損失:

> preds = network(images)
> loss = F.cross_entropy(preds, labels) # Calculating the loss

> loss.item()
2.307542085647583

  計算損失對權重參數的梯度,通過backward()方法:

> network.conv1.weight.grad # before backward() called
None

> loss.backward() # Calculating the gradients

> network.conv1.weight.grad.shape
torch.Size([6, 1, 5, 5])

  這些梯度值計算完之後,將根據我們設定的優化器,來按照對應的方式來更新模型的權重參數,優化器通過 torch.optim 來創建:

> optimizer = optim.Adam(network.parameters(), lr=0.01)
> optimizer.step() # Updating the weights

  我們可以來檢查以下,更新完模型參數,同一個批次的圖像所對應的損失是不是減小了:

> preds = network(images)
> loss = F.cross_entropy(preds, labels)

> loss.item() # 可以發現值變小了
2.262690782546997

  整合前面的所有步驟,我們得到了以下訓練單個batch的完整代碼:

network = Network()

train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
optimizer = optim.Adam(network.parameters(), lr=0.01)

batch = next(iter(train_loader)) # Get Batch
images, labels = batch

preds = network(images) # Pass Batch
loss = F.cross_entropy(preds, labels) # Calculate Loss

loss.backward() # Calculate Gradients
optimizer.step() # Update Weights

print('loss1:', loss.item())
preds = network(images)
loss = F.cross_entropy(preds, labels)
print('loss2:', loss.item())

  繼續完善我們的代碼,實現可以訓練單個epoch中的所有batch圖像,只需修改爲:

network = Network()

train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
optimizer = optim.Adam(network.parameters(), lr=0.01)

total_loss = 0
total_correct = 0

for batch in train_loader: # Get Batch
    images, labels = batch 

    preds = network(images) # Pass Batch
    loss = F.cross_entropy(preds, labels) # Calculate Loss

    optimizer.zero_grad()
    loss.backward() # Calculate Gradients
    optimizer.step() # Update Weights

    total_loss += loss.item()
    total_correct += get_num_correct(preds, labels)
    
print(
    "epoch:", 0, 
    "total_correct:", total_correct, 
    "loss:", total_loss
)

  繼續完善我們的代碼,實現可以訓練多個epoch的圖像,只需一點小修改:

network = Network()

train_loader = torch.utils.data.DataLoader(train_set, batch_size=100)
optimizer = optim.Adam(network.parameters(), lr=0.01)

for epoch in range(10):
    
    total_loss = 0
    total_correct = 0
    
    for batch in train_loader: # Get Batch
        images, labels = batch 

        preds = network(images) # Pass Batch
        loss = F.cross_entropy(preds, labels) # Calculate Loss

        optimizer.zero_grad()
        loss.backward() # Calculate Gradients
        optimizer.step() # Update Weights

        total_loss += loss.item()
        total_correct += get_num_correct(preds, labels)

    print(
        "epoch", epoch, 
        "total_correct:", total_correct, 
        "loss:", total_loss
    )

  當我們執行這個代碼,輸出如下所示:

epoch 0 total_correct: 43301 loss: 447.59147948026657
epoch 1 total_correct: 49565 loss: 284.43429669737816
epoch 2 total_correct: 51063 loss: 244.08825492858887
epoch 3 total_correct: 51955 loss: 220.5841210782528
epoch 4 total_correct: 52551 loss: 204.73878084123135
epoch 5 total_correct: 52914 loss: 193.1240530461073
epoch 6 total_correct: 53195 loss: 184.50964668393135
epoch 7 total_correct: 53445 loss: 177.78808392584324
epoch 8 total_correct: 53629 loss: 171.81662507355213
epoch 9 total_correct: 53819 loss: 166.2412590533495

  以上就是我們利用PyTorch建立一個卷積神經網絡對Fashion-MNIST數據集進行預測的全過程,我們一塊磚、一片瓦的搭起了整個神經網絡,瞭解瞭如何build the modeltrain the model。麻雀雖小,五臟俱全,通過本文的案例,我們瞭解了PyTorch底層的工作原理,希望大家能夠舉一反三,應用PyTorch深度學習框架去解決更多的實際問題,加油!

  
  
  
  
  
  

  
  
  
  
  
  

  
  
  
  
  
  

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