diff --git a/content/course/NLP/01_intro/index.qmd b/content/course/NLP/01_intro/index.qmd index ed1835053..928048c27 100644 --- a/content/course/NLP/01_intro/index.qmd +++ b/content/course/NLP/01_intro/index.qmd @@ -1,5 +1,5 @@ --- -title: "Quelques éléments pour comprendre les enjeux" +title: "Quelques éléments pour comprendre les enjeux du NLP" date: 2020-10-15T13:00:00Z draft: false weight: 10 @@ -20,6 +20,7 @@ summary: | en oeuvre une série d'étapes de nettoyage de texte. Ce chapitre va explorer quelques méthodes classiques de nettoyage en s'appuyant sur le _Comte de Monte Cristo_ d'Alexandre Dumas. +bibliography: ../../../../reference.bib --- ::: {.cell .markdown} @@ -39,23 +40,36 @@ print_badges("content/course/NLP/01_intro.qmd") ::: - -{{% box status="warning" title="Warning" icon="fa fa-exclamation-triangle" %}} -Le NLP est un domaine immense de recherche. Cette page est une introduction +Le _NLP_ est un domaine immense de recherche. Cette page est une introduction fort incomplète à la question. Il s'agit de montrer la logique, quelques exemples avec `Python` et s'amuser avec comme base d'exemple un livre formidable :books: : -*Le Comte de Monte Cristo* -{{% /box %}} +*Le Comte de Monte Cristo*. + +Dans le cadre de l'introduction au NLP que vous pouvez retrouver dans +les différents chapitres, nous évoquons principalement les champs suivants du NLP: + +- _Preprocessing_ +- Approches _bag of words_ et contextuelles (n-grams, etc.) +- _Topics modelling_ +- _Word embedding_ + +Cela laisse de côté des champs très actifs de recherche +du NLP, notamment l'analyse de sentiment ou les modèles de +langage (modèles GPT par exemple). Les outils découverts +dans cette partie du cours permettront, si vous le désirez, +de bénéficier d'une base solide pour approfondir tel ou tel +sujet. + -## Base d'exemple +# Base d'exemple La base d'exemple est le *Comte de Monte Cristo* d'Alexandre Dumas. Il est disponible gratuitement sur le site [Project Gutemberg](http://www.gutenberg.org/ebooks/author/492) comme des milliers d'autres livres du domaine public. La manière la plus simple de le récupérer -est de télécharger avec le module `urllib` le fichier texte et le retravailler +est de télécharger avec le _package_ `request` le fichier texte et le retravailler légèrement pour ne conserver que le corpus du livre : ```{python} @@ -80,12 +94,13 @@ dumas[10000:10500] ``` -## La particularité des données textuelles +# La particularité des données textuelles -### Objectif +## Objectif Le *natural language processing* (NLP) ou -*traitement automatisé de la langue* (TAL) en Français, vise à extraire de l'information de textes à partir d'une analyse statistique du contenu. +*traitement automatisé de la langue* (TAL) en Français, +vise à extraire de l'information de textes à partir d'une analyse statistique du contenu. Cette définition permet d'inclure de nombreux champs d'applications au sein du NLP (traduction, analyse de sentiment, recommandation, surveillance, etc. ) ainsi que de méthodes. @@ -102,7 +117,7 @@ Si cette tâche n'était pas assez difficile comme ça, on peut ajouter d'autres * propres à chaque langue : il n'existe pas de règle de passage unique entre deux langues * grande dimension : des combinaisons infinies de séquences de mots -### Méthode +## Méthode L’unité textuelle peut être le mot ou encore une séquence de *n* mots (un *n-gramme*) ou encore une chaîne de caractères (e.g. la @@ -116,7 +131,7 @@ l’information transformée. Mais les étapes de nettoyage de texte sont indisp -## Nettoyer un texte +# Nettoyer un texte Les *wordclouds* sont des représentations graphiques assez pratiques pour visualiser les mots les plus fréquents. Elles sont très simples à implémenter en `Python` @@ -125,8 +140,8 @@ une image : ```{python} -#| include: false #| echo: true + import wordcloud import numpy as np import io @@ -158,9 +173,59 @@ d'articles ou mots de liaison qui perturbent l'analyse. Ces mots sont des de référence dans le domaine du NLP, permet de facilement retirer ces stopwords (cela pourrait également être fait avec la librairie plus récente, `spaCy`). Avant cela, il est nécessaire -de transformer notre texte en le découpant par unités fondamentales (les tokens) +de transformer notre texte en le découpant par unités fondamentales (les tokens). + +Les exemples suivants, extraits de @galianafuzzy, montrent l'intérêt du +nettoyage de textes lorsqu'on désire comparer des corpus +entre eux. En l'occurrence, il s'agit de comparer un corpus de +noms de produits dans des collectes automatisées de produits +de supermarché (_scanner-data_) avec des noms de produits +dans les données de l'`OpenFoodFacts`, une base de données +contributive. Sans nettoyage, le bruit l'emporte sur le signal +et il est impossible de déceler des similarités entre les jeux +de données. Le nettoyage permet d'harmoniser +un peu ces jeux de données pour avoir une chance d'être en +mesure de les comparer. + +::: {layout-ncol=2} +![`OpenFoodFacts` avant nettoyage](wordcloud_openfood_start.png) + +![_Scanner-data_ avant nettoyage](wordcloud_relevanc_start.png) +::: + +::: {layout-ncol=2} +![`OpenFoodFacts` après nettoyage](wordcloud_openfood_clean.png) + +![_Scanner-data_ après nettoyage](wordcloud_relevanc_clean.png) +::: + + +## Tokenisation + +::: {.cell .markdown} +```{=html} + +``` +::: + +```{python} +import nltk +nltk.download('punkt') +``` La tokenisation consiste à découper un texte en morceaux. Ces morceaux pourraient être des phrases, des chapitres, des n-grammes ou des mots. C'est @@ -169,7 +234,6 @@ cette dernière option que l'on va choisir, plus simple pour retirer les ```{python} import nltk -nltk.download('punkt') words = nltk.word_tokenize(dumas, language='french') words[1030:1050] @@ -185,34 +249,55 @@ words = [word for word in words if word.isalpha()] words[1030:1050] ``` -{{% box status="hint" title="Hint" icon="fa fa-lightbulb" %}} -Lors de la première utilisation de `NLTK`, il est nécessaire de télécharger -quelques éléments nécessaires à la tokenisation, notamment la ponctuation. -Pour cela, +Comme indiqué ci-dessus, pour télécharger +le corpus de ponctuation, il est +nécessaire d'exécuter la ligne de +commande suivante : + + -~~~python -import nltk -nltk.download('punkt') -~~~ -{{% /box %}} -### Retirer les stopwords +## Retirer les stop-words Le jeu de données est maintenant propre. On peut désormais retirer les -*stop words*. +mots qui n'apportent pas de sens et servent seulement à faire le +lien entre deux prépositions. On appelle ces mots des +*stop words* dans le domaine du NLP. -{{% box status="hint" title="Hint" icon="fa fa-lightbulb" %}} +::: {.cell .markdown} +```{=html} + ``` +::: + +Comme indiqué ci-dessus, pour télécharger +le corpus de stopwords[^2], il est +nécessaire d'exécuter la ligne de +commande suivante : -{{% /box %}} +```{python} +import nltk +nltk.download('stopwords') +``` +[^2]: Le corpus de _stop-words_ de `NLTK` +est relativement limité. Il est recommandé +de privilégier celui de `spaCy`, plus +complet, pour éliminer plus de mots +valises. ```{python} from nltk.corpus import stopwords @@ -230,8 +315,6 @@ de sens commencent à se dégager, notamment les noms des personnages (Fernand, Mercédès, Villefort, etc.) - - ```{python} #| echo: true wc = make_wordcloud(' '.join(words)) @@ -242,15 +325,11 @@ plt.imshow(wc, interpolation='bilinear') plt.axis("off") ``` -```{python} -#| echo: false -plt.savefig("featured.png") -``` -### *Stemming* +## *Stemming* Pour réduire la complexité d'un texte, on peut tirer partie de -"classes d'équivalence" : on peut +_"classes d'équivalence"_ : on peut considérer que différentes formes d’un même mot (pluriel, singulier, conjugaison) sont équivalentes et les remplacer par une même forme dite canonique. Il existe deux approches dans le domaine : @@ -271,6 +350,7 @@ conjugaisons ou variations comme les pluriels, on applique une méthode de nombreuses variantes d’un mot comme un seul et même mot. Par exemple, une fois que l’on applique un stemming, "chats" et "chat" deviennent un même mot. + Cette approche a l'avantage de réduire la taille du vocabulaire à maîtriser pour l'ordinateur et le modélisateur. Il existe plusieurs algorithmes de *stemming*, notamment le *Porter Stemming Algorithm* ou le @@ -287,41 +367,72 @@ print(stemmed[1030:1050]) A ce niveau, les mots commencent à être moins intelligibles par un humain. La machine prendra le relais, on lui a préparé le travail -{{% box status="note" title="Note" icon="fa fa-comment" %}} -Il existe aussi le stemmer suivant : +::: {.cell .markdown} +```{=html} + +``` +::: -### Reconnaissance des entités nommées +# Reconnaissance des entités nommées Cette étape n'est pas une étape de préparation mais illustre la capacité des librairies `Python` a extraire du sens d'un texte. La librairie `spaCy` permet de faire de la reconnaissance d'entités nommées, ce qui peut -être pratique pour extraire rapidement certains personnages de notre oeuvre +être pratique pour extraire rapidement certains personnages de notre oeuvre. + +::: {.cell .markdown} +```{=html} + +``` +::: ~~~python #!pip install deplacy #!python -m spacy download fr_core_news_sm -import pkg_resources,imp -imp.reload(pkg_resources) import spacy nlp=spacy.load("fr_core_news_sm") doc = nlp(dumas) +import spacy +from spacy import displacy displacy.render(doc, style="ent", jupyter=True) ~~~ -## Représentation d'un texte sous forme vectorielle +# Représentation d'un texte sous forme vectorielle Une fois nettoyé, le texte est plus propice à une représentation vectorielle. En fait, implicitement, on a depuis le début adopté une démarche *bag of words*. -Il s'agit d'une représentation, sans souci de contexte (ordre, utilisation), +Il s'agit d'une représentation, sans souci de contexte (ordre des mots, contexte d'utilisation), où chaque *token* représente un élément dans un vocabulaire de taille $|V|$. On peut ainsi avoir une représentation matricielle les occurrences de chaque *token* dans plusieurs documents (par exemple plusieurs livres, @@ -334,3 +445,5 @@ On élimine ainsi certains mots très fréquents ou au contraire très rares. La pondération la plus simple est basée sur la fréquence des mots dans le document. C'est l'objet de la métrique **tf-idf** (term frequency - inverse document frequency) abordée dans un prochain chapitre. + +# Références \ No newline at end of file diff --git a/content/course/NLP/02_exoclean/index.qmd b/content/course/NLP/02_exoclean/index.qmd index d15cdbd0b..2c5e46b12 100644 --- a/content/course/NLP/02_exoclean/index.qmd +++ b/content/course/NLP/02_exoclean/index.qmd @@ -1,5 +1,5 @@ --- -title: "Nettoyer un texte: approche bag-of-words (exercices)" +title: "Nettoyer un texte: des exercices pour découvrir l'approche bag-of-words" date: 2020-10-29T13:00:00Z draft: false weight: 20 @@ -21,6 +21,7 @@ summary: | Dans cette série d'exercice nous mettons en oeuvre de manière plus approfondie les différentes méthodes présentées précedemment. +bibliography: ../../../../reference.bib --- ::: {.cell .markdown} @@ -59,7 +60,7 @@ On prendra appui sur l'approche *bag of words* présentée dans le chapitre pré [^1]: L'approche *bag of words* est déjà, si on la pousse à ses limites, très intéressante. Elle peut notamment faciliter la mise en cohérence de différents corpus par la méthode des appariements flous -(cf. [Galiana et al. 2022](https://epic-davinci-acb57b.netlify.app/#1). +(cf. [@galianafuzzy](https://epic-davinci-acb57b.netlify.app/#1). Le [chapitre sur ElasticSearch](#elastic) présent dans cette partie du cours présente quelques éléments de ce travail sur les données de l'`OpenFoodFacts` @@ -71,33 +72,46 @@ Ce notebook est librement inspiré de : * https://www.kaggle.com/meiyizi/spooky-nlp-and-topic-modelling-tutorial/notebook Les chapitres suivants permettront d'introduire aux enjeux de modélisation -de corpus textuels. Dans un premier temps, le modèle LDA permettra d'explorer -le principe des modèles bayésiens à couche cachés pour modéliser les sujets (*topics*) -présents dans un corpus et segmenter ces topics selon les mots qui les composent. +de corpus textuels. Dans un premier temps, le modèle `LDA` permettra d'explorer +le principe des modèles bayésiens à couche cachées pour modéliser les sujets (*topics*) +présents dans un corpus et segmenter ces _topics_ selon les mots qui les composent. Le dernier chapitre de la partie visera à prédire quel texte correspond à quel auteur à partir d'un modèle `Word2Vec`. Cela sera un pas supplémentaire dans la formalisation puisqu'il s'agira de représenter chaque mot d'un texte sous forme d'un vecteur de grande dimension, ce qui nous permettra de rapprocher les mots entre eux dans un espace complexe. +Cette technique, dite des plongements de mots (_Word Embedding_), +permet ainsi de transformer une information complexe difficilement quantifiable +comme un mot +en un objet numérique qui peut ainsi être rapproché d'autres par des méthodes +algébriques. Pour découvrir ce concept, ce [post de blog](https://ssphub.netlify.app/post/word-embedding/) +est particulièrement utile. En pratique, la technique des +plongements de mots permet d'obtenir des tableaux comme celui-ci: +![](word_embedding.png) -## Librairies nécessaires + +# Librairies nécessaires Cette page évoquera les principales librairies pour faire du NLP, notamment : * [WordCloud](https://github.com/amueller/word_cloud) * [nltk](https://www.nltk.org/) -* [spacy](https://spacy.io/) +* [SpaCy](https://spacy.io/) * [Keras](https://keras.io/) * [TensorFlow](https://www.tensorflow.org/) Il faudra également installer les librairies `gensim` et `pywaffle` -{{% box status="warning" title="Attention" icon="fa fa-exclamation-triangle" %}} +::: {.cell .markdown} +```{=html} + +``` +::: La liste des modules à importer est assez longue, la voici : @@ -142,20 +159,32 @@ nltk.download('wordnet') nltk.download('omw-1.4') ``` -## Données utilisées - -{{% box status="exercise" title="Exercice" icon="fas fa-pencil-alt" %}} +# Données utilisées -**Exercice 1 : Importer les données spooky** [OPTIONNEL] +::: {.cell .markdown} +```{=html} + +``` +::: + ```{python} #| echo: false import pandas as pd ``` -1. Importer le jeu de données `spooky` à partir de l'URL sous le nom `train`. L'encoding est `latin-1` ```{python} #| include: false @@ -166,8 +195,6 @@ url='https://github.com/GU4243-ADS/spring2018-project1-ginnyqg/raw/master/data/s train = pd.read_csv(url,encoding='latin-1') ``` -2. Mettre des majuscules au nom des colonnes. - ```{python} #| include: false #| echo: false @@ -176,7 +203,6 @@ train = pd.read_csv(url,encoding='latin-1') train.columns = train.columns.str.capitalize() ``` -3. Retirer le prefix `id` de la colonne `Id` et appeler la nouvelle colonne `ID`. ```{python} #| include: false @@ -187,21 +213,15 @@ train['ID'] = train['Id'].str.replace("id","") ``` -4. Mettre l'ancienne colonne `Id` en index. - ```{python} #| include: false #| echo: false -#3. Retirer le prefixe id -train = train.set_index('ID') +#4. Mettre Id en index +train = train.set_index('Id') #train.head() ``` - - -{{% /box %}} - Si vous ne faites pas l'exercice 1, pensez à charger les données en executant la fonction `get_data.py` : ```{python} @@ -217,8 +237,6 @@ train = getdata.create_train_dataframes() Ce code introduit une base nommée `train` dans l'environnement. - - Le jeu de données met ainsi en regard un auteur avec une phrase qu'il a écrite : ```{python} @@ -231,22 +249,22 @@ sampsize = train.shape[0] ``` -On peut se rendre compte que les extraits des 3 auteurs ne sont pas forcément équilibrés dans le jeu de données. Il faudra en tenir compte dans la prédiction. +On peut se rendre compte que les extraits des 3 auteurs ne sont +pas forcément équilibrés dans le jeu de données. +Il faudra en tenir compte dans la prédiction. ```{python} -#| include: false #| echo: true fig = plt.figure() g = sns.barplot(x=['Edgar Allen Poe', 'Mary W. Shelley', 'H.P. Lovecraft'], y=train['Author'].value_counts()) -g ``` -```{python} -g.figure.get_figure() +::: {.cell .markdown} +```{=html} + +``` +::: ## Fréquence d'un mot @@ -271,37 +293,60 @@ constructions d'indices de similarité cosinus reposent sur ce type d'approche Avant de s'adonner à une analyse systématique du champ lexical de chaque auteur, on se focaliser dans un premier temps sur un unique mot, le mot *fear*. -{{% box status="note" title="Note" icon="fa fa-comment" %}} +::: {.cell .markdown} +```{=html} + +``` +::: -**Exercice 2 : Fréquence d'un mot** +::: {.cell .markdown} +```{=html} + +``` +::: + +A l'issue de la question 1, vous devriez obtenir le tableau +de fréquence suivant: ```{python} #| echo: false +#| include: false #1. Compter le nombre de phrase pour chaque auteur avec fear def nb_occurrences(word, train_data): train_data['wordtoplot'] = train_data['Text'].str.contains(word).astype(int) - table = train.groupby('Author').sum() + table = train_data.groupby('Author').sum() data = table.to_dict()['wordtoplot'] return table table = nb_occurrences("fear", train) -table.head() ``` -2. Utiliser `pywaffle` pour obtenir les graphiques ci-dessous qui résument -de manière synthétique le nombre d'occurrences du mot *"fear"* par auteur. +```{python} +#| echo: false +table.head() +``` ```{python} #| include: false @@ -324,18 +369,28 @@ def graph_occurrence(word, train_data): fig = graph_occurrence("fear", train) ``` +Ceci permet d'obtenir le _waffle chart_ suivant: + ```{python} #| echo: false fig.get_figure() ``` +On remarque ainsi de manière très intuitive +le déséquilibre de notre jeu de données +lorsqu'on se focalise sur le terme _"peur"_ +où Mary Shelley représente près de 50% +des observations. + ```{python} #| echo: false fig.get_figure().savefig("featured.png") ``` - -3. Refaire l'analyse avec le mot *"horror"*. +Si on reproduit cette analyse avec le terme _"horror"_, on peut +en conclure que la peur est plus évoquée par Mary Shelley +(sentiment assez naturel face à la créature du docteur Frankenstein) alors +que Lovecraft n'a pas volé sa réputation d'écrivain de l'horreur ! ```{python} #| include: false @@ -350,23 +405,29 @@ fig = graph_occurrence("horror", train) fig.get_figure() ``` -La peur est ainsi plus évoquée par Mary Shelley -(sentiment assez naturel face à la créature du docteur Frankenstein) alors -que Lovecraft n'a pas volé sa réputation d'écrivain de l'horreur ! - -{{% /box %}} -## Premier *wordcloud* +# Premier *wordcloud* Pour aller plus loin dans l'analyse du champ lexical de chaque auteur, on peut représenter un `wordcloud` qui permet d'afficher chaque mot avec une taille proportionnelle au nombre d'occurrence de celui-ci. -{{% box status="exercise" title="Exercice" icon="fas fa-pencil-alt" %}} +::: {.cell .markdown} +```{=html} + +``` +::: + ```{python} #| include: false @@ -379,7 +440,9 @@ def graph_wordcloud(author, train_data, varname = "Text"): all_text = ' '.join([text for text in txt]) wordcloud = WordCloud(width=800, height=500, random_state=21, - max_words=2000).generate(all_text) + max_words=2000, + background_color = "white", + colormap='Set2').generate(all_text) return wordcloud n_topics = ["HPL","EAP","MWS"] @@ -393,13 +456,15 @@ for i in range(len(n_topics)): ax.axis('off') ``` +Le _wordcloud_ pour nos différents auteurs est le suivant: + ```{python} #| echo: false fig.get_figure() ``` - -2. Calculer les 25 mots plus communs pour chaque auteur et représenter les trois histogrammes des décomptes. +Enfin, si on fait un histogramme des fréquences, +cela donnera : ```{python} #| include: false @@ -434,29 +499,45 @@ l'analyser (sauf si on est intéressé par la loi de Zipf, cf. exercice suivant). -{{% /box %}} - ## Aparté: la loi de Zipf -{{% box status="hint" title="La loi de Zipf" icon="fa fa-lightbulb" %}} +::: {.cell .markdown} +```{=html} + +``` +::: -On va estimer le modèle suivant par GLM via `statsmodels`: +Un modèle exponentiel peut se représenter par un modèle de Poisson ou, si +les données sont très dispersées, par un modèle binomial négatif. Pour +plus d'informations, consulter l'annexe de @galiana2020segregation. +La technique économétrique associée pour l'estimation est +les modèles linéaires généralisés (GLM) qu'on peut +utiliser en `Python` via le +package `statsmodels`[^3]: + +[^3]: La littérature sur les modèles gravitaires, présentée dans @galiana2020segregation, +donne quelques arguments pour privilégier les modèles GLM à des modèles log-linéaires +estimés par moindres carrés ordinaires. $$ \mathbb{E}\bigg( f(n_i)|n_i \bigg) = \exp(\beta_0 + \beta_1 \log(n_i)) $$ -Prenons les résultats de l'exercice précédent et enrichissons les du rang et de la fréquence d'occurrence d'un mot : - +Prenons les résultats de l'exercice précédent et enrichissons les du rang et de la fréquence d'occurrence d'un mot : ```{python} count_words = pd.DataFrame({'counter' : train @@ -476,6 +557,8 @@ count_words = count_words.assign( ) ``` +Commençons par représenter la relation entre la fréquence et le rang: + ```{python} #| include: false #| echo: true @@ -484,13 +567,15 @@ g.set(xscale="log", yscale="log") g ``` +Nous avons bien, graphiquement, une relation log-linéaire entre les deux: + ```{python} g.figure.get_figure() ``` +Avec `statsmodels`, vérifions plus formellement cette relation: ```{python} -#| output: hide import statsmodels.api as sm exog = sm.add_constant(np.log(count_words['rank'].astype(float))) @@ -498,15 +583,15 @@ exog = sm.add_constant(np.log(count_words['rank'].astype(float))) model = sm.GLM(count_words['freq'].astype(float), exog, family = sm.families.Poisson()).fit() # Afficher les résultats du modèle -print(model.summary().as_html()) +print(model.summary()) ``` - +Le coefficient de la régression est presque 1 ce qui suggère bien une relation +quasiment log-linéaire entre le rang et la fréquence d'occurrence d'un mot. +Dit autrement, le mot le plus utilisé l'est deux fois plus que le deuxième +mois le plus fréquent qui l'est trois plus que le troisième, etc. -{{% /box %}} - - -## Nettoyage d'un texte +# Nettoyage d'un texte Les premières étapes dans le nettoyage d'un texte, qu'on a développé au cours du [chapitre précédent](#nlp), sont : @@ -515,23 +600,42 @@ développé au cours du [chapitre précédent](#nlp), sont : * suppression des *stopwords* Cela passe par la tokenisation d'un texte, c'est-à-dire la décomposition -de celui-ci en unités lexicales (les *tokens*). Ces unités lexicales peuvent être de différentes natures, selon l'analyse que l'on désire mener. Ici, on va définir les tokens comme étant les mots utilisés. - -Plutôt que de faire soi-même ce travail de nettoyage, avec des fonctions mal optimisées, on peut utiliser la librairie `nltk` comme détaillé [précédemment](#nlp). +de celui-ci en unités lexicales (les *tokens*). +Ces unités lexicales peuvent être de différentes natures, +selon l'analyse que l'on désire mener. +Ici, on va définir les tokens comme étant les mots utilisés. +Plutôt que de faire soi-même ce travail de nettoyage, +avec des fonctions mal optimisées, +on peut utiliser la librairie `nltk` comme détaillé [précédemment](#nlp). -{{% box status="exercise" title="Exercice" icon="fas fa-pencil-alt" %}} -**Exercice 4 : Nettoyage du texte** +::: {.cell .markdown} +```{=html} + +``` +::: + +Pour rappel, au début de l'exercice, le `DataFrame` présente l'aspect suivant: + + ```{python} #| echo: false train.head(2) ``` -1. Tokeniser chaque phrase avec `nltk`. Le `DataFrame` devrait maintenant avoir cet aspect : +Après tokenisation, il devrait avoir cet aspect : ```{python} #| include: true @@ -546,7 +650,7 @@ train_clean = (train train_clean.head(2) ``` -2. Retirer les stopwords avec `nltk`. +Après le retrait des stopwords, cela donnera: ```{python} #| include: false @@ -564,12 +668,20 @@ train_clean = (train_clean train_clean.head(2) ``` -{{% /box %}} -{{% box status="hint" title="Hint" icon="fa fa-lightbulb" %}} +::: {.cell .markdown} +```{=html} + +``` +::: + Ce petit nettoyage permet d'arriver à un texte plus intéressant en termes d'analyse lexicale. Par exemple, si on reproduit l'analyse précédente... : @@ -603,12 +715,13 @@ mettre en place les classes d'équivalence développées dans la mot par une forme canonique : * la **racinisation** (*stemming*) assez fruste mais rapide, notamment -en présence de fautes d’orthographe. Dans ce cas, chevaux peut devenir chev -mais être ainsi confondu avec chevet ou cheveux. Elles est généralement plus simple à mettre en oeuvre, quoique +en présence de fautes d’orthographe. Dans ce cas, _chevaux_ peut devenir _chev_ +mais être ainsi confondu avec _chevet_ ou _cheveux_. +Cette méthode est généralement plus simple à mettre en oeuvre, quoique plus fruste. * la **lemmatisation** qui requiert la connaissance des statuts -grammaticaux (exemple : chevaux devient cheval). +grammaticaux (exemple : _chevaux_ devient _cheval_). Elle est mise en oeuvre, comme toujours avec `nltk`, à travers un modèle. En l'occurrence, un `WordNetLemmatizer` (WordNet est une base lexicographique ouverte). Par exemple, les mots *"women"*, *"daughters"* @@ -622,7 +735,11 @@ for word in ["women","daughters", "leaves"]: print("The lemmatized form of %s is: {}".format(lemm.lemmatize(word)) % word) ``` -{{% box status="note" title="Note" icon="fa fa-comment" %}} +::: {.cell .markdown} +```{=html} + +``` +::: On va se restreindre au corpus d'Edgar Allan Poe et repartir de la base de données -brute : +brute: ```{python} eap_clean = train[train["Author"] == "EAP"] @@ -648,8 +765,24 @@ eap_clean = ' '.join(eap_clean['Text']) word_list = nltk.word_tokenize(eap_clean) ``` + +::: {.cell .markdown} +```{=html} + +``` +::: + +Le `WordNetLemmatizer` donnera le résultat suivant: + ```{python} #| include: false #| echo: false @@ -663,24 +796,36 @@ print("---------------------------") print(lemmatized_output[:209]) ``` -{{% /box %}} - +# TF-IDF: calcul de fréquence -## TF-IDF: calcul de fréquence - -Le calcul [tf-idf](https://fr.wikipedia.org/wiki/TF-IDF) (term frequency–inverse document frequency) permet de calculer un score de proximité entre un terme de recherche et un document (c'est ce que font les moteurs de recherche). +Le calcul [tf-idf](https://fr.wikipedia.org/wiki/TF-IDF) (term _frequency–inverse document frequency_) +permet de calculer un score de proximité entre un terme de recherche et un +document (c'est ce que font les moteurs de recherche). * La partie `tf` calcule une fonction croissante de la fréquence du terme de recherche dans le document à l'étude ; * La partie `idf` calcule une fonction inversement proportionnelle à la fréquence du terme dans l'ensemble des documents (ou corpus). -Le score total, obtenu en multipliant les deux composantes, permet ainsi de donner un score d'autant plus élevé que le terme est surréprésenté dans un document (par rapport à l'ensemble des documents). Il existe plusieurs fonctions, qui pénalisent plus ou moins les documents longs, ou qui sont plus ou moins *smooth*. +Le score total, obtenu en multipliant les deux composantes, +permet ainsi de donner un score d'autant plus élevé que le terme est surréprésenté dans un document +(par rapport à l'ensemble des documents). +Il existe plusieurs fonctions, qui pénalisent plus ou moins les documents longs, +ou qui sont plus ou moins *smooth*. -{{% box status="exercise" title="Exercice" icon="fas fa-pencil-alt" %}} +::: {.cell .markdown} +```{=html} + +``` +::: ```{python} #| include: false @@ -690,12 +835,12 @@ Le score total, obtenu en multipliant les deux composantes, permet ainsi de donn from sklearn.feature_extraction.text import TfidfVectorizer tfidf = TfidfVectorizer(stop_words=stopwords.words("english")) tfs = tfidf.fit_transform(train['Text']) +#print(tfs) ``` -2. Après avoir construit la matrice de documents x terms avec le code suivant, rechercher les lignes où les termes ayant la structure `abandon` sont non-nuls. - ```{python} -#| echo: false +#| echo: true + feature_names = tfidf.get_feature_names() corpus_index = [n for n in list(tfidf.vocabulary_.keys())] import pandas as pd @@ -704,7 +849,8 @@ df = pd.DataFrame(tfs.todense(), columns=feature_names) df.head() ``` -Les lignes sont les suivantes : +Les lignes où les termes de abandon sont non nuls +sont les suivantes : ```{python} #| include: true @@ -716,7 +862,6 @@ print(tempdf.index) tempdf.head(5) ``` -3. Trouver les 50 extraits où le score TF-IDF est le plus élevé et l'auteur associé. Vous devriez obtenir le classement suivant: ```{python} #| include: true @@ -733,30 +878,35 @@ Les 10 scores les plus élevés sont les suivants : print(train.iloc[list_fear[:9]]['Text'].values) ``` -{{% /box %}} - On remarque que les scores les plus élévés sont soient des extraits courts où le mot apparait une seule fois, soit des extraits plus longs où le mot fear apparaît plusieurs fois. -{{% box status="note" title="Note" icon="fa fa-comment" %}} -La matrice `document x terms` est un exemple typique de matrice sparse puisque, dans des corpus volumineux, une grande diversité de vocabulaire peut être trouvée. -{{% /box %}} +::: {.cell .markdown} +```{=html} + +``` +::: -## Approche contextuelle: les *n-gramms* -{{% box status="note" title="Note" icon="fa fa-comment" %}} -Pour être en mesure de mener cette analyse, il est nécessaire de télécharger un corpus supplémentaire : +# Approche contextuelle: les *n-gramms* -```{python, echo=TRUE, eval=TRUE} +Pour être en mesure de mener cette analyse, il est nécessaire de télécharger un corpus supplémentaire : + +```{python} import nltk nltk.download('genesis') nltk.corpus.genesis.words('english-web.txt') ``` -{{% /box %}} - Il s'agit maintenant de raffiner l'analyse. On s'intéresse non seulement aux mots et à leur fréquence, mais aussi aux mots qui suivent. Cette approche est essentielle pour désambiguiser les homonymes. Elle permet aussi d'affiner les modèles "bag-of-words". Le calcul de n-grams (bigrams pour les co-occurences de mots deux-à-deux, tri-grams pour les co-occurences trois-à-trois, etc.) constitue la méthode la plus simple pour tenir compte du contexte. @@ -768,27 +918,51 @@ On s'intéresse non seulement aux mots et à leur fréquence, mais aussi aux mot * les performances décroissent très rapidement en fonction de n, et les coûts de stockage des données augmentent rapidement (environ n fois plus élevé que la base de données initiale). -{{% box status="exercise" title="Exercice" icon="fas fa-pencil-alt" %}} - -**Exercice 7 : n-grams et contexte du mot fear** - On va, rapidement, regarder dans quel contexte apparaît le mot `fear` dans l'oeuvre d'Edgar Allan Poe (EAP). Pour cela, on transforme d'abord -le corpus EAP en tokens `nltk` : +le corpus EAP en tokens `nltk : ```{python} -#| include: false #| echo: true -eap_clean = train_clean[train_clean["Author"] == "EAP"] + +eap_clean = train[train["Author"] == "EAP"] eap_clean = ' '.join(eap_clean['Text']) -#Tokenisation naïve sur les espaces entre les mots => on obtient une liste de mots tokens = eap_clean.split() print(tokens[:10]) text = nltk.Text(tokens) print(text) ``` -1. Utiliser la méthode `concordance` pour afficher le contexte dans lequel apparaît le terme `fear`. La liste devrait ressembler à celle-ci: +Vous aurez besoin des fonctions ` BigramCollocationFinder.from_words` et `BigramAssocMeasures.likelihood_ratio` : + +```{python} +from nltk.collocations import BigramCollocationFinder +from nltk.metrics import BigramAssocMeasures +``` + +::: {.cell .markdown} +```{=html} + +``` +::: + + +Avec la méthode `concordance` (question 1), +la liste devrait ressembler à celle-ci: ```{python} #| include: true @@ -800,21 +974,12 @@ text.concordance("fear") print('\n') ``` -Même si on peut facilement voir le mot avant et après, cette liste est assez difficile à interpréter car elle recoupe beaucoup d'information. +Même si on peut facilement voir le mot avant et après, cette liste est assez difficile à interpréter car elle recoupe beaucoup d'informations. La `collocation` consiste à trouver les bi-grammes qui -apparaissent le plus fréquemment ensemble. Parmi toutes les paires de deux mots observées, il s'agit de sélectionner, à partir d'un modèle statistique, les "meilleures". - -2. Sélectionner et afficher les meilleures collocation, par exemple selon le critère du ratio de vraisemblance. - -Vous aurez besoin des fonctions ` BigramCollocationFinder.from_words` et `BigramAssocMeasures.likelihood_ratio` : - -```{python} -from nltk.collocations import BigramCollocationFinder -from nltk.metrics import BigramAssocMeasures -``` - - +apparaissent le plus fréquemment ensemble. Parmi toutes les paires de deux mots observées, +il s'agit de sélectionner, à partir d'un modèle statistique, les "meilleures". +On obtient donc avec cette méthode (question 2): ```{python} #| include: false @@ -825,11 +990,7 @@ bcf = BigramCollocationFinder.from_words(text) bcf.nbest(BigramAssocMeasures.likelihood_ratio, 20) ``` - - -Lorsque deux mots sont fortement associés, cela est parfois dû au fait qu'ils apparaissent rarement. Il est donc parfois nécessaire d'appliquer des filtres, par exemple ignorer les bigrammes qui apparaissent moins de 5 fois dans le corpus. - -3. Refaire la question précédente en utilisant toujours un modèle `BigramCollocationFinder` suivi de la méthode `apply_freq_filter` pour ne conserver que les bigrammes présents au moins 5 fois. Puis, au lieu d'utiliser la méthode de maximum de vraisemblance, testez la méthode `nltk.collocations.BigramAssocMeasures().jaccard`. +Si on modélise les meilleures collocations: ```{python} #| include: false @@ -846,9 +1007,11 @@ for collocation in collocations: print(c) ``` -Cette liste a un peu plus de sens, on a des noms de personnages, de lieux mais aussi des termes fréquemment employés ensemble (*Chess Player* par exemple). +Cette liste a un peu plus de sens, +on a des noms de personnages, de lieux mais aussi des termes fréquemment employés ensemble +(*Chess Player* par exemple). -4. Ne s'intéresser qu'aux *collocations* qui concernent le mot *fear* +En ce qui concerne les _collocations_ du mot fear: ```{python} #| include: false @@ -877,7 +1040,5 @@ Si on mène la même analyse pour le terme *love*, on remarque que de manière l collocations_word("love") ``` -{{% /box %}} - - +# Références diff --git a/content/course/NLP/_index.md b/content/course/NLP/_index.md index c094db05d..1240491c5 100644 --- a/content/course/NLP/_index.md +++ b/content/course/NLP/_index.md @@ -1,19 +1,19 @@ --- -title: "Partie 4: Natural Language Processing (NLP)" +title: "Partie 4 : Natural Language Processing (NLP)" date: 2020-10-14T13:00:00Z draft: false weight: 39 slug: "nlp" icon: book icon_pack: fas -#linktitle: "Partie 4: Natural Language Processing (NLP)" +#linktitle: "Partie 4 : Natural Language Processing (NLP)" summary: | L'un des grands avantages comparatifs de Python par rapport aux langages concurrents (R notamment) est dans la richesse des librairies de Traitement du Langage Naturel (mieux connu sous son acronyme anglais : NLP pour natural langage processing). Cette partie vise à illustrer la richesse de cet écosystème à partir - de quelques exemples littéraires: Dumas, Poe, Shelley, Lovecraft. + de quelques exemples littéraires : Dumas, Poe, Shelley, Lovecraft. type: book --- @@ -22,7 +22,7 @@ des exemples de :books: pour s'amuser. Dans un premier temps, cette partie propose d'explorer *bag of words* pour montrer comment transformer un corpus en outil propre à une -analyse statistique: +analyse statistique : * Elle propose d'abord une introduction aux enjeux du nettoyage des données textuelles à travers l'analyse du *Comte de Monte Cristo* d'Alexandre Dumas diff --git a/reference.bib b/reference.bib index a46f85184..b42129826 100644 --- a/reference.bib +++ b/reference.bib @@ -17,6 +17,13 @@ @article{athey2019machine publisher={Annual Reviews} } +@article{galianafuzzy, + title={Fuzzy matching on big-data An illustration with scanner data and crowd-sourced nutritional data}, + author={Galiana, Lino and Castillo, Milena Suarez}, + year={2022}, + publisher={Proceedings of the 2022 "Journées de Méthodologie Statistiques"} +} + @@ -27,6 +34,13 @@ @book{mckinney2012python publisher={" O'Reilly Media, Inc."} } +@article{galiana2020segregation, + title={Residential segregation, daytime segregation and spatial frictions: an analysis from mobile phone data }, + author={Galiana, Lino and S{\'e}m{\'e}curbe, Fran{\c{c}}ois and Sakarovitch, Benjamin and Smoreda, Zbigniew}, + year={2020}, + publisher={Insee Working Paper} +} + @inproceedings{galiana2022, author = {Galiana, Lino and Suarez Castillo, Milena}, title = {Fuzzy Matching on Big-Data: An Illustration with Scanner and Crowd-Sourced Nutritional Datasets},