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

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

La compilación de modelos de aprendizaje automático con Keras consiste en ensamblar capas y componentes básicos de procesamiento de datos, al igual que ensamblaríamos los ladrillos Lego. Estas capas nos permiten especificar la secuencia de transformaciones que queremos realizar en nuestra entrada. Como nuestro algoritmo de aprendizaje recibe 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, en función de si compilamos un n-grama o un modelo de secuencia. Sin embargo, 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 única puntuación de probabilidad. Por ejemplo, generar 0.2 para una muestra de entrada determinada significa "un 20% de confianza de que esta muestra está en la primera clase (clase 1) y un 80% de que está en la segunda clase (clase 0)". Para obtener esa puntuación de probabilidad, la función de activación de la última capa debe ser una función sigmoidea, y la función de entropía cruzada que se usa para entrenar el modelo. (Consulta la figura 10, izquierda).

Cuando hay más de 2 clases (clasificación de clases múltiples), nuestro modelo debería generar una puntuación de probabilidad por clase. La suma de estas puntuaciones debe ser 1. Por ejemplo, generar {0: 0.2, 1: 0.7, 2: 0.1} significa “un 20% de confianza de que esta muestra está en la clase 0, un 70% de que está en la clase 1 y un 10% de que está en la clase 2”. Para obtener estas puntuaciones, la función de activación de la última capa debe ser softmax, y la función de pérdida usada para entrenar el modelo debe ser la entropía cruzada 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 genera la cantidad apropiada 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 restantes del modelo para modelos n-grama y modelos de secuencia.

Cuando la proporción 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 pequeños y densos. Esto se debe a que las relaciones de incorporación se aprenden en un espacio denso y esto sucede mejor con muchas muestras.

Compilar un modelo n-grama [opción A]

Nos referimos a los modelos que procesan los tokens de forma independiente (sin tener en cuenta el orden de las palabras) como modelos n-grama. Los perceptrones simples de varias capas (incluidas las máquinas de boosting de gradientes de regresión logística y los modelos de máquinas de vectores de soporte) pertenecen a esta categoría. No pueden usar información sobre el orden del texto.

Comparamos el rendimiento de algunos de los modelos de n-grama 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 buena exactitud y requieren relativamente poco procesamiento.

Con el siguiente código, se define un modelo de MLP de dos capas en tf.keras y se agregan algunas capas de retirados para la regularización a fin de evitar el sobreajuste a 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 [opción B]

Nos referimos a los modelos que pueden aprender de la proximidad 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.

Los modelos de secuencia suelen tener una mayor cantidad de parámetros para aprender. La primera capa en estos modelos es una capa de incorporación, que aprende la relación entre las palabras en un espacio vectorial denso. Aprender las 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. De este modo, podemos aprender la relación entre las palabras en nuestro conjunto de datos mediante el uso de otros conjuntos de datos. Para ello, podemos transferir una incorporación obtenida de otro conjunto de datos a nuestra capa de incorporación. Estas incorporaciones se conocen como incorporaciones previamente entrenadas. El uso de una incorporación previamente entrenada le da al modelo una ventaja en el proceso de aprendizaje.

Hay incorporaciones previamente entrenadas que se entrenaron con corpus grandes, como GloVe. GloVe se entrenó con varios corpus (principalmente en Wikipedia). Probamos el entrenamiento de nuestros modelos de secuencias con una versión de incorporaciones GloVe y observamos que si congelábamos los pesos de las incorporaciones previamente entrenadas y entrenamos solo el resto de la red, los modelos no tuvieron un buen rendimiento. Esto podría deberse a que el contexto en el que se entrenó la capa de incorporación podría ser diferente del contexto en el que la estábamos usando.

Es posible que las incorporaciones de GloVe entrenadas con datos de Wikipedia no se alineen con los patrones de lenguaje de nuestro conjunto de datos de IMDb. Es posible que las relaciones inferidas necesiten un poco de actualización; es decir, los pesos de incorporación pueden necesitar ajustes contextual. Esto lo hacemos en dos etapas:

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

  2. Las incorporaciones ajustadas generan una mayor exactitud. Sin embargo, esto se genera con el gasto de una mayor potencia de procesamiento necesaria para entrenar la red. Si la cantidad es suficiente, podríamos aprender lo mismo sobre una incorporación desde cero. Observamos que, para S/W > 15K, comenzar desde cero produce de manera eficaz casi la misma exactitud que usar una incorporación ajustada.

Comparamos diferentes modelos de secuencias, como CNN, sepCNN, RNN (LSTM y GRU), CNN-RNN y RNN apilado, mediante la variación de las arquitecturas del modelo. Descubrimos que sepCNN, una variante de red convolucional que suele ser más eficiente en cuanto a los datos y el procesamiento, tiene 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, debemos entrenar el modelo. El entrenamiento implica realizar una predicción basada en el estado actual del modelo, calcular qué tan incorrecta es la predicción y actualizar los pesos 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 converja 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 con una métrica. Usamos la exactitud como métrica en nuestros experimentos.
  • Función de pérdida: Es una 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 los 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 mediante el método compile.

Tabla 2: Parámetros de aprendizaje

Parámetro de aprendizaje Valor
Métrica accuracy
Función de pérdida: clasificación binaria binary_crossentropy
Función de pérdida: clasificación de clases múltiples sparse_categorical_crossentropy
Optimizador adam

El entrenamiento real se realiza con el método fit. Según el tamaño de tu conjunto de datos, este es el método en el que se consumirán la mayoría de los ciclos de procesamiento. En cada iteración de entrenamiento, se usa la cantidad de muestras de batch_size de tus datos de entrenamiento para calcular la pérdida, y los pesos se actualizan una vez, según este valor. El proceso de entrenamiento completa una epoch una vez que el modelo haya visto 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 un número predeterminado de ciclos de entrenamiento. Para optimizar esto, podemos detenernos antes, cuando la exactitud de la validación se estabiliza entre ciclos de entrenamiento consecutivos, lo que demuestra que el modelo ya no se entrena.

Hiperparámetro de entrenamiento Valor
Tasa de aprendizaje 1e-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 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]

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