VI-ME-RÉ-BAR sur des données «réelles»
Contenu
VI-ME-RÉ-BAR sur des données «réelles»¶
Aujourd’hui nous allons analyser un jeu de données qui comporte des images de pommes et de bananes avec comme objectif de classer automatiquement les images de pommes versus celles de bananes.
Import des bibliothèques¶
import os, re
from glob import glob as ls
import numpy as np # Matrix algebra library
import pandas as pd # Data table (DataFrame) library
import seaborn as sns; sns.set() # Graphs and visualization library
from PIL import Image # Image processing library
import matplotlib.pyplot as plt # Library to make graphs
# Configuration intégration dans Jupyter
%matplotlib inline
Import des utilitaires:
%load_ext autoreload
%autoreload 2
from utilities import *
Étape 0: chargement du jeu de données¶
Exercice: En vous inspirant de la feuille de bonnes
pratiques, chargez les images du jeu de
données “ApplesAndBananasSimple”, dans la variable images
, et
affichez les:
# YOUR CODE HERE
raise NotImplementedError()
Étape 1: prétraitement et [VI]sualisation¶
Observez les images en détails. Êtes vous capable de différencier les pommes des bananes ? Essayez d’imaginer comment votre cerveau réussi un tel exercice !
Prétraitement¶
Les données sont très souvent prétraitées c’est-à-dire résumées selon différentes caractéristiques : chaque élément du jeu de données est décrit par un ensemble d”attributs – propriétés ou caractéristiques mesurables de cet élément ; pour un animal, cela peut être sa taille, sa température corporelle, etc.
C’est également le cas dans notre jeu de données : une image est décrite par la couleur de chacun de ses pixels. Cependant les pixels sont trop nombreux pour nos besoins. Nous voulons à la place juste quelques attributs résumant les propriétés du fruit dans l’image, comme sa couleur ou sa forme moyenne: ce sont les données prétraitées. Nous verrons la semaine prochaine comment extraire ces attributs depuis les images.
Pour ce TP, une table contenant les données prétraitées vous est
fournie dans le fichier attributs.csv
qu’il suffit donc de charger:
df = pd.read_csv("attributs.csv", index_col=0)
df
Cette table de données contient en colonne les attributs (features) de nos images ainsi que leurs classes (vérité terrain, ground truth): 1 si c’est une image de pomme et -1 si c’est une banane.
Cette table est tout ce dont nous aurons besoin dans la suite.
Visualisation¶
Exercice: Tout d’abord, extrayez quelques statistiques simples et
observez votre table avec la méthode describe
:
# YOUR CODE HERE
raise NotImplementedError()
Nous pouvons visualiser cette table sous forme de carte de chaleur (heat map) aisément:
df.style.background_gradient(cmap='coolwarm')
La carte de chaleur est appliquée à chaque colonne indépendamment. Ainsi, nous visualisons la variation de chaque colonne même si les valeurs sont dans des unités différentes ou avec des ordres de grandeur différents, comme c’est le cas ici.
La prochaine étape est de calculer la corrélation entre les colonnes; comme vu en cours, ces corrélations ne sont pas représentatives quand les colonnes n’ont pas le même ordre de grandeur. Nous devons donc standardiser chaque colonne de sorte à avoir la moyenne à \(0\) et l’écart-type à \(1\).
Exercice: Standardisez la table : colonne par colonne, soustrayez
la moyenne puis divisez par l’écart-type; affectez le résultat à
dfstd
. Comme la semaine dernière, pas besoin de boucle. Utilisez la
vectorisation!
# YOUR CODE HERE
raise NotImplementedError()
Vérifiez que cette nouvelle table est effectivement standarisée à l’aide des commandes suivantes (vérifiez que la moyenne est à 0 et l’écart type à 1):
dfstd.describe()
assert dfstd.shape == df.shape
assert dfstd.index.equals(df.index)
assert dfstd.columns.equals(df.columns)
assert (abs(dfstd.mean()) < 0.01).all()
assert (abs(dfstd.std() - 1) < 0.1).all()
Mince, ce n’est pas exactement ce qu’on veut ! La valeur du fruit correspond à une étiquette et pas à une valeur; on veut donc garder les valeurs originelles -1 pour les bananes et 1 pour les pommes.
Pour cela, le plus simple est de réaffecter la colonne d’origine:
dfstd['class'] = df['class']
dfstd
Observations¶
Exercice : Regardez la carte de chaleur de la table standarisée; elle devrait être identique à la précédente:
# YOUR CODE HERE
raise NotImplementedError()
Exercice :
Produisez une carte de chaleur de la matrice de corrélation.
Indication: si nécessaire, consultez le TP de la semaine précédente.
# YOUR CODE HERE
raise NotImplementedError()
Quel(s) attribut(s) est (sont) correlé(s) au type de fruit ? Pouvez vous indiquer quel type de fruit est le «plus» rouge et lequel est le «plus» allongé ? Y a-t-il des exceptions ?
VOTRE RÉPONSE ICI
Il existe quelques valeurs aberrantes (outliers), comme, par exemple deux bananes rouges; on peut les identifier à l’aide d’une visualisation en nuage de points (scatter plot), où chaque image est positionnée en fonction des valeurs de ses attributs:
make_scatter_plot(dfstd, images, axis='square')
On peut aussi visualiser le jeu de données à l’aide d’un pair plot.
Exercice : En vous inspirant du TP de la semaine dernière,
utilisez Seaborn
pour produire un pair plot. Utilisez l’option
diag_kind
pour choisir une représentation en histogrammes sur la
diagonale et l’option palette
pour prendre Set2
comme palette de
couleurs:
# YOUR CODE HERE
raise NotImplementedError()
Remarque importante : un seul attribut (la rougeur ou l’élongation) est presque suffisant pour parfaitement séparer les pommes des bananes.
Étape 2: [ME]sure de performance ([ME]tric)¶
Mesurer les performances de ce problème de classification revient à déterminer notre capacité à séparer les pommes des bananes (un problème de classification).
Partition (split) du jeu de données en ensemble d’entraînement (training set) et ensemble de test (test set)¶
Séparons d’abord les colonnes de notre table selon:
X
: les attributs permettant d’effectuer des prédictionsY
: la vérité terrain: ce que l’on veut prédire; ici le type de fruit. Pour cela on définit:
X = dfstd[['redness', 'elongation']]
Y = dfstd['class']
Pourquoi cette notation \(X\) et \(Y\) ? Car on essaye de définir un modèle prédictif \(f\); pour chaque élément d’index \(i\), on veut prédire la vérité terrain \(Y_i\) à partir de la valeur \(X_i\) des attributs. Idéalement on aurait \(Y_i = f(X_i)\), pour tout \(i\).
Nous allons maintenant partitionner nos images en un ensemble d’entraînement et un ensemble de test.
L’ensemble d’entraînement servira à entraîner notre modèle prédictif \(f\), c’est-à-dire à ajuster ses paramètres pour qu’il colle au mieux aux données de cet ensemble.
L’ensemble de test servira à mesurer la performance de ce modèle prédictif.
Comme notre problème est de petite taille, on va partitionner les images en deux ensembles de même taille.
L’utilitaire split_data
fourni dans utilities.py
partitionne
aléatoirement l’index des lignes de X
et Y
en deux ensembles de
même taille:
train_index, test_index = split_data(X, Y, seed=0)
train_index, test_index
Séparons en conséquence la table des attributs X
:
Xtrain = X.iloc[train_index]
Xtest = X.iloc[test_index]
Exercice : Faites de même pour les étiquettes.
# YOUR CODE HERE
raise NotImplementedError()
assert Ytest.shape == Ytrain.shape
assert pd.concat([Ytest, Ytrain]).sort_index().equals(Y.sort_index())
assert Ytest.value_counts().sort_index().equals(Ytrain.value_counts().sort_index())
On peut maintenant regarder les images qui serviront à entraîner notre modèle prédictif.
image_grid(images.iloc[train_index], titles=train_index)
Exercice : Faites de même avec les images de test.
# YOUR CODE HERE
raise NotImplementedError()
Notez que l’ensemble d’entraînement et l’ensemble de test contiennent
la même proportion de pommes et de bananes que le jeu de données
initial. L’utilitaire split_data
a été conçu pour le garantir!
L’utilitaire make_scatter_plot
fourni permet aussi de visualiser les
ensembles d’entrainement et de test comme des nuages de points, en
représentant chaque donnée d’entraînement par l’image associée et
chaque donnée de test un point d’interrogation car sa classe
(pomme/banane) est cachée.
make_scatter_plot(dfstd, images, train_index, test_index, axis='square')
Calcul du taux d’erreur¶
On suppose que l’on a :
Un vecteur
solutions
contenant les valeurs cibles (les « vraies » classes : 1 pour les pommes et -1 pour les bananes).Un vecteur
prédictions
contenant des valeur prédites (les classes estimées).
Le taux d’erreur (error rate) est défini comme la fraction \(\frac e n\), où \(e\) est le nombre de prédictions incorrectes et \(n\) le nombre total de prédictions. C’est une mesure de performance les prédictions.
Exercice :
Déterminer à la main \(e\), \(n\) et le taux d’erreur pour l’exemple suivant :
solutions = pd.Series([1, 1, -1, 1, 1])
predictions = pd.Series([1, -1, 1, 1, 1])
VOTRE RÉPONSE ICI
Calculez le même taux d’erreur avec Python
Défi : cela peut se faire en une petite ligne 😉
# YOUR CODE HERE
raise NotImplementedError()
Ouvrez le fichier utilities.py, et complétez le code de la fonction
error_rate
en généralisant votre calcul précédent.Vérifiez votre fonction sur l’exemple ci-dessus
# YOUR CODE HERE
raise NotImplementedError()
Écrivez des tests (avec
assert
) qui vérifient que :le taux d’erreur entre
solution=Ytrain
etprediction=Ytrain
est nul (pourquoi ?)VOTRE RÉPONSE ICI
le taux d’erreur entre
solution=Ytrain
etprediction=[1,...,1]
est de 0,5 (pourquoi ?)VOTRE RÉPONSE ICI
le taux d’erreur entre
solution=Ytrain
etprediction=[0,...,0]
est de un (pourquoi ?)VOTRE RÉPONSE ICI
Astuce : vous pouvez utiliser
np.zeros(Ytrain.shape)
pour générer un tableau[0,...,0]
de même taille queYtrain
, et de mêmenp.ones(Ytrain.shape)
pour[1,. ..,1]
.
# YOUR CODE HERE
raise NotImplementedError()
La bibliothèque d’apprentissage statistique
scikit-learn
également
appelée sklearn
a une fonction accuracy_score
qui calcule la
précision des prédictions. Ainsi, et comme vérification
supplémentaire, nous pouvons tester que
error_rate + accuracy_score = 1
en utilisant les mêmes exemples que
ci-dessus.
from sklearn.metrics import accuracy_score
assert abs(error_rate(Ytrain, Ytrain) + accuracy_score(Ytrain, Ytrain) - 1) <= .1
assert abs(error_rate(Ytrain, np.zeros(Ytrain.shape)) + accuracy_score(Ytrain, np.zeros(Ytrain.shape)) - 1) <= .1
assert abs(error_rate(Ytrain, np.ones(Ytrain.shape)) + accuracy_score(Ytrain, np.ones (Ytrain.shape)) - 1) <= .1
Étape 3: [RÉ]férence (base line)¶
Mon premier classificateur : plus proche voisin¶
Le classificateur du plus proche voisin est une méthode très simple: pour classer une entrée non étiquetée, nous cherchons l’entrée étiquettée la plus proche et prenons son étiquette. Vous pourrez facilement le mettre en oeuvre plus tard dans le semestre en fin de projet 1.
Pour le moment nous allons utiliser scikit-learn
qui implante un
classificateur plus général appelé \(k\)-plus proches voisins (KNN:
\(k\)-Nearest Neighbors). Avec ce classificateur, une entrée non
étiquetée est classée en fonction des étiquettes de ses \(k\) plus
proches voisins. L’entrée sera prédite comme appartenant à la classe C
si la majorité de ces \(k\) plus proches voisins appartient à la classe
C. Le classificateur du plus proche voisin correspond au cas
particulier \(k=1\).
Exercice :
Importez le classificateur
KNeighborsClassifier
à partir desklearn.neighbors
. Utilisez-le pour construire un nouveau modèle, en fixant le nombre de voisins à un (comme vu lors du CM3). Appelez-leclassifier
.Entraînez ce modèle avec
Xtrain
en appelant la méthode « fit ».Utilisez ensuite le modèle formé pour créer deux vecteurs de prédiction
Ytrain_predicted
etYtest_predicted
en appelant le méthodepredict
.Calculez
e_tr
, le taux d’erreur de l’entrainement ete_te
le taux d’erreur des tests.
Indication : consultez le cours et la documentation aussi souvent
que nécessaire, en commençant par taper KNeighborsClassifier?
# YOUR CODE HERE
raise NotImplementedError()
print("NEAREST NEIGHBOR CLASSIFIER")
print("Training error:", e_tr)
print("Test error:", e_te)
Ce problème est simple: nous n’avons aucune erreur dans l’ensemble d’entraînement et deux erreurs dans l’ensemble de test.
Ensuite, nous superposons les prédictions faites sur l’ensemble de test sur un nuage de points …¶
# The training examples are shown as white circles and the test examples are black squares.
# The predictions made are shown as letters in the black squares.
make_scatter_plot(X, images,
train_index, test_index,
predicted_labels=Ytest_predicted, axis='square')
Exercice : Observez la figure. Qui y a-t-il selon l’axe des x et des y ? Rédigez la réponse.
VOTRE RÉPONSE ICI
… ensuite, nous montrons la « vérité terrain » et calculons le taux d’erreur¶
# The training examples are shown as white circles and the test examples are blue squares.
make_scatter_plot(X, images.apply(transparent_background_filter),
train_index, test_index,
predicted_labels='GroundTruth', axis='square')
Étape 4: [BAR]res d’erreur (error bar)¶
Une fois les résultats obtenus nous devons évaluer leur significativité en calculant les barres d’erreurs. Évidemment, comme nous n’avons que dix exemples de test, on ne peut observer plus que dix erreurs; dans l’absolu ce n’est pas énorme mais pour notre exemple ça le serait !
Barre d’erreur 1-sigma¶
Exercice : Déterminez une première estimation de la barre d’erreur en calculant l’erreur standard \(\sigma\) sur l’ensemble d’entrainement. Pour rappel, \(\sigma = \sqrt{\frac{e_{te} * (1-e_{te})}{n}}\) où \(e_{te}\) est le taux d’erreur et \(n\) est le nombre de tests effectués.
De combien d’exemples aurions nous eu besoin pour diviser cette barre d’erreur par un facteur de deux?
# YOUR CODE HERE
raise NotImplementedError()
print(f"TEST SET ERROR RATE: {e_te:.2f}")
print(f"TEST SET STANDARD ERROR: {sigma:.2f}")
assert abs( sigma - 0.13 ) < 0.1
Barre d’erreur par validation croisée (cross-validation)¶
Calculons maintenant une autre estimation de la barre d’erreur en répétant l’évaluation de performance pour de multiples partitions entre ensemble d’entraînement et ensemble de test. Pour cela on peut répéter 100 fois la partition du jeu de données en ensemble d’entraînement et de test et calculer la moyenne et l’écart-type de l’erreur standard de ces réplicats. En un sens, ce résultat est plus informatif puisque il prend en compte la variabilité de l’ensemble d’entraînement et de test.
Remarque: cette estimation reste cependant biaisée; mais cela est hors programme.
n_te = 10
SSS = StratifiedShuffleSplit(n_splits=n_te, test_size=0.5, random_state=5)
E = np.zeros([n_te, 1])
k = 0
for train_index, test_index in SSS.split(X, Y):
print("TRAIN:", train_index, "TEST:", test_index)
Xtrain, Xtest = X.iloc[train_index], X.iloc[test_index]
Ytrain, Ytest = Y.iloc[train_index], Y.iloc[test_index]
classifier.fit(Xtrain, Ytrain.ravel())
Ytrain_predicted = classifier.predict(Xtrain)
Ytest_predicted = classifier.predict(Xtest)
e_te = error_rate(Ytest, Ytest_predicted)
print("TEST ERROR RATE:", e_te)
E[k] = e_te
k = k + 1
e_te_ave = np.mean(E)
# It is bad practice to show too many decimal digits:
print("\n\nCV ERROR RATE: {e_te_ave:.2f}")
print("CV STANDARD DEVIATION: {np.std(E):.2f}")
sigma = np.sqrt(e_te_ave * (1-e_te_ave) / n_te)
print(f"TEST SET STANDARD ERROR (for comparison): {sigma:.2f}")
Conclusion¶
C’est la fin de notre première classification d’images par apprentissage statistique. Nous avons appliqué le schéma VI-ME-RÉ-BAR: nous avons [VI]sualisé les données prétraitées, introduit une [ME]sure pour le problème de classification, à savoir le taux d’erreur pour les prédictions. Nous avons procédé avec une méthode de [RÉ]férence (baseline en anglais) en utilisant le classificateur du plus proche voisin vu en cours. Enfin, nous avons estimé les performances de ce premier classificateur en calculant les [BAR]res d’erreurs sur de nombreux échantillons d’ensembles d’apprentissage/de test.
Les prédictions obtenues sont assez robustes, ce qui n’est pas surprenant étant donné que, à quelques valeurs aberrantes près, les images sont bien contraintes (fond blanc, taille constante des images et des fruits, …).
La semaine prochaine nous verrons comment prétraiter un nouveau jeu de données que vous aurez choisi; la semaine suivante nous testerons d’autres classificateurs.
Mettez à jour votre rapport et déposez votre travail.
Pour préparer la semaine prochaine, il vous reste à choisir un jeu de données parmi ceux proposés. Pour cela ouvrez la feuille jeux de données.