---
# System prepended metadata

title: idl_tp-final `scikit-learn` - classification de documents

---

---
jupyter:
  jupytext:
    formats: ipynb,md
    split_at_heading: true
    text_representation:
      extension: .md
      format_name: markdown
      format_version: '1.3'
      jupytext_version: 1.15.2
  kernelspec:
    display_name: Python 3 (ipykernel)
    language: python
    name: python3
---

<!-- LTeX: language=fr -->

<!-- #region slideshow={"slide_type": "slide"} -->
# idl_tp-final `scikit-learn` - classification de documents


Idée : attribuer la bonne catégorie à un document (*classification*)

Les catégorisations sont variées : Quels sont les exemples que nous avons déjà vus ?  les applications que vous connaissez ?

## Classification supervisée / non supervisée  

Exemple : quel est le thème principal de cette phrase ?
``
    I brought some muffins to church, I baked them myself.
``

- cooking ?
- religion ?
- architecture ?

### Méthode non-supervisée 

La méthode : 
1. on regarde les vecteurs des mots qui composent le document, et on calcule le ==centroïde== de ce vecteur. 
2. on calcule la distance entre ce centroïde et les vecteurs des classes elles-mêmes pour trouver ==le plus proche voisin==
3. on choisit la classe qui a la plus faible distance

![image](https://hackmd.io/_uploads/Hkn8c8Sx0.png)



### Méthode supervisée : demande beaucoup de données labélisées (TP ci-dessous) 

On cherche ici à trouver une fonction qui à partir de données d'entraînements **et de leur étiquette**, peut deviner l'étiquette (la classe) d'un nouveau document.



<!-- #endregion -->

---
 ************************** Début du TP *************************

---
## Scikit-learn  ?

[Documentation scikit-learn](https://scikit-learn.org/stable/index.html).

Scikit-learn est une bibliothèque Python dédiée à l'apprentissage artificiel qui repose sur [NumPy](https://numpy.org/) et [SciPy](https://scipy.org/). Il est écrit en Python et [Cython](https://cython.org/). Il s'interface très bien avec [matplotlib](https://matplotlib.org), [seaborn](https://seaborn.pydata.org/) ou [pandas](https://pandas.pydata.org/) (qui lui-même marche très bien avec [plotnine](https://plotnine.readthedocs.io/)). C'est devenu un incontournable du *machine learning* et des *data sciences* en Python.

Dans ce notebook on se limitera à la classification, une partie seulement de ce qu'offre `scikit-learn`.

La classification est souvent utilisée en TAL, par exemple dans les tâches d'analyse de sentiment, de détection d'émotion ou l'identification de la langue.

On va faire de l'apprentissage *supervisé* de classifieurs : l'idée est d'apprendre un modèle à partir de données réparties en classes (une classe et une seule pour chaque exemple), puis de se servir de ce modèle pour répartir parmi les mêmes classes des données nouvelles.

Dit autrement, on a un échantillon d'entraînement $\mathcal{D}$, composé de $n$ couples $(X_{i},Y_{i}), i=1, …, n$ où les $X_{i}$ sont les entrées (en général des **vecteurs** de traits ou *features*) et les $y_{i}$ seront les sorties, les classes à prédire. On cherche alors dans une famille $\mathbb{M}$ de modèles un modèle de classification $M$ qui soit le plus performant possible sur $\mathcal{D}$.

`scikit-learn` offre beaucoup d'algorithmes d'apprentissage. Vous en trouverez un aperçu sur [cette carte](https://scikit-learn.org/stable/tutorial/machine_learning_map/index.html) et sur ces listes : [supervisé](https://scikit-learn.org/stable/supervised_learning.html) / [nonsupervisé](https://scikit-learn.org/stable/unsupervised_learning.html).

Mais `scikit-learn` offre également les outils pour mener à bien les étapes d'une tâche de d'apprentissage :

- Manipuler les données, constituer un jeu de données d'entraînement et de test
- Entraînement du modèle
- Évaluation
- Optimisation des hyperparamètres

```bash
pip install -U scikit-learn
```

## Un premier exemple

### Les données

Comme vous le savez, c'est la clé de voute du *machine learning*. Nous allons travailler avec un des jeux de données fourni par scikit-learn : [le jeu de données de reconnaissance des vins](https://scikit-learn.org/stable/datasets/toy_dataset.html#wine-recognition-dataset)

C'est plus facile pour commencer parce que les données sont déjà nettoyées et organisées, mais vous pourrez bien sûr par la suite [charger des données venant d'autres sources](https://scikit-learn.org/stable/datasets/loading_other_datasets.html).

```python
from sklearn import datasets
wine = datasets.load_wine()
type(wine)
```

(La recommandation des développeurs et développeuses de `scikit-learn` est d'importer uniquement les parties qui nous intéresse plutôt que tout le package. Notez aussi le nom `sklearn` pour l'import.)

Ces jeux de données sont des objets `sklearn.utils._bunch.Bunch`. Organisés un peu comme des dictionnaires Python, ces objets contiennent :

- `data` : array NumPy à deux dimensions d'échantillons de données de dimensions `(n_samples, n_features)` = les inputs = les X
- `target` : les variables à prédire = les catégories des échantillons = les outputs = les Y
- `feature_names`
- `target_names`

Et d'autres trucs comme

```python3
print(wine.DESCR)
```

```python3
print(wine.feature_names)
```

```python3
print(wine.target_names)
```

Après avoir installé `pandas` ou `polars` :

```bash
pip install -U pandas polars
```

On peut convertir ces données en `DataFrame` pandas si on veut.

```python3
import pandas as pd

df = pd.DataFrame(data=wine.data,columns=wine.feature_names)
df["target"] = wine.target
df.head()
```

```python3
import polars as pl

df = pl.DataFrame(
    data=wine.data, schema=wine.feature_names).with_columns(
    target=pl.Series(wine.target)
)
df.head()
```

Mais l'essentiel est de retrouver nos inputs $X$ et outputs $y$ nécessaires à l'apprentissage.

```python
X_wine, y_wine = wine.data, wine.target
```

```python
X_wine.shape
```

```python
y_wine
```

Vous pouvez séparer les données en train et test facilement à l'aide de `sklearn.model_selection.train_test_split` (voir la [doc](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.train_test_split.html#sklearn.model_selection.train_test_split))

```python
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X_wine, y_wine, test_size=0.3)
y_train
```

```python
import matplotlib.pyplot as plt

plt.hist(y_train, align="right", label="train") 
plt.hist(y_test, align="left", label="test")
plt.legend()
plt.xlabel("Classe")
plt.ylabel("Nombre d'exemples")
plt.title("Répartition des classes") 
plt.show()
```

Il ne faut pas hésiter à recourir à des représentations graphiques quand vous manipulez les données.
Ici on voit que la répartition des classes à prédire n'est pas homogène pour les données de test. On peut y remédier en utilisant le paramètre `stratify`, qui fait appel à [`StratifiedShuffleSplit`](https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.StratifiedShuffleSplit.html) pour préserver la même répartition des classes dans le train et dans le test.

```python
X_train, X_test, y_train, y_test = train_test_split(X_wine, y_wine, test_size=0.25, stratify=y_wine)
plt.hist(y_train, align="right", label="train") 
plt.hist(y_test, align="left", label="test") 
plt.legend()
plt.xlabel("Classe")
plt.ylabel("Nombre d'exemples")
plt.title("Répartition des classes avec échantillonnage stratifié") 
plt.show()
```

## Entraînement

L'étape suivante est de choisir un algorithme (un *estimator* dans la terminologie de scikit-learn), de l'entraîner sur nos données (avec la fonction `fit()`) puis de faire la prédiction (avec la fonction `predict`).

Quelque soit l'algo choisi vous allez retrouver les fonctions `fit` et `predict`. Ce qui changera ce seront les paramètres à passer au constructeur de la classe de l'algo. Votre travail portera sur le choix de ces paramètres.

Exemple un peu bateau avec une méthode de type SVM.

```python
from sklearn.svm import LinearSVC
clf = LinearSVC(dual=True)
clf.fit(X_train, y_train)
```

```python
clf.predict(X_test)
```

## Évaluation

On fait l'évaluation en confrontant les prédictions sur les `X_test` et les `y_test`. La fonction `score` nous donne l'exactitude (*accuracy*) moyenne du modèle.

```python
clf.score(X_test, y_test)
```

Pour la classification il existe une classe bien pratique  `sklearn.metrics.classification_report`

```python
from sklearn.metrics import classification_report

y_pred = clf.predict(X_test)
print(classification_report(y_test, y_pred))
```

## Exercice 


Refaites une partition train/test différente et comparez les résultats.

## Validation croisée

Pour améliorer la robustesse de l'évaluation on peut utiliser la validation croisée (*cross-validation*). 
En pratique, on divise notre dataset en 5 (`part0`, `part1`, `part2`, `part3`, `part4`)  puis on entraîne notre modèle sur les parties ``[part0->part3]`` et on évalue sur les 20% restants, c'est-à-dire `part4`, puis on fait "glisser la fenêtre de sélection", en entraînant le modèle sur les parties `[part1->part4]` et on teste le résultat sur `part0`, et ainsi de suite. Dans cette configuration, on obtient donc 5 mesures. Quel est l'intérêt de cette méthode selon vous ? 

`scikit-learn` a des classes pour ça.

```python3! 
from sklearn.model_selection import cross_validate, cross_val_score
print(cross_validate(LinearSVC(), X_wine, y_wine)) # infos d'accuracy mais aussi de temps
print(cross_val_score(LinearSVC(), X_wine, y_wine)) # uniquement accuracy
```

## Optimisation des hyperparamètres

L'optimisation des hyperparamètres est la dernière étape. Ici encore `scikit-learn` nous permet de le faire de manière simple et efficace. Nous utiliserons `sklearn.model_selection.GridSearchCV` qui fait une recherche exhaustive sur tous les paramètres donnés au constructeur. Cette classe utilise aussi la validation croisée.

https://fr.wikipedia.org/wiki/Machine_%C3%A0_vecteurs_de_support

```python
from sklearn.svm import SVC
from sklearn.model_selection import GridSearchCV

param_grid =  {'C': [0.1, 0.5, 1, 10, 100, 1000], 'kernel':['linear']}
grid = GridSearchCV(LinearSVC(), param_grid, cv = 5, scoring = 'accuracy')
estimator = grid.fit(X_wine, y_wine)
print(estimator.cv_results_)
```

```python
df = pd.DataFrame(estimator.cv_results_)
df.sort_values('rank_test_score')
```

## Classification de textes

Le [dataset 20 newsgroups](https://scikit-learn.org/stable/auto_examples/text/plot_document_classification_20newsgroups.html) est un exemple de classification de textes proposé par `scikit-learn`. Il y a aussi [de la doc](https://scikit-learn.org/stable/modules/feature_extraction.html#text-feature-extraction) sur les traits (*features*) des documents textuels.

La classification avec des techniques non neuronales repose en grande partie sur les traits utilisés pour représenter les textes.

```python
from sklearn.datasets import fetch_20newsgroups

categories = [
    "sci.crypt",
    "sci.electronics",
    "sci.med",
    "sci.space",
]

data_train = fetch_20newsgroups(
    subset="train",
    categories=categories,
    shuffle=True,
)

data_test = fetch_20newsgroups(
    subset="test",
    categories=categories,
    shuffle=True,
)
```

```python
print(len(data_train.data))
print(len(data_test.data))
```

Ici on a un jeu de 2373 textes catégorisés pour l'entrainement. À nous d'en extraire les features désirées. Le modèle des [sacs de mots](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html) est le plus basique.

Attention aux valeurs par défaut des paramètres. Ici par exemple on passe tout en minuscule et la tokenisation est rudimentaire. Ça fonctionnera mal pour d'autres langues que l'anglais. Cependant, presque tout est modifiable et vous pouvez passer des fonctions de prétraitement personnalisées.

```python!
from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer(stop_words="english")
X_train = vectorizer.fit_transform(data_train.data) # données de train vectorisées
y_train = data_train.target
X_train.shape
```

Voilà la tête que ça a:

```python3
print(X_train[0, :])
```

```python3
X_test = vectorizer.transform(data_test.data)
y_test = data_test.target
```

Pour l'entraînement et l'évaluation on reprend le code vu auparavant

```python
clf = LinearSVC(C=0.5)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(classification_report(y_test, y_pred))
```

[TF⋅IDF](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html) est un raffinement de ce modèle, qui donne en général de meilleurs résultats. Il existe une librairie qui fait tous les calculs pour vous : 

```python
from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(
    sublinear_tf=True,
    max_df=0.5,
    stop_words='english'
)
X_train = vectorizer.fit_transform(data_train.data) # données de train vectorisées
y_train = data_train.target
X_train.shape

X_test = vectorizer.transform(data_test.data)
y_test = data_test.target

clf = LinearSVC(C=0.5)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(classification_report(y_test, y_pred))
```

## Projet à présenter la semaine du 28/04 

### 1. Un projet complet

L'archive [`imdb_smol.tar.gz`](https://github.com/LoicGrobol/apprentissage-artificiel/blob/main/data/imdb_smol.tar.gz) contient 602 critiques de films sous formes de fichiers textes, réparties en deux classes : positives et négatives (matérialisées par des sous-dossiers). Votre mission est de réaliser un script qui :

- Charge et vectorise ces données
- Entraîne et compare des classifieurs sur ce jeu de données

L'objectif est de déterminer quel type de vectorisation et de modèle semble le plus adapté et quels hyperparamètres choisir. Vous pouvez par exemple tester 
- (i) des SVM comme ci-dessus, 
- (ii) [un modèle de régression logistique](https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html) (recherche des paramètres pour la combinaison linéaire des traits puis passage par une fonction sigmoïde qui donne la probabilité d'appartenance à une classe donnée), 

![image](https://hackmd.io/_uploads/SySXf-LJex.png)
[source](https://medium.com/tell-ia/la-r%C3%A9gression-logistique-expliqu%C3%A9e-%C3%A0-ma-grand-m%C3%A8re-52a2ab30788)

(iii) [un arbre de décision](https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html) (séparation des échantillons en groupes similaires, permet une hiérarchisation des traits),
![image](https://hackmd.io/_uploads/S167EWU1gl.png)
[source](https://blent.ai/blog/a/arbres-de-decision-en-machine-learning)

(iv) [un modèle bayésien naïf](https://scikit-learn.org/stable/modules/generated/sklearn.naive_bayes.MultinomialNB.html) (fait l'hypothèse qu'une caractéristique qui est indépendante des autres est propre à une classe, eg si un fruit est rouge, alors c'est une pomme),


(v) [une forêt d'arbres de décision](https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html) (découpage du datasets en sous-ensembles sur lesquels est calculé un arbre de décision. L'ensemble forme une forêt, et pour un échantillon donné, chaque arbre apporte un vote pour une classe A ou B donnée. C'est la classe qui a le plus de votes qui gagne.)


### 2. D'autres traits

Essayez avec d'autres *features*  qui vous paraîtraient pertinente : La longueur moyenne des mots, le nombre ou le type d'adjectifs, la présence d'entités nommées, …

Pour récupérer ce genre de *features*, vous pouvez regarder du côté de [spaCy](http://spacy.io/) comme prétraitement de vos données.

