Ricerca nel sito web

Scrivere LeNet5 da zero in PyTorch


Introduzione

In questo articolo costruiremo una delle prime reti neurali convoluzionali mai introdotte (LeNet5). Stiamo costruendo questa CNN da zero in PyTorch e vedremo anche come si comporta su un set di dati del mondo reale.

Inizieremo esplorando l'architettura di LeNet5. Quindi caricheremo e analizzeremo il nostro set di dati, MNIST, utilizzando la classe fornita da torchvision. Utilizzando PyTorch, costruiremo il nostro LeNet5 da zero e lo addestreremo sui nostri dati. Infine, vedremo come si comporta il modello sui dati di test invisibili.

Prerequisiti

La conoscenza delle reti neurali sarà utile per comprendere questo articolo. Ciò significa avere familiarità con i diversi livelli delle reti neurali (livello di input, livelli nascosti, livello di output), funzioni di attivazione, algoritmi di ottimizzazione (varianti della discesa del gradiente), funzioni di perdita, ecc. Inoltre, familiarità con la sintassi Python e la libreria PyTorch è essenziale per comprendere i frammenti di codice presentati in questo articolo.

Si consiglia inoltre la conoscenza delle CNN. Ciò include la conoscenza dei livelli convoluzionali, dei livelli di pooling e del loro ruolo nell'estrazione di caratteristiche dai dati di input. Comprendere concetti come passo, riempimento e impatto della dimensione del kernel/filtro è utile.

LeNet5

LeNet5 è stato utilizzato per il riconoscimento dei caratteri scritti a mano ed è stato proposto da Yann LeCun e altri nel 1998 con l'articolo Gradient-Based Learning Applied to Document Recognition.

Comprendiamo l'architettura di LeNet5 come mostrato nella figura seguente:

Come indica il nome, LeNet5 ha 5 livelli di cui due convoluzionali e tre completamente connessi. Cominciamo con l'input. LeNet5 accetta come input un'immagine in scala di grigi di 32x32, indicando che l'architettura non è adatta per immagini RGB (canali multipli). Quindi l'immagine di input dovrebbe contenere un solo canale. Successivamente, iniziamo con i nostri livelli convoluzionali

Il primo strato convoluzionale ha una dimensione del filtro di 5x5 con 6 filtri di questo tipo. Ciò ridurrà la larghezza e l'altezza dell'immagine aumentando la profondità (numero di canali). L'output sarebbe 28x28x6. Successivamente, viene applicato il pooling per ridurre della metà la mappa delle caratteristiche, ovvero 14x14x6. La stessa dimensione del filtro (5x5) con 16 filtri viene ora applicata all'output seguita da un livello di pooling. Ciò riduce la mappa delle funzionalità di output a 5x5x16.

Successivamente, viene applicato uno strato convoluzionale di dimensioni 5x5 con 120 filtri per appiattire la mappa delle caratteristiche a 120 valori. Poi arriva il primo strato completamente connesso, con 84 neuroni. Infine, abbiamo lo strato di output che ha 10 neuroni di output, poiché i dati MNIST hanno 10 classi per ciascuna delle 10 cifre numeriche rappresentate.

Caricamento dati

Iniziamo caricando e analizzando i dati. Utilizzeremo il set di dati MNIST. Il set di dati MNIST contiene immagini di cifre numeriche scritte a mano. Le immagini sono in scala di grigi, tutte con una dimensione di 28x28, e sono composte da 60.000 immagini di training e 10.000 immagini di test.

Puoi vedere alcuni esempi di immagini qui sotto:

Importazione delle librerie

Iniziamo importando le librerie richieste e definendo alcune variabili (anche gli iperparametri e il device sono dettagliati per aiutare il pacchetto a determinare se addestrarsi su GPU o 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')

Caricamento e trasformazione dei dati

Utilizzando torchvision, caricheremo il set di dati in quanto ciò ci consentirà di eseguire facilmente qualsiasi passaggio di pre-elaborazione.

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

Capiamo il codice:

  • In primo luogo, i dati MNIST non possono essere utilizzati come per l’architettura LeNet5. L'architettura LeNet5 accetta che l'input sia 32x32 e le immagini MNIST siano 28x28. Possiamo risolvere questo problema ridimensionando le immagini, normalizzandole utilizzando la media e la deviazione standard precalcolate (disponibili online) e infine memorizzandole come tensori.
  • Impostiamo download=True nel caso in cui i dati non siano già scaricati.
  • Successivamente, utilizziamo i caricatori di dati. Ciò potrebbe non influire sulle prestazioni nel caso di un set di dati di piccole dimensioni come MNIST, ma può realmente ostacolare le prestazioni in caso di set di dati di grandi dimensioni ed è generalmente considerata una buona pratica. I caricatori di dati ci consentono di scorrere i dati in batch e i dati vengono caricati durante l'iterazione e non immediatamente all'avvio.
  • Specifichiamo la dimensione del batch e mescoliamo il set di dati durante il caricamento in modo che ogni batch presenti qualche variazione nei tipi di etichette che ha. Ciò aumenterà l’efficacia del nostro modello finale.

LeNet5 da zero

Diamo prima un'occhiata al codice:

#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

Definizione del modello LeNet5

Spiegherò il codice in modo lineare:

  • In PyTorch, definiamo una rete neurale creando una classe che eredita da nn.Module poiché contiene molti dei metodi che dovremo utilizzare.
  • Successivamente ci sono due passaggi principali. Il primo è inizializzare i livelli che utilizzeremo nella nostra CNN all'interno di __init__ e l'altro è definire la sequenza in cui tali livelli elaboreranno l'immagine. Questo è definito all'interno della funzione forward.
  • Per l'architettura stessa, definiamo prima gli strati convoluzionali utilizzando la funzione nn.Conv2D con la dimensione del kernel appropriata e i canali di input/output. Applichiamo anche il pooling massimo utilizzando la funzione nn.MaxPool2D. La cosa bella di PyTorch è che possiamo combinare il livello convoluzionale, la funzione di attivazione e il pooling massimo in un unico livello (verranno applicati separatamente, ma aiuta con l'organizzazione) utilizzando la funzione nn.Sequential .
  • Quindi definiamo gli strati completamente connessi. Nota che possiamo usare nn.Sequential anche qui e combinare le funzioni di attivazione e i livelli lineari, ma volevo mostrare che entrambe le cose sono possibili.
  • Infine, il nostro ultimo strato produce 10 neuroni che sono le nostre previsioni finali per le cifre.

Impostazione degli iperparametri

Prima dell'allenamento dobbiamo impostare alcuni iperparametri, come la funzione di perdita e l'ottimizzatore da utilizzare.

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)

Iniziamo inizializzando il nostro modello utilizzando il numero di classi come argomento, che in questo caso è 10. Quindi definiamo la nostra funzione di costo come perdita di entropia incrociata e ottimizzatore come Adam. Ci sono molte scelte per questi, ma questi tendono a dare buoni risultati con il modello e i dati forniti. Infine, definiamo total_step per tenere traccia migliore dei passaggi durante l'allenamento.

Formazione del modello

Ora possiamo addestrare il nostro modello:

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(epoca+1, num_epoche, i+1, passo_totale, perdita.oggetto()))

Vediamo cosa fa il codice:

  • Iniziamo ripetendo il numero di epoche e quindi i batch nei nostri dati di addestramento.
  • Convertiamo le immagini e le etichette in base al dispositivo che stiamo utilizzando, ovvero GPU o CPU.
  • Nel passaggio in avanti, facciamo previsioni utilizzando il nostro modello e calcoliamo la perdita in base a tali previsioni e alle nostre etichette effettive.
  • Successivamente, eseguiamo il passaggio all'indietro in cui aggiorniamo effettivamente i nostri pesi per migliorare il nostro modello
  • Quindi impostiamo i gradienti su zero prima di ogni aggiornamento utilizzando la funzione optimizer.zero_grad().
  • Quindi, calcoliamo i nuovi gradienti utilizzando la funzione loss.backward().
  • Infine, aggiorniamo i pesi con la funzione optimizer.step().

Possiamo vedere l'output come segue:

Come possiamo vedere, la perdita diminuisce ad ogni epoca, il che dimostra che il nostro modello sta effettivamente imparando. Si noti che questa perdita è nel set di addestramento e, se la perdita è troppo piccola (come nel nostro caso), può indicare un adattamento eccessivo. Esistono diversi modi per risolvere questo problema come la regolarizzazione, l'aumento dei dati e così via, ma non ne parleremo in questo articolo. Ora testiamo il nostro modello per vedere come si comporta.

Test del modello

Testiamo ora il nostro modello:

# Test the modelIn 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))

Come puoi vedere, il codice non è molto diverso da quello per l'allenamento. L'unica differenza è che non stiamo calcolando i gradienti (usando con torch.no_grad()), e nemmeno calcolando la perdita perché non abbiamo bisogno di backpropagare qui. Per calcolare la precisione risultante del modello, possiamo semplicemente calcolare il numero totale di previsioni corrette sul numero totale di immagini.

Usando questo modello, otteniamo una precisione del 98,8% circa, che è abbastanza buona:

Precisione dei test

Tieni presente che il set di dati MNIST è piuttosto semplice e piccolo per gli standard odierni e risultati simili sono difficili da ottenere per altri set di dati. Tuttavia, è un buon punto di partenza quando si impara il deep learning e le CNN.

Conclusione

Concludiamo ora quello che abbiamo fatto in questo articolo:

  • Abbiamo iniziato imparando l'architettura di LeNet5 e i diversi tipi di livelli in essa contenuti.
  • Successivamente, abbiamo esplorato il set di dati MNIST e caricato i dati utilizzando torchvision.
  • Quindi, abbiamo creato LeNet5 da zero insieme alla definizione degli iperparametri per il modello.
  • Infine, abbiamo addestrato e testato il nostro modello sul set di dati MNIST e il modello sembrava funzionare bene sul set di dati di test.

Lavoro futuro

Sebbene questa sembri un'ottima introduzione al deep learning in PyTorch, puoi estendere questo lavoro anche per saperne di più:

  • Puoi provare a utilizzare set di dati diversi, ma per questo modello avrai bisogno di set di dati in scala di grigi. Uno di questi set di dati è FashionMNIST.
  • Puoi sperimentare diversi iperparametri e vederne la migliore combinazione per il modello.
  • Infine, puoi provare ad aggiungere o rimuovere livelli dal set di dati per vedere il loro impatto sulla capacità del modello.

Articoli correlati: