word2vec

TensorFlow.org で表示 Google Colab で実行 GitHubでソースを表示 ノートブックをダウンロード

word2vec は単一のアルゴリズムではなく、大規模なデータセットから単語の埋め込みを学習するために使用できるモデルアーキテクチャと最適化のファミリです。word2vec により学習された埋め込みは、さまざまなダウンストリームの自然言語処理タスクで成功することが証明されています。

注意: このチュートリアルは、ベクトル空間での単語表現の効率的な推定単語とフレーズの分散表現とその構成に基づいていますが、論文の正確な実装ではなく、重要なアイデアを説明することを目的としています。

これらの論文では、単語の表現を学習するための 2 つの方法が提案されています。

  • 連続バッグオブワードモデルでは、周囲のコンテキストワードに基づいて中間の単語を予測します。コンテキストは、与えられた (中間) 単語の前後のいくつかの単語で構成されます。このアーキテクチャでは、コンテキスト内の単語の順序が重要ではないため、バッグオブワードモデルと呼ばれます。
  • 連続スキップグラムモデルは、同じ文の与えられた単語の前後の特定の範囲内の単語を予測します。この例を以下に示します。

このチュートリアルでは、スキップグラムアプローチを使用します。最初に、説明のために 1 つの文を使用して、スキップグラムとその他の概念について説明します。次に、小さなデータセットで独自の word2vec モデルをトレーニングします。このチュートリアルには、トレーニング済みの埋め込みをエクスポートして TensorFlow Embedding Projector で可視化するためのコードも含まれています。

スキップグラムとネガティブサンプリング

バッグオブワードモデルは、与えられたコンテキスト (前後の単語) から単語を予測しますが、スキップグラムモデルは、与えられた単語自体から単語のコンテキスト (前後の単語) を予測します。モデルは、トークンをスキップできる n-gram であるスキップグラムでトレーニングされます (例については、下の図を参照してください)。単語のコンテキストは、context_wordtarget_word の前後のコンテキストに現れる (target_word, context_word) の一連のスキップグラムペアによって表すことができます。

次の 8 つの単語の文を考えてみましょう。

The wide road shimmered in the hot sun.

この文の 8 つの単語のそれぞれのコンテキストワードは、ウィンドウサイズによって定義されます。ウィンドウサイズは、context word と見なすことができる target_word の前後の単語の範囲を指定します。以下は、さまざまなウィンドウサイズに基づくターゲットワードのスキップグラムの表です。

注意: このチュートリアルでは、ウィンドウサイズ n は、前後に n 個の単語があり、合計ウィンドウ 範囲が 2*n+1 個の単語であるということを意味します。

word2vec_skipgrams

スキップグラムモデルのトレーニングの目的は、与えられたターゲットワードからコンテキストワードを予測する確率を最大化することです。一連の単語 w1、w2、... wT の場合、目的は平均対数確率として記述できます。

word2vec_skipgram_objective

ここで、c はトレーニングコンテキストのサイズです。基本的なスキップグラムの定式化では、ソフトマックス関数を使用してこの確率を定義します。

word2vec_full_softmax

ここで、vv' は単語のターゲットとコンテキストのベクトル表現であり、W 語彙サイズです。

この定式化の分母を計算するには、語彙全体に対して完全なソフトマックスを実行する必要があります。これは、多くの場合、大きな項 (105-107) です。

ノイズコントラスト推定 (NCE) 損失関数は、完全なソフトマックスの効率的な近似値です。単語の分布をモデル化するのではなく、単語の埋め込みを学習することを目的として、NCE 損失を単純化してネガティブサンプリングを使用することができます。

ターゲットワードの単純化されたネガティブサンプリングの目的は、コンテキストワードをノイズ分布 Pn(w) ワードから抽出された num_ns のネガティブサンプルから区別することです。より正確には、語彙全体の完全なソフトマックスの効率的な近似は、スキップグラムペアの場合、コンテキストワードと num_ns ネガティブサンプル間の分類問題としてターゲットワードの損失を提示します。

ネガティブサンプルは、context_wordtarget_wordwindow_size の前後に現れないように、(target_word, context_word) ペアとして定義されます。この例の文の場合、以下はいくつかの潜在的なネガティブサンプルです (window_size2 の場合)。

(hot, shimmered)
(wide, hot)
(wide, sun)

次のセクションでは、1 つの文に対してスキップグラムとネガティブサンプルを生成します。また、サブサンプリング手法についても学習し、チュートリアルの後半でポジティブトレーニングとネガティブトレーニングサンプルの分類モデルをトレーニングします。

セットアップ

import io
import re
import string
import tqdm

import numpy as np

import tensorflow as tf
from tensorflow.keras import layers
2022-12-14 23:26:57.448971: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory
2022-12-14 23:26:57.449067: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory
2022-12-14 23:26:57.449077: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.
# Load the TensorBoard notebook extension
%load_ext tensorboard
SEED = 42
AUTOTUNE = tf.data.AUTOTUNE

例文をベクトル化する

次の文を考えてみましょう。

The wide road shimmered in the hot sun.

文をトークン化します。

sentence = "The wide road shimmered in the hot sun"
tokens = list(sentence.lower().split())
print(len(tokens))
8

トークンから整数インデックスへのマッピングを保存する語彙を作成します。

vocab, index = {}, 1  # start indexing from 1
vocab['<pad>'] = 0  # add a padding token
for token in tokens:
  if token not in vocab:
    vocab[token] = index
    index += 1
vocab_size = len(vocab)
print(vocab)
{'<pad>': 0, 'the': 1, 'wide': 2, 'road': 3, 'shimmered': 4, 'in': 5, 'hot': 6, 'sun': 7}

整数インデックスからトークンへのマッピングを保存する逆語彙を作成します。

inverse_vocab = {index: token for token, index in vocab.items()}
print(inverse_vocab)
{0: '<pad>', 1: 'the', 2: 'wide', 3: 'road', 4: 'shimmered', 5: 'in', 6: 'hot', 7: 'sun'}

文をベクトル化します。

example_sequence = [vocab[word] for word in tokens]
print(example_sequence)
[1, 2, 3, 4, 5, 1, 6, 7]

1 つの文からスキップグラムを生成する

tf.keras.preprocessing.sequence モジュールは、word2vec のデータ準備を簡素化する便利な関数を提供します。 tf.keras.preprocessing.sequence.skipgrams を使用して、範囲 [0, vocab_size) のトークンから指定された window_sizeexample_sequence からスキップグラムペアを生成します。

注意: negative_samples は、ここでは 0 に設定されています。これは、この関数によって生成されたネガティブサンプルのバッチ処理にコードが少し必要だからです。次のセクションでは、別の関数を使用してネガティブサンプリングを実行します。

window_size = 2
positive_skip_grams, _ = tf.keras.preprocessing.sequence.skipgrams(
      example_sequence,
      vocabulary_size=vocab_size,
      window_size=window_size,
      negative_samples=0)
print(len(positive_skip_grams))
26

いくつかのポジティブのスキップグラムを出力します。

for target, context in positive_skip_grams[:5]:
  print(f"({target}, {context}): ({inverse_vocab[target]}, {inverse_vocab[context]})")
(3, 5): (road, in)
(3, 2): (road, wide)
(2, 1): (wide, the)
(4, 5): (shimmered, in)
(4, 2): (shimmered, wide)

1 つのスキップグラムのネガティブサンプリング

skipgrams 関数は、指定されたウィンドウスパンをスライドすることにより、すべてのポジティブのスキップグラムのペアを返します。トレーニング用のネガティブサンプルとして機能する追加のスキップグラムのペアを生成するには、語彙からランダムな単語をサンプリングする必要があります。tf.random.log_uniform_candidate_sampler 関数を使用して、ウィンドウ内の特定のターゲットワードに対して num_ns のネガティブサンプルをサンプリングします。1 つのスキップグラムのターゲットワードで関数を呼び出し、コンテキストワードを真のクラスとして渡して、サンプリングから除外できます。

重要点: [5, 20] 範囲の num_ns(ポジティブなコンテキストワードあたりのネガティブサンプルの数)は、小規模なデータセットで機能することが示されています[2, 5] 範囲の num_ns は、より大きなデータセットの場合に十分です。

# Get target and context words for one positive skip-gram.
target_word, context_word = positive_skip_grams[0]

# Set the number of negative samples per positive context.
num_ns = 4

context_class = tf.reshape(tf.constant(context_word, dtype="int64"), (1, 1))
negative_sampling_candidates, _, _ = tf.random.log_uniform_candidate_sampler(
    true_classes=context_class,  # class that should be sampled as 'positive'
    num_true=1,  # each positive skip-gram has 1 positive context class
    num_sampled=num_ns,  # number of negative context words to sample
    unique=True,  # all the negative samples should be unique
    range_max=vocab_size,  # pick index of the samples from [0, vocab_size]
    seed=SEED,  # seed for reproducibility
    name="negative_sampling"  # name of this operation
)
print(negative_sampling_candidates)
print([inverse_vocab[index.numpy()] for index in negative_sampling_candidates])
tf.Tensor([2 1 4 3], shape=(4,), dtype=int64)
['wide', 'the', 'shimmered', 'road']

1 つのトレーニングサンプルを作成する

与えられたポジティブの (target_word, context_word) スキップグラムに対して、target_word のウィンドウ サイズの前後に現れない num_ns のネガティブサンプルのコンテキストワードもあります。1 のポジティブの context_wordnum_ns のネガティブのコンテキストワードを 1 つのテンソルにバッチ処理します。これにより、ターゲットワードごとにポジティブのスキップグラム (1 とラベル付ける) とネガティブのサンプル (0 とラベル付ける) のセットが生成されます。

# Add a dimension so you can use concatenation (in the next step).
negative_sampling_candidates = tf.expand_dims(negative_sampling_candidates, 1)

# Concatenate a positive context word with negative sampled words.
context = tf.concat([context_class, negative_sampling_candidates], 0)

# Label the first context word as `1` (positive) followed by `num_ns` `0`s (negative).
label = tf.constant([1] + [0]*num_ns, dtype="int64")

# Reshape the target to shape `(1,)` and context and label to `(num_ns+1,)`.
target = tf.squeeze(target_word)
context = tf.squeeze(context)
label = tf.squeeze(label)

上記のスキップグラムの例から、ターゲットワードのコンテキストと対応するラベルを確認してください。

print(f"target_index    : {target}")
print(f"target_word     : {inverse_vocab[target_word]}")
print(f"context_indices : {context}")
print(f"context_words   : {[inverse_vocab[c.numpy()] for c in context]}")
print(f"label           : {label}")
target_index    : 3
target_word     : road
context_indices : [5 2 1 4 3]
context_words   : ['in', 'wide', 'the', 'shimmered', 'road']
label           : [1 0 0 0 0]

(target, context, label) テンソルのタプルは、スキップグラム ネガティブサンプリング word2vec モデルをトレーニングするための 1 つのトレーニングサンプルを構成します。ターゲットの形状は (1,) であるのに対し、コンテキストとラベルの形状は (1+num_ns,) であることに注意してください。

print("target  :", target)
print("context :", context)
print("label   :", label)
target  : tf.Tensor(3, shape=(), dtype=int32)
context : tf.Tensor([5 2 1 4 3], shape=(5,), dtype=int64)
label   : tf.Tensor([1 0 0 0 0], shape=(5,), dtype=int64)

まとめ

この図は、文からトレーニングサンプルを生成する手順をまとめたものです。

word2vec_negative_sampling

temperaturecode という単語は、入力文の一部ではないことに注意してください。これらは、上の図で使用されている他の特定のインデックスと同様の語彙に属しています。

すべてのステップを 1 つの関数にコンパイルする

スキップグラム サンプリングテーブル

大規模なデータセットでは語彙が多くなり、ストップワードなどのより頻繁に使用される単語の数も多くなります。一般的に出現する単語 (theison など) のサンプリングから得られたトレーニングサンプルは、モデルの学習に役立つ情報をあまり提供しません。Mikolov et al. は、埋め込みの品質を改善するための有用な方法として、頻繁に使用される単語のサブサンプリングを提案しています。

tf.keras.preprocessing.sequence.skipgrams 関数は、任意のトークンをサンプリングする確率をエンコードするためのサンプリングテーブル引数を受け入れます。tf.keras.preprocessing.sequence.make_sampling_table を使用して、単語頻度ランクに基づく確率的サンプリングテーブルを生成し、それを skipgrams 関数に渡します。vocab_size が 10 の場合のサンプリング確率を調べます。

sampling_table = tf.keras.preprocessing.sequence.make_sampling_table(size=10)
print(sampling_table)
[0.00315225 0.00315225 0.00547597 0.00741556 0.00912817 0.01068435
 0.01212381 0.01347162 0.01474487 0.0159558 ]

sampling_table[i] は、データセットで i 番目に最も一般的な単語をサンプリングする確率を示します。この関数は、サンプリングの単語頻度の Zipf 分布を想定しています。

重要点: tf.random.log_uniform_candidate_sampler は、語彙頻度が対数一様 (Zipf の) 分布に従うことを既に想定しています。これらの分布加重サンプリングを使用すると、ネガティブのサンプリング目標をトレーニングするための単純な損失関数を使用して、Noise Contrastive Estimation (NCE) 損失を概算するのにも役立ちます。

トレーニングデータを生成する

上記のすべての手順を、任意のテキストデータセットから取得したベクトル化された文のリストに対して呼び出せる関数にコンパイルします。スキップグラムの単語ペアをサンプリングする前に、サンプリングテーブルが作成されることに注意してください。この関数は後のセクションで使用します。

# Generates skip-gram pairs with negative sampling for a list of sequences
# (int-encoded sentences) based on window size, number of negative samples
# and vocabulary size.
def generate_training_data(sequences, window_size, num_ns, vocab_size, seed):
  # Elements of each training example are appended to these lists.
  targets, contexts, labels = [], [], []

  # Build the sampling table for `vocab_size` tokens.
  sampling_table = tf.keras.preprocessing.sequence.make_sampling_table(vocab_size)

  # Iterate over all sequences (sentences) in the dataset.
  for sequence in tqdm.tqdm(sequences):

    # Generate positive skip-gram pairs for a sequence (sentence).
    positive_skip_grams, _ = tf.keras.preprocessing.sequence.skipgrams(
          sequence,
          vocabulary_size=vocab_size,
          sampling_table=sampling_table,
          window_size=window_size,
          negative_samples=0)

    # Iterate over each positive skip-gram pair to produce training examples
    # with a positive context word and negative samples.
    for target_word, context_word in positive_skip_grams:
      context_class = tf.expand_dims(
          tf.constant([context_word], dtype="int64"), 1)
      negative_sampling_candidates, _, _ = tf.random.log_uniform_candidate_sampler(
          true_classes=context_class,
          num_true=1,
          num_sampled=num_ns,
          unique=True,
          range_max=vocab_size,
          seed=seed,
          name="negative_sampling")

      # Build context and label vectors (for one target word)
      negative_sampling_candidates = tf.expand_dims(
          negative_sampling_candidates, 1)

      context = tf.concat([context_class, negative_sampling_candidates], 0)
      label = tf.constant([1] + [0]*num_ns, dtype="int64")

      # Append each element from the training example to global lists.
      targets.append(target_word)
      contexts.append(context)
      labels.append(label)

  return targets, contexts, labels

word2vec のトレーニングデータを準備する

スキップグラム ネガティブ サンプリング ベースの word2vec モデルで 1 つの文を処理する方法を理解することにより、より大きな文のリストからトレーニングサンプルを生成できます。

テキストコーパスをダウンロードする

このチュートリアルでは、シェイクスピア著作のテキストファイルを使用します。次の行を変更して、このコードを独自のデータで実行します。

path_to_file = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt')
Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt
1115394/1115394 [==============================] - 0s 0us/step

ファイルからテキストを読み取り、最初の数行を出力します。

with open(path_to_file) as f:
  lines = f.read().splitlines()
for line in lines[:20]:
  print(line)
First Citizen:
Before we proceed any further, hear me speak.

All:
Speak, speak.

First Citizen:
You are all resolved rather to die than to famish?

All:
Resolved. resolved.

First Citizen:
First, you know Caius Marcius is chief enemy to the people.

All:
We know't, we know't.

First Citizen:
Let us kill him, and we'll have corn at our own price.

空でない行を使用して、次の手順として tf.data.TextLineDataset オブジェクトを作成します。

text_ds = tf.data.TextLineDataset(path_to_file).filter(lambda x: tf.cast(tf.strings.length(x), bool))
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.9/site-packages/tensorflow/python/autograph/pyct/static_analysis/liveness.py:83: Analyzer.lamba_check (from tensorflow.python.autograph.pyct.static_analysis.liveness) is deprecated and will be removed after 2023-09-23.
Instructions for updating:
Lambda fuctions will be no more assumed to be used in the statement where they are used, or at least in the same block. https://github.com/tensorflow/tensorflow/issues/56089

コーパスから文をベクトル化する

TextVectorization レイヤーを使用して、コーパスから文をベクトル化します。このレイヤの使用について詳しくは、テキスト分類のチュートリアルを参照してください。上記の最初の数文から、テキストは大文字または小文字にする必要があり、句読点を削除する必要があることに注意してください。これを行うには、TextVectorization レイヤーで使用する custom_standardization function を定義します。

# Now, create a custom standardization function to lowercase the text and
# remove punctuation.
def custom_standardization(input_data):
  lowercase = tf.strings.lower(input_data)
  return tf.strings.regex_replace(lowercase,
                                  '[%s]' % re.escape(string.punctuation), '')


# Define the vocabulary size and the number of words in a sequence.
vocab_size = 4096
sequence_length = 10

# Use the `TextVectorization` layer to normalize, split, and map strings to
# integers. Set the `output_sequence_length` length to pad all samples to the
# same length.
vectorize_layer = layers.TextVectorization(
    standardize=custom_standardization,
    max_tokens=vocab_size,
    output_mode='int',
    output_sequence_length=sequence_length)

テキストデータセットで TextVectorization.adapt を呼び出して語彙を作成します。

vectorize_layer.adapt(text_ds.batch(1024))

レイヤーの状態がテキストコーパスを表すように調整されると、TextVectorization.get_vocabulary を使用して語彙にアクセスできます。この関数は、頻度によって (降順で) 並べ替えられたすべての語彙トークンのリストを返します。

# Save the created vocabulary for reference.
inverse_vocab = vectorize_layer.get_vocabulary()
print(inverse_vocab[:20])
['', '[UNK]', 'the', 'and', 'to', 'i', 'of', 'you', 'my', 'a', 'that', 'in', 'is', 'not', 'for', 'with', 'me', 'it', 'be', 'your']

vectorize_layer を使用して、text_ds (tf.data.Dataset) 内の各要素のベクトルを生成できるようになりました。Dataset.batchDataset.prefetchDataset.mapDataset.unbatch を適用します。

# Vectorize the data in text_ds.
text_vector_ds = text_ds.batch(1024).prefetch(AUTOTUNE).map(vectorize_layer).unbatch()

データセットから配列を取得する

これで、整数でエンコードされた文の tf.data.Dataset ができました。word2vec モデルをトレーニングするためのデータセットを準備するには、データセットを文ベクトル シーケンスのリストにフラット化します。この手順は、データセット内の各文を繰り返し処理してポジティブなサンプルとネガティブなサンプルを生成するために必要です。

注意: 前に定義した generate_training_data() は TensorFlow 以外の Python/NumPy 関数を使用するため、tf.data.Dataset.maptf.py_functiontf.numpy_function を使用することもできます。

sequences = list(text_vector_ds.as_numpy_iterator())
print(len(sequences))
32777

sequences からいくつかのサンプルを調べます。

for seq in sequences[:5]:
  print(f"{seq} => {[inverse_vocab[i] for i in seq]}")
[ 89 270   0   0   0   0   0   0   0   0] => ['first', 'citizen', '', '', '', '', '', '', '', '']
[138  36 982 144 673 125  16 106   0   0] => ['before', 'we', 'proceed', 'any', 'further', 'hear', 'me', 'speak', '', '']
[34  0  0  0  0  0  0  0  0  0] => ['all', '', '', '', '', '', '', '', '', '']
[106 106   0   0   0   0   0   0   0   0] => ['speak', 'speak', '', '', '', '', '', '', '', '']
[ 89 270   0   0   0   0   0   0   0   0] => ['first', 'citizen', '', '', '', '', '', '', '', '']

シーケンスからトレーニングサンプルを生成する

sequences は、int でエンコードされた文のリストになりました。前に定義した generate_training_data 関数を呼び出すだけで、word2vec モデルのトレーニングサンプルを生成できます。要約すると、関数は各シーケンスの各単語を反復処理して、ポジティブおよびネガティブなコンテキストワードを収集します。ターゲット、コンテキスト、およびラベルの長さは同じであり、トレーニングサンプルの総数を表す必要があります。

targets, contexts, labels = generate_training_data(
    sequences=sequences,
    window_size=2,
    num_ns=4,
    vocab_size=vocab_size,
    seed=SEED)

targets = np.array(targets)
contexts = np.array(contexts)[:,:,0]
labels = np.array(labels)

print('\n')
print(f"targets.shape: {targets.shape}")
print(f"contexts.shape: {contexts.shape}")
print(f"labels.shape: {labels.shape}")
100%|██████████| 32777/32777 [00:49<00:00, 667.71it/s]
targets.shape: (65425,)
contexts.shape: (65425, 5)
labels.shape: (65425, 5)

データセットを構成してパフォーマンスを改善する

潜在的に多数のトレーニングサンプルに対して効率的なバッチ処理を実行するには、tf.data.Dataset API を使用します。このステップの後、word2vec モデルをトレーニングするための (target_word, context_word), (label) 要素の tf.data.Dataset オブジェクトが作成されます。

BATCH_SIZE = 1024
BUFFER_SIZE = 10000
dataset = tf.data.Dataset.from_tensor_slices(((targets, contexts), labels))
dataset = dataset.shuffle(BUFFER_SIZE).batch(BATCH_SIZE, drop_remainder=True)
print(dataset)
<BatchDataset element_spec=((TensorSpec(shape=(1024,), dtype=tf.int64, name=None), TensorSpec(shape=(1024, 5), dtype=tf.int64, name=None)), TensorSpec(shape=(1024, 5), dtype=tf.int64, name=None))>

Dataset.cacheDataset.prefetch を適用してパフォーマンスを向上させます。

dataset = dataset.cache().prefetch(buffer_size=AUTOTUNE)
print(dataset)
<PrefetchDataset element_spec=((TensorSpec(shape=(1024,), dtype=tf.int64, name=None), TensorSpec(shape=(1024, 5), dtype=tf.int64, name=None)), TensorSpec(shape=(1024, 5), dtype=tf.int64, name=None))>

モデルとトレーニング

word2vec モデルは、スキップグラムからの真のコンテキストワードと、ネガティブサンプリングによって取得された偽のコンテキストワードを区別する分類器として実装できます。ターゲットワードとコンテキストワードの埋め込みの間で内積乗算を実行して、ラベルの予測を取得し、データセット内の真のラベルに対する損失関数を計算できます。

サブクラス化された word2vec モデル

Keras Subclassing API を使用して、次のレイヤーで word2vec モデルを定義します。

  • target_embedding: tf.keras.layers.Embedding レイヤーは単語がターゲットワードとして表示されたときにその単語の埋め込みを検索します。このレイヤーのパラメータ数は (vocab_size * embedded_dim) です。
  • context_embedding: これはもう一つの tf.keras.layers.Embedding レイヤーで単語がコンテキストワードとして表示されたときに、その単語の埋め込みを検索します。このレイヤーのパラメータ数は、target_embedding のパラメータ数と同じです。つまり、(vocab_size * embedded_dim) です。
  • dots: これはトレーニングペアからターゲットとコンテキストの埋め込みの内積を計算する tf.keras.layers.Dot レイヤーです。
  • flatten: tf.keras.layers.Flatten レイヤーは、dots レイヤーの結果をロジットにフラット化します。

サブクラス化されたモデルを使用すると、(target, context) ペアを受け入れる call() 関数を定義し、対応する埋め込みレイヤーに渡すことができる context_embedding の形状を変更して、target_embedding で内積を実行し、フラット化された結果を返します。

重要点: target_embedding レイヤーと context embedded レイヤーも共有できます。また、両方の埋め込みを連結して、最終的な word2vec 埋め込みとして使用することもできます。

class Word2Vec(tf.keras.Model):
  def __init__(self, vocab_size, embedding_dim):
    super(Word2Vec, self).__init__()
    self.target_embedding = layers.Embedding(vocab_size,
                                      embedding_dim,
                                      input_length=1,
                                      name="w2v_embedding")
    self.context_embedding = layers.Embedding(vocab_size,
                                       embedding_dim,
                                       input_length=num_ns+1)

  def call(self, pair):
    target, context = pair
    # target: (batch, dummy?)  # The dummy axis doesn't exist in TF2.7+
    # context: (batch, context)
    if len(target.shape) == 2:
      target = tf.squeeze(target, axis=1)
    # target: (batch,)
    word_emb = self.target_embedding(target)
    # word_emb: (batch, embed)
    context_emb = self.context_embedding(context)
    # context_emb: (batch, context, embed)
    dots = tf.einsum('be,bce->bc', word_emb, context_emb)
    # dots: (batch, context)
    return dots

損失関数の定義とモデルのコンパイル

簡単にするためには、ネガティブサンプリング損失の代わりに tf.keras.losses.CategoricalCrossEntropy を使用できます。独自のカスタム損失関数を記述する場合は、次のようにします。

def custom_loss(x_logit, y_true):
      return tf.nn.sigmoid_cross_entropy_with_logits(logits=x_logit, labels=y_true)

モデルを構築します。128 の埋め込み次元で word2vec クラスをインスタンス化します (さまざまな値を試してみてください)。モデルを tf.keras.optimizers.Adam オプティマイザーでコンパイルします。

embedding_dim = 128
word2vec = Word2Vec(vocab_size, embedding_dim)
word2vec.compile(optimizer='adam',
                 loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True),
                 metrics=['accuracy'])

また、TensorBoard のトレーニング統計をログに記録するコールバックを定義します。

tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir="logs")

いくつかのエポックで、dataset でモデルをトレーニングします。

word2vec.fit(dataset, epochs=20, callbacks=[tensorboard_callback])
Epoch 1/20
63/63 [==============================] - 8s 109ms/step - loss: 1.6082 - accuracy: 0.2312
Epoch 2/20
63/63 [==============================] - 0s 3ms/step - loss: 1.5890 - accuracy: 0.5521
Epoch 3/20
63/63 [==============================] - 0s 3ms/step - loss: 1.5419 - accuracy: 0.6035
Epoch 4/20
63/63 [==============================] - 0s 3ms/step - loss: 1.4601 - accuracy: 0.5742
Epoch 5/20
63/63 [==============================] - 0s 3ms/step - loss: 1.3626 - accuracy: 0.5812
Epoch 6/20
63/63 [==============================] - 0s 3ms/step - loss: 1.2656 - accuracy: 0.6077
Epoch 7/20
63/63 [==============================] - 0s 3ms/step - loss: 1.1744 - accuracy: 0.6421
Epoch 8/20
63/63 [==============================] - 0s 3ms/step - loss: 1.0895 - accuracy: 0.6768
Epoch 9/20
63/63 [==============================] - 0s 3ms/step - loss: 1.0105 - accuracy: 0.7108
Epoch 10/20
63/63 [==============================] - 0s 3ms/step - loss: 0.9374 - accuracy: 0.7411
Epoch 11/20
63/63 [==============================] - 0s 3ms/step - loss: 0.8698 - accuracy: 0.7659
Epoch 12/20
63/63 [==============================] - 0s 3ms/step - loss: 0.8075 - accuracy: 0.7873
Epoch 13/20
63/63 [==============================] - 0s 3ms/step - loss: 0.7502 - accuracy: 0.8078
Epoch 14/20
63/63 [==============================] - 0s 3ms/step - loss: 0.6978 - accuracy: 0.8247
Epoch 15/20
63/63 [==============================] - 0s 3ms/step - loss: 0.6499 - accuracy: 0.8395
Epoch 16/20
63/63 [==============================] - 0s 3ms/step - loss: 0.6062 - accuracy: 0.8532
Epoch 17/20
63/63 [==============================] - 0s 3ms/step - loss: 0.5664 - accuracy: 0.8652
Epoch 18/20
63/63 [==============================] - 0s 3ms/step - loss: 0.5302 - accuracy: 0.8759
Epoch 19/20
63/63 [==============================] - 0s 3ms/step - loss: 0.4972 - accuracy: 0.8856
Epoch 20/20
63/63 [==============================] - 0s 3ms/step - loss: 0.4673 - accuracy: 0.8948
<keras.callbacks.History at 0x7f60f34a8310>

TensorBoard は、word2vec モデルの精度と損失を表示します。

#docs_infra: no_execute
%tensorboard --logdir logs

埋め込みのルックアップと分析

Model.get_layerLayer.get_weights を使用して、モデルから重みを取得します。TextVectorization.get_vocabulary 関数は、1 行に 1 つのトークンでメタデータファイルを作成するための語彙を提供します。

weights = word2vec.get_layer('w2v_embedding').get_weights()[0]
vocab = vectorize_layer.get_vocabulary()

ベクトルとメタデータファイルを作成して保存します。

out_v = io.open('vectors.tsv', 'w', encoding='utf-8')
out_m = io.open('metadata.tsv', 'w', encoding='utf-8')

for index, word in enumerate(vocab):
  if index == 0:
    continue  # skip 0, it's padding.
  vec = weights[index]
  out_v.write('\t'.join([str(x) for x in vec]) + "\n")
  out_m.write(word + "\n")
out_v.close()
out_m.close()

vectors.tsvmetadata.tsv をダウンロードして、取得した埋め込みを埋め込みプロジェクタで分析します。

try:
  from google.colab import files
  files.download('vectors.tsv')
  files.download('metadata.tsv')
except Exception:
  pass

次のステップ

このチュートリアルでは、ゼロからネガティブサンプリングを使用してスキップグラムの word2vec モデルを実装し、取得した単語の埋め込みを視覚化する方法を実演しました。