CSC 8607 – Introduction au deep learning

Portail informatique

CI1 : Premiers pas

Le but de cette séance est de mettre en place l'environnement de travail et de pratiquer les notions vues en cours. Nous allons voir comment utiliser SLURM, créer un enrionnement virtuel avec Mamba, entraîner un modèle simple et visualiser l'apprentissage.

Utilisation de SLURM (∼20mn, – facile)

Nous allons accéder à des GPUs en utilisant un logiciel appelé SLURM. Slurm permet des ressources partagées sur plusieurs serveurs comme des GPUs ou des CPUs.

Il va falloir créer un compte sur le SLURM de l'école. Générer une clef SSH (si vous n'en avez pas déjà une), et envoyer à votre enseignant la clef PUBLIQUE par mail, ainsi que votre pseudo de connexion aux machines de l'école.

Pour accéder aux machines contenant les GPUs, nous allons utiliser SSH. Comme la machine se trouve dans le réseau de l'école, on ne peut y accéder que depuis l'école, ou à travers le VPN, ou en passant par le portail SSH publique. Demander à votre enseignant l'adresse IP. Si vous utiliser Linux, vous pouvez configurer le fichier ~/.ssh/config que la manière suivante pour un accès facilité :
Host LAB_GATEWAY User VOTRE_IDENTIFIANT_TSP Hostname ssh1.imtbs-tsp.eu IdentitiesOnly yes IdentityFile CHEMIN_VERS_CLEF_SSH_PRIVEE ServerAliveInterval 60 ServerAliveCountMax 2 Host tsp-client User VOTRE_IDENTIFIANT_TSP Hostname ADRESSE_IP IdentitiesOnly yes IdentityFile CHEMIN_VERS_CLEF_SSH_PRIVEE ServerAliveInterval 60 ServerAliveCountMax 2 ProxyJump LAB_GATEWAY
Vous pourrez alors vous connecter depuis n'importe où en faisant ssh tsp-client. Si vous êtes sous Windows, une solution consiste à utiliser WSL2 ou Putty.
La machine sur laquelle vous vous connectez est peu puissante : n'y exécutez pas de programmes couteux en ressources !

Dans Slurm, vous avez la possibilité de demander des ressources. Si celles-ci sont disponibles, elles vous seront attribuées et vous serez connectés aux ressources en question dans un nouveau terminal. Commencez par exécuter la commande nvidia-smi. Cette commande donne un aperçu des GPUs et de leur utilisation. Normalement, sur la machine client configurée plus haut cette commande ne fonctionne pas (il n'y a pas de GPU). Maintenant, demandez des ressources en mode interactif à l'aide de la commande srun --time=10:00:00 --gres=gpu:1 --pty bash. Les commandes bash commencent par un s. Ici, nous demandons un GPU (--gres=gpu:1) pour une durée de 10 heures avec bash comme pseudo-terminal (--pty bash). Si des ressources sont disponibles, vous devriez arriver dans un nouveau terminal avec un GPU. Exécutez nvidia-smi pour vérifier que c'est bien le cas. Pour quitter le terminal, vous pouvez faire Ctrl + D, ou taper exit.

Une commande utile de Slurm est squeue (squeue -u $USER permet de visualiser uniquement vos jobs). Elle permet de regarder l'utilisation des ressources et d'avoir des éléments clefs à propos des jobs (allocation de ressource dans Slurm). En particulier, on peut voir le JobID qui nous permet d'annuler un job (i.e., de le terminer) à l'aide de la commande scancel MON_JOB_ID. Demandez l'accès à un GPU, puis ouvrez un nouveau terminal et reconnectez vous au cluster de GPU. Vérifier que vous voyez votre job en train d'être exécuté, et arrêtez-le. Sur la fenêtre d'origine où le job tournait, vous devriez avoir un message pour vous annoncer la fin du job.

Comme les entraînements de modèles peuvent être très long, il est rare qu'on les démarre avec le mode interactif. À la place, on utilise la commande sbatch qui prend en argument un script bash. Ce script bash peut exécuter toutes les commandes qu'il veut, et en particulier, il peut appeler un script Python qui va faire l'entraînement du modèle. Le script bash doit aussi contenir un entête spécial qui va permettre de spécifier à SLURM les ressources demandées. Voici un exemple d'en-tête :
#!/bin/bash #SBATCH -t 1:00:00 #SBATCH --gres=gpu:1 # Activation de l'environnement virtuel source ~/.bashrc source activate deeplearning
Avec cet en-tête, nous demandons un GPU pour une heure. Vous pouvez consultez la liste des commandes sur la documentation de SLURM.
Écrivez un script affichant un simple message à l'écran à l'aide d'echo et soumettez le code. Où la sortie du programme a-t'elle été écrite ?

Création d'un environnement virtuel Python (∼20mn, – facile)

Dans cette section, vous apprendrez à créer un environnement virtuel à l'aide de Mamba et à installer PyTorch avec support CUDA.

Commencez par installer Miniforge en exécutant les commandes suivantes :

curl -L -O "https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-$(uname)-$(uname -m).sh" bash Miniforge3-$(uname)-$(uname -m).sh

Répondez yes à toutes les questions demandant une réponse positive ou négative. Ensuite, déconnectez-vous de ssh et reconnectez-vous. Vous devriez maintenant pouvoir exécuter la commande mamba. Si ce n'est pas le cas, ajoutez dans le fichier ~/.bash_profile:

if [ -f ~/.bashrc ]; then . ~/.bashrc fi

Créez un environnement virtuel en exécutant la commande suivante :

mamba create -n deeplearning python=3.10

Activez l'environnement nouvellement créé :

mamba activate deeplearning
Il faudra toujours utiliser cette commande pour pouvoir accéder à l'environnement virtuel.

Quelle commande utilisez-vous pour vérifier la version de Python installée dans votre environnement actif ?
python --version

Nous allons maintenant installer PyTorch en exécutant la commande :

pip install torch torchvision torchaudio tensorboard

Quel est l’avantage d’utiliser PyTorch avec support CUDA ?
CUDA permet d'utiliser les GPU pour accélérer les calculs, ce qui est particulièrement utile pour l'entraînement de modèles de deep learning.

Vérifiez que PyTorch est correctement installé en exécutant les commandes suivantes dans un terminal Python (après avoir demandé un GPU et activé l'environnement virtuel) :

import torch print(torch.__version__) print(torch.cuda.is_available())

Si la commande torch.cuda.is_available() retourne True, votre installation est prête à utiliser un GPU.

Que signifie si torch.cuda.is_available() retourne False ?
Cela signifie soit que votre système n'a pas de GPU compatible, soit que l'installation de CUDA ou des pilotes n'est pas correctement configurée.

Exercices théorique (papier et crayon)

Dans cette section, vous allez consolider vos connaissances sur les concepts vus en cours, notamment le perceptron multicouche, la rétropropagation, la descente de gradient stochastique, les fonctions d'activation, et les fonctions de perte.

Dessinez un perceptron multicouche (MLP) avec :

  • Une couche d'entrée avec 3 neurones
  • Une couche cachée avec 4 neurones
  • Une couche de sortie avec 2 neurones

Combien de paramètres possède ce modèle (on ne prend pas en compte les biais) ?
20

Écrivez les équations du forward pass en notation matricielle pour ce réseau. On considère que la fonction d'activation est la fonction ReLU. On pourra appeler l'entrée X et les poids des couches W1 et W2. Quelles sont les dimensions de W1 et W2 ?
Y = ReLU(X*W1^T)*W2^T W1 a pour dimension 4x3 et W2 2x4.

Quelle est la principale différence entre un perceptron simple et un perceptron multicouche ?
Un perceptron multicouche a une ou plusieurs couches cachées qui permettent de modéliser des relations non linéaires, contrairement au perceptron simple qui ne peut résoudre que des problèmes linéairement séparables.

Pourquoi utilisons-nous la règle de la chaîne pour calculer les gradients dans la rétropropagation ?
La règle de la chaîne permet de calculer efficacement les dérivées partielles des paramètres (poids et biais) en remontant les contributions d'erreur couche par couche.

Écrivez le graph de calcul pour la fonction f(x, y, z) = x/y + z.

Effectuez le forward pass dans ce graph de calcul avec x = 2, y = 4, z = 0.
Vous devriez obtenir 0.5 en sortie.

Effectuez maintenant la backpropagation pour calculer le gradient de f.
La dérivée par rapport à x est 0.25.
La dérivée par rapport à y est -0.125.
La dérivée par rapport à z est 1.

Effectuez une étape de la descente de gradient avec un learning rate de 1. Quelles sont les nouvelles valeurs de x, y, et z ? La sortie a-t-elle bien diminuée ?
x=1.5, y=4.25, z=-1. La sortie vaut maintenant -0.65, elle a bien diminué.

Quelles sont les principales raisons d'utiliser des batchs plutôt que l'ensemble des données ou un exemple unique ?
Les batchs offrent un bon compromis entre la rapidité de convergence et la stabilité des gradients, tout en réduisant les besoins en mémoire.

Tracez les courbes des fonctions d'activation suivantes :
  • ReLU
  • Sigmoid
  • Tanh

Votre premier réseau de neurones

Dans cette section, vous allez implémenter un réseau de neurones simple en utilisant PyTorch et visualiser le processus d'entraînement avec TensorBoard.

Étape 1 : Préparation des données

Téléchargez et chargez le dataset CIFAR-10 :

import torch import torchvision from torchvision import transforms, datasets transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # Normalisation ]) trainset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=True) testset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)

Que fait la classe torch.utils.data.DataLoader ?
Elle crée un itérateur pour charger les données en batch, avec des options de mélange (shuffle) pour le dataset d'entraînement.

Quelle est la différence entre l'ensemble d'entraînement et l'ensemble de test ?
L'ensemble d'entraînement est utilisé pour ajuster les paramètres du modèle, tandis que l'ensemble de test est utilisé pour évaluer ses performances sur des données non vues.

Quelles sont les transformations faites sur les données ? Pourquoi ?
On commence par transformer l'entrée en Tensor. Un tenseur est la structure manipulée par Pytorch, il faut donc transformer l'entrée en Tensor. Ensuite, on normalise les données. Les deux arguments sont les moyennes et les écart-types de la transformation pour chaque channel (RGB). À la fin, on aura : output[channel] = (input[channel] - mean[channel]) / std[channel]. Cette transformation est souvent utilise pour faciliter l'apprentissage et la régularisation.

Étape 2 : Implémentation du réseau

Nous allons commencer par implémenter un MLP (multi-layer perceptron) simple. Pour cela, créez un nouveau module MLP qui contiendra un MLP avec une couche cachée. L'entrée est une image de taille 32*32*3 (RGB), la couche cachée aura 128 neurones, et la sortie 10 neurones correspondant aux dix classes.
import torch.nn as nn import torch.nn.functional as F class MLP(nn.Module): def __init__(self): super(MLP, self).__init__() self.fc1 = nn.Linear(32*32*3, 128) # Couche cachée self.fc2 = nn.Linear(128, 10) # Couche de sortie

Dans la méthode forward, pourquoi devons-nous aplatir l’image avec x.view(-1, 32*32*3) ?
Le MLP s’attend à une entrée sous forme de vecteur, donc les dimensions 3D (canaux, hauteur, largeur) doivent être converties en un vecteur 1D.

Ajoutez une méthode forward à votre module. On considèrera la fonction d'activation ReLU.
def forward(self, x): x = x.view(-1, 32*32*3) # Aplatir l'image x = F.relu(self.fc1(x)) # Activation ReLU x = self.fc2(x) # Sortie return x

Quelle fonction faut-il généralement utiliser à la fin du réseau de neurone pour les tâches de classification multiclasse et pourquoi ?
La fonction Softmax est utilisée car elle transforme les logits en probabilités normalisées pour chaque classe. Cependant, ce n'est pas obligatoire dans tous les cas.

Étape 3 : Entraînement du modèle

Configurez l’entraînement :

device = ( "cuda" if torch.cuda.is_available() else "cpu" ) print(f"Using {device} device") model = MLP().to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.01) # Entraînement for epoch in range(10): # 10 époques running_loss = 0.0 for inputs, labels in trainloader: inputs = inputs.to(device) labels = labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() print(f"Époque {epoch+1}, Perte : {running_loss/len(trainloader)}")

Quelle est la différence entre optimizer.zero_grad() et loss.backward() ?
optimizer.zero_grad() remet les gradients à zéro avant le calcul, tandis que loss.backward() calcule les gradients via la backpropagation.

Pourquoi doit-on mettre écrire .to(device) après la définition du modèle et dans la boucle d'entraînement ?
On doit envoyer les données d'entraînement et les poids du modèle sur le GPU (s'il est disponible).

Étape 4 : Évaluation sur l'ensemble de test

Après l'entraînement, évaluez le modèle sur l'ensemble de test :

model.eval() with torch.no_grad(): correct = 0 total = 0 for images, labels in testloader: images = images.to(device) labels = labels.to(device) outputs = model(images) _, predicted = torch.max(outputs, 1) total += labels.size(0) correct += (predicted == labels).sum().item() metric = correct / total print(f"Test metric: {metric:.2f}")

Pourquoi devons-nous utiliser model.eval() et torch.no_grad() lors de l'évaluation ?
model.eval() désactive les mécanismes comme Dropout ou BatchNorm pour garantir une évaluation correcte. torch.no_grad() désactive le calcul des gradients, ce qui réduit l'utilisation de la mémoire.

Quelle est la métrique calculée ici ?
L'accuracy.

Quelle serait la valeur de cette métrique pour un classificateur aléatoire (on considère que toutes les classes sont équitablement réparties) ? Battez-vous cette valeur ?
Un classificateur aléatoire aurait une accuracy de 0.10.

Étape 5 : Sauvegarde et chargement du modèle

Sauvegardez le modèle après l'entraînement :

torch.save(model.state_dict(), "mlp_model.pth")

Pour charger le modèle plus tard :

model.load_state_dict(torch.load("mlp_model.pth"))

Pourquoi est-il important de sauvegarder un modèle entraîné ?
Cela permet de réutiliser le modèle sans avoir à le réentraîner, ce qui économise du temps et des ressources.

Que se passe-t-il si vous essayez de charger un modèle sauvegardé avec une architecture différente de celle utilisée lors de la sauvegarde ?
Une erreur sera levée car les poids et les biais sauvegardés ne correspondent pas à la nouvelle architecture.
Dans cet exercice, nous avons explicitement donné tout le code pour faire l'entraînement et le test. Dans les TP suivants, ce sera à vous de tout faire, mais le code restera toujours assez semblable.

Utilisation de Tensorboard

Dans cet exercice, vous allez imaginer un scénario où vous devez intégrer TensorBoard pour visualiser les métriques d'entraînement et d'évaluation d'un modèle de deep learning.

Un modèle est entraîné sur un ensemble de données pour prédire la classe d'images. Vous devez enregistrer les informations suivantes dans TensorBoard :

  • La perte d'entraînement à chaque époque
  • La perte de test à chaque époque
  • L'exactitude (accuracy) sur l'ensemble de test

Exemple de code pour enregistrer les métriques :

from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter("runs/mlp_experiment") # Exemple dans une boucle d'entraînement for epoch in range(num_epochs): ... writer.add_scalar("Loss/train", train_loss, epoch) writer.add_scalar("Loss/test", test_loss, epoch) writer.add_scalar("Accuracy/test", test_accuracy, epoch)
En pratique, on fait un découpage en train/validation/test et on visualise les résultats intermédiaire sur la validation et non le test.

Pourquoi est-il utile d'utiliser TensorBoard pour suivre les métriques pendant l'entraînement ?
TensorBoard permet de visualiser les progrès de l'entraînement en temps réel, de détecter les problèmes (comme un sur-apprentissage) et de comparer les résultats de différentes expériences de manière organisée.

Nous allons avoir besoin de visualiser les résultats dans Tensorboard. Pour cela, il va falloir transférer les données générées par Tensorboard (dans runs/mlp_experiment si l'on suit l'exemple ci-dessus) du serveur vers notre ordinateur. Faites cela en utilisant la commande scp.

Installez Tensorboard sur votre ordinateur (pip install tensorboard), puis ouvrez Tensorboard (tensorboard --logdir=runs) et vérifiez que vous pouvez visualiser les expériences.

Ajoutez des exemples d'images mal classées à TensorBoard après chaque époque. Voici un exemple :

# Ajouter une image au tableau de bord TensorBoard from torchvision.utils import make_grid images = make_grid(images_mal_classees) # images_mal_classees contient des images incorrectement classées writer.add_image("Mauvaises classifications", images, epoch)

Quelle information importante peut-on extraire en visualisant les images mal classées dans TensorBoard ?
Cela peut révéler des patterns dans les erreurs du modèle, comme des classes confondues ou des données bruitées, aidant ainsi à améliorer l'entraînement.

Ajoutez une visualisation des poids de la première couche du réseau :

# Exemple d'enregistrement des poids sous forme d'image weights = model.fc1.weight.data # Par exemple, les poids de la première couche fully-connected writer.add_histogram("Poids/Layer 1", weights, epoch)

Pourquoi est-il intéressant de visualiser les poids dans TensorBoard ?
Cela permet de détecter des anomalies dans l'évolution des poids, comme des poids qui explosent ou qui stagnent, ce qui peut indiquer des problèmes dans l'entraînement.

Vous pouvez également enregistrer les hyperparamètres (par exemple, le taux d'apprentissage, la taille des batchs) et les résultats finaux pour suivre les performances de vos expériences :

# Exemple d'enregistrement des hyperparamètres et résultats finaux hyperparams = { "learning_rate": 0.001, "batch_size": 64, "num_epochs": num_epochs, } final_results = { "final_accuracy": test_accuracy, "final_loss": test_loss, } writer.add_hparams(hyperparams, final_results)

Pourquoi est-il important d'enregistrer les hyperparamètres et les résultats finaux dans TensorBoard ?
Cela permet de comparer facilement les différentes expériences pour identifier les combinaisons d'hyperparamètres qui donnent les meilleurs résultats.

Comment interpréteriez-vous les résultats si un taux d'apprentissage plus élevé (par exemple, 0.01) conduit à une perte d'entraînement plus faible mais à une accuracy de test inférieure ?
Cela pourrait indiquer que le modèle sur-apprend rapidement à l'ensemble d'entraînement mais ne généralise pas bien à l'ensemble de test, ce qui est un signe d'overfit.

Assurez-vous de fermer le writer après l'entraînement :

writer.close()