2A.ml - Texte et machine learning#

Links: notebook, html, python, slides, GitHub

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.

from jyquickhelper import add_notebook_menu
add_notebook_menu()

Données#

Nous allons travailler sur des données twitter collectées avec le mot-clé macron : tweets_macron_sijetaispresident_201609.zip.

from ensae_teaching_cs.data import twitter_zip
df = twitter_zip(as_df=True)
df.head(n=2).T
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
df.shape
(5088, 20)

5000 tweets n’est pas assez pour tirer des conclusions mais cela donne une idée. On supprime les valeurs manquantes.

data = df[["retweet_count", "text"]].dropna()
data.shape
(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.

data.sort_values("retweet_count", ascending=False).head()
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 (m_1, m_2, ..., m_k). On définit sa probabilité comme :

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, 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 :

P(c | a, b) = \frac{ \# (a, b, c)}{ \# (a, b)}

C’est le nombre de fois où on observe la séquence (a,b,c) divisé par le nombre de fois où on observe la séquence (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.

from nltk.tokenize import TweetTokenizer
tknzr = TweetTokenizer(preserve_case=False)
tokens = tknzr.tokenize(data.loc[0, "text"])
tokens
['#sijétaisprésident',
 'se',
 'serait',
 'la',
 'fin',
 'du',
 'monde',
 '...',
 'mdr',
 '😂']

n-grammes#

from nltk.util import ngrams
generated_ngrams = ngrams(tokens, 4, pad_left=True, pad_right=True)
list(generated_ngrams)
[(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.

from sklearn.feature_extraction.text import CountVectorizer
count_vect = CountVectorizer()
counts = count_vect.fit_transform(data["text"])
counts.shape
(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.

type(counts)
scipy.sparse.csr.csr_matrix
counts[:5,:5].toarray()
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)
data.loc[0,"text"]
'#SiJétaisPrésident se serait la fin du monde... mdr 😂'
counts[0,:].sum()
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.

idf(t) = \log \frac{\# D}{\#\{d \; | \; t \in d \}}

Où :

  • \#D est le nombre de tweets

  • \#\{d \; | \; t \in d \} est le nombre de tweets contenant le mot t

f(t,d) est le nombre d’occurences d’un mot t dans un document d.

tf(t,d) = \frac{1}{2} + \frac{1}{2} \frac{f(t,d)}{\max_{t' \in d} f(t',d)}

On construit le nombre tfidf(t,f)

tdidf(t,d) = tf(t,d) idf(t)

Le terme idf(t) favorise les mots présent dans peu de documents, le terme 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.

from sklearn.feature_extraction.text import TfidfTransformer
tfidf = TfidfTransformer()
res = tfidf.fit_transform(counts)
res.shape
(5087, 11924)
res[0,:].sum()
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#

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 (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.

sentences = [tknzr.tokenize(_) for _ in data["text"]]
sentences[0]
['#sijétaisprésident',
 'se',
 'serait',
 'la',
 'fin',
 'du',
 'monde',
 '...',
 'mdr',
 '😂']
import gensim, logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : %(message)s', level=logging.INFO)

model = gensim.models.Word2Vec(sentences, min_count=1)
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'}
model.wv.similar_by_word("fin")
[('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)]
model.wv["fin"].shape
(100,)
model.wv["fin"]
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#

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"])
tfidf.shape
(5087, 1000)
from sklearn.decomposition import NMF, LatentDirichletAllocation
lda = LatentDirichletAllocation(n_components=10, max_iter=5,
                                learning_method='online',
                                learning_offset=50.,
                                random_state=0)
lda.fit(tfidf)
LatentDirichletAllocation(learning_method='online', learning_offset=50.0,
                          max_iter=5, random_state=0)
tf_feature_names = tfidf_vectorizer.get_feature_names()
tf_feature_names[100:103]
C:Python395_x64libsite-packagessklearnutilsdeprecation.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)
['avoir', 'bac', 'bah']
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()
print_top_words(lda, tf_feature_names, 10)
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
tr = lda.transform(tfidf)
tr[:5]
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 ]])
tr.shape
(5087, 10)
import pyLDAvis
import pyLDAvis.sklearn
pyLDAvis.enable_notebook()
pyLDAvis.sklearn.prepare(lda, tfidf, tfidf_vectorizer)
C:Python395_x64libsite-packagesipykernelipkernel.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_x64libsite-packagessklearnutilsdeprecation.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_x64libsite-packagespyLDAvis_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(

Exercice 4 : LDA#

Recommencer en supprimant les stop-words pour avoir des résultats plus propres.