このページは Cloud Translation API によって翻訳されました。
Switch to English

合成グラフを使用した感情分類のグラフ正則化

TensorFlow.orgで見る Google Colabで実行 GitHubでソースを表示する

概観

このノートブックは、レビューのテキストを使用して、映画レビューをポジティブまたはネガティブに分類します。これは、 バイナリ分類の例であり、重要で広く適用可能な種類の機械学習問題です。

与えられた入力からグラフを作成することにより、このノートブックでのグラフの正則化の使用法を示します。入力に明示的なグラフが含まれていない場合に、ニューラル構造化学習(NSL)フレームワークを使用してグラフ正則化モデルを構築するための一般的なレシピは、次のとおりです。

  1. 入力内の各テキストサンプルの埋め込みを作成します。これは、 word2vecSwivelBERTなどの事前トレーニング済みモデルを使用して実行できます。
  2. 「L2」距離、「コサイン」距離などの類似性メトリックを使用して、これらの埋め込みに基づいてグラフを作成します。グラフのノードはサンプルに対応し、グラフのエッジはサンプルのペア間の類似性に対応します。
  3. 上記の合成グラフとサンプル特徴からトレーニングデータを生成します。結果のトレーニングデータには、元のノードフィーチャに加えて隣接フィーチャが含まれます。
  4. Kerasの順次、機能、またはサブクラスAPIを使用して、ニューラルネットワークを基本モデルとして作成します。
  5. NSLフレームワークによって提供されるGraphRegularizationラッパークラスで基本モデルをラップして、新しいグラフKerasモデルを作成します。この新しいモデルには、トレーニング目標の正則化項としてグラフ正則化損失が含まれます。
  6. グラフKerasモデルをトレーニングして評価します。

必要条件

  1. ニューラル構造化学習パッケージをインストールします。
  2. tensorflow-hubをインストールします。
pip install --quiet neural-structured-learning
pip install --quiet tensorflow-hub
WARNING: You are using pip version 20.2.1; however, version 20.2.2 is available.
You should consider upgrading via the '/tmpfs/src/tf_docs_env/bin/python -m pip install --upgrade pip' command.
WARNING: You are using pip version 20.2.1; however, version 20.2.2 is available.
You should consider upgrading via the '/tmpfs/src/tf_docs_env/bin/python -m pip install --upgrade pip' command.

依存関係とインポート

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の映画レビューのテキストが含まれています。これらは、トレーニング用の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]

映画レビューは長さが異なる場合があります。以下のコードは、1番目と2番目のレビューの単語数を示しています。ニューラルネットワークへの入力は同じ長さでなければならないため、後でこれを解決する必要があります。

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形式で埋め込みを作成します。結果の埋め込みを、各サンプルのIDを表す追加機能とともにTFRecord形式で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

グラフを作成する

これでサンプルの埋め込みができたので、それらを使用して類似性グラフを作成します。つまり、このグラフのノードはサンプルに対応し、このグラフのエッジはノードのペア間の類似性に対応します。

ニューラル構造化学習は、サンプルの埋め込みに基づいてグラフを構築するためのグラフ構築ライブラリを提供します。埋め込みを比較し、それらの間のエッジを構築するための類似性の尺度として、 コサイン類似性を使用します。また、類似性のしきい値を指定することもできます。これを使用して、最終的なグラフから異なるエッジを破棄できます。この例では、類似性のしきい値として0.99を使用し、ランダムシードとして12345を使用して、429,415の双方向エッジを持つグラフになります。ここでは、 局所性依存ハッシュ (LSH)のグラフビルダーのサポートを使用して、グラフの構築を高速化しています。グラフビルダーのLSHサポートの使用の詳細については、 build_graph_from_config APIドキュメントをご覧ください。

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ファイルの2つの有向エッジで表されるため、ファイルには合計429,415 * 2 = 858,830行が含まれます。

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

サンプル機能

私たちは、使用して我々の問題のためのサンプルの特徴を作成tf.train.Example形式をして、それらを持続TFRecordフォーマット。各サンプルには、次の3つの機能が含まれます。

  1. id :サンプルのノードID。
  2. words :単語IDを含む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 :これは、この例の各映画レビューで考慮される単語の最大数です。

  • vocab_size :これは、この例で考慮される語彙のサイズです。

  • distance_type :これは、サンプルをその近傍と正規化するために使用される距離メトリックです。

  • graph_regularization_multiplier :これは、全体的な損失関数におけるグラフ正則化項の相対的な重みを制御します。

  • num_neighbors :グラフの正則化に使用される近傍の数。この値は、上記のnsl.tools.pack_nbrsを呼び出すときに使用した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()

データを準備する

レビュー(整数の配列)は、ニューラルネットワークに入力する前にテンソルに変換する必要があります。この変換は、いくつかの方法で実行できます。

  • 配列を01のベクトルに変換し、ワンホットエンコーディングと同様に、単語の出現を示します。たとえば、シーケンス[3, 5]は、 10000であるインデックス35を除いてすべてゼロの10000次元ベクトルになります。次に、これを、浮動小数点ベクトルデータを処理できるネットワークの最初のレイヤー( Denseレイヤー)にします。ただし、このアプローチはメモリをnum_words * num_reviewsが、 num_words * num_reviewsサイズの行列が必要です。

  • あるいは、すべて同じ長さになるように配列をパディングしてから、形状max_length * num_reviews整数テンソルを作成することもできます。この形状を処理できる埋め込みレイヤーをネットワークの最初のレイヤーとして使用できます。

このチュートリアルでは、2番目の方法を使用します。

映画のレビューは同じ長さでなければならないため、以下で定義されている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')

モデルを構築する

ニューラルネットワークは、レイヤーを積み重ねることによって作成されます。これには、2つの主要なアーキテクチャ上の決定が必要です。

  • モデルで使用するレイヤーの数は?
  • 各レイヤーに使用する隠しユニットの数は?

この例では、入力データは単語インデックスの配列で構成されています。予測するラベルは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. この固定長の出力ベクトルは、64個の非表示ユニットを持つ完全に接続された( Dense )レイヤーを介してパイプ処理されます。
  5. 最後のレイヤーは単一の出力ノードに密に接続されています。 sigmoid活動化関数を使用すると、この値は0〜1の浮動小数点数であり、確率または信頼水準を表します。

隠しユニット

上記のモデルには、入力と出力の間に2つの中間層または「非表示」層があり、 Embedding層は除外されています。出力(ユニット、ノード、またはニューロン)の数は、レイヤーの表現空間の次元です。言い換えれば、内部表現を学習するときにネットワークが許容される自由の量です。

モデルに非表示の単位(高次元の表現空間)やレイヤーが多い場合、ネットワークはより複雑な表現を学習できます。ただし、ネットワークの計算コストが高くなり、不要なパターン(トレーニングデータのパフォーマンスは向上するがテストデータのパフォーマンスは向上しないパターン)の学習につながる可能性があります。これはオーバーフィッティングと呼ばれます

損失関数とオプティマイザ

モデルには、損失関数とトレーニング用のオプティマイザーが必要です。これはバイナリ分類問題であり、モデルは確率(シグモイドアクティベーションを持つ単一ユニットレイヤー)を出力するため、 binary_crossentropy損失関数を使用します。

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

検証セットを作成する

トレーニングするときは、これまでに見たことのないデータについてモデルの精度をチェックしたいと思います。元のトレーニングデータの一部を分離して検証セットを作成します。 (今、テストセットを使用しないのはなぜですか?私たちの目標は、トレーニングデータのみを使用してモデルを開発および調整し、テストデータを1回だけ使用して精度を評価することです)。

このチュートリアルでは、最初のトレーニングサンプルの約10%(25000の10%)をトレーニング用のラベル付きデータとして取得し、残りを検証データとして取得します。最初のトレーニング/テスト分割は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

モデルを評価する

それでは、モデルのパフォーマンスを見てみましょう。 2つの値が返されます。損失(エラーを表す数値。値が小さいほど優れています)、および精度。

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

4つのエントリがあります。トレーニングおよび検証中に監視された各メトリックに1つです。これらを使用して、比較のためにトレーニングと検証の損失、およびトレーニングと検証の精度をプロットできます。

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ラッパークラスを使用して、ベース(bi-LSTM)モデルをラップし、グラフの正規化を含めます。グラフの正規化されたモデルをトレーニングおよび評価するための残りの手順は、基本モデルの手順と同様です。

グラフ正則化モデルを作成する

グラフの正則化の段階的な利点を評価するために、新しいベースモデルインスタンスを作成します。これは、 modelがすでにいくつかの反復でトレーニングされており、このトレーニングされたモデルを再利用してグラフ正則化モデルを作成することが、 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'])

辞書には合計5つのエントリがあります。トレーニングロス、トレーニング精度、トレーニンググラフロス、検証ロス、検証精度です。比較のためにそれらをすべて一緒にプロットできます。グラフ損失はトレーニング中にのみ計算されることに注意してください。

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%高くなります。これは主に、トレーニングサンプル間の構造的類似性がトレーニングサンプル自体に加えて使用されるグラフ正則化モデルの半教師あり学習によるものです。

結論

入力に明示的なグラフが含まれていない場合でも、Neural Structured Learning(NSL)フレームワークを使用してグラフの正則化を使用する方法を示しました。レビューの埋め込みに基づいて類似性グラフを合成したIMDB映画レビューの感情分類のタスクを検討しました。ハイパーパラメータや監視の量を変えたり、さまざまなモデルアーキテクチャを使用したりして、さらに実験することをお勧めします。