2A.ml - Texte et machine learning

Links: notebook, html, PDF, 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 2016
delimit_mention NaN NaN
lang fr fr
id_str 7.76067e+17 7.76068e+17
text_mention NaN NaN
retweet_count 4 5
favorite_count 3 8
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
nb_urls 0 0
nb_symbols 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)]

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.

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)
2019-08-27 22:26:47,360 : INFO : collecting all words and their counts
2019-08-27 22:26:47,365 : INFO : PROGRESS: at sentence #0, processed 0 words, keeping 0 word types
2019-08-27 22:26:47,416 : INFO : collected 13262 word types from a corpus of 76468 raw words and 5087 sentences
2019-08-27 22:26:47,418 : INFO : Loading a fresh vocabulary
2019-08-27 22:26:47,541 : INFO : effective_min_count=1 retains 13262 unique words (100% of original 13262, drops 0)
2019-08-27 22:26:47,542 : INFO : effective_min_count=1 leaves 76468 word corpus (100% of original 76468, drops 0)
2019-08-27 22:26:47,637 : INFO : deleting the raw counts dictionary of 13262 items
2019-08-27 22:26:47,639 : INFO : sample=0.001 downsamples 46 most-common words
2019-08-27 22:26:47,640 : INFO : downsampling leaves estimated 56080 word corpus (73.3% of prior 76468)
2019-08-27 22:26:47,713 : INFO : estimated required memory for 13262 words and 100 dimensions: 17240600 bytes
2019-08-27 22:26:47,716 : INFO : resetting layer weights
2019-08-27 22:26:52,708 : INFO : training model with 3 workers on 13262 vocabulary and 100 features, using sg=0 hs=0 sample=0.001 negative=5 window=5
2019-08-27 22:26:52,805 : INFO : worker thread finished; awaiting finish of 2 more threads
2019-08-27 22:26:52,819 : INFO : worker thread finished; awaiting finish of 1 more threads
2019-08-27 22:26:52,824 : INFO : worker thread finished; awaiting finish of 0 more threads
2019-08-27 22:26:52,826 : INFO : EPOCH - 1 : training on 76468 raw words (56081 effective words) took 0.1s, 612614 effective words/s
2019-08-27 22:26:52,920 : INFO : worker thread finished; awaiting finish of 2 more threads
2019-08-27 22:26:52,936 : INFO : worker thread finished; awaiting finish of 1 more threads
2019-08-27 22:26:52,939 : INFO : worker thread finished; awaiting finish of 0 more threads
2019-08-27 22:26:52,941 : INFO : EPOCH - 2 : training on 76468 raw words (56217 effective words) took 0.1s, 573436 effective words/s
2019-08-27 22:26:53,039 : INFO : worker thread finished; awaiting finish of 2 more threads
2019-08-27 22:26:53,057 : INFO : worker thread finished; awaiting finish of 1 more threads
2019-08-27 22:26:53,058 : INFO : worker thread finished; awaiting finish of 0 more threads
2019-08-27 22:26:53,059 : INFO : EPOCH - 3 : training on 76468 raw words (56093 effective words) took 0.1s, 557268 effective words/s
2019-08-27 22:26:53,159 : INFO : worker thread finished; awaiting finish of 2 more threads
2019-08-27 22:26:53,169 : INFO : worker thread finished; awaiting finish of 1 more threads
2019-08-27 22:26:53,171 : INFO : worker thread finished; awaiting finish of 0 more threads
2019-08-27 22:26:53,173 : INFO : EPOCH - 4 : training on 76468 raw words (56139 effective words) took 0.1s, 588450 effective words/s
2019-08-27 22:26:53,272 : INFO : worker thread finished; awaiting finish of 2 more threads
2019-08-27 22:26:53,285 : INFO : worker thread finished; awaiting finish of 1 more threads
2019-08-27 22:26:53,294 : INFO : worker thread finished; awaiting finish of 0 more threads
2019-08-27 22:26:53,296 : INFO : EPOCH - 5 : training on 76468 raw words (56158 effective words) took 0.1s, 558758 effective words/s
2019-08-27 22:26:53,298 : INFO : training on a 382340 raw words (280688 effective words) took 0.6s, 477586 effective words/s
model.wv.similar_by_word("fin")
2019-08-27 22:26:53,308 : INFO : precomputing L2-norms of word weight vectors
[('pays', 0.9997762441635132),
 ('...', 0.9997516870498657),
 ('ce', 0.9997488260269165),
 ('ma', 0.9997472763061523),
 ('dans', 0.9997463822364807),
 ('aux', 0.9997367262840271),
 ('serait', 0.999735951423645),
 ('au', 0.9997355937957764),
 ('mon', 0.99973464012146),
 ('sont', 0.9997326135635376)]
model.wv["fin"].shape
(100,)
model.wv["fin"]
array([ 2.32595250e-01,  2.69504368e-01,  6.57317191e-02, -1.63778067e-01,
        2.98107769e-02,  1.43973693e-01, -2.75658280e-01,  1.47964388e-01,
        4.25490625e-02,  1.66759137e-02,  1.54966384e-01, -1.97812200e-01,
        3.54901105e-02, -1.70589596e-01, -7.84644186e-02, -1.79385781e-01,
       -4.96363780e-03, -4.90222909e-02, -1.40104191e-02, -2.18923450e-01,
       -1.68525562e-01, -1.17111236e-01, -1.86863635e-02,  6.92968369e-02,
       -1.65455744e-01,  2.12157056e-01,  1.72820672e-01, -1.59844220e-01,
        1.03320450e-01, -1.33226082e-01, -1.20906383e-01,  5.72424270e-02,
       -3.53067756e-01, -2.51862526e-01, -8.87881219e-03, -2.69470382e-02,
        3.10455565e-03, -1.56045780e-01, -3.86689082e-02,  8.92276615e-02,
        6.16491288e-02, -1.10004105e-01,  1.72906280e-01,  1.54520154e-01,
        3.32178503e-01, -1.18936740e-01, -4.14087683e-01, -1.25814110e-01,
       -7.06550032e-02,  1.44609332e-01, -7.11866021e-02,  5.05076945e-02,
       -1.38344392e-01, -7.07699284e-02, -3.88593614e-01,  4.95251715e-02,
       -9.68720466e-02,  1.65556192e-01,  3.32178548e-02, -4.04830649e-03,
       -7.50351697e-02,  2.31092736e-01, -4.24880497e-02, -2.17754051e-01,
        1.42272621e-01, -1.90135650e-02,  4.59194835e-03, -4.14611495e-05,
       -1.59860477e-02, -2.11898208e-01, -3.30838598e-02, -3.85266095e-02,
       -1.93522722e-01,  6.58056065e-02,  1.23298422e-01, -1.12771457e-02,
        5.45086153e-02,  2.93010473e-01,  7.99539387e-02,  2.86573529e-01,
       -8.95702988e-02,  3.14540379e-02, -1.97025128e-02,  1.18431516e-01,
       -6.50832281e-02, -1.11422045e-02,  6.01087287e-02, -2.52260953e-01,
       -7.19161183e-02, -2.07218483e-01, -1.46282479e-01, -1.39513528e-02,
       -8.61635730e-02,  1.38646886e-01, -1.13585591e-01,  5.55804409e-02,
        3.42568606e-01, -6.98400661e-02, -6.13498827e-03,  8.57257843e-02],
      dtype=float32)

Tagging

L’objectif est de tagger les mots comme déterminer si un mot est un verbe, un adjectif …

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(batch_size=128, doc_topic_prior=None,
                          evaluate_every=-1, learning_decay=0.7,
                          learning_method='online', learning_offset=50.0,
                          max_doc_update_iter=100, max_iter=5,
                          mean_change_tol=0.001, n_components=10, n_jobs=None,
                          perp_tol=0.1, random_state=0, topic_word_prior=None,
                          total_samples=1000000.0, verbose=0)
tf_feature_names = tfidf_vectorizer.get_feature_names()
tf_feature_names[100:103]
['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
Topic #1:
macron co https de la est le il et hollande
Topic #2:
sijetaispresident je les de la et le des en pour
Topic #3:
notaires eu organiserais mets carte nouveaux journées installation cache créer
Topic #4:
sijetaispresident interdirais les je ballerines la serait serais bah de
Topic #5:
ministre de sijetaispresident la je premier mort et nommerais président
Topic #6:
cours le supprimerais jour sijetaispresident lundi samedi semaine je vendredi
Topic #7:
port interdirait démissionnerais promesses heure rendrai ballerine mes changement christineboutin
Topic #8:
seraient sijetaispresident gratuits aux les nos putain éducation nationale bonne
Topic #9:
bordel seront légaliserai putes gratuites pizza mot virerais vitesse dutreil
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:python372_x64libsite-packagespyLDAvis_prepare.py:257: FutureWarning: Sorting because non-concatenation axis is not aligned. A future version
of pandas will change to not sort by default.
To accept the future behavior, pass 'sort=False'.
To retain the current behavior and silence the warning, pass 'sort=True'.
  return pd.concat([default_term_info] + list(topic_dfs))

Exercice 4 : LDA

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