Recherche de site Web

Mise en commun mondiale dans les réseaux de neurones convolutifs


Introduction

Les opérations de pooling sont depuis un certain temps un pilier des réseaux de neurones convolutifs. Alors que des processus tels que le pooling maximum et le pooling moyen ont souvent occupé une place plus centrale, leurs cousins moins connus, le pooling maximum global et le pooling moyen global, sont devenus tout aussi importants. Dans cet article, nous explorerons ce qu’impliquent les variantes globales des deux techniques de pooling courantes et comment elles se comparent.

#  article dependencies
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')

Conditions préalables

  • Compréhension de base des CNN : Familiarité avec l'architecture des CNN, y compris les couches telles que les couches convolutionnelles, de pooling et entièrement connectées.
  • Concepts de pooling : connaissance des techniques de pooling courantes (par exemple, pooling maximum, pooling moyen) utilisées pour réduire les dimensions spatiales dans les CNN.
  • Algèbre linéaire et opérations tensorielles : compréhension des opérations matricielles et des manipulations tensorielles, car la mise en commun globale implique de réduire un tenseur multidimensionnel à une dimension inférieure.
  • Fonctions d'activation : connaissance de base de l'impact des fonctions d'activation (par exemple, ReLU, sigmoïde) sur les fonctionnalités extraites par les couches CNN.
  • Maîtrise du framework : expérience avec des frameworks d'apprentissage profond tels que TensorFlow ou PyTorch, en particulier dans la mise en œuvre de couches de pooling personnalisées.

Le réseau neuronal convolutif classique

De nombreux débutants en vision par ordinateur sont souvent initiés aux réseaux de neurones convolutifs comme réseau de neurones idéal pour les données d'image, car ils conservent la structure spatiale de l'image d'entrée tout en apprenant/en extrayant des caractéristiques. Ce faisant, il est capable d'apprendre les relations entre les pixels voisins et la position des objets dans l'image, ce qui en fait un réseau neuronal très puissant.

Un perceptron multicouche fonctionnerait également dans un contexte de classification d'images, mais ses performances seront sévèrement dégradées par rapport à son homologue convnet simplement parce qu'il détruit immédiatement la structure spatiale de l'image en l'aplatissant/vectorisant, supprimant ainsi la plupart des relations entre les images voisines. pixels.

Combo extracteur de fonctionnalités et classificateur

De nombreux réseaux de neurones convolutifs classiques sont en fait une combinaison de convnets et de MLP. En regardant les architectures de LeNet et AlexNet par exemple, on peut clairement voir que leurs architectures ne sont que quelques couches de convolution avec des couches linéaires attachées à la fin.

Cette configuration a beaucoup de sens, elle a permis aux couches de convolution de faire ce qu'elles font de mieux, c'est-à-dire extraire des caractéristiques dans des données à deux dimensions spatiales. Ensuite, les caractéristiques extraites sont transmises sur des couches linéaires afin qu'elles puissent également faire ce pour quoi elles excellent, trouver des relations entre les vecteurs de caractéristiques et les cibles.

Un défaut dans la conception

Le problème de cette conception est que les couches linéaires ont une très forte propension à surajuster les données. La régularisation des abandons a été introduite pour aider à atténuer ce problème, mais il demeure néanmoins un problème. De plus, pour un réseau neuronal qui se targue de ne pas détruire les structures spatiales, le convnet classique le faisait quand même, bien que plus profondément dans le réseau et à un moindre degré.

Solutions modernes à un problème classique

Afin d'éviter ce problème de surajustement dans les convnets, la prochaine étape logique après avoir essayé la régularisation des abandons était de se débarrasser complètement des couches linéaires. Si les couches linéaires doivent être exclues, il faut rechercher une toute nouvelle façon de sous-échantillonner les cartes de caractéristiques et de produire une représentation vectorielle de taille égale au nombre de classes en question. C’est précisément là qu’intervient la mutualisation mondiale.

Considérons une tâche de classification à 4 classes, tandis que les couches de convolution 1 x 1 aideront à sous-échantillonner les cartes de caractéristiques jusqu'à ce qu'elles soient au nombre de 4, le pooling global aidera à créer une représentation vectorielle longue de 4 éléments qui pourra ensuite être utilisée par la fonction de perte dans calculer les pentes.

Mise en commun moyenne mondiale

Toujours sur la même tâche de classification décrite ci-dessus, imaginez un scénario dans lequel nous sentons que nos couches de convolution sont à une profondeur adéquate mais que nous avons 8 cartes de caractéristiques de taille (3, 3). Nous pouvons utiliser une couche de convolution 1 x 1 afin de sous-échantillonner les 8 cartes de caractéristiques à 4. Nous avons maintenant 4 matrices de taille (3, 3) alors que nous avons réellement besoin d'un vecteur de 4 éléments.

Une façon de dériver un vecteur à 4 éléments à partir de ces cartes de caractéristiques consiste à calculer la moyenne de tous les pixels de chaque carte de caractéristiques et à la renvoyer sous la forme d'un seul élément. C’est essentiellement ce qu’implique la mise en commun moyenne mondiale.

Mise en commun maximale globale

Tout comme le scénario ci-dessus dans lequel nous aimerions produire un vecteur de 4 éléments à partir de 4 matrices, dans ce cas, au lieu de prendre la valeur moyenne de tous les pixels de chaque carte de caractéristiques, nous prenons la valeur maximale et la renvoyons en tant qu'élément individuel dans la représentation vectorielle d’intérêt.

Analyse comparative des méthodes de pooling global

L'objectif de l'analyse comparative ici est de comparer les deux techniques de pooling global en fonction de leurs performances lorsqu'elles sont utilisées pour générer des représentations vectorielles de classification. L'ensemble de données à utiliser pour l'analyse comparative est l'ensemble de données FashionMNIST qui contient des images de 28 pixels sur 28 pixels d'articles de mode courants.

#  loading training data
training_set = Datasets.FashionMNIST(root='./', download=True,
                                      transform=transforms.ToTensor())
 loading validation data
validation_set = Datasets.FashionMNIST(root='./', download=True, train=False,
                                        transform=transforms.ToTensor())

Étiquette

Description

T-shirt

1

Pantalon

2

Arrêtez-vous

3

Robe

4

Manteau

5

Sandale

6

Chemise

7

Baskets

8

Sac

9

Bottine

Convnet avec pooling moyen mondial

Le convnet défini ci-dessous utilise une couche de convolution 1 x 1 en tandem avec une mise en commun moyenne globale au lieu de couches linéaires pour produire une représentation vectorielle à 10 éléments sans régularisation. Concernant la mise en œuvre du pooling moyen global dans PyTorch, tout ce qui doit être fait est d'utiliser la classe de pooling moyen standard mais d'utiliser un noyau/filtre de taille égale à la taille de chaque carte de fonctionnalités individuelle. Pour illustrer, les cartes de fonctionnalités sortant de la couche 6 sont de taille (3, 3) donc afin d'effectuer un pooling moyen global, un noyau de taille 3 est utilisé. Remarque : le simple fait de prendre la valeur moyenne de chaque carte de caractéristiques donnera le même résultat.

class ConvNet_1(nn.Module):
  def __init__(self):
    super().__init__()
    self.network = nn.Sequential(
        #  layer 1
        nn.Conv2d(1, 8, 3, padding=1),
        nn.ReLU(), #  feature map size = (28, 28)
        #  layer 2
        nn.Conv2d(8, 8, 3, padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2), #  feature map size = (14, 14)
        #  layer 3
        nn.Conv2d(8, 16, 3, padding=1),
        nn.ReLU(), #  feature map size = (14, 14)
        #  layer 4
        nn.Conv2d(16, 16, 3, padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2), #  feature map size = (7, 7)
        #  layer 5
        nn.Conv2d(16, 32, 3, padding=1),
        nn.ReLU(), #  feature map size = (7, 7)
        #  layer 6
        nn.Conv2d(32, 32, 3, padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2), #  feature map size = (3, 3)
        #  output layer
        nn.Conv2d(32, 10, 1),
        nn.AvgPool2d(3)
    )

  def forward(self, x):
    x = x.view(-1, 1, 28, 28)
    output = self.network(x)
    output = output.view(-1, 10)
    return torch.sigmoid(output)

Convnet avec Global Max Pooling

ConvNet_2 ci-dessous, en revanche, remplace les couches linéaires par une couche de convolution 1 x 1 travaillant en tandem avec un pooling maximum global afin de produire un vecteur de 10 éléments sans régularisation. Semblable au pooling moyen global, pour implémenter le pooling maximum global dans PyTorch, il faut utiliser la classe de pooling maximum standard avec une taille de noyau égale à la taille de la carte de fonctionnalités à ce stade. Remarque : le simple fait de dériver la valeur maximale des pixels dans chaque carte de caractéristiques donnerait les mêmes résultats.

class ConvNet_2(nn.Module):
  def __init__(self):
    super().__init__()
    self.network = nn.Sequential(
        #  layer 1
        nn.Conv2d(1, 8, 3, padding=1),
        nn.ReLU(), #  feature map size = (28, 28)
        #  layer 2
        nn.Conv2d(8, 8, 3, padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2), #  feature map size = (14, 14)
        #  layer 3
        nn.Conv2d(8, 16, 3, padding=1),
        nn.ReLU(), #  feature map size = (14, 14)
        #  layer 4
        nn.Conv2d(16, 16, 3, padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2), #  feature map size = (7, 7)
        #  layer 5
        nn.Conv2d(16, 32, 3, padding=1),
        nn.ReLU(), #  feature map size = (7, 7)
        #  layer 6
        nn.Conv2d(32, 32, 3, padding=1),
        nn.ReLU(),
        nn.MaxPool2d(2), #  feature map size = (3, 3)
        #  output layer
        nn.Conv2d(32, 10, 1),
        nn.MaxPool2d(3)
    )

  def forward(self, x):
    x = x.view(-1, 1, 28, 28)
    output = self.network(x)
    output = output.view(-1, 10)
    return torch.sigmoid(output)

Classe de réseau neuronal convolutif

La classe définie ci-dessous contient les fonctions de formation et de classification à utiliser pour la formation et l'utilisation des convnets.

class ConvolutionalNeuralNet():
  def __init__(self, network):
    self.network = network.to(device)
    self.optimizer = torch.optim.Adam(self.network.parameters(), lr=3e-4)

  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)

    #  defining accuracy function
    def accuracy(network, dataloader):
      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)

    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 = []

      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)    

ConvNet_1 (mise en commun moyenne globale)

ConvNet_1 utilise la mise en commun moyenne globale pour produire un vecteur de classification. La définition des paramètres d'intérêt et la formation pour 60 époques produisent un journal métrique tel qu'analysé ci-dessous.

model_1 = ConvolutionalNeuralNet(ConvNet_1())

log_dict_1 = model_1.train(nn.CrossEntropyLoss(), epochs=60, batch_size=64, 
                       training_set=training_set, validation_set=validation_set)

D'après le journal obtenu, la précision de la formation et de la validation a augmenté au cours de la formation du modèle. La précision de la validation commence à environ 66 % avant d’augmenter régulièrement jusqu’à une valeur légèrement inférieure à 80 % à la 28e époque. Une forte augmentation jusqu'à une valeur inférieure à 85 % est ensuite observée à la 31e époque avant de finalement culminer à environ 87 % à la 60e époque.

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

ConvNet_2 (mise en commun maximale globale)

ConvNet_2 utilise le pooling maximum global au lieu du pooling moyen global pour produire un vecteur de classification à 10 éléments. En gardant tous les paramètres identiques et en s'entraînant pendant 60 époques, on obtient le journal métrique ci-dessous.

model_2 = ConvolutionalNeuralNet(ConvNet_2())

log_dict_2 = model_2.train(nn.CrossEntropyLoss(), epochs=60, batch_size=64, 
                       training_set=training_set, validation_set=validation_set)

Dans l’ensemble, la précision de la formation et de la validation a augmenté au cours de 60 époques. La précision de la validation commence à un peu moins de 70 % avant de fluctuer tout en augmentant régulièrement jusqu'à une valeur d'un peu moins de 85 % à la 60e époque.

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')
plt.savefig('maxpool_benchmark.png', dpi=1000)

Comparaison des performances

En comparant les performances des deux techniques de pooling global, on peut facilement en déduire que le pooling moyen global fonctionne mieux, du moins sur l'ensemble de données que nous avons choisi d'utiliser (FashionMNIST). Cela semble vraiment logique puisque le pooling moyen global produit une valeur unique qui est représentative de la nature générale de tous les pixels dans chaque carte de caractéristiques, par opposition au pooling maximum global qui produit une valeur unique isolée sans tenir compte des autres pixels présents dans la carte. carte des caractéristiques. Cependant, pour parvenir à un verdict plus concluant, une analyse comparative doit être effectuée sur plusieurs ensembles de données.

La mutualisation mondiale sous le capot

Afin de comprendre pourquoi la mise en commun globale fonctionne réellement, nous devons écrire une fonction qui nous permettra de visualiser la sortie d'une couche intermédiaire dans un réseau neuronal convolutif. On pense souvent que les réseaux de neurones sont des modèles de boîte noire, mais il existe certaines façons d’essayer au moins d’ouvrir la boîte noire dans le but de comprendre ce qui se passe à l’intérieur. C'est exactement ce que fait la fonction ci-dessous.

def visualize_layer(model, dataset, image_idx: int, layer_idx: int):
  """
  This function visulizes intermediate layers in a convolutional neural 
  network defined using the PyTorch sequential class 
  """
  #  creating a dataloader
  dataloader = DataLoader(dataset, 250)

  #  deriving a single batch from dataloader
  for images, labels in dataloader:
    images, labels = images.to(device), labels.to(device)
    break

  #  deriving output from layer of interest
  output = model.network.network[:layer_idx].forward(images[image_idx])
  #  deriving output shape
  out_shape = output.shape

  #  classifying image
  predicted_class = model.predict(images[image_idx])

  print(f'actual class: {labels[image_idx]}\npredicted class: {torch.argmax(predicted_class)}')

  #  visualising layer
  plt.figure(dpi=150)
  plt.title(f'visualising output')
  plt.imshow(np.transpose(make_grid(output.cpu().view(out_shape[0], 1, 
                                                        out_shape[1], 
                                                        out_shape[2]), 
                                    padding=2, normalize=True), (1,2,0)))
  plt.axis('off')

Pour utiliser la fonction, les paramètres doivent être correctement compris. Le modèle fait référence à un réseau neuronal à convolution instancié de la même manière que nous l'avons fait dans cet article, les autres types ne fonctionneront pas avec cette fonction. Dans ce cas, l'ensemble de données peut être n'importe quel ensemble de données, mais de préférence l'ensemble de validation. Image_idx est l'index d'une image dans le premier lot de l'ensemble de données fourni, la fonction définit un lot comme 250 images donc image_idx peut aller de 0 à 249. Layer_idx, en revanche, ne fait pas exactement référence aux couches de convolution, il fait référence aux couches tel que défini par la classe séquentielle PyTorch comme indiqué ci-dessous.

model_1.network
 output
>>>> ConvNet_1(
  (network): Sequential(
    (0): Conv2d(1, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (1): ReLU()
    (2): Conv2d(8, 8, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (3): ReLU()
    (4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (5): Conv2d(8, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (6): ReLU()
    (7): Conv2d(16, 16, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (8): ReLU()
    (9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (10): Conv2d(16, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (11): ReLU()
    (12): Conv2d(32, 32, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
    (13): ReLU()
    (14): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
    (15): Conv2d(32, 10, kernel_size=(1, 1), stride=(1, 1))
    (16): AvgPool2d(kernel_size=3, stride=3, padding=0)
  )
)

Pourquoi la mutualisation moyenne mondiale fonctionne

Afin de comprendre pourquoi le pooling moyen global fonctionne, nous devons visualiser la sortie de la couche de sortie juste avant que le pooling moyen global ne soit effectué, cela correspond à la couche 15, nous devons donc récupérer/indexer les couches jusqu'à la couche 15, ce qui implique que layer_idx= 16. En utilisant model_1 (ConvNet_1), nous produisons les résultats ci-dessous.

visualize_layer(model=model_1, dataset=validation_set, image_idx=2, layer_idx=16)
 output
>>>> actual class: 1
>>>> predicted class: 1

Lorsque nous visualisons la sortie de l'image 3 (index 2) juste avant la mise en commun de la moyenne globale, nous pouvons voir que le modèle a prédit correctement sa classe en tant que classe 1 (pantalon), comme vu ci-dessus. En regardant la visualisation, nous pouvons voir que la carte de caractéristiques à l'index 1 possède en moyenne les pixels les plus brillants par rapport aux autres cartes de caractéristiques. En d’autres termes, le convnet a appris à classer les images en « activant » davantage de pixels dans la carte des caractéristiques d’intérêt juste avant la mise en commun moyenne globale. Lorsque la mise en commun de la moyenne globale est ensuite effectuée, l'élément de valeur la plus élevée sera situé à l'index 1, d'où la raison pour laquelle il est choisi comme classe correcte.

Production moyenne mondiale de mise en commun.

Pourquoi Global Max Pooling fonctionne

En gardant tous les paramètres identiques mais en utilisant model_2 (ConvNet_2) dans ce cas, nous obtenons les résultats ci-dessous. Encore une fois, le convnet classe correctement cette image comme appartenant à la classe 1. En regardant la visualisation produite, nous pouvons voir que la carte des caractéristiques à l'index 1 contient le pixel le plus brillant.

Le convnet a dans ce cas appris à classer les images en « activant » les pixels les plus brillants de la carte des caractéristiques d'intérêt juste avant le regroupement maximum global.

visualize_layer(model=model_2, dataset=validation_set, image_idx=2, layer_idx=16)
 output
>>>> actual class: 1
>>>> predicted class: 1

Sortie de pooling maximale globale.

Remarques finales

Dans cet article, nous avons exploré ce qu’implique la mise en commun moyenne et maximale globale. Nous avons discuté des raisons pour lesquelles ils sont utilisés et de la manière dont ils se comparent les uns aux autres. Nous avons également développé une intuition quant à leur fonctionnement en effectuant une biopsie de nos convnets et en visualisant les couches intermédiaires.

Articles connexes: