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

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)
2016-10-03 00:42:12,299 : INFO : collecting all words and their counts
2016-10-03 00:42:12,299 : INFO : PROGRESS: at sentence #0, processed 0 words, keeping 0 word types
2016-10-03 00:42:12,327 : INFO : collected 13263 word types from a corpus of 76467 raw words and 5087 sentences
2016-10-03 00:42:12,392 : INFO : min_count=1 retains 13263 unique words (drops 0)
2016-10-03 00:42:12,392 : INFO : min_count leaves 76467 word corpus (100% of original 76467)
2016-10-03 00:42:12,484 : INFO : deleting the raw counts dictionary of 13263 items
2016-10-03 00:42:12,484 : INFO : sample=0.001 downsamples 46 most-common words
2016-10-03 00:42:12,484 : INFO : downsampling leaves estimated 56079 word corpus (73.3% of prior 76467)
2016-10-03 00:42:12,484 : INFO : estimated required memory for 13263 words and 100 dimensions: 17241900 bytes
2016-10-03 00:42:12,559 : INFO : resetting layer weights
2016-10-03 00:42:12,893 : INFO : training model with 3 workers on 13263 vocabulary and 100 features, using sg=0 hs=0 sample=0.001 negative=5
2016-10-03 00:42:12,893 : INFO : expecting 5087 sentences, matching count from corpus used for vocabulary survey
2016-10-03 00:42:13,376 : INFO : worker thread finished; awaiting finish of 2 more threads
2016-10-03 00:42:13,376 : INFO : worker thread finished; awaiting finish of 1 more threads
2016-10-03 00:42:13,391 : INFO : worker thread finished; awaiting finish of 0 more threads
2016-10-03 00:42:13,391 : INFO : training on 382335 raw words (280377 effective words) took 0.5s, 575841 effective words/s
model.similar_by_word("fin")
2016-10-03 00:42:18,089 : INFO : precomputing L2-norms of word weight vectors
[('bien', 0.9993120431900024),
 ('mon', 0.9993098974227905),
 ('ministre', 0.9993038177490234),
 ('3', 0.9993023872375488),
 ('/', 0.9992935657501221),
 ('ma', 0.999293327331543),
 ('dans', 0.9992847442626953),
 ('merde', 0.9992846250534058),
 ('€', 0.9992778301239014),
 ('#bayrou', 0.9992730617523193)]
model["fin"].shape
(100,)
model["fin"]
array([ 0.07167088,  0.01037968, -0.0347001 ,  0.13618709, -0.04936545,
        0.06919333, -0.07977331, -0.04044997, -0.10819082, -0.12842277,
        0.08534056, -0.07823293, -0.09098279,  0.01913173, -0.23570576,
        0.03220233,  0.0213078 ,  0.24803311,  0.03564203, -0.03161603,
       -0.03495153, -0.07290805,  0.03283146, -0.00746943,  0.07626498,
        0.08364875,  0.00777306, -0.07886092,  0.12784876,  0.07973223,
        0.06004526,  0.05176223, -0.06843602, -0.1104833 , -0.10198253,
        0.0043983 , -0.09931083, -0.05931143, -0.04970795, -0.02507968,
       -0.24674726,  0.02844877,  0.23148981, -0.01295278,  0.01698331,
       -0.09990757, -0.10728305, -0.09666982,  0.07170945,  0.06428482,
       -0.06271251, -0.00600671,  0.20045315,  0.06947709,  0.19523463,
        0.05860612, -0.00956623,  0.05660482, -0.04068514, -0.02396445,
        0.10675795, -0.08158956, -0.01152411, -0.04703977,  0.09980039,
        0.10392339, -0.07677256, -0.00409548,  0.06592534, -0.00480576,
        0.00601762, -0.09637805,  0.0136368 , -0.07772852, -0.08013346,
       -0.16120504, -0.03272396, -0.02562772,  0.07080878,  0.01316294,
       -0.26966235,  0.0906961 , -0.16877754,  0.06177418,  0.09502917,
       -0.05076392,  0.2041503 , -0.07089066,  0.09621479,  0.02082463,
       -0.06236167,  0.09625127, -0.08881571,  0.19672391,  0.03073362,
       -0.07315189,  0.0742472 , -0.03126399,  0.05871766,  0.13382684], 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_topics=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_jobs=1, n_topics=10, 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

Exercice 4 : LDA

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