diff --git a/negation_conversion.py b/negation_conversion.py index fe49686..0dd6b2a 100644 --- a/negation_conversion.py +++ b/negation_conversion.py @@ -15,6 +15,47 @@ sentences = ["This is not a test.", "We do not like washing dishes which lead to the decision of buying a dishwasher." ] +from nltk.parse.corenlp import CoreNLPServer +from nltk.parse.corenlp import CoreNLPParser, CoreNLPDependencyParser +set_negatives = set(['no','not','never']) +# https://nlp.stanford.edu/software/stanford-parser-full-2018-10-17.zip + +def is_negative_tree(tree): + lower_leaves = [x.lower() for x in tree.leaves()] + if len(set_negatives.intersection(lower_leaves)) > 0 and tree.label() not in ['RB','DT']: + return True + else: + return False + +def deepest_negative_subtree(tree): + negative_subtrees = [] + negative_subtrees_height = [] + for s in tree.subtrees(): + if is_negative_tree(s): + negative_subtrees.append(s) + negative_subtrees_height.append(s.height()) + if len(negative_subtrees) == 0: + return tree + else: + for s in negative_subtrees: + if s.height() == min(negative_subtrees_height): + return s + +def negative_scope_pos(pos_list): + scope_pos_list = [] + not_dummy = False + for pos_word in pos_list: + # Fin du scope + if pos_word[1] in ['IN','CC','WDT'] and not_dummy==True and len(scope_pos_list)>0: + not_dummy = False + # Intérieur du scope + if not_dummy==True: + scope_pos_list.append(pos_word) + # Début du scope + if pos_word[0].lower() in set_negatives: + not_dummy = True + return scope_pos_list + def convert_negated_words(sentence): # Pour évaluer cette tâche, nous allons utiliser cette fonction pour tester la portée de la négation @@ -34,12 +75,42 @@ def convert_negated_words(sentence): # # Mettre votre code ici. Vous pouvez effacer ces commentaires. # - converted_sentence = "This is not NOT_a NOT_test ." # A MODIFIER + + # Analyser la phrase + parse = next(parser.raw_parse(sentence)) + + # Extraire les noeuds qui représentent une négation + + print(parse) + + dns_pos_list = [] + for st in parse.subtrees(): + if is_negative_tree(st): + current_dns = deepest_negative_subtree(st) + dns_pos_list.append(negative_scope_pos(current_dns.pos())) + + unique_dns_pos_list = [x for i, x in enumerate(dns_pos_list) if i == dns_pos_list.index(x)] + + converted_sentence = sentence + for dns_pos in unique_dns_pos_list: + to_replace = " ".join(x[0] for x in dns_pos) + replacement = " ".join("NOT_"+x[0] for x in dns_pos) + converted_sentence = converted_sentence.replace(to_replace,replacement) + return converted_sentence.strip() if __name__ == '__main__': + server = CoreNLPServer("/home/francois/stanford-corenlp-full-2018-10-05/stanford-corenlp-3.9.2.jar", + "/home/francois/stanford-corenlp-full-2018-10-05/stanford-english-corenlp-2018-10-05-models.jar") + server.start() + parser = CoreNLPParser() + output_file = open("/home/francois/nlp_a2019_tp2/nlp_a2019_tp2/output_negative.txt","w") for sent in sentences: print("\nS:", sent) + output_file.write("S: "+sent) converted = convert_negated_words(sent) - print("N:", converted) \ No newline at end of file + print("N:", converted) + output_file.write("\nN: "+converted.replace("."," .").replace(","," ,")+"\n\n") + output_file.close() + server.stop() \ No newline at end of file diff --git a/rapport.md b/rapport.md index ab57915..db4851b 100644 --- a/rapport.md +++ b/rapport.md @@ -12,7 +12,9 @@ output: pdf_document ## Introduction -L'objectif de cet exercice est de construire des modèles qui permettent de classifier des commentaires variés selon leur polarité. Nous disposons d'un jeu d'entraînement balancé composé de 3000 commentaires positifs et 3000 négatifs. Le jeu de test est aussi balancé et est composé de 986 commentaires positifs et 982 négatifs. Tous les commentaires sont en anglais. Toutes les tâches de traitement du langage naturel ont été effectuées à l'aide de la librairie *nltk* de Python. +L'objectif de cet exercice est de construire des modèles qui permettent de classifier des commentaires variés selon leur polarité. Nous disposons d'un jeu d'entraînement balancé composé de 3000 commentaires positifs et 3000 négatifs. Le jeu de test est aussi balancé et est composé de 986 commentaires positifs et 982 négatifs. Tous les commentaires sont en anglais. Pour des fins d'analyse, les commentaires positifs et négatifs, fournis séparément, ont été assemblés dans un seul corpus d'entraînement et un seul corpus de test. + +Toutes les tâches de traitement du langage naturel ont été effectuées à l'aide de la librairie *nltk* de Python. Les tâches qui ont trait à l'entraînement de modèles d'apprentissage statistique ont été effectuées avec la librairie Scikit-Learn. ## Prétraitement @@ -27,59 +29,134 @@ Pour chacune de ces étapes, certaines techniques seront expérimentées. Celles ### Tokenisation -La tokenisation a été effectuée à l'aide de **Tok-tok**. Ce tokenizer récent fonctionne à l'aide d'expressions régulières. Comme il fonctionne à l'aide de phrases et non de documents, les commentaires ont d'abord été segmentés en phrases à l'aide de **Punkt**. La segmentation sera aussi conservée, car elle sera utilisée à l'étape de la sélection des types, en utilisant un étiquetage de classes grammaticale (POS). Une correction a été apportée aux documents, car de nombreuses fin de phrases étaient représentées par deux espaces au lieu d'un point et d'un espace. Cette substitution a été effectuée avec une expression régulière. Enfin, comme Tok-tok conserve le dernier caractère de ponctuation de la phrase comme un token, celui-ci est supprimé avant de retourner le résultat. +La tokenisation a été effectuée à l'aide de **Tok-tok**. Ce tokenizer récent fonctionne à l'aide d'expressions régulières. Comme il fonctionne à l'aide de phrases et non de documents, les commentaires ont d'abord été segmentés en phrases à l'aide de **Punkt**. La segmentation sera aussi conservée, car elle sera utilisée à l'étape de la sélection des types, en utilisant un étiquetage de classes grammaticale (POS). + +Une correction a été apportée aux documents, car de nombreuses fin de phrases étaient représentées par deux espaces au lieu d'un point et d'un espace. Cette substitution a été effectuée avec une expression régulière. Enfin, comme Tok-tok conserve le dernier caractère de ponctuation de la phrase comme un token, celui-ci est supprimé avant de retourner le résultat. ### Normalisation Deux types de normalisation sont testées dans cet exercice: la désuffixation et la lemmatisation. -La désuffixation (*stemming*) est effectuée à l'aide de l'algorithme **Porter stemmer**. Cet algorithme ne conserve que la racine du mot, ce qui a pour effet de produire des racines qui peuvent n'avoir aucun lien avec la nature du mot. Par exemple, *was* devient *wa*. La lemmatisation est effectuée à l'aide du **WordNet Lemmatizer**, qui utiliser la fonction *morphy* incluse dans WordNet. Si le mot n'est pas trouvé dans WordNet, il est retourné inchangé. Cet algorithme retourne par défaut un lemme qui est un nom, mais il est aussi possible de spécifier de retourner un verbe. C'est ce choix qui a été effectué dans le cas de cet exercice, car dans plusieurs cas nous avons des phrases incomplètes et ceci nous assure de retrouver au moins un verbe dans la phrase. +La désuffixation (*stemming*) est effectuée à l'aide de l'algorithme **Porter stemmer**. Cet algorithme ne conserve que la racine du mot, ce qui a pour effet de produire des racines qui peuvent n'avoir aucun lien avec la nature du mot. Par exemple, *was* devient *wa*. + +La lemmatisation est effectuée à l'aide du **WordNet Lemmatizer**, qui utiliser la fonction *morphy* incluse dans WordNet. Si le mot n'est pas trouvé dans WordNet, il est retourné inchangé. Cet algorithme retourne par défaut un lemme qui est un nom, mais il est aussi possible de spécifier de retourner un verbe. C'est ce choix qui a été effectué dans le cas de cet exercice, car dans plusieurs cas nous avons des phrases incomplètes et ceci nous assure de retrouver au moins un verbe dans la phrase. ### Sélection des types Trois types de sélection des types sont testés dans cet exercice: -- La fréquence du mot est supérieure à 3 dans le corpus. Cette approche est implémentée à l'aide d'un dictionnaire qui recense le dénombrement de chaque type. -- Le mot ne figure pas dans une liste de mots-outils (*stop-words*). Cette approche utilise la liste de mots-outils incluse dans *nltk* pour l'anglais. -- Le mot appartient à une classe grammaticale ouverte. L'algorithme par défaut utilise le Penn Treebank, mais on effectue l'étiquetage à l'aide des **Universal POS tags** qui sont plus simples, comme on veut identifier des classes ouvertes à haut niveau, soit les noms, adjectifs, adverbes et verbes, représentés par NOUN, ADJ, ADV et VERB. Le mot *I* n'est pas classé correctement dans plusieurs cas, il est considéré comme un nom. Comme c'est un pronom, qui est une classe fermée, il est aussi éliminé de la liste des tokens sélectionnés. +- La fréquence du mot est supérieure à 3 dans le corpus. + +Cette approche est implémentée à l'aide d'un dictionnaire qui recense le dénombrement de chaque type. + +- Le mot ne figure pas dans une liste de mots-outils (*stop-words*). + +Cette approche utilise la liste de mots-outils incluse dans *nltk* pour l'anglais. + +- Le mot appartient à une classe grammaticale ouverte. + +L'algorithme par défaut utilise le Penn Treebank, mais on effectue l'étiquetage à l'aide des **Universal POS tags** qui sont plus simples, comme on veut identifier des classes ouvertes à haut niveau, soit les noms, adjectifs, adverbes et verbes, représentés par NOUN, ADJ, ADV et VERB. Le mot *I* n'est pas classé correctement dans plusieurs cas, il est considéré comme un nom. Comme c'est un pronom, qui est une classe fermée, il est aussi éliminé de la liste des tokens sélectionnés. + +L'usage de ces techniques de sélection des types limitent le nombre d'attributs et aident à réduire le surapprentissage. ### Vectorisation Trois types de vectorisations ont été testées dans cet exercice: -- +- Cardinalité des mots dans le commentaire + +On utilise la fonction **CountVectorizer** de Scikit-Learn. Comme les données sont déjà tokenisées et transformées, on remplace les fonctions par défaut par la fonction identité. + +- Occurence des mots dans le commentaire + +On utilise la même fonction que pour la vectorisation en compteurs, mais en ajoutant une contrainte binaire à l'aide d'un parametre additionnel. + +- Mesure TF-IDF des mots présente dans le commentaire, comparé à l'ensemble des documents. + +On utilise la fonction **TfidfVectorizer** de Scikit-Learn + + +Il est important de noter que ces trois méthodes produisent des matrices creuses afin de limiter la taille en mémoire des objets et augmenter la performance des algorithmes d'apprentissage. + +## Attributs additionnels + +### Compteur de polarités + +Afin de compter les mots ayant une polarité positive ou négative dans chacun des commentaires, on utilise l'extension SentiWordnet de Wordnet. On identifie d'abord le *synset* de WordNet, soit le groupe sémantique auquel appartient le mot, puis on extrait les valeurs de polarités associées à ce synset depuis SentiWordnet. + +Lorsque le score est positif, on ajoute 1 au compteur de mots positifs, et lorsqu'il est négatif, on ajoute 1 au compteur de mots négatifs. On ne compte pas les mots neutres. Notons que nous n'utilisons pas les phrases ici, mais seulement les mots des commentaires, sous le modèle du sac de mots. + +On crée aussi un attribut qui comprend le nombre de mots dans le commentaire. + +Ces trois attributs sont ajoutés aux matrices créées par l'étape de vectorisation. Les matrices résultantes sont toujours creuses, en utilisant lesa fonctions `csr_matrix` et `hstack` de Scikit-Learn. ## Entraînement des modèles -Deux types de modèles sont entrainés: +Deux types de modèles sont entrainés sur chacunes des 18 combinaisons de normalisation, sélection et vectorisation: -- Naive Bayes -- Régression logistique - -### Naive Bayes - -### Régression logistique +- Naive Bayes, en utilisant la classe MultinomialNB du module `naive_bayes` de Scikit-Learn et en conservant les paramètres par défaut. +- Régression logistique, en utilisant la classe LogisticRegression du module `linear_model` de Scikit-Learn et en conservant les paramètres par défaut. ## Analyse de la performance -Pour chacun des modèles entraînés, différentes métriques de performance sont calculées à l'aide du jeu de données de test. +Pour chacun des modèles entraînés, les métriques de performance suivantes sont calculées à l'aide du jeu de données de test: + +- Précision +$$ +\frac{(pos,pos)}{(pos,pos)+(neg,pos)} +$$ +- Rappel +$$ +\frac{(pos,pos)}{(pos,pos)+(pos,neg)} +$$ +- Exactitude +$$ +\frac{(pos,pos)+(neg,neg)}{(pos,pos)+(neg,pos)+(neg,neg)+(pos,neg)} +$$ + +Pour les modèles entrainés avec l'algorithme Naive Bayes, les résultats apparaissent à la Figure 1. Pour ceux entraînés avec l'algorithme de régression logistique, les résultats apparaissent à la Figure 2. + +#### Naive Bayes ![Métriques de performance: Naive Bayes](table_metriques_nb.pdf) +On observe qu'avec l'approche de Naive Bayes, peu importe l'algorithme de normalisation, la sélection des classes ouvertes et la vectorisation TF-IDF donnent une plus grande précision, mais offrent aussi le rappel le plus bas. La vectorisation par occurences offre la meilleure exactitude et une précision comparable, pour un rappel généralement plus élevé. +La désuffixation, la sélection par classe ouverte et la vectorisation par occurence semblent former la meilleure combinaison à utiliser avec le classificateur Naive Bayes. + +#### Régression logistique + ![Métriques de performance: Régression logistique](table_metriques_rl.pdf) +Contrairement à la situation avec le classificateur Naive Bayes, avec la régression logistique, le rappel est plus grand avec la vectorisation TF-IDF qu'avec les deux autres approches. La normalisation par désuffixation offre des performances légèrement supérieures sur les trois mesures dans la majorité des situations. Cependant, si on veut augmenter le rappel, il est mieux d'opter pour les approches utilisant la lemmatisation. Il n'y a pas de combinaison qui se démarque véritablement dans ce cas-ci, car les valeurs des métriques sont plutôt similaires. La combinaison désuffixation, sélection par classe ouverte et vectorisation TF-IDF serait une bonne approche. + ## Conclusion +Le principal constat que l'on peut tirer de cet exercice est qu'il y a plusieurs approches disponibles pour effectuer le prétraitement d'un corpus en vue d'effectuer l'entraînement d'un modèle d'apprentissage statistique. Ces différentes méthodes peuvent influencer une ou plusieurs mesures de performance de l'algorithme d'entraînement. Si on veut utiliser l'algorithme Naive Bayes, on peut utiliser des approches moins sophistiquées de normalisation et de vectorisation, tel que le compteur d'occurences, sans perdre beaucoup de valeur statistique, alors que pour la régression logistique, nous sommes avantagés par les techniques qui conservent davantage de précision dans les données, tel que la vectorisation TF-IDF. + +\pagebreak + # Analyse syntaxique - portée d'une négation ## Introduction + + ## Conclusion # Références +## Classification de textes + - [Punkt Sentence Tokenizer](https://www.nltk.org/api/nltk.tokenize.html#module-nltk.tokenize.punkt) - [Tok-tok tokenizer](https://www.nltk.org/api/nltk.tokenize.html#module-nltk.tokenize.toktok) - [Porter stemmer](https://www.nltk.org/api/nltk.stem.html#nltk.stem.porter.PorterStemmer) - [WordNet Lemmatizer](https://www.nltk.org/api/nltk.stem.html#nltk.stem.wordnet.WordNetLemmatizer) - [POS tagging](https://www.nltk.org/api/nltk.tag.html#module-nltk.tag) +- [CountVectorizer¶](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.CountVectorizer.html) +- [TfidfVectorizer¶](https://scikit-learn.org/stable/modules/generated/sklearn.feature_extraction.text.TfidfVectorizer.html) +- [Naive Bayes](https://scikit-learn.org/stable/modules/naive_bayes.html) +- [Generalized Linear Models](https://scikit-learn.org/stable/modules/linear_model.html) + +## Analyse syntaxique + +- [Syntax Parsing with CoreNLP and NLTK](https://www.districtdatalabs.com/syntax-parsing-with-corenlp-and-nltk)