LeNet5 von Grund auf in PyTorch schreiben
Einführung
In diesem Artikel werden wir eines der frühesten jemals eingeführten Convolutional Neural Networks (LeNet5) erstellen. Wir bauen dieses CNN von Grund auf in PyTorch und werden auch sehen, wie es sich auf einem realen Datensatz verhält.
Wir werden beginnen, indem wir die Architektur von LeNet5 erkunden. Dann werden wir unser Dataset, MNIST, unter Verwendung der bereitgestellten Klasse aus torchvision laden und analysieren. Mit PyTorch werden wir unser LeNet5 von Grund auf neu erstellen und es mit unseren Daten trainieren. Schließlich werden wir sehen, wie das Modell mit den unbekannten Testdaten abschneidet.
Voraussetzungen
Kenntnisse über neuronale Netzwerke werden hilfreich sein, um diesen Artikel zu verstehen. Dies bedeutet, mit den verschiedenen Schichten neuronaler Netzwerke (Eingabeschicht, verborgene Schichten, Ausgabeschicht), Aktivierungsfunktionen, Optimierungsalgorithmen (Varianten des Gradientenabstiegs), Verlustfunktionen usw. vertraut zu sein. Darüber hinaus ist Vertrautheit mit der Python-Syntax und der PyTorch-Bibliothek unerlässlich, um die im Artikel vorgestellten Code-Snippets zu verstehen.
Ein Verständnis von CNNs wird ebenfalls empfohlen. Dazu gehört das Wissen über Faltungsschichten, Pooling-Schichten und deren Rolle beim Extrahieren von Merkmalen aus Eingabedaten. Das Verständnis von Konzepten wie Schrittweite, Padding und der Einfluss der Kernel-/Filtergröße ist von Vorteil.
LeNet5
LeNet5 wurde zur Erkennung von handgeschriebenen Zeichen verwendet und wurde 1998 von Yann LeCun und anderen in dem Papier "Gradientenbasiertes Lernen angewendet auf die Dokumentenerkennung" vorgeschlagen.
Lass uns die Architektur von LeNet5 verstehen, wie in der folgenden Abbildung dargestellt:
Wie der Name schon sagt, besteht LeNet5 aus 5 Schichten, darunter zwei konvolutionale und drei vollständig verbundene Schichten. Lassen Sie uns mit dem Eingang beginnen. LeNet5 akzeptiert als Eingabe ein Graustufenbild von 32x32, was bedeutet, dass die Architektur nicht für RGB-Bilder (mehrere Kanäle) geeignet ist. Das Eingabebild sollte also nur einen Kanal enthalten. Danach beginnen wir mit unseren konvolutionalen Schichten.
Die erste Convolution-Schicht hat eine Filtergröße von 5x5 mit 6 solchen Filtern. Dies wird die Breite und Höhe des Bildes verringern, während die Tiefe (Anzahl der Kanäle) erhöht wird. Die Ausgabe wäre 28x28x6. Danach wird Pooling angewendet, um die Merkmalskarte um die Hälfte zu verkleinern, d.h. 14x14x6. Die gleiche Filtergröße (5x5) mit 16 Filtern wird nun auf die Ausgabe angewendet, gefolgt von einer Pooling-Schicht. Dies reduziert die Ausgabemerkmalskarte auf 5x5x16.
Nach diesem Schritt wird eine Faltungsschicht der Größe 5x5 mit 120 Filtern angewendet, um die Merkmalskarte auf 120 Werte zu glätten. Danach folgt die erste vollständig verbundene Schicht mit 84 Neuronen. Schließlich haben wir die Ausgabeschicht, die 10 Ausgabeneuronen hat, da die MNIST-Daten 10 Klassen für jede der dargestellten 10 Ziffern enthalten.
Datenladen
Lass uns damit beginnen, die Daten zu laden und zu analysieren. Wir werden den MNIST-Datensatz verwenden. Der MNIST-Datensatz enthält Bilder von handgeschriebenen Ziffern. Die Bilder sind in Graustufen, alle mit einer Größe von 28x28, und bestehen aus 60.000 Trainingsbildern und 10.000 Testbildern.
Sie können unten einige Beispiele von Bildern sehen:
Die Bibliotheken importieren
Lass uns damit beginnen, die erforderlichen Bibliotheken zu importieren und einige Variablen zu definieren (Hyperparameter und device sind ebenfalls detailliert, um dem Paket zu helfen zu bestimmen, ob auf GPU oder CPU trainiert werden soll):
Load in relevant libraries, and alias where appropriate
import torch
import torch.nn as nn
import torchvision
import torchvision.transforms as transforms
Define relevant variables for the ML task
batch_size = 64
num_classes = 10
learning_rate = 0.001
num_epochs = 10
Device will determine whether to run the training on GPU or CPU.
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
Daten Laden und Transformieren
Mit torchvision laden wir den Datensatz, da dies es uns ermöglicht, alle Vorverarbeitungsschritte einfach durchzuführen.
#Loading the dataset and preprocessing
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)
Lass uns den Code verstehen:
- Zunächst kann der MNIST-Datensatz nicht direkt für die LeNet5-Architektur verwendet werden. Die LeNet5-Architektur erwartet eine Eingabe von 32x32, während die MNIST-Bilder 28x28 groß sind. Wir können dies beheben, indem wir die Bilder neu skalieren, sie mit dem vorab berechneten Mittelwert und der Standardabweichung (online verfügbar) normalisieren und sie schließlich als Tensors speichern.
- Wir setzen
download=True, falls die Daten noch nicht heruntergeladen wurden. - Als nächstes nutzen wir Datenlader. Dies könnte die Leistung bei einem kleinen Datensatz wie MNIST nicht beeinträchtigen, kann jedoch die Leistung bei großen Datensätzen erheblich beeinträchtigen und wird allgemein als gute Praxis angesehen. Datenlader ermöglichen es uns, durch die Daten in Batches zu iterieren, und die Daten werden während der Iteration und nicht auf einmal zu Beginn geladen.
- Wir legen die Batch-Größe fest und mischen den Datensatz beim Laden, damit jede Charge eine gewisse Varianz in den Arten von Labels hat, die sie enthält. Dies wird die Wirksamkeit unseres endgültigen Modells erhöhen.
LeNet5 von Grund auf
Lass uns zuerst den Code anschauen:
#Defining the convolutional neural network
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
Das LeNet5-Modell definieren
Ich werde den Code linear erklären:
- In PyTorch definieren wir ein neuronales Netzwerk, indem wir eine Klasse erstellen, die von
nn.Moduleerbt, da sie viele der Methoden enthält, die wir benötigen werden. - Es gibt danach zwei Hauptschritte. Der erste besteht darin, die Schichten zu initialisieren, die wir in unserem CNN innerhalb von
__init__verwenden werden, und der andere besteht darin, die Reihenfolge zu definieren, in der diese Schichten das Bild verarbeiten. Dies wird innerhalb derforward-Funktion definiert. - Für die Architektur selbst definieren wir zunächst die konvolutionalen Schichten mit der Funktion
nn.Conv2Dunter Verwendung der passenden Kernelgröße und der Eingangs-/Ausgangskanäle. Außerdem wenden wir Max-Pooling mit der Funktionnn.MaxPool2Dan. Das Schöne an PyTorch ist, dass wir die konvolutionale Schicht, die Aktivierungsfunktion und das Max-Pooling in einer einzigen Schicht kombinieren können (sie werden separat angewendet, aber es hilft bei der Organisation) unter Verwendung der Funktionnn.Sequential. - Dann definieren wir die vollständig verbundenen Schichten. Beachten Sie, dass wir hier auch
nn.Sequentialverwenden und die Aktivierungsfunktionen mit den linearen Schichten kombinieren können, aber ich wollte zeigen, dass beides möglich ist. - Schließlich gibt unsere letzte Schicht 10 Neuronen aus, die unsere endgültigen Vorhersagen für die Ziffern sind.
Hyperparameter einstellen
Vor dem Training müssen wir einige Hyperparameter festlegen, wie die Verlustfunktion und den zu verwendenden Optimierer.
model = LeNet5(num_classes).to(device)
#Setting the loss function
cost = nn.CrossEntropyLoss()
#Setting the optimizer with the model parameters and learning rate
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
#this is defined to print how many steps are remaining when training
total_step = len(train_loader)
Wir beginnen damit, unser Modell mit der Anzahl der Klassen als Argument zu initialisieren, was in diesem Fall 10 ist. Dann definieren wir unsere Kostenfunktion als Kreuzentropieverlust und den Optimierer als Adam. Es gibt viele Optionen dafür, aber diese führen in der Regel zu guten Ergebnissen mit dem Modell und den gegebenen Daten. Schließlich definieren wir total_step, um die Schritte beim Training besser nachverfolgen zu können.
Modelltraining
Nun können wir unser Modell trainieren:
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)
#Forward pass
outputs = model(images)
loss = cost(outputs, labels)
#Backward and optimize
optimizer.zero_grad()
loss.backward()
optimizer.step()
if (i+1) % 400 == 0:
print ('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}'
.format(Epoche+1, Anzahl_Epochen, i+1, Gesamt_Schritte, Verlust.wert())
Lass uns sehen, was der Code macht:
- Wir beginnen damit, die Anzahl der Epochen zu durchlaufen und dann die Batches in unseren Trainingsdaten.
- Wir konvertieren die Bilder und die Labels entsprechend dem verwendeten Gerät, d.h. GPU oder CPU.
- Im Vorwärtsdurchlauf machen wir Vorhersagen mit unserem Modell und berechnen den Verlust basierend auf diesen Vorhersagen und unseren tatsächlichen Etiketten.
- Als nächstes führen wir den Rückwärtsdurchlauf durch, bei dem wir tatsächlich unsere Gewichte aktualisieren, um unser Modell zu verbessern.
- Dann setzen wir die Gradienten vor jedem Update auf Null, indem wir die Funktion
optimizer.zero_grad()verwenden. - Dann berechnen wir die neuen Gradienten mit der Funktion
loss.backward(). - Und schließlich aktualisieren wir die Gewichte mit der
optimizer.step()Funktion.
Wir können die Ausgabe wie folgt sehen:
Wie wir sehen können, nimmt der Verlust mit jeder Epoche ab, was zeigt, dass unser Modell tatsächlich lernt. Beachten Sie, dass dieser Verlust im Trainingssatz ist, und wenn der Verlust viel zu klein ist (wie in unserem Fall), kann das auf Overfitting hinweisen. Es gibt mehrere Möglichkeiten, dieses Problem zu lösen, wie z.B. Regularisierung, Datenaugmentierung und so weiter, aber darauf werden wir in diesem Artikel nicht eingehen. Lassen Sie uns nun unser Modell testen, um zu sehen, wie es abschneidet.
Modelltest
Lass uns jetzt unser Modell testen:
Test the model
In test phase, we don't need to compute gradients (for memory efficiency)
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))
Wie Sie sehen können, ist der Code nicht so anders als der für das Training. Der einzige Unterschied besteht darin, dass wir keine Gradienten berechnen (mit with torch.no_grad()) und auch den Verlust nicht berechnen, da wir hier keine Rückpropagation benötigen. Um die resulting Genauigkeit des Modells zu berechnen, können wir einfach die Gesamtzahl der richtigen Vorhersagen über die Gesamtzahl der Bilder berechnen.
Mit diesem Modell erreichen wir eine Genauigkeit von etwa 98,8 %, was ziemlich gut ist:
Genauigkeit testen
Bitte beachten Sie, dass der MNIST-Datensatz für die heutigen Standards ziemlich grundlegend und klein ist und ähnliche Ergebnisse bei anderen Datensätzen schwer zu erzielen sind. Dennoch ist es ein guter Ausgangspunkt, um Deep Learning und CNNs zu lernen.
Schlussfolgerung
Lassen Sie uns nun zusammenfassen, was wir in diesem Artikel gemacht haben:
- Wir haben damit begonnen, die Architektur von LeNet5 und die verschiedenen Arten von Schichten darin zu lernen.
- Als nächstes erkundeten wir den MNIST-Datensatz und luden die Daten mit
torchvision. - Dann haben wir LeNet5 von Grund auf neu erstellt und die Hyperparameter für das Modell festgelegt.
- Schließlich haben wir unser Modell auf dem MNIST-Datensatz trainiert und getestet, und das Modell schien gut auf dem Testdatensatz abzuschneiden.
Zukünftige Arbeit
Obwohl dies eine wirklich gute Einführung in Deep Learning mit PyTorch zu sein scheint, können Sie diese Arbeit erweitern, um mehr zu lernen:
- Sie können versuchen, verschiedene Datensätze zu verwenden, aber für dieses Modell benötigen Sie Graustufen-Datensätze. Ein solcher Datensatz ist FashionMNIST.
- Sie können mit verschiedenen Hyperparametern experimentieren und die beste Kombination für das Modell herausfinden.
- Schließlich können Sie versuchen, Schichten aus dem Datensatz hinzuzufügen oder zu entfernen, um deren Einfluss auf die Leistungsfähigkeit des Modells zu sehen.




