Comparer des classificateurs#
Dans la feuille précédente, nous avons observé que les performances pouvaient varier selon le niveau de prétraitement des données (données brutes, données nettoyées, extractions d’attributs, etc.). Cependant, l’analyse de performance n’a été conduite qu’avec un seul classificateur (plus proches voisins).
Il est posssible que chaque classificateur donne des résultats différents pour un niveau de pré-traitement donné.
Dans cette feuille, on étudie les performances selon le type de classificateur. Les étapes seront:
Déclarer et entraîner les différents classificateurs sur nos données;
Visualiser les performances de chaque classificateur;
Comprendre et identifier le sous-apprentisssage et le surapprentissage;
Étudier un comité de classificateurs et l’incertitude dans les données;
Pour aller plus loin ♣: Apprentissage profond et transfert d’apprentissage.
Entraînement des différents classificateurs#
Import des bibliothèques#
import os, re
from glob import glob as ls
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
import pandas as pd
import seaborn as sns; sns.set()
from PIL import Image
%load_ext autoreload
%autoreload 2
import warnings
warnings.filterwarnings("ignore")
from sys import path
from utilities import *
from intro_science_donnees import data
from intro_science_donnees import *
Import des données#
Nous chargeons les images prétraitées dans la feuille précédente :
dataset_dir = 'clean_data'
images = load_images(dataset_dir, "*.png")
Vérifions l’import en affichant les 20 premières images :
image_grid(images[:20])
Pour mettre en œuvre des classificateurs, chargez dans df_features
les
attributs extraits dans la fiche précédente.
# VOTRE CODE ICI
raise NotImplementedError()
On vérifie que nos données sont normalisées :
df_features.describe()
Déclaration des classificateurs#
Nous allons à présent importer les classificateurs depuis la librairie
scikit-learn
. Pour cela, on stockera:
les noms des classificateurs dans la variable
model_name
;les classificateurs eux-mêmes dans la variable
model_list
.
from sklearn.neural_network import MLPClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.neighbors import RadiusNeighborsClassifier
from sklearn.svm import SVC
from sklearn.gaussian_process import GaussianProcessClassifier
from sklearn.gaussian_process.kernels import RBF
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.discriminant_analysis import QuadraticDiscriminantAnalysis
model_name = ["Nearest Neighbors", "Parzen Windows", "Linear SVM", "RBF SVM", "Gaussian Process",
"Decision Tree", "Random Forest", "Neural Net", "AdaBoost",
"Naive Bayes", "QDA"]
model_list = [
KNeighborsClassifier(3),
RadiusNeighborsClassifier(radius=12.0),
SVC(kernel="linear", C=0.025, probability=True),
SVC(gamma=2, C=1, probability=True),
GaussianProcessClassifier(1.0 * RBF(1.0)),
DecisionTreeClassifier(max_depth=10),
RandomForestClassifier(max_depth=10, n_estimators=10, max_features=1),
MLPClassifier(alpha=1, max_iter=1000),
AdaBoostClassifier(),
GaussianNB(),
QuadraticDiscriminantAnalysis()]
La bibliothèque scikit-learn
nous simplifie la tâche : bien que les
classificateurs aient des fonctionnements très différents, leurs
interfaces sont identiques (rappelez vous la notion d’encapsulation
vue dans le cours «Programmation Modulaire»). Les méthodes sont:
.fit
pour entraîner le classificateur sur des données d’entraînement;.predict
pour prédire des étiquettes sur les données de test;.predict_proba
pour obtenir des prédictions sur les données de test sous forme de probabilités sur les classes;.score
pour calculer la performance du classificateur.
Visualisation des performances des classificateurs#
Nous allons à présent faire des tests systématiques sur cet ensemble
de données (attributs de l’analyse de variance univiarié). La fonction
systematic_model_experiment(data_df, model_name, model_list, sklearn_metric)
permet de réaliser ces tests systématiques.
??systematic_model_experiment
Que renvoit cette fonction ?
VOTRE RÉPONSE ICI
from sklearn.metrics import balanced_accuracy_score as sklearn_metric
compar_results = systematic_model_experiment(df_features, model_name, model_list, sklearn_metric)
compar_results.style.format(precision=2).background_gradient(cmap='Blues')
Exercice
Quelle méthode obtient les meilleures performances de test?
model_list[compar_results.perf_te.argmax()]
VOTRE RÉPONSE ICI
Exercice
On peut également représenter les performances dans un graphique en
barres. Sachant que le nom de la métrique s’obtient avec sklearn_metric.__name__
, ajouter à la figure le nom des axes pertinents.
compar_results[['perf_tr', 'perf_te']].plot.bar()
plt.ylim(0.5, 1)
# VOTRE CODE ICI
raise NotImplementedError()
Sous-apprentissage et surapprentissage#
Lorsque l’on entraîne un classificateur sur des données, deux comportements sont à éviter:
Le sous-apprentissage (under-fitting).
Le sur-apprentissage (over-fitting).
Ces concepts sont vus en Semaine 8
Analysons quels classificateurs ont sur-appris respectivement sous-appris. Pour
cela nous allons identifier les classificateurs dont les performances de test sont
inférieures à la performance de test médiane. tebad
est un vecteur de booléens.
tebad = compar_results.perf_te < compar_results.perf_te.median()
tebad
Exercice
Calculer maintenant dans trbad
les classificateurs dont les performances
d’entrainement sont inférieurs à la performance d’entrainement médiane. trbad
est un vecteur de booléens.
# VOTRE CODE ICI
raise NotImplementedError()
On peut alors identifier les classificateurs qui ont sur-appris (qui correpondent à True
)
c’est-à-dire ceux dont la performance d’entraînement est supérieure à la médiane et
celle de test est inférieure à la médiane. On écrit :
overfitted = tebad & ~trbad
Exercice
De la même facon, définir underfitted
un vecteur de booléens qui indique les classificateurs dont les performances
d’entrainement et de test sont inférieures à la médiane
# VOTRE CODE ICI
raise NotImplementedError()
Exercice
Implémenter en vous inspirant du code ci-dessus la fonction analyse_model_experiments
qui:
prend en entrée les résultats de plusieurs classificateurs sous la forme d’un tableau
identifie par deux vecteurs de booléens les classificateurs dont les performances de test, respectivement d’entrainement, sont inférieures à la performance de test, resp d’entrainement médiane.
identifie dans des vecteurs de booléens les classificateurs qui ont sur-appris dans
overfitted
et respectivement sous-appris dansunderfitted
ajoute deux colonnes au tableau puis le renvoit
show_source(analyze_model_experiments)
Puis on applique la fonction à compar_results
analyze_model_experiments(compar_results)
Exercice
Quelles sont les classificateurs qui ont sous-appris resp. sur-appris?
VOTRE RÉPONSE ICI
Comité de classificateurs et incertitude#
Un comité de classificateurs est un classificateur dans lequel les réponses de plusieurs classificateurs sont combinées en une seule réponse. En d’autres termes, les classificateurs votent pour les prédictions émises.
On pourrait considérer notre liste de classificateur model_list
comme un comité de classificateurs, entraînés sur les mêmes données
d’entraînement, et faisant des prédictions sur les mêmes données de
test. Regardez le code suivant; il est en réalité simple: on définit
les méthodes fit
, predict
, predict_proba
et score
comme
faisant la synthèse des résultats des classificateurs contenus dans
self.model_list
.
class ClassifierCommittee():
def __init__(self, model_list):
self.model_list = model_list
def fit(self,X,y):
for model in self.model_list:
model.fit(X,y)
def predict(self,X):
predictions = []
for model in self.model_list:
predictions.append(model.predict(X))
predictions = np.mean(np.array(predictions),axis = 0)
results = []
for v in predictions:
if v < 0:
results.append(-1)
else:
results.append(1)
return np.array(results)
def predict_proba(self,X):
predictions = []
for model in self.model_list:
predictions.append(model.predict_proba(X))
return np.swapaxes(np.array(predictions), 0, 1)
def score(self,X,y):
scores = []
for model in self.model_list:
scores.append(model.score(X,y))
return np.swapaxes(np.array(scores), 0, 1)
On définit un comité à l’aide de ce code:
commitee = ClassifierCommittee(model_list)
Exercice
Quelles seraient les performances d’un tel classificateur? Calculer les performances de commitee
# VOTRE CODE ICI
raise NotImplementedError()
La performance de l’ensemble de classificateurs n’est pas forcément meilleure que les classificateurs pris individuellement. Cependant, l’accord ou le désaccord des classificateurs sur les prédictions peut nous donner des informations sur l’incertitude des données.
En effet, chaque classificateur peut émettre des probabilités sur les
classes avec la méthode predict_proba
. Pour quantifier
l’incertitude d’une image, on peut distinguer deux cas de figure :
l’incertitude aléatorique : les classificateurs sont plus ou moins certains de leur prédiction.
l’incertitude épistémique : les classificateurs sont d’accord ou pas entre eux.
Ces incertitudes vont être d’autant plus fortes pour les images proches des frontières de décision (les bananes rouges par exemple). Intéressons nous aux images avec une faible incertitude épistémique (les classificateurs sont d’accord) et une grande incertitude aléatorique (les classificateurs sont incertains de la prédiction). Cela correspond à des images se situant aux abords de la frontière de décision entre nos deux catégories.
Incertitude aléatorique#
Nous allons utiliser l”entropie de Shannon, qui est une mesure en théorie de l’information permettant d’estimer la quantité d’information contenu dans une source d’information (ici notre probabilité sur les classes):
où \(x_i\) est la probabilité de classification sur la classe \(i\).
On récupère alors les prédictions de nos images pour les 11 classificateurs :
X = df_features.iloc[:, :-1].to_numpy()
Y = df_features.iloc[:, -1].to_numpy()
commitee.fit(X, Y)
prediction_probabilities = commitee.predict_proba(X)
prediction_probabilities.shape
La dimension de notre matrice de prédiction est donc: (nombre d'images, nombre de classificateur, nombre de classess)
. Appliquons
l’entropie pour chaque prédiction de chaque classificateur :
from scipy.stats import entropy
entropies_per_classifier = entropy(np.swapaxes(prediction_probabilities, 0, 2))
On moyenne les entropies d’une image entre les classificateurs :
entropies = np.mean(entropies_per_classifier, axis = 0)
Puis on ajoute ces valeurs dans une table que l’on trie par ordre décroissant d’entropie. Les images avec l’entropie la plus grande sont les plus incertaines et donc les plus informatives pour le modèle :
df_uncertainty = pd.DataFrame({"images" : images,
"aleatoric" : entropies})
df_uncertainty = df_uncertainty.sort_values(by=['aleatoric'],ascending=False)
df_uncertainty.style.background_gradient(cmap='RdYlGn_r')
Affichons les 10 images les plus incertaines pour le comité de classificateurs, selon cette mesure aléatorique de l’incertitude. Ces images nous donne une idée de l’ambiguité intrinsèque de notre base de données.
uncertain_aleatoric_images = df_uncertainty['images'].tolist()
image_grid(uncertain_aleatoric_images[:10])
# VOTRE CODE ICI
raise NotImplementedError()
Exercice
Ces résultats vous semblent-ils surprenants? Expliquer.
VOTRE RÉPONSE ICI
Incertitude épistémique#
Pour l’incertitude épistémique, on va simplement moyenner les écarts types entre entre les classificateurs. Comme on n’a que deux classes l’écart type entre les classificateurs pour la première classe est la même que pour la seconde:
# std entre les classificateurs
epistemic_uncertainty = np.std(prediction_probabilities[:,:,0], axis = 1)
print(epistemic_uncertainty.shape)
print(epistemic_uncertainty)
Exercice
Ajouter cette mesure au tableau df_uncertainty
.
# VOTRE CODE ICI
raise NotImplementedError()
df_uncertainty = df_uncertainty.sort_values(by=['epistemic'],ascending=False)
df_uncertainty.style.background_gradient(cmap='RdYlGn_r')
df_uncertainty[["aleatoric",'epistemic']].corr()
Exercice
Les valeurs d’incertitude aléatorique (entropie) et épistémiques (std) ne semblent pas très corrélées. Afficher les 10 images les plus incertaines selon la mesure d’incertitude épistémique :
# VOTRE CODE ICI
raise NotImplementedError()
Exercice
Afficher les 10 images les plus certaines selon la mesure d’incertitude épistémique :
# VOTRE CODE ICI
raise NotImplementedError()
Exercice
Ces résultats vous semblent-ils surprenants? Expliquer.
VOTRE RÉPONSE ICI
Pour aller plus loin ♣: apprentissage profond#
En classe, vous avez vu ce qu’est un réseau de neurones artificiel et l”apprentissage profond. En résumé, il s’agit de modèles (classificateurs par exemple) dont l’architecture est articulée en «couches» composées de différentes transformations non linéaires: des couches de neurones, des convolutions ou d’autres transformations.
Ces réseaux profonds comptent beaucoup de paramètres (neurones) à optimiser. Ils nécessitent des infrastructures spéciales pour les entraîner: de puissantes cartes graphiques (GPU), comme celles utilisées pour les jeux vidéos, qui permettent de paralléliser les calculs. Nous ne disposons pas de telles infrastructures sur le service JupyterHub de l’université.
Nous allons donc utiliser l”apprentissage par transfert (Transfer Learning) pour quand même faire du deep dans ce cours.
MobileNet et transfert#
La suite de cette feuille utilise les bibliothèques tensorflow
et
keras
. Celles-ci sont installées sur JupyterHub, mais pas forcément
ailleurs (elles sont assez volumineuses).
Nous allons utiliser un réseau pré-entraîné appelé MobileNet
, dont
l’architecture a été optimisé pour fonctionner sur les téléphones
portables. Il a été pré-entraîné pour reconnaître mille catégories
différentes (dont des animaux, des objets de la vie courante etc.). Nous n’avons plus qu’à l’entraîner un peu plus pour notre tâche spécifique.
Depuis la librairie keras
, importons le modèle pré-entraîné sur la
base de donnée imagenet
:
import tensorflow as tf
tf.config.run_functions_eagerly(True)
mobilenet = tf.keras.applications.MobileNet(include_top=False, weights='imagenet', input_shape = (32,32,3))
Observons l’architecture en couches de ce réseau de neurones; chaque ligne ci-dessous décrit brièvement l’une des couches successives :
mobilenet.summary()
Exercice
Combien ce réseau a-t-il de paramètres? Parmi ces paramètres, combien sont entraînables?
VOTRE RÉPONSE ICI
En déclarant le réseau avec le paramètre include_top=False
, on a
retiré la dernière couche de neurone. Le transfert d’apprentissage
va pouvoir être fait en ajoutant des nouvelles couches de neurones
entraînables à la fin de ce réseau pré-entraîné (dont les paramètres
vont être gelés). Ainsi, on pourra entraîner seulement les dernières
couches de notre réseau sans avoir à réentraîner tout le réseau, ce
qui nécessiterait autrement une puissante carte graphique. En d’autres
termes, le réseau mobilenet
va nous donner des attributs abstraits
sur lesquels nous allons réentraîner un nouveau réseau de neurones,
plus petit.
On commence par geler les poids du réseau mobilenet
:
for layer in mobilenet.layers:
layer.trainable=False
Vérifiez que tous les paramètres de mobilenet
sont à présent
non-entraînables :
mobilenet.summary()
On va à présent ajouter de nouvelles couches entraînables de neurones :
from tensorflow import keras
from keras.models import Model
from keras.layers import Dense,GlobalAveragePooling2D
myneuralnet = keras.Sequential(
[
mobilenet,
GlobalAveragePooling2D(),
Dense(64, activation="relu", name="layer1"),
Dense(64, activation="relu", name="layer2"),
Dense(2, activation="softmax",name="layer3"),
]
)
Configurons le réseau mobilenet
pour prendre en entrée des images de
taille (32,32,3)
:
myneuralnet.build((None,32,32,3))
myneuralnet.summary()
Exercice
À présent combien ce réseau a-t-il de paramètres? Parmi ces paramètres, combien sont entraînables?
# VOTRE CODE ICI
raise NotImplementedError()
Chargeons nos données recentrées et recadrées de taille 32x32 :
df_clean = pd.read_csv('clean_data.csv', index_col=0) # chargement du fichier dans le notebook
df_clean
On souhaite avoir une variable X
de type np.ndarray
de taille
(nimages, 32, 32, 3)
c’est à dire (nombre d'images, largeur, hauteur, nombre de couches de couleurs)
:
Exercice
Définissez le nombre d’images que vous avez dans nimages.
# VOTRE CODE ICI
raise NotImplementedError()
X = np.array(df_clean.drop(['class'], axis=1)).reshape((nimages, 32, 32, 3))
Il est nécessaire d’encoder les étiquettes sous la forme d’un one-hot
vector c’est-à-dire d’un tableau y_onehot
de taille (nombre d'images, nombre de classes)
tel que y_onehot[i,j]==1
si la i-ème
étiquette est la j-ème classe et 0
sinon. Dans notre cas, la i-ème
ligne vaut (1,0)
si le i-ème fruit est une pomme, et (0,1)
si
c’est une banane :
y = np.array(df_clean["class"])
classes = np.unique(y)
class_to_class_number = { cls: number for number, cls in enumerate(classes) }
y_onehot = np.zeros((len(y), len(np.unique(y))))
for i, label in enumerate(y):
y_onehot[i, class_to_class_number[label]] = 1
On doit maintenant définir l”optimisateur et la fonction de coût qui vont permettre d’optimiser les paramètres de nos neurones. Les poids des neurones vont être optimisés pour minimiser la fonction de coût (loss) :
opt = keras.optimizers.Adam(learning_rate=0.01)
myneuralnet.compile(loss='categorical_crossentropy', metrics=["accuracy"], optimizer=opt)
Avant de pouvoir entraîner notre réseau, on va d’abord diviser nos données en deux: les données d’entraînement et les données de test :
test = np.array(y == 1, dtype = int)
X_train = np.concatenate((X[:165], X[333:333+ 79]), axis = 0)
y_train = np.concatenate((y_onehot[:165], y_onehot[333:333+ 79]), axis = 0)
X_test = np.concatenate((X[165:333], X[333+ 79:]), axis = 0)
y_test = np.concatenate((y_onehot[165:333], y_onehot[333+ 79:]), axis = 0)
On peut finalement entraîner notre réseau de neurone !
Attention : Le temps de calcul étant potentiellement élevé, la cellule ci-dessous est désactivée par défaut. Mettez en commentaire la première ligne pour lancer le calcul.
%%script echo cellule désactivée
from sklearn.model_selection import StratifiedKFold
import pdb
stop = pdb.set_trace
def create_model():
myneuralnet = keras.Sequential(
[
mobilenet,
GlobalAveragePooling2D(),
Dense(64, activation="relu", name="layer1"),
Dense(64, activation="relu", name="layer2"),
Dense(2, activation="softmax",name="layer3"),
])
myneuralnet.build((None,32,32,3))
opt = keras.optimizers.Adam(learning_rate=0.01)
myneuralnet.compile(loss='categorical_crossentropy', metrics=["accuracy"], optimizer=opt)
return myneuralnet
def train_evaluate(model, x_train, y_train, x_test, y_test):
history = model.fit(x_train, y_train, epochs = 10, shuffle = True)
return model.evaluate(x_test, y_test)
kFold = StratifiedKFold(n_splits=10)
scores = np.zeros(10)
idx = 0
for train, test in kFold.split(X, y):
model = create_model()
scores[idx] = train_evaluate(model, X[train], y_onehot[train], X[test], y_onehot[test])[1]
idx += 1
print(scores)
print(scores.mean())
Afficher les valeurs d’accuracy au fur et à mesure des itérations d’optimisations :
%%script echo cellule désactivée
plt.plot(scores)
plt.xlabel("Nombre d'itération d'optimisations")
plt.ylabel("Accuracy")
Conclusion#
Cette feuille a fait un tour d’horizon d’outils de classification à votre disposition. Prenez ici quelques notes sur ce que vous avez appris, observé, interprété.
VOTRE RÉPONSE ICI
Faites maintenant votre propre analyse et préparez votre diaporama!