Etapa 4: criar, treinar e avaliar o modelo

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

A criação de modelos de machine learning com o Keras tem como foco a montagem de camadas em camadas, elementos fundamentais de processamento de dados, assim como a montagem dos blocos de Lego. Essas camadas permitem especificar a sequência de transformações que queremos realizar na entrada. À medida que nosso algoritmo de aprendizado recebe uma única entrada de texto e gera uma única classificação, é possível 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 forma diferente, dependendo se estamos criando um modelo n-grama ou sequencial. Mas, independentemente do tipo de modelo, a última camada será a mesma para um determinado problema.

Como criar a última camada

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

Quando há mais de duas classes (classificação multiclasse), nosso modelo precisa gerar uma pontuação de probabilidade por classe. A soma dessas pontuações precisa ser 1. Por exemplo, a saída de {0: 0.2, 1: 0.7, 2: 0.1} significa “20% de confiança de que essa amostra está na classe 0, 70% de que está na classe 1 e 10% de 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 deve ser entropia cruzada categórica. Consulte a Figura 10 para a direita.

Última camada

Figura 10: última camada

O código abaixo define uma função que usa o número de classes como entrada e gera o número apropriado de unidades de camada, como uma unidade para classificação binária e uma unidade para cada classe, além da 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-grama e sequenciais.

Quando a proporção de S/W é pequena, descobrimos que os modelos de n-grama 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 porque as relações de embedding são aprendidas em espaçosos densos, e isso acontece melhor em muitas amostras.

Criar modelo n-grama [Opção A]

Os modelos que processam os tokens de forma independente (não considerando a ordem das palavras) são modelos n-grama. Perceptrons simples de várias camadas (incluindo regressão logística), máquinas de otimização de gradiente e máquinas de vetor de suporte estão nessa categoria. Eles não podem usar informações sobre ordenação de texto.

Comparamos o desempenho de alguns dos modelos de n-grama mencionados acima e observamos que os perceptrons de várias camadas (MLPs) normalmente têm um desempenho melhor do que outras opções. Os MLPs são simples de definir e entender, fornecem boa precisão e exigem computação relativamente baixa.

O código a seguir define um modelo de MLP de duas camadas em tf.keras, adicionando algumas camadas de dropout para regularização (para evitar overfitting 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

Modelo de sequência de criação [Opção B]

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

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

As palavras em um determinado conjunto de dados provavelmente não são exclusivas a esse conjunto de dados. Portanto, é possível aprender a relação entre as palavras no nosso conjunto de dados usando outros conjuntos. 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 inicial no processo de aprendizado.

Há embeddings pré-treinados disponíveis que foram treinados com grandes corpora, como o GloVe. O GloVe foi treinado em vários corpora (principalmente na Wikipédia). Testamos 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 tiveram um bom desempenho. Isso pode acontecer porque o contexto em que a camada de embedding foi treinada pode ser diferente do contexto em que estávamos usando a camada.

Os embeddings do GloVe treinados em dados da Wikipédia podem não se alinhar aos padrões de linguagem em nosso conjunto de dados IMDb. As relações inferidas podem precisar de algumas atualizações, ou seja, os pesos de embedding podem precisar de ajuste contextual. Isso é feito 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 desta 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 precisos em todos os pesos na rede. Esse processo é chamado de embedding ajustado.

  2. Embeddings ajustadas produzem melhor precisão. No entanto, isso acontece devido ao aumento da capacidade de computação necessária para treinar a rede. Com um número suficiente de amostras, poderíamos muito bem aprender uma incorporação do zero. Observamos que, para S/W > 15K, começar do zero de maneira eficaz gera aproximadamente a mesma precisão que o uso de incorporação refinada.

Comparamos modelos de sequência diferentes, como CNN, sepCNN, RNN (LSTM & GRU), CNN-RNN e RNN empilhada, para variações das arquiteturas do modelo. Descobrimos que as sepCNNs, uma variante de rede convolucional que geralmente são mais eficientes em termos de dados e computação, têm um desempenho melhor do que os outros modelos.

O código abaixo cria 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 treinar o modelo. O treinamento envolve fazer uma previsão com base no estado atual do modelo, calcular a previsão incorreta e atualizar os pesos ou parâmetros da rede para minimizar esse erro e melhorar a previsão do modelo. Repetimos esse processo até que nosso modelo tenha convergido e não possa mais aprender. Existem três parâmetros principais a serem escolhidos para esse processo (consulte a Tabela 2).

  • Métrica: como medir o desempenho do nosso modelo usando uma métrica. Usamos precisão como a métrica nos nossos experimentos.
  • Função de perda: uma função usada para calcular um valor de perda que o processo de treinamento tenta minimizar ao ajustar 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 da rede serão atualizados com base na saída da função de perda. Usamos o conhecido otimizador Adam nos nossos experimentos.

No Keras, podemos transmitir esses parâmetros de aprendizado para um modelo usando o método de compilação.

Parâmetro de aprendizado Valor
Métrica accuracy
Função de perda: classificação binária binário_entropia
Função de perda: classificação multiclasse Sparse_categorical_crossentropy
Otimizador Adam

Tabela 2: parâmetros de aprendizado

O treinamento real acontece usando o método fit. Dependendo do tamanho do conjunto de dados, este é o método usado na maioria dos ciclos de computação. Em cada iteração de treinamento, o número de batch_size amostras dos dados de treinamento são usados para calcular a perda, e os pesos são atualizados uma vez, de acordo com esse valor. O processo de treinamento completa um epoch assim que o modelo vê todo o conjunto de dados de treinamento. Ao final de cada época, usamos o conjunto de dados de validação para avaliar o desempenho do modelo. Repetimos o treinamento usando o conjunto de dados para um número predeterminado de períodos. É possível otimizar isso interrompendo antecipadamente, quando a acurácia da validação se estabiliza entre épocas consecutivas, mostrando que o modelo não está mais treinando.

Codelab de treinamento Valor
Taxa de aprendizado De 1 a 3
Períodos 1000
Tamanho do lote 512
Parada antecipada parâmetro: val_loss, paciência: 1

Tabela 3: Como treinar hiperparâmetros

O código da 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 aqui exemplos de código para treinar o modelo de sequência.