Introduction à l'apprentissage automatique: TP3 - Exercice 1


Détection de spam


Dans ce TP, nous allons entraîner des classifieurs pour décider si un mail est un spam ou non.


Tout d'abord, quelques indications sur l'utilisation des méthodes d'apprentissage de ̀scikit-learn.

Les méthodes d'apprentissage supervisé de scikit-learn permettent de définir un objet, doté de différents attributs et méthodes, dont cross_val_score (pour calculer un score de validation croisée), fit (pour procéder à l'apprentissage), predict (pour prédire les classes des éléments d'une base de test), ou score pour calculer la proportion d'observations bien classées dans la base de test, sur laquelle on peut comparer la classe prédite à la "vraie" classe.

Ci-dessous, un exemple d'utilisation de la classification au plus proche voisin, dans un scénario où on suppose disposer d'une base d'apprentissage $(X_{train},y_{train})$, et d'une base de test $X_{test}$ pour laquelle on connaît $y_{test}$, de manière à valider l'apprentissage sur la base de test. Si on veut changer de classifieur, il suffit d'utiliser un autre constructeur que neighbors.KNeighborsClassifier et de passer les paramètres adéquats.

# (le code suivant ne peut pas être exécuté "tel quel"...)

# classifieur au plus proche voisin (on peut changer le paramètre n_neighbors):
knn = neighbors.KNeighborsClassifier(n_neighbors=1)  

# calcul d'un score moyen de validation croisée "à 5 plis" sur (X_train,y_train)
scores = cross_val_score(knn,X_train,y_train,cv=5)
print("score moyen de validation croisée: %0.3f (+/- %0.3f)" % (scores.mean(),2*scores.std()))

# la prédiction d'une nouvelle observation consistera à chercher le p.p.v. dans X_train, 
# et à associer la classe de ce p.p.v., donnée par y_train:
knn.fit(X_train,y_train)  
# Remarque: il n'y a pas d'apprentissage à proprement parler pour les p.p.v., 
# il s'agit juste de préciser la base dans laquelle seront cherchés les plus proches voisins

# on stocke dans y_pred les classes prédites sur un ensemble de test X_test:
y_pred = knn.predict(X_test)  

# calcul d'un score lorsqu'on connaît les vraies classes des observations de X_test: 
# (proportion d'observations pour lesquelles y_test==y_pred)
score = knn.score(X_test,y_test)

Préliminaires


Commençons par charger les bibliothèques utiles au TD.

In [ ]:
import numpy as np
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn import neighbors, linear_model, naive_bayes, metrics
%matplotlib inline 

Ensuite, on charge les données: récupérez au préalable le fichier spambase.data disponible sur le UCI Machine Learning Repository (allez dans "Data folder"). La description complète de la base est dans le fichier spambase.name, à ouvrir avec un éditeur de texte.


La cellule suivante charge les données. On forme une base d'entraînement avec 80% des données (choix aléatoire), et on garde 20% des données pour faire une base de test. Dans la cellule suivante, on fixe la graîne du générateur aléatoire (random_state=42, la valeur est arbitraire) de manière à ce que l'on ait tous les mêmes résultats afin de faciliter la comparaison.

In [ ]:
data = np.loadtxt('spambase.data', delimiter=',')
X_train, X_test, y_train, y_test = train_test_split(data[:,:-1], data[:,-1], test_size=0.2, random_state=42)
# pour vérifier que les données sont bien chargées:
print(data)  
print(data.shape)
print(X_train.shape)
print(y_train.shape)
print(X_test.shape)
print(y_test.shape)
print(X_train)
print(y_train)

Question 1. A partir de la description de la base de données, justifiez la manière employée pour charger les données en X (observations) et y (labels). Quelles sont les caractéristiques des observations, les labels, et quel est le rapport avec le problème initial?

Votre réponse:

</font>

Classification aux plus proches voisins


Mettez en oeuvre les classifications au plus proche voisin et aux 5 plus proches voisins. Vous calculerez le score moyen de validation croisée à 5 plis sur la base d'apprentissage ainsi que le score obtenu sur la base de test. Vous vous inspirerez du code détaillé en introduction.

Question 2. Quelle est la métrique utilisée pour déterminer les plus proches voisins? Quel est ce "score" calculé exactement? Quel lien entre score de validation croisée et score sur la base de test?

In [ ]:
# votre code ici pour 1-ppv:
In [ ]:
# votre code ici pour 5-ppv:

Votre réponse:

</font>

Question 3. Pourquoi la métrique utilisée n'est-elle pas adaptée aux observations ?

Votre réponse:

</font>

Question 4. Pré-traitez les données par standardisation, comme expliqué ici sur la documentation scikit-learn (utilisez le StandardScaler comme indiqué dans la 3ème cellule, de manière à utiliser la même normalisation sur la base d'apprentissage et sur la base de test, c'est important), puis recalculez les scores des deux classifieurs précédents.

In [ ]:
# votre code:

Votre réponse:

</font>

Classifieur naïf de Bayes gaussien et régression logistique


Question 5. Pourquoi le classifieur naïf de Bayes gaussien ne nécessite-t-il pas de standardisation préalable des données ? (vous pouvez vérifier que la normalisation joue tout de même un faible rôle: elle a sans doute une influence sur le comportement de l'algorithme d'estimation des paramètres).

Mettez en oeuvre le classifieur naïf de Bayes gaussien (lisez le début de la documentation où vous retrouverez le contenu du cours, puis la syntaxe de GaussianNB ici), ainsi que le classifieur de la régression logistique (documentation).

Pour ce dernier classifieur, passez l'option max_iter=2000 si vous avez un avertissement concernant la convergence de l'optimisation, de la manière suivante: LR = linear_model.LogisticRegression(max_iter=2000)

In [ ]:
# votre code pour le classifieur naïf de Bayes gaussien (sur les données originales):
In [ ]:
# votre code pour la régression logistique (sur les données standardisées):

Votre réponse:

</font>

Analyse des résultats


On dispose des matrices de confusion, décrites ici, et des rapports de classification, décrits .

Question 6. Affichez ces matrices et rapports pour les quatre classifieurs testés.

In [ ]:
# votre code ici:

Question 7. Ici, par quoi pourraient s'expliquer les performances modestes du classifieur naïf de Bayes ? A ce stade, quel classifieur préfére-t-on et pourquoi? Dans une application de détection de spams, cherche-t-on réellement à minimiser le taux d'erreur global?

Réponse:

</font>

Le classifieur bayésien naïf gaussien et le classifieur de la régression logistique s'appuient tous deux sur la règle du maximum a posteriori. Ils permettent d'estimer la probabilité a posteriori $p(C_1|x)$ et détectent un spam lorsque $p(C_1|x)>1/2$, où $C_1$ désigne la classe "spam" et $x$ est une observation. Les deux classifieurs mettent en oeuvre le classifieur de Bayes, qui minimise le risque moyen de prédiction (le taux d'erreur). Le taux d'erreur "compte" de la même manière les erreurs sur les deux classes.

Si on préfère réduire le taux de faux positif de la méthode (proportion de mails détectés à tort comme "spam"), on peut relever le seuil de probabilité.

Les classifieurs LogisticRegression et GaussianNB possèdent tous deux une méthode predict_proba qui, pour un tableau d'observations, fournit la probabilité a posteriori de chaque classe, comme l'affiche la cellule suivante. On remarque que pour chaque observation $x$, $p(C_0|x)+p(C_1|x)=1$. (attention, la documentation n'est pas très claire, predict_proba fournit bien la probabilité a posteriori, et pas la vraisemblance $p(x|C_k)$)

Remarquons qu'aucune probabilité n'est fournie par la classification aux plus proches voisins.

In [ ]:
print("probabilités a posteriori pour GNB:")
print(GNB.predict_proba(X_test))
print("\nprobabilités a posteriori pour LR:")
print(LR.predict_proba(X_test_standard))
In [ ]:
p=0.5  # constatez que p=0.5 fournit les mêmes résultats pour y_pred_lr et y_pred_LRb
y_pred_LRb = (LR.predict_proba(X_test_standard)[:,1] >= p).astype(int)
#print(y_pred_LRb)   # pour visualiser les classes prédites
#print(y_pred_lr)
score_LRb = 1-np.mean(np.abs(y_test-y_pred_LRb))  # calcul du taux de reconnaissance

print("\nRégression logistique à seuil:")
print('score sur la base de test: %2f' %score_LRb)
print(metrics.classification_report(y_test,y_pred_LRb))
print("matrice de confusion:")
print(metrics.confusion_matrix(y_test,y_pred_LRb))

Question 8. Quelle valeur du seuil de probabilité $p$ faut-il choisir pour assurer un rappel de la classe "non spam" d'au moins 0.98? Que penser de cet algorithme de détection de spam?

Réponse:

</font>

In [ ]: