Classification de galaxies avec Python

1. Introduction

La catégorisation automatique des images représente un enjeu essentiel dans le domaine du traitement de données. En astrophysique, avec le lancement du télescope James Webb, des millions de données vont être recueillies. Cependant, cet afflux de données sera difficile à traiter. Je me souviens d'une conférence donnée par l’un de mes professeurs en astrophysique, qui a grandement contribué à la construction de James Webb. Il nous disait qu'il manquait déjà de main-d'œuvre pour traiter les données de Hubble, et que la tâche de traitement de ce nouveau satellite serait faramineuse. Il nous confiait cette mission, notre département d'astrophysique à l’Université de Montréal étant très reconnu.

Après avoir étudié l'intelligence artificielle appliquée à la physique, notamment avec le modèle de Hopfield et les perceptrons, j’ai voulu m’orienter vers des méthodes plus récentes, notamment via PyTorch. C’est dans ce cadre que j’ai découvert "Galaxy Zoo – The Galaxy Challenge" et que j’ai décidé, moi aussi, de tenter ma propre approche. L’objectif est de concevoir un système sophistiqué capable de classifier avec précision différentes galaxies en se basant sur des méthodes d’apprentissage en profondeur, telles que les réseaux de neurones convolutifs (CNN). Pour cela, j’ai suivi plusieurs étapes importantes : exploration initiale des données, préparation et prétraitement des images, création et entraînement d’un CNN personnalisé. Ce document expose toutes les étapes suivies, les choix techniques effectués tout au long du processus, ainsi que les conclusions tirées de notre modèle final, avec un regard critique.

Toutes les données sont disponibles ici : https://www.kaggle.com/c/galaxy-zoo-the-galaxy-challenge/data

Préparation des fonction utilitaire

Voir le code

Préparation des images

Le format initial des images n'est pas optimal. Le format est grand (424 × 424) et il y a beaucoup d'espace vide autour des galaxies. Écrivez un code qui effectue les tâches suivantes :

  1. Découpage de l'image pour obtenir une taille réduite de moitié autour du centre (212 × 212)
  2. Dégradation de la résolution pour que l'image ait 64 pixels de chaque côté
  3. Sauvegardez toutes les images pré-traitées dans un nouveau dossier
Voir le code

Préparation des jeux de données (dataset)

Voir le code
Voir le code

Définition du CNN

Les données sont maintenant compatibles avec PyTorch et sont prêtes à être analysées. Il faut ensuite définir un CNN qui va prendre en entrée les images 64 × 64 avec 3 couleurs et nous donner une probabilité pour les 37 classes en sortie. Pour commencer, on définit un CNN avec les éléments suivants :

Structure Générale

  1. Couches Convolutives (conv_stack) : Le modèle commence avec une couche convolutive prenant des images à 3 canaux (RGB) et les transforme en un ensemble de cartes de caractéristiques à 6 canaux. Une deuxième couche convolutive augmente davantage le nombre de canaux de 6 à 16 pour extraire des caractéristiques plus profondes. Ces couches fonctionnent en appliquant un ensemble de filtres (ou noyaux) sur l'image d'entrée. Ces filtres se déplacent à travers l'image (un processus appelé convolution) pour détecter des caractéristiques telles que les bords, les textures ou d'autres motifs spécifiques. En ajustant les poids de ces filtres pendant l'entraînement, le réseau apprend à extraire les caractéristiques les plus pertinentes pour la tâche donnée.
  2. Normalisation par Lots (BatchNorm2d) : Chaque couche convolutive est suivie d'une normalisation par lots, qui agit en normalisant les sorties de la couche précédente pour chaque mini-lot d'entraînement. Elle ajuste et met à l'échelle les activations pour que leur distribution ait une moyenne proche de 0 et une variance proche de 1. Cela réduit le problème du décalage des covariables, stabilise l'apprentissage et permet d'utiliser des taux d'apprentissage plus élevés.
  3. Activation (ReLU) : Chaque normalisation est suivie de la fonction d'activation ReLU (Rectified Linear Unit), qui transforme les valeurs négatives en zéro et laisse passer les valeurs positives. Cela introduit une non-linéarité essentielle pour apprendre des relations complexes entre les entrées et les sorties.
  4. Pooling (self.pool) : Une opération de pooling (généralement de taille 2×2) réduit la dimensionnalité des cartes de caractéristiques, en conservant les informations les plus importantes. Cela augmente la robustesse aux petites variations de position.
  5. Aplatissement (Flatten) : Avant de passer aux couches pleinement connectées, les cartes de caractéristiques 2D doivent être converties en un vecteur 1D. L'aplatissement permet cette transformation, préparant les données à la suite du traitement.
  6. Couches Pleinement Connectées (linear_stack) : Les caractéristiques extraites sont ensuite passées à des couches entièrement connectées. Ces couches combinent les caractéristiques pour produire des représentations de plus haut niveau, puis génèrent des prédictions pour les 37 classes possibles.
  7. Activation Finale (Sigmoid) : La dernière couche utilise une fonction sigmoïde pour transformer les valeurs de sortie (logits) en probabilités entre 0 et 1. Chaque neurone représente la probabilité que l'image appartienne à une classe donnée.
Voir le code

Entraînement du réseau

Maintenant que notre CNN (réseau de neurones convolutif) est prêt, on peut commencer son entraînement à reconnaître les galaxies. Pour cela, on procède à une segmentation de l'ensemble des données en deux parties distinctes : environ 80 % des informations sont consacrées à l'entraînement du modèle afin qu'il puisse assimiler les caractéristiques des galaxies, et 20 % des données restantes sont utilisées pour tester comment le modèle se comporte sur des exemples qu'il ne connaît pas. Cela permet de vérifier s'il comprend bien l'ensemble ou s'il se contente juste d'apprendre par cœur les images (phénomène appelé surapprentissage ou surajustement).

Pour orienter l'apprentissage du réseau neuronal, une fonction de perte est employée. Dans notre cas, il s'agit de la MSE (Mean Squared Error), ou erreur quadratique moyenne. À chaque prédiction du réseau est associée une valeur d'erreur correspondant à l'écart entre la prédiction et la réponse attendue. On élève cette valeur au carré afin d'éliminer les valeurs négatives et d'amplifier les grosses erreurs.

Ensuite, la moyenne de ces erreurs est calculée sur toutes les données disponibles. Plus cet écart est important, plus le modèle est éloigné de la bonne réponse. Pour corriger cette erreur, il est nécessaire de mettre à jour les poids internes (les paramètres qui déterminent les connexions entre les neurones) en utilisant un optimiseur, ici Adam (Adaptive Moment Estimation).

Adam est un algorithme d'optimisation qui ajuste automatiquement la taille des pas (learning rate) pour chaque poids, en se basant sur les gradients antérieurs. Il fusionne deux concepts :

Grâce à cela, notre modèle apprend de manière plus rapide et efficace que s’il utilisait une méthode de mise à jour simple. Enfin, nous effectuons 6 époques d’entraînement, ce qui signifie que le modèle parcourt six fois l’ensemble des données d’apprentissage, lui permettant d’apprendre progressivement à identifier les galaxies avec une meilleure précision.

Voir le code

Processus d'entraînement (train_loop)

  1. Initialisation des Hyper-paramètres : Avant de commencer l'entraînement, quelques paramètres doivent être statué, tels que le taux d'apprentissage (learning_rate) et le nombre d'époques (epochs). Le taux d'apprentissage contrôle à quel point les poids du modèle sont ajustés par rapport au gradient de la fonction de perte. Les époques représentent le nombre de fois que l'ensemble des données d'entraînement est passé à travers le réseau.
  2. Fonction de Perte: nn.MSELoss() : La fonction de perte Mean Squared Error (MSE) mesure la différence quadratique moyenne entre les valeurs prédites et les valeurs réelles, pour estimer la performance du modèle. La perte MSE est calculée comme suit :

    $$\text{MSE} = \frac{1}{n} \sum_{i=1}^{n} (y_{\text{pred},i} - y_{\text{true},i})^2$$

    n est le nombre d'échantillons, ypred sont les prédictions du modèle, et ytrue sont les valeurs cibles réelles. Ainsi le modèle s'efforce de minimiser cette perte, et donc de réduire l'erreur entre ses prédictions et les valeurs réelles.
  3. Optimiseur: optim.Adam() : L'optimiseur Adam est un algorithme de descente de gradient qui ajuste les poids du réseau de manière adaptative, en utilisant des estimations des premiers et seconds moments des gradients. Plus concrètement, il trouve efficacement un bon ensemble de poids pour le réseau, permettant de minimiser la fonction de perte sur les données d'entraînement, tout en évitant un surajustement.
  4. Itération sur les Données d'Entraînement : Pour chaque époque, le modèle itère sur le DataLoader d'entraînement, qui fournit des lots (batches) des données d'entrée (X) et des étiquettes cibles (y). Pour chaque lot, le processus suivant est exécuté :
  5. Affichage de la Perte : La perte pour chaque lot est imprimée périodiquement pour surveiller le progrès de l'entraînement.

Processus de Test (test_loop)

Après chaque époque d'entraînement, le modèle est évalué sur l'ensemble de test :

  1. Pas de Gradient : torch.no_grad() est utilisé pour désactiver le calcul du gradient, pour économiser du temps car ce n'est pas nécessaire ici.
  2. Itération sur les Données de Test : Le modèle fait des prédictions sur l'ensemble de test, et la perte est calculée de la même manière que pendant l'entraînement, mais sans effectuer de rétropropagation ni de mise à jour des poids.
  3. Affichage de la Perte de Test : La perte moyenne sur l'ensemble de test est imprimée pour évaluer les performances du modèle sur des données non vues pendant l'entraînement.
Voir le code

Évaluation du modèle

Voir le code

Amélioration du modèle

Chaque fois qu'une image est chargée pendant l'entraînement, elle peut être horizontalement retournée, légèrement tournée, ou son contraste et sa luminosité ajustés de manière aléatoire avant d'être passée au modèle. Cela signifie que même si l'image originale est la même, le modèle la voit différemment à chaque fois, ce qui peut simuler un ensemble de données plus grand et plus divers.

L'idée derrière ceci est que les transformations font varier les données existantes à chaque itération de l'entraînement, ce qui peut aider notre modèle à mieux généraliser. Finalement la fonction transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) de PyTorch sert à normaliser les images en ajustant les canaux de couleur selon une moyenne et un écart-type spécifiques. Cela réduit les écarts de luminosité et de contraste entre les images et aide le modèle à converger plus rapidement lors de l'apprentissage, car les valeurs d'entrée sont mises à l'échelle de manière plus uniforme. Je l'ai tiré de la documentation PyTorch, ces valeurs spécifiques semblent être une référence reconnu qui a été sur de grande variété d'image.
Voir le code
En plus j'ai amelioré le CCN avec une couche convolutive supplémentaire qui permet au modèle d'extraire des caractéristiques plus abstraites et complexes des images. En général, les premières couches d'un CNN apprennent à détecter des caractéristiques simples comme les bords ou les textures simples, tandis que les couches plus profondes apprennent à identifier des caractéristiques plus complexes.

Ensuite, j'ai ajouté une couche pleinement connectée qui permet d'augmenter le niveau d'abstraction que le modèle peut atteindre en combinant les caractéristiques apprises par les couches convolutives. Cela permet au modèle de mieux intégrer et interpréter les caractéristiques extraites pour faire des prédictions plus précises.
Voir le code
De plus j'ai poussé les époques jusqu'à 30, voici le résultat : Courbe de la fonction de perte

RMSE sur les données de test: 0.09339757263660431

C'est mieux ! Je pense même que j'aurai pu accroître les époques ! Le système semblait encore stable. Avec ceci on obtient la 30ème place du classment, avec 10 ans de retard...

Voilà c'est tout pour ce projet, je serai curieux de savoir si depuis tout ce temps des techniques plus puissantes pourraient battre le 1er du classment : 0.07491 par un chercheur de chez DeepMind. La suite au prochain episode...