Dans cette partie, nous allons traiter le problème de la classification supervisée, et voir comment utiliser les features que nous avons créées pour alimenter nos algorithmes d’apprentissage, essentiellement dans le cadre de la catégorisation de texte.
Mais attends, on a vraiment besoin d’algorithme d’apprentissage supervisé pour catégoriser du texte ? On ne peut pas juste assigner quelques mots-clés à des catégories, et hop ?
Vous faites référence aux « systèmes experts » (des règles de fonctionnement codées à la main) qui ne sont malheureusement pas assez flexibles dans la plupart des cas qui nous intéressent et demandent beaucoup de maintenance, qui va en grandissant avec l’échelle de nos données et le nombre de classes à traiter. Cela reste cependant une alternative valable et robuste dans certains cas, à ne pas négliger automatiquement.
Petit rappel de la méthodologie
La première question à se poser est : qu’est-ce que l'on veut accomplir ?
Dans cette partie, nous allons nous concentrer sur le problème de classification : pouvoir assigner une catégorie à un document texte fourni en entrée. Par exemple, un type d’actualité associé à un article, les catégories d’une page Wikipédia, le tag associé à un tweet, etc.
Pour cela on va avoir besoin d’un jeu de données d’entraînement. L’idée, comme d’habitude est qu’il soit équilibré par catégorie et relativement consistant. Cette première étape est essentielle afin de s’assurer du bon fonctionnement du reste des traitements.
La seconde étape sera de faire passer nos données non-structurées de texte à la moulinette que l’on a vue dans la première partie pour en sortir des features utilisables et structurées, par exemple sous forme vectorielle.
Une fois notre jeu de donnée d’entraînement et de test formaté, on va pouvoir appliquer les méthodes classiques de classification (SVM, réseau de neurones, régression logistique) sur ces données afin de résoudre la problématique énoncée.
S’en suit un réglage des hyperparamètres par recherche de grille (ou autre méthodologie) pour améliorer le modèle en fonction de la mesure de performance choisie.
Rappel sur les algorithmes de classification
Dans ce chapitre, on va se concentrer sur la famille d’algorithme de classification Naïve Bayes et sur la régression logistique. Ces algorithmes permettent de catégoriser le sentiment d’un texte (positif ou négatif), ou encore si un e-mail est un spam ou non.
Pour rappel, l’algorithme Naive Bayes (multinomial ou non) permet d’effectuer des classifications probabilistes, qui assignent la probabilité d’appartenance à une classe.
C’est un type d’algorithme génératif, où l’on va modéliser chaque classe et essayer de déterminer la probabilité l’appartenance d’une observation à cette classe. A contrario, les algorithmes discriminatifs essaient de comprendre les caractéristiques différenciantes entre les différentes classes possibles.
Evaluation du sentiment d’un corpus de commentaires Amazon
Comme terrain d’études, nous allons utiliser le jeu de données de commentaire de produits Amazon. L’objectif sera de déterminer si un commentaire est positif ou pas.
Petit rappel de classification Naive Bayes
Un peu plus formellement, le problème de classification se traduit ainsi : "trouver la classe 'c' qui a la probabilité la plus grande étant donné le document 'd' fourni" :
ˆc=argmaxcp(c|d)
On ne sait pas calculer p(c|d) directement, on a donc besoin de simplifier un peu le problème. Ainsi, d’après le théorème de Bayes on a :
p(c|d)=p(d|c)p(c)p(d)
L’objectif étant une maximisation sur la classe c, p(d) n’influence pas le résultat. Le problème peut être simplifié :
ˆc=argmaxcp(d|c)p(c)
Comme on l’a vu dans les chapitres précédents, le document 'd' est représenté par un certain nombres de features (les mots qu’il contient), sans conserver l’ordre de ces mots (bag-of-words) et en considérant qu’ils sont indépendant. On a ainsi pour un document de N mots wi
p(d|c)=∏Ni=1p(wi|c)
Dans le cadre d’étude de texte, nous travaillons sur des probabilité faibles. Nous allons donc plutôt travailler à l’échelle logarithmique, ce qui ne change rien au problème de maximisation (la fonction log est monotone strictement croissante). Cela nous permet, en bonus, de travailler avec des sommes.
ˆc=argmaxclog(p(d|c)p(c))=argmaxclogp(c)+∑log(p(wi|c))
Dans le cadre d’une classification binaire de texte avec unigramme.
La question restante est donc : comment estimer p(c) et p(wi|c) à partir de notre jeu de données d’entraînement. Vous l’aurez deviné, on va utiliser des fréquences
La probabilité d’une classe est simplement la fréquence d’apparition de la classe dans le jeu de données d’entraînement :
p(c)=NcNtotaldoc
Et la probabilité d’un mot dans une classe est simplement : la fréquence d’apparition de ce mot dans un type de document par rapport au nombre de mot total dans c.
p(wi|c)=Nwidans c∑VNwtdans c
On lisse cette probabilité pour les mots qui n'apparaitraientt pas dans une classe, ce qui évite de rendre nulle notre fonction de vraisemblance si un mot est à zéro prob (lissage Laplacien) :
p(wi|c)=Nw_i dans c+1∑VNw_t dans c+|V|
Testons le modèle
Nous allons utiliser ce jeu de données uniquement pour le test de la détection de sentiment. Avec NLTK, il suffit de charger les données dans un tableau labellisé pour créer notre classifieur, avec chaque mot associé à un booléen confirmant son existence dans le document (en l’occurence ici dans le commentaire).
C'est parti ! Chargeons les données et formatons-les en bag-of-words associé à des booléens :
import nltk
import os
from tools import ap
def format_sentence(sent):
return ({ word: True for word in nltk.word_tokenize(sent.decode('utf-8')) })
def load_training_set():
training = []
for fp in os.listdir(ap('aclImdb/train/pos')):
example = '{}/{}'.format(ap('aclImdb/train/pos'), fp)
with open(example) as fp:
for i in fp:
training.append([format_sentence(i), 'pos'])
for fp in os.listdir(ap('aclImdb/train/neg')):
example = '{}/{}'.format(ap('aclImdb/train/neg'), fp)
with open(example) as fp:
for i in fp:
training.append([format_sentence(i), 'neg'])
return training
training = load_training_set()
On peut maintenant entraîner notre classifieur, qui va utiliser les comptages expliqués plus haut pour créer le modèle probabiliste.
from nltk.classify import NaiveBayesClassifier
classifier = NaiveBayesClassifier.train(train)
Je vous invite très fortement à regarder le code d’implémentation du classifieur, voire d’implémenter le vôtre pour comparer les performances et comprendre comment il fonctionne : http://www.nltk.org/_modules/nltk/classify/naivebayes.html
Maintenant qu’on a entraîné notre modèle, on peut par exemple observer les features les plus représentatives des classes :
classifier.show_most_informative_features(n=25)
Most Informative Features Avoid = True neg : pos = 93.4 : 1.0 2/10 = True neg : pos = 75.7 : 1.0 4/10 = True neg : pos = 64.2 : 1.0 *1/2 = True neg : pos = 57.0 : 1.0 3/10 = True neg : pos = 43.6 : 1.0 Boll = True neg : pos = 37.7 : 1.0 Uwe = True neg : pos = 36.3 : 1.0 7/10 = True pos : neg = 33.2 : 1.0 WORST = True neg : pos = 27.8 : 1.0 8/10 = True pos : neg = 27.5 : 1.0 unwatchable = True neg : pos = 26.7 : 1.0 stinker = True neg : pos = 26.4 : 1.0 Paulie = True pos : neg = 24.3 : 1.0 awful. = True neg : pos = 23.0 : 1.0 1/10 = True neg : pos = 22.7 : 1.0 excellently = True pos : neg = 22.2 : 1.0 MST3K = True neg : pos = 22.1 : 1.0 Capote = True pos : neg = 21.7 : 1.0 awfulness = True neg : pos = 21.0 : 1.0 Highly = True pos : neg = 20.5 : 1.0 dreck = True neg : pos = 19.9 : 1.0 Ajay = True neg : pos = 19.0 : 1.0 complement = True pos : neg = 18.3 : 1.0 incoherent = True neg : pos = 18.2 : 1.0 Prom = True neg : pos = 17.7 : 1.0
Ca parait relativement réaliste sur les ratio - l'utilisation de avoid est très significative d'une review negative pour un ratio d'utilisation de 1 / 93.4 etc
Et les performances de classification sur les données test :
print(accuracy(classifier, test))
0.88754
On voit qu'on arrive à avoir une précision de prédiction de sentiments relativement intéressante (88.75% de précision) pour un premier essai sur le jeu de données test. Pour aller plus loin dans l’amélioration des performances, il peut être judicieux d’effectuer une validation croisée 😉
Conclusion
L'application d'algorithmes de classification classiques fonctionne dès lors que l'on sait quelles features utiliser à partir de notre corpus de départ.