-
Notifications
You must be signed in to change notification settings - Fork 45
/
02_exoclean.qmd
1051 lines (790 loc) · 31.6 KB
/
02_exoclean.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: "Nettoyer un texte: des exercices pour découvrir l'approche bag-of-words"
date: 2020-10-29T13:00:00Z
draft: false
weight: 20
slug: nlpexo
tags:
- NLP
- nltk
- Littérature
- preprocessing
- Exercice
categories:
- NLP
- Exercice
type: book
description: |
Ce chapitre continue de présenter l'approche de __nettoyage de données__
du `NLP` en s'appuyant sur le corpus de trois auteurs
anglo-saxons : Mary Shelley, Edgar Allan Poe, H.P. Lovecraft.
Dans cette série d'exercice nous mettons en oeuvre de manière
plus approfondie les différentes méthodes présentées
précédemment.
bibliography: ../../reference.bib
image: featured_nlp_exo.png
---
::: {.cell .markdown}
```{python}
#| echo: false
#| output: 'asis'
#| include: true
#| eval: true
import sys
sys.path.insert(1, '../../') #insert the utils module
from utils import print_badges
#print_badges(__file__)
print_badges("content/NLP/02_exoclean.qmd")
```
:::
Cette page approfondit certains aspects présentés dans la
[partie introductive](#nlp). Après avoir travaillé sur le
*Comte de Monte Cristo*, on va continuer notre exploration de la littérature
avec cette fois des auteurs anglophones :
* Edgar Allan Poe, (EAP) ;
* HP Lovecraft (HPL) ;
* Mary Wollstonecraft Shelley (MWS).
Les données sont disponibles ici : [spooky.csv](https://github.com/GU4243-ADS/spring2018-project1-ginnyqg/blob/master/data/spooky.csv) et peuvent être requétées via l'url
<https://github.com/GU4243-ADS/spring2018-project1-ginnyqg/raw/master/data/spooky.csv>.
Le but va être dans un premier temps de regarder dans le détail les termes les plus fréquemment utilisés par les auteurs, de les représenter graphiquement.
On prendra appui sur l'approche *bag of words* présentée dans le chapitre précédent[^1].
[^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. [@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`
Ce notebook est librement inspiré de :
* https://www.kaggle.com/enerrio/scary-nlp-with-spacy-and-keras
* https://github.com/GU4243-ADS/spring2018-project1-ginnyqg
* 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é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:
:::{#fig-relevanc-table-embedding}
![](word_embedding.png)
Illustration de l'intérêt des _embeddings_ [@galianafuzzy]
:::
## 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/)
* [Keras](https://keras.io/)
* [TensorFlow](https://www.tensorflow.org/)
Il faudra également installer les librairies `gensim` et `pywaffle`
::: {.cell .markdown}
```{=html}
<div class="alert alert-warning" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-lightbulb"></i> Hint</h3>
```
Comme dans la [partie précédente](#nlp), il faut télécharger quelques éléments pour que `NTLK` puisse fonctionner correctement. Pour cela, faire :
~~~python
import nltk
nltk.download('stopwords')
nltk.download('punkt')
nltk.download('genesis')
nltk.download('wordnet')
nltk.download('omw-1.4')
~~~
```{=html}
</div>
```
:::
La liste des modules à importer est assez longue, la voici :
```{python}
#| output: hide
#| echo: true
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from wordcloud import WordCloud
import base64
import string
import re
import nltk
from collections import Counter
from time import time
# from sklearn.feature_extraction.stop_words import ENGLISH_STOP_WORDS as stopwords
from sklearn.metrics import log_loss
import matplotlib.pyplot as plt
#!pip install pywaffle
from pywaffle import Waffle
from nltk.stem import WordNetLemmatizer
from sklearn.feature_extraction.text import TfidfVectorizer, CountVectorizer
from sklearn.decomposition import NMF, LatentDirichletAllocation
nltk.download('stopwords')
nltk.download('punkt')
nltk.download('genesis')
nltk.download('wordnet')
nltk.download('omw-1.4')
```
## Données utilisées
::: {.cell .markdown}
```{=html}
<div class="alert alert-success" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> Exercice 1 : Importer les données spooky</h3>
```
*Pour ceux qui ont envie de tester leurs connaissances en pandas*
1. Importer le jeu de données `spooky` à partir de l'URL <https://github.com/GU4243-ADS/spring2018-project1-ginnyqg/raw/master/data/spooky.csv> sous le nom `train`. L'encoding est `latin-1`
2. Mettre des majuscules au nom des colonnes.
3. Retirer le prefix `id` de la colonne `Id` et appeler la nouvelle colonne `ID`.
4. Mettre l'ancienne colonne `Id` en index.
```{=html}
</div>
```
:::
```{python}
#| echo: false
import pandas as pd
```
```{python}
#| include: false
#| echo: false
#1. Import des données
url='https://github.com/GU4243-ADS/spring2018-project1-ginnyqg/raw/master/data/spooky.csv'
train = pd.read_csv(url,encoding='latin-1')
```
```{python}
#| include: false
#| echo: false
#2. Majuscules aux noms des colonnes
train.columns = train.columns.str.capitalize()
```
```{python}
#| include: false
#| echo: false
#3. Retirer le prefixe id
train['ID'] = train['Id'].str.replace("id","")
```
```{python}
#| include: false
#| echo: false
#4. Mettre Id en index
train = train.set_index('Id')
#train.head()
```
Si vous ne faites pas l'exercice 1, pensez à charger les données en executant la fonction `get_data.py` :
```{python}
import requests
url = 'https://raw.githubusercontent.com/linogaliana/python-datascientist/master/content/NLP/get_data.py'
r = requests.get(url, allow_redirects=True)
open('getdata.py', 'wb').write(r.content)
import getdata
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}
train.head()
```
```{python}
#| echo: false
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.
```{python}
#| echo: true
fig = plt.figure()
g = sns.barplot(x=['Edgar Allen Poe', 'Mary W. Shelley', 'H.P. Lovecraft'], y=train['Author'].value_counts())
```
::: {.cell .markdown}
```{=html}
<div class="alert alert-info" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-comment"></i> Note</h3>
```
L'approche *bag of words* est présentée de
manière plus extensive dans le [chapitre précédent](#nlp).
L'idée est d'étudier la fréquence des mots d'un document et la
surreprésentation des mots par rapport à un document de
référence (appelé *corpus*).
Cette approche un peu simpliste mais très
efficace : on peut calculer des scores permettant par exemple de faire
de classification automatique de document par thème, de comparer la
similarité de deux documents. Elle est souvent utilisée en première analyse,
et elle reste la référence pour l'analyse de textes mal
structurés (tweets, dialogue tchat, etc.).
Les analyses tf-idf (*term frequency-inverse document frequency*) ou les
constructions d'indices de similarité cosinus reposent sur ce type d'approche.
```{=html}
</div>
```
:::
### Fréquence d'un mot
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*.
::: {.cell .markdown}
```{=html}
<div class="alert alert-comment" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-comment"></i> Note</h3>
```
L'exercice ci-dessous présente une représentation graphique nommée
*waffle chart*. Il s'agit d'une approche préférable aux
camemberts qui sont des graphiques manipulables car l'oeil humain se laisse
facilement berner par cette représentation graphique qui ne respecte pas
les proportions.
```{=html}
</div>
```
:::
::: {.cell .markdown}
```{=html}
<div class="alert alert-success" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> Exercice 2 : Fréquence d'un mot</h3>
```
1. Compter le nombre de phrases, pour chaque auteur, où apparaît le mot `fear`.
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.
3. Refaire l'analyse avec le mot *"horror"*.
```{=html}
</div>
```
:::
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_data.groupby('Author').sum()
data = table.to_dict()['wordtoplot']
return table
table = nb_occurrences("fear", train)
```
```{python}
#| echo: false
table.head()
```
```{python}
#| include: false
#| echo: false
#2. Faire un graphique d'occurences avc pywaffle
def graph_occurrence(word, train_data):
table = nb_occurrences(word, train_data)
data = table.to_dict()['wordtoplot']
fig = plt.figure(
FigureClass=Waffle,
rows=15,
values=data,
title={'label': 'Utilisation du mot "%s" par les auteurs' %word, 'loc': 'left'},
labels=["{0} ({1})".format(k, v) for k, v in data.items()]
)
return fig
fig = graph_occurrence("fear", train)
```
Ceci permet d'obtenir le _waffle chart_ suivant :
```{python}
#| echo: false
#| label: fig-waffle-fear
#| fig-cap: "Répartition du terme fear dans le corpus de nos trois auteurs"
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_nlp_exo.png")
```
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
#| echo: false
#3. Graphe d'occurences avec le mot horror
fig = graph_occurrence("horror", train)
```
```{python}
#| echo: false
fig.get_figure()
```
### 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.
::: {.cell .markdown}
```{=html}
<div class="alert alert-success" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> Exercice 3 : Wordcloud</h3>
```
1. En utilisant la fonction `wordCloud`, faire trois nuages de mot pour représenter les mots les plus utilisés par chaque auteur.
2. Calculer les 25 mots plus communs pour chaque auteur et représenter les trois histogrammes des décomptes.
```{=html}
</div>
```
:::
```{python}
#| include: false
#| echo: false
#1. Wordclouds
def graph_wordcloud(author, train_data, varname = "Text"):
txt = train_data[train_data['Author']==author][varname]
all_text = ' '.join([text for text in txt])
wordcloud = WordCloud(width=800, height=500,
random_state=21,
max_words=2000,
background_color = "white",
colormap='Set2').generate(all_text)
return wordcloud
n_topics = ["HPL","EAP","MWS"]
fig = plt.figure(figsize=(15, 12))
for i in range(len(n_topics)):
ax = fig.add_subplot(2,2,i+1)
wordcloud = graph_wordcloud(n_topics[i], train)
ax.imshow(wordcloud)
ax.axis('off')
```
Le _wordcloud_ pour nos différents auteurs est le suivant :
```{python}
#| echo: false
fig.get_figure()
```
Enfin, si on fait un histogramme des fréquences,
cela donnera :
```{python}
#| include: false
#| echo: false
#2. Histogramme de décompte
count_words = pd.DataFrame({'counter' : train
.groupby('Author')
.apply(lambda s: ' '.join(s['Text']).split())
.apply(lambda s: Counter(s))
.apply(lambda s: s.most_common(25))
.explode()}
)
count_words[['word','count']] = pd.DataFrame(count_words['counter'].tolist(),
index=count_words.index)
count_words = count_words.reset_index()
g = sns.FacetGrid(count_words, row="Author")
g.map_dataframe(sns.barplot, x="word", y="count")
```
```{python}
#| echo: false
g.figure.get_figure()
```
On voit ici que ce sont des mots communs, comme *"the"*, *"of"*, etc. sont très
présents. Mais ils sont peu porteurs d'information, on peut donc les éliminer
avant de faire une analyse syntaxique poussée.
Ceci est une démonstration par l'exemple qu'il vaut mieux nettoyer le texte avant de
l'analyser (sauf si on est intéressé
par la loi de Zipf, cf. exercice suivant).
### Aparté : la loi de Zipf
::: {.cell .markdown}
```{=html}
<div class="alert alert-warning" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> La loi de Zipf</h3>
```
Dans son sens strict, la loi de Zipf prévoit que
dans un texte donné, la fréquence d'occurrence $f(n_i)$ d'un mot est
liée à son rang $n_i$ dans l'ordre des fréquences par une loi de la forme
$f(n_i) = c/n_i$ où $c$ est une constante. Zipf, dans les années 1930, se basait sur l'oeuvre
de Joyce, *Ulysse* pour cette affirmation.
Plus généralement, on peut dériver la loi de Zipf d'une distribution exponentielle des fréquences : $f(n_i) = cn_{i}^{-k}$. Cela permet d'utiliser la famille des modèles linéaires généralisés, notamment les régressions poissonniennes, pour mesurer les paramètres de la loi. Les modèles linéaire traditionnels en `log` souffrent en effet, dans ce contexte, de biais (la loi de Zipf est un cas particulier d'un modèle gravitaire, où appliquer des OLS est une mauvaise idée, cf. [@galiana2020segregation](https://linogaliana.netlify.app/publication/2020-segregation/) pour les limites).
```{=html}
</div>
```
:::
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 :
```{python}
count_words = pd.DataFrame({'counter' : train
.groupby('Author')
.apply(lambda s: ' '.join(s['Text']).split())
.apply(lambda s: Counter(s))
.apply(lambda s: s.most_common())
.explode()}
)
count_words[['word','count']] = pd.DataFrame(count_words['counter'].tolist(), index=count_words.index)
count_words = count_words.reset_index()
count_words = count_words.assign(
tot_mots_auteur = lambda x: (x.groupby("Author")['count'].transform('sum')),
freq = lambda x: x['count'] / x['tot_mots_auteur'],
rank = lambda x: x.groupby("Author")['count'].transform('rank', ascending = False)
)
```
Commençons par représenter la relation entre la fréquence et le rang:
```{python}
#| include: false
#| echo: true
g = sns.lmplot(y = "freq", x = "rank", hue = 'Author', data = count_words, fit_reg = False)
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}
import statsmodels.api as sm
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())
```
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.
## 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 :
* suppression de la ponctuation
* 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).
::: {.cell .markdown}
```{=html}
<div class="alert alert-success" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> Exercice 4 : Nettoyage du texte</h3>
```
Repartir de `train`, notre jeu de données d'entraînement. Pour rappel, `train` a la structure suivante :
1. Tokeniser chaque phrase avec `nltk`.
2. Retirer les stopwords avec `nltk`.
```{=html}
</div>
```
:::
Pour rappel, au début de l'exercice, le `DataFrame` présente l'aspect suivant :
```{python}
#| echo: false
train.head(2)
```
Après tokenisation, il devrait avoir cet aspect :
```{python}
#| include: true
#| echo: false
#1. Tokenisation
train_clean = (train
.groupby(["ID","Author"])
.apply(lambda s: nltk.word_tokenize(' '.join(s['Text'])))
.apply(lambda words: [word for word in words if word.isalpha()])
)
train_clean.head(2)
```
Après le retrait des stopwords, cela donnera :
```{python}
#| include: false
#| echo: false
#2. Enlever les stopwords.
from nltk.corpus import stopwords
stop_words = set(stopwords.words('english'))
train_clean = (train_clean
.apply(lambda words: [w for w in words if not w in stop_words])
.reset_index(name='tokenized')
)
train_clean.head(2)
```
::: {.cell .markdown}
```{=html}
<div class="alert alert-warning" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-lightbulb"></i> Hint</h3>
```
La méthode `apply` est très pratique ici car nous avons une phrase par ligne. Plutôt que de faire un `DataFrame` par auteur, ce qui n'est pas une approche très flexible, on peut directement appliquer la tokenisation
sur notre `DataFrame` grâce à `apply`, sans le diviser.
```{=html}
</div>
```
:::
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... :
```{python}
#| include: false
#| echo: true
train_clean["Text"] = train_clean['tokenized'].apply(lambda s: " ".join(map(str, s)))
n_topics = ["HPL","EAP","MWS"]
fig = plt.figure(figsize=(15, 12))
for i in range(len(n_topics)):
ax = fig.add_subplot(2,2,i+1)
wordcloud = graph_wordcloud(n_topics[i], train_clean)
ax.imshow(wordcloud)
ax.axis('off')
fig
```
```{python}
#| echo: false
fig.get_figure()
```
Pour aller plus loin dans l'harmonisation d'un texte, il est possible de
mettre en place les classes d'équivalence développées dans la
[partie précédente](#nlp) afin de remplacer différentes variations d'un même
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_.
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_).
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"*
et *"leaves"* seront ainsi lemmatisés de la manière suivante :
```{python}
from nltk.stem import WordNetLemmatizer
lemm = WordNetLemmatizer()
for word in ["women","daughters", "leaves"]:
print("The lemmatized form of %s is: {}".format(lemm.lemmatize(word)) % word)
```
::: {.cell .markdown}
```{=html}
<div class="alert alert-info" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-comment"></i> Note</h3>
```
Pour disposer du corpus nécessaire à la lemmatisation, il faut, la première fois,
télécharger celui-ci grâce aux commandes suivantes :
~~~python
import nltk
nltk.download('wordnet')
nltk.download('omw-1.4')
~~~
```{=html}
</div>
```
:::
On va se restreindre au corpus d'Edgar Allan Poe et repartir de la base de données
brute:
```{python}
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()
word_list = nltk.word_tokenize(eap_clean)
```
::: {.cell .markdown}
```{=html}
<div class="alert alert-warning" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> Exercice 5 : Lemmatisation avec nltk</h3>
```
Utiliser un `WordNetLemmatizer` et observer le résultat.
Optionnel: Effectuer la même tâche avec `spaCy`
```{=html}
</div>
```
:::
Le `WordNetLemmatizer` donnera le résultat suivant :
```{python}
#| include: false
#| echo: false
#Exercice 5 : WordNetLemmatizer
lemmatizer = WordNetLemmatizer()
lemmatized_output = ' '.join([lemmatizer.lemmatize(w) for w in word_list])
print(" ".join(word_list[:43]))
print("---------------------------")
print(lemmatized_output[:209])
```
## 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).
* 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*.
::: {.cell .markdown}
```{=html}
<div class="alert alert-success" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> Exercice 6 : TF-IDF: calcul de fréquence</h3>
```
1. Utiliser le vectoriseur TF-IdF de `scikit-learn` pour transformer notre corpus en une matrice `document x terms`. Au passage, utiliser l'option `stop_words` pour ne pas provoquer une inflation de la taille de la matrice. Nommer le modèle `tfidf` et le jeu entraîné `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.
3. Trouver les 50 extraits où le score TF-IDF est le plus élevé et l'auteur associé. Vous devriez obtenir le classement suivant :
```{=html}
</div>
```
:::
```{python}
#| include: false
#| echo: false
#1. TfIdf de scikit
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(stop_words=stopwords.words("english"))
tfs = tfidf.fit_transform(train['Text'])
#print(tfs)
```
```{python}
#| echo: true
feature_names = tfidf.get_feature_names_out()
corpus_index = [n for n in list(tfidf.vocabulary_.keys())]
import pandas as pd
df = pd.DataFrame(tfs.todense(), columns=feature_names)
df.head()
```
Les lignes où les termes de abandon sont non nuls
sont les suivantes :
```{python}
#| include: true
#| echo: false
#2. Lignes où les termes de abandon sont non nuls.
tempdf = df.loc[(df.filter(regex = "abandon")!=0).any(axis=1)]
print(tempdf.index)
tempdf.head(5)
```
```{python}
#| include: true
#| echo: false
#3. 50 extraits avec le TF-IDF le plus élevé.
list_fear = df["fear"].sort_values(ascending =False).head(n=50).index.tolist()
train.iloc[list_fear].groupby('Author').count()['Text'].sort_values(ascending = False)
```
Les 10 scores les plus élevés sont les suivants :
```{python}
print(train.iloc[list_fear[:9]]['Text'].values)
```
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.
::: {.cell .markdown}
```{=html}
<div class="alert alert-info" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-comment"></i> Note</h3>
```
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.
```{=html}
</div>
```
:::
## Approche contextuelle: les *n-gramms*
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')
```
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.
`nltk` offre des methodes pour tenir compte du contexte : pour ce faire, nous calculons les n-grams, c'est-à-dire l'ensemble des co-occurrences successives de mots n-à-n. En général, on se contente de bi-grams, au mieux de tri-grams :
* les modèles de classification, analyse du sentiment, comparaison de documents, etc. qui comparent des n-grams avec n trop grands sont rapidement confrontés au problème de données sparse, cela réduit la capacité prédictive des modèles ;
* 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).
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 :
```{python}
#| echo: true
eap_clean = train[train["Author"] == "EAP"]
eap_clean = ' '.join(eap_clean['Text'])
tokens = eap_clean.split()
print(tokens[:10])
text = nltk.Text(tokens)
print(text)
```
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}
<div class="alert alert-success" role="alert">
<h3 class="alert-heading"><i class="fa-solid fa-pencil"></i> Exercice 7 : n-grams et contexte du mot fear</h3>
```
1. Utiliser la méthode `concordance` pour afficher le contexte dans lequel apparaît le terme `fear`.
2. Sélectionner et afficher les meilleures collocation, par exemple selon le critère du ratio de vraisemblance.
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`.
4. Ne s'intéresser qu'aux *collocations* qui concernent le mot *fear*
```{=html}
</div>
```
:::
Avec la méthode `concordance` (question 1),
la liste devrait ressembler à celle-ci:
```{python}
#| include: true
#| echo: false
# 1. Methode concordance
print("Exemples d'occurences du terme 'fear' :")
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'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".
On obtient donc avec cette méthode (question 2):
```{python}
#| include: false
#| echo: false
# 2. Modélisation des meilleures collocations
bcf = BigramCollocationFinder.from_words(text)
bcf.nbest(BigramAssocMeasures.likelihood_ratio, 20)
```
Si on modélise les meilleures collocations: