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.
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.
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:
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.
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.