引言
在本文中,我們將建构一個最早被提出的卷積神經網絡(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