Skip to content
Snippets Groups Projects
Commit 84ad806a authored by SUR Frederic's avatar SUR Frederic
Browse files

2023-2024

parent 80b32ca4
Branches
No related tags found
No related merge requests found
%% Cell type:markdown id: tags:
# Introduction à l'apprentissage automatique: TP3 - Exercice 1
<br>
## Détection de spam
<br>
Dans ce TP, nous allons entraîner des classifieurs pour décider si un mail est un spam ou non.
<br>
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.
```python
# (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)
```
%% Cell type:markdown id: tags:
## 1. Préliminaires
<br>
Commençons par charger les bibliothèques utiles au TD.
%% Cell type:code id: tags:
``` python
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
# on ignore les avertissements "future warning"
from warnings import simplefilter
simplefilter(action='ignore', category=FutureWarning)
# pour ignorer les avertissements "future warning"
#from warnings import simplefilter
#simplefilter(action='ignore', category=FutureWarning)
```
%% Cell type:markdown id: tags:
Ensuite, on charge les données: récupérez au préalable le fichier `spambase.data` disponible sur le [UCI Machine Learning Repository](http://archive.ics.uci.edu/ml/datasets/Spambase) (allez dans "Data folder"). La description complète de la base est dans le fichier `spambase.name`, à ouvrir avec un éditeur de texte.
Ensuite, on charge les données: récupérez au préalable le fichier `spambase.data` disponible sur le [UCI Machine Learning Repository](http://archive.ics.uci.edu/ml/datasets/Spambase) (cliquez sur "Download"). La description complète de la base est dans le fichier `spambase.name`, à ouvrir avec un éditeur de texte.
<br>
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=1`, la valeur est arbitraire) de manière à ce que l'on ait tous les mêmes résultats afin de faciliter la comparaison.
%% Cell type:code id: tags:
``` python
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=1)
# pour vérifier que les données sont bien chargées:
print("dataset:")
print(data)
print("\nTOTAL - nombre d'observations, nombre de caractéristiques:")
print(data.shape)
print("\nAPPRENTISSAGE - nombre d'observations, nombre de caractéristiques:")
print(X_train.shape)
print("\nAPPRENTISSAGE - nombre de labels associés aux obervations:")
print(y_train.shape)
print("\nTEST - nombre d'observations, nombre de caractéristiques:")
print(X_test.shape)
print("\nTEST - nombre de labels associés aux obervations:")
print(y_test.shape)
print("\nobservations, base d'apprentissage:")
print(X_train)
print("\nlabels associés, base d'apprentissage:")
print(y_train)
```
%% Cell type:markdown id: tags:
__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?
%% Cell type:markdown id: tags:
<font color=red>
Votre réponse:
</font>
%% Cell type:markdown id: tags:
## 2. Classification aux plus proches voisins
<br>
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?
%% Cell type:code id: tags:
``` python
# votre code ici pour 1-ppv:
```
%% Cell type:code id: tags:
``` python
# votre code ici pour 5-ppv:
```
%% Cell type:markdown id: tags:
<font color=red>
Votre réponse:
</font>
%% Cell type:markdown id: tags:
__Question 3__. Pourquoi la métrique utilisée n'est-elle pas adaptée aux observations ?
%% Cell type:markdown id: tags:
<font color=red>
Votre réponse:
</font>
%% Cell type:markdown id: tags:
__Question 4__. Pré-traitez les données par standardisation, comme expliqué ici sur [la documentation scikit-learn](https://scikit-learn.org/stable/modules/preprocessing.html) (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.
__Question 4__. Pré-traitez les données par standardisation, comme expliqué ici sur [la documentation scikit-learn](https://scikit-learn.org/stable/modules/preprocessing.html) (utilisez `StandardScaler` 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.
%% Cell type:code id: tags:
``` python
# votre code:
```
%% Cell type:markdown id: tags:
<font color=red>
Votre réponse:
</font>
%% Cell type:markdown id: tags:
## 3. Classifieur naïf de Bayes gaussien et classifieur de la régression logistique
<br>
__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](https://scikit-learn.org/stable/modules/naive_bayes.html) où vous retrouverez le contenu du cours, puis la syntaxe de `GaussianNB` [ici](https://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.GaussianNB.html)), ainsi que le classifieur de la régression logistique ([documentation](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html)).
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)`
%% Cell type:code id: tags:
``` python
# votre code pour le classifieur naïf de Bayes gaussien (sur les données originales):
```
%% Cell type:code id: tags:
``` python
# votre code pour la régression logistique (sur les données standardisées):
```
%% Cell type:markdown id: tags:
<font color=red>
Votre réponse:
</font>
%% Cell type:markdown id: tags:
## 4. Analyse des résultats
<br>
On dispose des matrices de confusion, décrites [ici](https://scikit-learn.org/stable/modules/generated/sklearn.metrics.confusion_matrix.html), et des rapports de classification, décrits [](https://scikit-learn.orgz/stable/modules/generated/sklearn.metrics.classification_report.html).
__Question 6__. Affichez ces matrices et rapports pour les quatre classifieurs testés.
%% Cell type:code id: tags:
``` python
# votre code ici:
```
%% Cell type:markdown id: tags:
__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?
%% Cell type:markdown id: tags:
<font color=red>
Réponse:
</font>
%% Cell type:markdown id: tags:
## 5. Toutes les erreurs ne se valent pas...
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.
%% Cell type:code id: tags:
``` python
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))
```
%% Cell type:code id: tags:
``` python
# faites varier le seuil de détection p:
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("\nClassification de la régression logistique pour un seuil de probabilité p=%.3f" %p)
print('score sur la base de test: %.3f' %score_LRb)
print(metrics.classification_report(y_test,y_pred_LRb))
print("matrice de confusion:")
print(metrics.confusion_matrix(y_test,y_pred_LRb))
```
%% Cell type:markdown id: tags:
__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?
%% Cell type:markdown id: tags:
<font color=red>
Réponse:
</font>
%% Cell type:code id: tags:
``` python
```
......
%% Cell type:markdown id: tags:
# Introduction à l'apprentissage automatique: TP3 - Exercice 2
<br>
### Reconnaissance de chiffres manuscrits
<br>
La cellule ci-dessous permet de charger 10000 images de chiffres manuscrits extraits de la célèbre base de données MNIST décrite __[ici](https://www.openml.org/d/554)__. La base originale contient 70000 images de taille 28x28 pixels, mais on restreint le nombre d'images afin de garder des temps de calcul raisonnables dans ce TD. On utilise 9000 observations comme base d'apprentissage, et on réserve 1000 observations comme base de test.
Les 28x28=784 caractéristiques sont les niveaux de gris en chaque pixel. Les caractéristiques seront normalisées à des valeurs entre 0 et 1.
%% Cell type:code id: tags:
``` python
from sklearn import datasets, neighbors, linear_model, metrics
%matplotlib inline
# dataset natif sklearn (ce n'est pas MNIST): (lignes suivantes à "décommenter" pour utiliser ce jeu de données)
# size_images=(8,8)
# digits = datasets.load_digits()
# X_digits = digits.data
# y_digits = digits.target
# Mnist database: (il faut quelques dizaines de secondes pour charger la base)
# les données sont décrites ici: https://www.openml.org/d/554
size_images=(28,28)
X_digits, y_digits = datasets.fetch_openml('mnist_784', version=1, return_X_y=True, as_frame=False)
X_digits, y_digits = datasets.fetch_openml('mnist_784', version=1, return_X_y=True, as_frame=False, parser='auto')
X_digits=X_digits[:10000,:]/255. # on normalise le niveau de gris 8 bits entre 0 et 1
y_digits=y_digits[:10000]
n_samples = len(X_digits)
print("nombre total d'observations (apprentissage + test): %d" % n_samples)
n_features = len(X_digits[0])
print("nombre de caractéristiques par observation: %d" % n_features)
X_train = X_digits[: 9000]
y_train = y_digits[: 9000]
X_test = X_digits[9000 :]
y_test = y_digits[9000 :]
print("nombre d'observations dans la base d'apprentissage: %d" %len(X_train))
print("nombre d'observations dans la base de test: %d" %len(X_test))
```
%% Cell type:markdown id: tags:
La cellule suivante définit une fonction qui permet d'afficher les 150 premières images de la base de test, ainsi que la classe véritable et la classe déterminée par l'algorithme de classification. Nous nous servirons de cette fonction plus tard.
La cellule suivante définit une fonction qui permet d'afficher les 150 premières images de la base de test, ainsi que la classe véritable et la classe déterminée par l'algorithme de classification (passées en argument de la fonction). Nous nous servirons de cette fonction plus tard.
%% Cell type:code id: tags:
``` python
import numpy as np
import matplotlib.pyplot as plt
def affichage_150_images(X_test,y_test,y_pred):
plt.figure(figsize=[15,12])
for n in range(150):
plt.subplot(10,15,n+1,xticks=[],yticks=[])
plt.imshow(np.reshape(X_test[n,:],size_images),cmap='gray_r')
if y_pred[n]==y_test[n]:
plt.text(0.1,0.1,str(y_pred[n])+' / '+str(y_test[n]),fontsize=6,bbox=dict(facecolor='white', alpha=1))
else:
plt.text(0.1,0.1,str(y_pred[n])+' / '+str(y_test[n]),fontsize=6,bbox=dict(facecolor='red', alpha=1))
plt.suptitle('classe predite / classe réelle')
plt.show();
```
%% Cell type:markdown id: tags:
La cellule suivante effectue une classification au plus proche voisin (`n_neighbors=1`) de la base des chiffres manuscrits extraite de MNIST, et affiche le résultat de la classification de 150 images.
__Question 1__. A quoi correspondent les temps de calcul affichés? Les erreurs vous semblent-elles qualitativement explicables? Notez le score de précision (proportion d'observations correctement classées).
%% Cell type:code id: tags:
``` python
# classification au plus proche voisin et affichage
knn = neighbors.KNeighborsClassifier(n_neighbors=1)
%time knn.fit(X_train, y_train)
%time y_pred_nn = knn.predict(X_test)
print('KNN score: %f' % metrics.accuracy_score(y_test, y_pred_nn))
affichage_150_images(X_test,y_test,y_pred_nn)
```
%% Cell type:markdown id: tags:
<font color=red>
Réponse:
</font>
%% Cell type:markdown id: tags:
__Question 2__. Quelles sont les informations fournies par `classification_report` et `confusion_matrix` du module `metrics` ?
%% Cell type:code id: tags:
``` python
# votre code (voir l'exercice 1):
```
%% Cell type:markdown id: tags:
<font color=red>
Réponse:
</font>
%% Cell type:markdown id: tags:
__Question 3__. Comparez aux résultats obtenus par
- la classification naïve bayésienne gaussienne décrite dans la [documentation scikit-learn](http://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.GaussianNB.html)
- à la régression logistique, décrite dans la [documentation scikit-learn](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html)
Comparez également les temps de calcul.
%% Cell type:code id: tags:
``` python
from sklearn import naive_bayes
# votre code pour le classifieur naif Gaussien:
```
%% Cell type:code id: tags:
``` python
from sklearn import linear_model
# votre code pour la régression logistique:
```
%% Cell type:markdown id: tags:
<font color=red>
Réponse:
</font>
%% Cell type:markdown id: tags:
__Remarque__: les méthodes modernes arrivent à des précisions supérieures à 99% (sur la base MNIST entière)
Voir les "error rates" sur: http://yann.lecun.com/exdb/mnist/
%% Cell type:code id: tags:
``` python
```
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment