Extraction d’attributs#
Les traitements réalisés dans cette feuille sont majoritairement des rappels des semaines précédentes. Le but de cette feuille est de vous fournir une trame afin d’appliquer sereinement les traitements vus les semaines précédentes
La feuille comprends trois parties:
Le prétraitement de vos données par extraction d’avant plan, recentrage et recadrage (rappels de la semaine 6)
L’extraction d’attributs ad-hoc (rappels de la semaine 4)
La sélection d’attributs pertinents
Dans un premier temps, vous exécuterez cette feuille sur le jeu de données de pommes et de bananes. Puis vous la reprendrez avec votre propre jeu de données, en visualisant les résultats à chaque étape, et en ajustant comme nécessaire.
1. Prétraitement (rappels de la semaine 6)#
Nous allons détecter le centre du fruit et recadrer l’image sur ce
centre (comme en Semaine 6). Nous ferons ensuite une sauvegarde
intermédiaire: Enfin nous enregistrerons les images prétraitées dans
un dossier clean_data
, ainsi qu’une table des pixels dans
clean_data.csv
.
Import des bibliothèques#
Nous commençons par importer les bibliothèques dont nous aurons
besoin. Comme d’habitude, nous vous fournissons quelques utilitaires
dans le fichier utilities.py
. Vous pouvez ajouter vos propres
fonctions 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
import scipy
from scipy import signal
import pandas as pd
import seaborn as sns
from glob import glob as ls
import sys
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import balanced_accuracy_score as sklearn_metric
from utilities import *
from intro_science_donnees import data
from intro_science_donnees import *
Pour avoir une première idée de l’impact des prétraitements sur la
classification, nous calculerons au fur et à mesure les performances
d’un classificateur – ici du plus proche voisin kNN – et nous les
stockerons dans une table de données performances
:
sklearn_model = KNeighborsClassifier(n_neighbors=3)
performances = pd.DataFrame(columns = ['Traitement', 'perf_tr', 'std_tr', 'perf_te', 'std_te'])
performances
Import des données#
En mettant en commentaire la ligne 2 ou la ligne 1, vous choisirez ici sur quel jeu de données le prétraitement s’applique: d’abord les pommes et les bananes, puis le vôtre.
dataset_dir = os.path.join(data.dir, 'ApplesAndBananas')
# dataset_dir = 'data'
images = load_images(dataset_dir, "?[012]?.png")
image_grid(images)
Calculons les performances de notre classificateur en l’appliquant directement à la représentation en pixels des images :
df_raw = images.apply(image_to_series)
df_raw['class'] = df_raw.index.map(lambda name: 1 if name[0] == 'a' else -1)
Exercice
Calculez la validation croisée en appliquant df_cross_validate
à la
table df_raw
en nommant les performances p_tr
et p_te
ainsi que
les écart-types s_tr
et s_te
, pour les ensembles d’entrainement et
de test respectivement. La métrique de performance a été définie lors
de l’import des librairies et se nomme sklearn_metric
.
# Validation croisée
# VOTRE CODE ICI
raise NotImplementedError()
metric_name = sklearn_metric.__name__.upper()
print("AVERAGE TRAINING {0:s} +- STD: {1:.2f} +- {2:.2f}".format(metric_name, p_tr, s_tr))
print("AVERAGE TEST {0:s} +- STD: {1:.2f} +- {2:.2f}".format(metric_name, p_te, s_te))
Ajoutons ces résultats à notre table performances
:
performances.loc[0] = ["Images brutes", p_tr, s_tr, p_te, s_te]
performances.style.format(precision=2).background_gradient(cmap='Blues')
Extraction de l’avant-plan#
Pour trouver le centre du fruit, il faut déjà arriver à séparer les
pixels qui appartiennent au fruit de ceux qui appartiennent au
fond. Souvenez-vous, vous avez déjà fait cela en Semaine 6 avec la
fonction foreground_filter
.
Dans l’exemple suivant, on utilise une variante
foreground_redness_filter
qui sépare le fruit du fond en prenant en
compte la rougeur de l’objet et pas uniquement les valeurs de
luminosité.
Attention
Comme pour foreground_filter
, cette fonction a besoin d’un seuil
(compris entre 0 et 1) sur les valeurs des pixels à partir duquel on
décide s’il s’agit du fruit ou du fond. Avec les pommes et les
bananes, on se contentera de la valeur 2/3. Avec vos données, faites
varier cette valeur de seuil pour trouver la valeur qui semble la plus
adéquate.
foreground_imgs = [foreground_redness_filter(img, theta=.75)
for img in images]
image_grid(foreground_imgs)
On peut voir que selon s’il s’agit d’objets sombres sur fond clair ou
d’objets clairs sur fond sombre, on n’obtient pas les mêmes valeurs
booléenne en sortie de foreground_filter
. La fonction
invert_if_light_background
inverse simplement les valeurs booléennes
si une majorité de True
est détectée. Voilà le résultat.
foreground_imgs = [invert_if_light_background(foreground_redness_filter(img, theta=.75))
for img in images]
image_grid(foreground_imgs)
C’est légèrement mieux mais les images restent très bruitées; on
choisit d’appliquer un filtre afin de réduire les pixels isolés. C’est
ce qu’on fait avec la fonction scipy.ndimage.gaussian_filter()
.
foreground_imgs = [scipy.ndimage.gaussian_filter(
invert_if_light_background(
foreground_redness_filter(img, theta=.6)),
sigma=.2)
for img in images]
image_grid(foreground_imgs)
Les traitements précédents résultent d’un certain nombre de choix faits au fil de l’eau (seuil, filtre par rougeur, débruitage).
Exercice
Définissez ci-dessous la fonction my_foreground_filter
qui prend en
entrée une image et:
lui applique
foreground_redness_filter
inverse les valeurs booléennes si le fond est blanc avec la fonction
invert_if_light_background
applique le filtre gaussien et supprime les pixels isolés avec un sigma à 0.2
Cette fonction extrait donc l’objet de l’arrière-plan
# VOTRE CODE ICI
raise NotImplementedError()
Cette fonction fait partie intégrale de la narration des
traitements que nous menons dans cette feuille. C’est pour cela que
nous la définissons directement dans cette feuille, et non dans
utilities.py
comme on l’aurait fait pour du code réutilisable.
Détection du centre du fruit#
Nous allons à présent déterminer la position du centre du fruit, en prenant la moyenne des coordonnées des pixels de l’avant plan.
Faisons cela sur la première image:
img = images.iloc[0]
img
Exercice
Calculez l’avant plan de l’image dansforeground
avec la fonction
my_foreground_filter
que vous venez de créer.
On extrait ensuite les coordonnées de ses pixels :
# VOTRE CODE ICI
raise NotImplementedError()
coordinates = np.argwhere(foreground)
que l’on affiche comme un nuage de points :
plt.scatter(coordinates[:,1], -coordinates[:,0], marker="x");
Exercice
Calculez les coordonnées du barycentre des pixels de l’avant plan –
c’est-à-dire la moyenne des coordonnées sur les X et les Y – afin
d’estimer les coordonnées du centre du fruit. On garde le résultat
dans center
. Pour rappel, vous avez déjà calculé cela en Semaine 6.
# VOTRE CODE ICI
raise NotImplementedError()
plt.scatter(coordinates[:,1], -coordinates[:,0], marker="x");
plt.scatter(center[0], -center[1], 300, c='r', marker='+',linewidth=5);
Ce n’est pas parfait: du fait des groupes de pixels à droite qui ont été détectées comme de l’avant plan, le centre calculé est plus à droite que souhaité. Mais cela reste un bon début.
Recadrage#
Maintenant que nous avons (approximativement) détecté le centre du
fruit, il nous suffit de recadrer autour de ce centre. Une fonction
crop_around_center
est fournie pour cela. Comparons le résultat de
cette fonction par rapport à crop_image
utilisé en feuille2 :
crop_image(img)
Exercice : Finissez d’écrire la fonction crop_around_center
. Il ne manque que les coordonnées à droite (right) et en bas (bottom).
crop_around_center(img, center)
On constate que le recadrage sur le fruit est amélioré, même si pas encore parfait.
Récapitulatif du prétraitement#
À nouveau, nous centralisons tous les choix faits au fil de l’eau en une unique fonction effectuant le prétraitement. Cela facilite l’application de ce traitement à toute image et permet de documenter les choix faits :
def my_preprocessing(img):
"""
Prétraitement d'une image
- Calcul de l'avant plan
- Mise en transparence du fond
- Calcul du centre
- Recadrage autour du centre
"""
foreground = my_foreground_filter(img)
img = transparent_background(img, foreground)
coordinates = np.argwhere(foreground)
if len(coordinates) == 0: # Cas particulier: il n'y a aucun pixel dans l'avant plan
width, height = img.size
center = (width/2, height/2)
else:
center = (np.mean(coordinates[:, 1]), np.mean(coordinates[:, 0]))
img = crop_around_center(img, center)
return img
plt.imshow(my_preprocessing(images.iloc[0]));
Exercice
Appliquez le prétraitement à toutes les images que vous mettrez dans clean_images
.
# VOTRE CODE ICI
raise NotImplementedError()
image_grid(clean_images)
Performance de la classification après prétraitement#
Convertissons maintenant les images prétraitées dans leurs représentations en pixels, regroupées dans une table:
# conversion
df_clean = clean_images.apply(image_to_series)
# ajout des étiquettes
df_clean['class'] = df_clean.index.map(lambda name: 1 if name[0] == 'a' else -1)
Exercice
À l’aide de la fonction df_cross_validate
, calculez les performance
du classificateur sur les images prétraitées.
# VOTRE CODE ICI
raise NotImplementedError()
Exercice
Ajoutez ces résultats à la table performances
.
# VOTRE CODE ICI
raise NotImplementedError()
performances.style.format(precision=2).background_gradient(cmap='Blues')
Sauvegarde intermédiaire#
Nous sauvegardons maintenant les images prétraitées dans le répertoire
clean_data
au format PNG
:
os.makedirs('clean_data', exist_ok=True)
for name, img in clean_images.items():
img.save(os.path.join('clean_data', os.path.splitext(name)[0]+".png"))
Explication
splitext
sépare un nom de fichier de son extension :
os.path.splitext("machin.jpeg")
Nous sauvegardons la table de données dans un fichier clean_data.csv
:
df_clean.to_csv('clean_data.csv')
Ainsi il sera possible de travailler sur la suite de cette feuille et les feuilles ultérieures sans avoir besoin de refaire le prétraitement :
df_clean = pd.read_csv('clean_data.csv', index_col=0)
Extraction des attributs (rappels de la semaine 4)#
Durant les semaines précédentes, vous avez déjà implémenté des attributs tels que :
La rougeur (redness) et l’élongation durant la semaine 4;
D’autres attributs (adhoc, pattern matching, PCA, etc.) durant le premier projet.
L’idée de cette section est de réappliquer ces attributs sur vos nouvelles données.
Filtres#
Récapitulons les types de filtres que nous avons en magasin:
La rougeur (redness)#
Il s’agit d’un filtre de couleur qui extrait la différence entre la
couche rouge et la couche verte (R-G). Lors de la Semaine 2, nous
avons fait la moyenne de ce filtre sur les fruits pour obtenir un
attribut (valeur unique par image). Ici, affichons simplement la
différence R-G
:
image_grid([redness_filter(img)
for img in clean_images])
Exercice
À quelles couleurs correspondent les zones claires resp. sombres? Pourquoi le fond n’apparaît-il pas toujours avec la même clarté?
VOTRE RÉPONSE ICI
Variante de la rougeur#
Il s’agit d’un filtre de couleur qui extrait la rougeur de chaque pixel calculée avec \(R-(G+B)/2\) :
image_grid([difference_filter(img)
for img in clean_images])
Pour d’autres idées de mesures sur les couleurs, consulter cette page wikipédia.
Seuillage#
Souvenez vous que vous pouvez également seuiller les valeurs des
pixels (par couleur ou bien par luminosité). C’est ce que l’on fait
dans les fonctions foreground_filter
ou foreground_color_filter
.
Indication
N’oubliez pas de convertir les images en tableau numpy
pour
appliquer les opérateurs binaires <
, >
, ==
, etc.
image_grid([np.mean(np.array(img), axis = 2) < 100
for img in clean_images])
Contours#
Pour extraire les contours d’une image (préalablement seuillée), on
doit soustraire l’image seuillée avec elle même en la décalant d’un
pixel vers le haut (resp. à droite). On fourni cette extraction de
contours avec la fonction contours
:
image_grid([contours(np.mean(np.array(img), axis = 2) < 100 )
for img in clean_images])
Création d’attributs à partir des filtres#
Maintenant que nous avons récapitulé les filtres en notre possession, nous allons calculer un large ensemble d’attributs sur nos images. Une fois cet ensemble recueilli, nous allons ensuite sélectionner uniquement les attributs les plus pertinents.
On se propose d’utiliser trois attributs sur les couleurs:
redness
: moyenne de la différence des couches rouges et vertes (R-G), en enlevant le fond avecforeground_color_filter
;greenness
: La même chose avec les couches (G-B);blueness
: La même chose avec les couches (B-R).
Ainsi que trois autres attributs sur la forme:
elongation
: différence de variance selon les axes principaux des pixels du fruits (cf Semaine2);perimeter
: nombre de pixels extraits du contour;surface
: nombre de pixelsTrue
après avoir extrait la forme.
Exercice
Créez la table df_features
qui contient ces six paramètres ainsi que
les attributs que vous avez implémenté dans le projet précédent.
Indications
Dans le Projet 1, nous avions défini la table ainsi:
df = pd.DataFrame({
'redness': mesimages.apply(redness),
'elongation': mesimages.apply(elongation),
'class': mesimages.index.map(lambda name: 1 if name[0] == 'a' else -1),
})
df
# VOTRE CODE ICI
raise NotImplementedError()
df_features
Les amplitudes des valeurs sont très différentes. Il faut donc normaliser ce tableau de données (rappel de la semaine 3 et du projet
afin que les moyennes des colonnes soient égales à 0 et les déviations standard des colonnes soient égales à 1.
Rappel: notez l’utilisation d’une toute petite valeur epsilon pour éviter une division par 0 au cas où une colonne soit constante :
epsilon = sys.float_info.epsilon
df_features = (df_features - df_features.mean())/(df_features.std() + epsilon) # normalisation
df_features.describe() # nouvelles statistiques de notre jeu de donnée
On ajoute nos étiquettes (1 pour les pommes, -1 pour les bananes) dans la dernière colonne :
df_features["class"] = df_clean["class"]
Et on remplace les NA par des 0, par défaut:
df_features[df_features.isna()] = 0
df_features.style.background_gradient(cmap='coolwarm')
Exercice
Calculer les performances de notre classificateur sur les attributs et
ajouter les à notre table performances
:
# VOTRE CODE ICI
raise NotImplementedError()
performances.style.format(precision=2).background_gradient(cmap='Blues')
Sélection des attributs (nouveau!)#
Maintenant que nous avons extrait un ensemble d’attributs, nous souhaitons analyser lesquels améliorent le plus les performances de notre classificateur. Pour cela, nous tenterons deux approches :
Analyse de variance univariée : On considère que les attributs qui, pris individuellement, corrèlent le plus avec nos étiquettes amélioreront le plus la performance une fois groupés.
Analyse de variance multi-variée : On considère qu’il existe un sous-ensemble d’attributs permettant d’améliorer davantage les performances que les attributs étudiés séparément.
Analyse de variance univariée#
Dans cette approche, on commence par calculer les corrélations de chacun de nos attributs avec les étiquettes :
# Compute correlation matrix
corr = df_features.corr()
corr.style.format(precision=2).background_gradient(cmap='coolwarm')
Pour les pommes et les bananes, seule la « redness » resp. l“« elongation » a une légère correlation resp. anti-corrélation avec les étiquettes. Nous allons rajouter de nouveaux attributs sur les couleurs afin d’identifier s’il y aurait un attribut qui corrélerait davantage avec les étiquettes.
NB: On en profite pour renormaliser en même temps que l’on ajoute des attributs.
On utilise la fonction get_colors()
donnée dans utilities
show_source(get_colors)
Exercice
Appliquez get_colors()
à vos images clean_images
.
# VOTRE CODE ICI
raise NotImplementedError()
s = df_features.iloc[3].replace({'redness': 4})
df_features
header = ['R','G','B','M=maxRGB', 'm=minRGB', 'C=M-m', 'R-(G+B)/2', 'G-B', 'G-(R+B)/2', 'B-R', 'B-(G+R)/2', 'R-G', '(G-B)/C', '(B-R)/C', '(R-G)/C', '(R+G+B)/3', 'C/V']
df_features_large = df_features.drop("class", axis = 1)
df_features_large = pd.concat([df_features_large, clean_images.apply(get_colors)], axis=1)
Exercice
Normalisez df_features_large
. On utilisera un epsilon au cas où certaines valeurs de std() soit nulles.
# VOTRE CODE ICI
raise NotImplementedError()
Exercice
Remplacez les valeurs de NA par des 0 puis rajouter les étiquettes (1 pour les pommes, -1 pour les bananes) dans la dernière colonne (sans normalisation!)
# VOTRE CODE ICI
raise NotImplementedError()
df_features_large
On vérifie les performances de notre classificateur sur ce large
ensemble d’attributs et on les ajoute à notre table performances
:
# Validation croisée (LENT)
p_tr, s_tr, p_te, s_te = df_cross_validate(df_features_large, sklearn_model, sklearn_metric)
metric_name = sklearn_metric.__name__.upper()
print("AVERAGE TRAINING {0:s} +- STD: {1:.2f} +- {2:.2f}".format(metric_name, p_tr, s_tr))
print("AVERAGE TEST {0:s} +- STD: {1:.2f} +- {2:.2f}".format(metric_name, p_te, s_te))
performances.loc[3] = ["23 attributs ad-hoc", p_tr, s_tr, p_te, s_te]
performances.style.format(precision=2).background_gradient(cmap='Blues')
Exercice
Calculez la matrice de correlation comme on a fait plus haut.
# VOTRE CODE ICI
raise NotImplementedError()
Dans l’approche univariée, les attributs qui nous intéresse le plus sont ceux qui ont une grande corrélation en valeur absolue avec les étiquettes. Autrement dit, les valeurs très positives (corrélation) ou très négatives (anti-corrélation) de la dernière colonne sont intéressants pour nous.
On va donc ordonner les attributs qui corrèlent le plus avec nos étiquettes (en valeur absolue) :
# Sort by the absolute value of the correlation coefficient
sval = corr_large['class'][:-1].abs().sort_values(ascending=False)
ranked_columns = sval.index.values
print(ranked_columns)
Sélectionnons seulement les trois premiers attributs et visualisons leur valeurs dans un pair-plot :
col_selected = ranked_columns[0:3]
df_features_final = pd.DataFrame.copy(df_features_large)
df_features_final = df_features_final[col_selected]
df_features_final['class'] = df_features_large["class"]
g = sns.pairplot(df_features_final, hue="class", markers=["o", "s"], diag_kind="hist")
Trouver le nombre optimal d’attributs#
On s’intéresse à présent au nombre optimal d’attributs. Pour cela, on calcule les performances en rajoutant les attributs dans l’ordre du classement fait dans la sous-section précédente (classement en fonction de la corrélation avec les étiquettes).
Exercice
Importez le modèle KNN avec trois voisins dans l’objet sklearn_model
.
# On importe notre modèle
from sklearn.metrics import balanced_accuracy_score as sklearn_metric
# VOTRE CODE ICI
raise NotImplementedError()
feat_lc_df, ranked_columns = feature_learning_curve(df_features_large, sklearn_model, sklearn_metric)
plt.errorbar(feat_lc_df.index+1, feat_lc_df['perf_tr'], yerr=feat_lc_df['std_tr'], label='Training set')
plt.errorbar(feat_lc_df.index+1, feat_lc_df['perf_te'], yerr=feat_lc_df['std_te'], label='Test set')
plt.xticks(np.arange(1, 22, 1))
plt.xlabel('Number of features')
plt.ylabel(sklearn_metric.__name__)
plt.legend(loc='lower right');
Exercice Combien d’attributs pensez-vous utile de conserver? Justifiez.
VOTRE RÉPONSE ICI
Exercice
Exportez votre table df_features_final
dans le fichier
features_data.csv
contenant les attributs utiles. Pour l’exemple,
nous exporterons les trois premiers attributs comme dans l’exemple
plus haut :
# VOTRE CODE ICI
raise NotImplementedError()
Enfin, on ajoute les performance de notre classificateur sur ce
sous-ensemble d’attributs sélectionnées par analyse de variance
univariée et on les ajoute à notre tableau de données performances
:
# Validation croisée
p_tr, s_tr, p_te, s_te = df_cross_validate(df_features_final, sklearn_model, sklearn_metric)
metric_name = sklearn_metric.__name__.upper()
print("AVERAGE TRAINING {0:s} +- STD: {1:.2f} +- {2:.2f}".format(metric_name, p_tr, s_tr))
print("AVERAGE TEST {0:s} +- STD: {1:.2f} +- {2:.2f}".format(metric_name, p_te, s_te))
performances.loc[4] = ["3 attributs par analyse de variance univarié", p_tr, s_tr, p_te, s_te]
performances.style.format(precision=2).background_gradient(cmap='Blues')
♣ Analyse de variance multi-variée#
La seconde approche est de considérer les attributs de manière groupés et non pas par ordre d’importance en fonction de leur corrélation individuelle avec les étiquettes. Peut-être que deux attributs, ayant chacun une faible corrélation avec les étiquettes, permettront une bonne performance de classification pris ensemble.
Pour analyser cela, on considère toutes les paires d’attributs et on calcule nos performances avec ces paires. Si le calcul est trop long, réduisez le nombre d’attributs à considérer.
best_perf = -1
std_perf = -1
best_i = 0
best_j = 0
nattributs = 23
for i in np.arange(nattributs):
for j in np.arange(i+1,nattributs):
df = df_features_large[[ranked_columns[i], ranked_columns[j], 'class']]
p_tr, s_tr, p_te, s_te = df_cross_validate(df_features_large, sklearn_model, sklearn_metric)
if p_te > best_perf:
best_perf = p_te
std_perf = s_te
best_i = i
best_j = j
metric_name = sklearn_metric.__name__.upper()
print('BEST PAIR: {}, {}'.format(ranked_columns [best_i], ranked_columns[best_j]))
print("AVERAGE TEST {0:s} +- STD: {1:.2f} +- {2:.2f}".format(metric_name, p_te, s_te))
Exercice
Quelle est la paire d’attributs qui donne les meilleurs performances? Est-ce que l’approche multi-variée est nécessaire avec les pommes et les bananes? Avec votre jeu de données?
VOTRE RÉPONSE ICI
Conclusion#
Cette feuille a fait un tour d’horizon d’outils à votre disposition pour le prétraitement de vos images et l’extraction d’attributs. Prenez ici quelques notes sur ce que vous avez appris, observé, interprété.
VOTRE RÉPONSE ICI
Si le cours de la Semaine 8 a eu lieu, vous pouvez passer à la feuille sur la comparaison de classificateurs!
Sinon, commencez votre propre analyse.