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

  1. 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édictions

  • Y: 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_dataa é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

  1. 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

  1. Importez le classificateur KNeighborsClassifier à partir de sklearn.neighbors. Utilisez-le pour construire un nouveau modèle, en fixant le nombre de voisins à un (voir le CM3). Appelez-le classifier.

  2. Entraînez ce modèle avec Xtrain en appelant la méthode « fit ».

  3. Utilisez ensuite le modèle formé pour créer deux vecteurs de prédiction Ytrain_predicted et Ytest_predicted en appelant le méthode predict.

  4. Calculez e_tr, le taux d’erreur de l’entrainement et e_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}}\)\(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.