Étape 4: Créer, entraîner et évaluer votre modèle

Dans cette section, nous allons aborder la création, l'entraînement et l'évaluation de notre modèle. À l'étape 3, nous avons choisi d'utiliser un modèle de n-grammes ou un modèle de séquence en utilisant notre ratio S/W. Il est maintenant temps d'écrire notre algorithme de classification et de l'entraîner. Pour cela, nous allons utiliser TensorFlow avec l'API tf.keras.

La création de modèles de machine learning avec Keras consiste à assembler des couches, des composants fondamentaux de traitement des données, de la même manière que nous assemblerions des briques Lego. Ces couches nous permettent de spécifier la séquence de transformations à effectuer sur notre entrée. Comme notre algorithme d'apprentissage reçoit une seule entrée de texte et génère une classification unique, nous pouvons créer une pile linéaire de couches à l'aide de l'API du modèle Sequential.

Pile linéaire de couches

Figure 9: Pile linéaire de calques

La couche d'entrée et les couches intermédiaires seront construites différemment, selon que nous construisons un modèle de n-gramme ou de séquence. Mais quel que soit le type de modèle, la dernière couche sera la même pour un problème donné.

Construire la dernière couche

Lorsque nous n'avons que deux classes (classification binaire), notre modèle doit générer un seul score de probabilité. Par exemple, obtenir 0.2 pour un échantillon d'entrée donné signifie "20% de confiance que cet échantillon se trouve dans la première classe (classe 1) et 80% qu'il se trouve dans la deuxième classe (classe 0)". Pour générer un tel score de probabilité, la fonction d'activation de la dernière couche doit être une fonction sigmoïde et la fonction de perte utilisée pour entraîner le modèle croisé doit être binaire. (voir la Figure 10, à gauche).

Lorsqu'il y a plus de deux classes (classification à classes multiples), notre modèle doit générer un score de probabilité par classe. La somme de ces scores doit être égale à 1. Par exemple, obtenir {0: 0.2, 1: 0.7, 2: 0.1} signifie "20% de confiance pour que cet échantillon se trouve dans la classe 0, 70% pour qu'il appartienne à la classe 1 et 10% pour qu'il appartienne à la classe 2". Pour générer ces scores, la fonction d'activation de la dernière couche doit être softmax, et la fonction de perte utilisée pour entraîner le modèle doit être une entropie croisée catégorielle. (voir la Figure 10, à droite).

Dernier calque

Figure 10: Dernière couche

Le code suivant définit une fonction qui accepte le nombre de classes en entrée et génère le nombre approprié d'unités de couche (une unité pour la classification binaire, sinon une unité pour chaque classe) ainsi que la fonction d'activation appropriée:

def _get_last_layer_units_and_activation(num_classes):
    """Gets the # units and activation function for the last network layer.

    # Arguments
        num_classes: int, number of classes.

    # Returns
        units, activation values.
    """
    if num_classes == 2:
        activation = 'sigmoid'
        units = 1
    else:
        activation = 'softmax'
        units = num_classes
    return units, activation

Les deux sections suivantes expliquent comment créer les couches de modèle restantes pour les modèles de n-grammes et les modèles de séquence.

Lorsque le ratio S/W est faible, nous avons constaté que les modèles de n-grammes étaient plus performants que les modèles de séquence. Les modèles séquentiels sont plus efficaces lorsqu'il existe un grand nombre de petits vecteurs denses. En effet, les relations de représentation vectorielle continue sont apprises dans un espace dense, ce qui se produit mieux sur de nombreux échantillons.

Créer un modèle de n-grammes [Option A]

Les modèles qui traitent les jetons de manière indépendante (sans tenir compte de l'ordre des mots) sont appelés modèles de n-grammes. Les perceptrons simples à plusieurs couches (y compris les machines d'optimisation de gradient de régression logistique et les modèles de machines à vecteur de support) appartiennent tous à cette catégorie. Ils ne peuvent utiliser aucune information concernant l'ordre du texte.

Nous avons comparé les performances de certains des modèles de n-grammes mentionnés ci-dessus et constaté que les perceptrons multicouches (MLP) étaient généralement plus performants que les autres options. Les MLP sont simples à définir et à comprendre, offrent une bonne précision et nécessitent relativement peu de calculs.

Le code suivant définit un modèle MLP à deux couches dans tf.keras, en ajoutant quelques couches d'abandon pour la régularisation afin d'éviter le surapprentissage des échantillons d'entraînement.

from tensorflow.python.keras import models
from tensorflow.python.keras.layers import Dense
from tensorflow.python.keras.layers import Dropout

def mlp_model(layers, units, dropout_rate, input_shape, num_classes):
    """Creates an instance of a multi-layer perceptron model.

    # Arguments
        layers: int, number of `Dense` layers in the model.
        units: int, output dimension of the layers.
        dropout_rate: float, percentage of input to drop at Dropout layers.
        input_shape: tuple, shape of input to the model.
        num_classes: int, number of output classes.

    # Returns
        An MLP model instance.
    """
    op_units, op_activation = _get_last_layer_units_and_activation(num_classes)
    model = models.Sequential()
    model.add(Dropout(rate=dropout_rate, input_shape=input_shape))

    for _ in range(layers-1):
        model.add(Dense(units=units, activation='relu'))
        model.add(Dropout(rate=dropout_rate))

    model.add(Dense(units=op_units, activation=op_activation))
    return model

Créer un modèle de séquence [Option B]

Nous faisons référence à des modèles qui peuvent apprendre de la contiguïté des jetons en tant que modèles de séquence. Cela inclut les classes de modèles CNN et RNN. Les données sont prétraitées en tant que vecteurs séquentiels pour ces modèles.

Les modèles de séquence ont généralement un plus grand nombre de paramètres à apprendre. La première couche de ces modèles est une couche de représentation vectorielle continue, qui apprend la relation entre les mots dans un espace vectoriel dense. L'apprentissage des relations entre les mots fonctionne mieux sur de nombreux échantillons.

Les mots d'un jeu de données ne sont probablement pas uniques à ce jeu de données. Nous pouvons ainsi apprendre la relation entre les mots de notre ensemble de données en utilisant un ou plusieurs autres ensembles de données. Pour ce faire, nous pouvons transférer une représentation vectorielle continue apprise à partir d'un autre ensemble de données dans notre couche de représentation vectorielle continue. Ces représentations vectorielles continues sont appelées représentations vectorielles continues pré-entraînées. L'utilisation d'une représentation vectorielle continue pré-entraînée donne au modèle une longueur d'avance dans le processus d'apprentissage.

Des représentations vectorielles continues pré-entraînées sont disponibles et ont été entraînées à l'aide de grands corpus, tels que GloVe. GloVe a été entraîné sur plusieurs corpus (principalement Wikipédia). Nous avons testé l'entraînement de nos modèles séquentiels à l'aide d'une version de représentations vectorielles continues GloVe et avons constaté que les modèles ne fonctionnaient pas bien si nous figetions les pondérations des représentations vectorielles continues pré-entraînées et n'entraînions que le reste du réseau. Cela peut être dû au fait que le contexte dans lequel la couche de représentation vectorielle continue a été entraînée peut être différent de celui dans lequel nous l'avons utilisée.

Les représentations vectorielles continues GloVe entraînées sur des données Wikipédia peuvent ne pas correspondre aux modèles de langage de notre ensemble de données IMDb. Les relations déduites peuvent nécessiter une mise à jour, c'est-à-dire que les pondérations des représentations vectorielles continues peuvent nécessiter un réglage contextuel. Pour cela, nous procédons en deux étapes:

  1. Lors de la première exécution, avec les pondérations de la couche de représentation vectorielle continue figée, nous laissons le reste du réseau apprendre. À la fin de cette exécution, les pondérations du modèle atteignent un état nettement supérieur à leurs valeurs non initialisées. Pour la deuxième exécution, nous permettons à la couche de représentation vectorielle continue d'apprendre également, en ajustant toutes les pondérations du réseau avec précision. Nous appelons ce processus l'utilisation d'une représentation vectorielle continue affinée.

  2. Les représentations vectorielles continues affinées offrent une meilleure précision. Toutefois, cela s'accompagne d'une augmentation de la puissance de calcul nécessaire à l'entraînement du réseau. Avec un nombre suffisant d'échantillons, nous pourrions tout aussi bien apprendre une représentation vectorielle continue à partir de zéro. Nous avons observé que, pour S/W > 15K, partir de zéro produit un rendement à peu près identique à celui obtenu avec une intégration affinée.

Nous avons comparé différents modèles de séquence tels que CNN, sepCNN, RNN (LSTM & GRU), CNN-RNN et RNN empilé, en variant les architectures de modèles. Nous avons constaté que les sepCNN, une variante de réseau convolutif souvent plus efficace en termes de données et de calcul, sont plus performants que les autres modèles.

Le code suivant construit un modèle sepCNN à quatre couches:

from tensorflow.python.keras import models
from tensorflow.python.keras import initializers
from tensorflow.python.keras import regularizers

from tensorflow.python.keras.layers import Dense
from tensorflow.python.keras.layers import Dropout
from tensorflow.python.keras.layers import Embedding
from tensorflow.python.keras.layers import SeparableConv1D
from tensorflow.python.keras.layers import MaxPooling1D
from tensorflow.python.keras.layers import GlobalAveragePooling1D

def sepcnn_model(blocks,
                 filters,
                 kernel_size,
                 embedding_dim,
                 dropout_rate,
                 pool_size,
                 input_shape,
                 num_classes,
                 num_features,
                 use_pretrained_embedding=False,
                 is_embedding_trainable=False,
                 embedding_matrix=None):
    """Creates an instance of a separable CNN model.

    # Arguments
        blocks: int, number of pairs of sepCNN and pooling blocks in the model.
        filters: int, output dimension of the layers.
        kernel_size: int, length of the convolution window.
        embedding_dim: int, dimension of the embedding vectors.
        dropout_rate: float, percentage of input to drop at Dropout layers.
        pool_size: int, factor by which to downscale input at MaxPooling layer.
        input_shape: tuple, shape of input to the model.
        num_classes: int, number of output classes.
        num_features: int, number of words (embedding input dimension).
        use_pretrained_embedding: bool, true if pre-trained embedding is on.
        is_embedding_trainable: bool, true if embedding layer is trainable.
        embedding_matrix: dict, dictionary with embedding coefficients.

    # Returns
        A sepCNN model instance.
    """
    op_units, op_activation = _get_last_layer_units_and_activation(num_classes)
    model = models.Sequential()

    # Add embedding layer. If pre-trained embedding is used add weights to the
    # embeddings layer and set trainable to input is_embedding_trainable flag.
    if use_pretrained_embedding:
        model.add(Embedding(input_dim=num_features,
                            output_dim=embedding_dim,
                            input_length=input_shape[0],
                            weights=[embedding_matrix],
                            trainable=is_embedding_trainable))
    else:
        model.add(Embedding(input_dim=num_features,
                            output_dim=embedding_dim,
                            input_length=input_shape[0]))

    for _ in range(blocks-1):
        model.add(Dropout(rate=dropout_rate))
        model.add(SeparableConv1D(filters=filters,
                                  kernel_size=kernel_size,
                                  activation='relu',
                                  bias_initializer='random_uniform',
                                  depthwise_initializer='random_uniform',
                                  padding='same'))
        model.add(SeparableConv1D(filters=filters,
                                  kernel_size=kernel_size,
                                  activation='relu',
                                  bias_initializer='random_uniform',
                                  depthwise_initializer='random_uniform',
                                  padding='same'))
        model.add(MaxPooling1D(pool_size=pool_size))

    model.add(SeparableConv1D(filters=filters * 2,
                              kernel_size=kernel_size,
                              activation='relu',
                              bias_initializer='random_uniform',
                              depthwise_initializer='random_uniform',
                              padding='same'))
    model.add(SeparableConv1D(filters=filters * 2,
                              kernel_size=kernel_size,
                              activation='relu',
                              bias_initializer='random_uniform',
                              depthwise_initializer='random_uniform',
                              padding='same'))
    model.add(GlobalAveragePooling1D())
    model.add(Dropout(rate=dropout_rate))
    model.add(Dense(op_units, activation=op_activation))
    return model

Entraîner le modèle

Maintenant que nous avons construit l'architecture du modèle, nous devons l'entraîner. L'entraînement consiste à effectuer une prédiction basée sur l'état actuel du modèle, à calculer à quel point la prédiction est incorrecte et à mettre à jour les pondérations ou les paramètres du réseau afin de minimiser cette erreur et d'améliorer les prédictions du modèle. Nous répétons ce processus jusqu'à ce que le modèle converge et ne puisse plus apprendre. Vous devez choisir trois paramètres clés pour ce processus (voir le Tableau 2).

  • Métrique: comment mesurer les performances du modèle à l'aide d'une métrique. Nous avons utilisé la métrique accuracy (justesse) dans nos tests.
  • Fonction de perte: fonction utilisée pour calculer une valeur de perte que le processus d'entraînement tente ensuite de minimiser en ajustant les pondérations du réseau. Pour les problèmes de classification, la perte d'entropie croisée fonctionne bien.
  • Optimizer: fonction qui décide comment les pondérations du réseau seront mises à jour en fonction du résultat de la fonction de perte. Nous avons utilisé l'optimiseur populaire Adam dans nos tests.

Dans Keras, nous pouvons transmettre ces paramètres d'apprentissage à un modèle à l'aide de la méthode compile.

Tableau 2: Paramètres d'apprentissage

Paramètre d'apprentissage Valeur
Métrique accuracy
Fonction de perte : classification binaire binary_crossentropy
Fonction de perte – classification à classes multiples sparse_categorical_crossentropy
Optimiseur adam

L'entraînement proprement dit s'effectue à l'aide de la méthode fit. En fonction de la taille de votre ensemble de données, il s'agit de la méthode qui consiste à passer la plupart des cycles de calcul. À chaque itération d'entraînement, un nombre batch_size d'échantillons de vos données d'entraînement est utilisé pour calculer la perte, et les pondérations sont mises à jour une fois en fonction de cette valeur. Le processus d'entraînement termine une epoch une fois que le modèle a vu la totalité de l'ensemble de données d'entraînement. À la fin de chaque époque, nous utilisons l'ensemble de données de validation pour évaluer le niveau d'apprentissage du modèle. Nous répétons l'entraînement avec l'ensemble de données pendant un nombre d'époques prédéterminé. Pour l'optimiser, nous pouvons arrêter prématurément, lorsque la justesse de la validation se stabilise entre des époques consécutives, indiquant ainsi que le modèle n'est plus en cours d'entraînement.

Hyperparamètre d'entraînement Valeur
Taux d'apprentissage 1e à 3
Époques 1 000
Taille de lot 512
Arrêt prématuré paramètre: val_loss, patience: 1

Tableau 3: Hyperparamètres d'entraînement

Le code Keras suivant met en œuvre le processus d'entraînement à l'aide des paramètres choisis dans les tableaux 2 et 3 ci-dessus:

def train_ngram_model(data,
                      learning_rate=1e-3,
                      epochs=1000,
                      batch_size=128,
                      layers=2,
                      units=64,
                      dropout_rate=0.2):
    """Trains n-gram model on the given dataset.

    # Arguments
        data: tuples of training and test texts and labels.
        learning_rate: float, learning rate for training model.
        epochs: int, number of epochs.
        batch_size: int, number of samples per batch.
        layers: int, number of `Dense` layers in the model.
        units: int, output dimension of Dense layers in the model.
        dropout_rate: float: percentage of input to drop at Dropout layers.

    # Raises
        ValueError: If validation data has label values which were not seen
            in the training data.
    """
    # Get the data.
    (train_texts, train_labels), (val_texts, val_labels) = data

    # Verify that validation labels are in the same range as training labels.
    num_classes = explore_data.get_num_classes(train_labels)
    unexpected_labels = [v for v in val_labels if v not in range(num_classes)]
    if len(unexpected_labels):
        raise ValueError('Unexpected label values found in the validation set:'
                         ' {unexpected_labels}. Please make sure that the '
                         'labels in the validation set are in the same range '
                         'as training labels.'.format(
                             unexpected_labels=unexpected_labels))

    # Vectorize texts.
    x_train, x_val = vectorize_data.ngram_vectorize(
        train_texts, train_labels, val_texts)

    # Create model instance.
    model = build_model.mlp_model(layers=layers,
                                  units=units,
                                  dropout_rate=dropout_rate,
                                  input_shape=x_train.shape[1:],
                                  num_classes=num_classes)

    # Compile model with learning parameters.
    if num_classes == 2:
        loss = 'binary_crossentropy'
    else:
        loss = 'sparse_categorical_crossentropy'
    optimizer = tf.keras.optimizers.Adam(lr=learning_rate)
    model.compile(optimizer=optimizer, loss=loss, metrics=['acc'])

    # Create callback for early stopping on validation loss. If the loss does
    # not decrease in two consecutive tries, stop training.
    callbacks = [tf.keras.callbacks.EarlyStopping(
        monitor='val_loss', patience=2)]

    # Train and validate model.
    history = model.fit(
            x_train,
            train_labels,
            epochs=epochs,
            callbacks=callbacks,
            validation_data=(x_val, val_labels),
            verbose=2,  # Logs once per epoch.
            batch_size=batch_size)

    # Print results.
    history = history.history
    print('Validation accuracy: {acc}, loss: {loss}'.format(
            acc=history['val_acc'][-1], loss=history['val_loss'][-1]))

    # Save model.
    model.save('IMDb_mlp_model.h5')
    return history['val_acc'][-1], history['val_loss'][-1]

Pour trouver des exemples de code pour l'entraînement du modèle de séquence, cliquez ici.