#@title Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
Explanation: Copyright 2019 Google LLC
End of explanation
"""


!pip install --quiet neural-structured-learning

"""
Explanation: 자연 그래프를 사용한, 문서 분류를 위한 그래프 정규화
<table class="tfo-notebook-buttons" align="left">
  <td><a target="_blank" href="https://www.tensorflow.org/neural_structured_learning/tutorials/graph_keras_mlp_cora"><img src="https://www.tensorflow.org/images/tf_logo_32px.png">TensorFlow.org에서 보기</a></td>
  <td><a target="_blank" href="https://colab.research.google.com/github/tensorflow/docs-l10n/blob/master/site/ko/neural_structured_learning/tutorials/graph_keras_mlp_cora.ipynb"><img src="https://www.tensorflow.org/images/colab_logo_32px.png">Run in Google Colab</a></td>
  <td><a target="_blank" href="https://github.com/tensorflow/docs-l10n/blob/master/site/ko/neural_structured_learning/tutorials/graph_keras_mlp_cora.ipynb"><img src="https://www.tensorflow.org/images/GitHub-Mark-32px.png">View source on GitHub</a></td>
</table>

개요
그래프 정규화는 Neural Graph Learning의 더 넓은 패러다임에서 사용되는 특정 기술입니다(Bui et al., 2018). 핵심 아이디어는 레이블이 지정된 데이터와 레이블이 없는 데이터를 모두 활용하여 그래프 정규화 목표를 갖고 신경망 모델을 훈련하는 것입니다.
이 튜토리얼에서는 그래프 정규화를 사용하여 자연(유기적) 그래프를 형성하는 문서를 분류하는 방법을 살펴봅니다.
Neural Structured Learning(NSL) 프레임워크를 사용하여 그래프 정규화 모델을 생성하는 일반적인 방법은 다음과 같습니다.

입력 그래프 및 샘플 특성에서 훈련 데이터를 생성합니다. 그래프의 노드는 샘플에 해당하고, 그래프의 간선은 샘플 쌍 간의 유사성에 해당합니다. 결과 훈련 데이터에는 원래 노드 특성 외에도 이웃 특성이 포함됩니다.
Keras 순차, 함수형 또는 서브 클래스 API를 사용하여 신경망을 기본 모델로 만듭니다.
NSL 프레임워크에서 제공하는 GraphRegularization 래퍼 클래스로 기본 모델을 래핑하여 새 그래프 Keras 모델을 만듭니다. 이 새로운 모델은 훈련 목표에서 그래프 정규화 손실을 정규화 항으로 포함합니다.
그래프 Keras 모델을 훈련하고 평가합니다.

설정
Neural Structured Learning 패키지를 설치합니다.
End of explanation
"""


import neural_structured_learning as nsl

import tensorflow as tf

# Resets notebook state
tf.keras.backend.clear_session()

print("Version: ", tf.__version__)
print("Eager mode: ", tf.executing_eagerly())
print(
    "GPU is",
    "available" if tf.config.list_physical_devices("GPU") else "NOT AVAILABLE")

"""
Explanation: 종속성 및 가져오기
End of explanation
"""


!wget --quiet -P /tmp https://linqs-data.soe.ucsc.edu/public/lbc/cora.tgz
!tar -C /tmp -xvzf /tmp/cora.tgz

"""
Explanation: Cora 데이터세트
Cora 데이터세트는 노드가 머신러닝 논문을 나타내고 간선이 논문 쌍 간의 인용을 나타내는 인용 그래프입니다. 관련된 작업은 각 논문을 7가지 범주 중 하나로 분류하는 것을 목표로 하는 문서 분류입니다. 즉, 7개의 클래스가 있는 다중 클래스 분류 문제입니다.
그래프
원래 그래프에는 방향이 있습니다. 그러나 이 예에서는 이 그래프의 방향 없는 버전을 고려합니다. 따라서 A 논문이 B 논문을 인용하면 B 논문도 A를 인용한 것으로 간주합니다. 이것이 반드시 사실은 아니지만, 이 예에서는 인용을 유사성에 대한 프록시로 간주하며, 일반적으로 교환 속성입니다.
특성
입력의 각 논문에는 효과적으로 두 가지 특성이 포함되어 있습니다.


Words: 종이에 있는 텍스트를 표현한 밀집 멀티-핫 단어 주머니(bag-of-words)입니다. Cora 데이터세트의 어휘에는 1433개의 고유한 단어가 포함되어 있습니다. 따라서 이 특성의 길이는 1433이고, 위치 'i'의 값은 주어진 논문에서 해당 어휘의 단어 'i'가 존재하는지 여부를 나타내는 0/1입니다.


Label: 논문의 클래스 ID(카테고리)를 나타내는 단일 정수입니다.


Cora 데이터세트 다운로드하기
End of explanation
"""


!wget https://raw.githubusercontent.com/tensorflow/neural-structured-learning/master/neural_structured_learning/examples/preprocess/cora/preprocess_cora_dataset.py

!python preprocess_cora_dataset.py \
--input_cora_content=/tmp/cora/cora.content \
--input_cora_graph=/tmp/cora/cora.cites \
--max_nbrs=5 \
--output_train_data=/tmp/cora/train_merged_examples.tfr \
--output_test_data=/tmp/cora/test_examples.tfr

"""
Explanation: Cora 데이터를 NSL 형식으로 변환하기
Cora 데이터세트를 전처리하고 Neural Structured Learning에 필요한 형식으로 변환하기 위해 NSL github 리포지토리에 포함된 'preprocess_cora_dataset.py' 스크립트를 실행합니다. 이 스크립트는 다음을 수행합니다.

원래 노드 특성과 그래프를 사용하여 이웃 특성을 생성합니다.
tf.train.Example 인스턴스를 포함하는 훈련 및 테스트 데이터 분할을 생성합니다.
결과 훈련 및 테스트 데이터를 TFRecord 형식으로 유지합니다.
End of explanation
"""


### Experiment dataset
TRAIN_DATA_PATH = '/tmp/cora/train_merged_examples.tfr'
TEST_DATA_PATH = '/tmp/cora/test_examples.tfr'

### Constants used to identify neighbor features in the input.
NBR_FEATURE_PREFIX = 'NL_nbr_'
NBR_WEIGHT_SUFFIX = '_weight'

"""
Explanation: 전역 변수
훈련 및 테스트 데이터에 대한 파일 경로는 위의 'preprocess_cora_dataset.py' 스크립트를 호출하는 데 사용된 명령 줄 플래그 값을 기반으로 합니다.
End of explanation
"""


class HParams(object):
  """Hyperparameters used for training."""
  def __init__(self):
    ### dataset parameters
    self.num_classes = 7
    self.max_seq_length = 1433
    ### neural graph learning parameters
    self.distance_type = nsl.configs.DistanceType.L2
    self.graph_regularization_multiplier = 0.1
    self.num_neighbors = 1
    ### model architecture
    self.num_fc_units = [50, 50]
    ### training parameters
    self.train_epochs = 100
    self.batch_size = 128
    self.dropout_rate = 0.5
    ### eval parameters
    self.eval_steps = None  # All instances in the test set are evaluated.

HPARAMS = HParams()

"""
Explanation: 하이퍼 매개변수
HParams의 인스턴스를 사용하여 훈련 및 평가에 사용되는 다양한 하이퍼 매개변수 및 상수를 포함합니다. 아래에서 각각에 대해 간략하게 설명합니다.


num_classes: 총 7개의 클래스가 있습니다.


max_seq_length: 어휘의 크기이며, 입력의 모든 인스턴스는 밀집 멀티-핫, 단어 주머니(bag-of-words)의 표현을 갖습니다. 즉, 단어의 값이 1이면 해당 단어가 입력에 있음을 나타내고, 값이 0이면 그렇지 않음을 나타냅니다.


distance_type: 샘플을 이웃으로 정규화하는 데 사용되는 거리 메트릭입니다.


graph_regularization_multiplier: 전체 손실 함수에서 그래프 정규화 항의 상대적 가중치를 제어합니다.


num_neighbors: 그래프 정규화에 사용되는 이웃의 수입니다. 이 값은 preprocess_cora_dataset.py를 실행할 때 위에 사용된 max_nbrs 명령 줄 인수보다 작거나 같아야 합니다.


num_fc_units: 신경망에서 완전 연결된 레이어의 수입니다.


train_epochs: 훈련 epoch의 수입니다.


batch_size: 훈련 및 평가에 사용되는 배치 크기입니다.


dropout_rate: 각 완전 연결 레이어의 드롭아웃 비율을 제어합니다.


eval_steps: 평가가 완료된 것으로 간주하기 전에 처리할 배치의 수입니다. None으로 설정하면, 테스트세트의 모든 인스턴스가 평가됩니다.
End of explanation
"""


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 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 label.
    """
    # The 'words' feature is a multi-hot, bag-of-words representation of the
    # original raw text. A default value is required for examples that don't
    # have the feature.
    feature_spec = {
        'words':
            tf.io.FixedLenFeature([HPARAMS.max_seq_length],
                                  tf.int64,
                                  default_value=tf.constant(
                                      0,
                                      dtype=tf.int64,
                                      shape=[HPARAMS.max_seq_length])),
        '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.FixedLenFeature(
            [HPARAMS.max_seq_length],
            tf.int64,
            default_value=tf.constant(
                0, dtype=tf.int64, shape=[HPARAMS.max_seq_length]))

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

    label = features.pop('label')
    return features, label

  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(TRAIN_DATA_PATH, training=True)
test_dataset = make_dataset(TEST_DATA_PATH)

"""
Explanation: 훈련 및 테스트 데이터 로드하기
이 노트북의 앞부분에서 설명한 것처럼 입력 훈련 및 테스트 데이터는 'preprocess_cora_dataset.py'에 의해 생성되었습니다. 데이터를 두 개의 tf.data.Dataset 객체로 로드합니다. 하나는 훈련용이고 다른 하나는 테스트용입니다.
모델의 입력 레이어에서 각 샘플의 'words' 및 'label' 특성뿐만 아니라 hparams.num_neighbors 값을 기반으로 해당 이웃 특성도 추출합니다. 이웃이 hparams.num_neighbors보다 적은 인스턴스에는 존재하지 않는 이웃 특성에 대해 더미 값이 할당됩니다.
End of explanation
"""


for feature_batch, label_batch in train_dataset.take(1):
  print('Feature list:', list(feature_batch.keys()))
  print('Batch of inputs:', feature_batch['words'])
  nbr_feature_key = '{}{}_{}'.format(NBR_FEATURE_PREFIX, 0, 'words')
  nbr_weight_key = '{}{}{}'.format(NBR_FEATURE_PREFIX, 0, NBR_WEIGHT_SUFFIX)
  print('Batch of neighbor inputs:', feature_batch[nbr_feature_key])
  print('Batch of neighbor weights:',
        tf.reshape(feature_batch[nbr_weight_key], [-1]))
  print('Batch of labels:', label_batch)

"""
Explanation: 내용을 보기 위해 훈련 데이터세트를 살펴보겠습니다.
End of explanation
"""


for feature_batch, label_batch in test_dataset.take(1):
  print('Feature list:', list(feature_batch.keys()))
  print('Batch of inputs:', feature_batch['words'])
  print('Batch of labels:', label_batch)

"""
Explanation: 내용을 보기 위해 테스트 데이터세트를 살펴보겠습니다.
End of explanation
"""


def make_mlp_sequential_model(hparams):
  """Creates a sequential multi-layer perceptron model."""
  model = tf.keras.Sequential()
  model.add(
      tf.keras.layers.InputLayer(
          input_shape=(hparams.max_seq_length,), name='words'))
  # Input is already one-hot encoded in the integer format. We cast it to
  # floating point format here.
  model.add(
      tf.keras.layers.Lambda(lambda x: tf.keras.backend.cast(x, tf.float32)))
  for num_units in hparams.num_fc_units:
    model.add(tf.keras.layers.Dense(num_units, activation='relu'))
    # For sequential models, by default, Keras ensures that the 'dropout' layer
    # is invoked only during training.
    model.add(tf.keras.layers.Dropout(hparams.dropout_rate))
  model.add(tf.keras.layers.Dense(hparams.num_classes, activation='softmax'))
  return model

"""
Explanation: 모델 정의
그래프 정규화를 사용하는 방법을 보여주기 위해 먼저 이 문제에 대한 기본 모델을 빌드합니다. 2개의 숨겨진 레이어와 그 사이에 드롭아웃이 있는 간단한 피드 포워드 신경망을 사용합니다. tf.Keras 프레임워크에서 지원하는 모든 모델 유형(순차, 함수형 및 서브 클래스)을 사용하여 기본 모델을 생성하는 방법을 설명합니다.
순차 기본 모델
End of explanation
"""


def make_mlp_functional_model(hparams):
  """Creates a functional API-based multi-layer perceptron model."""
  inputs = tf.keras.Input(
      shape=(hparams.max_seq_length,), dtype='int64', name='words')

  # Input is already one-hot encoded in the integer format. We cast it to
  # floating point format here.
  cur_layer = tf.keras.layers.Lambda(
      lambda x: tf.keras.backend.cast(x, tf.float32))(
          inputs)

  for num_units in hparams.num_fc_units:
    cur_layer = tf.keras.layers.Dense(num_units, activation='relu')(cur_layer)
    # For functional models, by default, Keras ensures that the 'dropout' layer
    # is invoked only during training.
    cur_layer = tf.keras.layers.Dropout(hparams.dropout_rate)(cur_layer)

  outputs = tf.keras.layers.Dense(
      hparams.num_classes, activation='softmax')(
          cur_layer)

  model = tf.keras.Model(inputs, outputs=outputs)
  return model

"""
Explanation: 함수형 기본 모델
End of explanation
"""


def make_mlp_subclass_model(hparams):
  """Creates a multi-layer perceptron subclass model in Keras."""

  class MLP(tf.keras.Model):
    """Subclass model defining a multi-layer perceptron."""

    def __init__(self):
      super(MLP, self).__init__()
      # Input is already one-hot encoded in the integer format. We create a
      # layer to cast it to floating point format here.
      self.cast_to_float_layer = tf.keras.layers.Lambda(
          lambda x: tf.keras.backend.cast(x, tf.float32))
      self.dense_layers = [
          tf.keras.layers.Dense(num_units, activation='relu')
          for num_units in hparams.num_fc_units
      ]
      self.dropout_layer = tf.keras.layers.Dropout(hparams.dropout_rate)
      self.output_layer = tf.keras.layers.Dense(
          hparams.num_classes, activation='softmax')

    def call(self, inputs, training=False):
      cur_layer = self.cast_to_float_layer(inputs['words'])
      for dense_layer in self.dense_layers:
        cur_layer = dense_layer(cur_layer)
        cur_layer = self.dropout_layer(cur_layer, training=training)

      outputs = self.output_layer(cur_layer)

      return outputs

  return MLP()

"""
Explanation: 서브 클래스 기본 모델
End of explanation
"""


# Create a base MLP model using the functional API.
# Alternatively, you can also create a sequential or subclass base model using
# the make_mlp_sequential_model() or make_mlp_subclass_model() functions
# respectively, defined above. Note that if a subclass model is used, its
# summary cannot be generated until it is built.
base_model_tag, base_model = 'FUNCTIONAL', make_mlp_functional_model(HPARAMS)
base_model.summary()

"""
Explanation: 기본 모델 생성하기
End of explanation
"""


# Compile and train the base MLP model
base_model.compile(
    optimizer='adam',
    loss='sparse_categorical_crossentropy',
    metrics=['accuracy'])
base_model.fit(train_dataset, epochs=HPARAMS.train_epochs, verbose=1)

"""
Explanation: 기본 MLP 모델 훈련하기
End of explanation
"""


# Helper function to print evaluation metrics.
def print_metrics(model_desc, eval_metrics):
  """Prints evaluation metrics.

  Args:
    model_desc: A description of the model.
    eval_metrics: A dictionary mapping metric names to corresponding values. It
      must contain the loss and accuracy metrics.
  """
  print('\n')
  print('Eval accuracy for ', model_desc, ': ', eval_metrics['accuracy'])
  print('Eval loss for ', model_desc, ': ', eval_metrics['loss'])
  if 'graph_loss' in eval_metrics:
    print('Eval graph loss for ', model_desc, ': ', eval_metrics['graph_loss'])

eval_results = dict(
    zip(base_model.metrics_names,
        base_model.evaluate(test_dataset, steps=HPARAMS.eval_steps)))
print_metrics('Base MLP model', eval_results)

"""
Explanation: 기본 MLP 모델 평가하기
End of explanation
"""


# Build a new base MLP model.
base_reg_model_tag, base_reg_model = 'FUNCTIONAL', make_mlp_functional_model(
    HPARAMS)

# Wrap the base MLP 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='sparse_categorical_crossentropy',
    metrics=['accuracy'])
graph_reg_model.fit(train_dataset, epochs=HPARAMS.train_epochs, verbose=1)

"""
Explanation: 그래프 정규화로 MLP 모델 훈련하기
그래프 정규화를 기존 tf.Keras.Model의 손실 항에 통합하려면 몇 줄의 코드만 있으면 됩니다. 기본 모델은 래핑되어 새로운 tf.Keras 서브 클래스 모델을 생성하며, 손실에는 그래프 정규화가 포함됩니다.
그래프 정규화의 점진적 이점을 평가하기 위해 새 기본 모델 인스턴스를 생성합니다. 이는 base_model이 이미 몇 번의 반복 동안 훈련되었으며, 이 훈련된 모델을 재사용하여 그래프 정규화 모델을 만드는 것은 base_model에 대한 공정한 비교가 되지 않기 때문입니다.
End of explanation
"""


eval_results = dict(
    zip(graph_reg_model.metrics_names,
        graph_reg_model.evaluate(test_dataset, steps=HPARAMS.eval_steps)))
print_metrics('MLP + graph regularization', eval_results)

"""
Explanation: 그래프 정규화로 MLP 모델 평가하기
End of explanation
"""


