Recherche de site Web

Écrire LeNet5 à partir de zéro dans PyTorch


Introduction

Dans cet article, nous allons construire l'un des premiers réseaux de neurones convolutionnels jamais introduits, (LeNet5). Nous construisons ce CNN à partir de zéro dans PyTorch et nous verrons également comment il fonctionne sur un ensemble de données du monde réel.

Nous commencerons par explorer l'architecture de LeNet5. Nous allons ensuite charger et analyser notre ensemble de données, MNIST, en utilisant la classe fournie de torchvision. En utilisant PyTorch, nous construirons notre LeNet5 de zéro et le formerons sur nos données. Enfin, nous verrons comment le modèle se comporte sur les données de test non vues.

Conditions préalables

La connaissance des réseaux de neurones sera utile pour comprendre cet article. Cela se traduit par la familiarité avec les différentes couches des réseaux de neurones (couche d'entrée, couches cachées, couche de sortie), les fonctions d'activation, les algorithmes d'optimisation (variantes de la descente de gradient), les fonctions de perte, etc. De plus, la familiarité avec la syntaxe Python et la bibliothèque PyTorch est essentielle pour comprendre les extraits de code présentés dans cet article.

Une compréhension des réseaux de neurones convolutionnels (CNN) est également recommandée. Cela comprend des connaissances sur les couches de convolution, les couches de pooling, et leur rôle dans l'extraction des caractéristiques des données d'entrée. Comprendre des concepts tels que le stride, le padding et l'impact de la taille du noyau/filtre est bénéfique.

LeNet5

LeNet5 a été utilisé pour la reconnaissance de caractères manuscrits et a été proposé par Yann LeCun et d'autres en 1998 avec l'article "Apprentissage basé sur le gradient appliqué à la reconnaissance de documents."

Comprenons l'architecture de LeNet5 comme le montre la figure ci-dessous :

Comme son nom l'indique, LeNet5 a 5 couches, avec deux couches convolutionnelles et trois couches entièrement connectées. Commençons par l'entrée. LeNet5 accepte comme entrée une image en niveaux de gris de 32x32, ce qui indique que l'architecture n'est pas adaptée aux images RVB (multiples canaux). Ainsi, l'image d'entrée doit contenir un seul canal. Après cela, nous commençons avec nos couches convolutionnelles.

La première couche de convolution a une taille de filtre de 5x5 avec 6 filtres de ce type. Cela va réduire la largeur et la hauteur de l'image tout en augmentant la profondeur (nombre de canaux). La sortie serait de 28x28x6. Ensuite, un pooling est appliqué pour réduire la carte des caractéristiques de moitié, c'est-à-dire 14x14x6. La même taille de filtre (5x5) avec 16 filtres est maintenant appliquée à la sortie suivie d'une couche de pooling. Cela réduit la carte de caractéristiques de sortie à 5x5x16.

Après cela, une couche de convolution de taille 5x5 avec 120 filtres est appliquée pour aplatir la carte des caractéristiques en 120 valeurs. Vient ensuite la première couche entièrement connectée, avec 84 neurones. Enfin, nous avons la couche de sortie qui a 10 neurones de sortie, puisque les données MNIST ont 10 classes pour chacun des 10 chiffres numériques représentés.

Chargement des données

Commençons par charger et analyser les données. Nous allons utiliser le jeu de données MNIST. Le jeu de données MNIST contient des images de chiffres numériques écrits à la main. Les images sont en niveaux de gris, toutes de taille 28x28, et composées de 60 000 images d'entraînement et 10 000 images de test.

Vous pouvez voir quelques exemples d'images ci-dessous :

Importer les bibliothèques

Commençons par importer les bibliothèques nécessaires et définir quelques variables (les hyperparamètres et device sont également détaillés pour aider le package à déterminer s'il faut entraîner sur GPU ou CPU) :

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')

Chargement et transformation des données

En utilisant torchvision, nous allons charger le jeu de données car cela nous permettra d'effectuer facilement toutes les étapes de prétraitement.

#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)

Comprenons le code :

  • Tout d'abord, les données MNIST ne peuvent pas être utilisées telles quelles pour l'architecture LeNet5. L'architecture LeNet5 accepte une entrée de 32x32 et les images MNIST mesurent 28x28. Nous pouvons résoudre ce problème en redimensionnant les images, en les normalisant à l'aide de la moyenne et de l'écart type pré-calculés (disponibles en ligne), et enfin en les stockant sous forme de tenseurs.
  • Nous avons défini download=True au cas où les données ne seraient pas déjà téléchargées.
  • Ensuite, nous utilisons des chargeurs de données. Cela peut ne pas affecter les performances dans le cas d'un petit ensemble de données comme MNIST, mais cela peut vraiment nuire aux performances en cas de grands ensembles de données et est généralement considéré comme une bonne pratique. Les chargeurs de données nous permettent d'itérer à travers les données par lots, et les données sont chargées pendant l'itération et non pas en une seule fois au départ.
  • Nous spécifions la taille du lot et mélangeons le jeu de données lors du chargement afin que chaque lot présente une certaine variance dans les types d'étiquettes qu'il contient. Cela augmentera l'efficacité de notre modèle final.

LeNet5 de A à Z

Regardons d'abord le code :

#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

Définir le modèle LeNet5

Je vais expliquer le code de manière linéaire :

  • Dans PyTorch, nous définissons un réseau de neurones en créant une classe qui hérite de nn.Module, car elle contient de nombreuses méthodes que nous devrons utiliser.
  • Il y a deux étapes principales après cela. La première consistera à initialiser les couches que nous allons utiliser dans notre CNN à l'intérieur de __init__, et l'autre est de définir la séquence dans laquelle ces couches traiteront l'image. Cela est défini à l'intérieur de la fonction forward.
  • Pour l'architecture elle-même, nous commençons par définir les couches convolutionnelles en utilisant la fonction nn.Conv2D avec la taille de noyau appropriée et les canaux d'entrée/sortie. Nous appliquons également un max pooling en utilisant la fonction nn.MaxPool2D. L'avantage de PyTorch est que nous pouvons combiner la couche convolutionnelle, la fonction d'activation et le max pooling en une seule couche (elles seront appliquées séparément, mais cela aide à l'organisation) en utilisant la fonction nn.Sequential.
  • Ensuite, nous définissons les couches entièrement connectées. Notez que nous pouvons également utiliser nn.Sequential ici et combiner les fonctions d'activation et les couches linéaires, mais je voulais montrer que l'un ou l'autre est possible.
  • Enfin, notre dernière couche produit 10 neurones qui représentent nos prédictions finales pour les chiffres.

Réglage des hyperparamètres

Avant l'entraînement, nous devons définir certains hyperparamètres, tels que la fonction de perte et l'optimiseur à utiliser.

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)

Nous commençons par initialiser notre modèle en utilisant le nombre de classes comme argument, qui dans ce cas est 10. Ensuite, nous définissons notre fonction de coût comme une perte d'entropie croisée et l'optimiseur comme Adam. Il existe de nombreux choix pour cela, mais ceux-ci tendent à donner de bons résultats avec le modèle et les données fournies. Enfin, nous définissons total_step pour mieux suivre les étapes lors de l'entraînement.

Entraînement du modèle

Maintenant, nous pouvons entraîner notre modèle :

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(epoch + 1, num_epochs, i + 1, total_step, loss.item())

Voyons ce que fait le code :

  • Nous commençons par itérer à travers le nombre d'époques, puis les lots dans nos données d'entraînement.
  • Nous.convertissons les images et les étiquettes en fonction de l'appareil que nous utilisons, c'est-à-dire GPU ou CPU.
  • Dans la propagation avant, nous faisons des prédictions en utilisant notre modèle et calculons la perte basée sur ces prédictions et nos étiquettes réelles.
  • Ensuite, nous effectuons la rétropropagation où nous mettons réellement à jour nos poids pour améliorer notre modèle.
  • Nous mettons ensuite les gradients à zéro avant chaque mise à jour en utilisant la fonction optimizer.zero_grad().
  • Ensuite, nous calculons les nouveaux gradients en utilisant la fonction loss.backward().
  • Et enfin, nous mettons à jour les poids avec la fonction optimizer.step().

Nous pouvons voir le résultat comme suit :

Comme nous pouvons le voir, la perte diminue avec chaque époque, ce qui montre que notre modèle apprend effectivement. Notez que cette perte est sur l'ensemble d'entraînement, et si la perte est beaucoup trop petite (comme c'est le cas ici), cela peut indiquer un surapprentissage. Il existe plusieurs façons de résoudre ce problème, telles que la régularisation, l'augmentation des données, etc., mais nous n'entrerons pas dans ce sujet dans cet article. Testons maintenant notre modèle pour voir comment il se comporte.

Test du modèle

Testons maintenant notre modèle :

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))

Comme vous pouvez le voir, le code n'est pas si différent de celui pour l'entraînement. La seule différence est que nous ne calculons pas les gradients (en utilisant with torch.no_grad()), et nous ne calculons pas non plus la perte car nous n'avons pas besoin de rétropropager ici. Pour calculer la précision résultante du modèle, nous pouvons simplement calculer le nombre total de prédictions correctes par rapport au nombre total d'images.

En utilisant ce modèle, nous obtenons une précision d'environ 98,8 %, ce qui est plutôt bon :

Tester la précision

Notez que le jeu de données MNIST est assez basique et petit selon les normes d'aujourd'hui, et des résultats similaires sont difficiles à obtenir pour d'autres jeux de données. Néanmoins, c'est un bon point de départ pour apprendre l'apprentissage profond et les CNN.

Conclusion

Concluons maintenant ce que nous avons fait dans cet article :

  • Nous avons commencé par apprendre l'architecture de LeNet5 et les différents types de couches qui la composent.
  • Ensuite, nous avons exploré le jeu de données MNIST et chargé les données en utilisant torchvision.
  • Ensuite, nous avons construit LeNet5 à partir de zéro tout en définissant les hyperparamètres pour le modèle.
  • Enfin, nous avons entraîné et testé notre modèle sur le jeu de données MNIST, et le modèle semblait bien performer sur le jeu de données de test.

Travail futur

Bien que cela semble être une très bonne introduction à l'apprentissage profond avec PyTorch, vous pouvez étendre ce travail pour en apprendre davantage également :

  • Vous pouvez essayer d'utiliser différents ensembles de données, mais pour ce modèle, vous aurez besoin d'ensembles de données en niveaux de gris. Un tel ensemble de données est FashionMNIST.
  • Vous pouvez expérimenter avec différents hyperparamètres et voir la meilleure combinaison d'entre eux pour le modèle.
  • Enfin, vous pouvez essayer d'ajouter ou de supprimer des couches du jeu de données pour voir leur impact sur les capacités du modèle.