Section courante

A propos

Section administrative du site

Travailler avec des données textuelles

L'objectif de cette page est d'explorer certains des principaux outils scikit-learn sur une seule tâche pratique : analyser une collection de documents texte (messages de groupes de discussion) sur vingt sujets différents.

Dans cette page, nous verrons comment :

Configuration du didacticiel

Pour commencer avec ce didacticiel, vous devez d'abord avoir installé scikit-learn et toutes ses dépendances requises.

La source de ce didacticiel se trouve dans le dossier scikit-learn :

scikit-learn/doc/tutorial/text_analytics/

Le dossier du tutoriel doit contenir les dossiers suivants :

Vous pouvez déjà copier les squelettes dans un nouveau dossier quelque part sur votre disque dur nommé sklearn_tut_workspace où vous éditerez vos propres fichiers pour les exercices tout en gardant les squelettes d'origine intacts :

cp -r skeletons work_directory/sklearn_tut_workspace

Les algorithmes de l'apprentissage automatique ont besoin de données. Accédez à chaque sous-dossier $TUTORIAL_HOME/data et exécutez le script fetch_data.py à partir de là (après les avoir lus au préalable).

Par exemple :

cd $TUTORIAL_HOME/data/languages
less fetch_data.py
python fetch_data.py

Chargement de l'ensemble de données 20 groupes de discussion

L'ensemble de données s'appelle «Twenty Newsgroups». Voici la description officielle traduit en français, citée sur le site Web :

«L'ensemble de données 20 Newsgroups est une collection d'environ 20 000 documents de newsgroups, répartis (presque) uniformément sur 20 newsgroups différents. À notre connaissance, il a été initialement collecté par Ken Lang, probablement pour son article « Newsweeder : Apprendre à filtrer les nouvelles du net », bien qu'il ne mentionne pas explicitement cette collection. La collection 20 Newsgroups est devenue un ensemble de données populaire pour les expériences d'applications textuelles de techniques d'apprentissage automatique, telles que la classification de texte et le regroupement de texte.»

Dans ce qui suit, on utilise le chargeur de données intégré pour 20 groupes de discussion de scikit-learn. Alternativement, il est possible de télécharger l'ensemble de données manuellement à partir du site Web et d'utiliser la fonction sklearn.datasets.load_files en le pointant vers le sous-dossier 20news-bydate-train du dossier d'archives non compressé.

Afin d'obtenir des temps d'exécution plus rapides pour ce premier exemple, nous allons travailler sur un ensemble de données partiel avec seulement 4 catégories sur les 20 disponibles dans l'ensemble de données :

>>> categories = ['alt.atheism', 'soc.religion.christian',
...               'comp.graphics', 'sci.med']

Nous pouvons maintenant charger la liste des fichiers correspondant à ces catégories comme suit :

>>> from sklearn.datasets import fetch_20newsgroups
>>> twenty_train = fetch_20newsgroups(subset='train',
...     categories=categories, shuffle=True, random_state=42)

L'ensemble de données renvoyé est un «bundle» scikit-learn : un simple objet holder avec des champs accessibles à la fois sous forme de clefs de dictionnaire Python ou d'attributs d'objet pour plus de commodité, par exemple target_names contient la liste des noms de catégories demandés :

>>> twenty_train.target_names
['alt.atheism', 'comp.graphics', 'sci.med', 'soc.religion.christian']

Les fichiers eux-mêmes sont chargés en mémoire dans l'attribut data. Pour référence, les noms de fichiers sont également disponibles :

>>> len(twenty_train.data)
2257
>>> len(twenty_train.filenames)
2257

Affichons les premières lignes du premier fichier chargé :

>>> print("\n".join(twenty_train.data[0].split("\n")[:3]))
From: sd345@city.ac.uk (Michael Collier)
Subject: Converting images to HP LaserJet III?
Nntp-Posting-Host: hampton

>>> print(twenty_train.target_names[twenty_train.target[0]])
comp.graphics

Les algorithmes d'apprentissage supervisé nécessiteront une étiquette de catégorie pour chaque document de l'ensemble d'apprentissage. Dans ce cas, la catégorie est le nom du groupe de discussion qui se trouve également être le nom du dossier contenant les documents individuels.

Pour des raisons de rapidité et d'efficacité de l'espace, scikit-learn charge l'attribut cible sous forme de tableau d'entiers correspondant à l'index du nom de catégorie dans la liste target_names. L'identifiant entier de catégorie de chaque échantillon est entreposé dans l'attribut cible :

>>> twenty_train.target[:10]
array([1, 1, 3, 3, 3, 3, 3, 2, 2, 2])

Il est possible de récupérer les noms des catégories comme suit :

>>> for t in twenty_train.target[:10]:
...     print(twenty_train.target_names[t])
...
comp.graphics
comp.graphics
soc.religion.christian
soc.religion.christian
soc.religion.christian
soc.religion.christian
soc.religion.christian
sci.med
sci.med
sci.med

Vous pouvez remarquer que les échantillons ont été mélangés de manière aléatoire (avec une graine RNG fixe) : cela est utile si vous sélectionnez uniquement les premiers échantillons pour entraîner rapidement un modèle et obtenir une première idée des résultats avant de réentraîner ultérieurement sur l'ensemble de données complet.

Extraction de fonctionnalités à partir de fichiers texte

Afin d'effectuer l'apprentissage automatique sur des documents texte, nous devons d'abord transformer le contenu du texte en vecteurs de caractéristiques numériques.

Sacs de mots

La manière la plus intuitive de procéder est la représentation par sacs de mots :

La représentation des sacs de mots implique que n_features est le nombre de mots distincts dans le corpus : ce nombre est généralement supérieur à 100 000.

Si n_samples == 10000, l'entreposage de X sous forme de tableau numpy de type float32 nécessiterait 10000 x 100000 x 4 octets = 4 Go de RAM, ce qui est à peine gérable sur les ordinateurs d'aujourd'hui.

Heureusement, la plupart des valeurs de X seront des zéros, car pour un document donné, moins de quelques milliers de mots distincts seront utilisés. Pour cette raison, nous disons que les sacs de mots sont généralement des ensembles de données éparses de grande dimension. Nous pouvons économiser beaucoup de mémoire en entreposant uniquement les parties non nulles des vecteurs de caractéristiques en mémoire.

Les matrices scipy.sparse sont des structures de données étant exactement cela, et scikit-learn dispose d'un support intégré pour ces structures.

Jeton de texte avec scikit-learn

Le prétraitement du texte, les jetons et le filtrage des mots vides sont inclus dans une composante de haut niveau capable de créer un dictionnaire de fonctionnalités et de transformer des documents en vecteurs de fonctionnalités :

>>> from sklearn.feature_extraction.text import CountVectorizer
>>> count_vect = CountVectorizer()
>>> X_train_counts = count_vect.fit_transform(twenty_train.data)
>>> X_train_counts.shape
(2257, 35788)

CountVectorizer prend en charge le comptage de N-grammes de mots ou de caractères consécutifs. Une fois ajusté, le vectoriseur a construit un dictionnaire d'indices de caractéristiques :

>>> count_vect.vocabulary_.get(u'algorithm')
4690

La valeur indicielle d'un mot dans le vocabulaire est liée à sa fréquence dans l'ensemble du corpus d'apprentissage.

Des occurrences aux fréquences

Le décompte des occurrences est un bon début, mais il y a un problème : les documents plus longs auront des valeurs de comptage moyennes plus élevées que les documents plus courts, même s'ils peuvent traiter des mêmes sujets.

Pour éviter ces écarts potentiels, il suffit de diviser le nombre d'occurrences de chaque mot dans un document par le nombre total de mots dans le document : ces nouvelles fonctionnalités sont appelées tf (Term Frequency).

Une autre amélioration de tf consiste à réduire les poids des mots apparaissant dans de nombreux documents du corpus et étant donc moins informatifs que ceux n'apparaissant que dans une plus petite partie du corpus.

Cette réduction d'échelle est appelée tf-idf (Term Frequency times Inverse Document Frequency).

Les deux fonctions tf et tf-idf peuvent être calculées comme suit :

>>> from sklearn.feature_extraction.text import TfidfTransformer
>>> tf_transformer = TfidfTransformer(use_idf=False).fit(X_train_counts)
>>> X_train_tf = tf_transformer.transform(X_train_counts)
>>> X_train_tf.shape
(2257, 35788)

Dans l'exemple de code ci-dessus, nous utilisons d'abord la méthode fit(..) pour ajuster notre estimateur aux données et ensuite la méthode transform(..) pour transformer notre matrice de comptage en une représentation tf-idf. Ces deux étapes peuvent être combinées pour obtenir le même résultat final plus rapidement en évitant le traitement redondant. Cela se fait en utilisant la méthode fit_transform(..) comme indiqué ci-dessous et comme mentionné dans la note de la section précédente :

>>> tfidf_transformer = TfidfTransformer()
>>> X_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)
>>> X_train_tfidf.shape
(2257, 35788)

Entraîner un classificateur

Maintenant que nous avons nos fonctionnalités, nous pouvons entraîner un classificateur pour essayer de prédire la catégorie d'un article. Commençons par un classificateur bayésien naïf, fournissant une bonne base pour cette tâche. scikit-learn comprend plusieurs variantes de ce classificateur ; la plus adaptée au nombre de mots est la variante multinomiale :

>>> from sklearn.naive_bayes import MultinomialNB
>>> clf = MultinomialNB().fit(X_train_tfidf, twenty_train.target)

Pour essayer de prédire le résultat sur un nouveau document, nous devons extraire les caractéristiques en utilisant presque la même chaîne d'extraction de caractéristiques que précédemment. La différence est que nous appelons transform au lieu de fit_transform sur les transformateurs, car ils ont déjà été ajustés à l'ensemble d'apprentissage :

>>> docs_new = ['God is love', 'OpenGL on the GPU is fast']
>>> X_new_counts = count_vect.transform(docs_new)
>>> X_new_tfidf = tfidf_transformer.transform(X_new_counts)

>>> predicted = clf.predict(X_new_tfidf)

>>> for doc, category in zip(docs_new, predicted):
...     print('%r => %s' % (doc, twenty_train.target_names[category]))
...
'God is love' => soc.religion.christian
'OpenGL on the GPU is fast' => comp.graphics

Création d'un pipeline

Afin de faciliter l'utilisation du vectoriseur => transformateur => classificateur, scikit-learn fournit une classe Pipeline se comportant comme un classificateur composé :

>>> from sklearn.pipeline import Pipeline
>>> text_clf = Pipeline([('vect', CountVectorizer()),
...                      ('tfidf', TfidfTransformer()),
...                      ('clf', MultinomialNB()),
... ])

Les noms vect, tfidf et clf (classifier) ??sont arbitraires. Nous pouvons maintenant entraîner le modèle avec une seule commande :

>>> text_clf = text_clf.fit(twenty_train.data, twenty_train.target)

Évaluation des performances sur l'ensemble de test

L'évaluation de la précision prédictive du modèle est tout aussi simple :

>>> import numpy as np
>>> twenty_test = fetch_20newsgroups(subset='test',
...     categories=categories, shuffle=True, random_state=42)
>>> docs_test = twenty_test.data
>>> predicted = text_clf.predict(docs_test)
>>> np.mean(predicted == twenty_test.target)
0.834...

Autrement dit, nous avons atteint une précision de 83,4 %. Voyons si nous pouvons faire mieux avec une machine à vecteurs de support linéaire (SVM), étant largement considérée comme l'un des meilleurs algorithmes de classification de texte (bien qu'elle soit également un peu plus lente que Bayes naïf). Nous pouvons changer l'apprenant en connectant simplement un objet classificateur différent à notre pipeline :

>>> from sklearn.linear_model import SGDClassifier
>>> text_clf = Pipeline([('vect', CountVectorizer()),
...                      ('tfidf', TfidfTransformer()),
...                      ('clf', SGDClassifier(loss='hinge', penalty='l2',
...                                            alpha=1e-3, n_iter=5)),
... ])
>>> _ = text_clf.fit(twenty_train.data, twenty_train.target)
>>> predicted = text_clf.predict(docs_test)
>>> np.mean(predicted == twenty_test.target)
0.912...

scikit-learn fournit également des utilitaires pour une analyse plus détaillée des performances des résultats :

>>> from sklearn import metrics
>>> print(metrics.classification_report(twenty_test.target, predicted,
...     target_names=twenty_test.target_names))
...                                         
                        precision    recall  f1-score   support

           alt.atheism       0.94      0.82      0.87       319
         comp.graphics       0.88      0.98      0.92       389
               sci.med       0.95      0.89      0.92       396
soc.religion.christian       0.90      0.95      0.92       398

           avg / total       0.92      0.91      0.91      1502


>>> metrics.confusion_matrix(twenty_test.target, predicted)
array([[261,  10,  12,  36],
       [  5, 380,   2,   2],
       [  7,  32, 353,   4],
       [  6,  11,   4, 377]])

Comme prévu, la matrice de confusion montre que les messages des groupes de discussion sur l'athéisme et le christianisme sont plus souvent confondus les uns avec les autres qu'avec les graphiques informatiques.

Réglage des paramètres à l'aide de la recherche par grille

Nous avons déjà rencontré certains paramètres tels que use_idf dans TfidfTransformer. Les classificateurs ont également tendance à avoir de nombreux paramètres; par exemple, MultinomialNB inclut un paramètre de lissage alpha et SGDClassifier a un paramètre de pénalité alpha et des termes de perte et de pénalité configurables dans la fonction objective.

Au lieu de modifier les paramètres des différentes composantes de la chaîne, il est possible d'exécuter une recherche exhaustive des meilleurs paramètres sur une grille de valeurs possibles. Nous essayons tous les classificateurs sur des mots ou des bigrammes, avec ou sans idf, et avec un paramètre de pénalité de 0,01 ou 0,001 pour le SVM linéaire :

>>> from sklearn.grid_search import GridSearchCV
>>> parameters = {'vect__ngram_range': [(1, 1), (1, 2)],
...               'tfidf__use_idf': (True, False),
...               'clf__alpha': (1e-2, 1e-3),
... }

De toute évidence, une recherche aussi exhaustive peut être coûteuse. Si nous disposons de plusieurs cours de processeur, nous pouvons indiquer au moteur de recherche de grille d'essayer ces huit combinaisons de paramètres en parallèle avec le paramètre n_jobs. Si nous donnons à ce paramètre une valeur de -1, la recherche de grille détectera le nombre de cours installés et les utilisera tous :

>>> gs_clf = GridSearchCV(text_clf, parameters, n_jobs=-1)

L'instance de recherche de grille se comporte comme un modèle scikit-learn normal. Exécutons la recherche sur un sous-ensemble plus petit des données d'entraînement pour accélérer le calcul :

>>> gs_clf = gs_clf.fit(twenty_train.data[:400], twenty_train.target[:400])

Le résultat de l'appel de fit sur un objet GridSearchCV est un classificateur que nous pouvons utiliser pour prédire :

>>> twenty_train.target_names[gs_clf.predict(['God is love'])]
'soc.religion.christian'

mais sinon, c'est un objet assez gros et peu pratique. Nous pouvons cependant obtenir les paramètres optimaux en inspectant l'attribut grid_scores_ de l'objet, étant une liste de paires paramètres/score. Pour obtenir les meilleurs attributs de notation, nous pouvons faire :

>>> best_parameters, score, _ = max(gs_clf.grid_scores_, key=lambda x: x[1])
>>> for param_name in sorted(parameters.keys()):
... print("%s: %r" % (param_name, best_parameters[param_name]))
...
clf__alpha: 0.001
tfidf__use_idf: True
vect__ngram_range: (1, 1)

>>> score
0.902...

Exercices

Pour faire les exercices, copiez le contenu du dossier «skeletons» dans un nouveau dossier nommé «workspace» :

cp -r skeletons workspace

Vous pouvez ensuite modifier le contenu de l'espace de travail sans craindre de perdre les instructions d'exercice originales.

Lancez ensuite un interpréteur de commande ipython et exécutez le script en cours avec :

[1] %run workspace/exercise_XX_script.py arg1 arg2 arg3

Si une exception est déclenchée, utilisez %debug pour lancer une session ipdb post-mortem.

Affinez l'implémentation et itérez jusqu'à ce que l'exercice soit résolu.

Pour chaque exercice, le fichier squelette fournit toutes les instructions d'importation nécessaires, le code standard pour charger les données et un exemple de code pour évaluer la précision prédictive du modèle.

Exercice 1 : Identification de la langue

Ligne de commande ipython :

%run workspace/exercise_01_language_train_model.py data/languages/paragraphs/

Exercice 2 : Analyse des sentiments sur les critiques de films

Ligne de commande ipython :

%run workspace/exercise_02_sentiment.py data/movie_reviews/txt_sentoken/

Exercice 3 : Utilitaire de classification de texte en ligne de commande

En utilisant les résultats des exercices précédents et le module cPickle de la bibliothèque standard, écrivez un utilitaire en ligne de commande détectant la langue d'un texte fourni sur stdin et estime la polarité (positive ou négative) si le texte est écrit en anglais.

Point bonus si l'utilitaire est capable de donner un niveau de confiance pour ses prédictions.



Dernière mise à jour : Mardi, le 5 novembre 2024