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 ».
"""
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}
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)
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 |
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ù :
$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. ]])
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_x64\lib\site-packages\sklearn\decomposition\online_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...\r\nVotez 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