Ce notebook contient les éléments d'introduction au machine learning
%matplotlib inline
import numpy as np # charge un package pour le numérique
import matplotlib.pyplot as plt # charge un package pour les graphiques
On charge le jeu de données digits disponoible dans le package scikit-learn (nom d'import sklearn). Ce jeu de données contient des images de chiffres numérisés. On va s'en servir au cours de cette séance pour étudier les principaux enjeu en classification (supervisée).
# Chargement des données disponible dans le package sklearn
from sklearn.datasets import load_digits
digits = load_digits()
X, y = digits.data, digits.target
print("Nombre de pixels : {}".format(X.shape[1]))
print("Nombre d'observations : {}".format(X.shape[0]))
print("Nombre de classes : {}".format(len(np.unique(y))))
# Choix d'une observation quelconques de la base
idx_to_test = 15
print("Affichage d'une ligne de la matrice / image:")
print(X[idx_to_test, :])
print("Affichage de la classe / chiffre associé:")
print(y[idx_to_test])
Faites varier le choix de l'indice. Sans afficher la classe arrivez-vous à reconnaitre le chiffre représenté?
Les images scannées sont de taille 8 x 8 et comportent donc 64 pixels chacune. Elles sont stockées sous la forme de vecteurs ligne, qu'il faut remettre dans un ordre lisible pour les identifiés. L'affichage graphique est proposé avec les commandes qui suivent.
# Utilisation de la fonction imshow pour l'affichage de l'image numéro idx_to_test:
imgplot = plt.imshow(np.reshape(X[idx_to_test, :], (8, 8)))
# Amélioration de la visualisation (niveau de gris) et de la légende:
imgplot = plt.imshow(np.reshape(X[idx_to_test, :], (8, 8)),
cmap='gray', aspect='equal', interpolation='nearest')
# Attention aux accents: ne pas oublier le u (Unicode) ci-dessous
plt.title(u'Le chiffre numéro %s est un %s' % (idx_to_test, y[idx_to_test]))
Pour mieux comprendre la base de données on va s'intéresser à quelques statistiques. On commence par calculer les moyennes et variances par classes pour chacun des chiffres. La moyenne par classe se visualise comme une image qui est une représentantion moyenne pour chaque chiffre de zéro à neuf. Idem pour la variance, ce qui permet alors de voir les parties avec les plus grandes variations entre les membres d'une même classe.
# Récupérer les modalités possible prises (Il y en a bien 10!)
classes_list = np.unique(y).astype(int)
print "Liste des classes en présence: ", classes_list
for
calculer le représentant moyen pour chaque chiffreimgplot = plt.imshow(np.reshape(np.mean(X[y == 0, :], axis=0), (8, 8)),
cmap='gray', aspect='equal', interpolation='nearest')
# Calculer un représentant moyen pour chaque chiffre
Xi_mean = [np.mean(X[y == cls], axis=0) for cls in classes_list]
# Fonction d'affichage d'une liste d'image
def disp_pics(pic_list, title=''):
"""" Fonction qui affiche une liste d'image codée en vecteur """""
fig, axs = plt.subplots(nrows=2, ncols=5, figsize=(12, 4))
plt.suptitle(title, fontsize=16)
for i in range(10):
opt = dict(cmap='gray', aspect='equal', interpolation='nearest')
axs.flat[i].imshow(pic_list[i].reshape(8, 8), **opt)
axs.flat[i].set_title("Chiffre: %s" % i)
# Contre-balancer l'affichage pas terrible de matplotlib
plt.tight_layout()
plt.subplots_adjust(top=0.85)
# Affichage des images moyennes par classe pour les données
disp_pics(Xi_mean, title=(u"Moyennes sur l'ensemble des données"))
# Calculer de la variance par chiffre
Xi_var = [np.var(X[y == cls], axis=0) for cls in classes_list]
# Affichage des images de variance par classe pour les données
disp_pics(Xi_var, title=(u"Variances sur l'ensemble des donneés"))
On procède de manière classique en réservant 80% des données pour la partie apprentissage, et 20% pour l'évaluation des classifieurs que l'on a construit sur la première partie. En effet il n'est pas raisonnable de tester la perfromance sur 100% des données. Cela donnera lieux à du sur-apprentissage (en: overfitting). La généralisation des méthodes apprises serait alors très mauvaise
from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.8,
random_state=0)
print("Nb d'échantillons d'apprentissage : {}".format(X_train.shape[0]))
print("Nb d'échantillons de validation : {}".format(X_test.shape[0]))
from sklearn.lda import LDA
clf = LDA() # choix de la méthode LDA comme premier classifieur
clf.fit(X_train, y_train) # calibration de la méthode sur nos données
y_pred = clf.predict(X_test) # prédiction de la méthode sur la partie à tester
# Chargement d'une mesure standard de performance
from sklearn.metrics import accuracy_score
# accuracy : pourcentage de bonnes predictions
print "Accuracy : ", accuracy_score(y_test, y_pred)
print "Accuracy bis: : ", np.mean(y_test == y_pred) # mesure d'erreur 0/1
print("Le classifieur propose une bonne prédiction dans %s %% des cas."
% (100 * accuracy_score(y_test, y_pred)))
y_rand = np.random.randint(10, size=y_test.shape)
accuracy_score(y_rand, y_pred)
# Choix d'un chiffre:
chf = 8
print "Chiffre choisi: %s " % chf
# Chargement de deux autres mesure de performance
from sklearn.metrics import precision_score, recall_score
# Calcul de la précision:
precision_value = precision_score(y_test, y_pred,average=None)
print "Precision : ", precision_value[chf]
print "Precision bis : ", np.mean(np.logical_and(y_pred == chf, y_test == chf))
print("Le classifieur prédit le chiffre %s "
" avec raison dans %s %% de ses prédictions.\n"
"Autrement dit, dans %s %% de ses prédictions le classifieur prédit %s"
" alors que le vrai chiffre est différent.\n" %
(chf, 100 * precision_value[chf], 100 * (1 - precision_value[chf]), chf))
# Calcul du rappel:
recall_value = recall_score(y_test, y_pred,average=None) # en Anglais rappel = recall
print "Rappel : ", recall_value[chf]
print "Rappel bis : ", np.mean(np.logical_and(y_pred == chf, y_test == chf))
print("Le classifieur prédit le chiffre %s avec raison dans %s"
"%% des cas où le vrai chiffre est un %s.\n"
"En revanche %s %% des chiffres qui sont vraiment des %s"
" sont prédit à tord par un autre chiffre." %
(chf, 100 * recall_value[chf], chf, 100 * (1 - recall_value[chf]), chf))
Afficher la précision et le rappel pour tous les valeurs de chiffre de 0 à 9 en utilisant une boucle "for" pour le classifieur que l'on vient de considérer.
from sklearn.metrics import classification_report, f1_score
print classification_report(y_test, y_pred)
Vérifier que la précision et le rappel moyen donnés dans le dernier tableau sont la moyenne de la précision et du rappel obtenus pour chaque chiffre.
Les courbes d'apprentissage permettent de quantifier le gain de performance obtenu en augmentant la taille des données. Cela permet de répondre à des questions comme:
N_range = np.linspace(15, X_train.shape[0], 20).astype(int)
clf = LDA()
clf_name = 'LDA'# choix de la méthode LDA comme premier classifieur
def plot_learning_curve(clf, clf_name):
training_error = []
test_error = []
for N in N_range:
XN = X_train[:N]
yN = y_train[:N]
clf.fit(XN, yN)
training_error.append(accuracy_score(clf.predict(XN), yN))
test_error.append(accuracy_score(clf.predict(X_test), y_test))
plt.figure()
plt.plot(N_range, training_error, label='training')
plt.plot(N_range, test_error, label='test')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
plt.title(u'Courbe d\'apprentissage pour la méthode ' + clf_name)
plt.xlabel('Nombre de points d\'entrainement')
plt.ylabel('Pourcentage d\'erreurs')
plot_learning_curve(clf,clf_name)
# Chargement d'une autre méthode de classification (KNN)
from sklearn.neighbors import KNeighborsClassifier
# Liste des classifieurs évalués
classifiers = [('LDA', LDA()),
('KNN_k=1', KNeighborsClassifier(n_neighbors=1))]
Pour chaque classifieur on évalue la performance par le score sur les données de test et le temps d'éxecution
from sklearn.metrics import confusion_matrix
from sklearn.grid_search import GridSearchCV
import pandas as pd # charge un package pour le traitement des données
from timeit import timeit # charge un package pour des mesures de temps
# Definition des métriques de performance
def perf_compute(clf, name, loops=10):
"""
Calcule le temps d'apprentissage, de prediction, le score
et la matrice de confusion d'un classifieur
"""
# On initialise le conteneur
perf = pd.Series(name=name)
# On crée les callables qu'on passera à la fonction de profiling
fit = lambda: clf.fit(X_train, y_train)
score = lambda: clf.score(X_test, y_test)
# On profile le temps des phases d'entrainement et de prédiction en ms
perf['train_tps'] = timeit(fit, number=loops) / loops * 1000
perf['test_tps'] = timeit(score, number=loops) / loops * 1000
perf['total_tps'] = perf.train_tps + perf.test_tps
# On calcule le score en pourcentage
perf['score'] = fit().score(X_test, y_test) * 100
# On calcule la matrice de confusion
perf['conf_mat'] = [confusion_matrix(fit().predict(X_test), y_test)]
# Normalisation par ligne de la matrice de confusion pour avoir des pourcentages d'erreurs.
# cm_normalized = cm.astype('float') / cm.sum(axis=1)[:, np.newaxis]
return perf
# On lance le calcule de performance. On profile en bouclant 100 fois
perfs = pd.DataFrame([perf_compute(clf, name) for name, clf in classifiers])
perfs = perfs.sort('score')
perfs['train_tps test_tps total_tps score'.split()].T
# Barplot des performances
plt.style.use('ggplot') # sortie graphique améliorées (tester sans!)
fig, axs = plt.subplots(ncols=2, figsize=(12, 4))
perfs['score'].plot(kind='barh',
title='Pourcentage d\'erreurs commises (en %)',
ax=axs[0])
perfs[['train_tps', 'test_tps', 'total_tps']].plot(kind='barh',
title='Temps (en ms)',
ax=axs[1])
import seaborn as sns
def plot_conf_mat(perf, ax, title='Model'):
"""
Affichage de la matrice de confusion
"""
sns.heatmap(perf.conf_mat[0], ax=ax, square=True, annot=True)
ax.set_title('{}: {}\nScore: {:.2f}'.format(title, perf.name, perf.score))
ax.set_xlabel('True label')
ax.set_ylabel('Predicted label')
# Affichage du plus mauvais et du meilleur classifieur
# Les classifieurs sont classés par scores croissant
fig, axs = plt.subplots(ncols=2, figsize=(12, 4))
plot_conf_mat(perfs.iloc[0], ax=axs[0], title='Pire model')
plot_conf_mat(perfs.iloc[-1], ax=axs[1], title='Meilleur model')
# courbe d'apprentissage pour KNN
plot_learning_curve(classifiers[1][1], classifiers[1][0])
from sklearn.naive_bayes import GaussianNB
from sklearn.svm import SVC
estimator = GaussianNB()
estimator = SVC(gamma=0.001)
Les résultats varient en fonction du découpage apprentissage/validation initial. Obtenez-vous des les même résultat :
en changeant la graine de l'aléa ('random_state=10')?
en changeant le ratio apprentissage/validation?
Quelle valeur de k dans le k-NN donne la meilleure performance? Faire varier k entre 1 et 10 et afficher un graphique de performance en fonction de k.
Il y a-t-il un compromis entre la performance de prédiction et le temps de calcul?