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

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

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

Visão geral

Este caderno classifica as 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.

Vamos demonstrar o uso da regularização de gráficos neste caderno, construindo um gráfico a partir da entrada fornecida. A receita geral para a construção de um modelo com gráfico regularizado usando a estrutura Neural Structured Learning (NSL) quando a entrada não contém um gráfico explícito é a seguinte:

  1. Crie incorporações para cada amostra de texto na entrada. Isso pode ser feito usando modelos pré-treinados, como word2vec , Swivel , BERT etc.
  2. Crie um gráfico com base nessas incorporações usando uma métrica de similaridade, como a distância 'L2', a distância 'cosseno' etc.
  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 originais do nó.
  4. Crie uma rede neural como modelo base usando a API sequencial, funcional ou subclasse Keras.
  5. Embrulhe o modelo base com a classe de wrapper GraphRegularization, fornecida pela estrutura NSL, para criar um novo modelo Keras de gráfico. Esse novo modelo incluirá uma perda de regularização gráfica como o termo de regularização em seu objetivo de treinamento.
  6. Treine e avalie o modelo Keras do gráfico.

Exigências

  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 do IMDB contém o texto de 50.000 críticas de filmes do Internet Movie Database . Eles são divididos em 25.000 análises para treinamento e 25.000 análises para testes. Os conjuntos de treinamento e teste são equilibrados , o que significa que eles contêm um número igual de análises positivas e negativas.

Neste tutorial, usaremos uma versão pré-processada do conjunto de dados IMDB.

Faça o download do conjunto de dados IMDB pré-processado

O conjunto de dados IMDB é fornecido com o TensorFlow. Já foi pré-processado, de modo que as revisões (sequências de palavras) foram convertidas em sequências de números inteiros, onde cada número 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 comuns 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 é pré-processado: cada exemplo é uma matriz de números inteiros que representam as palavras da resenha do filme. Cada rótulo é um valor inteiro de 0 ou 1, em que 0 é uma crítica negativa e 1 é uma crítica 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 revisões foi convertido em números inteiros, onde cada número inteiro representa uma palavra específica em um dicionário. Aqui está 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 críticas de filmes podem ter diferentes comprimentos. O código abaixo mostra o número de palavras na primeira e na segunda revisões. Como 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 números inteiros novamente em palavras

Pode ser útil saber como converter números inteiros novamente no texto correspondente. Aqui, criaremos uma função auxiliar para consultar um objeto de dicionário que contém o número inteiro para o 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 incorporações para amostras de texto e o uso de uma função de similaridade para comparar as incorporações.

Antes de prosseguir, primeiro criamos um diretório para armazenar artefatos criados por este tutorial.

mkdir -p /tmp/imdb

Crie exemplos de incorporação

Usaremos as combinações giratórias pré-treinadas para criar tf.train.Example formato tf.train.Example para cada amostra na entrada. Armazenaremos as incorporação resultantes no formato TFRecord , juntamente com um recurso adicional que representa o ID de cada amostra. Isso é importante e nos permitirá combinar as amostras de incorporação 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

Construa um gráfico

Agora que temos os exemplos de incorporação, vamos usá-los para criar um gráfico de similaridade, ou seja, os nós neste gráfico corresponderão a amostras e as arestas neste gráfico corresponderão à similaridade entre pares de nós.

O Neural Structured Learning fornece uma biblioteca de criação de gráficos para criar um gráfico com base em exemplos de incorporação. Ele usa a semelhança de cosseno como medida de similaridade para comparar os embeddings e criar 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, terminamos com um gráfico que possui 445.327 arestas bidirecionais.

 nsl.tools.build_graph(['/tmp/imdb/embeddings.tfr'],
                      '/tmp/imdb/graph_99.tsv',
                      similarity_threshold=0.99)
 

Recursos de exemplo

Criamos recursos de amostra para o 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. words : Uma lista int64 contendo IDs de palavras.
  3. label : Um int64 singleton que identifica a classe de destino 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

Aumentar os dados de treinamento com vizinhos gráficos

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 da NSL fornece uma biblioteca para combinar o gráfico e os recursos de amostra para produzir os dados finais de treinamento para regularização do gráfico. Os dados de treinamento resultantes incluirão recursos de amostra originais e recursos de seus vizinhos correspondentes.

Neste tutorial, consideramos arestas não direcionadas e usamos no máximo 3 vizinhos por amostra para aumentar os dados de treinamento com vizinhos gráficos.

 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 base

Agora estamos prontos para construir um modelo base sem regularização gráfica. Para construir esse modelo, podemos usar as combinações usadas na construção do gráfico, ou podemos aprender novas combinações em conjunto, juntamente com a tarefa de classificação. Para os propósitos deste caderno, faremos o último.

Variáveis ​​globais

 NBR_FEATURE_PREFIX = 'NL_nbr_'
NBR_WEIGHT_SUFFIX = '_weight'
 

Hiperparâmetros

Usaremos uma instância do HParams para incluir vários hiperparâmetros e constantes usados ​​para treinamento e avaliação. Descrevemos brevemente cada um deles abaixo:

  • num_classes : Existem 2 classes - positivas e negativas .

  • max_seq_length : este é o número máximo de palavras consideradas em cada crítica de filme neste exemplo.

  • vocab_size : este é o tamanho do vocabulário considerado para este exemplo.

  • distance_type : Essa é a métrica da 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_neighbors : O número de vizinhos usados ​​para regularização de gráficos. Este valor deve ser menor ou igual ao argumento max_nbrs usado acima ao chamar nsl.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 da conclusão da avaliação. Se definido como None , todas as instâncias no conjunto de testes serã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 números 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 ocorrência de palavras, semelhante a uma codificação one-hot. Por exemplo, a sequência [3, 5] se tornaria um vetor dimensional de 10000 que é todos os zeros, exceto os índices 3 e 5 , que são esses. Em seguida, torne essa a primeira camada em nossa rede - uma camada Dense - capaz de lidar com dados vetoriais de ponto flutuante. Essa abordagem num_words * num_reviews memória, porém, exigindo uma matriz de tamanho num_words * num_reviews .

  • Como alternativa, podemos preencher as matrizes para que todas tenham o mesmo comprimento e criar um tensor inteiro da forma 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 de filmes devem ter o mesmo tamanho, usaremos a função pad_sequence definida abaixo para padronizar os comprimentos.

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

Construa o modelo

Uma rede neural é criada empilhando camadas - isso requer duas decisões arquiteturais 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 a serem preditos 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 empilhadas efetivamente sequencialmente para criar o classificador:

  1. A primeira camada é uma camada de Input que aceita o vocabulário codificado por número inteiro.
  2. A próxima camada é uma camada de Embedding , que pega o vocabulário codificado por número 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 à matriz 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. Esse vetor de saída de comprimento fixo é canalizado através de uma camada totalmente conectada ( Dense ) com 64 unidades ocultas.
  5. A última camada está densamente conectada com um único nó de saída. Usando a função de ativação sigmoid , esse valor é um valor flutuante entre 0 e 1, representando uma probabilidade ou nível de confiança.

Unidades ocultas

O modelo acima tem duas camadas intermediárias ou "ocultas", entre a entrada e a saída, e excluindo a camada Embedding . O número de saídas (unidades, nós ou neurônios) é a dimensão do espaço representacional da camada. Em outras palavras, a quantidade de liberdade que a rede é permitida ao aprender uma representação interna.

Se um modelo tiver mais unidades ocultas (um espaço de representação de maior dimensão) e / ou mais camadas, a rede poderá aprender representações mais complexas. No entanto, torna a rede mais cara em termos de computação 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 ajuste excessivo .

Função de perda e otimizador

Um modelo precisa de uma função de perda e um otimizador para treinamento. Como esse é 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 nos dados que ele não tinha visto antes. Crie um conjunto de validação separando uma fração dos dados de treinamento originais. (Por que não usar o conjunto de testes agora? Nosso objetivo é desenvolver e ajustar nosso modelo usando apenas os dados de treinamento e depois usá-los apenas uma vez para avaliar nossa precisão).

Neste tutorial, coletamos aproximadamente 10% das amostras de treinamento inicial (10% de 25000) como dados rotulados para treinamento e o restante como dados de validação. Como a divisão inicial de trem / teste foi de 50/50 (25.000 amostras cada), a divisão efetiva de trem / validação / teste 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 925ms/step - loss: 0.6930 - accuracy: 0.5092 - val_loss: 0.6924 - val_accuracy: 0.5006
Epoch 2/10
21/21 [==============================] - 19s 894ms/step - loss: 0.6890 - accuracy: 0.5465 - val_loss: 0.7294 - val_accuracy: 0.5698
Epoch 3/10
21/21 [==============================] - 19s 883ms/step - loss: 0.6785 - accuracy: 0.6208 - val_loss: 0.6489 - val_accuracy: 0.7043
Epoch 4/10
21/21 [==============================] - 19s 890ms/step - loss: 0.6592 - accuracy: 0.6400 - val_loss: 0.6523 - val_accuracy: 0.6866
Epoch 5/10
21/21 [==============================] - 19s 883ms/step - loss: 0.6413 - accuracy: 0.6923 - val_loss: 0.6335 - val_accuracy: 0.7004
Epoch 6/10
21/21 [==============================] - 21s 982ms/step - loss: 0.6053 - accuracy: 0.7188 - val_loss: 0.5716 - val_accuracy: 0.7183
Epoch 7/10
21/21 [==============================] - 18s 879ms/step - loss: 0.5204 - accuracy: 0.7619 - val_loss: 0.4511 - val_accuracy: 0.7930
Epoch 8/10
21/21 [==============================] - 19s 882ms/step - loss: 0.4719 - accuracy: 0.7758 - val_loss: 0.4244 - val_accuracy: 0.8094
Epoch 9/10
21/21 [==============================] - 18s 880ms/step - loss: 0.3695 - accuracy: 0.8431 - val_loss: 0.3567 - val_accuracy: 0.8487
Epoch 10/10
21/21 [==============================] - 19s 891ms/step - loss: 0.3504 - accuracy: 0.8500 - val_loss: 0.3219 - val_accuracy: 0.8652

Avalie o modelo

Agora, vamos ver o desempenho do modelo. 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 [==============================] - 17s 85ms/step - loss: 0.4116 - accuracy: 0.8221
[0.4116455018520355, 0.8221200108528137]

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 plotar a perda de treinamento e validação para comparação, bem como a precisão do 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 descida de gradiente - deve minimizar a quantidade desejada em cada iteração.

Regularização de grafos

Agora estamos prontos para tentar a regularização de gráficos usando o modelo básico que criamos acima. Usaremos a classe de wrapper GraphRegularization fornecida pela estrutura Neural Structured Learning para agrupar o modelo base (bi-LSTM) para incluir a regularização de gráficos. O restante das etapas de treinamento e avaliação do modelo regularizado por gráfico é semelhante ao do modelo base.

Criar modelo regularizado por gráfico

Para avaliar o benefício incremental da regularização de gráficos, criaremos uma nova instância do modelo base. Isso ocorre porque o model já foi treinado para algumas iterações e a reutilização desse modelo treinado para criar um modelo com gráfico regularizado 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.6930 - accuracy: 0.5246 - scaled_graph_loss: 2.9800e-06 - val_loss: 0.6929 - val_accuracy: 0.4998
Epoch 2/10
21/21 [==============================] - 21s 988ms/step - loss: 0.6909 - accuracy: 0.5200 - scaled_graph_loss: 7.8452e-06 - val_loss: 0.6838 - val_accuracy: 0.5917
Epoch 3/10
21/21 [==============================] - 21s 980ms/step - loss: 0.6656 - accuracy: 0.6277 - scaled_graph_loss: 6.1205e-04 - val_loss: 0.6591 - val_accuracy: 0.6905
Epoch 4/10
21/21 [==============================] - 21s 981ms/step - loss: 0.6395 - accuracy: 0.6846 - scaled_graph_loss: 0.0016 - val_loss: 0.5860 - val_accuracy: 0.7171
Epoch 5/10
21/21 [==============================] - 21s 980ms/step - loss: 0.5388 - accuracy: 0.7573 - scaled_graph_loss: 0.0043 - val_loss: 0.4910 - val_accuracy: 0.7844
Epoch 6/10
21/21 [==============================] - 21s 989ms/step - loss: 0.4105 - accuracy: 0.8281 - scaled_graph_loss: 0.0146 - val_loss: 0.3353 - val_accuracy: 0.8612
Epoch 7/10
21/21 [==============================] - 21s 986ms/step - loss: 0.3416 - accuracy: 0.8681 - scaled_graph_loss: 0.0203 - val_loss: 0.4134 - val_accuracy: 0.8209
Epoch 8/10
21/21 [==============================] - 21s 981ms/step - loss: 0.4230 - accuracy: 0.8273 - scaled_graph_loss: 0.0144 - val_loss: 0.4755 - val_accuracy: 0.7696
Epoch 9/10
21/21 [==============================] - 22s 1s/step - loss: 0.4905 - accuracy: 0.7950 - scaled_graph_loss: 0.0080 - val_loss: 0.3862 - val_accuracy: 0.8382
Epoch 10/10
21/21 [==============================] - 21s 978ms/step - loss: 0.3384 - accuracy: 0.8754 - scaled_graph_loss: 0.0215 - val_loss: 0.3002 - val_accuracy: 0.8811

Avalie o modelo

 graph_reg_results = graph_reg_model.evaluate(test_dataset, steps=HPARAMS.eval_steps)
print(graph_reg_results)
 
196/196 [==============================] - 16s 84ms/step - loss: 0.3852 - accuracy: 0.8301
[0.385225385427475, 0.830079972743988]

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'])

No total, existem cinco entradas 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 plotá-los todos juntos para comparação. Observe que a perda de gráfico é calculada apenas 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 semi-supervisionado e, mais especificamente, a regularização de gráficos no contexto deste tutorial, podem ser realmente poderosos quando a quantidade de dados de treinamento é pequena. A falta de dados de treinamento é compensada aproveitando a similaridade entre as amostras de treinamento, o que não é possível no aprendizado supervisionado tradicional.

Definimos a taxa de supervisão como a proporção de amostras de treinamento em relação ao número total de amostras, que inclui amostras de treinamento, validação e teste. Neste caderno, usamos uma taxa de supervisão de 0,05 (ou seja, 5% dos dados rotulados) para treinar tanto o modelo básico quanto o modelo regularizado por gráficos. 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, à medida que a taxa de superivisão diminui, a precisão do modelo também diminui. Isso vale tanto para o modelo base quanto para o modelo com gráfico regularizado, independentemente da arquitetura do modelo usada. No entanto, observe que o modelo com gráfico regularizado tem um desempenho melhor que o modelo base para as duas arquiteturas. Em particular, para o modelo Bi-LSTM, quando a taxa de supervisão é de 0,01, a precisão do modelo com gráfico regularizado é ~ 20% maior que a do modelo base. Isso ocorre principalmente devido ao aprendizado semi-supervisionado para o modelo de gráfico regularizado, onde a similaridade estrutural entre as amostras de treinamento é usada além das próprias amostras de treinamento.

Conclusão

Demonstramos o uso da regularização de grafos usando a estrutura Neural Structured Learning (NSL), mesmo quando a entrada não contém um gráfico explícito. Consideramos a tarefa de classificação de sentimentos das revisões de filmes do IMDB, para as quais sintetizamos um gráfico de similaridade com base em incorporações de revisões. Incentivamos os usuários a experimentar ainda mais variando os hiperparâmetros, a quantidade de supervisão e usando diferentes arquiteturas de modelo.