Etapa 4: criar, treinar e avaliar o modelo

Nesta seção, trabalharemos para criar, treinar e avaliar nosso modelo. Na Etapa 3, escolhemos usar um modelo n-gram ou de sequência, utilizando nossa proporção S/W. Agora é hora de escrever e treinar nosso algoritmo de classificação. Para isso, usaremos o TensorFlow com a API tf.keras.

A criação de modelos de machine learning com o Keras consiste em reunir camadas e elementos básicos de processamento de dados, assim como nós montamos blocos de Lego. Essas camadas permitem especificar a sequência de transformações que queremos realizar na entrada. Como nosso algoritmo de aprendizado recebe uma única entrada de texto e gera uma única classificação, podemos criar uma pilha linear de camadas usando a API Modelo sequencial.

Pilha linear de camadas

Figura 9: pilha linear de camadas

A camada de entrada e as camadas intermediárias serão construídas de maneira diferente, dependendo se estamos construindo um n-gram ou um modelo sequencial. Independentemente do tipo de modelo, a última camada será a mesma para qualquer problema.

Como criar a última camada

Quando temos apenas 2 classes (classificação binária), o modelo gera uma única pontuação de probabilidade. Por exemplo, gerar 0.2 para uma determinada amostra de entrada significa "20% de confiança de que essa amostra está na primeira classe (classe 1), 80% que ela está na segunda classe (classe 0)". Para produzir essa pontuação de probabilidade, a função de ativação da última camada precisa ser uma função sigmoide, e a função de perda usada para treinar o modelo deve ser binária de perda. Consulte a Figura 10 à esquerda.

Quando há mais de duas classes (classificação multiclasse), nosso modelo gera uma pontuação de probabilidade por classe. A soma dessas pontuações deve ser 1. Por exemplo, gerar {0: 0.2, 1: 0.7, 2: 0.1} significa "20% de confiança de que essa amostra está na classe 0, 70% que está na classe 1 e 10% que está na classe 2". Para gerar essas pontuações, a função de ativação da última camada precisa ser softmax, e a função de perda usada para treinar o modelo precisa ser entropia cruzada categórica. Consulte a Figura 10, à direita.

Última camada

Figura 10: última camada

O código a seguir define uma função que recebe o número de classes como entrada e gera o número apropriado de unidades de camada (1 unidade para classificação binária, caso contrário, 1 unidade para cada classe) e a função de ativação adequada:

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

As duas seções a seguir mostram a criação das camadas de modelo restantes para modelos n-gram e sequenciais.

Quando a proporção S/W é pequena, descobrimos que os modelos n-gram têm melhor desempenho do que os modelos sequenciais. Os modelos sequenciais são melhores quando há um grande número de vetores pequenos e densos. Isso ocorre porque as relações de embedding são aprendidas em um espaço denso, o que acontece melhor em muitas amostras.

Criar um modelo n-grama [opção A]

Os modelos que processam os tokens de maneira independente (sem considerar a ordem das palavras) são chamados de modelos n-gram. Perceptrons de várias camadas simples (incluindo regressão logística, máquinas de otimização de gradiente e modelos de máquinas de vetor de suporte) se enquadram nessa categoria. Eles não podem usar nenhuma informação sobre ordenação de texto.

Comparamos o desempenho de alguns dos modelos "n-gram" mencionados acima e observamos que perceptrons de várias camadas (MLPs, na sigla em inglês) normalmente têm melhor desempenho do que outras opções. As MLPs são simples de definir e entender, proporcionam boa precisão e exigem relativamente pouca computação.

O código a seguir define um modelo MLP de duas camadas no tf.keras, adicionando algumas camadas de dropout para regularização e evitar o overfitting (links em inglês) em amostras de treinamento.

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

Criar modelo sequencial [Opção B]

Nos referimos a modelos que podem aprender com a contingência de tokens como modelos de sequência. Isso inclui classes de modelos CNN e RNN. Os dados são pré-processados como vetores sequenciais para esses modelos.

Os modelos sequenciais geralmente têm um número maior de parâmetros para aprender. A primeira camada nesses modelos é uma de embedding, que aprende a relação entre as palavras em um espaço vetorial denso. Aprender relações entre palavras funciona melhor em muitas amostras.

As palavras de um determinado conjunto de dados provavelmente não são exclusivas desse conjunto. Podemos, assim, aprender a relação entre as palavras em nosso conjunto de dados usando outros conjuntos de dados. Para fazer isso, podemos transferir um embedding aprendido de outro conjunto de dados para nossa camada de embedding. Esses embeddings são chamados de embeddings pré-treinados. O uso de uma incorporação pré-treinada dá ao modelo uma vantagem no processo de aprendizado.

Existem embeddings pré-treinados disponíveis que foram treinados usando corpos grandes, como o GloVe. O GloVe foi treinado em vários corpora (principalmente na Wikipédia). Testamos o treinamento dos nossos modelos sequenciais usando uma versão dos embeddings do GloVe e observamos que, se congelamos os pesos dos embeddings pré-treinados e treinamos apenas o restante da rede, os modelos não teriam um bom desempenho. Isso pode ter ocorrido porque o contexto em que a camada de embedding foi treinada pode ser diferente do contexto em que ela estava sendo usada.

Os embeddings do GloVe treinados com dados da Wikipédia podem não estar alinhados com os padrões de linguagem do conjunto de dados IMDb. As relações inferidas podem precisar de alguma atualização, ou seja, os pesos de embedding podem precisar de ajuste contextual. Fazemos isso em duas etapas:

  1. Na primeira execução, com os pesos da camada de embedding congelados, permitimos que o restante da rede aprenda. No final da execução, os pesos do modelo alcançam um estado muito melhor do que os valores não inicializados. Na segunda execução, permitimos que a camada de embedding também aprenda, fazendo ajustes em todos os pesos da rede. Chamamos esse processo de uso de uma incorporação ajustada.

  2. Os embeddings ajustados produzem mais precisão. No entanto, isso resulta no aumento da capacidade de computação necessária para treinar a rede. Considerando um número suficiente de amostras, também poderíamos aprender um embedding do zero. Observamos que, para S/W > 15K, começar do zero produz efetivamente a mesma precisão que usar incorporação ajustada.

Comparamos diferentes modelos de sequência, como CNN, sepCNN, RNN (LSTM e GRU), CNN-RNN e RNN empilhada, e variarmos as arquiteturas do modelo. Descobrimos que o sepCNNs, uma variante de rede convolucional que costuma ser mais eficiente em termos de dados e computação, tem um desempenho melhor do que os outros modelos.

O código a seguir constrói um modelo sepCNN de quatro camadas:

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

Treine seu modelo

Agora que construímos a arquitetura do modelo, precisamos treiná-lo. O treinamento envolve fazer uma previsão com base no estado atual do modelo, calcular o nível de incorreção da previsão e atualizar os pesos ou parâmetros da rede para minimizar esse erro e melhorar a previsão do modelo. Repetimos esse processo até o modelo convergir e não conseguir mais aprender. Há três parâmetros principais que podem ser escolhidos para esse processo (consulte a Tabela 2).

  • Métrica: como medir o desempenho do nosso modelo usando uma métrica. Usamos acurácia como métrica nos nossos experimentos.
  • Função de perda: usada para calcular um valor de perda que o processo de treinamento tenta minimizar ajustando os pesos da rede. Para problemas de classificação, a perda de entropia cruzada funciona bem.
  • Optimizer: uma função que decide como os pesos de rede serão atualizados com base na saída da função de perda. Usamos o famoso otimizador Adam nos nossos experimentos.

No Keras, é possível transmitir esses parâmetros de aprendizado para um modelo usando o método compile.

Tabela 2: parâmetros de aprendizado

Parâmetro de aprendizado Valor
Métrica accuracy
Função de perda - classificação binária binary_crossentropy
Função de perda - classificação multiclasse sparse_categorical_crossentropy
Otimizador adam

O treinamento real acontece com o método fit. Dependendo do tamanho do conjunto de dados, esse é o método em que a maioria dos ciclos de computação será gasto. Em cada iteração de treinamento, o número batch_size de amostras dos dados de treinamento é usado para calcular a perda, e os pesos são atualizados uma vez, com base nesse valor. O processo de treinamento conclui uma epoch quando o modelo vê todo o conjunto de dados de treinamento. No final de cada período, usamos o conjunto de dados de validação para avaliar o desempenho do modelo de aprendizado. Repetimos o treinamento com o conjunto de dados por um número predeterminado de períodos. É possível otimizar isso interrompendo o treinamento antecipadamente, quando a acurácia da validação se estabiliza entre períodos consecutivos, mostrando que o modelo não está mais em treinamento.

Treinamento de hiperparâmetros Valor
Taxa de aprendizado 1e a 3
Períodos 1000
Tamanho do lote 512
Parada antecipada parâmetro: val_loss, paciência: 1

Tabela 3: treinar hiperparâmetros

O código Keras a seguir implementa o processo de treinamento usando os parâmetros escolhidos nas tabelas 2 e 3 acima:

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]

Veja exemplos de código para treinar o modelo sequencial aqui.