Analyse complète#

Dans cette feuille, nous allons mener une première analyse sur des données, afin d’obtenir une référence. Nous utiliserons d’abord les données brutes (représentation en pixel), puis nous extraierons automatiquement des attributs par Analyse en Composante Principale.

Vous ferez l’ensemble de cette feuille avec le jeu de données fourni (pommes et bananes). La semaine prochaine, vous ferez la même chose pour analyser votre propre jeu de données.

Import des librairies#

On commence par importer les librairies dont nous aurons besoin. Comme d’habitude, nous utiliserons un fichier utilities.py où nous vous fournissons quelques fonctions et que vous complèterez au fur et à mesure du projet:

# Automatically reload code when changes are made
%load_ext autoreload
%autoreload 2
import os
from PIL import Image, ImageDraw, ImageFont
import matplotlib.pyplot as plt
%matplotlib inline
from scipy import signal
import seaborn as sns

from intro_science_donnees import *
from utilities import *

Chargement des images#

On travaille sur le jeu de données des pommes et des bananes (un jeu de données plus grands qu’au début du cours!)

from intro_science_donnees import data
dataset_dir = os.path.join(data.dir, 'ApplesAndBananas')

images = load_images(dataset_dir, "*.png")

Nous allons aborder des jeux de données beaucoup plus grands que les précédents, en nombre et en résolution des images; il faut donc être un peu prudent et parfois patient. Cela prendrai par exemple du temps pour lire et afficher toutes les images dans la feuille Jupyter. Aussi, dans les exemples suivants, nous n’affichons que les premières et dernières images:

head = images.head()
image_grid(head, titles=head.index)
tail = images.tail()
image_grid(tail, titles=tail.index)

Étape 2: Redimensionner et recadrer#

Il est généralement nécessaire de redimensionner et/ou de recadrer les images. Dans les cours précédents, nous avons utilisé des images recadrées pour se simplifier la tâche. Si l’objet d’intérêt est petit au milieu de l’image, il est préférable d’éliminer une partie de l’arrière-plan pour faciliter la suite de l’analyse. De même, si l’image a une résolution élevée, on peut la réduire pour accélérer les calculs sans pour autant détériorer les performances. Dans l’ensemble, les images ne doivent pas dépasser 100x100 pixels.

Dans les cellules suivantes, on recadre et redimensionne les images en 32x32 pixels. Aucun effort particulier n’est fait pour centrer l’image sur le fruit. Vous pourrez faire mieux les semaines suivantes, dans la fiche qui s’appellera pretraitement.md!

images_cropped = images.apply(crop_image)
image_grid(images_cropped.head())

Étape 3: Représentation en pixels#

Dans les cours précédents, nous avons extrait des attributs avec des fonctions ad-hoc, comme la rougeur (redness) ou l’élongation. Les attributs ad-hoc sont pratiques pour faciliter la visualisation des données. On peut cependant obtenir d’assez bons résultats en partant directement des données brutes – ici les valeurs des pixels de nos images recadrées – et en extrayant automatiquement des attributs par décomposition en valeurs singulières, à la base de la technique de PCA. Cette analyse, présentée ci-dessous est simple à mettre en œuvre et peut donner une première base de résultats.

Vous trouverez dans utilities.py une fonction appelée image_to_series qui transforme une image en une série unidimensionnelle contenant les valeurs de tous les pixels de l’image. En appliquant cette fonction à toutes les images, on obtient un tableau de données où chaque ligne correspond à une image:

show_source(image_to_series)
df = images_cropped.apply(image_to_series)
df

Exercice: Pouvez-vous expliquer le nombre de colonnes que nous obtenons?

VOTRE RÉPONSE ICI

Nous rajoutons à notre tableau de données une colonne contenant l’étiquette (vérité terrain, ground truth) de chaque image; dans le jeu de données fournies, ce sera 1 pour une pomme et -1 pour une banane:

df['étiquette'] = df.index.map(lambda name: 1 if name[0] == 'a' else -1)
df

Exercice: Afficher les statistiques descriptives de Pandas sur votre base de données. Peut-on interpréter ces statistiques?

# VOTRE CODE ICI
raise NotImplementedError()

Sauvegarde intermédiaire#

Nous allons sauvegarder ces données brutes dans un fichier:

df.to_csv('crop_data.csv')

Cela vous permettra par la suite de reprendre la feuille à partir d’ici, sans avoir à reexécuter tous les traitements depuis le début:

df = pd.read_csv('crop_data.csv', index_col=0)
df

Visualisation des données brutes par carte thermique#

Les données de la représentation en pixels sont volumineuses; par principe il faut tout de même essayer de les visualiser. On peut pour cela utiliser une carte thermique avec Seaborn.

plt.figure(figsize=(20,20))
sns.heatmap(df, cmap="YlGnBu");

Comme vous le constatez, les données ne sont pas aléatoires: il y a des corrélations entre les lignes. Cela reste cependant difficilement exploitable visuellement.

Étape 4: Performance de référence (BAseline)#

À présent, nous allons appliquer la méthode des plus proches voisins sur la représentation en pixels pour avoir une performance de référence.

On déclare le classifieur par plus proche voisin (KNN), depuis la librairie scikit-learn.

from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import balanced_accuracy_score as sklearn_metric
sklearn_model = KNeighborsClassifier(n_neighbors=3)

Comme les séances précédentes, on calcule la performance et les barres d’erreurs du classifieur par validation croisée (cross validate), en divisant de multiples fois nos données en ensemble d’entraînement et en ensemble de test.

La fonction df_cross_validate fournie automatise tout le processus. Consultez son code pour retrouver les étapes:

show_source(df_cross_validate)
p_tr, s_tr, p_te, s_te = df_cross_validate(df, sklearn_model, sklearn_metric, verbose=True)

On a obtenu cette performance en comparants nos images pixel à pixel (distance euclidienne), sans aucun attributs.

Exercice: Est-ce que ce score vous semble étonnant?

VOTRE RÉPONSE ICI

Réduction de la dimension par analyse en composants principales#

Si vous reprenez cette feuille à partir d’ici, vous pouvez charger les données pour éviter d’avoir à tout reexécuter:

df = pd.read_csv('crop_data.csv', index_col=0)
df

Comme noté lorsque nous avons visualisé les données brutes, la représentation en pixels contient beaucoup trop d’information pour être directement exploitable: chaque ligne est un vecteur de grandes dimensions (\(32 \times 32\)). Il faut donc réduire la dimension en synthétisant l’information contenue dans chaque ligne en un petit nombre d’attributs. C’est ce que nous avions fait avec des attributs ad-hoc. Nous allons cette fois utiliser la technique d”Analyse en Composantes Principales (PCA: Principal Component Analysis). Elle utilise un algorithme de décomposition de matrices en valeur singulière (SVD) pour déterminer les «directions principales» des données, c’est-à-dire là où elles ont la plus grande variance. Nous l’avions déjà utilisé lors du TP 2 pour calculer l’élongation d’une forme.

Dans ce qui suit, vous pourrez vous aider des tutoriels que vous pouvez trouver sur internet (par exemple ici ou ), et de la documentation de pandas et/ou de numpy.

Exercice: Construisez un tableau df_scaled contenant les colonnes de df après normalisation. Vérifiez que la normalisation a fonctionné en appelant df.describe() avant et après la normalisation.

Indications:

  • La dernière colonne de df contient les étiquettes; il faut donc l’ignorer. Pour celà, vous pouvez utiliser la méthode drop avec l’option axis=1 pour supprimer une colonne.

  • Certaines colonnes pourraient être constantes, donc d’écart type nul. Dans ce cas, pour éviter une division par zéro, vous pouvez systématiquement ajouter la valeur sys.float_info.epsilon qui, comme son nom le suggère, est une toute petite valeur non nulle.

import sys
# VOTRE CODE ICI
raise NotImplementedError()
df_scaled.describe()
assert df_scaled.shape[1] == 32 * 32 * 3
assert (abs(df_scaled.mean()) < 0.01).all()
assert (abs(df_scaled.std()-1) < 0.01).all()

Exercice: Effectuez une décomposition en valeurs singulières de df_scaled. Stockez les matrices résultantes dans les variables U, S et V.

Indication: Utilisez la fonction np.linalg.svd() vue dans la fonction élongation des précédents TP avec l’option full_matrices = True

# VOTRE CODE ICI
raise NotImplementedError()
U.shape
S.shape
V.shape

Pour les curieux souhaitant en savoir plus sur la signification de ces matrices u, s, v, nous renvoyons à cet article. Pour aujourd’hui, il suffira de savoir ce qui suit:

  • U est une matrice carrée dont les colonnes sont les attributs extraits automatiquement; chacun est une combinaison linéaire de colonnes de df_scaled. Il y en a toujours un grand nombre, mais ils sont triés par ordre d’intérêt: les premiers contiennent le plus d’information (axes de plus grande variance).

  • S est un vecteur contenant les valeurs singulières correspondant aux attributs. Les carrés de celles-ci sont les valeurs propres; elles donnent l’importance de chaque attribut pour expliquer la variance des données.

Un scree plot affiche toujours les valeurs propres dans une courbe descendante, en classant les valeurs propres de la plus grande à la plus petite. Cette courbe aide à sélectionner le nombre de vecteurs propres qui expliquent la majorité de la variance des données.

Exercice: Afficher le scree plot des valeurs propres

Indications:

  • Valeurs singulières et valeurs propres sont deux termes différents ! Relisez les paragraphes précédents

  • Les valeurs singulières sont déjà triées de la plus grande à la plus petite

  • Renormaliser les valeurs propres par leur somme pour obtenir un pourcentage

  • N’affichez que les 50 premières valeurs

  • Faites un scree plot avec plt.plot

# VOTRE CODE ICI
raise NotImplementedError()

D’après le scree plot obtenu, on voit que les premiers attributs contiennent la majorité de l’information. Nous prendrons prendront les cinq premières.

Exercice: Créer un nouveau tableau de données svd_df avec les cinq premières colonnes de U et l’étiquette du fruit dans la dernière colonne.

Indication: Extraire les cinq premières colonnes de U, les passer comme argument à pd.DataFrame en spécifiant l’index de df comme index. Puis insérer la colonne étiquette de df.

# VOTRE CODE ICI
raise NotImplementedError()
svd_df
assert svd_df.shape == (491,6)
assert (svd_df.columns == 'étiquette').any()

On va maintenant afficher des diagrammes de dispersion par paires des attributs obtenus. Observez-vous que certaines caractéristiques ou paires de caractéristiques séparent bien les données?

g = sns.pairplot(svd_df, hue="étiquette", markers=["o", "s"], diag_kind="hist")

Exercice: Calculer les performances obtenues avec la méthode des trois plus proches voisins en utilisant les cinq premiers attributs. Comparez les performances à celles obtenues avec la représentation des pixels directement.

Indication: Utilisez df_cross_validate comme plus haut sur la bonne table.

# VOTRE CODE ICI
raise NotImplementedError()

Pour aller plus loin \(\clubsuit\): Tracer le score de performance (accuracy) sur l’ensemble de test en fonction du nombre d’attributs de la PCA prise en compte par le classifieur (de 1 à 10 par exemple).

# VOTRE CODE ICI
raise NotImplementedError()

Conclusion#

Terminez toujours par une conclusion. Qu’avez-vous appris ? Qu’est-ce qui pourrait être amélioré ?

Cette première feuille vous fait passer par le formatage de base des données et un flux de travail d’analyse de données de base. Les feuilles des semaines suivantes aborderont d’autres aspects de l’analyse (biais, pré-traîtement, classifieur).

Bonne pratique: Terminez toujours votre travail par « Kernel > Restart & Run all » pour vérifier que toutes vos cellules fonctionnent dans leur ordre d’exécution.