Normalizzazione batch nelle reti neurali convoluzionali
La normalizzazione batch è un termine comunemente menzionato nel contesto delle reti neurali convoluzionali. In questo articolo esploreremo cosa comporta effettivamente e i suoi eventuali effetti sulle prestazioni o sul comportamento generale delle reti neurali convoluzionali.
Prerequisiti
- Python: per eseguire il codice qui all'interno, la tua macchina avrà bisogno di Python installato. I lettori dovrebbero avere un'esperienza di base nella codifica Python prima di continuare
- Nozioni di base sul deep learning: questo articolo tratta i concetti essenziali per applicare la teoria del deep learning e ci si aspetta che i lettori abbiano una certa esperienza con i termini pertinenti e la teoria di base.
Il termine normalizzazione
import torch
import torch.nn as nn
import torch.nn.functional as F
import torchvision
import torchvision.transforms as transforms
import torchvision.datasets as Datasets
from torch.utils.data import Dataset, DataLoader
import numpy as np
import matplotlib.pyplot as plt
import cv2
from tqdm.notebook import tqdm
import seaborn as sns
from torchvision.utils import make_grid
if torch.cuda.is_available():
device = torch.device('cuda:0')
print('Running on the GPU')
else:
device = torch.device('cpu')
print('Running on the CPU')
La normalizzazione in statistica si riferisce al processo di vincolo dei dati o di un insieme di valori compresi nell'intervallo compreso tra 0 e 1. Piuttosto inopportunamente, in alcuni ambienti la normalizzazione si riferisce anche al processo di impostazione della media di una distribuzione di dati su zero e della sua deviazione standard a 1.
In realtà, questo processo di impostare la media di una distribuzione su 0 e la sua deviazione standard su 1 è chiamato standardizzazione. A causa di alcune libertà, tuttavia, viene anche chiamata normalizzazione o normalizzazione del punteggio z. È importante imparare questa distinzione e tenerla presente.
Preelaborazione dei dati
La preelaborazione dei dati si riferisce ai passaggi eseguiti nella preparazione dei dati prima di essere forniti a un algoritmo di machine learning o deep learning. I due processi (normalizzazione e standardizzazione) menzionati nella sezione precedente sono fasi di preelaborazione dei dati.
Normalizzazione min-max
La normalizzazione min-max è uno dei metodi più comuni per normalizzare i dati. Tipico del suo nome, vincola i punti dati all'interno dell'intervallo compreso tra 0 e 1 impostando il valore minimo nel set di dati su 0, il massimo su 1 e tutto il resto scalato di conseguenza. L'equazione seguente fornisce una descrizione matematica del processo di normalizzazione min-max. Essenzialmente si tratta di sottrarre il valore minimo nel set di dati da ciascun punto dati, quindi dividerlo per l'intervallo (massimo - minimo).
Utilizzando la funzione seguente possiamo replicare il processo di normalizzazione min-max. Utilizzando questa funzione possiamo sviluppare un'intuizione su ciò che realmente accade dietro le quinte.
def min_max_normalize(data_points: np.array):
"""
This function normalizes data by constraining
data points between the range of 0 & 1
"""
# convert list to numpy array
if type(data_points) == list:
data_points = np.array(data_points)
else:
pass
# create a list to hold normalized data
normalized = []
# derive minimum and maximum values
minimum = data_points.min()
maximum = data_points.max()
# convert to list for iteration
data_points = list(data_points)
# normalizing data
for value in data_points:
normalize = (value-minimum)/(maximum-minimum)
normalized.append(round(normalize, 2))
return np.array(normalized)
Creiamo una serie di valori casuali utilizzando NumPy, quindi proviamo a normalizzarli utilizzando la funzione di normalizzazione min-max definita sopra.
# creating a random set of data points
data = np.random.rand(50)*20
# normalizing data points
normalized = min_max_normalize(data)
Dai grafici sottostanti, si può vedere che prima della normalizzazione, i valori variavano da o a 20 con una grande maggioranza di punti dati con valori compresi tra 5 e 10. Dopo la normalizzazione, tuttavia, si può vedere che i valori ora variano tra 0 e 1 con la stragrande maggioranza dei punti dati ha valori compresi tra 0,25 e 0,5. Nota: se/quando esegui questo codice, la distribuzione dei dati sarà diversa da quella utilizzata in questo articolo poiché viene generata in modo casuale.
# visualising distribution
figure, axes = plt.subplots(1, 2, sharey=True, dpi=100)
sns.histplot(data, ax=axes[0])
axes[0].set_title('unnormalized')
sns.histplot(normalized, ax=axes[1])
axes[1].set_title('min-max normalized')
Normalizzazione del punteggio Z
La normalizzazione del punteggio Z, chiamata anche standardizzazione, è il processo che imposta la media e la deviazione standard di una distribuzione di dati rispettivamente su 0 e 1. L'equazione seguente è l'equazione matematica che governa la normalizzazione del punteggio z, comporta la sottrazione della media della distribuzione dal valore da normalizzare prima di dividere per la deviazione standard della distribuzione.
La funzione definita di seguito replica il processo di normalizzazione dello z-score, con questa funzione possiamo vedere più da vicino cosa comporta effettivamente.
def z_score_normalize(data_points: np.array):
"""
This function normalizes data by computing
their z-scores
"""
# convert list to numpy array
if type(data_points) == list:
data_points = np.array(data_points)
else:
pass
# create a list to hold normalized data
normalized = []
# derive mean and and standard deviation
mean = data_points.mean()
std = data_points.std()
# convert to list for iteration
data_points = list(data_points)
# normalizing data
for value in data_points:
normalize = (value-mean)/std
normalized.append(round(normalize, 2))
return np.array(normalized)
Utilizzando la distribuzione dei dati generata nella sezione precedente, proviamo a normalizzare i punti dati utilizzando la funzione z-score.
# normalizing data points
z_normalized = z_score_normalize(data)
# check the mean value
z_normalized.mean()
>>>> -0.0006
# check the standard deviation
z_normalized.std()
>>>> 1.0000
Ancora una volta, dalle visualizzazioni, possiamo vedere che la distribuzione originale ha valori che vanno da 0 a 20 mentre i valori normalizzati del punteggio z sono ora centrati attorno a 0 (una media di zero) e un intervallo di circa -1,5 a 1,5 che è una gamma più gestibile.
# visualizing distributions
figure, axes = plt.subplots(1, 2, sharey=True, dpi=100)
sns.histplot(data, ax=axes[0])
axes[0].set_title('unnormalized')
sns.histplot(z_normalized, ax=axes[1])
axes[1].set_title('z-score normalized')
Motivi della preelaborazione
Quando consideriamo i dati nell'apprendimento automatico, consideriamo i singoli punti dati come funzionalità. Tutte queste funzionalità in genere non sono sulla stessa scala. Ad esempio, considera una casa con 3 camere da letto e un soggiorno di 400 piedi quadrati. Queste due funzionalità sono su scale così distanti che se vengono inserite in un algoritmo di apprendimento automatico previsto per essere ottimizzato mediante la discesa del gradiente. L'ottimizzazione sarebbe piuttosto noiosa, poiché la funzionalità con la scala più grande avrà la precedenza su tutte le altre. Per facilitare il processo di ottimizzazione, è una buona idea avere tutti i punti dati all'interno della stessa scala.
Normalizzazione negli strati di convoluzione
I punti dati in un'immagine sono i suoi pixel. I valori dei pixel in genere vanno da 0 a 255; ecco perché, prima di inserire le immagini in una rete neurale convoluzionale, è una buona idea normalizzarle in qualche modo in modo da mettere tutti i pixel in un intervallo gestibile.
Anche quando ciò viene fatto, durante l'addestramento di un convnet, i pesi (elementi nei suoi filtri) potrebbero diventare troppo grandi e quindi produrre mappe di caratteristiche con pixel distribuiti su un'ampia gamma. Ciò essenzialmente rende alquanto inutile la normalizzazione effettuata durante la fase di preelaborazione. Inoltre, ciò potrebbe ostacolare il processo di ottimizzazione rendendolo lento o in casi estremi potrebbe portare a un problema chiamato gradienti instabili, che potrebbe sostanzialmente impedire a convnet di ottimizzare ulteriormente i suoi pesi.
Per prevenire questo problema, viene introdotta una normalizzazione in ogni strato del convento. Questa normalizzazione è denominata Normalizzazione batch.
Il processo di normalizzazione batch
La normalizzazione batch essenzialmente imposta i pixel in tutte le mappe delle caratteristiche in uno strato di convoluzione su una nuova media e una nuova deviazione standard. In genere, si inizia normalizzando il punteggio z di tutti i pixel, quindi si continua moltiplicando i valori normalizzati per un parametro arbitrario alfa (scala) prima di aggiungere un altro parametro arbitrario beta (offset).
Questi due parametri alfa e beta sono parametri apprendibili che convnet utilizzerà quindi per garantire che i valori dei pixel nelle mappe delle caratteristiche rientrino in un intervallo gestibile, migliorando così il problema dei gradienti instabili.
Normalizzazione batch in azione
Per valutare realmente gli effetti della normalizzazione batch negli strati di convoluzione, dobbiamo confrontare due convnet, uno senza normalizzazione batch e l'altro con normalizzazione batch. Per questo utilizzeremo l'architettura LeNet-5 e il set di dati MNIST.
Classe di set di dati e rete neurale convoluzionale
In questo articolo, il set di dati MNIST verrà utilizzato per scopi di benchmarking come menzionato in precedenza. Questo set di dati è costituito da immagini da 28 x 28 pixel di cifre scritte a mano che vanno dalle cifre da 0 a 9 etichettate di conseguenza.
Immagini campione dal set di dati MNIST.
Può essere caricato in PyTorch utilizzando il blocco di codice seguente. Il set di training è composto da 60.000 immagini mentre il set di validazione è composto da 10.000 immagini. Poiché utilizzeremo questo set di dati con LeNet-5, le immagini dovranno essere ridimensionate a 32 x 32 pixel come definito nel parametro trasforma.
# loading training data
training_set = Datasets.MNIST(root='./', download=True,
transform=transforms.Compose([transforms.ToTensor(),
transforms.Resize((32, 32))]))
# loading validation data
validation_set = Datasets.MNIST(root='./', download=True, train=False,
transform=transforms.Compose([transforms.ToTensor(),
transforms.Resize((32, 32))]))
Per la formazione e l'utilizzo delle nostre reti di connessione, utilizzeremo la classe seguente denominata "ConvolutionalNeuralNet()". Questa classe contiene metodi che aiuteranno ad addestrare e classificare le istanze utilizzando il convnet addestrato. Il metodo train() contiene anche funzioni di supporto interne come init_weights() e accuratezza.
class ConvolutionalNeuralNet():
def __init__(self, network):
self.network = network.to(device)
self.optimizer = torch.optim.Adam(self.network.parameters(), lr=1e-3)
def train(self, loss_function, epochs, batch_size,
training_set, validation_set):
# creating log
log_dict = {
'training_loss_per_batch': [],
'validation_loss_per_batch': [],
'training_accuracy_per_epoch': [],
'validation_accuracy_per_epoch': []
}
# defining weight initialization function
def init_weights(module):
if isinstance(module, nn.Conv2d):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
elif isinstance(module, nn.Linear):
torch.nn.init.xavier_uniform_(module.weight)
module.bias.data.fill_(0.01)
# defining accuracy function
def accuracy(network, dataloader):
network.eval()
total_correct = 0
total_instances = 0
for images, labels in tqdm(dataloader):
images, labels = images.to(device), labels.to(device)
predictions = torch.argmax(network(images), dim=1)
correct_predictions = sum(predictions==labels).item()
total_correct+=correct_predictions
total_instances+=len(images)
return round(total_correct/total_instances, 3)
# initializing network weights
self.network.apply(init_weights)
# creating dataloaders
train_loader = DataLoader(training_set, batch_size)
val_loader = DataLoader(validation_set, batch_size)
# setting convnet to training mode
self.network.train()
for epoch in range(epochs):
print(f'Epoch {epoch+1}/{epochs}')
train_losses = []
# training
print('training...')
for images, labels in tqdm(train_loader):
# sending data to device
images, labels = images.to(device), labels.to(device)
# resetting gradients
self.optimizer.zero_grad()
# making predictions
predictions = self.network(images)
# computing loss
loss = loss_function(predictions, labels)
log_dict['training_loss_per_batch'].append(loss.item())
train_losses.append(loss.item())
# computing gradients
loss.backward()
# updating weights
self.optimizer.step()
with torch.no_grad():
print('deriving training accuracy...')
# computing training accuracy
train_accuracy = accuracy(self.network, train_loader)
log_dict['training_accuracy_per_epoch'].append(train_accuracy)
# validation
print('validating...')
val_losses = []
# setting convnet to evaluation mode
self.network.eval()
with torch.no_grad():
for images, labels in tqdm(val_loader):
# sending data to device
images, labels = images.to(device), labels.to(device)
# making predictions
predictions = self.network(images)
# computing loss
val_loss = loss_function(predictions, labels)
log_dict['validation_loss_per_batch'].append(val_loss.item())
val_losses.append(val_loss.item())
# computing accuracy
print('deriving validation accuracy...')
val_accuracy = accuracy(self.network, val_loader)
log_dict['validation_accuracy_per_epoch'].append(val_accuracy)
train_losses = np.array(train_losses).mean()
val_losses = np.array(val_losses).mean()
print(f'training_loss: {round(train_losses, 4)} training_accuracy: '+
f'{train_accuracy} validation_loss: {round(val_losses, 4)} '+
f'validation_accuracy: {val_accuracy}\n')
return log_dict
def predict(self, x):
return self.network(x)
Lenet-5
LeNet-5 (Y. Lecun et al) è una delle prime reti neurali convoluzionali progettate specificamente per riconoscere/classificare immagini di cifre scritte a mano. La sua architettura è illustrata nell'immagine sopra e la sua implementazione in PyTorch è fornita nel seguente blocco di codice.
class LeNet5(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
self.pool1 = nn.AvgPool2d(2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.pool2 = nn.AvgPool2d(2)
self.linear1 = nn.Linear(5*5*16, 120)
self.linear2 = nn.Linear(120, 84)
self.linear3 = nn. Linear(84, 10)
def forward(self, x):
x = x.view(-1, 1, 32, 32)
#----------
# LAYER 1
#----------
output_1 = self.conv1(x)
output_1 = torch.tanh(output_1)
output_1 = self.pool1(output_1)
#----------
# LAYER 2
#----------
output_2 = self.conv2(output_1)
output_2 = torch.tanh(output_2)
output_2 = self.pool2(output_2)
#----------
# FLATTEN
#----------
output_2 = output_2.view(-1, 5*5*16)
#----------
# LAYER 3
#----------
output_3 = self.linear1(output_2)
output_3 = torch.tanh(output_3)
#----------
# LAYER 4
#----------
output_4 = self.linear2(output_3)
output_4 = torch.tanh(output_4)
#-------------
# OUTPUT LAYER
#-------------
output_5 = self.linear3(output_4)
return(F.softmax(output_5, dim=1))
Utilizzando l'architettura LeNet-5 sopra definita, creeremo un'istanza di model_1
, un membro della classe ConvolutionalNeuralNet, con i parametri visualizzati nel blocco di codice. Questo modello servirà come base di riferimento per scopi di benchmarking.
# training model 1
model_1 = ConvolutionalNeuralNet(LeNet5())
log_dict_1 = model_1.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_set, validation_set=validation_set)
Dopo l'addestramento per 10 epoche e la visualizzazione della precisione dal registro metrico che riceviamo in cambio, possiamo vedere che sia la precisione dell'addestramento che quella della convalida sono aumentate nel corso dell'addestramento. Nel nostro esperimento, l'accuratezza della validazione è iniziata a circa il 93% dopo la prima epoca prima di procedere ad aumentare costantemente nelle successive 9 iterazioni, per terminare infine a poco più del 98% entro l'epoca 10.
sns.lineplot(y=log_dict_1['training_accuracy_per_epoch'], x=range(len(log_dict_1['training_accuracy_per_epoch'])), label='training')
sns.lineplot(y=log_dict_1['validation_accuracy_per_epoch'], x=range(len(log_dict_1['validation_accuracy_per_epoch'])), label='validation')
plt.xlabel('epoch')
plt.ylabel('accuracy')
LeNet-5 normalizzato in batch
Poiché il tema di questo articolo è incentrato sulla normalizzazione batch negli strati di convoluzione, la norma batch viene applicata solo sui due strati di convoluzione presenti in questa architettura, come illustrato nell'immagine sopra.
class LeNet5_BatchNorm(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 6, 5)
self.batchnorm1 = nn.BatchNorm2d(6)
self.pool1 = nn.AvgPool2d(2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.batchnorm2 = nn.BatchNorm2d(16)
self.pool2 = nn.AvgPool2d(2)
self.linear1 = nn.Linear(5*5*16, 120)
self.linear2 = nn.Linear(120, 84)
self.linear3 = nn. Linear(84, 10)
def forward(self, x):
x = x.view(-1, 1, 32, 32)
#----------
# LAYER 1
#----------
output_1 = self.conv1(x)
output_1 = torch.tanh(output_1)
output_1 = self.batchnorm1(output_1)
output_1 = self.pool1(output_1)
#----------
# LAYER 2
#----------
output_2 = self.conv2(output_1)
output_2 = torch.tanh(output_2)
output_2 = self.batchnorm2(output_2)
output_2 = self.pool2(output_2)
#----------
# FLATTEN
#----------
output_2 = output_2.view(-1, 5*5*16)
#----------
# LAYER 3
#----------
output_3 = self.linear1(output_2)
output_3 = torch.tanh(output_3)
#----------
# LAYER 4
#----------
output_4 = self.linear2(output_3)
output_4 = torch.tanh(output_4)
#-------------
# OUTPUT LAYER
#-------------
output_5 = self.linear3(output_4)
return(F.softmax(output_5, dim=1))
Utilizzando il segmento di codice riportato di seguito, possiamo creare un'istanza di model_2
con la normalizzazione batch inclusa e iniziare l'addestramento con gli stessi parametri di model_1. Quindi, forniamo punteggi di precisione...
# training model 2
model_2 = ConvolutionalNeuralNet(LeNet5_BatchNorm())
log_dict_2 = model_2.train(nn.CrossEntropyLoss(), epochs=10, batch_size=64,
training_set=training_set, validation_set=validation_set)
Osservando il grafico, è chiaro che sia la precisione dell'addestramento che quella della validazione sono aumentate nel corso dell'addestramento in modo simile al modello senza normalizzazione batch. L'accuratezza della validazione dopo la prima epoca era appena superiore al 95%, 3 punti percentuali in più rispetto a model_1
nello stesso punto, prima di aumentare gradualmente e culminare a circa il 98,5%, 0,5% in più rispetto a model_1< /codice>.
sns.lineplot(y=log_dict_2['training_accuracy_per_epoch'], x=range(len(log_dict_2['training_accuracy_per_epoch'])), label='training')
sns.lineplot(y=log_dict_2['validation_accuracy_per_epoch'], x=range(len(log_dict_2['validation_accuracy_per_epoch'])), label='validation')
plt.xlabel('epoch')
plt.ylabel('accuracy')
Confronto di modelli
Confrontando entrambi i modelli, è chiaro che il modello LeNet-5 con strati di convoluzione normalizzati in batch ha sovraperformato il modello normale senza strati di convoluzione normalizzati in batch. Si può quindi affermare con certezza che la normalizzazione batch ha contribuito ad aumentare le prestazioni in questo caso.
Confrontando le perdite di addestramento e validazione tra i modelli LeNet-5 regolari e normalizzati per batch si vede anche che il modello normalizzato per batch raggiunge valori di perdita inferiori più velocemente rispetto al modello normale. Questo è un puntatore alla normalizzazione batch che aumenta la velocità con cui il modello ottimizza i suoi pesi nella direzione corretta o in altre parole, la normalizzazione batch aumenta la velocità con cui il convnet apprende.
Perdite di formazione e convalida.
Osservazioni finali
In questo articolo, abbiamo esplorato cosa comporta la normalizzazione in un contesto di machine learning/deep learning. Abbiamo anche esplorato i processi di normalizzazione come fasi di preelaborazione dei dati e come la normalizzazione possa essere portata oltre la preelaborazione e negli strati di convoluzione tramite il processo di normalizzazione batch.
Successivamente, abbiamo esaminato il processo di normalizzazione batch stesso prima di valutarne gli effetti confrontando due varianti di convnet LeNet-5 (una senza norma batch e l'altra con norma batch) sul set di dati MNIST. Dai risultati, abbiamo dedotto che la normalizzazione dei lotti ha contribuito ad un aumento delle prestazioni e alla velocità di ottimizzazione del peso. Ci sono stati anche alcuni suggerimenti che impediscono lo spostamento della covariata interna, ma su questo potrebbe anche non essere stato raggiunto un consenso.