Modèle symbolique : un document est une suite de symboles (mots)
with open("zola_ventre-de-paris.txt") as in_stream:
doc = [word for line in in_stream for word in line.strip().split()]
doc[5:10]
Ce qu'on sait bien gérer : les nombres !
Le problème est donc : comment représenter des documents par une suite de nombres en conservant les informations qu'on veut exploiter ?
Idée : représenter un document par la distribution brute de son lexique (le nombre d'occurences de chaque mot)
from collections import Counter
with open("zola_ventre-de-paris.txt") as in_stream:
doc1 = Counter(word for line in in_stream for word in line.strip().split())
doc1
with open("balzac_petits-bourgeois.txt") as in_stream:
doc2 = Counter(word for line in in_stream for word in line.strip().split())
voc = sorted(set(doc1.keys()).union(doc2.keys()))
vec_1 = [doc1[word] for word in voc]
vec_2 = [doc2[word] for word in voc]
vec_1
Ici vec_1
et vec_2
sont des listes des nombres (des vecteurs) de même longueur auxquelles on va pouvoir appliquer les merveilleux outils que nous offre la statistique.
On pourrait déjà se servir de ce genre de représentation pour faire des travaux intéressants, au hasard de la classification de documents, pourquoi pas avec des SVMs…
# Quick 'n dirty BOWization
import re
import math
def to_bow(texts):
freqs = [Counter(re.split(r'\W+', t.lower())) for t in texts]
voc = sorted(set().union(*[c.keys() for c in freqs]))
return [[c[word] for word in voc] for c in freqs]
def l2(b1, b2):
return math.sqrt(sum((x-y)**2 for x, y in zip(b1, b2)))
d1 = "Un crocodile"
d2 = "Un crocodile, un crocodile, un crocodile"
d3 = "Un éléphant"
b1, b2, b3 = to_bow([d1, d2, d3])
print(
'\n'.join('\t'.join([str(x) for x in l]) for l in [b1, b2, b3])
)
print(l2(b1, b2))
print(l2(b1, b3))
Heureusement ce n'est pas très compliqué de normaliser pour avoir des fréquences relatives
n1 = [x/sum(b1) for x in b1]
n2 = [x/sum(b2) for x in b2]
n3 = [x/sum(b3) for x in b3]
print(l2(n1, n2))
print(l2(n1, n3))
print(n1, n2, n3)
Modifier le script précédent pour qu'il génère des sacs de mots utilisant les fréquences relatives plutot que les nombres d'occurences
Par exemple : un indicateur de spécificité assez grossier d'un mot $W$ dans un texte $T$. $$ S(W, T) = \frac{\text{nombre d'occurrences de W dans le texte T}}{\text{nombre d'occurences de W dans l'ensemble du corpus}} $$
Le calcul pour un seul mot n'est pas beaucoup plus compliqué que le calcul des fréquences relatives
d1 = "Un crocodile"
d2 = "Un crocodile, un crocodile, un crocodile"
d3 = "Un éléphant"
bows = to_bow([d1, d2, d3])
total_crocodile = sum(l[0] for l in bows)
[l[0]/total_crocodile for l in bows]
Une solution c'est de voir l'ensemble des sacs-de-mots non plus comme une liste de listes, mais comme un tableau à deux dimensions
print('\n'.join(' '.join([str(x) for x in l]) for l in bows))
Avec cette vision, les deux opérations sont en fait quasiment identiques
Il ne nous manque plus qu'une interface agréable pour le faire
import numpy as np
Numpy est une bibliothèque de calcul numérique pour Python. Elle est à la base de nombreux autres (dont scikit-learn
et gensim
dont on reparlera) et est assez incontournable si on veut faire du calcul en Python.
Ses possibilités sont très nombreuses, mais on se contentra ici de ses fonctions de base : la manipulation de tableaux de nombres.
numpy.array
¶La classe de base de numpy est numpy.ndarray
, qui permet de représenter des tableaux de nombres de dimensions arbitraires (chez les informaticiens on appelle ça des tenseurs, ce qui fait ricaner les mathématiciens).
On les créé en général avec la fonction numpy.array
a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
a
Il y a beaucoup de façons de créer des ndarray
s. Comme toujours, il est recommandé de jeter un œil sur la doc
a
Contrairement à nos listes de listes précédentes, où on n'a accès qu'à une dimension à la fois, ici on peut accéder librement aux deux dimensions
print(a[0, 0])
print(a[2, 1])
Par convention, les coordonnées sont données dans l'ordre $(\text{ligne}, \text{colonne})$.
a
On peut aussi — comme pour les listes habituelles de Python — adresser des tranches
a[0, 1:3]
a[:2, 1]
a[1:3, :2]
a[0, :]
a
Le tableau reste quand même une séquence indexable, sur laquelle on peut itérer, dans l'ordre de ses dimensions
a[0][1] # Strictement équivalent à `a[0, 1]
for l in a:
print(l)
for x in l:
print(x)
Quelques fonctions bien pratiques
a
a.max()
a.min()
a.mean()
a.sum()
a.transpose()
a.sum(axis=0)
Les opérations habituelles sont définies et ont le sens qu'on a envie qu'elles aient
a
a*2
a/3
a + 4
a - 5
a
Les opérations sont aussi définies entre deux tableaux
b = np.array([[1, 2, 3], [4, 5, 6], [0, 0, 0]])
b
a+b
a*b
a/b
Une notion un peu plus compliquée mais qui va nous servir tout de suite
a
c = np.array([2, 4, 8])
c
a*c
Explication : si un des tableaux a moins de dimensions que l'autre, numpy fait automatiquement la conversion pour que tout se passe comme si on avait multiplié par
np.broadcast_to(c, [3,3])
Multiplier par un tableau à une dimension revient donc à multiplier colonne par colonne
Rappel : on avait une liste de sacs de mots et on voulait obtenir un tableau de spécificités
bows
Il suffit de sommer chaque colonne
bows_array = np.array(bows)
cols_total = bows_array.sum(axis=0)
print(bows_array)
print(cols_total)
Puis de diviser
bows_array/cols_total
Modifier le script de BoWization précédent pour qu'il renvoie non plus les fréquences relatives de chaque mot mais leur tf⋅idf avec la définition suivante pour un mot $w$, un document $D$ et un corpus $C$
np.log
Pistes de recherche:
keepdims
de np.sum
np.transpose
np.count_nonzero
np.array([[1, 0], [2, 0]]) > 0
Dans les représentations qu'on a vu, les vecteurs qui composent chaque texte sont essentiellement composés de zéros
Pour certaines applications ce n'est pas un problème
Mais pour les réseaux de neurones, par exemple, c'est très peu efficace.
Il y a plusieurs autres solutions: