TP 7 : Réseau de neurones avec Pytorch

Programmation pour l’IA

On considère le fichier sonar.csv. Il contient les résultats d’une expérience menée par Gorman and Sejnowski. Dans cette expérience, un sonar est pointé vers un objet cylindrique fait en métal (M) ou en pierre (R). Chaque colonne de ce fichier contient des valeurs correspondant au niveau d’énergie de certaines fréquences relevées en réponse à un sonar.

Avec Pytorch, écrire un programme complet contenant une classe permettant de charger les données (qu’on aura pris soin de séparer en un une partie de données d’entraînement et de données test), un modèle de réseau de neurone et un algorithme permettant de l’entraîner.

Quelle est la précision de votre modèle ?

Note: Le but de cet est de vous forcer à créer une architecture sans trop de contraintes. On ne cherchera pas forcément à avoir une précision très bonne, même s’il est conseillé d’expérimenter avec quelques architectures différentes par curiosité.

MNIST est un jeu de données emblématique en apprentissage machine. Il comporte 60000 images en niveau de gris de taille \(28 \times 28\), chacune représentant un chiffre entre \(0\) et \(9\). Le chiffre représenté est donné comme un label. En d’autre mot, chaque exemple est une matrice \(28 \times 28\) de nombre entre \(0\) et \(1\) (\(0\) représentant un pixel noir et \(1\) représentant un pixel blanc) et son label est le nombre entre \(0\) et \(9\) représenté par cette image. Notre but est d’entraîner un classifieur pour ce problème.

On peut obtenir un dataset pytorch tout prêt pour MNIST grâce à torchvision. Le programme suivant téléchargera le jeu de données dans un dossier data (s’il n’existe pas déjà) et renvoie un dataset. Par défaut, le jeu de données contient des images au format PIL mais le paramètre optionnel transform=transforms.ToTensor() convertit chaque exemple en un tenseur [1, 28, 28].

import torchvision

d = torchvision.datasets.MNIST(root='./data', download=True, transform=transforms.ToTensor())

On pourra aussi obtenir \(10000\) images tests :

test = torchvision.datasets.MNIST(root='./test', train=False, download=True, transform=transforms.ToTensor())
  1. On va observer quelques exemples du jeu de données.
    1. Affichez avec print la valeur d[5][0] et d[5][1]. Quelle est la forme de d[5][0] ? Quelle est la classe de cet exemple ?
    2. Affichez l’image avec matplotlib. On pourra utiliser : plt.imshow(d[5][0].squeeze()) pour la tracer. La méthode squeeze() enlève les dimensions égale à \(1\) d’un tenseur.
    3. Affichez d’autres exemples du dataset.
  2. On va essayer d’apprendre un séparateur linéaire. Le problème ici est que l’entrée est une matrice \(28 \times 28\) et non pas un vecteur, comme nous l’avons fait jusqu’ici. On a besoin d’une couche nn.Flatten() qui convertit un tenseur de forme \((s_0,\dots,s_k)\) en un tenseur de forme \((s_0, \sum_{i=1}^k s_i)\).
    1. Proposez un modèle qui a une couche nn.Flatten() et une couche linéaire. On attend à ce que la sortie soit un vecteur de taille 10. Nous n’avons pas besoin d’une couche softmax car nous entraînerons avec une fonction d’erreur nn.CrossEntropy() qui applique déjà la fonction softmax pour calculer l’erreur.
    2. Écrire une fonction predict(m,x) qui prédit la classe de x[0],...,x[b-1] selon le modèle m où x est un tenseur de forme [b, 28, 28]. On appliquera une fonction softmax à la sortie de m et une fonction argmax pour trouver la classe (on suppose qu’on a un encodage one hot).
    3. Entraînez le modèle pour 5 epochs avec l’optimisateur Adam, un pas de \(0.03\) et une erreur d’entropie croisée. Pour l’entraînement, pensez à encoder les classes en one hot avant le calcul de l’entropie. Affichez la précision du modèle à la fin de chaque epoch.
  3. On va considérer l’architecture suivante :
    • Une couche de convolution 2d : 1 channel d’entrée, 16 channels de sorties, kernels de taille \(3 \times 3\). On utilisera un padding de \(1\) pour conserver la taille originale du tenseur.
    • Une couche ReLU.
    • Une couche de MaxPool2d avec une taille de kernel \(2\).
    • Une couche de convolution 2d encore : 16 channels d’entrée (puisqu’on avait 16 kernels dans la première couche), 32 channels de sortie, kernels de taille \(3 \times 3\). On utilisera toujours un padding de \(1\) pour conserver les dimensions.
    • Une couche ReLU.
    • Une couche de MaxPool2d avec une taille de kernel \(2\).
    • Une couche linéaire.
    1. Le réseau doit renvoyer un vecteur de taille \(10\) (puisqu’on a \(10\) classes). Quelle est la taille de la dernière couche linéaire ?
    2. Reprenez le code de la question précédente mais avec ce nouveau modèle. Utilisez une taille de batch de 128. Quelle est votre précision après 5 epochs ? Pensez à sauvegarder votre modèle à la fin.
    3. Affichez quelques exemples où votre modèle se trompe, avec la valeur prédite et la vraie valeur.
    4. Expérimentez différents modèles. Par exemple :
      • Une seule couche Convolution + ReLU + MaxPool2d.
      • Moins de kernel dans chaque couche.
  4. Bonus: On peut voir dans le cas de MINST que le temps d’entraînement pour une epoch peut être assez long. Cela motive l’utilisation d’architecture spécialisée comme des GPU. Si vous avez un GPU disponible sur votre machine (torch.cuda.is_available() doit retourner True), testez votre code sur GPU. Pour cela, on enverra le modèle sur le GPU avec m.to("cuda") où m est votre modèle. Il faudra aussi envoyer tous les tenseurs utilisés avec m sur le GPU. Par exemple, si on veut évaluer m sur un tenseur X en faisant m(X), il faut que X soit sur le GPU. On peut le faire en faisant X = X.to("cuda").

Si vous n’avez pas de GPU sur votre machine ou s’il n’est pas configuré, vous pouvez tester sur Google Colab mais cela nécessite un compte google ou Kaggle Notebook (compte nécessaire aussi).