Des mots aux sacs de mots

Links: notebook, html, PDF, python, slides, slides(2), GitHub

La tokenisation consiste à découper un texte en token, l’approche sac de mots consiste à compter les occurences de chaque mot dans chaque document de la base de données.

from jyquickhelper import add_notebook_menu
add_notebook_menu()
texte = """
Mardi 20 février, à la médiathèque des Mureaux (Yvelines), le chef de l’Etat a accompagné
la locataire de la rue de Valois pour la remise officielle du rapport
sur les bibliothèques, rédigé par leur ami commun, l’académicien
Erik Orsenna, avec le concours de Noël Corbin, inspecteur général
des affaires culturelles. L’occasion de présenter les premières
mesures en faveur d’un « plan bibliothèques ».
"""

Pipeline de traitement

Maintenant qu’on sait découper en mots ou couples de mots, il faut appliquer sur une liste de textes. On crée une petite liste de textes.

import pandas
df = pandas.DataFrame(dict(text=[texte, "tout petit texte"]))
df
text
0 \nMardi 20 février, à la médiathèque des Murea...
1 tout petit texte

Et on applique l’objet CountVectorizer :

from sklearn.feature_extraction.text import CountVectorizer
cd = CountVectorizer()
cd.fit(df["text"])
res = cd.transform(df["text"])
res
<2x51 sparse matrix of type '<class 'numpy.int64'>'
    with 51 stored elements in Compressed Sparse Row format>

On récupère une matrice sparse où chaque colonne compte le nombre d’occurence d’un mot dans le texte :

res.todense()
matrix([[1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 5, 2, 1, 1, 1, 1, 1, 1, 1,
         1, 4, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1,
         1, 1, 1, 1, 0, 0, 1, 1, 1],
        [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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
         0, 0, 0, 0, 1, 1, 0, 0, 0]], dtype=int64)

Les mots sont les suivants :

cd.vocabulary_
{'mardi': 27,
 '20': 0,
 'février': 19,
 'la': 22,
 'médiathèque': 30,
 'des': 13,
 'mureaux': 29,
 'yvelines': 50,
 'le': 23,
 'chef': 7,
 'de': 12,
 'etat': 17,
 'accompagné': 2,
 'locataire': 26,
 'rue': 43,
 'valois': 49,
 'pour': 38,
 'remise': 42,
 'officielle': 33,
 'du': 14,
 'rapport': 41,
 'sur': 45,
 'les': 24,
 'bibliothèques': 6,
 'rédigé': 44,
 'par': 35,
 'leur': 25,
 'ami': 4,
 'commun': 8,
 'académicien': 1,
 'erik': 16,
 'orsenna': 34,
 'avec': 5,
 'concours': 9,
 'noël': 31,
 'corbin': 10,
 'inspecteur': 21,
 'général': 20,
 'affaires': 3,
 'culturelles': 11,
 'occasion': 32,
 'présenter': 40,
 'premières': 39,
 'mesures': 28,
 'en': 15,
 'faveur': 18,
 'un': 48,
 'plan': 37,
 'tout': 47,
 'petit': 36,
 'texte': 46}

Un tokenizer différent

La classe CountVectorizer est facilement paramétrable. On peut en particulier changer le tokenizer :

from nltk.tokenize import word_tokenize
count_vect = CountVectorizer(tokenizer=word_tokenize)
counts = count_vect.fit_transform(df["text"])
counts.shape
(2, 62)
counts.todense()
matrix([[1, 1, 6, 2, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 5, 2, 1,
         1, 1, 1, 1, 1, 1, 1, 3, 4, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
         1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 4],
        [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, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
         0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0]],
       dtype=int64)

Hashing

Le nombre de mots distincts peut être très grand surtout si la source des textes est bruitée (faute d’orthographe, spams, …). Pour réduire le nombre de mots, on peut utiliser un hash à valeur dans un ensemble plus petit que le nombre de mots découverts : c’est une sorte de modulo. Deux mots pourront être comptabilisés dans la même colonne. On utilise la classe HashingVectorizer.

from sklearn.feature_extraction.text import HashingVectorizer
cd = HashingVectorizer(n_features=5)
cd.fit(df["text"])
res = cd.transform(df["text"])
res.todense()
matrix([[-0.08247861,  0.16495722, -0.41239305,  0.74230749,  0.49487166],
        [-0.4472136 ,  0.        ,  0.89442719,  0.        ,  0.        ]])

La classe utilise la classe FeatureHasher et plus précisément le code dans _hashing.pyx.

cd = HashingVectorizer(n_features=5, binary=True)
cd.fit(df["text"])
res = cd.transform(df["text"])
res.todense()
matrix([[0.4472136 , 0.4472136 , 0.4472136 , 0.4472136 , 0.4472136 ],
        [0.70710678, 0.        , 0.70710678, 0.        , 0.        ]])

Réduire les dimensions tout en gardant une certaine forme de proximité.

df2 = pandas.DataFrame(dict(text=[texte,
                  " ".join(texte.split()[1:-1]),
                  " ".join(texte.split()[5:-5]),
                  " ".join(texte.split()[10:-10]) + ' machine',
                  " ".join(texte.split()[20:-20]) + ' learning',
                  " ".join(texte.split()[25:-25]) + ' statistique',
                  " ".join(texte.split()[30:-30]) + ' nouveau',
                  "tout petit texte"]))
df2
text
0 \nMardi 20 février, à la médiathèque des Murea...
1 20 février, à la médiathèque des Mureaux (Yvel...
2 médiathèque des Mureaux (Yvelines), le chef de...
3 chef de l’Etat a accompagné la locataire de la...
4 de Valois pour la remise officielle du rapport...
5 officielle du rapport sur les bibliothèques, r...
6 bibliothèques, rédigé par nouveau
7 tout petit texte
cd = HashingVectorizer(n_features=8, binary=False)
cd.fit(df2["text"])
res = cd.transform(df2["text"])
res.todense()
matrix([[-0.08873565,  0.3549426 , -0.1774713 ,  0.        , -0.26620695,
          0.        ,  0.79862086,  0.3549426 ],
        [-0.09053575,  0.36214298, -0.18107149,  0.        , -0.18107149,
          0.        ,  0.81482171,  0.36214298],
        [-0.10783277,  0.43133109, -0.21566555,  0.        ,  0.        ,
          0.        ,  0.75482941,  0.43133109],
        [-0.24806947,  0.3721042 ,  0.        , -0.12403473, -0.12403473,
          0.        ,  0.86824314,  0.12403473],
        [-0.20412415,  0.81649658,  0.        ,  0.20412415,  0.20412415,
          0.20412415,  0.40824829,  0.        ],
        [ 0.        ,  0.35355339,  0.35355339,  0.35355339,  0.        ,
          0.70710678, -0.35355339,  0.        ],
        [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
          0.        ,  0.70710678, -0.70710678],
        [ 0.        ,  0.        ,  0.        ,  0.        ,  0.        ,
          0.        ,  1.        ,  0.        ]])
from sklearn.metrics.pairwise import pairwise_distances
pandas.DataFrame(pairwise_distances(res))
0 1 2 3 4 5 6 7
0 0.000000 0.087352 0.293731 0.388511 0.916931 1.561800 1.171556 0.634632
1 0.087352 0.000000 0.217844 0.368633 0.883337 1.564650 1.166111 0.608569
2 0.293731 0.217844 0.000000 0.455795 0.797058 1.543129 1.241976 0.700244
3 0.388511 0.368633 0.455795 0.000000 0.826704 1.561579 0.973412 0.513336
4 0.916931 0.883337 0.797058 0.826704 0.000000 1.130625 1.192749 1.087889
5 1.561800 1.564650 1.543129 1.561579 1.130625 0.000000 1.581139 1.645329
6 1.171556 1.166111 1.241976 0.973412 1.192749 1.581139 0.000000 0.765367
7 0.634632 0.608569 0.700244 0.513336 1.087889 1.645329 0.765367 0.000000

tf-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 documents

  • \#\{d \; | \; t \in d \} est le nombre de documents 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) = 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. Sur deux documents, cela ne sert pas à grand-chose. On utilise la classe TfidfVectorizer.

from sklearn.feature_extraction.text import TfidfVectorizer
cd = TfidfVectorizer()
cd.fit(df["text"])
res = cd.transform(df["text"])
res.todense()
matrix([[0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.20100756, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.10050378, 0.50251891, 0.20100756, 0.10050378,
         0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.10050378, 0.40201513, 0.20100756, 0.20100756,
         0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.        , 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.        , 0.        , 0.10050378, 0.10050378,
         0.10050378],
        [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.        ,
         0.        , 0.        , 0.        , 0.        , 0.        ,
         0.        , 0.        , 0.        , 0.        , 0.        ,
         0.        , 0.57735027, 0.        , 0.        , 0.        ,
         0.        , 0.        , 0.        , 0.        , 0.        ,
         0.        , 0.57735027, 0.57735027, 0.        , 0.        ,
         0.        ]])

Si on décompose :

from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.pipeline import make_pipeline
pipe = make_pipeline(CountVectorizer(), TfidfTransformer())
pipe.fit(df['text'])
res = pipe.transform(df['text'])
res.todense()
matrix([[0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.20100756, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.10050378, 0.50251891, 0.20100756, 0.10050378,
         0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.10050378, 0.40201513, 0.20100756, 0.20100756,
         0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.        , 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.10050378, 0.10050378, 0.10050378, 0.10050378,
         0.10050378, 0.        , 0.        , 0.10050378, 0.10050378,
         0.10050378],
        [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.        ,
         0.        , 0.        , 0.        , 0.        , 0.        ,
         0.        , 0.        , 0.        , 0.        , 0.        ,
         0.        , 0.57735027, 0.        , 0.        , 0.        ,
         0.        , 0.        , 0.        , 0.        , 0.        ,
         0.        , 0.57735027, 0.57735027, 0.        , 0.        ,
         0.        ]])

Ca marche aussi sur les hash.

pipe = make_pipeline(HashingVectorizer(n_features=5), TfidfTransformer())
pipe.fit(df['text'])
res = pipe.transform(df['text'])
res.todense()
matrix([[-0.06142775,  0.17266912, -0.30713875,  0.77701104,  0.51800736],
        [-0.4472136 ,  0.        ,  0.89442719,  0.        ,  0.        ]])

Sur un jeu de données

L’idée est d’appliquer une LDA ou Latent Dirichet Application.

from papierstat.datasets import load_tweet_dataset
tweet = load_tweet_dataset()
tweet = tweet[tweet["text"].notnull()]
tweet.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
pipeline = make_pipeline(CountVectorizer(), TfidfTransformer())
res = pipeline.fit_transform(tweet['text'])
count = pipeline.steps[0][-1]
voc = count.get_feature_names()

Le code suivant marche parce que la base n’est pas trop petite.

data = pandas.DataFrame(res.todense(), columns=voc)
data['whole_tweet'] = tweet['text']
data[data['œuvre'] > 0].head()
00 000 0000 0079 00h 04 06 09 0ccnpoxuwu 0cxdedblpx ... îles îls œil œufs œuvre œuvrer œuvrerais œuvres δlex whole_tweet
109 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.0 0.0 0.354877 0.0 0.0 0.0 0.0 #Macron appelle à poursuivre l'œuvre de #Rocar...
151 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.0 0.0 0.328786 0.0 0.0 0.0 0.0 Quelle est-t-elle cette "œuvre de Michel Rocar...
4063 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.0 0.0 0.399841 0.0 0.0 0.0 0.0 #SiJetaisPresident je durcirais les pénalités ...

3 rows × 11925 columns

from sklearn.decomposition import LatentDirichletAllocation
lda = LatentDirichletAllocation(n_components=10)
lda.fit(res)
c:python365_x64libsite-packagessklearndecompositiononline_lda.py:536: DeprecationWarning: The default value for 'learning_method' will be changed from 'online' to 'batch' in the release 0.20. This warning was introduced in 0.18.
  DeprecationWarning)
LatentDirichletAllocation(batch_size=128, doc_topic_prior=None,
             evaluate_every=-1, learning_decay=0.7, learning_method=None,
             learning_offset=10.0, max_doc_update_iter=100, max_iter=10,
             mean_change_tol=0.001, n_components=10, n_jobs=1,
             n_topics=None, perp_tol=0.1, random_state=None,
             topic_word_prior=None, total_samples=1000000.0, verbose=0)
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, voc, 10)
Topic #0:
sieste imposerai cantine frites scolaire self affaires obligatoire profiterais grec
Topic #1:
pizza domicile meufs purge constitution haute drogues trahison demissionerai do
Topic #2:
organiserai semaines mets organiserais kebabs lep roux raie cache empire
Topic #3:
abolirai ss10 desigual urgent intérieur avancés wiko morsay téléréalité lcp
Topic #4:
interdirais les sijetaispresident seraient port ballerines camembert démissionnerais droit gratuits
Topic #5:
rendrais écoles légal mandat poudlard obligatoire sortie vive bouton 8h
Topic #6:
légaliserai promesses famille illégal rendrai tiendrais arfa dutreil beuh sports
Topic #7:
sijetaispresident je ministre légaliserais co https culture nommerais les la
Topic #8:
sijetaispresident de je la les le et co https macron
Topic #9:
chocolatine bac pain kfc lenorman cc lait tacos notaires christineboutin
comp = lda.transform(res)
comp.shape
(5087, 10)

Pour chaque tweet, le score plus élevé indique la classe dans laquelle classé le tweet.

val = comp[1,:]
val, val.argmax(), tweet['text'][1]
(array([0.02214216, 0.02214216, 0.02214216, 0.02214216, 0.02214224,
        0.02214216, 0.02214216, 0.02214232, 0.80072034, 0.02214216]),
 8,
 '#SiJétaisPrésident je donnerai plus de vacances 😌, genre 1mois de cours 1 mois de vacances etc...rnVotez pour moi !😂')

On regarde les tweets les plus représentatifs du topic 3.

prediction = pandas.DataFrame(comp)
prediction['tweet'] = tweet['text']
t3 = prediction.sort_values(3, ascending=False)
t3.head()
0 1 2 3 4 5 6 7 8 9 tweet
383 0.021209 0.021209 0.021209 0.606043 0.021210 0.021209 0.021209 0.021210 0.224283 0.021209 Sondage @OdoxaSondages Décryptage demain en pl...
2391 0.026966 0.026966 0.026966 0.600969 0.026966 0.026966 0.026966 0.026966 0.183305 0.026966 Jss sure c'est Raouf qui a cree ce # 😂😂 @justd...
2354 0.039798 0.039799 0.039798 0.599248 0.039889 0.039798 0.039798 0.039870 0.082205 0.039798 #SiJetaisPresident j'abrogerais #college2016...
370 0.022561 0.022561 0.022561 0.591920 0.022561 0.022561 0.022561 0.022562 0.227592 0.022561 Sondage @13h15 / @OdoxaSondages. Décryptage de...
4706 0.020940 0.020940 0.020940 0.588835 0.020940 0.020940 0.020940 0.020941 0.243645 0.020940 Politiciens criminels #Sarkozy #Macron #LePen ...
print("\n---\n".join(list(t3['tweet'].head())))
Sondage @OdoxaSondages Décryptage demain en plateau dans le @13h15 de @LaurentDelahous #Macron https://t.co/wJex9S0Ute nice #Nice06
---
Jss sure c'est Raouf qui a cree ce # 😂😂 @justdelgrosso #SiJetaisPresident
---
#SiJetaisPresident j'abrogerais #college2016...
---
Sondage @13h15 / @OdoxaSondages. Décryptage demain en plateau dans le @13h15 de @LaurentDelahous #Précision #Macron https://t.co/wAEbrJxY7m
---
Politiciens criminels #Sarkozy #Macron #LePen #Fabius (y a quand même des gosses morts pour ce dernier) la liste est longue #stopcorruption