Prétraitement des données

Les traîtements réalisés dans cette feuille sont majoritairement des rappels des semaines précédentes. Le but de cette feuille est que vous puissiez appliquer facilement et rapidement les traitements vus les semaines précédentes afin de vous consacrer à l’interprétation et l’application de ces traîtements sur vos données.

La feuille comprends trois parties:

  1. Le nettoyage de vos données par recentrage et recadrage (rappel de la semaine 6)

  2. La création d’attributs ad-hocs (rappel de la semaine 2)

  3. La séléction des attributs pertinents (nouveau)

Nous calculerons les performances d’un classifieur pour voir comment elles évoluent au fil des traîtements. Dans notre exemple, nous utiliserons un classifieur par plus proche voisin (k-NN):

# Load nearest neighbor classifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import balanced_accuracy_score as sklearn_metric
sklearn_model = KNeighborsClassifier(n_neighbors=3)

Nous stockerons les performances dans une variable performances de type pd.DataFrame:

import pandas as pd
performances = pd.DataFrame(columns = ['Traîtement', 'perf_tr', 'std_tr', 'perf_te', 'std_te'])
performances

1. Recadrage et recentrage des données (Rappel de la semaine 6)

Lors de la Semaine 7, vous avez prétraité les données de manière naïve:

  • Recadrage en 32x32 centré sur l’image et non le fruit

  • Enregistrement des nouvelles données dans un fichier crop_data.csv

Nous allons améliorer notre recadrage, de manière à recentrer l’image sur le fruit avant de recadrer l’image sur ce centre. Vous avez déjà rencentré vos images sur le smiley en Semaine 6 (Arcimboldo). Nous enregistrerons ces nouvelles données recentrées et recadrées dans un fichier clean_data.csv.

Ces étapes fonctionnent sur les données de pommes et de bananes. Vérifiez qu’elles s’appliquent correctement sur vos données en visualisant les résultats à chaque étape

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 les fonctions des semaines précédentes. 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
import scipy
from scipy import signal
import seaborn as sns
from glob import glob as ls
import sys

from utilities import *
from intro_science_donnees import *

Import des données

En commentant la ligne 1 ou la ligne 2, vous choisirez ici sur quel jeu de donnée vous travaillerez: les pommes et les bananes ou le vôtre.

dataset_dir = os.path.join(data_dir, 'apples_and_bananas')
# dataset_dir = 'data'

images = load_images(dataset_dir, "*.png")

Vérifions l’import des images en affichant les 20 premières images:

image_grid(images[:20])

On vérifie les performance de notre classifieur sur les données brutes et on les ajoute à notre tableau de données performances

df_raw = images.apply(image_to_series)
df_raw['étiquette'] = df_raw.index.map(lambda name: 1 if name[0] == 'a' else -1)
# Validation croisée (LENT)
p_tr, s_tr, p_te, s_te = df_cross_validate(df_raw, 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[0] = ["Données brutes", p_tr, s_tr, p_te, s_te]
performances.style.set_precision(2).background_gradient(cmap='Blues')

Détection du centre du fruit

Pour trouver le centre du fruit, il faut déjà arriver à séparer les pixels qui appartiennent aux fruits de ceux qui appartiennent au fond. Souvenez-vous, vous avez déjà fait cela en Semaine 2 avec la fonction foreground_filter.

Ici, on utilisera une fonction foreground_color_filter qui sépare le fruit du fond en prenant en compte la couleur de l’objet et pas uniquement les valeurs de luminosité.

NB: 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.

NB2: Pour accélérer l’éxécution de la cellule suivante, on n’affichera que les 20 premières images du jeu de données.

boolean_images = [foreground_color_filter(img, theta=2/3) for img in images]
image_grid(boolean_images[:20], titles=images.index)

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 suivante inverse simplement les valeurs booléennes si une majorité de True est détectée. Visualiser le résultat.

foreground_images = [invert_if_light_background(img) for img in boolean_images]
image_grid(foreground_images[:20], titles=images.index)

C’est légèrement mieux et nous nous en contenterons.

Nous avons à présent des pixels True représentant (grossièrement) les fruits, et des pixels False représentant le fond. À présent, nous allons:

  1. Extraire les coordonnées des pixels True et les afficher en nuages de points.

  2. Calculer le barycentre des pixels True, c’est à dire la moyenne des coordonnées sur les X et les Y afin d’estimer du centre du fruit.

  3. Recentrer en 32x32 autour du fruit,

Montrons un exemple sur la première image:

img = foreground_images[0]
plt.imshow(img)

Pour aller plus loin \(\clubsuit\): L’image reste très bruitée, on pourra appliquer un filtre sur l’image afin d’éliminer un peu les pixels isolés.

img = scipy.ndimage.generic_filter(img, np.nanmean, size=3)
plt.imshow(img)
coordinates = np.argwhere(img)
plt.scatter(coordinates[:,1], -coordinates[:,0], marker="x")

center = (np.mean(coordinates[:,1]), np.mean(coordinates[:,0]))
plt.scatter(center[0], -center[1], 300, c='r', marker='+',linewidth=5)

Recadrage

Maintenant que nous avons (approximativement) détecté le barycentre du fruit, il nous suffit de recadrer autour de ce barycentre avec la fonction crop_around_center_image. Comparons le résultat de cette fonction par rapport à l’ancienne fonction crop_image de la semaine précédente.

original_img = images[0]
crop_image(original_img) 
crop_around_center(original_img, center)

Le recadrage sur le fruit est amélioré sur cette image. Pour appliquer ce recadrage à toutes les images, on rassemblera ces fragments de code dans une fonction:

def crop_around_center_image(original_img):
    img = foreground_color_filter(original_img, theta=2/3)
    img = invert_if_light_background(img)
    img = scipy.ndimage.generic_filter(img, np.nanmean, size=3)
    coordinates = np.argwhere(img)
    center = (np.mean(coordinates[:,1]), np.mean(coordinates[:,0]))
    if np.isnan(center[0]) or np.isnan(center[1]):
        width, height = original_img.size
        center = (width/2, height/2)
    return crop_around_center(original_img, center) 

Conversion et sauvegarde intermédiaire

De la même manière que dans la Semaine 7, nous allons:

  1. Appliquer notre recentrage et recadrage sur toutes les données

  2. Convertir ces données en DataFrame de la librairie pandas

  3. Sauvegarder ces données brutes dans un fichier appelé clean_data.csv

La cellule suivante peut prendre du temps à l’éxécution.

# 1. Appliquer notre recentrage et recadrage sur toutes les données (LENT)
clean_images = images.apply(crop_around_center_image)
image_grid(clean_images[:20])
# 2. Convertir ces données en DataFrame de la librairie pandas
df_clean = clean_images.apply(image_to_series) # conversion
df_clean['étiquette'] = df_clean.index.map(lambda name: 1 if name[0] == 'a' else -1)  # ajout des étiquettes
df_clean
# 3. Sauvegarder ces données brutes dans un fichier appelé clean_data.csv
df_clean.to_csv('clean_data.csv') # export des données dans un fichier

df_clean = pd.read_csv('clean_data.csv', index_col=0)  # chargement du fichier dans le notebook

On vérifie les performance de notre classifieur sur les données recentrées et on les ajoute à notre tableau de données performances

# Validation croisée (LENT)
p_tr, s_tr, p_te, s_te = df_cross_validate(df_clean, 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[1] = ["Données recentrées", p_tr, s_tr, p_te, s_te]
performances.style.set_precision(2).background_gradient(cmap='Blues')

2. Création des attributs (Rappel de la semaine 2)

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 2

  • D’autres attributs (adhoc, pattern matching, PCA etc…) durant le premier mini-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.

redness_images = [redness_filter(img) for img in clean_images]
image_grid(redness_images[:20])

Exercice: À quelles couleurs correspondent les zones claires resp. sombres?

YOUR ANSWER HERE

Variante de la rougeur

Il s’agit d’un filtre de couleur qui extrait R-(G+B)/2. Ce filtre utilise également l’intensité moyenne (R+G+B)/3.

redness_alt_images = [difference_filter(img) for img in clean_images]
image_grid(redness_alt_images[:20])

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.

NB: N’oubliez pas de convertir les images en numpy.array pour appliquer les opérateurs binaires <, >, == etc.

threshold_images = [np.mean(np.array(img), axis = 2) < 100 for img in clean_images]
image_grid(threshold_images[:20])

Contours

Pour extraire les contours d’une image (préalablement seuillée), on doit soustraire l’image seuillée avec elle même en ma décalant d’un pixel vers le haut (resp. à droite). On fourni cette extraction de contour avec la fonction contours:

contour_images = [contours(img) for img in clean_images]
image_grid(contour_images[:20])

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 cette ensemble receuilli, nous allons ensuite sélectionner uniquement les attributs les plus pertinents.

On se propose d’utiliser trois descripteurs sur les couleurs:

  1. redness : moyenne de la différence des couches rouges et vertes (R-G), en enlevant le fond avec foreground_color_filter

  2. greeness: La même chose avec les couches (G-B)

  3. blueness: La même chose avec les couches (B-R)

Ainsi que trois autres descripteurs sur la forme:

  1. elongation: différence de variance selon les axes principaux des pixels du fruits (cf Semaine2)

  2. perimeter: nombre de pixels extraits du contour

  3. surface: nombre de pixels True après avoir extrait la forme.

df_features = pd.DataFrame({'redness': clean_images.apply(redness),
                            'greeness': clean_images.apply(greeness),
                            'blueness': clean_images.apply(blueness),
                            'elongation': clean_images.apply(elongation),
                            'perimeter': clean_images.apply(perimeter),
                            'surface': clean_images.apply(surface)})
df_features

Exercice: Nous vous encourageons à ajouter les attributs que vous avez implémenté dans le mini-projet précédent.

Les amplitudes des valeurs sont très différentes. Il faut donc normaliser ce tableau de données (rappel de la semaine 7) afin que les moyennes des colonnes soient égales à 0 et les déviations standard des colonnes soient égales à 1.

NB: Pensez à ajouter une toute petite valeur epsilon pour éviter une division par 0 au cas où la std d’une colonne est nulle.

epsilon = sys.float_info.epsilon # epsilon
df_features = (df_features - df_features.mean())/(df_features.std() + epsilon) # normalisation 
df_features.describe() # nouvelles statistiques de notre jeu de donnée

Enfin, on ajoute nos étiquettes (1 pour les pommes, -1 pour les bananes) dans la dernière colonne:

df_features["étiquette"] = df_clean["étiquette"]
df_features.style.background_gradient(cmap='RdYlGn_r')

On vérifie les performance de notre classifieur sur les attributs et on les ajoute à notre tableau de données performances

# Validation croisée (LENT)
p_tr, s_tr, p_te, s_te = df_cross_validate(df_features, 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[2] = ["6 attributs adhocs", p_tr, s_tr, p_te, s_te]
performances.style.set_precision(2).background_gradient(cmap='Blues')

3. Sélection des attributs (Nouveau)

Maintenant que nous avons sélectionné un ensemble d’attributs, nous souhaitons analyser lesquels améliorent le plus les performances de notre classifieur. Pour cela, on pourrait considérer deux approches:

  • Analyse de variance univariée: On considére que les attributs, pris individuellement, qui 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éments.

Analyse de variance univariée

Dans cette approche, on commence par calculer les correlations de nos attributs avec les étiquettes:

# Compute correlation matrix
corr = df_features.corr()
corr.style.set_precision(2).background_gradient(cmap='coolwarm')

Pour les pommes et les bananes, seule la “blueness” resp. la “redness” a une légère correlation resp. anti-correlation avec les étiquettes. Nous allons rajouter de nouveaux attributs sur les couleurs afin d’identifier s’il 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.

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("étiquette", axis = 1)

for i,h in enumerate(header):
    df_features_large[h] = [get_colors(img)[i] for img in clean_images]
    
epsilon = sys.float_info.epsilon # epsilon
df_features_large = (df_features_large - df_features_large.mean())/(df_features_large.std() + epsilon) # normalisation 
df_features_large.describe() # nouvelles statistiques de notre jeu de donnée
    
    
df_features_large["étiquette"] = df_clean["étiquette"]
df_features_large

On vérifie les performance de notre classifieur sur ce large ensemble d’attributs et on les ajoute à notre tableau de données 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", p_tr, s_tr, p_te, s_te]
performances.style.set_precision(2).background_gradient(cmap='Blues')
# Compute correlation matrix
corr_large = df_features_large.corr()
corr_large.style.set_precision(2).background_gradient(cmap='coolwarm')

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 features qui corrèlent le plus avec nos étiquettes (en valeur absolue):

# Sort by the absolute value of the correlation coefficient
sval = corr_large['étiquette'][:-1].abs().sort_values(ascending=False)
ranked_columns = sval.index.values
print(ranked_columns) 

Sélectionnons seulement les 5 premiers et affichons leur valeurs dans un pair-plot:

col_selected = ranked_columns[0:5]
df_features_final = pd.DataFrame.copy(df_features_large)
df_features_final = df_features_final[col_selected]

df_features_final['étiquette'] = df_features_large["étiquette"]
g = sns.pairplot(df_features_final, hue="étiquette", 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).

# On importe notre modèle
from sklearn.metrics import balanced_accuracy_score as sklearn_metric
sklearn_model = KNeighborsClassifier(n_neighbors=3)
feat_lc_df, ranked_columns = feature_learning_curve(df_features_large, sklearn_model, sklearn_metric)
#feat_lc_df[['perf_tr', 'perf_te']].plot()
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

YOUR ANSWER HERE

On pourra exporter un nouveau fichier CSV appelé features_data.csv contenant les attributs utiles. Pour l’exemple, nous exporterons les 5 premiers attributs comme dans l’exemple plus haut.

df_features_final.to_csv('features_data.csv') # export des données dans un fichier
#df_features_final = pd.read_csv('features_data.csv')  # chargement du fichier dans le notebook

Enfin, on ajoute les performance de notre classifieur 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 (LENT)
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] = ["5 attributs par analyse de variance univarié", p_tr, s_tr, p_te, s_te]
performances.style.set_precision(2).background_gradient(cmap='Blues')

Analyse de variance multi-variée \(\clubsuit\)

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

best_perf = -1
std_perf = -1
best_i = 0
best_j = 0
for i in np.arange(5): 
    for j in np.arange(i+1,5): 
        df = df_features_large[[ranked_columns[i], ranked_columns[j], 'étiquette']]
        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’attribut qui donne les meilleurs perforances? Est-ce que l’approche multi-variée est nécessaire avec les pommes et les bananes? Avec votre jeu de données?

Conclusion

Terminez toujours par une conclusion. Qu’avez-vous appris ? Qu’est-ce qui pourrait être amélioré ?

Cette première feuille vous fait passer par le formatage de base des données et un flux de travail d’analyse de données de base. Les feuilles des semaines suivantes aborderont d’autres aspects de l’analyse (biais, pré-traîtement, classifieur).

Bonne pratique: Terminez toujours votre travail par “Kernel > Restart & Run all” pour vérifier que toutes vos cellules fonctionnent dans leur ordre d’exécution.