![]() | ![]() | ![]() |
Visão geral
Este caderno classifica resenhas de filmes como positivas ou negativas usando o texto da resenha. Este é um exemplo de classificação binária , um tipo importante e amplamente aplicável de problema de aprendizado de máquina.
Demonstraremos o uso de regularização de gráfico neste notebook construindo um gráfico a partir da entrada fornecida. A receita geral para construir um modelo regularizado por gráfico usando o framework Neural Structured Learning (NSL) quando a entrada não contém um gráfico explícito é a seguinte:
- Crie embeddings para cada amostra de texto na entrada. Isso pode ser feito usando modelos pré-treinados como word2vec , Swivel , BERT etc.
- Construa um gráfico com base nesses embeddings usando uma métrica de similaridade, como a distância 'L2', distância 'cosseno' etc. Os nós no gráfico correspondem às amostras e as bordas no gráfico correspondem à similaridade entre pares de amostras.
- Gere dados de treinamento a partir do gráfico sintetizado acima e dos recursos de amostra. Os dados de treinamento resultantes conterão recursos vizinhos, além dos recursos do nó original.
- Crie uma rede neural como modelo básico usando a API sequencial, funcional ou de subclasse de Keras.
- Envolva o modelo básico com a classe wrapper GraphRegularization, que é fornecida pela estrutura NSL, para criar um novo modelo Keras de gráfico. Este novo modelo incluirá uma perda de regularização de gráfico como termo de regularização em seu objetivo de treinamento.
- Treine e avalie o modelo gráfico de Keras.
Requisitos
- Instale o pacote Neural Structured Learning.
- Instale o tensorflow-hub.
pip install --quiet neural-structured-learning
pip install --quiet tensorflow-hub
Dependências e importações
import matplotlib.pyplot as plt
import numpy as np
import neural_structured_learning as nsl
import tensorflow as tf
import tensorflow_hub as hub
# Resets notebook state
tf.keras.backend.clear_session()
print("Version: ", tf.__version__)
print("Eager mode: ", tf.executing_eagerly())
print("Hub version: ", hub.__version__)
print(
"GPU is",
"available" if tf.config.list_physical_devices("GPU") else "NOT AVAILABLE")
Version: 2.3.0 Eager mode: True Hub version: 0.8.0 GPU is NOT AVAILABLE
Conjunto de dados IMDB
O conjunto de dados IMDB contém o texto de 50.000 resenhas de filmes do Internet Movie Database . Elas são divididas em 25.000 análises para treinamento e 25.000 análises para teste. Os conjuntos de treinamento e teste são equilibrados , o que significa que contêm um número igual de avaliações positivas e negativas.
Neste tutorial, usaremos uma versão pré-processada do conjunto de dados IMDB.
Baixe o conjunto de dados IMDB pré-processado
O conjunto de dados IMDB vem empacotado com TensorFlow. Já foi pré-processado de forma que as resenhas (sequências de palavras) foram convertidas em sequências de inteiros, onde cada inteiro representa uma palavra específica em um dicionário.
O código a seguir baixa o conjunto de dados IMDB (ou usa uma cópia em cache se já tiver sido baixado):
imdb = tf.keras.datasets.imdb
(pp_train_data, pp_train_labels), (pp_test_data, pp_test_labels) = (
imdb.load_data(num_words=10000))
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb.npz 17465344/17464789 [==============================] - 0s 0us/step
O argumento num_words=10000
mantém as 10.000 palavras mais frequentes nos dados de treinamento. As palavras raras são descartadas para manter o tamanho do vocabulário gerenciável.
Explore os dados
Vamos dedicar um momento para entender o formato dos dados. O conjunto de dados vem pré-processado: cada exemplo é um array de inteiros que representam as palavras da crítica do filme. Cada rótulo é um valor inteiro de 0 ou 1, onde 0 é uma revisão negativa e 1 é uma revisão positiva.
print('Training entries: {}, labels: {}'.format(
len(pp_train_data), len(pp_train_labels)))
training_samples_count = len(pp_train_data)
Training entries: 25000, labels: 25000
O texto das resenhas foi convertido em inteiros, onde cada inteiro representa uma palavra específica em um dicionário. Esta é a aparência da primeira revisão:
print(pp_train_data[0])
[1, 14, 22, 16, 43, 530, 973, 1622, 1385, 65, 458, 4468, 66, 3941, 4, 173, 36, 256, 5, 25, 100, 43, 838, 112, 50, 670, 2, 9, 35, 480, 284, 5, 150, 4, 172, 112, 167, 2, 336, 385, 39, 4, 172, 4536, 1111, 17, 546, 38, 13, 447, 4, 192, 50, 16, 6, 147, 2025, 19, 14, 22, 4, 1920, 4613, 469, 4, 22, 71, 87, 12, 16, 43, 530, 38, 76, 15, 13, 1247, 4, 22, 17, 515, 17, 12, 16, 626, 18, 2, 5, 62, 386, 12, 8, 316, 8, 106, 5, 4, 2223, 5244, 16, 480, 66, 3785, 33, 4, 130, 12, 16, 38, 619, 5, 25, 124, 51, 36, 135, 48, 25, 1415, 33, 6, 22, 12, 215, 28, 77, 52, 5, 14, 407, 16, 82, 2, 8, 4, 107, 117, 5952, 15, 256, 4, 2, 7, 3766, 5, 723, 36, 71, 43, 530, 476, 26, 400, 317, 46, 7, 4, 2, 1029, 13, 104, 88, 4, 381, 15, 297, 98, 32, 2071, 56, 26, 141, 6, 194, 7486, 18, 4, 226, 22, 21, 134, 476, 26, 480, 5, 144, 30, 5535, 18, 51, 36, 28, 224, 92, 25, 104, 4, 226, 65, 16, 38, 1334, 88, 12, 16, 283, 5, 16, 4472, 113, 103, 32, 15, 16, 5345, 19, 178, 32]
As resenhas de filmes podem ter durações diferentes. O código a seguir mostra o número de palavras na primeira e na segunda revisões. Visto que as entradas para uma rede neural devem ter o mesmo comprimento, precisaremos resolver isso mais tarde.
len(pp_train_data[0]), len(pp_train_data[1])
(218, 189)
Converta os inteiros de volta em palavras
Pode ser útil saber como converter números inteiros de volta ao texto correspondente. Aqui, criaremos uma função auxiliar para consultar um objeto de dicionário que contém o inteiro para mapeamento de string:
def build_reverse_word_index():
# A dictionary mapping words to an integer index
word_index = imdb.get_word_index()
# The first indices are reserved
word_index = {k: (v + 3) for k, v in word_index.items()}
word_index['<PAD>'] = 0
word_index['<START>'] = 1
word_index['<UNK>'] = 2 # unknown
word_index['<UNUSED>'] = 3
return dict((value, key) for (key, value) in word_index.items())
reverse_word_index = build_reverse_word_index()
def decode_review(text):
return ' '.join([reverse_word_index.get(i, '?') for i in text])
Downloading data from https://storage.googleapis.com/tensorflow/tf-keras-datasets/imdb_word_index.json 1646592/1641221 [==============================] - 0s 0us/step
Agora podemos usar a função decode_review
para exibir o texto da primeira revisão:
decode_review(pp_train_data[0])
"<START> this film was just brilliant casting location scenery story direction everyone's really suited the part they played and you could just imagine being there robert <UNK> is an amazing actor and now the same being director <UNK> father came from the same scottish island as myself so i loved the fact there was a real connection with this film the witty remarks throughout the film were great it was just brilliant so much that i bought the film as soon as it was released for <UNK> and would recommend it to everyone to watch and the fly fishing was amazing really cried at the end it was so sad and you know what they say if you cry at a film it must have been good and this definitely was also <UNK> to the two little boy's that played the <UNK> of norman and paul they were just brilliant children are often left out of the <UNK> list i think because the stars that play them all grown up are such a big profile for the whole film but these children are amazing and should be praised for what they have done don't you think the whole story was so lovely because it was true and was someone's life after all that was shared with us all"
Construção de gráfico
A construção do gráfico envolve a criação de embeddings para amostras de texto e, em seguida, o uso de uma função de similaridade para comparar os embeddings.
Antes de prosseguir, primeiro criamos um diretório para armazenar artefatos criados por este tutorial.
mkdir -p /tmp/imdb
Crie exemplos de embeddings
Usaremos embeddings Swivel pré-treinados para criar embeddings no formato tf.train.Example
para cada amostra na entrada. Armazenaremos os embeddings resultantes no formato TFRecord
junto com um recurso adicional que representa o ID de cada amostra. Isso é importante e nos permitirá combinar os embeddings de amostra com os nós correspondentes no gráfico posteriormente.
pretrained_embedding = 'https://tfhub.dev/google/tf2-preview/gnews-swivel-20dim/1'
hub_layer = hub.KerasLayer(
pretrained_embedding, input_shape=[], dtype=tf.string, trainable=True)
def _int64_feature(value):
"""Returns int64 tf.train.Feature."""
return tf.train.Feature(int64_list=tf.train.Int64List(value=value.tolist()))
def _bytes_feature(value):
"""Returns bytes tf.train.Feature."""
return tf.train.Feature(
bytes_list=tf.train.BytesList(value=[value.encode('utf-8')]))
def _float_feature(value):
"""Returns float tf.train.Feature."""
return tf.train.Feature(float_list=tf.train.FloatList(value=value.tolist()))
def create_embedding_example(word_vector, record_id):
"""Create tf.Example containing the sample's embedding and its ID."""
text = decode_review(word_vector)
# Shape = [batch_size,].
sentence_embedding = hub_layer(tf.reshape(text, shape=[-1,]))
# Flatten the sentence embedding back to 1-D.
sentence_embedding = tf.reshape(sentence_embedding, shape=[-1])
features = {
'id': _bytes_feature(str(record_id)),
'embedding': _float_feature(sentence_embedding.numpy())
}
return tf.train.Example(features=tf.train.Features(feature=features))
def create_embeddings(word_vectors, output_path, starting_record_id):
record_id = int(starting_record_id)
with tf.io.TFRecordWriter(output_path) as writer:
for word_vector in word_vectors:
example = create_embedding_example(word_vector, record_id)
record_id = record_id + 1
writer.write(example.SerializeToString())
return record_id
# Persist TF.Example features containing embeddings for training data in
# TFRecord format.
create_embeddings(pp_train_data, '/tmp/imdb/embeddings.tfr', 0)
25000
Construir um gráfico
Agora que temos os embeddings de amostra, vamos usá-los para construir um grafo de similaridade, ou seja, os nós neste gráfico corresponderão às amostras e as arestas neste gráfico corresponderão à similaridade entre pares de nós.
O Neural Structured Learning fornece uma biblioteca de construção de gráfico para construir um gráfico com base em exemplos de embeddings. Ele usa similaridade de cosseno como medida de similaridade para comparar embeddings e construir arestas entre eles. Também nos permite especificar um limite de similaridade, que pode ser usado para descartar arestas diferentes do gráfico final. Neste exemplo, usando 0,99 como o limite de similaridade e 12345 como a semente aleatória, terminamos com um gráfico que tem 429.415 arestas bidirecionais. Aqui, estamos usando o suporte do construtor de gráfico para hashing sensível à localidade (LSH) para acelerar a construção de gráfico. Para obter detalhes sobre como usar o suporte LSH do construtor gráfico, consulte a documentação da API build_graph_from_config
.
graph_builder_config = nsl.configs.GraphBuilderConfig(
similarity_threshold=0.99, lsh_splits=32, lsh_rounds=15, random_seed=12345)
nsl.tools.build_graph_from_config(['/tmp/imdb/embeddings.tfr'],
'/tmp/imdb/graph_99.tsv',
graph_builder_config)
Cada borda bidirecional é representada por duas bordas direcionadas no arquivo TSV de saída, de modo que o arquivo contenha 429.415 * 2 = 858.830 linhas totais:
wc -l /tmp/imdb/graph_99.tsv
858830 /tmp/imdb/graph_99.tsv
Recursos de amostra
Criamos recursos de amostra para nosso problema usando o formato tf.train.Example
e os TFRecord
formato TFRecord
. Cada amostra incluirá os três recursos a seguir:
- id : o ID do nó da amostra.
- palavras : uma lista int64 contendo IDs de palavras.
- rótulo : Um singleton int64 identificando a classe alvo da revisão.
def create_example(word_vector, label, record_id):
"""Create tf.Example containing the sample's word vector, label, and ID."""
features = {
'id': _bytes_feature(str(record_id)),
'words': _int64_feature(np.asarray(word_vector)),
'label': _int64_feature(np.asarray([label])),
}
return tf.train.Example(features=tf.train.Features(feature=features))
def create_records(word_vectors, labels, record_path, starting_record_id):
record_id = int(starting_record_id)
with tf.io.TFRecordWriter(record_path) as writer:
for word_vector, label in zip(word_vectors, labels):
example = create_example(word_vector, label, record_id)
record_id = record_id + 1
writer.write(example.SerializeToString())
return record_id
# Persist TF.Example features (word vectors and labels) for training and test
# data in TFRecord format.
next_record_id = create_records(pp_train_data, pp_train_labels,
'/tmp/imdb/train_data.tfr', 0)
create_records(pp_test_data, pp_test_labels, '/tmp/imdb/test_data.tfr',
next_record_id)
50000
Aumente os dados de treinamento com vizinhos do gráfico
Como temos os recursos de amostra e o gráfico sintetizado, podemos gerar os dados de treinamento aumentados para o aprendizado estruturado neural. A estrutura NSL fornece uma biblioteca para combinar o gráfico e os recursos de amostra para produzir os dados de treinamento finais para a regularização do gráfico. Os dados de treinamento resultantes incluirão recursos de amostra originais, bem como recursos de seus vizinhos correspondentes.
Neste tutorial, consideramos bordas não direcionadas e usamos no máximo 3 vizinhos por amostra para aumentar os dados de treinamento com os vizinhos do gráfico.
nsl.tools.pack_nbrs(
'/tmp/imdb/train_data.tfr',
'',
'/tmp/imdb/graph_99.tsv',
'/tmp/imdb/nsl_train_data.tfr',
add_undirected_edges=True,
max_nbrs=3)
Modelo básico
Agora estamos prontos para construir um modelo básico sem regularização de gráfico. Para construir este modelo, podemos usar embeddings que foram usados na construção do gráfico ou podemos aprender novos embeddings juntamente com a tarefa de classificação. Para o propósito deste bloco de notas, faremos o último.
Variáveis globais
NBR_FEATURE_PREFIX = 'NL_nbr_'
NBR_WEIGHT_SUFFIX = '_weight'
Hiperparâmetros
Usaremos uma instância de HParams
para incluir vários hiperparâmetros e constantes usados para treinamento e avaliação. Descrevemos resumidamente cada um deles abaixo:
num_classes : Existem 2 classes - positiva e negativa .
max_seq_length : Este é o número máximo de palavras consideradas para cada crítica de filme neste exemplo.
vocab_size : este é o tamanho do vocabulário considerado para este exemplo.
distance_type : Esta é a métrica de distância usada para regularizar a amostra com seus vizinhos.
graph_regularization_multiplier : controla o peso relativo do termo de regularização do gráfico na função de perda geral.
num_neighs : o número de vizinhos usados para regularização do gráfico. Este valor deve ser menor ou igual ao argumento
max_nbrs
usado acima ao invocarnsl.tools.pack_nbrs
.num_fc_units : O número de unidades na camada totalmente conectada da rede neural.
train_epochs : o número de épocas de treinamento.
batch_size : tamanho do lote usado para treinamento e avaliação.
eval_steps : O número de lotes a serem processados antes que a avaliação considerada seja concluída. Se definido como
None
, todas as instâncias no conjunto de teste são avaliadas.
class HParams(object):
"""Hyperparameters used for training."""
def __init__(self):
### dataset parameters
self.num_classes = 2
self.max_seq_length = 256
self.vocab_size = 10000
### neural graph learning parameters
self.distance_type = nsl.configs.DistanceType.L2
self.graph_regularization_multiplier = 0.1
self.num_neighbors = 2
### model architecture
self.num_embedding_dims = 16
self.num_lstm_dims = 64
self.num_fc_units = 64
### training parameters
self.train_epochs = 10
self.batch_size = 128
### eval parameters
self.eval_steps = None # All instances in the test set are evaluated.
HPARAMS = HParams()
Prepare os dados
As revisões - as matrizes de inteiros - devem ser convertidas em tensores antes de serem alimentadas na rede neural. Essa conversão pode ser feita de duas maneiras:
Converta as matrizes em vetores de
0
1
s, indicando a ocorrência da palavra, semelhante a uma codificação one-hot. Por exemplo, a sequência[3, 5]
se tornaria um vetor de10000
dimensões que são todos zeros, exceto para os índices3
e5
, que são uns. Em seguida, torne esta a primeira camada em nossa rede - uma camadaDense
- que pode lidar com dados vetoriais de ponto flutuante. Essa abordagemnum_words * num_reviews
memória, porém, requer uma matriz de tamanhonum_words * num_reviews
.Como alternativa, podemos preencher os arrays para que todos tenham o mesmo comprimento e, em seguida, criar um tensor inteiro de formato
max_length * num_reviews
. Podemos usar uma camada de incorporação capaz de lidar com essa forma como a primeira camada em nossa rede.
Neste tutorial, usaremos a segunda abordagem.
Como as resenhas do filme devem ter a mesma duração, usaremos a função pad_sequence
definida abaixo para padronizar as durações.
def make_dataset(file_path, training=False):
"""Creates a `tf.data.TFRecordDataset`.
Args:
file_path: Name of the file in the `.tfrecord` format containing
`tf.train.Example` objects.
training: Boolean indicating if we are in training mode.
Returns:
An instance of `tf.data.TFRecordDataset` containing the `tf.train.Example`
objects.
"""
def pad_sequence(sequence, max_seq_length):
"""Pads the input sequence (a `tf.SparseTensor`) to `max_seq_length`."""
pad_size = tf.maximum([0], max_seq_length - tf.shape(sequence)[0])
padded = tf.concat(
[sequence.values,
tf.fill((pad_size), tf.cast(0, sequence.dtype))],
axis=0)
# The input sequence may be larger than max_seq_length. Truncate down if
# necessary.
return tf.slice(padded, [0], [max_seq_length])
def parse_example(example_proto):
"""Extracts relevant fields from the `example_proto`.
Args:
example_proto: An instance of `tf.train.Example`.
Returns:
A pair whose first value is a dictionary containing relevant features
and whose second value contains the ground truth labels.
"""
# The 'words' feature is a variable length word ID vector.
feature_spec = {
'words': tf.io.VarLenFeature(tf.int64),
'label': tf.io.FixedLenFeature((), tf.int64, default_value=-1),
}
# We also extract corresponding neighbor features in a similar manner to
# the features above during training.
if training:
for i in range(HPARAMS.num_neighbors):
nbr_feature_key = '{}{}_{}'.format(NBR_FEATURE_PREFIX, i, 'words')
nbr_weight_key = '{}{}{}'.format(NBR_FEATURE_PREFIX, i,
NBR_WEIGHT_SUFFIX)
feature_spec[nbr_feature_key] = tf.io.VarLenFeature(tf.int64)
# We assign a default value of 0.0 for the neighbor weight so that
# graph regularization is done on samples based on their exact number
# of neighbors. In other words, non-existent neighbors are discounted.
feature_spec[nbr_weight_key] = tf.io.FixedLenFeature(
[1], tf.float32, default_value=tf.constant([0.0]))
features = tf.io.parse_single_example(example_proto, feature_spec)
# Since the 'words' feature is a variable length word vector, we pad it to a
# constant maximum length based on HPARAMS.max_seq_length
features['words'] = pad_sequence(features['words'], HPARAMS.max_seq_length)
if training:
for i in range(HPARAMS.num_neighbors):
nbr_feature_key = '{}{}_{}'.format(NBR_FEATURE_PREFIX, i, 'words')
features[nbr_feature_key] = pad_sequence(features[nbr_feature_key],
HPARAMS.max_seq_length)
labels = features.pop('label')
return features, labels
dataset = tf.data.TFRecordDataset([file_path])
if training:
dataset = dataset.shuffle(10000)
dataset = dataset.map(parse_example)
dataset = dataset.batch(HPARAMS.batch_size)
return dataset
train_dataset = make_dataset('/tmp/imdb/nsl_train_data.tfr', True)
test_dataset = make_dataset('/tmp/imdb/test_data.tfr')
Construir o modelo
Uma rede neural é criada pelo empilhamento de camadas - isso requer duas decisões arquitetônicas principais:
- Quantas camadas usar no modelo?
- Quantas unidades ocultas usar para cada camada?
Neste exemplo, os dados de entrada consistem em uma matriz de índices de palavras. Os rótulos para prever são 0 ou 1.
Usaremos um LSTM bidirecional como nosso modelo base neste tutorial.
# This function exists as an alternative to the bi-LSTM model used in this
# notebook.
def make_feed_forward_model():
"""Builds a simple 2 layer feed forward neural network."""
inputs = tf.keras.Input(
shape=(HPARAMS.max_seq_length,), dtype='int64', name='words')
embedding_layer = tf.keras.layers.Embedding(HPARAMS.vocab_size, 16)(inputs)
pooling_layer = tf.keras.layers.GlobalAveragePooling1D()(embedding_layer)
dense_layer = tf.keras.layers.Dense(16, activation='relu')(pooling_layer)
outputs = tf.keras.layers.Dense(1, activation='sigmoid')(dense_layer)
return tf.keras.Model(inputs=inputs, outputs=outputs)
def make_bilstm_model():
"""Builds a bi-directional LSTM model."""
inputs = tf.keras.Input(
shape=(HPARAMS.max_seq_length,), dtype='int64', name='words')
embedding_layer = tf.keras.layers.Embedding(HPARAMS.vocab_size,
HPARAMS.num_embedding_dims)(
inputs)
lstm_layer = tf.keras.layers.Bidirectional(
tf.keras.layers.LSTM(HPARAMS.num_lstm_dims))(
embedding_layer)
dense_layer = tf.keras.layers.Dense(
HPARAMS.num_fc_units, activation='relu')(
lstm_layer)
outputs = tf.keras.layers.Dense(1, activation='sigmoid')(dense_layer)
return tf.keras.Model(inputs=inputs, outputs=outputs)
# Feel free to use an architecture of your choice.
model = make_bilstm_model()
model.summary()
Model: "functional_1" _________________________________________________________________ Layer (type) Output Shape Param # ================================================================= words (InputLayer) [(None, 256)] 0 _________________________________________________________________ embedding (Embedding) (None, 256, 16) 160000 _________________________________________________________________ bidirectional (Bidirectional (None, 128) 41472 _________________________________________________________________ dense (Dense) (None, 64) 8256 _________________________________________________________________ dense_1 (Dense) (None, 1) 65 ================================================================= Total params: 209,793 Trainable params: 209,793 Non-trainable params: 0 _________________________________________________________________
As camadas são efetivamente empilhadas sequencialmente para construir o classificador:
- A primeira camada é uma camada de
Input
que pega o vocabulário codificado por inteiro. - A próxima camada é uma camada de
Embedding
, que pega o vocabulário codificado por inteiro e procura o vetor de incorporação para cada índice de palavras. Esses vetores são aprendidos à medida que o modelo treina. Os vetores adicionam uma dimensão ao array de saída. As dimensões resultantes são:(batch, sequence, embedding)
. - Em seguida, uma camada LSTM bidirecional retorna um vetor de saída de comprimento fixo para cada exemplo.
- Este vetor de saída de comprimento fixo é canalizado por uma camada totalmente conectada (
Dense
) com 64 unidades ocultas. - A última camada está densamente conectada a um único nó de saída. Usando a função de ativação
sigmoid
, este valor é uma flutuação entre 0 e 1, representando uma probabilidade ou nível de confiança.
Unidades ocultas
O modelo acima possui duas camadas intermediárias ou "ocultas", entre a entrada e a saída, e excluindo a camada de Embedding
. O número de saídas (unidades, nós ou neurônios) é a dimensão do espaço representacional para a camada. Em outras palavras, a quantidade de liberdade que a rede tem ao aprender uma representação interna.
Se um modelo tiver mais unidades ocultas (um espaço de representação de dimensão superior) e / ou mais camadas, a rede pode aprender representações mais complexas. No entanto, torna a rede mais cara computacionalmente e pode levar ao aprendizado de padrões indesejados - padrões que melhoram o desempenho nos dados de treinamento, mas não nos dados de teste. Isso é chamado de overfitting .
Função de perda e otimizador
Um modelo precisa de uma função de perda e um otimizador para treinamento. Como este é um problema de classificação binária e o modelo gera uma probabilidade (uma camada de unidade única com ativação sigmóide), usaremos a função de perda binary_crossentropy
.
model.compile(
optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
Crie um conjunto de validação
Ao treinar, queremos verificar a precisão do modelo em dados que não vimos antes. Crie um conjunto de validação separando uma fração dos dados de treinamento originais. (Por que não usar o conjunto de teste agora? Nosso objetivo é desenvolver e ajustar nosso modelo usando apenas os dados de treinamento e, em seguida, usar os dados de teste apenas uma vez para avaliar nossa precisão).
Neste tutorial, pegamos cerca de 10% das amostras de treinamento inicial (10% de 25.000) como dados rotulados para treinamento e o restante como dados de validação. Como a divisão de treinamento / teste inicial foi de 50/50 (25.000 amostras cada), a divisão de treinamento / validação / teste efetiva que temos agora é 5/45/50.
Observe que 'train_dataset' já foi agrupado e embaralhado.
validation_fraction = 0.9
validation_size = int(validation_fraction *
int(training_samples_count / HPARAMS.batch_size))
print(validation_size)
validation_dataset = train_dataset.take(validation_size)
train_dataset = train_dataset.skip(validation_size)
175
Treine o modelo
Treine o modelo em minilotes. Durante o treinamento, monitore a perda e a precisão do modelo no conjunto de validação:
history = model.fit(
train_dataset,
validation_data=validation_dataset,
epochs=HPARAMS.train_epochs,
verbose=1)
Epoch 1/10 /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/keras/engine/functional.py:543: UserWarning: Input dict contained keys ['NL_nbr_0_words', 'NL_nbr_1_words', 'NL_nbr_0_weight', 'NL_nbr_1_weight'] which did not match any model input. They will be ignored by the model. [n for n in tensors.keys() if n not in ref_input_names]) 21/21 [==============================] - 19s 917ms/step - loss: 0.6930 - accuracy: 0.5081 - val_loss: 0.6924 - val_accuracy: 0.5518 Epoch 2/10 21/21 [==============================] - 18s 878ms/step - loss: 0.6902 - accuracy: 0.5319 - val_loss: 0.6587 - val_accuracy: 0.6465 Epoch 3/10 21/21 [==============================] - 18s 879ms/step - loss: 0.6338 - accuracy: 0.6731 - val_loss: 0.5882 - val_accuracy: 0.7310 Epoch 4/10 21/21 [==============================] - 18s 872ms/step - loss: 0.4889 - accuracy: 0.7854 - val_loss: 0.4445 - val_accuracy: 0.8047 Epoch 5/10 21/21 [==============================] - 18s 872ms/step - loss: 0.3911 - accuracy: 0.8369 - val_loss: 0.3870 - val_accuracy: 0.8352 Epoch 6/10 21/21 [==============================] - 18s 877ms/step - loss: 0.3544 - accuracy: 0.8542 - val_loss: 0.3420 - val_accuracy: 0.8571 Epoch 7/10 21/21 [==============================] - 19s 900ms/step - loss: 0.3262 - accuracy: 0.8700 - val_loss: 0.3135 - val_accuracy: 0.8762 Epoch 8/10 21/21 [==============================] - 18s 871ms/step - loss: 0.2770 - accuracy: 0.8977 - val_loss: 0.2739 - val_accuracy: 0.8923 Epoch 9/10 21/21 [==============================] - 18s 872ms/step - loss: 0.2863 - accuracy: 0.8958 - val_loss: 0.2703 - val_accuracy: 0.8942 Epoch 10/10 21/21 [==============================] - 18s 875ms/step - loss: 0.2232 - accuracy: 0.9150 - val_loss: 0.2543 - val_accuracy: 0.9037
Avalie o modelo
Agora, vamos ver como o modelo funciona. Dois valores serão retornados. Perda (um número que representa nosso erro, valores mais baixos são melhores) e precisão.
results = model.evaluate(test_dataset, steps=HPARAMS.eval_steps)
print(results)
196/196 [==============================] - 16s 82ms/step - loss: 0.3748 - accuracy: 0.8500 [0.37483155727386475, 0.8500000238418579]
Crie um gráfico de precisão / perda ao longo do tempo
model.fit()
retorna um objeto History
que contém um dicionário com tudo o que aconteceu durante o treinamento:
history_dict = history.history
history_dict.keys()
dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy'])
Existem quatro entradas: uma para cada métrica monitorada durante o treinamento e a validação. Podemos usá-los para traçar a perda de treinamento e validação para comparação, bem como a precisão de treinamento e validação:
acc = history_dict['accuracy']
val_acc = history_dict['val_accuracy']
loss = history_dict['loss']
val_loss = history_dict['val_loss']
epochs = range(1, len(acc) + 1)
# "-r^" is for solid red line with triangle markers.
plt.plot(epochs, loss, '-r^', label='Training loss')
# "-b0" is for solid blue line with circle markers.
plt.plot(epochs, val_loss, '-bo', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend(loc='best')
plt.show()
plt.clf() # clear figure
plt.plot(epochs, acc, '-r^', label='Training acc')
plt.plot(epochs, val_acc, '-bo', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend(loc='best')
plt.show()
Observe que a perda de treinamento diminui a cada época e a precisão do treinamento aumenta a cada época. Isso é esperado ao usar uma otimização de gradiente descendente - deve minimizar a quantidade desejada em cada iteração.
Regularização de grafos
Agora estamos prontos para tentar a regularização de gráfico usando o modelo básico que construímos acima. Usaremos a classe wrapper GraphRegularization
fornecida pelo framework Neural Structured Learning para envolver o modelo básico (bi-LSTM) para incluir a regularização de gráfico. O restante das etapas para treinar e avaliar o modelo regularizado por gráfico são semelhantes às do modelo básico.
Criar modelo regularizado por gráfico
Para avaliar o benefício incremental da regularização do gráfico, criaremos uma nova instância do modelo base. Isso ocorre porque o model
já foi treinado para algumas iterações e reutilizar esse modelo treinado para criar um modelo regularizado por gráfico não será uma comparação justa para o model
.
# Build a new base LSTM model.
base_reg_model = make_bilstm_model()
# Wrap the base model with graph regularization.
graph_reg_config = nsl.configs.make_graph_reg_config(
max_neighbors=HPARAMS.num_neighbors,
multiplier=HPARAMS.graph_regularization_multiplier,
distance_type=HPARAMS.distance_type,
sum_over_axis=-1)
graph_reg_model = nsl.keras.GraphRegularization(base_reg_model,
graph_reg_config)
graph_reg_model.compile(
optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
Treine o modelo
graph_reg_history = graph_reg_model.fit(
train_dataset,
validation_data=validation_dataset,
epochs=HPARAMS.train_epochs,
verbose=1)
Epoch 1/10 /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow/python/framework/indexed_slices.py:432: UserWarning: Converting sparse IndexedSlices to a dense Tensor of unknown shape. This may consume a large amount of memory. "Converting sparse IndexedSlices to a dense Tensor of unknown shape. " 21/21 [==============================] - 22s 1s/step - loss: 0.6925 - accuracy: 0.5135 - scaled_graph_loss: 7.8682e-06 - val_loss: 0.6925 - val_accuracy: 0.5207 Epoch 2/10 21/21 [==============================] - 22s 1s/step - loss: 0.6902 - accuracy: 0.5373 - scaled_graph_loss: 2.3502e-05 - val_loss: 0.6591 - val_accuracy: 0.6627 Epoch 3/10 21/21 [==============================] - 21s 981ms/step - loss: 0.6376 - accuracy: 0.6942 - scaled_graph_loss: 0.0028 - val_loss: 0.6867 - val_accuracy: 0.5343 Epoch 4/10 21/21 [==============================] - 20s 975ms/step - loss: 0.6240 - accuracy: 0.7031 - scaled_graph_loss: 9.6606e-04 - val_loss: 0.5891 - val_accuracy: 0.7572 Epoch 5/10 21/21 [==============================] - 20s 973ms/step - loss: 0.5111 - accuracy: 0.7896 - scaled_graph_loss: 0.0059 - val_loss: 0.4260 - val_accuracy: 0.8207 Epoch 6/10 21/21 [==============================] - 21s 981ms/step - loss: 0.3816 - accuracy: 0.8508 - scaled_graph_loss: 0.0157 - val_loss: 0.3182 - val_accuracy: 0.8682 Epoch 7/10 21/21 [==============================] - 20s 976ms/step - loss: 0.3488 - accuracy: 0.8704 - scaled_graph_loss: 0.0202 - val_loss: 0.3156 - val_accuracy: 0.8749 Epoch 8/10 21/21 [==============================] - 20s 973ms/step - loss: 0.3227 - accuracy: 0.8815 - scaled_graph_loss: 0.0198 - val_loss: 0.2746 - val_accuracy: 0.8932 Epoch 9/10 21/21 [==============================] - 21s 1s/step - loss: 0.3058 - accuracy: 0.8958 - scaled_graph_loss: 0.0220 - val_loss: 0.2938 - val_accuracy: 0.8833 Epoch 10/10 21/21 [==============================] - 21s 979ms/step - loss: 0.2789 - accuracy: 0.9008 - scaled_graph_loss: 0.0233 - val_loss: 0.2622 - val_accuracy: 0.8981
Avalie o modelo
graph_reg_results = graph_reg_model.evaluate(test_dataset, steps=HPARAMS.eval_steps)
print(graph_reg_results)
196/196 [==============================] - 16s 82ms/step - loss: 0.3543 - accuracy: 0.8508 [0.354336142539978, 0.8507599830627441]
Crie um gráfico de precisão / perda ao longo do tempo
graph_reg_history_dict = graph_reg_history.history
graph_reg_history_dict.keys()
dict_keys(['loss', 'accuracy', 'scaled_graph_loss', 'val_loss', 'val_accuracy'])
Há cinco entradas no total no dicionário: perda de treinamento, precisão de treinamento, perda de gráfico de treinamento, perda de validação e precisão de validação. Podemos representá-los todos juntos para comparação. Observe que a perda do gráfico só é calculada durante o treinamento.
acc = graph_reg_history_dict['accuracy']
val_acc = graph_reg_history_dict['val_accuracy']
loss = graph_reg_history_dict['loss']
graph_loss = graph_reg_history_dict['scaled_graph_loss']
val_loss = graph_reg_history_dict['val_loss']
epochs = range(1, len(acc) + 1)
plt.clf() # clear figure
# "-r^" is for solid red line with triangle markers.
plt.plot(epochs, loss, '-r^', label='Training loss')
# "-gD" is for solid green line with diamond markers.
plt.plot(epochs, graph_loss, '-gD', label='Training graph loss')
# "-b0" is for solid blue line with circle markers.
plt.plot(epochs, val_loss, '-bo', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend(loc='best')
plt.show()
plt.clf() # clear figure
plt.plot(epochs, acc, '-r^', label='Training acc')
plt.plot(epochs, val_acc, '-bo', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend(loc='best')
plt.show()
O poder da aprendizagem semi-supervisionada
O aprendizado semissupervisionado e, mais especificamente, a regularização de gráfico no contexto deste tutorial, pode ser muito poderoso quando a quantidade de dados de treinamento é pequena. A falta de dados de treinamento é compensada pelo aproveitamento da similaridade entre as amostras de treinamento, o que não é possível no aprendizado supervisionado tradicional.
Definimos a proporção de supervisão como a proporção de amostras de treinamento para o número total de amostras, que inclui treinamento, validação e amostras de teste. Neste notebook, usamos uma taxa de supervisão de 0,05 (ou seja, 5% dos dados rotulados) para treinar o modelo base e o modelo regularizado por gráfico. Ilustramos o impacto da taxa de supervisão na precisão do modelo na célula abaixo.
# Accuracy values for both the Bi-LSTM model and the feed forward NN model have
# been precomputed for the following supervision ratios.
supervision_ratios = [0.3, 0.15, 0.05, 0.03, 0.02, 0.01, 0.005]
model_tags = ['Bi-LSTM model', 'Feed Forward NN model']
base_model_accs = [[84, 84, 83, 80, 65, 52, 50], [87, 86, 76, 74, 67, 52, 51]]
graph_reg_model_accs = [[84, 84, 83, 83, 65, 63, 50],
[87, 86, 80, 75, 67, 52, 50]]
plt.clf() # clear figure
fig, axes = plt.subplots(1, 2)
fig.set_size_inches((12, 5))
for ax, model_tag, base_model_acc, graph_reg_model_acc in zip(
axes, model_tags, base_model_accs, graph_reg_model_accs):
# "-r^" is for solid red line with triangle markers.
ax.plot(base_model_acc, '-r^', label='Base model')
# "-gD" is for solid green line with diamond markers.
ax.plot(graph_reg_model_acc, '-gD', label='Graph-regularized model')
ax.set_title(model_tag)
ax.set_xlabel('Supervision ratio')
ax.set_ylabel('Accuracy(%)')
ax.set_ylim((25, 100))
ax.set_xticks(range(len(supervision_ratios)))
ax.set_xticklabels(supervision_ratios)
ax.legend(loc='best')
plt.show()
<Figure size 432x288 with 0 Axes>
Pode-se observar que conforme a razão de superivisão diminui, a precisão do modelo também diminui. Isso é verdadeiro para o modelo básico e para o modelo regularizado por gráfico, independentemente da arquitetura do modelo usada. No entanto, observe que o modelo regularizado por gráfico tem um desempenho melhor do que o modelo básico para ambas as arquiteturas. Em particular, para o modelo Bi-LSTM, quando a taxa de supervisão é 0,01, a precisão do modelo regularizado por gráfico é ~ 20% maior do que a do modelo básico. Isso se deve principalmente ao aprendizado semissupervisionado para o modelo regularizado por gráfico, em que a similaridade estrutural entre as amostras de treinamento é usada além das próprias amostras de treinamento.
Conclusão
Demonstramos o uso de regularização de gráfico usando o framework Neural Structured Learning (NSL), mesmo quando a entrada não contém um gráfico explícito. Consideramos a tarefa de classificação de sentimento das resenhas de filmes do IMDB para as quais sintetizamos um gráfico de similaridade com base em embeddings de resenhas. Incentivamos os usuários a experimentar mais, variando hiperparâmetros, a quantidade de supervisão e usando diferentes arquiteturas de modelo.