# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     name: python3
# ---

# + [markdown] id="i2k0Dk1lqqDY" colab_type="text"
# # Graph regularization for sentiment classification using synthesized graphs

# + [markdown] id="A7OmkGEGqsP5" colab_type="text"
# - What to do if natural graph is not available ?

# + [markdown] id="YCPo1er1q0Mn" colab_type="text"
# - Create a graph from the data. Synthesize the graph from the data

# + [markdown] id="MYn4uNOHrWHo" colab_type="text"
# # Recipe

# + [markdown] id="ljsh1aH7q84P" colab_type="text"
# The general recipe for building a graph-regularized model using the Neural Structured Learning (NSL) framework when the input does not contain an explicit graph is as follows:
#
# - Create embeddings for each text sample in the input. This can be done using pre-trained models such as word2vec, Swivel, BERT etc.
#
# - Build a graph based on these embeddings by using a similarity metric such as the 'L2' distance, 'cosine' distance, etc. Nodes in the graph correspond to samples and edges in the graph correspond to similarity between pairs of samples.
#
# - Generate training data from the above synthesized graph and sample features. The resulting training data will contain neighbor features in addition to the original node features.
#
# - Create a neural network as a base model using the Keras sequential, functional, or subclass API.
#
# - Wrap the base model with the GraphRegularization wrapper class, which is provided by the NSL framework, to create a new graph Keras model. This new model will include a graph regularization loss as the regularization term in its training objective.
# Train and evaluate the graph Keras model.
#

# + id="HVRmBAA1qWB2" colab_type="code" colab={}
# %tensorflow_version 2.x

# + id="U2cU2wJorH9o" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 0} outputId="4a38a6bd-e676-4bd8-bca6-0f9782f47c9e"
# ! pip install --quiet neural-structured-learning
# ! pip install --quiet tensorflow-hub

# + id="887KywmzrLOo" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 0} outputId="493c72fa-abc2-4eff-ee9f-843ca656a947"
import matplotlib.pyplot as plt
import numpy as np

import neural_structured_learning as nsl

import tensorflow as tf
tf.compat.v1.enable_v2_behavior()

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.test.is_gpu_available() else "NOT AVAILABLE")


# + [markdown] id="iwKBSQM4rTy_" colab_type="text"
# # Get the data

# + [markdown] id="Bw3-Ybparaja" colab_type="text"
# The IMDB dataset contains the text of 50,000 movie reviews from the Internet Movie Database. These are split into 25,000 reviews for training and 25,000 reviews for testing. The training and testing sets are balanced, meaning they contain an equal number of positive and negative reviews.

# + [markdown] id="Zauy6bmjrcw1" colab_type="text"
# The IMDB dataset comes packaged with TensorFlow. It has already been preprocessed such that the reviews (sequences of words) have been converted to sequences of integers, where each integer represents a specific word in a dictionary.
#
# The following code downloads the IMDB dataset (or uses a cached copy if it has already been downloaded):

# + id="l944L6qtrQCv" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 0} outputId="69f4eb67-c8d2-4d79-d506-833ee4d84266"
imdb = tf.keras.datasets.imdb
(pp_train_data, pp_train_labels), (pp_test_data, pp_test_labels) = (
    imdb.load_data(num_words=10000))

# + [markdown] id="cVGrOPP2rg6V" colab_type="text"
# The argument num_words=10000 keeps the top 10,000 most frequently occurring words in the training data. The rare words are discarded to keep the size of the vocabulary manageable.

# + [markdown] id="vRebICgHriuz" colab_type="text"
# ## Explore the data
#
# Let's take a moment to understand the format of the data. The dataset comes preprocessed: each example is an array of integers representing the words of the movie review. Each label is an integer value of either 0 or 1, where 0 is a negative review, and 1 is a positive review.

# + id="GmltCJ82rei1" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 0} outputId="02a876e7-2b50-45cb-cd59-1eb8fbcfb5fc"
print('Training entries: {}, labels: {}'.format(
    len(pp_train_data), len(pp_train_labels)))
training_samples_count = len(pp_train_data)


# + [markdown] id="IcKb-8CqroVl" colab_type="text"
# The text of reviews have been converted to integers, where each integer represents a specific word in a dictionary. Here's what the first review looks like:

# + id="7lKzlmZgrmnl" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 0} outputId="8802ba99-d239-4d1e-af28-53b78a8f9e15"
print(pp_train_data[0])


# + [markdown] id="4zJ7g9T2rvmf" colab_type="text"
# Movie reviews may be different lengths. The below code shows the number of words in the first and second reviews. Since inputs to a neural network must be the same length, we'll need to resolve this later.

# + [markdown] id="2qgcBibFrx9s" colab_type="text"
# ## Convert the integers back to words
#
# It may be useful to know how to convert integers back to the corresponding text. Here, we'll create a helper function to query a dictionary object that contains the integer to string mapping:

# + id="yynWR2yPrsjX" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 0} outputId="ddeaf9c0-fde8-4ae9-cd3d-9b6d0e9cc45b"
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])



# + id="VtG1MoQQsQoT" colab_type="code" colab={}
reverse_word_index = build_reverse_word_index()


# + id="stEgU3V8sSS-" colab_type="code" colab={}
def decode_review(text):
  return ' '.join([reverse_word_index.get(i, '?') for i in text])


# + id="rdMDkXLKsUZE" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 0} outputId="8e223217-5d1c-45f9-985e-2e6715b5b157"
decode_review(pp_train_data[0])

# + [markdown] id="cPI6qhN2shTJ" colab_type="text"
# # Constructing the graph

# + [markdown] id="kdwprAmesogC" colab_type="text"
# Graph construction involves creating embeddings for text samples and then using a similarity function to compare the embeddings.

# + id="22HRfu3cs3-h" colab_type="code" colab={}
# ! mkdir -p /tmp/imdb

# + id="mZi8wNaasYPa" colab_type="code" colab={}
# This is necessary because hub.KerasLayer assumes tensor hashability, which
# is not supported in eager mode.
tf.compat.v1.disable_tensor_equality()

# + [markdown] id="HAJyPcuKs6sw" colab_type="text"
# ## Create sample embeddings
#
# We will use pretrained Swivel embeddings to create embeddings in the tf.train.Example format for each sample in the input. We will store the resulting embeddings in the TFRecord format along with an additional feature that represents the ID of each sample. This is important and will allow us match sample embeddings with corresponding nodes in the graph later.

# + id="0SULmsK4svFJ" colab_type="code" colab={}
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)


# + id="iDUWEmEetMLX" colab_type="code" colab={}
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()))


# + id="M-RFrqChthgg" colab_type="code" colab={}
def create_embedding_example(word_vector, record_id):
    """Create tf.Example containing the sample's embedding and its ID."""
    # We build from text not from tokens
    text = decode_review(word_vector)
    # Remove the batch_size
    text = tf.reshape(text, shape=[-1, ])

    # Use the swivel embedding layer here to create embeddings from the text.
    sentence_emedding = hub_layer(text)

    # Embedding is 2D we need 1D
    sentence_emedding = tf.reshape(sentence_emedding, shape=[-1])

    features = {
        'id' : _bytes_feature(str(record_id)),
        'embedding' : _float_feature(sentence_emedding.numpy())
    }

    return tf.train.Example(features=tf.train.Features(feature=features))
    


# + id="Pr8htqN71lQL" colab_type="code" colab={}
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 += 1
            writer.write(example.SerializeToString())
    
    return record_id



# + id="rKVSCebm2K9j" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 0} outputId="78d5ba90-c735-486a-97a6-5118042fc029"
# Persist TF.Example features containing embeddings for training data in
# TFRecord format.
create_embeddings(pp_train_data, '/tmp/imdb/embeddings.tfr', 0)

# + [markdown] id="GUFtCqzC3Q75" colab_type="text"
# # Build The Graph

# + [markdown] id="QxfmtD7t3WUV" colab_type="text"
# Now that we have the sample embeddings, we will use them to build a similarity graph, i.e, nodes in this graph will correspond to samples and edges in this graph will correspond to similarity between pairs of nodes.

# + [markdown] id="q3mraJJp3Y--" colab_type="text"
# Neural Structured Learning provides a graph building library to build a graph based on sample embeddings. It uses cosine similarity as the similarity measure to compare embeddings and build edges between them. It also allows us to specify a similarity threshold, which can be used to discard dissimilar edges from the final graph. In this example, using 0.99 as the similarity threshold, we end up with a graph that has 445,327 bi-directional edges.

# + id="IlwwdNKf2NLl" colab_type="code" colab={}
nsl.tools.build_graph(embedding_files=['../tmp/imdb/embeddings.tfr'], 
                      output_graph_path='../tmp/imdb/graph_99.tsv', similarity_threshold=0.99)


# + [markdown] id="F1PmOUQo3_Tj" colab_type="text"
# Note: Graph quality and by extension, embedding quality, are very important for graph regularization. While we have used Swivel embeddings in this notebook, using BERT embeddings for instance, will likely capture review semantics more accurately. We encourage users to use embeddings of their choice and as appropriate to their needs.

# + [markdown] id="d5hDxYw14YYV" colab_type="text"
# # Create the neighbors for the graphs

# + [markdown] id="yVrWzFH84ia6" colab_type="text"
# We create sample features for our problem using the tf.train.Example format and persist them in the TFRecord format. Each sample will include the following three features:
#
#     id: The node ID of the sample.
#     words: An int64 list containing word IDs.
#     label: A singleton int64 identifying the target class of the review.
#

# + id="3Q2Ii0LW4bIZ" colab_type="code" colab={}
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))


# + id="8tFRg3J74vSW" colab_type="code" colab={}
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 += 1
            writer.write(example.SerializeToString())
    return record_id


# + id="B2L285Lj-y5T" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 0} outputId="5eb69b95-bd09-4f48-b07b-bd06b7a6d9c3"
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)


# + [markdown] id="1lmyHxfl-_ne" colab_type="text"
# # Augment training data with graph neighbors

# + [markdown] id="adTGFCAF_FYm" colab_type="text"
# Since we have the sample features and the synthesized graph, we can generate the augmented training data for Neural Structured Learning. 
#
# The NSL framework provides a library to combine the graph and the sample features to produce the final training data for graph regularization. 
#
# The resulting training data will include original sample features as well as features of their corresponding neighbors.

# + [markdown] id="9BxisxKk_Jmv" colab_type="text"
# In this tutorial, we consider undirected edges and use a maximum of 3 neighbors per sample to augment training data with graph neighbors.

# + id="r3zdgEWT36U9" colab_type="code" colab={}
nsl.tools.pack_nbrs(
    labeled_examples_path ='/tmp/imdb/train_data.tfr',
    unlabeled_examples_path = '',
    graph_path='/tmp/imdb/graph_99.tsv', 
    output_training_data_path='/tmp/imdb/nsl_train_data.tfr',
    add_undirected_edges=True,
    max_nbrs=3, )

# + [markdown] id="3ZodA930_2lY" colab_type="text"
# We are now ready to build a base model without graph regularization. In order to build this model, we can either use embeddings that were used in building the graph, or we can learn new embeddings jointly along with the classification task. For the purpose of this notebook, we will do the latter.

# + id="6JEaibkv_M4m" colab_type="code" colab={}
NBR_FEATURE_PREFIX = 'NL_nbr_'
NBR_WEIGHT_SUFFIX = '_weight'


# + [markdown] id="GTU8ufSCAJ1l" colab_type="text"
# # Hyperparameters
#
# We will use an instance of HParams to inclue various hyperparameters and constants used for training and evaluation. We briefly describe each of them below:
#
#

# + [markdown] id="Jva6JC7Ecesv" colab_type="text"
#
#     num_classes: There are 2 classes -- positive and negative.
#
#     max_seq_length: This is the maximum number of words considered from each movie review in this example.
#
#     vocab_size: This is the size of the vocabulary considered for this example.
#
#     distance_type: This is the distance metric used to regularize the sample with its neighbors.
#
#     graph_regularization_multiplier: This controls the relative weight of the graph regularization term in the overall loss function.
#
#     num_neighbors: The number of neighbors used for graph regularization. This value has to be less than or equal to the max_nbrs argument used above when invoking nsl.tools.pack_nbrs.
#
#     num_fc_units: The number of units in the fully connected layer of the neural network.
#
#     train_epochs: The number of training epochs.
#
#     batch_size: Batch size used for training and evaluation.
#
#     eval_steps: The number of batches to process before deeming evaluation is complete. If set to None, all instances in the test set are evaluated.

# + id="Qgo0HOb8AISl" colab_type="code" colab={}
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()


# + [markdown] id="G2b5sCRta4vU" colab_type="text"
# # Prepare the Data

# + [markdown] id="nRM6wec6a8b3" colab_type="text"
# The reviews—the arrays of integers—must be converted to tensors before being fed into the neural network. This conversion can be done a couple of ways:
#
# - Convert the arrays into vectors of 0s and 1s indicating word occurrence, similar to a one-hot encoding. For example, the sequence [3, 5] would become a 10000-dimensional vector that is all zeros except for indices 3 and 5, which are ones. Then, make this the first layer in our network—a Dense layer—that can handle floating point vector data. This approach is memory intensive, though, requiring a num_words * num_reviews size matrix.
#
# - Alternatively, we can pad the arrays so they all have the same length, then create an integer tensor of shape max_length * num_reviews. We can use an embedding layer capable of handling this shape as the first layer in our network.
#

# + [markdown] id="k29zWmVYbGZ3" colab_type="text"
# we will use the second approach.
#
# Since the movie reviews must be the same length, we will use the pad_sequence function defined below to standardize the lengths.

# + id="3x5zAquDANw7" colab_type="code" colab={}
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])



# + id="rLYwjj_NbydW" colab_type="code" colab={}
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.
    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)
    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


# + id="2A8I3a-xcAT-" colab_type="code" colab={}
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.
    """
    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


# + id="ByOnWiGZcGkt" colab_type="code" colab={}
train_dataset = make_dataset('/tmp/imdb/nsl_train_data.tfr', True)
test_dataset = make_dataset('/tmp/imdb/test_data.tfr')


# + [markdown] id="yMDcLBFgcPjj" colab_type="text"
# # Build the model

# + id="W26KgkRrcQnC" colab_type="code" colab={}
# 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)

# Feel free to use an architecture of your choice.


# + id="pNumE0V_cyWw" colab_type="code" colab={}
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)



# + id="TczUi_eOcsGT" colab_type="code" colab={}
model = make_bilstm_model()

# + id="xc_MYLW5c6yw" colab_type="code" colab={}
model.compile(
    optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])


# + [markdown] id="S5udSARedA23" colab_type="text"
# Create a validation set

# + id="VKypx0-7dBPW" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 35} outputId="ed0af3a7-4d4a-44a2-c95b-838297e263b8"
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)

# + id="SUW7s6xfdFZg" colab_type="code" colab={}
#  Train the model

# + id="Uk5BibjidE5H" colab_type="code" colab={}
history = model.fit(
    train_dataset,
    validation_data=validation_dataset,
    epochs=HPARAMS.train_epochs,
    verbose=1)


# + id="9hUMvCl1dhSm" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 54} outputId="5fc80805-a377-4d8d-db13-7bec0aa20c7e"
results = model.evaluate(test_dataset, steps=HPARAMS.eval_steps)
print(results)

# + id="MTkMgw8cd2Ir" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 35} outputId="c35f7b22-abbb-47b2-c427-39ef89f28c24"
history_dict = history.history
history_dict.keys()

# + id="Yxsz8TrOd3Z6" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 295} outputId="439160e4-0fd6-4c08-e877-cc64d21e5fa4"
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()

# + [markdown] id="8Hbkrkbrelgx" colab_type="text"
# # Graph regularization

# + [markdown] id="tkzTfGP6enm8" colab_type="text"
#
# We are now ready to try graph regularization using the base model that we built above. We will use the GraphRegularization wrapper class provided by the Neural Structured Learning framework to wrap the base (bi-LSTM) model to include graph regularization. The rest of the steps for training and evaluating the graph-regularized model are similar to that of the base model.

# + [markdown] id="RlMESyHtev15" colab_type="text"
# ## Create graph-regularized model
#
# To assess the incremental benefit of graph regularization, we will create a new base model instance. This is because model has already been trained for a few iterations, and reusing this trained model to create a graph-regularized model will not be a fair comparison for model.

# + id="c2tSkewrd4pd" colab_type="code" colab={}
# Build a new base LSTM model.
base_reg_model = make_bilstm_model()

# + id="q2lNekExeyUP" colab_type="code" colab={}
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)

# + id="QC7q8GEEfSje" colab_type="code" colab={}
graph_reg_model = nsl.keras.GraphRegularization(base_model=base_reg_model, graph_reg_config=graph_reg_config)

# + id="lzxfeS_zfiC9" colab_type="code" colab={}
graph_reg_model.compile(optimizer='Adam', loss='binary_crossentropy', metrics=['acc'])

# + id="YElE7s45fpt8" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 441} outputId="0faf9263-c13d-4c16-bd8e-7beaaf1b2651"
graph_reg_history = graph_reg_model.fit(
    train_dataset,
    validation_data=validation_dataset,
    epochs=HPARAMS.train_epochs,
    verbose=1)

# + id="-3ERaPWCfr2_" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 54} outputId="d921001e-a45c-4b2f-bbad-dad55372f76e"
graph_reg_results = graph_reg_model.evaluate(test_dataset, steps=HPARAMS.eval_steps)
print(graph_reg_results)

# + id="QOSc1QRufuYu" colab_type="code" colab={}
graph_reg_history_dict = graph_reg_history.history
graph_reg_history_dict.keys()
acc = graph_reg_history_dict['acc']
val_acc = graph_reg_history_dict['val_acc']
loss = graph_reg_history_dict['loss']
graph_loss = graph_reg_history_dict['graph_loss']
val_loss = graph_reg_history_dict['val_loss']
val_graph_loss = graph_reg_history_dict['val_graph_loss']

# + id="TgGoP5dBf2BL" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 295} outputId="6ee2d9c7-1754-4f3a-ed34-69db743cec37"
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')
# "-ms" is for solid magenta line with square markers.
plt.plot(epochs, val_graph_loss, '-ms', label='Validation graph loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend(loc='best')
plt.show()

# + id="7H0oIXxwfz51" colab_type="code" colab={"base_uri": "https://localhost:8080/", "height": 295} outputId="b0e42803-1b1d-4859-eb58-3b3e69b70864"
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()


# + [markdown] id="3euvan1nhR8L" colab_type="text"
# # About Semi Supervised Learning

# + [markdown] id="kuPleKPxheEF" colab_type="text"
# Semi-supervised learning and more specifically, graph regularization in the context of this tutorial, can be really powerful when the amount of training data is small. 
#
# The lack of training data is compensated by leveraging similarity among the training samples, which is not possible in traditional supervised learning.
#
# We define supervision ratio as the ratio of training samples to the total number of samples which includes training, validation, and test samples. 
#
# In this notebook, we have used a supervision ratio of 0.05 (i.e, 5% of the labeled data) for training both the base model as well as the graph-regularized model. We illustrate the impact of the supervision ratio on model accuracy in the cell below.

# + [markdown] id="ObKM4Qr_hc-V" colab_type="text"
# ![image.png](data:image/png;base64,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<KEY>

# + [markdown] id="sFEwQiEuhoYE" colab_type="text"
# It can be observed that as the superivision ratio decreases, model accuracy also decreases. 
#
# This is true for both the base model and for the graph-regularized model, regardless of the model architecture used. 
#
# However, notice that the graph-regularized model performs better than the base model for both the architectures. In particular, for the Bi-LSTM model, when the supervision ratio is 0.01, the accuracy of the graph-regularized model is ~20% higher than that of the base model. 
#
# This is primarily because of semi-supervised learning for the graph-regularized model, where structural similarity among training samples is used in addition to the training samples themselves.

StarcoderdataJupyterScriptsDedupFiltered