Esta página foi traduzida pela API Cloud Translation.
Switch to English

Regularização de gráfico para classificação de sentimento usando gráficos sintetizados

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub

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:

  1. Crie embeddings para cada amostra de texto na entrada. Isso pode ser feito usando modelos pré-treinados como word2vec , Swivel , BERT etc.
  2. 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.
  3. 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.
  4. Crie uma rede neural como modelo básico usando a API sequencial, funcional ou de subclasse de Keras.
  5. 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.
  6. Treine e avalie o modelo gráfico de Keras.

Requisitos

  1. Instale o pacote Neural Structured Learning.
  2. 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:

  1. id : o ID do nó da amostra.
  2. palavras : uma lista int64 contendo IDs de palavras.
  3. 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 de 10000 dimensões que são todos zeros, exceto para os índices 3 e 5 , que são uns. Em seguida, torne esta a primeira camada em nossa rede - uma camada Dense - que pode lidar com dados vetoriais de ponto flutuante. Essa abordagem num_words * num_reviews memória, porém, requer uma matriz de tamanho num_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:

  1. A primeira camada é uma camada de Input que pega o vocabulário codificado por inteiro.
  2. 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) .
  3. Em seguida, uma camada LSTM bidirecional retorna um vetor de saída de comprimento fixo para cada exemplo.
  4. Este vetor de saída de comprimento fixo é canalizado por uma camada totalmente conectada ( Dense ) com 64 unidades ocultas.
  5. 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()

png

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()

png

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()

png

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()

png

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>

png

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.