Регуляризация графов для классификации настроений с использованием синтезированных графов

Посмотреть на TensorFlow.org Запускаем в Google Colab Посмотреть исходный код на GitHub

Обзор

Эта записная книжка классифицирует обзоры фильмов как положительные или отрицательные, используя текст обзора. Это пример двоичной классификации, важной и широко применяемой задачи машинного обучения.

Мы продемонстрируем использование регуляризации графа в этой записной книжке, построив граф из заданных входных данных. Общий рецепт построения модели с регуляризацией графа с использованием структуры нейронного структурированного обучения (NSL), когда входные данные не содержат явного графа, выглядит следующим образом:

  1. Создайте вложения для каждого образца текста во входных данных. Это можно сделать с помощью предварительно обученных моделей, таких как word2vec , Swivel , BERT и т. Д.
  2. Постройте граф на основе этих вложений, используя метрику подобия, такую ​​как расстояние «L2», расстояние «косинус» и т. Д. Узлы в графе соответствуют выборкам, а ребра в графе соответствуют сходству между парами выборок.
  3. Сгенерируйте обучающие данные из синтезированного выше графика и примеров функций. Результирующие обучающие данные будут содержать соседние функции в дополнение к исходным функциям узла.
  4. Создайте нейронную сеть в качестве базовой модели, используя последовательный, функциональный или подклассовый API Keras.
  5. Оберните базовую модель классом-оболочкой GraphRegularization, который предоставляется фреймворком NSL, чтобы создать новую модель графа Keras. Эта новая модель будет включать потерю регуляризации графа в качестве члена регуляризации в цели обучения.
  6. Обучите и оцените модель графа Кераса.

Требования

  1. Установите пакет нейронного структурированного обучения.
  2. Установите tenorflow-hub.
pip install --quiet neural-structured-learning
pip install --quiet tensorflow-hub

Зависимости и импорт

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

Набор данных IMDB

Набор данных IMDB содержит текст 50 000 обзоров фильмов из базы данных Internet Movie Database . Они разделены на 25 000 обзоров для обучения и 25 000 обзоров для тестирования. Наборы для обучения и тестирования сбалансированы , то есть содержат одинаковое количество положительных и отрицательных отзывов.

В этом руководстве мы будем использовать предварительно обработанную версию набора данных IMDB.

Скачать предварительно обработанный набор данных IMDB

Набор данных IMDB поставляется в комплекте с TensorFlow. Он уже был предварительно обработан, так что обзоры (последовательности слов) были преобразованы в последовательности целых чисел, где каждое целое число представляет определенное слово в словаре.

Следующий код загружает набор данных IMDB (или использует кэшированную копию, если она уже была загружена):

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

Аргумент num_words=10000 сохраняет первые 10 000 наиболее часто встречающихся слов в обучающих данных. Редкие слова отбрасываются, чтобы объем словарного запаса оставался управляемым.

Изучите данные

Давайте потратим время, чтобы понять формат данных. Набор данных предварительно обработан: каждый пример представляет собой массив целых чисел, представляющих слова из обзора фильма. Каждая метка представляет собой целое число 0 или 1, где 0 - отрицательный отзыв, а 1 - положительный отзыв.

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

Текст отзывов был преобразован в целые числа, где каждое целое число представляет собой определенное слово в словаре. Вот как выглядит первый обзор:

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]

Рецензии на фильмы могут быть разной длины. В приведенном ниже коде показано количество слов в первом и втором обзоре. Поскольку входы в нейронную сеть должны быть одинаковой длины, нам нужно будет решить эту проблему позже.

len(pp_train_data[0]), len(pp_train_data[1])
(218, 189)

Преобразуйте целые числа обратно в слова

Может быть полезно узнать, как преобразовать целые числа обратно в соответствующий текст. Здесь мы создадим вспомогательную функцию для запроса объекта словаря, который содержит преобразование целого числа в строку:

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

Теперь мы можем использовать функцию decode_review для отображения текста для первого обзора:

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"

Построение графа

Построение графа включает создание вложений для образцов текста, а затем использование функции подобия для сравнения вложений.

Прежде чем продолжить, мы сначала создаем каталог для хранения артефактов, созданных в этом руководстве.

mkdir -p /tmp/imdb

Создать образцы встраиваний

Мы будем использовать предварительно обученные вложения Swivel для создания вложений в формате tf.train.Example для каждого образца во входных данных. Мы будем хранить полученные вложения в формате TFRecord вместе с дополнительной функцией, которая представляет идентификатор каждого образца. Это важно и позволит нам позже сопоставить образцы вложения с соответствующими узлами в графе.

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

Построить график

Теперь, когда у нас есть образцы вложения, мы будем использовать их для построения графа подобия, то есть узлы в этом графе будут соответствовать образцам, а ребра в этом графе будут соответствовать подобию между парами узлов.

Neural Structured Learning предоставляет библиотеку построения графиков для построения графиков на основе примеров внедрения. Он использует косинусное сходство в качестве меры сходства для сравнения вложений и построения ребер между ними. Это также позволяет нам указать порог подобия, который можно использовать, чтобы отбросить несходные ребра из окончательного графа. В этом примере, используя 0,99 в качестве порога сходства и 12345 в качестве случайного начального числа, мы получаем граф, который имеет 429 415 двунаправленных ребер. Здесь мы используем поддержку построителем графов хеширования с учетом локальности (LSH), чтобы ускорить построение графа. Дополнительные сведения об использовании поддержки LSH построителя графов см. build_graph_from_config документации 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)

Каждое двунаправленное ребро представлено двумя направленными ребрами в выходном файле TSV, так что этот файл содержит 429 415 * 2 = 858 830 строк:

wc -l /tmp/imdb/graph_99.tsv
858830 /tmp/imdb/graph_99.tsv

Примеры функций

Мы создаем образцы функций для нашей проблемы, используя формат tf.train.Example , и сохраняем их в формате TFRecord . Каждый образец будет включать следующие три функции:

  1. id : ID узла образца.
  2. слова : список int64, содержащий идентификаторы слов.
  3. label : Одноэлементный int64, определяющий целевой класс обзора.
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

Дополните обучающие данные соседями по графу

Поскольку у нас есть образцы функций и синтезированный график, мы можем сгенерировать расширенные обучающие данные для нейронного структурированного обучения. Инфраструктура NSL предоставляет библиотеку для объединения графа и примеров функций для получения окончательных обучающих данных для регуляризации графа. Результирующие обучающие данные будут включать исходные образцы функций, а также характеристики их соответствующих соседей.

В этом руководстве мы рассматриваем неориентированные ребра и используем максимум 3 соседа на выборку, чтобы дополнить обучающие данные соседями графа.

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)

Базовая модель

Теперь мы готовы построить базовую модель без регуляризации графа. Чтобы построить эту модель, мы можем либо использовать вложения, которые использовались при построении графа, либо мы можем изучить новые вложения вместе с задачей классификации. В рамках этой записной книжки мы сделаем последнее.

Глобальные переменные

NBR_FEATURE_PREFIX = 'NL_nbr_'
NBR_WEIGHT_SUFFIX = '_weight'

Гиперпараметры

Мы будем использовать экземпляр HParams для включения различных гиперпараметров и констант, используемых для обучения и оценки. Кратко опишем каждый из них ниже:

  • num_classes : есть 2 класса - положительный и отрицательный .

  • max_seq_length : это максимальное количество слов, учитываемых в каждом обзоре фильма в этом примере.

  • Dictionary_size : это размер словаря, рассматриваемого в этом примере.

  • distance_type : это показатель расстояния, используемый для упорядочения выборки с ее соседями.

  • graph_regularization_multiplier : это контролирует относительный вес члена регуляризации графика в общей функции потерь.

  • num_neighbors : количество соседей, используемых для регуляризации графа. Это значение должно быть меньше или равно аргументу max_nbrs используемому выше при вызовеnsl.tools.pack_nbrs .

  • num_fc_units : количество единиц в полностью подключенном слое нейронной сети.

  • train_epochs : Количество эпох обучения.

  • batch_size : размер пакета, используемый для обучения и оценки.

  • eval_steps : количество пакетов для обработки до того, как оценка будет признана завершенной. Если установлено значение None , оцениваются все экземпляры в наборе тестов.

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

Подготовьте данные

Обзоры - массивы целых чисел - должны быть преобразованы в тензоры перед подачей в нейронную сеть. Это преобразование можно выполнить двумя способами:

  • Преобразуйте массивы в векторы 0 с и 1 с, указывающие на появление слова, аналогично однократному кодированию. Например, последовательность [3, 5] станет 10000 мерным вектором, в котором все нули, за исключением индексов 3 и 5 , которые равны единицам. Затем сделайте это первым слоем в нашей сети - Dense слоем - который может обрабатывать векторные данные с плавающей запятой. Однако этот подход требует num_words * num_reviews памяти и требует num_words * num_reviews размера num_words * num_reviews .

  • В качестве альтернативы мы можем дополнить массивы, чтобы все они имели одинаковую длину, а затем создать целочисленный тензор формы max_length * num_reviews . Мы можем использовать слой внедрения, способный обрабатывать эту форму, как первый слой в нашей сети.

В этом уроке мы будем использовать второй подход.

Поскольку обзоры фильмов должны быть одинаковой длины, мы будем использовать функцию pad_sequence определенную ниже, для стандартизации длин.

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

Построить модель

Нейронная сеть создается путем наложения слоев - это требует двух основных архитектурных решений:

  • Сколько слоев использовать в модели?
  • Сколько скрытых единиц использовать для каждого слоя?

В этом примере входные данные состоят из массива индексов слов. Метки для прогнозирования: 0 или 1.

Мы будем использовать двунаправленный LSTM в качестве нашей базовой модели в этом руководстве.

# 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
_________________________________________________________________

Слои эффективно складываются последовательно для построения классификатора:

  1. Первый уровень - это Input уровень, который принимает целочисленный словарь.
  2. Следующий уровень - это уровень Embedding , который берет словарь в целочисленном кодировании и ищет вектор внедрения для каждого индекса слова. Эти векторы изучаются по мере обучения модели. Векторы добавляют измерение к выходному массиву. В результате получаются следующие размеры: (batch, sequence, embedding) .
  3. Затем двунаправленный уровень LSTM возвращает выходной вектор фиксированной длины для каждого примера.
  4. Этот выходной вектор фиксированной длины передается по конвейеру через полностью связанный ( Dense ) слой с 64 скрытыми блоками.
  5. Последний слой плотно связан с единственным выходным узлом. При использовании sigmoid функции активации это значение представляет собой число с плавающей запятой между 0 и 1, представляющее вероятность или уровень достоверности.

Скрытые юниты

Вышеупомянутая модель имеет два промежуточных или «скрытых» слоя, между входом и выходом, за исключением слоя Embedding . Количество выходов (единиц, узлов или нейронов) - это размерность репрезентативного пространства для слоя. Другими словами, степень свободы, которую дает сеть при изучении внутреннего представления.

Если модель имеет больше скрытых единиц (пространство представления более высокой размерности) и / или больше слоев, тогда сеть может изучить более сложные представления. Однако это делает сеть более дорогостоящей с точки зрения вычислений и может привести к изучению нежелательных шаблонов - шаблонов, которые повышают производительность на обучающих данных, но не на тестовых данных. Это называется переобучением .

Функция потерь и оптимизатор

Модель нуждается в функции потерь и оптимизаторе для обучения. Поскольку это проблема двоичной классификации, и модель выводит вероятность ( binary_crossentropy слой с активацией сигмоида), мы будем использовать binary_crossentropy потерь binary_crossentropy .

model.compile(
    optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

Создайте набор для проверки

Во время обучения мы хотим проверить точность модели на данных, которых она раньше не видела. Создайте набор проверки , выделив часть исходных данных обучения. (Почему бы не использовать набор для тестирования сейчас? Наша цель - разработать и настроить нашу модель, используя только обучающие данные, а затем использовать тестовые данные только один раз, чтобы оценить нашу точность).

В этом руководстве мы берем примерно 10% исходных обучающих выборок (10% от 25000) как помеченные данные для обучения, а оставшиеся - как данные проверки. Поскольку начальное разделение поездов / тестов составляло 50/50 (по 25000 образцов каждый), эффективное разделение поездов / проверок / тестов, которое у нас сейчас есть, составляет 5/45/50.

Обратите внимание, что «train_dataset» уже был перемешан и перемешан.

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

Обучите модель

Обучайте модель мини-сериями. Во время обучения следите за потерями модели и точностью на проверочном наборе:

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

Оцените модель

Теперь посмотрим, как работает модель. Будут возвращены два значения. Потеря (число, которое представляет нашу ошибку, чем ниже значение, тем лучше) и точность.

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]

Создайте график точности / потерь с течением времени

model.fit() возвращает объект History который содержит словарь со всем, что произошло во время обучения:

history_dict = history.history
history_dict.keys()
dict_keys(['loss', 'accuracy', 'val_loss', 'val_accuracy'])

Есть четыре записи: по одной для каждой отслеживаемой метрики во время обучения и проверки. Мы можем использовать их для построения графика потерь при обучении и проверке для сравнения, а также для оценки точности обучения и проверки:

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

Обратите внимание, что потери при обучении уменьшаются с каждой эпохой, а точность обучения увеличивается с каждой эпохой. Это ожидается при использовании оптимизации градиентного спуска - она ​​должна минимизировать желаемое количество на каждой итерации.

Регуляризация графа

Теперь мы готовы попробовать регуляризацию графа с использованием базовой модели, которую мы построили выше. Мы будем использовать GraphRegularization оболочку GraphRegularization предоставляемый фреймворком нейронного структурированного обучения, чтобы обернуть базовую (bi-LSTM) модель, чтобы включить регуляризацию графа. Остальные шаги для обучения и оценки модели с регуляризацией графа аналогичны шагам базовой модели.

Создать модель с регуляризацией графа

Чтобы оценить дополнительные преимущества регуляризации графа, мы создадим новый экземпляр базовой модели. Это связано с тем, что model уже обучена для нескольких итераций, и повторное использование этой обученной модели для создания модели с регуляризацией графа не будет справедливым сравнением для 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'])

Обучите модель

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

Оцените модель

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]

Создайте график точности / потерь с течением времени

graph_reg_history_dict = graph_reg_history.history
graph_reg_history_dict.keys()
dict_keys(['loss', 'accuracy', 'scaled_graph_loss', 'val_loss', 'val_accuracy'])

Всего в словаре пять записей: потеря обучения, точность обучения, потеря графа обучения, потеря проверки и точность проверки. Мы можем построить их все вместе для сравнения. Обратите внимание, что потеря графика вычисляется только во время обучения.

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

Возможности обучения без учителя

Полу-контролируемое обучение и, в частности, регуляризация графиков в контексте этого руководства, могут быть действительно эффективными, когда объем обучающих данных невелик. Отсутствие обучающих данных компенсируется за счет использования сходства обучающих выборок, что невозможно при традиционном обучении с учителем.

Мы определяем коэффициент наблюдения как отношение обучающих выборок к общему количеству выборок, которое включает в себя обучающие, проверочные и тестовые выборки. В этом блокноте мы использовали коэффициент наблюдения 0,05 (т. Е. 5% помеченных данных) для обучения как базовой модели, так и модели с регуляризацией графа. Мы проиллюстрируем влияние коэффициента наблюдения на точность модели в ячейке ниже.

# 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

Можно заметить, что с уменьшением коэффициента контроля точность модели также снижается. Это верно как для базовой модели, так и для модели с регуляризацией графов, независимо от используемой архитектуры модели. Однако обратите внимание, что модель с регуляризацией графа работает лучше, чем базовая модель для обеих архитектур. В частности, для модели Bi-LSTM, когда коэффициент наблюдения равен 0,01, точность модели с регуляризацией графа на ~ 20% выше, чем у базовой модели. Это в первую очередь из-за частично контролируемого обучения для модели с регуляризацией графа, где структурное сходство между обучающими выборками используется в дополнение к самим обучающим выборкам.

Заключение

Мы продемонстрировали использование регуляризации графа с помощью структуры нейронного структурированного обучения (NSL), даже если входные данные не содержат явного графа. Мы рассмотрели задачу тональной классификации обзоров фильмов IMDB, для которых синтезировали граф сходства на основе вложения обзоров. Мы призываем пользователей к дальнейшим экспериментам, варьируя гиперпараметры, объем надзора и используя разные архитектуры моделей.