PyTorch實戰:經典模型LeNet5實現手寫體識別

在上一篇博客CNN核心概念理解中,咱們以LeNet爲例介紹了CNN的重要概念。在這篇博客中,咱們將利用著名深度學習框架PyTorch實現LeNet5,而且利用它實現手寫體字母的識別。訓練數據採用經典的MNIST數據集。本文主要分爲兩個部分,一是如何使用PyTorch實現LeNet模型,二是實現數據準備、定義網絡、定義損失函數、訓練、測試等完整流程。html

1、LeNet模型定義git

LeNet是識別手寫字母的經典網絡,雖然年代久遠,但從學習的角度仍不失爲一個優秀的範例。要實現這個網絡,首先來看看這個網絡的結構:github

這是一個簡單的前向傳播的網絡,它接受32x32圖片做爲輸入,通過卷積、池化和全鏈接層的計算,最終給出輸出結果。實現的過程並不複雜:網絡

 1 from torch import nn
 2 from torch.nn import functional as F
 3 
 4 class LeNet(nn.Module):
 5     def __init__(self):
 6         super(LeNet, self).__init__()
 7         # 1 input image channel, 6 output channels, 5x5 square convolution
 8         self.conv1 = nn.Conv2d(1, 6, 5, padding=2)
 9         self.conv2 = nn.Conv2d(6, 16, 5)
10         # an affine operation: y = Wx + b
11         self.fc1 = nn.Linear(16 * 5 * 5, 120)
12         self.fc2 = nn.Linear(120, 84)
13         self.fc3 = nn.Linear(84, 10)
14         
15         
16     def forward(self, x):
17         x = F.relu(self.conv1(x))
18         # Max pooling over a (2, 2) window
19         x = F.max_pool2d(x, 2)
20         x = F.relu(self.conv2(x))
21         x = F.max_pool2d(x, 2)
22 
23         x = x.view(x.size(0), -1)
24         x = F.relu(self.fc1(x))
25         x = F.relu(self.fc2(x))
26         x = self.fc3(x)
27         return x

 

咱們繼承了nn.Module模塊,在__init__中完成了卷積層和全鏈接層的初始化。值得注意的是因爲池化層沒有參數,所以並無一塊兒初始化。初始化參數包括輸入個數、輸出個數,卷積層的參數還有卷積核大小。除此以外在第一個卷積層C1中還定義了padding,這是由於數據集中圖片是28x28的,padding=2代表輸入的時候在圖片四周各填充2個像素的空白,將輸入變成了32x32。框架

在forward中咱們實現了前向傳播。這裏咱們根據定義對輸入依次進行卷積、激活、池化等操做,最後返回計算結果。在全鏈接層以前,有一個對數據的展開操做,咱們使用Tensor的view函數實現,這個函數能夠將Tensor轉變成任意合法的形狀。咱們只定義了forward函數,而沒有定義backword函數,這是由於PyTorch的自動微分功能自動幫咱們完成了反向傳播的定義。函數

LeNet模型這樣就定義完成了。可是須要注意的是,這個網絡和最初LeCun論文中的實現略有不一樣:學習

  • 原始論文中C3與S2並非全鏈接而是部分鏈接,這樣能減小部分計算量。而現代CNN模型中,好比AlexNet,ResNet等,都採起全鏈接的方式了。咱們的實如今這裏作了一些簡化。
  • 原文中使用雙曲正切做爲激活函數,而咱們使用了收斂速度更快的ReLu函數。
  • 按照原文描述,網絡最後一層爲高斯鏈接層。而咱們爲了簡單起見仍是用了全鏈接層。

LeNet實際上是一個比較「古老」的模型了,咱們沒必要追求完美的復現,理解其中關鍵的概念便可。測試

 

2、準備數據優化

爲PyTorch準備數據很是方便。對於一些經典數據集,PyTorch已經將它們封裝好了,咱們能夠直接拿來用。固然MNIST數據集也在此列,可是咱們仍然定義了本身的數據集,由於這種方法能夠處理更通用的狀況。爲了定義本身的數據集,首先要繼承torch.utils.data.database類,而後實現至少__getitem__和__len__兩個方法。ui

 1 import gzip, struct
 2 import numpy as np
 3 import torch.utils.data as data
 4 
 5 class MnistDataset(data.Dataset):
 6     def __init__(self, path, train=True):
 7         self.path = path
 8         if train:
 9             X, y = self._read('train-images-idx3-ubyte.gz', 
10                               'train-labels-idx1-ubyte.gz')
11         else:
12             X, y = self._read('t10k-images-idx3-ubyte.gz', 
13                               't10k-labels-idx1-ubyte.gz')
14             
15         self.images = torch.from_numpy(X.reshape(-1, 1, 28, 28)).float()
16         self.labels = torch.from_numpy(y.astype(int))
17         
18     def __getitem__(self, index):
19         return self.images[index], self.labels[index]
20 
21     def __len__(self):
22         return len(self.images)
23 
24     def _read(self, image, label):
25         with gzip.open(self.path + image, 'rb') as fimg:
26             magic, num, rows, cols = struct.unpack(">IIII", fimg.read(16))
27             X = np.frombuffer(fimg.read(), dtype=np.uint8).reshape(-1, rows, cols)
28         with gzip.open(self.path + label) as flbl:
29             magic, num = struct.unpack(">II", flbl.read(8))
30             y = np.frombuffer(flbl.read(), dtype=np.int8)
31         return X, y

因爲官網上提供的MNIST數據集是gzip壓縮格式,所以咱們在讀取的時候首先要解壓,而後轉成numpy形式,最後轉成Tensor保存起來。以後在__getitem__中返回相應的數據和類別就能夠了,__len__函數直接返回數據集的大小。因爲MNIST數據集有訓練和測試兩部分,所以須要分類處理。

 

3、使用數據訓練網絡

咱們首先用DataLoader類加載數據集,DataLoader負責將數據轉化成適當的形式放入模型訓練。使用DataLoader能夠方便地控制微批次大小、線程數等參數。

1 train_dataset = MnistDataset('./data/')
2 train_loader = data.DataLoader(train_dataset, shuffle=True, batch_size=256, 
3                           num_workers=4)

這時候能夠測試數據有沒有成功加載進來,如圖所示。

 下一步定義評價函數和優化器,這一步很重要,但不是本文重點。直接給出代碼:

1 criterion = nn.CrossEntropyLoss(reduction='sum')
2 optimizer = optim.Adam(net.parameters(), lr=1e-3, betas=(0.9, 0.99))

 最後的給出訓練過程的簡化版。這個兩層循環就是實際的訓練過程,外層循環控制遍歷數據集的次數,內層循環控制每一次參數更新。

1 for epoch in range(5):
2     for (inputs, label) in train_loader:
3         # zero the parameter gradients
4         optimizer.zero_grad()
5         # forward + backward + optimize
6         output = net(inputs)
7         loss = criterion(output, label)
8         loss.backward()
9         optimizer.step()

 

3、模型評估

模型通過訓練以後,將測試集輸入放入模型,將輸出和標籤比對能夠計算出模型的準確率等信息,進而對模型不斷優化。此外若是想要了解模型到底學到了什麼東西,還能夠將中間層結果輸出。如圖所示:

這部分代碼沒有給出,完整代碼能夠到Github頁面查看。

相關文章
相關標籤/搜索