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

Dans cette section, nous allons travailler à 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-gramme ou de séquence avec 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.

Créer des modèles de machine learning avec Keras consiste à assembler des couches, des composants de traitement des données, un peu comme nous assembler des briques Lego. Ces couches nous permettent de spécifier la séquence de transformations que nous souhaitons effectuer sur notre entrée. Étant donné que notre algorithme d'apprentissage analyse 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 de couches linéaire

Figure 9: Pile de couches linéaire

La couche d'entrée et les couches intermédiaires seront construites différemment, selon que nous créons un n-gramme ou un modèle de séquence. 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 renvoyer un seul score de probabilité. Par exemple, la sortie de 0.2 pour un échantillon d'entrée donné signifie que "20% des chances que cet échantillon soit 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 doit être 1} .

Lorsqu'il existe plus de deux classes (classification multiclasse), notre modèle doit générer un score de probabilité par classe. La somme de ces scores doit être égale à 1. Par exemple, la sortie de {0: 0.2, 1: 0.7, 2: 0.1} signifie"20% de confiance pour que cet échantillon soit dans la classe 0, 70% qu'il soit dans la classe 1 et 10% qu'il se trouve dans la classe 2". Pour obtenir 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 prend 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) et 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 à n-grammes obtiennent de meilleures performances que les modèles de séquence. Les modèles de séquence sont plus efficaces lorsqu'il existe un grand nombre de petits vecteurs denses. En effet, les relations de représentation vectorielle sont apprises dans un espace dense, ce qui se produit mieux sur de nombreux échantillons.

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

Les modèles qui traitent les jetons indépendamment (sans tenir compte de l'ordre des mots) sont appelés modèles à n-grammes. Les perceptrons multicouches simples (y compris la régression logistique), les machines de boosting de gradient et les machines à vecteurs sont tous concernés par cette catégorie. Ils ne peuvent pas exploiter les informations sur l'ordre du texte.

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

Le code suivant définit un modèle de MLP à deux couches dans tf.keras, en ajoutant quelques couches d'abandon pour la régularisation (pour é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]

Les modèles 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 de séquence 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ésentations vectorielles continues, qui apprend la relation entre les mots d'un espace vectoriel dense. L'apprentissage des relations verbales fonctionne mieux sur de nombreux exemples.

Les mots d'un ensemble de données spécifique ne sont très probablement pas propres à cet ensemble de données. Nous pouvons ainsi apprendre la relation entre les mots de notre ensemble de données à l'aide d'autres ensembles de données. Pour ce faire, nous pouvons transférer une représentation vectorielle continue 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 permet au modèle de prendre une longueur d'avance dans le processus d'apprentissage.

Des représentations vectorielles continues pré-entraînées disponibles 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, si nous gelions les pondérations des représentations vectorielles continues pré-entraînées et n'entraînions que le reste du réseau, ces modèles ne donneraient pas de bons résultats. Cela peut être dû au fait que le contexte dans lequel la couche de représentations vectorielles continues a été entraîné peut être différent de celui dans lequel nous l'utilisions.

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. Par exemple, les pondérations des représentations vectorielles continues peuvent nécessiter un ajustement contextuel. Nous procédons en deux étapes:

  1. Lors de la première exécution, avec les pondérations de la couche de représentations vectorielles continues figées, nous permettons au reste du réseau d'apprendre. À la fin de cette exécution, les pondérations du modèle atteignent un état bien meilleur que celui de ses valeurs non initialisées. Pour la seconde exécution, nous autorisons la couche d'intégration à apprendre également, en effectuant des ajustements précis pour toutes les pondérations du réseau. Ce processus consiste à utiliser une représentation vectorielle continue optimisée.

  2. Les représentations vectorielles continues précises améliorent la précision. Toutefois, cela entraîne une augmentation de la puissance de calcul nécessaire à l'entraînement du réseau. Compte tenu d'un nombre suffisant d'échantillons, nous pourrions tout aussi bien apprendre une représentation vectorielle continue à partir de zéro. Nous avons constaté que, pour S/W > 15K, partir de zéro permet d'obtenir à peu près la même justesse que l'optimisation précise de la représentation vectorielle.

Nous avons comparé différents modèles séquentiels tels que CNN, sepCNN, RNN (LSTM &GRU), CNN-RNN et RNN empilé, en faisant varier les architectures de modèles. Nous avons découvert que sepCNNs, une variante de réseau convolutif souvent plus économe en données et en calcul, est plus performante 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 implique une prédiction basée sur l'état actuel du modèle, le calcul de l'erreur de prédiction et la mise à jour des pondérations ou des paramètres du réseau afin de minimiser cette erreur et d'améliorer la prédiction du modèle. Nous répétons ce processus jusqu'à ce que notre modèle converge et ne puisse plus apprendre. Vous devez choisir trois paramètres clés pour ce processus (consultez le Tableau 2).

  • Métrique : comment mesurer les performances de notre 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étermine la manière dont les pondérations du réseau seront mises à jour en fonction du résultat de la fonction de perte. Nous avons utilisé le célèbre optimiseur Adam lors de nos tests.

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

Paramètre d'apprentissage Value
Métrique accuracy
Fonction de perte – Classification binaire entropie binaire
Fonction de perte – Classification multiclasse croisée_croisée_catégorique
Optimiseur Adam

Tableau 2: Paramètres d'apprentissage

L'entraînement proprement dit s'effectue via la méthode fit. Selon la taille de votre ensemble de données, il s'agit de la méthode dans laquelle la plupart des cycles de calcul seront dépensés. À chaque itération d'entraînement, le nombre batch_size d'échantillons issus de vos données d'entraînement est utilisé pour calculer la perte, puis les pondérations sont mises à jour une seule fois en fonction de cette valeur. Le processus d'entraînement termine un epoch une fois que le modèle a vu l'intégralité 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 les performances d'apprentissage du modèle. Nous répétons l'entraînement à l'aide de l'ensemble de données pendant un nombre prédéterminé d'époques. Nous pouvons optimiser cela en arrêtant tôt le processus, lorsque la justesse de la validation se stabilise entre les époques consécutives, ce qui indique que le modèle ne s'entraîne plus.

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

Tableau 3: Entraîner des hyperparamètres

Le code Keras suivant met en œuvre le processus d'entraînement à l'aide des paramètres choisis dans les tables 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 obtenir des exemples de code permettant d'entraîner le modèle de séquence, cliquez ici.