Étape 3 : Préparez vos données

Avant de pouvoir transmettre nos données à un modèle, elles doivent être transformées en un format compréhensible par le modèle.

Tout d'abord, les échantillons de données que nous avons collectés peuvent être dans un ordre spécifique. Nous ne voulons pas que les informations associées à l'ordre des échantillons aient une incidence sur la relation entre les textes et les étiquettes. Par exemple, si un ensemble de données est trié par classe, puis divisé en ensembles d'entraînement/validation, ces ensembles ne seront pas représentatifs de la distribution globale des données.

Une bonne pratique simple pour vous assurer que le modèle n'est pas affecté par l'ordre des données est de toujours brasser les données avant toute autre action. Si vos données sont déjà divisées en ensembles d'entraînement et de validation, veillez à transformer vos données de validation de la même manière que vos données d'entraînement. Si vous n'avez pas encore d'ensembles d'entraînement et de validation distincts, vous pouvez diviser les échantillons après le brassage. Il est courant d'utiliser 80% des échantillons pour l'entraînement et 20% pour la validation.

Ensuite, les algorithmes de machine learning prennent des nombres en entrée. Cela signifie que nous devons convertir les textes en vecteurs numériques. Ce processus comporte deux étapes:

  1. Tokenisation : divisez les textes en sous-textes plus petits, ce qui permet une bonne généralisation des relations entre les textes et les étiquettes. Cela détermine le "vocabulaire" de l'ensemble de données (ensemble de jetons uniques présents dans les données).

  2. Vectorization: définissez une mesure numérique appropriée pour caractériser ces textes.

Voyons comment réaliser ces deux étapes pour les vecteurs à n-grammes et les vecteurs de séquence, et comment optimiser les représentations de vecteurs à l'aide de techniques de sélection de caractéristiques et de normalisation.

Vecteurs à N-grammes [Option A]

Dans les paragraphes suivants, nous verrons comment procéder à la tokenisation et à la vectorisation pour les modèles à n-grammes. Nous verrons également comment optimiser la représentation des n-grammes à l'aide de techniques de sélection et de normalisation des caractéristiques.

Dans un vecteur à n-gramme, le texte est représenté par une collection de n-grammes uniques, c'est-à-dire des groupes de n jetons adjacents (généralement des mots). Prenons l'exemple du texte The mouse ran up the clock. Ici, les unigrammes de mots (n = 1) sont ['the', 'mouse', 'ran', 'up', 'clock'], les mots de bigramme (n = 2) sont ['the mouse', 'mouse ran', 'ran up', 'up the', 'the clock'], etc.

Tokenisation

Nous avons constaté que la tokenisation en unigrammes et bigrammes de mots offre une bonne précision tout en réduisant le temps de calcul.

Vectorisation

Une fois que nous avons divisé nos échantillons de texte en n-grammes, nous devons les transformer en vecteurs numériques que nos modèles de machine learning peuvent traiter. L'exemple ci-dessous montre les index attribués aux unigrammes et aux bigrammes générés pour deux textes.

Texts: 'The mouse ran up the clock' and 'The mouse ran down'
Index assigned for every token: {'the': 7, 'mouse': 2, 'ran': 4, 'up': 10,
  'clock': 0, 'the mouse': 9, 'mouse ran': 3, 'ran up': 6, 'up the': 11, 'the
clock': 8, 'down': 1, 'ran down': 5}

Une fois les index attribués aux n-grammes, nous veillons généralement à l'aide de l'une des options suivantes.

Encodage one-hot : chaque exemple de texte est représenté par un vecteur indiquant la présence ou l'absence de jeton dans le texte.

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1]

Encodage du nombre : chaque exemple de texte est représenté par un vecteur indiquant le nombre d'un jeton dans le texte. Notez que l'élément correspondant à l'unigramme ("#" en gras ci-dessous) est désormais représenté par "2", car le mot "the" apparaît deux fois dans le texte.

'The mouse ran up the clock' = [1, 0, 1, 1, 1, 0, 1, 2, 1, 1, 1, 1]

Encodage tf-idf: le problème avec les deux approches ci-dessus est que les mots courants qui apparaissent dans des fréquences similaires dans tous les documents (c'est-à-dire les mots qui ne sont pas particulièrement spécifiques aux exemples de texte de l'ensemble de données) ne sont pas pénalisés. Par exemple, le mot "a" apparaît très fréquemment dans tous les textes. Par conséquent, un nombre de jetons plus élevé pour "le" que pour d'autres mots plus pertinents n'est pas très utile.

'The mouse ran up the clock' = [0.33, 0, 0.23, 0.23, 0.23, 0, 0.33, 0.47, 0.33,
0.23, 0.33, 0.33] (See Scikit-learn TfidfTransformer)

Il existe de nombreuses autres représentations vectorielles, mais les trois ci-dessus sont les plus couramment utilisées.

Nous avons observé que l'encodage tf-idf est légèrement supérieur aux deux autres en termes de justesse (en moyenne: 0,25-15% plus élevée). Nous recommandons donc d'utiliser cette méthode pour vectoriser les n-grammes. Cependant, gardez à l'esprit qu'elle utilise plus de mémoire (car elle utilise une représentation en virgule flottante) et prend plus de temps à effectuer des calculs, en particulier pour les ensembles de données volumineux. Elle peut être deux fois plus longue dans certains cas.

Sélection des caractéristiques.

Lorsque nous convertissons tout le texte d'un ensemble de données en jetons uni+bigrammes, nous pouvons obtenir des dizaines de milliers de jetons. Tous ces jetons/fonctionnalités ne contribuent pas à la prédiction par étiquette. Nous pouvons donc supprimer certains jetons, par exemple ceux qui apparaissent très rarement dans l'ensemble de données. Nous pouvons également mesurer l'importance des caractéristiques (part de chaque jeton pour les prédictions d'étiquettes) et n'inclure que les jetons les plus informatifs.

De nombreuses fonctions statistiques utilisent des caractéristiques et les étiquettes correspondantes pour générer le score d'importance des caractéristiques. Les deux fonctions couramment utilisées sont f_classif et chi2. Nos tests montrent que ces deux fonctions offrent les mêmes performances.

Plus important encore, nous avons constaté que la justesse atteint environ 20 000 caractéristiques pour de nombreux ensembles de données (voir la Figure 6). L'ajout de caractéristiques supérieures à ce seuil n'apporte que très peu d'amélioration, ce qui entraîne parfois un surapprentissage et réduit les performances.

Principaux K et justesse

Figure 6: Comparatif des principales caractéristiques K et de la précision Dans les ensembles de données, la justesse est inférieure à environ 20 000 caractéristiques.

Normalization

La normalisation convertit toutes les valeurs de caractéristiques/d'échantillons en valeurs petites et similaires. Cela simplifie la convergence de la descente de gradient dans les algorithmes d'apprentissage. D'après ce que nous avons vu, la normalisation lors du prétraitement des données semble ne pas apporter beaucoup de valeur aux problèmes de classification de texte. Nous vous recommandons d'ignorer cette étape.

Le code suivant réunit toutes les étapes ci-dessus:

  • Tokeniser les échantillons de texte en uni+bigrammes
  • Vectoriser à l'aide de l'encodage tf-idf
  • Sélectionnez uniquement les 20 000 premières caractéristiques du vecteur de jetons en supprimant les jetons qui apparaissent moins de deux fois et en utilisant f_classif pour calculer l'importance des caractéristiques.
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import f_classif

# Vectorization parameters
# Range (inclusive) of n-gram sizes for tokenizing text.
NGRAM_RANGE = (1, 2)

# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Whether text should be split into word or character n-grams.
# One of 'word', 'char'.
TOKEN_MODE = 'word'

# Minimum document/corpus frequency below which a token will be discarded.
MIN_DOCUMENT_FREQUENCY = 2

def ngram_vectorize(train_texts, train_labels, val_texts):
    """Vectorizes texts as n-gram vectors.

    1 text = 1 tf-idf vector the length of vocabulary of unigrams + bigrams.

    # Arguments
        train_texts: list, training text strings.
        train_labels: np.ndarray, training labels.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val: vectorized training and validation texts
    """
    # Create keyword arguments to pass to the 'tf-idf' vectorizer.
    kwargs = {
            'ngram_range': NGRAM_RANGE,  # Use 1-grams + 2-grams.
            'dtype': 'int32',
            'strip_accents': 'unicode',
            'decode_error': 'replace',
            'analyzer': TOKEN_MODE,  # Split text into word tokens.
            'min_df': MIN_DOCUMENT_FREQUENCY,
    }
    vectorizer = TfidfVectorizer(**kwargs)

    # Learn vocabulary from training texts and vectorize training texts.
    x_train = vectorizer.fit_transform(train_texts)

    # Vectorize validation texts.
    x_val = vectorizer.transform(val_texts)

    # Select top 'k' of the vectorized features.
    selector = SelectKBest(f_classif, k=min(TOP_K, x_train.shape[1]))
    selector.fit(x_train, train_labels)
    x_train = selector.transform(x_train).astype('float32')
    x_val = selector.transform(x_val).astype('float32')
    return x_train, x_val

Avec la représentation du vecteur à n-grammes, nous supprimons beaucoup d'informations sur l'ordre des mots et sur sa grammaire (au mieux, nous pouvons conserver des informations de tri partielle lorsque n > 1). C'est ce qu'on appelle une approche basée sur le sac de mots. Cette représentation est utilisée conjointement avec des modèles qui ne tiennent pas compte du tri, tels que la régression logistique, les perceptrons multicouches, les machines de boosting de gradient et les machines à vecteurs.

Vecteurs de séquence [Option B]

Dans les paragraphes suivants, nous verrons comment procéder à la tokenisation et à la vectorisation pour les modèles de séquence. Nous verrons également comment optimiser la représentation de la séquence à l'aide de techniques de sélection et de normalisation des caractéristiques.

Dans certains exemples de texte, l'ordre des mots est essentiel pour la signification du texte. Par exemple, les phrases suivantes : "Je détestais mon trajet domicile-travail. Mon nouveau vélo n'a pu être compris que dans l'ordre. Les modèles tels que les CNN/RNN peuvent déduire la signification des mots d'un échantillon. Pour ces modèles, nous représentons le texte sous la forme d'une séquence de jetons préservant l'ordre.

Tokenisation

Le texte peut être représenté par une séquence de caractères ou une séquence de mots. Nous avons constaté que la représentation au niveau du mot offre de meilleures performances que les jetons de caractère. Il s'agit également de la norme générale suivie par le secteur. L'utilisation de jetons de caractère n'est pertinente que si les textes comportent beaucoup de fautes de frappe, ce qui n'est généralement pas le cas.

Vectorisation

Une fois que nous avons converti nos échantillons de texte en séquences de mots, nous devons les transformer en vecteurs numériques. L'exemple ci-dessous montre les index attribués aux unigrammes générés pour deux textes, puis la séquence d'index de jetons auxquels le premier texte est converti.

Texts: 'The mouse ran up the clock' and 'The mouse ran down'
Index assigned for every token: {'clock': 5, 'ran': 3, 'up': 4, 'down': 6, 'the': 1, 'mouse': 2}.
NOTE: 'the' occurs most frequently, so the index value of 1 is assigned to it.
Some libraries reserve index 0 for unknown tokens, as is the case here.
Sequence of token indexes: 'The mouse ran up the clock' = [1, 2, 3, 4, 1, 5]

Deux options sont disponibles pour vectoriser les séquences de jetons:

Encodage one-hot: les séquences sont représentées par des vecteurs de mots dans un espace à n dimensions, où n = taille du vocabulaire. Cette représentation fonctionne très bien lorsque nous procédons à la tokenisation en tant que caractères, et que le vocabulaire est donc petit. Lorsque nous procédons à la tokenisation en tant que mots, le vocabulaire comporte généralement des dizaines de milliers de jetons, ce qui rend les vecteurs one-hot très creux et inefficaces. Exemple :

'The mouse ran up the clock' = [
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 1, 0, 0, 0, 0],
  [0, 0, 0, 1, 0, 0, 0],
  [0, 0, 0, 0, 1, 0, 0],
  [0, 1, 0, 0, 0, 0, 0],
  [0, 0, 0, 0, 0, 1, 0]
]

Représentations vectorielles continues de mots: les mots ont une signification. Par conséquent, nous pouvons représenter des jetons de mot dans un espace vectoriel dense (environ quelques centaines de nombres réels), où l'emplacement et la distance entre les mots indiquent leur similitude sémantique (voir la Figure 7). Cette représentation est appelée représentations vectorielles continues de mots.

Représentations vectorielles continues de mots

Figure 7: Représentations vectorielles continues de mots

Les modèles de séquence comportent souvent une couche de représentation vectorielle continue comme première couche. Cette couche apprend à transformer les séquences d'index de mots en vecteurs de représentation vectorielle continue de mots au cours du processus d'entraînement, de sorte que chaque index soit mis en correspondance avec un vecteur dense de valeurs réelles représentant l'emplacement de ce mot dans l'espace sémantique (voir la Figure 8).

Couche de représentation vectorielle continue

Figure 8: Couche de représentation vectorielle

Sélection des caractéristiques.

Tous les mots contenus dans nos données ne contribuent pas à la prédiction des étiquettes. Nous pouvons optimiser notre processus d'apprentissage en supprimant de notre vocabulaire les mots rares ou non pertinents. En fait, nous constatons que l'utilisation des 20 000 caractéristiques les plus fréquentes est généralement suffisante. C'est également le cas pour les modèles de n-grammes (voir la Figure 6).

Réunissons toutes les étapes ci-dessus dans la vectorisation séquentielle. Le code suivant effectue ces tâches:

  • Tokenise les textes en mots
  • Crée un vocabulaire à partir des 20 000 jetons principaux
  • Convertit les jetons en vecteurs de séquence
  • Recense les séquences jusqu'à une longueur de séquence fixe
from tensorflow.python.keras.preprocessing import sequence
from tensorflow.python.keras.preprocessing import text

# Vectorization parameters
# Limit on the number of features. We use the top 20K features.
TOP_K = 20000

# Limit on the length of text sequences. Sequences longer than this
# will be truncated.
MAX_SEQUENCE_LENGTH = 500

def sequence_vectorize(train_texts, val_texts):
    """Vectorizes texts as sequence vectors.

    1 text = 1 sequence vector with fixed length.

    # Arguments
        train_texts: list, training text strings.
        val_texts: list, validation text strings.

    # Returns
        x_train, x_val, word_index: vectorized training and validation
            texts and word index dictionary.
    """
    # Create vocabulary with training texts.
    tokenizer = text.Tokenizer(num_words=TOP_K)
    tokenizer.fit_on_texts(train_texts)

    # Vectorize training and validation texts.
    x_train = tokenizer.texts_to_sequences(train_texts)
    x_val = tokenizer.texts_to_sequences(val_texts)

    # Get max sequence length.
    max_length = len(max(x_train, key=len))
    if max_length > MAX_SEQUENCE_LENGTH:
        max_length = MAX_SEQUENCE_LENGTH

    # Fix sequence length to max value. Sequences shorter than the length are
    # padded in the beginning and sequences longer are truncated
    # at the beginning.
    x_train = sequence.pad_sequences(x_train, maxlen=max_length)
    x_val = sequence.pad_sequences(x_val, maxlen=max_length)
    return x_train, x_val, tokenizer.word_index

vectorisation d'étiquette

Nous avons vu comment convertir des échantillons de données textuelles en vecteurs numériques. Un processus similaire doit être appliqué aux étiquettes. Nous pouvons simplement convertir les étiquettes en valeurs dans la plage [0, num_classes - 1]. Par exemple, s'il existe trois classes, nous pouvons simplement utiliser les valeurs 0, 1 et 2 pour les représenter. En interne, le réseau utilise ces vecteurs one-hot pour représenter ces valeurs (pour éviter de déduire une relation incorrecte entre les étiquettes). Cette représentation dépend de la fonction de perte et de la fonction d'activation de la dernière couche que nous utilisons dans notre réseau de neurones. Nous les étudierons plus en détail dans la section suivante.