Ta strona została przetłumaczona przez Cloud Translation API.
Switch to English

Regularyzacja grafów dla klasyfikacji sentymentów za pomocą zsyntetyzowanych wykresów

Zobacz na TensorFlow.org Uruchom w Google Colab Wyświetl źródło na GitHub

Przegląd

Ten notatnik klasyfikuje recenzje filmów jako pozytywne lub negatywne na podstawie tekstu recenzji. To jest przykład klasyfikacji binarnej , ważnego i szeroko stosowanego problemu uczenia maszynowego.

W tym notatniku zademonstrujemy użycie regularyzacji wykresów, budując wykres na podstawie podanych danych wejściowych. Ogólna recepta na zbudowanie modelu regularyzowanego grafem przy użyciu struktury uczenia się neuronowego (NSL), gdy dane wejściowe nie zawierają jawnego wykresu, jest następująca:

  1. Utwórz osadzenia dla każdego próbki tekstu w danych wejściowych. Można to zrobić za pomocą wstępnie wytrenowanych modeli, takich jak word2vec , Swivel , BERT itp.
  2. Zbuduj wykres w oparciu o te osadzenia, używając metryki podobieństwa, takiej jak odległość „L2”, odległość „cosinus” itp. Węzły na wykresie odpowiadają próbkom, a krawędzie na wykresie odpowiadają podobieństwu między parami próbek.
  3. Wygeneruj dane treningowe z powyższego zsyntetyzowanego wykresu i przykładowych funkcji. Wynikowe dane uczące będą zawierały sąsiadujące elementy oprócz oryginalnych funkcji węzła.
  4. Utwórz sieć neuronową jako model podstawowy za pomocą sekwencyjnego, funkcjonalnego lub podklasowego interfejsu API Keras.
  5. Zawiń model podstawowy klasą otoki GraphRegularization, która jest udostępniana przez strukturę NSL, aby utworzyć nowy model wykresu Keras. Ten nowy model będzie zawierał wykres utraty regularyzacji jako termin regularyzacji w celu szkoleniowym.
  6. Wytrenuj i oceń model wykresu Keras.

Wymagania

  1. Zainstaluj pakiet Neural Structured Learning.
  2. Zainstaluj koncentrator tensorflow.
pip install --quiet neural-structured-learning
pip install --quiet tensorflow-hub

Zależności i import

 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

Zbiór danych IMDB

Zbiór danych IMDB zawiera tekst 50 000 recenzji filmów z internetowej bazy danych filmów . Są one podzielone na 25 000 recenzji do szkoleń i 25 000 recenzji do testów. Zestawy treningowe i testowe są zbilansowane , co oznacza, że ​​zawierają równą liczbę pozytywnych i negatywnych recenzji.

W tym samouczku użyjemy wstępnie przetworzonej wersji zestawu danych IMDB.

Pobierz wstępnie przetworzony zbiór danych IMDB

Zestaw danych IMDB jest dostarczany z TensorFlow. Został on już wstępnie przetworzony w taki sposób, że recenzje (sekwencje słów) zostały przekonwertowane na sekwencje liczb całkowitych, gdzie każda liczba całkowita reprezentuje określone słowo w słowniku.

Poniższy kod pobiera zestaw danych IMDB (lub używa kopii z pamięci podręcznej, jeśli została już pobrana):

 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

Argument num_words=10000 zachowuje 10 000 najczęściej występujących słów w danych num_words=10000 . Rzadkie słowa są odrzucane, aby utrzymać wielkość słownictwa w zarządzaniu.

Przeglądaj dane

Poświęćmy chwilę, aby zrozumieć format danych. Zestaw danych jest wstępnie przetworzony: każdy przykład jest tablicą liczb całkowitych reprezentujących słowa recenzji filmu. Każda etykieta jest liczbą całkowitą równą 0 lub 1, gdzie 0 to negatywna recenzja, a 1 to pozytywna opinia.

 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

Tekst recenzji został przekonwertowany na liczby całkowite, gdzie każda liczba całkowita reprezentuje określone słowo w słowniku. Oto jak wygląda pierwsza recenzja:

 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]

Recenzje filmów mogą mieć różną długość. Poniższy kod pokazuje liczbę słów w pierwszej i drugiej recenzji. Ponieważ dane wejściowe do sieci neuronowej muszą mieć taką samą długość, będziemy musieli rozwiązać ten problem później.

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

Zamień liczby całkowite z powrotem na słowa

Warto wiedzieć, jak przekonwertować liczby całkowite z powrotem na odpowiedni tekst. Tutaj utworzymy funkcję pomocniczą do odpytywania obiektu słownika, który zawiera odwzorowanie liczby całkowitej na ciąg:

 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

Teraz możemy użyć funkcji decode_review do wyświetlenia tekstu do pierwszej recenzji:

 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"

Konstrukcja wykresu

Konstrukcja wykresu polega na utworzeniu osadzeń dla próbek tekstu, a następnie użyciu funkcji podobieństwa w celu porównania osadzeń.

Zanim przejdziemy dalej, najpierw tworzymy katalog do przechowywania artefaktów utworzonych w tym samouczku.

mkdir -p /tmp/imdb

Utwórz przykładowe osadzenia

Użyjemy wstępnie wytrenowanych osadzeń Swivel do tworzenia tf.train.Example formacie tf.train.Example dla każdej próbki w wejściu. Będziemy przechowywać wynikowe osadzenia w formacie TFRecord wraz z dodatkową funkcją, która reprezentuje identyfikator każdej próbki. Jest to ważne i pozwoli nam później dopasować przykładowe osadzenia z odpowiednimi węzłami na wykresie.

 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

Zbuduj wykres

Teraz, gdy mamy już przykładowe osadzenia, użyjemy ich do zbudowania wykresu podobieństwa, tj. Węzły na tym wykresie będą odpowiadały próbkom, a krawędzie na tym wykresie będą odpowiadały podobieństwu między parami węzłów.

Neural Structured Learning zapewnia bibliotekę budującą wykresy do tworzenia wykresów w oparciu o przykładowe osadzenia. Używa podobieństwa cosinusowego jako miary podobieństwa do porównywania osadzeń i tworzenia krawędzi między nimi. Pozwala nam również określić próg podobieństwa, który można wykorzystać do odrzucenia niepodobnych krawędzi z końcowego wykresu. W tym przykładzie, używając 0,99 jako progu podobieństwa, otrzymujemy wykres, który ma 445,327 dwukierunkowych krawędzi.

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

Przykładowe funkcje

Tworzymy przykładowe funkcje dla naszego problemu przy użyciu formatu tf.train.Example i tf.train.Example je w formacie TFRecord . Każda próbka będzie zawierać następujące trzy funkcje:

  1. id : identyfikator węzła próbki.
  2. words : lista int64 zawierająca identyfikatory słów.
  3. label : singleton int64 identyfikujący klasę docelową recenzji.
 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

Rozszerz dane treningowe o sąsiadów wykresów

Ponieważ mamy przykładowe funkcje i zsyntetyzowany wykres, możemy wygenerować rozszerzone dane szkoleniowe dla uczenia się opartego na neuronach. Struktura NSL zapewnia bibliotekę umożliwiającą połączenie wykresu i przykładowych funkcji w celu uzyskania ostatecznych danych szkoleniowych do regularyzacji wykresów. Wynikowe dane szkoleniowe będą zawierały oryginalne przykładowe elementy, a także cechy odpowiadających im sąsiadów.

W tym samouczku rozważymy nieukierunkowane krawędzie i wykorzystamy maksymalnie 3 sąsiadów na próbkę, aby rozszerzyć dane szkoleniowe o sąsiadów wykresu.

 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)
 

Model podstawowy

Jesteśmy teraz gotowi do zbudowania modelu podstawowego bez regularyzacji wykresów. Aby zbudować ten model, możemy albo użyć osadzeń, które zostały użyte do zbudowania wykresu, albo możemy nauczyć się nowych osadzeń wspólnie z zadaniem klasyfikacyjnym. Na potrzeby tego notatnika zrobimy to drugie.

Zmienne globalne

 NBR_FEATURE_PREFIX = 'NL_nbr_'
NBR_WEIGHT_SUFFIX = '_weight'
 

Hiperparametry

Wykorzystamy instancję HParams aby uwzględnić różne hiperparametry i stałe używane do treningu i oceny. Poniżej krótko opisujemy każdy z nich:

  • num_classes : Istnieją 2 klasy - pozytywne i negatywne .

  • max_seq_length : Jest to maksymalna liczba rozważanych słów z każdej recenzji filmu w tym przykładzie.

  • vocab_size : jest to rozmiar słownictwa branego pod uwagę w tym przykładzie.

  • distance_type : Jest to metryka odległości używana do regulowania próbki względem jej sąsiadów.

  • graph_regularization_multiplier : kontroluje względną wagę składnika regularyzacji wykresu w ogólnej funkcji straty.

  • num_neighbors : liczba sąsiadów użytych do uregulowania grafu. Wartość ta musi być mniejsza lub równa argumentowi max_nbrs powyżej podczas wywoływania nsl.tools.pack_nbrs .

  • num_fc_units : liczba jednostek w pełni połączonej warstwie sieci neuronowej.

  • train_epochs : liczba okresów szkoleniowych.

  • batch_size : rozmiar partii używany do szkolenia i oceny.

  • eval_steps : liczba partii do przetworzenia przed uznaniem, że ocena została zakończona. Jeśli jest ustawiona na None , oceniane są wszystkie wystąpienia w zestawie testowym.

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

Przygotuj dane

Recenzje - tablice liczb całkowitych - muszą zostać przekonwertowane na tensory, zanim zostaną wprowadzone do sieci neuronowej. Tę konwersję można przeprowadzić na kilka sposobów:

  • Konwertuj tablice na wektory o długości 0 si 1 s, wskazując wystąpienie słowa, podobnie jak w przypadku kodowania z jednym gorącym kodem. Na przykład sekwencja [3, 5] stałaby się 10000 wymiarowym wektorem zawierającym wszystkie zera z wyjątkiem indeksów 3 i 5 , które są jedynkami. Następnie uczyń z niej pierwszą warstwę w naszej sieci - warstwę Dense - która może obsługiwać zmiennoprzecinkowe dane wektorowe. To podejście wymaga jednak dużej ilości pamięci i wymaga macierzy rozmiarów num_words * num_reviews .

  • Alternatywnie możemy max_length * num_reviews tablice, aby wszystkie miały tę samą długość, a następnie utworzyć tensor całkowity o kształcie max_length * num_reviews . Możemy użyć warstwy osadzającej zdolnej do obsługi tego kształtu jako pierwszej warstwy w naszej sieci.

W tym samouczku użyjemy drugiego podejścia.

Ponieważ recenzje filmów muszą mieć taką samą długość, użyjemy zdefiniowanej poniżej funkcji pad_sequence aby ustandaryzować długości.

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

Zbuduj model

Sieć neuronowa jest tworzona przez układanie warstw - wymaga to dwóch głównych decyzji architektonicznych:

  • Ile warstw użyć w modelu?
  • Ile ukrytych jednostek użyć na każdej warstwie?

W tym przykładzie dane wejściowe składają się z tablicy indeksów słów. Etykiety do prognozowania to 0 lub 1.

W tym samouczku użyjemy dwukierunkowego LSTM jako modelu podstawowego.

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

Warstwy są efektywnie układane sekwencyjnie, aby zbudować klasyfikator:

  1. Pierwsza warstwa to warstwa Input , która pobiera słownik zakodowany w postaci liczb całkowitych.
  2. Następną warstwą jest warstwa Embedding , która pobiera słownik zakodowany w postaci liczb całkowitych i wyszukuje wektor osadzania dla każdego indeksu słów. Te wektory są uczone jako model pociągów. Wektory dodają wymiar do tablicy wyjściowej. Wynikowe wymiary to: (batch, sequence, embedding) .
  3. Następnie dwukierunkowa warstwa LSTM zwraca wektor wyjściowy o stałej długości dla każdego przykładu.
  4. Ten wektor wyjściowy o stałej długości jest przepuszczany przez w pełni połączoną ( Dense ) warstwę z 64 ukrytymi jednostkami.
  5. Ostatnia warstwa jest gęsto połączona z jednym węzłem wyjściowym. Korzystając z sigmoid funkcji aktywacji, ta wartość jest liczbą zmiennoprzecinkową między 0 a 1, reprezentującą prawdopodobieństwo lub poziom ufności.

Ukryte jednostki

Powyższy model ma dwie warstwy pośrednie lub „ukryte”, między wejściem a wyjściem, z wyłączeniem warstwy Embedding . Liczba wyjść (jednostek, węzłów lub neuronów) to wymiar przestrzeni reprezentacyjnej dla warstwy. Innymi słowy, ilość swobody, na jaką pozwala sieć podczas uczenia się wewnętrznej reprezentacji.

Jeśli model ma więcej ukrytych jednostek (bardziej wymiarową przestrzeń reprezentacji) i / lub więcej warstw, wówczas sieć może nauczyć się bardziej złożonych reprezentacji. Jednak powoduje to, że sieć jest bardziej kosztowna obliczeniowo i może prowadzić do uczenia się niepożądanych wzorców - wzorców, które poprawiają wydajność na danych uczących, ale nie na danych testowych. Nazywa się to nadmiernym dopasowaniem .

Funkcja strat i optymalizator

Model potrzebuje funkcji straty i optymalizatora do treningu. Ponieważ jest to problem klasyfikacji binarnej, a model generuje prawdopodobieństwo (warstwa pojedynczej jednostki z aktywacją sigmoidalną), użyjemy funkcji straty binary_crossentropy .

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

Utwórz zestaw walidacyjny

Podczas treningu chcemy sprawdzić dokładność modelu na danych, których wcześniej nie widział. Utwórz zestaw walidacyjny, oddzielając ułamek oryginalnych danych szkoleniowych. (Dlaczego nie skorzystać teraz z zestawu testowego? Naszym celem jest opracowanie i dostrojenie naszego modelu przy użyciu wyłącznie danych szkoleniowych, a następnie wykorzystanie danych testowych tylko raz, aby ocenić naszą dokładność).

W tym samouczku pobieramy około 10% początkowych próbek treningowych (10% z 25000) jako oznakowane dane do uczenia, a pozostałe jako dane walidacyjne. Ponieważ początkowy podział pociąg / test wynosił 50/50 (po 25000 próbek), efektywny podział na pociąg / walidację / test, jaki mamy teraz, to 5/45/50.

Zauważ, że „train_dataset” został już wsadowany i przetasowany.

 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

Wytrenuj model

Trenuj model w małych partiach. Podczas uczenia monitoruj utratę modelu i dokładność na zbiorze walidacyjnym:

 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

Oceń model

Teraz zobaczmy, jak działa model. Zostaną zwrócone dwie wartości. Strata (liczba, która reprezentuje nasz błąd, niższe wartości są lepsze) i dokładność.

 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]

Utwórz wykres dokładności / utraty w czasie

model.fit() zwraca obiekt History który zawiera słownik ze wszystkim, co wydarzyło się podczas treningu:

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

Istnieją cztery wpisy: po jednym dla każdej monitorowanej metryki podczas szkolenia i walidacji. Możemy ich użyć do wykreślenia strat podczas szkolenia i walidacji w celu porównania, a także dokładności szkolenia i walidacji:

 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

Zauważ, że utrata treningu zmniejsza się z każdą epoką, a dokładność treningu rośnie z każdą epoką. Jest to oczekiwane w przypadku korzystania z optymalizacji zejścia gradientu - powinno minimalizować pożądaną ilość w każdej iteracji.

Regularyzacja wykresu

Jesteśmy teraz gotowi do wypróbowania regularyzacji wykresu przy użyciu modelu podstawowego, który zbudowaliśmy powyżej. GraphRegularization klasy opakowania GraphRegularization dostarczonej przez platformę Neural Structured Learning, aby opakować model podstawowy (bi-LSTM), aby uwzględnić regularyzację wykresu. Pozostałe kroki dotyczące uczenia i oceny modelu regularyzowanego wykresem są podobne do tych z modelu podstawowego.

Utwórz model regularyzowany wykresem

Aby ocenić przyrostowe korzyści regularyzacji wykresów, utworzymy nową instancję modelu podstawowego. Dzieje się tak, ponieważ model został już przeszkolony dla kilku iteracji, a ponowne użycie tego wytrenowanego modelu do utworzenia modelu regularyzowanego wykresem nie będzie sprawiedliwym porównaniem 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'])
 

Wytrenuj model

 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

Oceń model

 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]

Utwórz wykres dokładności / utraty w czasie

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

W słowniku znajduje się łącznie pięć wpisów: utrata treningu, dokładność treningu, utrata wykresu treningowego, utrata walidacji i dokładność walidacji. Możemy wykreślić je wszystkie razem dla porównania. Zauważ, że utrata wykresu jest obliczana tylko podczas treningu.

 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

Potęga uczenia się częściowo nadzorowanego

Częściowo nadzorowane uczenie się, a dokładniej regularyzacja wykresów w kontekście tego samouczka, może być naprawdę potężne, gdy ilość danych szkoleniowych jest niewielka. Brak danych szkoleniowych jest kompensowany przez wykorzystanie podobieństwa między próbkami szkoleniowymi, co nie jest możliwe w tradycyjnym uczeniu nadzorowanym.

Definiujemy wskaźnik nadzoru jako stosunek próbek szkoleniowych do całkowitej liczby próbek obejmujących próbki szkoleniowe, walidacyjne i testowe. W tym notatniku użyliśmy wskaźnika nadzoru 0,05 (tj. 5% oznaczonych danych) do uczenia zarówno modelu podstawowego, jak i modelu regularyzowanego wykresem. Ilustrujemy wpływ wskaźnika nadzoru na dokładność modelu w komórce poniżej.

 # 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

Można zaobserwować, że wraz ze spadkiem współczynnika superivision maleje również dokładność modelu. Dotyczy to zarówno modelu podstawowego, jak i modelu regularyzowanego grafem, niezależnie od używanej architektury modelu. Należy jednak zauważyć, że model regularyzowany wykresem działa lepiej niż model podstawowy dla obu architektur. W szczególności dla modelu Bi-LSTM, gdy współczynnik nadzoru wynosi 0,01, dokładność modelu regularyzowanego wykresem jest o ~ 20% wyższa niż modelu podstawowego. Dzieje się tak głównie z powodu częściowo nadzorowanego uczenia się dla modelu regularyzowanego grafem, w którym podobieństwo strukturalne między próbkami uczącymi jest używane oprócz samych próbek szkoleniowych.

Wniosek

Zademonstrowaliśmy użycie regularyzacji wykresów przy użyciu struktury uczenia się neuronowego (NSL), nawet jeśli dane wejściowe nie zawierają jawnego wykresu. Rozważaliśmy zadanie klasyfikacji sentymentalnej recenzji filmów IMDB, dla których zsyntetyzowaliśmy wykres podobieństwa oparty na osadzeniach recenzji. Zachęcamy użytkowników do dalszych eksperymentów, zmieniając hiperparametry, zakres nadzoru i używając różnych architektur modeli.