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 :
- Charger le contenu du fichier et les catégories
- Extraire des vecteurs de caractéristiques adaptés à l'apprentissage automatique
- Entraîner un modèle linéaire pour effectuer une catégorisation
- Utiliser une stratégie de recherche par grille pour trouver une bonne configuration des composants d'extraction de caractéristiques et du classificateur
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 :
- *.rst files - la source du document du tutoriel écrit avec sphinx
- data - dossier pour placer les ensembles de données utilisés pendant le tutoriel
- skeletons - exemples de scripts incomplets pour les exercices
- solutions - solutions des exercices
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 :
- attribuer un identifiant entier fixe à chaque mot apparaissant dans n'importe quel document de l'ensemble d'apprentissage (par exemple en créant un dictionnaire à partir de mots vers des indices entiers).
- pour chaque document #i, compter le nombre d'occurrences de chaque mot w et l'entreposer dans X[i, j] comme valeur de la caractéristique #j où j est l'index du mot w dans le dictionnaire.
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
- Écrivez un pipeline de classification de texte à l'aide d'un préprocesseur personnalisé et de CharNGramAnalyzer en utilisant les données des articles de Wikipédia comme ensemble d'entraînement.
- Évaluez les performances sur un ensemble de tests en attente.
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
- Écrivez un pipeline de classification de texte pour classer les critiques de films comme positives ou négatives.
- Trouvez un bon ensemble de paramètres à l'aide de la recherche par grille.
- Évaluez les performances sur un ensemble de tests en attente.
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.