AI & GPU
Comment comprendre facilement ResNet dans PyTorch

Comment comprendre facilement ResNet dans PyTorch

Introduction à ResNet

Qu'est-ce que ResNet ?

ResNet, abréviation de Residual Neural Network (réseau neuronal résiduel), est une architecture d'apprentissage profond introduite en 2015 par des chercheurs de Microsoft. Elle a été conçue pour résoudre le problème du gradient qui disparaît/explose, un problème courant rencontré lors de l'entraînement de réseaux neuronaux très profonds.

  1. Residual Neural Network : ResNet est un type de réseau neuronal qui utilise des "connexions de saut" ou des "connexions résiduelles" pour permettre l'entraînement de modèles beaucoup plus profonds. Ces connexions de saut permettent au réseau de contourner certaines couches, créant ainsi un "raccourci" qui aide à atténuer le problème du gradient qui disparaît.

  2. Résolution du problème du gradient qui disparaît/explose : Dans les réseaux neuronaux très profonds, les gradients utilisés pour la rétropropagation peuvent soit disparaître (devenir extrêmement petits) soit exploser (devenir extrêmement grands) lorsqu'ils se propagent à travers le réseau. Cela peut rendre l'apprentissage difficile pour le réseau, en particulier dans les couches les plus profondes. Les connexions de saut de ResNet aident à résoudre ce problème en permettant aux gradients de circuler plus facilement à travers le réseau.

Avantages de ResNet

  1. Amélioration des performances des réseaux neuronaux profonds : Les connexions de saut de ResNet permettent l'entraînement de réseaux neuronaux beaucoup plus profonds, ce qui peut conduire à une amélioration significative des performances dans diverses tâches, telles que la classification d'images, la détection d'objets et la segmentation sémantique.

  2. Convergence plus rapide pendant l'entraînement : Les connexions de saut de ResNet permettent également au réseau de converger plus rapidement pendant le processus d'entraînement, car elles facilitent la circulation des gradients à travers le réseau.

Implémentation de ResNet dans PyTorch

Configuration de l'environnement

  1. Installation de PyTorch : Pour commencer à utiliser ResNet avec PyTorch, vous devez d'abord installer la bibliothèque PyTorch. Vous pouvez télécharger et installer PyTorch depuis le site officiel (https://pytorch.org/ (opens in a new tab)) en fonction de votre système d'exploitation et de votre version de Python.

  2. Importation des bibliothèques nécessaires : Une fois que vous avez installé PyTorch, vous devez importer les bibliothèques nécessaires à votre projet. Cela comprend généralement PyTorch, NumPy et toutes les autres bibliothèques dont vous pourriez avoir besoin pour le prétraitement des données, la visualisation ou d'autres tâches.

import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt

Définition de l'architecture ResNet

Compréhension des blocs de base

  1. Couches de convolution : ResNet, comme de nombreux autres modèles d'apprentissage profond, utilise des couches de convolution comme principaux blocs de construction pour l'extraction de caractéristiques.

  2. Normalisation par lots : ResNet utilise également des couches de normalisation par lots (Batch Normalization) pour stabiliser le processus d'entraînement et améliorer les performances du modèle.

  3. Fonctions d'activation : L'architecture ResNet utilise généralement ReLU (Unité linéaire rectifiée) comme fonction d'activation, ce qui permet d'introduire de la non-linéarité dans le modèle.

  4. Couches de max-pooling : ResNet peut également inclure des couches de max-pooling ou de pooling moyenne pour réduire les dimensions spatiales des cartes d'attributs et introduire une invariance translationnelle.

Implémentation du bloc ResNet

  1. Connexion résiduelle : L'innovation clé de ResNet est la connexion résiduelle, qui permet au réseau de contourner certaines couches en ajoutant l'entrée d'une couche à sa sortie. Cela contribue à atténuer le problème du gradient qui disparaît.
class ResNetBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super(ResNetBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        
        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )
 
    def forward(self, x):
        residual = self.shortcut(x)
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)
        out += residual
        out = self.relu(out)
        return out
  1. Connexion de raccourci : En plus de la connexion résiduelle, ResNet utilise également une "connexion de raccourci" pour adapter les dimensions de l'entrée et de la sortie du bloc ResNet, si nécessaire.

Construction du modèle ResNet complet

  1. Empilement des blocs ResNet : Pour créer le modèle ResNet complet, vous devez empiler plusieurs blocs ResNet ensemble, en ajustant le nombre de couches et le nombre de filtres dans chaque bloc.

  2. Ajustement du nombre de couches : Les modèles ResNet existent en différentes variantes, telles que ResNet-18, ResNet-34, ResNet-50, ResNet-101 et ResNet-152, qui ont différents nombres de couches. Le nombre de couches affecte la complexité et les performances du modèle.

Implémentation de ResNet-18 dans PyTorch

Définition du modèle ResNet-18

  1. Couche d'entrée : La couche d'entrée du modèle ResNet-18 acceptera généralement une image d'une taille spécifique, telle que 224x224 pixels.

  2. Couches de convolution : Les couches de convolution initiales du modèle ResNet-18 extraient des caractéristiques de base de l'image d'entrée.

  3. Blocs ResNet : Le cœur du modèle ResNet-18 est l'empilement de plusieurs blocs ResNet, qui utilisent les connexions résiduelles pour permettre l'entraînement d'un réseau plus profond.

  4. Couche entièrement connectée : Après les couches de convolution et les blocs ResNet, le modèle aura une couche entièrement connectée pour effectuer la tâche de classification ou de prédiction finale.

  5. Couche de sortie : La couche de sortie du modèle ResNet-18 aura un nombre d'unités correspondant au nombre de classes dans le problème résolu.

class ResNet18(nn.Module):
    def __init__(self, num_classes=10):
        super(ResNet18, self).__init__()
        self.in_channels = 64
        self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(64)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
 
        self.layer1 = self._make_layer(64, 64, 2, stride=1)
        self.layer2 = self._make_layer(64, 128, 2, stride=2)
        self.layer3 = self._make_layer(128, 256, 2, stride=2)
        self.layer4 = self._make_layer(256, 512, 2, stride=2)
 
        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512, num_classes)
 
    def _make_layer(self, in_channels, out_channels, num_blocks, stride):
        layers = []
        layers.append(ResNetBlock(in_channels, out_channels, stride))
        self.in_channels = out_channels
        for i in range(1, num_blocks):
            layers.append(ResNetBlock(out_channels, out_channels))
        return nn.Sequential(*layers)
 
    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)
 
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)
 
        x = self.avgpool(x)
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x

Initialisation du modèle

Pour créer une instance du modèle ResNet-18, vous pouvez simplement instancier la classe ResNet18 :

model = ResNet18(num_classes=10)

Impression du résumé du modèle

Vous pouvez imprimer un résumé de l'architecture du modèle ResNet-18 en utilisant la fonction summary() de la bibliothèque torchsummary :

from torchsummary import summary
summary(model, input_size=(3, 224, 224))

Cela fournira un aperçu détaillé des couches du modèle, y compris le nombre de paramètres et la forme de sortie de chaque couche.

Entraînement du modèle ResNet-18

Préparation du jeu de données

Téléchargement et chargement du jeu de données

Pour cet exemple, nous utiliserons le jeu de données CIFAR-10, qui est une référence couramment utilisée pour les tâches de classification d'images. Vous pouvez télécharger le jeu de données à l'aide du module torchvision.datasets.CIFAR10 :

# Télécharger et charger le jeu de données CIFAR-10
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transforms.ToTensor())
test_dataset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transforms.ToTensor())

Prétraitement des données

Avant d'entraîner le modèle, vous devrez prétraiter les données, par exemple en normalisant les valeurs des pixels et en appliquant des techniques d'augmentation des données :

# Définition des transformations des données
transform_train = transforms.Compose([
    transforms.RandomCrop(32, padding=4),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
 
transform_test = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010))
])
 
# Création des chargeurs de données
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=2)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=100, shuffle=False, num_workers=2)

Définition de la boucle d'entraînement

Définition du dispositif (CPU ou GPU)

Pour profiter de l'accélération GPU, vous pouvez déplacer le modèle et les données vers le GPU :

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
model = model.to(device)

Définition de la fonction de perte et de l'optimiseurEnsuite, vous devrez définir la fonction de perte et l'optimiseur à utiliser lors du processus d'entraînement :

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1, momentum=0.9, weight_decay=5e-4)

Mise en place de la boucle d'entraînement

La boucle d'entraînement comprendra les étapes suivantes :

  1. Passage en avant à travers le modèle
  2. Calcul de la perte
  3. Rétropropagation des gradients
  4. Mise à jour des paramètres du modèle
  5. Suivi de la perte d'entraînement et de l'exactitude
num_epochs = 100
train_losses = []
train_accuracies = []
val_losses = []
val_accuracies = []
 
for epoch in range(num_epochs):
    # Phase d'entraînement
    model.train()
    running_loss = 0.0
    correct = 0
    total = 0
    for i, (inputs, labels) in enumerate(train_loader):
        inputs, labels = inputs.to(device), labels.to(device)
        
        optimizer.zero_grad()
        outputs = model(inputs)
        loss = criterion(outputs, labels)
 
## Optimisation du modèle
 
### Régularisation
 
La régularisation est une technique utilisée pour éviter le surajustement des modèles d'apprentissage profond. Le surajustement se produit lorsqu'un modèle se comporte bien sur les données d'entraînement mais échoue à généraliser à de nouvelles données inconnues. Les techniques de régularisation aident le modèle à généraliser mieux en introduisant une pénalité pour la complexité ou en ajoutant du bruit au processus d'entraînement.
 
Une technique de régularisation populaire est la régularisation L2, également connue sous le nom de dégradation du poids. Cette méthode ajoute un terme de pénalité à la fonction de perte qui est proportionnel à la magnitude au carré des poids du modèle. La fonction de perte avec une régularisation L2 peut être écrite comme suit :
 

perte = perte_originale + lambda * somme(w^2)


où `lambda` est la force de régularisation et `w` sont les poids du modèle.

Une autre technique de régularisation populaire est le dropout. Dropout met aléatoirement une partie des activations d'une couche à zéro pendant l'entraînement, réduisant ainsi efficacement la capacité du modèle et le forçant à apprendre des caractéristiques plus robustes. Cela aide à éviter le surajustement et peut améliorer les performances de généralisation du modèle.

Voici un exemple de mise en œuvre du Dropout dans un modèle PyTorch :

```python
import torch.nn as nn

class MonModèle(nn.Module):
    def __init__(self):
        super(MonModèle, self).__init__()
        self.fc1 = nn.Linear(64, 128)
        self.dropout = nn.Dropout(p=0.5)
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.fc1(x)
        x = self.dropout(x)
        x = self.fc2(x)
        return x

Dans cet exemple, la couche Dropout est appliquée après la première couche entièrement connectée, avec un taux de dropout de 0,5, ce qui signifie que 50% des activations seront mises aléatoirement à zéro pendant l'entraînement.

Algorithmes d'optimisation

Le choix de l'algorithme d'optimisation peut avoir un impact significatif sur les performances et la convergence d'un modèle d'apprentissage profond. Voici quelques algorithmes d'optimisation populaires utilisés en apprentissage profond :

Descente de gradient stochastique (SGD)

SGD est l'algorithme d'optimisation le plus basique, où les gradients sont calculés sur un seul exemple d'entraînement ou un petit lot d'exemples, et les poids sont mis à jour en conséquence. SGD peut être lent à converger, mais il est simple et efficace.

import torch.optim as optim
 
model = MonModèle()
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

Adam

Adam (Adaptive Moment Estimation) est un algorithme d'optimisation plus avancé qui permet de calculer des taux d'apprentissage adaptatifs pour chaque paramètre. Il combine les avantages de l'inertie et de RMSProp, ce qui en fait un choix populaire pour de nombreuses tâches d'apprentissage profond.

optimizer = optim.Adam(model.parameters(), lr=0.001)

AdaGrad

AdaGrad (Adaptive Gradient) est un algorithme d'optimisation qui adapte le taux d'apprentissage pour chaque paramètre en fonction des gradients historiques. Il est efficace pour les données clairsemées, mais il peut subir une réduction agressive du taux d'apprentissage au fil du temps.

optimizer = optim.Adagrad(model.parameters(), lr=0.01)

RMSProp

RMSProp (Root Mean Square Propagation) est un autre algorithme d'optimisation du taux d'apprentissage adaptatif qui maintient une moyenne mobile des gradients au carré. Il est particulièrement utile pour les objectifs non stationnaires, tels que ceux que l'on trouve dans les réseaux neuronaux récurrents.

optimizer = optim.RMSprop(model.parameters(), lr=0.001, alpha=0.99)

Le choix de l'algorithme d'optimisation dépend du problème spécifique, de la structure du modèle et des caractéristiques des données. Il est souvent judicieux d'expérimenter avec différents algorithmes et de comparer leurs performances sur votre tâche.

Transfert d'apprentissage

Le transfert d'apprentissage est une technique où un modèle entraîné sur un grand jeu de données est utilisé comme point de départ pour un modèle sur une tâche différente mais connexe. Cela peut être particulièrement utile lorsque le jeu de données cible est petit, car cela permet au modèle de tirer parti des fonctionnalités apprises sur le plus grand jeu de données.

Une approche courante de transfert d'apprentissage en apprentissage profond consiste à utiliser un modèle pré-entraîné, tel que ceux disponibles pour les tâches populaires de vision par ordinateur ou de traitement du langage naturel, et à affiner le modèle sur le jeu de données cible. Cela implique de geler les couches inférieures du modèle pré-entraîné et de n'entraîner que les couches supérieures sur les nouvelles données.

Voici un exemple de réglage fin d'un modèle ResNet pré-entraîné pour une tâche de classification d'image dans PyTorch :

import torchvision.models as models
import torch.nn as nn
 
# Charger le modèle ResNet pré-entraîné
resnet = models.resnet18(pretrained=True)
 
# Geler les paramètres du modèle pré-entraîné
for param in resnet.parameters():
    param.requires_grad = False
 
# Remplacer la dernière couche par une nouvelle couche entièrement connectée
num_features = resnet.fc.in_features
resnet.fc = nn.Linear(num_features, 10)  # Supposons 10 classes
 
# Entraîner le modèle sur le nouveau jeu de données
optimizer = optim.Adam(resnet.fc.parameters(), lr=0.001)

Dans cet exemple, nous chargeons d'abord le modèle ResNet18 pré-entraîné et congelons les paramètres des couches inférieures. Nous remplaçons ensuite la dernière couche entièrement connectée par une nouvelle couche ayant le nombre approprié de sorties pour notre tâche cible (10 classes dans ce cas). Enfin, nous entraînons le modèle en utilisant l'optimiseur Adam, en ne mettant à jour que les paramètres de la nouvelle couche entièrement connectée.

Le transfert d'apprentissage peut considérablement améliorer les performances des modèles d'apprentissage profond, en particulier lorsque le jeu de données cible est petit. C'est une technique puissante qui peut faire gagner du temps et des ressources lors du développement de modèles.

Interprétabilité du modèle

À mesure que les modèles d'apprentissage profond deviennent plus complexes et plus répandus, la nécessité de modèles interprétables est de plus en plus importante. L'interprétabilité fait référence à la capacité de comprendre et d'expliquer le processus de prise de décision interne d'un modèle.

Une technique populaire pour améliorer l'interprétabilité du modèle est l'utilisation de mécanismes d'attention. L'attention permet au modèle de se concentrer sur les parties les plus pertinentes de l'entrée lors de la prise de décision, et il peut être visualisé pour comprendre les caractéristiques que le modèle utilise.

Voici un exemple de mise en œuvre d'un mécanisme d'attention dans un modèle PyTorch pour une tâche de traitement du langage naturel :

import torch.nn as nn
import torch.nn.functional as F
 
class ModèleAttention(nn.Module):
    def __init__(self, taille_vocabulaire, taille_incorporation, taille_cachée):
        super(ModèleAttention, self).__init__()
        self.incorporation = nn.Embedding(taille_vocabulaire, taille_incorporation)
        self.lstm = nn.LSTM(taille_incorporation, taille_cachée, bidirectional=True, batch_first=True)
        self.attention = nn.Linear(taille_cachée * 2, 1)
 
    def forward(self, input_ids):
        # Incorporer l'entrée
        incorporée = self.incorporation(input_ids)
 
        # Passer l'entrée incorporée à travers le LSTM
        sortie_lstm, _ = self.lstm(incorporée)
 
        # Calculer les poids d'attention
        poids_attention = F.softmax(self.attention(sortie_lstm), dim=1)
 
        # Calculer la somme pondérée des sorties LSTM
        contexte = torch.sum(poids_attention * sortie_lstm, dim=1)
 
        return contexte

Dans cet exemple, le mécanisme d'attention est implémenté sous forme d'une couche linéaire qui prend en entrée les sorties du LSTM et produit un ensemble de poids d'attention. Ces poids sont ensuite utilisés pour calculer la somme pondérée des sorties du LSTM, qui est la sortie finale du modèle.

En visualisant les poids d'attention, vous pouvez obtenir des informations sur les parties de l'entrée sur lesquelles le modèle se concentre lors de la prise de décision. Cela peut vous aider à comprendre le processus de prise de décision du modèle et à identifier les biais potentiels ou les domaines d'amélioration.

Une autre technique pour améliorer l'interprétabilité du modèle est l'analyse de l'importance des caractéristiques. Cela consiste à identifier les caractéristiques les plus importantes que le modèle utilise pour prendre des décisions. Une méthode populaire pour cela est les valeurs de Shapley, qui permettent de quantifier la contribution de chaque caractéristique à la sortie du modèle.

L'amélioration de l'interprétabilité du modèle est un domaine de recherche important en apprentissage profond, car cela peut contribuer à instaurer la confiance dans ces modèles puissants et garantir qu'ils sont utilisés de manière responsable.

Conclusion

Dans ce tutoriel, nous avons abordé divers sujets liés à l'apprentissage profond, notamment l'optimisation du modèle, le transfert d'apprentissage et l'interprétabilité du modèle. Nous avons discuté de techniques telles que la régularisation, les algorithmes d'optimisation et les mécanismes d'attention, et fourni des exemples de mise en œuvre de ces concepts dans PyTorch.

À mesure que l'apprentissage profond continue d'évoluer et de se répandre, il est important de comprendre ces sujets avancés et de savoir les appliquer à vos propres projets. En maîtrisant ces techniques, vous serez mieux équipé pour construire des modèles d'apprentissage profond performants, robustes et interprétables, capables de résoudre un large éventail de problèmes.

N'oubliez pas que l'apprentissage profond est un domaine en constante évolution, il est donc important de rester à jour avec les dernières recherches et les meilleures pratiques. Continuez à explorer, à expérimenter et à apprendre, et vous serez bien parti pour devenir un expert en apprentissage profond.