在PyTorch中從頭開始撰寫LeNet5

引言

在本文中,我們將建构一個最早被提出的卷積神經網絡(LeNet5)。我們將使用PyTorch從零開始建立這個CNN,並將查看它在一個現實世界的數據集中的表現。

我們將從探索LeNet5的架構開始。然後,我們將使用torchvision提供的類來載入和分析我們的數據集MNIST。使用PyTorch,我們將從零開始建立我們的LeNet5並它在我們的數據上進行訓練。最後,我們將查看模型在未見過的測試數據上的表現。

前置知識

了解神經網絡將有助於理解本文。這意味著熟悉神經網絡的不同層(輸入層、隱藏層、輸出層)、激活函數、優化算法(梯度下降的變體)、損失函數等。此外,熟悉Python語法和PyTorch庫對於理解本文中展示的代码片段是必要的。

了解CNNs也是建議的。這包括對卷積層,池化層以及它們從輸入數據中提取特性的作用有所認識。了解像步伐(stride),填充(padding)以及核/過濾器大小(kernel/filter size)的影響是有益的。

LeNet5

LeNet5用於手寫字符的識別,是由Yann LeCun和其他人在1998年提出的,並在海報<diy6>Gradient-Based Learning Applied to Document Recognition中進行论述。

讓我們來了解以下圖表所展示的LeNet5架構:

如名稱所示,LeNet5具有5層,其中有2層卷積層和3層完全連接層。讓我們從輸入開始。LeNet5接受32×32的灰度圖像作為輸入,這意味著該架構不適合用於RGB圖像(多通道)。因此,輸入圖像應該只包含一個通道。在這個之後,我們開始使用卷積層

第1個卷積層具有5×5的過濾器大小,並有6個這樣的過濾器。這將減少圖像的寬度和高度,同時增加深度(通道數)。輸出將是28x28x6。在這個之後,池化被應用以將特徵圖减小一半,即是14x14x6。現在在第1個卷積層的輸出上應用相同尺寸的過濾器(5×5)和16個過濾器,然後是一個池化層。這將將輸出特徵圖減少至5x5x16。

在此之後,套用一個大小為5×5的卷積層,並有多少120個過濾器將特徵圖平坦化至120個值。然後來到的第一個完整連接層,有84個神經元。最後,我們有輸出層,其有10個輸出神經元,因為MNIST數據每個表示的10個數字類別都有10個類别。


數據加載

讓我們從加載和分析數據開始。我們將使用MNIST數據集。MNIST數據集包含手寫數字影像。這些影像是灰度的,所有影像的大小都是28×28,並由60,000個訓練影像和10,000個測試影像組成。

您可以在下方看到一些影像樣本:

導入庫

讓我們從導入所需的庫開始,並定義一些變量(超參數和device也详细说明,以幫助包確定是否在GPU或CPU上進行訓練):

# 載入相關庫,並按適當情況别名
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms

# 定義ML任務相關變量
batch_size = 64
num_classes = 10
learning_rate = 0.001
num_epochs = 10

# 設備將決定是否在GPU或CPU上運行訓練。
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

載入與轉化數據

使用 torchvision ,我們將載入數據集,因為這將讓我們能夠輕鬆地執行任何預處理步驟。

#載入數據集和預處理
train_dataset = torchvision.datasets.MNIST(root = './data',
                                           train = True,
                                           transform = transforms.Compose([
                                                  transforms.Resize((32,32)),
                                                  transforms.ToTensor(),
                                                  transforms.Normalize(mean = (0.1307,), std = (0.3081,))]),
                                           download = True)


test_dataset = torchvision.datasets.MNIST(root = './data',
                                          train = False,
                                          transform = transforms.Compose([
                                                  transforms.Resize((32,32)),
                                                  transforms.ToTensor(),
                                                  transforms.Normalize(mean = (0.1325,), std = (0.3105,))]),
                                          download=True)


train_loader = torch.utils.data.DataLoader(dataset = train_dataset,
                                           batch_size = batch_size,
                                           shuffle = True)


test_loader = torch.utils.data.DataLoader(dataset = test_dataset,
                                           batch_size = batch_size,
                                           shuffle = True)

讓我們理解一下代码:

  • 首先,MNIST 數據集不能直接使用,因為它對於 LeNet5 結構不适用。LeNet5 結構接受的輸入應該是 32×32 的,而 MNIST 圖片是 28×28 的。我們可以通过重置圖片大小、使用事先計算的平均值和標準差(可在网上找到)來标准化它們,並最後將它們存儲為張量來解決這個問題。
  • 我們將 download=True 置為真,以防數據尚未下載。
  • 接下來,我們使用數據加载器。對於像 MNIST 這樣的小數據集,這可能不會影響性能,但在大型數據集的情況下,它可能會大幅降低性能,因此通常被認為是一種好習慣。數據加载器讓我們可以按批次遍歷數據,而且在遍歷時加载數據,而不是一次性在開始時加载。
  • 我們指定批次大小,在載入時將數據集混排,這樣每個批次中不同類型的標籤就會有一些變異。這將提高我們最終模型的效果。

從頭開始建立LeNet5

我們先看一下代碼:

# 定義卷積神經網絡
class LeNet5(nn.Module):
    def __init__(self, num_classes):
        super(ConvNeuralNet, self).__init__()
        self.layer1 = nn.Sequential(
            nn.Conv2d(1, 6, kernel_size=5, stride=1, padding=0),
            nn.BatchNorm2d(6),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.layer2 = nn.Sequential(
            nn.Conv2d(6, 16, kernel_size=5, stride=1, padding=0),
            nn.BatchNorm2d(16),
            nn.ReLU(),
            nn.MaxPool2d(kernel_size = 2, stride = 2))
        self.fc = nn.Linear(400, 120)
        self.relu = nn.ReLU()
        self.fc1 = nn.Linear(120, 84)
        self.relu1 = nn.ReLU()
        self.fc2 = nn.Linear(84, num_classes)
        
    def forward(self, x):
        out = self.layer1(x)
        out = self.layer2(out)
        out = out.reshape(out.size(0), -1)
        out = self.fc(out)
        out = self.relu(out)
        out = self.fc1(out)
        out = self.relu1(out)
        out = self.fc2(out)
        return out

定義LeNet5模型

我將逐行解釋代碼:

  • 在PyTorch中,我們通過創建一個繼承自nn.Module的類來定義神經網絡,因為它包含我們將要用到的許多方法。
  • 在那之後主要有兩個步驟。第一個是者在__init__中初始化我們在CNN中将要使用的層,另一個是定義這些層處理圖像的順序。這是在forward函數內定義的。
  • 對於結構本身,我們首先使用nn.Conv2D函數定義卷積層,並設定適當的核大小以及輸入/輸出通道。我們還使用nn.MaxPool2D函數應用最大池化。PyTorch的一个好处是可以將卷積層、激活函數和最大池化組合在一個單一的層中(它們將分開應用,但這有助於組織)使用nn.Sequential函數。
  • 然後我們定義全連接層。注意我們可以在此處使用nn.Sequential,將激活函數和線性層合併,但我想展示這兩種方法都是可行的。
  • 最後,我們的最後一層輸出10個神經元,這是我們對於數字的最終預測。

設定超參數

在訓練之前,我們需要設定一些超參數,如損失函數和將使用的優化器。

model = LeNet5(num_classes).to(device)

#設定損失函數
cost = nn.CrossEntropyLoss()

#設定優化器,帶有模型參數和學習速率
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)

#這被定義為在訓練時打印還有多少步驟剩餘
total_step = len(train_loader)

我們首先使用類數作為參數初始化我們的模型,在這 слу 情況下是10。然後我們將我們的成本函數設定為交叉熵損失,優化器設定為Adam。這些有很多選擇,但這些通常在模型和給定的數據上給出良好的結果。最後,我們定義total_step以更好地追踪訓練時的步驟。


模型訓練

現在,我們可以訓練我們的模型:

total_step = len(train_loader)
for epoch in range(num_epochs):
    for i, (images, labels) in enumerate(train_loader):  
        images = images.to(device)
        labels = labels.to(device)
        
        #前向傳遞
        outputs = model(images)
        loss = cost(outputs, labels)
        #後向傳遞和優化
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if (i+1) % 400 == 0:
            print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}' 

.format(epoch+1, num_epochs, i+1, total_step, loss.item()))

讓我們看看這段程式碼在做什麼:

  • 我們首先遍歷迭代的回合數,然後是訓練數據中的批次。
  • 我們根據所使用的硬體,也就是GPU或CPU,來轉換影像和標籤。
  • 在正向傳遞過程中,我們使用模型來做出預測,並根據這些預測和實際標籤來計算損失。
  • 接下來,在向後傳遞過程中,我們實際上在更新我們的權重以改善我們的模型
  • 我們然後使用optimizer.zero_grad()函數在每次更新前將梯度設定為零。
  • 然後,我們使用loss.backward()函數來計算新的梯度。
  • 最後,我們使用optimizer.step()函數來更新權重。

我們可以看到以下的輸出:

如我們所見,損失隨著每個回合的減少,這顯示了我們的模型確實在學習。注意,這個損失是在訓練集中的,如果損失過小(如我們的案例中),這可能表示過擬合。有許多方法可以解決這個問題,例如regularization、數據增強等,但我們在本文中不會介紹這些。讓我們現在來測試我們的模型,看看它的表現如何。


模型測試

現在讓我們來測試我們的模型:

# 測試模型
# 在測試階段,我們不需要計算梯度(為了記憶體效率)
  
with torch.no_grad():
correct = 0
total = 0
for images, labels in test_loader:
        images = images.to(device)
        labels = labels.to(device)
        outputs = model(images)
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()
    print('Accuracy of the network on the 10000 test images: {} %'.format(100 * correct / total))

如您所見,程式碼與訓練時的程式碼並不一樣。唯一不同的是,我們不計算梯度(使用`with torch.no_grad()`),也不計算損失因為我們在此不需要反向傳播。為了計算模型的結果準確度,我們可以簡單地計算正確預測的總數除以圖像的總數。

使用此模型,我們得到了約98.8%的準確度,這相當不錯:

測試準確度

請注意,MNIST數據集對於今天的標準來說相當基本和小型,對於其他數據集來說獲得類似的結果困難。儘管如此,這是學習深度學習和卷積神經網絡(CNN)的良好起點。


結論

現在讓我們結論我們在本文中做了什麼:

  • 我們從學習LeNet5的結構以及該結構中的不同類型的層開始。
  • 接下來,我們探索了MNIST數據集並使用`torchvision`載入數據。
  • 然後,我们从头开始构建LeNet5,並為模型定義超參數。
  • 最後,我們在MNIST數據集上訓練和測試了我們的模型,並且該模型在測試數據集上表現出色。

未來工作

雖然這似乎是PyTorch中深入学习的一个非常好的 introduction,但您也可以將此工作延展以學習更多內容:

  • 您可以嘗試使用不同的數據集,但對於這個模型,您需要灰度數據集。一個這樣的數據集是FashionMNIST
  • 您可以嘗試使用不同的超參數,並查看它們對模型的最佳組合。
  • 最後,您可以嘗試從數據集中添加或移除層,以查看它們對模型能力的影響。

Source:
https://www.digitalocean.com/community/tutorials/writing-lenet5-from-scratch-in-python