Analyse de données
Contenu
Analyse de données¶
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 extrairons automatiquement des attributs par Analyse en Composantes Principales.
Dans un premier temps, l’analyse sera faite sur le jeu de données des pommes et des bananes, puis vous la relancerez sur 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
from scipy import signal
import seaborn as sns
from intro_science_donnees import *
from utilities import *
Chargement des images¶
En commentant la ligne 2 ou la ligne 3, vous choisirez ici sur quel jeu de données vous travaillerez: les pommes et les bananes ou le vôtre.
from intro_science_donnees import data
dataset_dir = os.path.join(data.dir, 'ApplesAndBananas')
# dataset_dir = 'data'
images = load_images(dataset_dir, "?[012]?.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. Par exemple, le jeu de
données des pommes et bananes contient plusieurs centaines
d’images. Cela prendrait du temps pour les afficher et les traiter
toutes dans cette feuille Jupyter. Aussi, ci-dessus, utilisons nous le
glob ?[012]?.png
pour ne charger que les images dont le nom fait
moins de trois caractères et dont le deuxième caractère est 0, 1, ou 2.
De même, dans les exemples suivants, nous n’afficherons 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¶
Comme vu au TP6, il est généralement nécessaire de redimensionner et/ou de recadrer les images. DLors du premier projet, 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 ensuite, dans la fiche sur le prétraitement.
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 (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. Vous pourrez le faire pour votre projet aux moments que vous jugez opportuns.
df = pd.read_csv('crop_data.csv', index_col=0)
df
[VI]sualisation 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¶
À 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 comparant nos images pixel à pixel (distance euclidienne), sans aucun attribut.
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 \times 3\)). 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, comme vu lors du CM3. Nous l’avions déjà utilisé lors du TP 3 et 4 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
là),
et de la documentation de pandas
et/ou de numpy
.
Exercice: Construisez un tableau df_scaled
contenant les
colonnes de df
après normalisation mais ne contenant pas les étiquettes. 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éthodedrop
.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 : Vous pourrez vous inspirer de la fonction elongation
vu lors du TP3/4 et du projet 1.
# VOTRE CODE ICI
raise NotImplementedError()
U.shape
S.shape
V.shape
Pour information (et qui vous servira pour le Projet 2):
U
est une matrice carrée dont les colonnes sont les attributs extraits automatiquement; chacun est une combinaison linéaire de colonnes dedf_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:
Utilisez
plt.plot
Les valeurs singulières sont déjà triées de la plus grande à la plus petite
N’utiliser que les 50 premières valeurs
Renormaliser les valeurs propres par leur somme pour obtenir un pourcentage
# 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 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)
Exercice: 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")
VOTRE REPONSE ICI
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.
# 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¶
Cette première feuille vous fait passer par le formatage de base des données et une analyse de données donnant une référence. Prenez ici quelques notes sur ce que vous avez appris, observé, interprété.
VOTRE RÉPONSE ICI
Les feuilles suivantes aborderont d’autres aspects de l’analyse (biais, prétraitement, classifieur). Ouvrez la feuille sur le prétraitement et l’extraction d’attributs.