Le but de cet exercice est de classifier les textes d'un corpus de textes sur différents sujets, de manière à regrouper les textes portant sur le même sujet. La méthode utilisée (statistiques sur un sac de mots) admet des variantes plus efficaces que ce que l'on utilise dans ce TP; ces variantes font toujours l'objet de recherches.
Nous allons utiliser un jeu de données de scikit-learn: des textes provenant de newsgroups (les ancètres des forums) consacrés à un sujet.
L'exercice est inspiré de la documentation scikit-learn
On commence par charger les bibliothèques utiles:
# "magic function" Jupyter pour l'affichage des graphiques dans le carnet:
%matplotlib inline
import time
from sklearn.datasets import fetch_20newsgroups
from sklearn.cluster import KMeans, MiniBatchKMeans
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn import metrics
from sklearn import cluster
from scipy.cluster.hierarchy import linkage, fcluster
import numpy as np
import matplotlib.pyplot as plt
Ensuite on charge des textes venant de quatre groupes de discussion (categories) provenant du dataset 20 newsgroups:
categories = [
'misc.forsale',
'rec.sport.baseball',
'comp.graphics',
'sci.space',
]
print("Loading 20newsgroups dataset for categories:")
print(categories)
dataset = fetch_20newsgroups(subset='all', categories=categories)
print("%d documents" % len(dataset.data))
print("%d categories" % len(dataset.target_names))
print()
On voit à l'aide des cellules suivantes que l'objet dataset a en particulier des attributs target (un entier représentant la catégorie du texte, ici entre 0 et 3 car on a extrait 4 catégories), target_names (les noms des catégories en anglais, dans l'ordre des entiers de target), et data (le texte des messages):
print(dir(dataset)) # dir permet de lister les attributs et méthodes d'un objet
print("contenu de l'attribut target: %a" % dataset.target)
print("contenu de l'attribut target_names: %a" % dataset.target_names)
print()
print()
no_doc=10 # essayez avec d'autres textes parmi les 3929 chargés
print("Document no: %d" % no_doc)
print("numéro de catégorie: %d" % dataset.target[no_doc])
print("ce qui correspond au sujet: %s" % dataset.target_names[dataset.target[no_doc]])
print()
print("Le texte est:")
print()
print(dataset.data[no_doc]) # vérifiez que le texte a l'air cohérent avec le sujet
L'objectif est de parvenir à classifier les textes du corpus par sujet, en supposant bien entendu que l'on ne connaisse pas le newsgroup d'appartenance. On va donc chercher une classification non-supervisée sur les textes stockés dans dataset.data, l'information dans dataset.target nous servira uniquement pour vérifier à la fin le résultat de notre classification.
Les algorithmes de clustering traitent des points dans un espace (vectoriel) muni d'une distance. La première étape est donc de transformer chaque texte en un vecteur.
Une approche standard dans le domaine de la fouille de textes est la transformation TF-IDF, décrite sur wikipedia (lisez le début de la page).
Il y a deux étapes:
Scikit-learn propose une fonction permettant de calculer la représentation TF-IDF des textes du corpus (plus de détails ici, complément d'information). Dans la cellule suivante, on associe à chaque texte du corpus un vecteur TF-IDF. Pour former le "sac de mots" (bag of words) au préalable, on ne tient pas compte des mots trop courants (max_df: les mots présents dans plus de 50% des documents sont éliminés) ou trop rares (min_df: les mots présents dans moins de deux documents dans le corpus sont éliminés), ainsi que des mots trop courants en anglais (stop_words, contenus dans une liste pré-définie). De manière à limiter les temps de calcul (et éviter la malédiction de la dimensionalité, comme on le verra plus tard), on construit des vecteurs de dimension $N$ en ne gardant comme vocabulaire que les $N$ mots les plus fréquents du corpus (on n'utilise pas min_df).
N=1000
vectorizer = TfidfVectorizer(max_features=N,max_df=0.5,stop_words='english')
vectors = vectorizer.fit_transform(dataset.data)
print("nombre de documents représentés %d" %vectors.shape[0])
print("nombre de mots dans le vocabulaire %d" %vectors.shape[1])
A ce stade, vectors[no_doc] est un vecteur TFIDF de dimension $N$ représentant le document d'indice $no{doc}$.
Le vocabulaire extrait est le suivant (essayez aussi sans stop_words='english' dans la cellule précédente, et différentes valeurs de $N$):
print(vectorizer.vocabulary_)
A titre illustratif, le code suivant représente le vecteur associé à quatre documents: en ordonnée on voit la statistique TF-IDF, en abscisse le numéro du mot dans le "sac de mots". (pas besoin de comprendre la syntaxe)
# l'affichage peut prendre un peu de temps
plt.figure();
no_doc=10
plt.bar(np.arange(N),np.asarray(vectors[no_doc].todense())[0],width=10);
plt.title("sujet:"+dataset.target_names[dataset.target[no_doc]]);
plt.figure();
no_doc=100
plt.bar(np.arange(N),np.asarray(vectors[no_doc].todense())[0],width=10);
plt.title("sujet:"+dataset.target_names[dataset.target[no_doc]]);
plt.figure();
no_doc=50
plt.bar(np.arange(N),np.asarray(vectors[no_doc].todense())[0],width=10);
plt.title("sujet:"+dataset.target_names[dataset.target[no_doc]]);
plt.figure();
no_doc=1000
plt.bar(np.arange(N),np.asarray(vectors[no_doc].todense())[0],width=10);
plt.title("sujet:"+dataset.target_names[dataset.target[no_doc]]);
Travail à faire: utilisez les algorithmes de classification hiérarchique (single-linkage, Ward) et k-means pour classifier les vecteurs TF-IDF (donc les textes) en quatre classes. Les labels (valeurs entre 0 et 3) calculés pour chaque vecteur TF-IDF correspondent au numéro de la classe identifiée.
On utilisera AgglomerativeClustering avec les options linkage='single' et linkage='ward' (cf documentation et MiniBatchKmeans (cf documentation).
# votre code:
Observez les différences de temps de calcul (obtenus en faisant la différence entre les valeurs retournées par time.time()).
Nous cherchons à présent à valider les classifications obtenues. La difficulté dans une classification non-supervisée est que les labels calculés sont arbitraires: le label 0 d'une classification n'a pas de raison de correspondre au "vrai" label 0. Commencez par afficher les labels attribués par chaque méthode aux 30 premiers textes:
# votre code:
On dispose des matrices de confusion (à lire)
Que peut-on dire des résultats suivants?
from sklearn.metrics import confusion_matrix
print("matrice de confusion pour 'single-linkage':")
print(confusion_matrix(dataset.target,labels_single))
plt.figure()
plt.imshow(confusion_matrix(dataset.target,labels_single)) # représentation visuelle
plt.colorbar();
print()
print("matrice de confusion pour 'Ward':")
print(confusion_matrix(dataset.target,labels_ward))
plt.figure()
plt.imshow(confusion_matrix(dataset.target,labels_ward)) # représentation visuelle
plt.colorbar();
print()
print("matrice de confusion pour 'KMeans':")
print(confusion_matrix(dataset.target,labels_KM))
plt.figure()
plt.imshow(confusion_matrix(dataset.target,labels_KM)) # représentation visuelle
plt.colorbar();
Testez l'algorithme avec une valeur grande de $N$ (taille du sac de mots, et aussi dimensionnalité de la représentation TF-IDF de chaque texte), et avec une valeur faible. Qu'en conclure?
Essayez également d'identifier plus de quatre sujets. Représentez le elbow plot pour les K-means.
Etendez vos résultats à l'ensemble du jeu de données 20newsgroups.