.. _td2asomenlprst: ================================= 2A.ml - Texte et machine learning ================================= .. only:: html **Links:** :download:`notebook `, :downloadlink:`html `, :download:`python `, :downloadlink:`slides `, :githublink:`GitHub|_doc/notebooks/td2a/td2a_some_nlp.ipynb|*` Revue de méthodes de `word embedding `__ statistiques (~ `NLP `__) ou comment transformer une information textuelle en vecteurs dans un espace vectoriel (*features*) ? Deux exercices sont ajoutés à la fin. .. code:: ipython3 from jyquickhelper import add_notebook_menu add_notebook_menu() .. contents:: :local: Données ------- Nous allons travailler sur des données twitter collectées avec le mot-clé macron : `tweets_macron_sijetaispresident_201609.zip `__. .. code:: ipython3 from ensae_teaching_cs.data import twitter_zip df = twitter_zip(as_df=True) df.head(n=2).T .. raw:: html
0 1
index 776066992054861825 776067660979245056
nb_user_mentions 0 0
nb_extended_entities 0 0
nb_hashtags 1 1
geo NaN NaN
text_hashtags , SiJétaisPrésident , SiJétaisPrésident
annee 2016.0 2016.0
delimit_mention NaN NaN
lang fr fr
id_str 776066992054861824.0 776067660979245056.0
text_mention NaN NaN
retweet_count 4.0 5.0
favorite_count 3.0 8.0
type_extended_entities [] []
text #SiJétaisPrésident se serait la fin du monde..... #SiJétaisPrésident je donnerai plus de vacance...
nb_user_photos 0.0 0.0
nb_urls 0.0 0.0
nb_symbols 0.0 0.0
created_at Wed Sep 14 14:36:04 +0000 2016 Wed Sep 14 14:38:43 +0000 2016
delimit_hash , 0, 18 , 0, 18
.. code:: ipython3 df.shape .. parsed-literal:: (5088, 20) 5000 tweets n’est pas assez pour tirer des conclusions mais cela donne une idée. On supprime les valeurs manquantes. .. code:: ipython3 data = df[["retweet_count", "text"]].dropna() data.shape .. parsed-literal:: (5087, 2) Construire une pondération -------------------------- Le texte est toujours délicat à traiter. Il n’est pas toujours évident de sortir d’une information binaire : un mot est-il présent ou pas. Les mots n’ont aucun sens numérique. Une liste de tweets n’a pas beaucoup de sens à part les trier par une autre colonne : les retweet par exemple. .. code:: ipython3 data.sort_values("retweet_count", ascending=False).head() .. raw:: html
retweet_count text
2038 842.0 #SiJetaisPresident travailler moins pour gagne...
2453 816.0 #SiJetaisPresident je ferais revenir l'été ave...
2627 529.0 #SiJetaisPresident le mcdo livrerai à domicile
1402 289.0 #SiJetaisPresident les devoirs ça serait de re...
2198 276.0 #SiJetaisPresident ? Président c'est pour les...
Sans cette colonne qui mesure la popularité, il faut trouver un moyen d’extraire de l’information. On découpe alors en mots et on constuire un modèle de langage : les `n-grammes `__. Si un tweet est constitué de la séquence de mots :math:`(m_1, m_2, ..., m_k)`. On définit sa probabilité comme : .. math:: P(tweet) = P(w_1, w_2) P(w_3 | w_2, w_1) P(w_4 | w_3, w_2) ... P(w_k | w_{k-1}, w_{k-2}) Dans ce cas, :math:`n=3` car on suppose que la probabilité d’apparition d’un mot ne dépend que des deux précédents. On estime chaque n-grammes comme suit : .. math:: P(c | a, b) = \frac{ \# (a, b, c)}{ \# (a, b)} C’est le nombre de fois où on observe la séquence :math:`(a,b,c)` divisé par le nombre de fois où on observe la séquence :math:`(a,b)`. Tokenisation ~~~~~~~~~~~~ Découper en mots paraît simple ``tweet.split()`` et puis il y a toujours des surprises avec le texte, la prise en compte des tirets, les majuscules, les espaces en trop. On utilse un *tokenizer* dédié : `TweetTokenizer `__ ou un tokenizer qui prend en compte le langage. .. code:: ipython3 from nltk.tokenize import TweetTokenizer tknzr = TweetTokenizer(preserve_case=False) tokens = tknzr.tokenize(data.loc[0, "text"]) tokens .. parsed-literal:: ['#sijétaisprésident', 'se', 'serait', 'la', 'fin', 'du', 'monde', '...', 'mdr', '😂'] n-grammes ~~~~~~~~~ - `N-Gram-Based Text Categorization: Categorizing Text With Python `__ .. code:: ipython3 from nltk.util import ngrams generated_ngrams = ngrams(tokens, 4, pad_left=True, pad_right=True) list(generated_ngrams) .. parsed-literal:: [(None, None, None, '#sijétaisprésident'), (None, None, '#sijétaisprésident', 'se'), (None, '#sijétaisprésident', 'se', 'serait'), ('#sijétaisprésident', 'se', 'serait', 'la'), ('se', 'serait', 'la', 'fin'), ('serait', 'la', 'fin', 'du'), ('la', 'fin', 'du', 'monde'), ('fin', 'du', 'monde', '...'), ('du', 'monde', '...', 'mdr'), ('monde', '...', 'mdr', '😂'), ('...', 'mdr', '😂', None), ('mdr', '😂', None, None), ('😂', None, None, None)] Exercice 1 : calculer des n-grammes sur les tweets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Nettoyage ~~~~~~~~~ Tous les modèles sont plus stables sans les stop-words, c’est-à-dire tous les mots présents dans n’importe quel documents et qui n’apporte pas de sens (à, de, le, la, …). Souvent, on enlève les accents, la ponctuation… Moins de variabilité signifie des statistiques plus fiable. Exercice 2 : nettoyer les tweets ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Voir `stem `__. Structure de graphe ------------------- On cherche cette fois-ci à construire des coordonnées pour chaque tweet. matrice d’adjacence ~~~~~~~~~~~~~~~~~~~ Une option courante est de découper chaque expression en mots puis de créer une matrice *expression x mot* ou chaque case indique la présence d’un mot dans une expression. .. code:: ipython3 from sklearn.feature_extraction.text import CountVectorizer count_vect = CountVectorizer() counts = count_vect.fit_transform(data["text"]) counts.shape .. parsed-literal:: (5087, 11924) On aboutit à une matrice sparse ou chaque expression est représentée à une vecteur ou chaque 1 représente l’appartenance d’un mot à l’ensemble. .. code:: ipython3 type(counts) .. parsed-literal:: scipy.sparse.csr.csr_matrix .. code:: ipython3 counts[:5,:5].toarray() .. parsed-literal:: array([[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]], dtype=int64) .. code:: ipython3 data.loc[0,"text"] .. parsed-literal:: '#SiJétaisPrésident se serait la fin du monde... mdr 😂' .. code:: ipython3 counts[0,:].sum() .. parsed-literal:: 8 td-idf ~~~~~~ Ce genre de technique produit des matrices de très grande dimension qu’il faut réduire. On peut enlever les mots rares ou les mots très fréquents. `td-idf `__ est une technique qui vient des moteurs de recherche. Elle construit le même type de matrice (même dimension) mais associe à chaque couple (document - mot) un poids qui dépend de la fréquence d’un mot globalement et du nombre de documents contenant ce mot. .. math:: idf(t) = \log \frac{\# D}{\#\{d \; | \; t \in d \}} Où : - :math:`\#D` est le nombre de tweets - :math:`\#\{d \; | \; t \in d \}` est le nombre de tweets contenant le mot :math:`t` :math:`f(t,d)` est le nombre d’occurences d’un mot :math:`t` dans un document :math:`d`. .. math:: tf(t,d) = \frac{1}{2} + \frac{1}{2} \frac{f(t,d)}{\max_{t' \in d} f(t',d)} On construit le nombre :math:`tfidf(t,f)` .. math:: tdidf(t,d) = tf(t,d) idf(t) Le terme :math:`idf(t)` favorise les mots présent dans peu de documents, le terme :math:`tf(t,f)` favorise les termes répétés un grand nombre de fois dans le même document. On applique à la matrice précédente. .. code:: ipython3 from sklearn.feature_extraction.text import TfidfTransformer tfidf = TfidfTransformer() res = tfidf.fit_transform(counts) res.shape .. parsed-literal:: (5087, 11924) .. code:: ipython3 res[0,:].sum() .. parsed-literal:: 2.6988143126521047 Exercice 3 : tf-idf sans mot-clés ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ La matrice ainsi créée est de grande dimension. Il faut trouver un moyen de la réduire avec `TfidfVectorizer `__. word2vec ~~~~~~~~ - `word2vec From theory to practice `__ - `Efficient Estimation of Word Representations in Vector Space `__ - `word2vec `__ Cet algorithme part d’une répresentation des mots sous forme de vecteur en un espace de dimension N = le nombre de mots distinct. Un mot est représenté par :math:`(0,0, ..., 0, 1, 0, ..., 0)`. L’astuce consiste à réduire le nombre de dimensions en compressant avec une ACP, un réseau de neurones non linéaires. .. code:: ipython3 sentences = [tknzr.tokenize(_) for _ in data["text"]] sentences[0] .. parsed-literal:: ['#sijétaisprésident', 'se', 'serait', 'la', 'fin', 'du', 'monde', '...', 'mdr', '😂'] .. code:: ipython3 import gensim, logging logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO) model = gensim.models.Word2Vec(sentences, min_count=1) .. parsed-literal:: 2022-02-12 18:46:39,284 : INFO : collecting all words and their counts 2022-02-12 18:46:39,284 : INFO : PROGRESS: at sentence #0, processed 0 words, keeping 0 word types 2022-02-12 18:46:39,331 : INFO : collected 13279 word types from a corpus of 76421 raw words and 5087 sentences 2022-02-12 18:46:39,332 : INFO : Creating a fresh vocabulary 2022-02-12 18:46:39,400 : INFO : Word2Vec lifecycle event {'msg': 'effective_min_count=1 retains 13279 unique words (100.0%% of original 13279, drops 0)', 'datetime': '2022-02-12T18:46:39.388519', 'gensim': '4.1.2', 'python': '3.9.5 (tags/v3.9.5:0a7dcbd, May 3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)]', 'platform': 'Windows-10-10.0.19043-SP0', 'event': 'prepare_vocab'} 2022-02-12 18:46:39,402 : INFO : Word2Vec lifecycle event {'msg': 'effective_min_count=1 leaves 76421 word corpus (100.0%% of original 76421, drops 0)', 'datetime': '2022-02-12T18:46:39.401509', 'gensim': '4.1.2', 'python': '3.9.5 (tags/v3.9.5:0a7dcbd, May 3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)]', 'platform': 'Windows-10-10.0.19043-SP0', 'event': 'prepare_vocab'} 2022-02-12 18:46:39,498 : INFO : deleting the raw counts dictionary of 13279 items 2022-02-12 18:46:39,498 : INFO : sample=0.001 downsamples 46 most-common words 2022-02-12 18:46:39,498 : INFO : Word2Vec lifecycle event {'msg': 'downsampling leaves estimated 56028.0861159631 word corpus (73.3%% of prior 76421)', 'datetime': '2022-02-12T18:46:39.498380', 'gensim': '4.1.2', 'python': '3.9.5 (tags/v3.9.5:0a7dcbd, May 3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)]', 'platform': 'Windows-10-10.0.19043-SP0', 'event': 'prepare_vocab'} 2022-02-12 18:46:39,663 : INFO : estimated required memory for 13279 words and 100 dimensions: 17262700 bytes 2022-02-12 18:46:39,663 : INFO : resetting layer weights 2022-02-12 18:46:39,679 : INFO : Word2Vec lifecycle event {'update': False, 'trim_rule': 'None', 'datetime': '2022-02-12T18:46:39.678678', 'gensim': '4.1.2', 'python': '3.9.5 (tags/v3.9.5:0a7dcbd, May 3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)]', 'platform': 'Windows-10-10.0.19043-SP0', 'event': 'build_vocab'} 2022-02-12 18:46:39,680 : INFO : Word2Vec lifecycle event {'msg': 'training model with 3 workers on 13279 vocabulary and 100 features, using sg=0 hs=0 sample=0.001 negative=5 window=5 shrink_windows=True', 'datetime': '2022-02-12T18:46:39.680669', 'gensim': '4.1.2', 'python': '3.9.5 (tags/v3.9.5:0a7dcbd, May 3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)]', 'platform': 'Windows-10-10.0.19043-SP0', 'event': 'train'} 2022-02-12 18:46:39,747 : INFO : worker thread finished; awaiting finish of 2 more threads 2022-02-12 18:46:39,755 : INFO : worker thread finished; awaiting finish of 1 more threads 2022-02-12 18:46:39,755 : INFO : worker thread finished; awaiting finish of 0 more threads 2022-02-12 18:46:39,755 : INFO : EPOCH - 1 : training on 76421 raw words (56059 effective words) took 0.1s, 847131 effective words/s 2022-02-12 18:46:39,813 : INFO : worker thread finished; awaiting finish of 2 more threads 2022-02-12 18:46:39,819 : INFO : worker thread finished; awaiting finish of 1 more threads 2022-02-12 18:46:39,823 : INFO : worker thread finished; awaiting finish of 0 more threads 2022-02-12 18:46:39,824 : INFO : EPOCH - 2 : training on 76421 raw words (56030 effective words) took 0.1s, 935688 effective words/s 2022-02-12 18:46:39,881 : INFO : worker thread finished; awaiting finish of 2 more threads 2022-02-12 18:46:39,890 : INFO : worker thread finished; awaiting finish of 1 more threads 2022-02-12 18:46:39,890 : INFO : worker thread finished; awaiting finish of 0 more threads 2022-02-12 18:46:39,890 : INFO : EPOCH - 3 : training on 76421 raw words (55944 effective words) took 0.1s, 905191 effective words/s 2022-02-12 18:46:39,952 : INFO : worker thread finished; awaiting finish of 2 more threads 2022-02-12 18:46:39,963 : INFO : worker thread finished; awaiting finish of 1 more threads 2022-02-12 18:46:39,971 : INFO : worker thread finished; awaiting finish of 0 more threads 2022-02-12 18:46:39,972 : INFO : EPOCH - 4 : training on 76421 raw words (56072 effective words) took 0.1s, 774904 effective words/s 2022-02-12 18:46:40,033 : INFO : worker thread finished; awaiting finish of 2 more threads 2022-02-12 18:46:40,039 : INFO : worker thread finished; awaiting finish of 1 more threads 2022-02-12 18:46:40,042 : INFO : worker thread finished; awaiting finish of 0 more threads 2022-02-12 18:46:40,042 : INFO : EPOCH - 5 : training on 76421 raw words (56047 effective words) took 0.1s, 906799 effective words/s 2022-02-12 18:46:40,043 : INFO : Word2Vec lifecycle event {'msg': 'training on 382105 raw words (280152 effective words) took 0.4s, 776815 effective words/s', 'datetime': '2022-02-12T18:46:40.043431', 'gensim': '4.1.2', 'python': '3.9.5 (tags/v3.9.5:0a7dcbd, May 3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)]', 'platform': 'Windows-10-10.0.19043-SP0', 'event': 'train'} 2022-02-12 18:46:40,044 : INFO : Word2Vec lifecycle event {'params': 'Word2Vec(vocab=13279, vector_size=100, alpha=0.025)', 'datetime': '2022-02-12T18:46:40.044429', 'gensim': '4.1.2', 'python': '3.9.5 (tags/v3.9.5:0a7dcbd, May 3 2021, 17:27:52) [MSC v.1928 64 bit (AMD64)]', 'platform': 'Windows-10-10.0.19043-SP0', 'event': 'created'} .. code:: ipython3 model.wv.similar_by_word("fin") .. parsed-literal:: [('mon', 0.9989398121833801), ('pays', 0.9989068508148193), ('ma', 0.9988953471183777), ('toutes', 0.9988815784454346), ('leur', 0.9987949132919312), ('tout', 0.9987940192222595), ('ses', 0.9987934231758118), ('mes', 0.998781144618988), ('france', 0.9987801909446716), ('au', 0.9987511038780212)] .. code:: ipython3 model.wv["fin"].shape .. parsed-literal:: (100,) .. code:: ipython3 model.wv["fin"] .. parsed-literal:: array([-0.09920651, 0.15360324, 0.10844447, 0.12709534, 0.15020044, -0.21826063, 0.07867183, 0.2793031 , -0.1988279 , -0.135458 , -0.08442771, -0.27579817, 0.05431064, 0.13231573, 0.06987454, -0.18821737, -0.0537038 , -0.10661628, -0.04758533, -0.3020647 , 0.1704731 , 0.0394745 , 0.12408937, -0.05706318, -0.05796036, 0.03647643, -0.18711708, -0.10510068, -0.10040793, -0.08600791, 0.13921241, -0.0547129 , 0.09572571, -0.10740169, -0.00452373, 0.28817332, -0.01231772, 0.06307271, 0.02313815, -0.22305253, 0.12906754, -0.20111138, -0.12507376, 0.06637593, 0.06323538, -0.2289281 , -0.18086989, 0.05065202, 0.04751947, 0.0070283 , 0.20169634, -0.15028226, 0.04512867, -0.08974832, -0.08562531, 0.23815149, 0.11708703, -0.08336464, -0.00898065, 0.00677549, -0.08762765, -0.06554074, 0.1182849 , 0.01473513, -0.11507029, 0.25605434, -0.05245751, 0.22131208, -0.27702177, 0.17844225, -0.28551322, 0.09160851, 0.19049928, 0.09809981, 0.18412267, -0.01433086, -0.06096153, -0.00965379, -0.04718976, 0.04390529, -0.2812708 , -0.00393267, -0.14382981, 0.09499372, -0.10859697, -0.07420573, 0.13133654, 0.06538489, 0.24226172, 0.03639907, 0.28915352, 0.05038366, 0.05872998, -0.0310102 , 0.30720538, 0.09244314, 0.20608151, 0.00660289, 0.07621165, 0.0461465 ], dtype=float32) Tagging ------- L’objectif est de tagger les mots comme déterminer si un mot est un verbe, un adjectif … grammar ~~~~~~~ Voir `html.grammar `__. CRF ~~~ Voir `CRF `__ HMM ~~~ Voir `HMM `__. Clustering ---------- Une fois qu’on a des coordonnées, on peut faire plein de choses. LDA ~~~ - `Latent Dirichlet Application `__ - `LatentDirichletAllocation `__ .. code:: ipython3 from sklearn.feature_extraction.text import TfidfVectorizer tfidf_vectorizer = TfidfVectorizer(max_df=0.95, min_df=2, max_features=1000) tfidf = tfidf_vectorizer.fit_transform(data["text"]) .. code:: ipython3 tfidf.shape .. parsed-literal:: (5087, 1000) .. code:: ipython3 from sklearn.decomposition import NMF, LatentDirichletAllocation lda = LatentDirichletAllocation(n_components=10, max_iter=5, learning_method='online', learning_offset=50., random_state=0) .. code:: ipython3 lda.fit(tfidf) .. parsed-literal:: LatentDirichletAllocation(learning_method='online', learning_offset=50.0, max_iter=5, random_state=0) .. code:: ipython3 tf_feature_names = tfidf_vectorizer.get_feature_names() tf_feature_names[100:103] .. parsed-literal:: C:\Python395_x64\lib\site-packages\sklearn\utils\deprecation.py:87: FutureWarning: Function get_feature_names is deprecated; get_feature_names is deprecated in 1.0 and will be removed in 1.2. Please use get_feature_names_out instead. warnings.warn(msg, category=FutureWarning) .. parsed-literal:: ['avoir', 'bac', 'bah'] .. code:: ipython3 def print_top_words(model, feature_names, n_top_words): for topic_idx, topic in enumerate(model.components_): print("Topic #%d:" % topic_idx) print(" ".join([feature_names[i] for i in topic.argsort()[- n_top_words - 1:][::-1]])) print() .. code:: ipython3 print_top_words(lda, tf_feature_names, 10) .. parsed-literal:: Topic #0: gratuit mcdo supprimerai école soir kebab macdo kfc domicile cc volonté Topic #1: macron co https de la est le il et hollande un Topic #2: sijetaispresident je les de la et le des en pour que Topic #3: notaires eu organiserais mets carte nouveaux journées installation cache créer sijetaispresident Topic #4: sijetaispresident interdirais les je ballerines la serait serais bah de interdit Topic #5: ministre de sijetaispresident la je premier mort et nommerais président plus Topic #6: cours le supprimerais jour sijetaispresident lundi samedi semaine je vendredi dimanche Topic #7: port interdirait démissionnerais promesses heure rendrai ballerine mes changement christineboutin tiendrais Topic #8: seraient sijetaispresident gratuits aux les nos putain éducation nationale bonne aurais Topic #9: bordel seront légaliserai putes gratuites pizza mot virerais vitesse dutreil vivre .. code:: ipython3 tr = lda.transform(tfidf) tr[:5] .. parsed-literal:: array([[0.02703569, 0.02703991, 0.75666556, 0.02703569, 0.02704012, 0.02703837, 0.02703696, 0.02703608, 0.02703592, 0.02703569], [0.02276328, 0.02277087, 0.79511841, 0.02276199, 0.02276289, 0.02276525, 0.02277065, 0.02276215, 0.02276251, 0.02276199], [0.02318042, 0.79137016, 0.02318268, 0.02318042, 0.02318137, 0.02318192, 0.0231807 , 0.02318045, 0.02318146, 0.02318042], [0.0294858 , 0.73460096, 0.02949239, 0.0294858 , 0.02949433, 0.0294906 , 0.0294873 , 0.02948597, 0.02948989, 0.02948696], [0.0260542 , 0.66003211, 0.02607499, 0.0260542 , 0.02605546, 0.13151004, 0.02605456, 0.0260542 , 0.02605602, 0.0260542 ]]) .. code:: ipython3 tr.shape .. parsed-literal:: (5087, 10) .. code:: ipython3 import pyLDAvis import pyLDAvis.sklearn pyLDAvis.enable_notebook() .. code:: ipython3 pyLDAvis.sklearn.prepare(lda, tfidf, tfidf_vectorizer) .. parsed-literal:: C:\Python395_x64\lib\site-packages\ipykernel\ipkernel.py:283: DeprecationWarning: `should_run_async` will not call `transform_cell` automatically in the future. Please pass the result to `transformed_cell` argument and any exception that happen during thetransform in `preprocessing_exc_tuple` in IPython 7.17 and above. and should_run_async(code) C:\Python395_x64\lib\site-packages\sklearn\utils\deprecation.py:87: FutureWarning: Function get_feature_names is deprecated; get_feature_names is deprecated in 1.0 and will be removed in 1.2. Please use get_feature_names_out instead. warnings.warn(msg, category=FutureWarning) C:\Python395_x64\lib\site-packages\pyLDAvis\_prepare.py:246: FutureWarning: In a future version of pandas all arguments of DataFrame.drop except for the argument 'labels' will be keyword-only. default_term_info = default_term_info.sort_values( .. raw:: html
Exercice 4 : LDA ~~~~~~~~~~~~~~~~ Recommencer en supprimant les stop-words pour avoir des résultats plus propres.