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 et utilitaires#
# Rechargement automatique
%load_ext autoreload
%autoreload 2
# Import des utilitaires
from intro_science_donnees import *
from utilities import *
# Configuration intégration dans Jupyter
%matplotlib widget
É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.
### BEGIN SOLUTION
from intro_science_donnees import *
dataset_dir = os.path.join(data.dir, 'ApplesAndBananasSimple')
images = load_images(dataset_dir, "*.png")
image_grid(images, titles=images.index)
### END SOLUTION)
É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.
Note
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
:
### BEGIN SOLUTION
df.describe()
### END SOLUTION
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!
### BEGIN SOLUTION
dfstd = (df - df.mean()) / df.std()
dfstd
### END SOLUTION
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.
### BEGIN SOLUTION
dfstd.style.background_gradient(cmap='coolwarm')
### END SOLUTION
Exercice
Produisez une carte de chaleur de la matrice de corrélation.
Indication: si nécessaire, consultez le cours 2.
### BEGIN SOLUTION
dfstd.corr().style.background_gradient(cmap ='coolwarm', axis=None)
### END SOLUTION
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 cours de la semaine dernière, utilisez Seaborn
pour produire un pair plot comparant la rougeur et l’élongation des
fruits, en représentant chaque classe par une couleur (option
hue
). Utilisez l’option palette
pour choisir Set2
comme palette
de couleurs et l’option diag_kind
pour choisir une représentation en
histogrammes sur la diagonale.
### BEGIN SOLUTION
import seaborn as sns
sns.pairplot(dfstd, hue="class", diag_kind="hist", palette='Set2');
### END SOLUTION
Attention
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']
Indication
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.
### BEGIN SOLUTION
Ytrain = Y.iloc[train_index]
Ytest = Y.iloc[test_index]
### END SOLUTION
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.
### BEGIN SOLUTION
image_grid(images.iloc[test_index], titles=test_index)
### END SOLUTION
Indication
Remarque
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])
BEGIN SOLUTION
\(e = 2\), \(n=5\), \(\frac{e}{n}= \frac{2}{5} = 0.4\)
END SOLUTION
### BEGIN SOLUTION
(predictions != solutions).mean()
### END SOLUTION
### BEGIN SOLUTION
error_rate(solutions, predictions)
### END SOLUTION
### BEGIN SOLUTION
assert error_rate(Ytrain, Ytrain) == 0
assert error_rate(Ytrain, np.ones(Ytrain.shape)) == 0.5
assert error_rate(Ytrain, np.zeros(Ytrain.shape)) == 1
### END SOLUTION
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 (voir le 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?
### BEGIN SOLUTION
from sklearn.neighbors import KNeighborsClassifier
classifier = KNeighborsClassifier(n_neighbors=1)
classifier.fit(Xtrain, Ytrain)
Ytrain_predicted = classifier.predict(Xtrain)
Ytest_predicted = classifier.predict(Xtest)
e_tr = error_rate(Ytrain, Ytrain_predicted)
e_te = error_rate(Ytest, Ytest_predicted)
### END SOLUTION
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.
BEGIN SOLUTION
en x: la rougeur du fruit (redness)
en y: l’élongation du fruit (elongation)
END SOLUTION
… 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?
### BEGIN SOLUTION
n_te = len(Ytest)
sigma = np.sqrt(e_te * (1-e_te) / n_te)
### END SOLUTION
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(f"\n\nCV ERROR RATE: {e_te_ave:.2f}")
print(f"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#
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.