Data Science, Python, Economie ...

Accueil

Vectoriser le texte

Introduction

L'apprentissage machine exploitant des variables numériques, il est nécessaire de convertir un texte à analyser en une représentation vectorielle. Cette représentation contient le contenu du texte, mais peut aussi comprendre des propriétés comme la longueur, l'auteur la source ou la date de publication.

Les textes peuvent être ainsi analysés en comparant leur représentation vectorielle à d'autres et en obtenir une mesure de similaritéSimilarité
Le but des méthodes d'analyse de la similarité est de créer une mesure unique permettant de comparer des vecteurs entre eux.

Ces techniques trouvent leur utilité en complément des algorithmes de [[clustering]], pour faciliter leur fonctionnement et pour évaluer leurs résultats.

Similarité et distance de Jaccard

Soit deux sets de vecteurs. La Similarité de Jaccard (Jaccard Similarity - $J(s_1,s2)$) des sets $s_1$ et $s_2$ est définie selon le ratio entre l'intersection et l'union des élémen...
sous forme de distance.

De nombreuses méthodes existent. Parmi les plus simples on a :

  • Fréquence lexicale
  • One-hot-encoding
  • TF-IDF : Term Frequency–Inverse Document Frequency (fréquence lexicale, fréquence inverse textuelle)

D'autres approches exploitant les réseaux de neurones existent, appelées "représentations distribuées1" (distributed representations). Elles se fondent sur le contexte ou la similarité sémantique entre les mots.

context_vector

Un vecteur contextuel issu d'un vecteur lexical. Source.

Présentation des techniques

Technique de vectorisation Fonction Utile pour Remarques  
Fréquence lexicale Comptage de la fréquence Modèles bayésiens Les mots les plus fréquents ne sont pas nécessairement les plus informatifs  
One-Hot Encoding Binarisation de l'occurence d'un mot (0, 1) Réseaux de neurones Tous les mots sont équidistants, ce qui rend la normalisation importante  
TF–IDF Normalise les fréquences lexicales entre les documents Flexible Des mots moyennement fréquents ne sont pas nécessairement représentatifs du sujet du texte  
Représentations distribuées Vectorisation fondée sur le contexte ou la similarité entre les mots Modéliser des relations complexes Très gourmandes en performance et nécessitent des outils supplémentaires (Tensorflow) pour analyser des larges volumes  

La préparation de l'analyse avec le bag-of-words

L'approche Bag-of-words consiste à attribuer à chaque document un vecteur de longueur égale à la taille du vocabulaire du corpus de textes analysés. Pour simplifier le calcul, on peut ordonner la liste selon l'ordre alphabétique ou créer un dictionnaire reliant chaque mot à sa position dans une liste.

Source : Applied Text Analysis with Python

Quels packages sont disponibles ?

  • NLTK : Créé pour les données textuelles, mais important en terme d'espace disque
  • Scikit-Learn : Moins flexible, plus rapide et plus léger
  • Gensim
  • SpacySpacy
    Bienvenue sur la méganote Spacy 🔥 ! SpaCy est une bibliothèque python facilitant les étapes du NLP.

    La documentation de spaCy est, je dois le dire, absolument la meilleure que j'ai pu lire sur le web :


    [[Pour bien commencer::https://spacy.io/usage]]
    [[La documentation de l'API::https://spacy.io/api]]
    [[Les modèles disponibles::https://spacy.io/models]]


    Une cheat-sheet est disponible.

    Installation

    Installation de spacy :

    !pip install spacy


    Il faut penser à installer le pack de ...
    : Facile d'utilisation, plutôt rapide mais moins flexible.

Les exemples inclus ici utilisent scikit-learn, mais on peut aller voir cet article pour plus d'exemples.

Vectorisation fréquentielle

La vectorisation la plus simple : on attribue à chaque élément du vecteur une valeur égale au nombre de récurrences dans le texte du mot correspondant. On peut soit garder des nombres entiers soit normaliser le vecteurNorme d'un vecteur
La norme est un élément de la définition d'un vecteur correspondant à sa longueur.

Définition

La norme d'un vecteur correspond à la longueur d'un vecteur, c'est-à-dire la distance qui sépare les deux points qui définissent le vecteur.

La définition formelle est la suivante1 :


Si $\overrightarrow{u} \in I!R^n$ alors la Norme ou la Magnitude de $\overrightarrow{u}$ est définie comme la longueur ou la magnitude du vecteur et peut être calculée en utilisant la formule $\vert\vert\vec{u}\ve...
.

Source : Applied Text Analysis with Python

Scikit-learn propose la méthode CountVectorizer du module sklearn.feature_extraction. Le modèle utilisé tokenise et normalise les textes par la même occasion. On passe dans .fit un itérable, une liste de strings ou de fichiers et crée un dictionnaire du corpus.

La méthode .transform() change chaque document en un array sparce et les combine dans une matrice :

À noter : Pour des corpus de grande taille, on peut utiliser HashingVectorizer qui hache le mot à son index, réduisant ainsi la mémoire utilisée (toutes les informations n'ont pas besoin d'être chargées dans la mémoire). Il n'y a toutefois pas de réversion au stade précédent dans ce cas .inverse_transform et l'on ne peut pas pondérer par fréquence inverse.

from sklearn.feature_extraction.text import CountVectorizer

vectorizer = CountVectorizer()
vectors = vectorizer.fit_transform(corpus)

Compressed Sparse Row

On obtient une matrice creuse (sparse matrix) où la plupart des éléments égaux à 0. Un exemple d'une telle matrice :

$$ \left(\begin{array}{cccccc} 10 & 0 & 0 & 0 & 0 & 0 \\ 0 & 30 & 0 & 40 & 0 & 0 \\ 0 & 0 & 0 & 60 & 70 & 0 \\ 0 & 0 & 0 & 0 & 0 & 80 \end{array}\right) $$

Le stockage se fait dans un format longManipuler les formats longs et larges de données
Les données tabulaires peuvent se présenter sous deux aspects : "long" ou "large".

Définition

Format long

Dans le cas des données dites longues, chaque ligne correspond à un attribut d'une observation dont l'identifiant et la valeur associés sont situés dans une colonne à part. On a donc un grand nombre de lignes, égal à attributs*observations.

La structure est semblable à un dictionnaire, avec des paires clé/valeur emboitées. Elle est pratique lorsque l'on récolte des données dont on ne...
particulier, appelé matrice creuse compressée (Compressed Sparse Row - CSR).

Elle correspond schématiquement aux données suivantes :

Value         = [ 10 20 30 40 50 60 70 80 ]
COL_INDEX = [  0  1  1  3  2  3  4  5 ]   
ROW_INDEX = [  0  2  4  7  8 ]

Sklearn renvoie un objet de type csr_matrix, qui correspond à une matrice creuse.

print(type(vectors))

> <class scipy.sparse.csr.csr_matrix>

print(vectors)

> (0, 108644)       4
(0, 110106)     1
(0, 57577)      2
(0, 24398)      2
(0, 79534)      1
(0, 100942)     1
(0, 37154)      1


:       :
(11313, 55901)  1
(11313, 93448)  1
...

On peut consulter la doc de scipy à ce sujet. Elle détient des attributs qui permettent d'accéder à ses données. Par exemple :

  • .indices: indices

On peut

Pour éviter les problèmes de compatibilité, il est possible de la convertir ensuite au format array :

vectors_np_matrix = vectors.toarray()

print(vectors_np_matrix)

> [ [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] ]

One-hot-encoding

(à faire)

TF-IDF (term frequency - inverse document frequency)

La vectorisation TF-IDF donne à chaque mot un poids selon son originalité et son importance dans le document.

On compare la fréquence avec laquelle le mot apparait dans le document à la fréquence inverse avec laquelle il apparaît au moins une fois dans les documents comparés.

Plusieurs variantes de calcul existent. Voici la méthode canonique :

  • $tf(t,d) = f_{t,d}$ correspond au nombre de fois où le terme $t$ est rencontré dans le document $d$.

  • $idf(t,D) = \log \frac{N}{1+n_{t}}$.

    • $N$ correspond au nombre total de documents
    • $n_t$ au nombre de documents contenant le terme $t$. On y ajoute 1 pour éviter les problèmes de division par 0.
    • Le logarithme du ratio est utilisé afin d'éviter que les termes trop rares ne produisent des résultats trop importants.

Le résultat :

$$ tfidf(t,d) = \frac{f_{t, d}}{\log \frac{N}{n_{t}}} $$

Cette mesure permet d'évaluer le potentiel sémantique de chaque mot :

  • $tf(t,d)$ : Plus un mot est retrouvé dans un document, plus il y influe sur le sens du document, ce qui sera utile pour comparer celui-ci aux corpus.
  • $idf(t,D$ : Moins un mot est utilisé au sein du corpus, plus il sera important pour évaluer la similitude entre deux documents où l'on le retrouve.

Les variantes viennent lisser les poids produits par la formule de départ. Par exemple, plutôt que de d'utiliser la fréquence brute $tf(t,d)= f_{t, d}$, on peut utiliser la fréquence relative au sein du document :

$$ tf(t,d)=f_{t, d} / \sum_{t^{\prime} \in d} f_{t^{\prime}, d} $$

On génère la matrice contenant les poids tfidf avec le transformeur TfidfVectorizer de sklearn.feature_extraction.text:

from sklearn.feature_extraction.text import TfidfVectorizer
tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(data_nlp)

Le vocabulaire associé à la position du mot dans l'index est consultable en inspectant l'attribut .vocabulary_ du transformeur :

tfidf_vec = tfidf_vectorizer.fit(data_nlp).vocabulary_
print(tfidf_vec)

{'clue': 8629,
 'crazy': 10154,
 'pant': 33931,
 'fragility': 17132,
 'porcelain': 35870,
 'doll': 12709,
 'ig': 21954,
 'rini': 39010,
 'jump': 24277,
 'conclusion': 9231,
 'unto': 48633,
 'shall': 41297,
 ...}

Accessoirement, on peut utiliser ce code tiré d'un cours de Datacamp, qui permet de ne conserver pour chaque texte que les n mots dont les scores sont les plus élevés.

from sklearn.feature_extraction.text import TfidfVectorizer

# On initialise le transformateur
tfidf_vectorizer = TfidfVectorizer()

# On entraîne le transformateur sur le corpus (data_nlp)
tfidf_train = tfidf_vectorizer.fit(data_nlp)

# On crée un dictionnaire {index : mot}
vocab = {v:k for k,v in tfidf_vec.vocabulary_.items()}

# Cette fonction retourne les indexes des top_n mots de la ligne vector_index 
# ayant le poids tfidf le plus élevé :
def return_weights(vocab, original_vocab, vector, vector_index, top_n):
    zipped = dict(zip(vector[vector_index].indices, vector[vector_index].data))
    
    # Let's transform that zipped dict into a series
    zipped_series = pd.Series({vocab[i]:zipped[i] for i in vector[vector_index].indices})
    
    # Let's sort the series to pull out the top n weighted words
    zipped_index = zipped_series.sort_values(ascending=False)[:top_n].index
    return [original_vocab[i] for i in zipped_index]

print(return_weights(vocab, tfidf_vec.vocabulary_, text_tfidf, 3, 5))

> [40507, 40394, 44197, 3928, 12690]

print([vocab[key] for key in [40507, 40394, 44197, 3928, 12690]]

> ['schütze', 'schaukeln', 'stroud', 'bartimäus', 'doherty']
# Cette fonction retourne les indices des top_n mots de chaque ligne, pour tout le corpus.
def words_to_filter(vocab, original_vocab, vector, top_n):

    filter_list = []
    for i in range(0, vector.shape[0]):
    
        filtered = return_weights(vocab, original_vocab, vector, i, top_n)
        filter_list.extend(filtered)
    # On utilise un set pour éviter les doublons
    return set(filter_list)

# On obtient une liste d'indices
filtered_words = words_to_filter(vocab, tfidf_vec.vocabulary_, text_tfidf, 3)

print(filtered_words)

> {32770,
 32774,
 8,
 10,
 12,
 32780,
 ...
 }
# En convertissant filtered_words en une liste, on peut l'utiliser pour retirer les mots 
# dont la valeur tf_idf est trop faible.
filtered_text = text_tfidf[:, list(filtered_words)]
print(filtered_text.shape)

> (8979, 13309)

# La matrice vectorisée initiale :
print(tfidf_vectorizer.fit_transform(data_nlp).shape)

>(8979, 52631)

# On constate que l'on a supprimé trois quarts des mots.

Références :

  1. https://hal.archives-ouvertes.fr/tel-02504543/document