Paso 4: Compila, entrena y evalúa tu modelo

En esta sección, trabajaremos para compilar, entrenar y evaluar nuestro modelo. En el Paso 3, elegimos usar un modelo de n-grama o un modelo de secuencia con nuestra proporción S/W. Ahora, es momento de escribir nuestro algoritmo de clasificación y entrenarlo. Para ello, usaremos TensorFlow con la API de tf.keras.

La creación de modelos de aprendizaje automático con Keras se trata de ensamblar capas, bloques de compilación de procesamiento de datos, de la misma manera que armamos ladrillos de Lego. Estas capas nos permiten especificar la secuencia de transformaciones que queremos realizar en nuestra entrada. A medida que nuestro algoritmo de aprendizaje toma una sola entrada de texto y genera una sola clasificación, podemos crear una pila lineal de capas con la API del modelo secuencial.

Pila lineal de capas

Figura 9: Pila lineal de capas

La capa de entrada y las capas intermedias se construirán de manera diferente, según se compile un modelo n-grama o de secuencia. Pero independientemente del tipo de modelo, la última capa será la misma para un problema determinado.

Construcción de la última capa

Cuando solo tenemos 2 clases (clasificación binaria), nuestro modelo debería generar una sola puntuación de probabilidad. Por ejemplo, obtener 0.2 para una muestra de entrada determinada significa “20% de confianza de que esta muestra está en la primera clase (clase 1), 80% de que está en la segunda clase (clase 0)”. Para generar una puntuación de probabilidad de este tipo, la función de activación de la última capa debe ser una función sigmoidea.

Cuando hay más de 2 clases (clasificación de clases múltiples), nuestro modelo debe generar una puntuación de probabilidad por clase. La suma de estas puntuaciones debe ser 1. Por ejemplo, el resultado {0: 0.2, 1: 0.7, 2: 0.1} significa “20% de confianza de que este ejemplo está en la clase 0, 70% de que está en la clase 1 y 10% de que está en la clase 2”. Para generar estos resultados, la función de activación de la última capa debe ser softmax, y la función de pérdida que se usa para entrenar el modelo debe ser entropía categórica. (Consulta la Figura 10, a la derecha).

Última capa

Figura 10: Última capa

El siguiente código define una función que toma la cantidad de clases como entrada y da como resultado la cantidad adecuada de unidades de capa (1 unidad para la clasificación binaria; de lo contrario, 1 unidad para cada clase) y la función de activación adecuada:

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

En las siguientes dos secciones, se explica la creación de las capas de modelos restantes para modelos de n-grama y modelos de secuencia.

Cuando la proporción de S/W es pequeña, descubrimos que los modelos n-grama tienen un mejor rendimiento que los modelos de secuencia. Los modelos de secuencia son mejores cuando hay una gran cantidad de vectores densos y pequeños. Esto se debe a que las relaciones de incorporación se aprenden en espacios densos y suceden mejor que muchas muestras.

Compila el modelo n-grama [Option A]

Nos referimos a los modelos que procesan los tokens de forma independiente (sin tener en cuenta el orden de las palabras) como modelos de n-grama. Los perceptrones simples de varias capas (incluida la regresión logística), las máquinas de boosting de gradientes y los modelos de vectores de asistencia se encuentran en esta categoría; no pueden aprovechar la información sobre el orden del texto.

Comparamos el rendimiento de algunos de los modelos n-gramas mencionados anteriormente y observamos que los perceptrones de varias capas (MLP) suelen tener un mejor rendimiento que otras opciones. Los MLP son fáciles de definir y comprender, proporcionan una precisión adecuada y requieren un procesamiento relativamente bajo.

El siguiente código define un modelo de MLP de dos capas en tf.keras, que agrega un par de capas de retirado para la regularización (a fin de evitar el sobreajuste en las muestras de entrenamiento).

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

Modelo de secuencia de compilación [Option B]

Nos referimos a los modelos que pueden aprender de la adyacencia de los tokens como modelos de secuencia. Esto incluye las clases de modelos CNN y RNN. Los datos se procesan previamente como vectores de secuencia para estos modelos.

Por lo general, los modelos de secuencia tienen una mayor cantidad de parámetros para aprender. La primera capa de estos modelos es una capa de incorporación, que aprende la relación entre las palabras en un espacio vectorial denso. Aprender relaciones de palabras funciona mejor en muchas muestras.

Lo más probable es que las palabras de un conjunto de datos determinado no sean exclusivas de ese conjunto de datos. Por lo tanto, podemos aprender la relación entre las palabras en nuestro conjunto de datos mediante otros conjuntos de datos. Para hacerlo, podemos transferir una incorporación aprendida de otro conjunto de datos a nuestra capa de incorporación. Estas incorporaciones se conocen como incorporaciones previamente entrenadas. Usar una incorporación previamente entrenada le da al modelo una ventaja en el proceso de aprendizaje.

Hay incorporaciones previamente entrenadas disponibles que se entrenaron con clases grandes, como GloVe. GloVe ha sido entrenado en varias corporaciones (principalmente Wikipedia). Probamos entrenar nuestros modelos de secuencia con una versión de las incorporaciones de GloVe y observamos que, si congelamos los pesos de las incorporaciones previamente entrenadas y entrenamos solo el resto de la red, los modelos no tuvieron un buen rendimiento. Esto puede deberse a que el contexto en el que se entrenó la capa de incorporación puede haber sido diferente del contexto en el que la usamos.

Las incorporaciones de GloVe entrenadas en los datos de Wikipedia pueden no alinearse con los patrones de lenguaje en nuestro conjunto de datos de IMDb. Las relaciones inferidas pueden requerir cierta actualización, es decir, las ponderaciones de incorporación pueden requerir ajuste contextual. Lo hacemos en dos etapas:

  1. En la primera ejecución, con los pesos de la capa de incorporación inmovilizados, permitimos que el resto de la red aprenda. Al final de esta ejecución, los pesos del modelo alcanzan un estado que es mucho mejor que sus valores no inicializados. Para la segunda ejecución, permitimos que la capa de incorporación también aprenda y realice ajustes precisos a todas las ponderaciones en la red. Nos referimos a este proceso como el uso de una incorporación mejorada.

  2. Las incorporaciones precisas generan mayor precisión. Sin embargo, esto ocurre con la mayor potencia de procesamiento necesaria para entrenar la red. Dada una cantidad suficiente de muestras, también podríamos aprender una incorporación desde cero. Observamos que, para S/W > 15K, a partir de cero, se obtiene casi la misma exactitud que con la incorporación precisa.

Comparamos diferentes modelos de secuencia, como CNN, sepCNN, RNN (LSTM y GRU), CNN-RNN y RNN apilados, que varían según las arquitecturas del modelo. Descubrimos que las sepCNN, una variante de red convolucional que suele ser más eficiente en cuanto a datos y procesamiento, tienen un mejor rendimiento que los otros modelos.

El siguiente código construye un modelo sepCNN de cuatro capas:

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

Entrena tu modelo

Ahora que construimos la arquitectura del modelo, necesitamos entrenar el modelo. El entrenamiento implica hacer una predicción en función del estado actual del modelo, calcular qué tan incorrecta es la predicción y actualizar las ponderaciones o los parámetros de la red para minimizar este error y hacer que el modelo prediga mejor. Repetimos este proceso hasta que nuestro modelo haya convergido y ya no pueda aprender. Se deben elegir tres parámetros clave para este proceso (consulta la Tabla 2.

  • Métrica: cómo medir el rendimiento de nuestro modelo mediante una métrica. Usamos la precisión como la métrica en nuestros experimentos.
  • Función de pérdida: función que se usa para calcular un valor de pérdida que el proceso de entrenamiento intenta minimizar mediante el ajuste de los pesos de la red. Para problemas de clasificación, la pérdida de entropía cruzada funciona bien.
  • Optimizador: Es una función que decide cómo se actualizarán los pesos de la red en función del resultado de la función de pérdida. Usamos el optimizador Adam popular en nuestros experimentos.

En Keras, podemos pasar estos parámetros de aprendizaje a un modelo con el método compile.

Parámetro de aprendizaje Valor
Métrica accuracy
Función de pérdida: Clasificación binaria entropía_binaria
Función de pérdida: Clasificación de clases múltiples entropía_categórica_dispersa
Optimizador adán

Tabla 2: Parámetros de aprendizaje

El entrenamiento real ocurre con el método fit. Según el tamaño de tu conjunto de datos, este es el método en el que se gastará la mayoría de los ciclos de procesamiento. En cada iteración de entrenamiento, se usa la cantidad batch_size de muestras de tus datos de entrenamiento para calcular la pérdida y las ponderaciones se actualizan una vez, según este valor. El proceso de entrenamiento completa un epoch una vez que el modelo vio el conjunto de datos de entrenamiento completo. Al final de cada ciclo de entrenamiento, usamos el conjunto de datos de validación para evaluar qué tan bien aprende el modelo. Repetimos el entrenamiento con el conjunto de datos para una cantidad predeterminada de ciclos de entrenamiento. Es posible que optimicemos esto si nos detenemos antes, cuando la exactitud de la validación se estabiliza entre ciclos de entrenamiento consecutivos, lo que muestra que el modelo ya no está entrenándose.

Hiperparámetro de entrenamiento Valor
Tasa de aprendizaje Entre 1e y 3
Ciclos de entrenamiento 1,000
Tamaño del lote 512
Interrupción anticipada parámetro: val_loss, paciencia: 1

Tabla 3: Hiperparámetros de entrenamiento

El siguiente código de Keras implementa el proceso de entrenamiento con los parámetros elegidos en las tablas 2 y 3 anteriores:

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]

Puedes encontrar ejemplos de código para entrenar el modelo de secuencia aquí.