Word2Vec — Скип-грамм
За некоторыми исключениями, модели машинного обучения не принимают необработанный текст в качестве входных данных. Последовательности слов сначала должны быть каким-то образом закодированы. Мы могли бы представить каждое предложение как мешок слов (BOW). Сначала мы находим все уникальные слова в текстовом корпусе. Затем мы сопоставляем каждое предложение с вектором, длина которого равна длине словаря (т. е. количеству уникальных слов), так что значения индексов, соответствующих словам, присутствующим в предложении, равны 1, а значения всех остальные индексы оставляют равными 0.
При таком подходе есть две проблемы:
- Вектор очень разреженный (т.е. большинство значений равны 0)
- Мы теряем информацию, относящуюся к контексту (то есть порядок слов в предложении).
В качестве альтернативы мы могли бы представить текст с помощью встраивания слов. Встраивание слов — это изученное представление текста, в котором связанные слова будут ближе друг к другу в пространстве признаков.
Встраивания слов можно вычислить путем обучения модели машинного обучения под названием Word2Vec. Существует два варианта Word2Vec — skip-gram и CBOW. Вариант с пропуском грамма берет целевое слово и пытается предсказать слова окружающего контекста, тогда как вариант CBOW (непрерывный пакет слов) берет набор слов контекста и пытается предсказать целевое слово. В этом посте мы рассмотрим вариант skip-gram.
Предположим, у нас есть следующее предложение:
The wide road shimmered in the hot sun.
Размер окна определяет диапазон слов по обе стороны от target_word
, которые можно рассматривать как context word
, в отличие от количества контекстных слов.
Алгоритм
Для каждого слова t = 1... T мы предсказываем окружающие слова в окне «радиуса» m. Мы обучаем модель машинного обучения, чтобы максимизировать вероятность любого контекстного слова с учетом текущего центрального слова.
Как и для других вероятностных моделей, мы пытаемся минимизировать отрицательную логарифмическую вероятность.
где P(w_{t+j}|w_{t}) можно сформулировать как функцию Softmax.
где
Последнее читается как вероятность выходного слова o при наличии центрального слова c. Напомним, что знаменатель в функции Softmax используется для нормализации результата, чтобы получить вероятность (т.е. число в диапазоне от 0 до 1).
Архитектура
Нейронная сеть со скип-граммой состоит из одного скрытого слоя. Ввод представляет собой набор слов (BOW) со значением 1 в позиции центрального слова. Результатом является вероятность нахождения определенного слова в каждой позиции контекстного окна. В следующем примере мы предполагаем, что используем размер окна 1. Как мы видим, изначально модель предсказывает, что слово shimmered
следует за словом wide
в предложении, а слово road
предшествует слову wide
в предложении. .
Важно отметить, что мы не писали строчные буквы. Таким образом, у нас есть The
и the
. Однако на практике вы бы это сделали.
Используя обратное распространение, модель корректирует веса до тех пор, пока ошибка не будет сведена к минимуму. Как мы видим, после нескольких итераций он правильно предсказывает, что последующее слово после wide
будет road
.
питон
Разберем отдельные участки кода из подробного учебника по word2vec от Google.
Предположим, у нас есть то же предложение, что и раньше.
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)
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]
Используя размер окна 2
, мы генерируем список всех возможных положительных обучающих выборок с учетом примера предложения.
window_size = 2
positive_skip_grams, _ = tf.keras.preprocessing.sequence.skipgrams( example_sequence, vocabulary_size=vocab_size, window_size=window_size, negative_samples=0)
for target, context in positive_skip_grams[:5]: print(f"({target}, {context}): ({inverse_vocab[target]}, {inverse_vocab[context]})")
(5, 4): (in, shimmered) (1, 6): (the, hot) (1, 5): (the, in) (1, 3): (the, road) (6, 5): (hot, in)
Используя первую пару целевых и контекстных слов, мы генерируем num_ns = 4
отрицательных обучающих выборок. Образец является отрицательным (т. е. ему присвоена метка 0), если контекстное слово не найдено внутри контекстного окна.
# 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])
# 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 : 6 target_word : hot context_indices : [7 2 1 4 3] context_words : ['sun', 'wide', 'the', 'shimmered', 'road'] label : [1 0 0 0 0]
На следующей диаграмме показана процедура создания обучающего примера:
Мы определяем функцию, которая будет динамически генерировать обучающие примеры с учетом списка предложений, размера окна, количества отрицательных выборок, размера словаря и случайного начального числа.
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
Теперь мы можем приступить к созданию обучающих примеров из большого списка предложений. То есть текстовый файл сочинения Шекспира.
path_to_file = tf.keras.utils.get_file('shakespeare.txt', 'https://storage.googleapis.com/download.tensorflow.org/data/shakespeare.txt') text_ds = tf.data.TextLineDataset(path_to_file).filter(lambda x: tf.cast(tf.strings.length(x), bool)) def custom_standardization(input_data): lowercase = tf.strings.lower(input_data) return tf.strings.regex_replace(lowercase, '[%s]' % re.escape(string.punctuation), '') vocab_size = 4096 sequence_length = 10 vectorize_layer = layers.TextVectorization( standardize=custom_standardization, max_tokens=vocab_size, output_mode='int', output_sequence_length=sequence_length) vectorize_layer.adapt(text_ds.batch(1024)) inverse_vocab = vectorize_layer.get_vocabulary() text_vector_ds = text_ds.batch(1024).prefetch(AUTOTUNE).map(vectorize_layer).unbatch() sequences = list(text_vector_ds.as_numpy_iterator()) 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', '', '', '', '', '', '', '', '']
Мы настраиваем набор данных, который будет использоваться для обучения модели.
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) 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) dataset = dataset.cache().prefetch(buffer_size=AUTOTUNE)
Мы определяем класс для модели 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
Мы будем представлять каждое слово в словаре, используя 128 измерений. Мы создаем экземпляр нашего класса Word2Vec. Мы компилируем модель, используя категориальную кроссэнтропию для нашей функции потерь.
embedding_dim = 128 word2vec = Word2Vec(vocab_size, embedding_dim) word2vec.compile(optimizer='adam', loss=tf.keras.losses.CategoricalCrossentropy(from_logits=True), metrics=['accuracy'])
Наконец, мы обучаем модель.
word2vec.fit(dataset, epochs=20)
Мы получаем вложения (то есть веса) между входным слоем и скрытым слоем.
weights = word2vec.get_layer('w2v_embedding').get_weights()[0]
Как мы видим, мы представляем каждое из 4096 слов словаря с помощью вектора вложения длиной 128.
weights.shape (4096, 128)
Мы можем исследовать вектор вложения, соответствующий первому слову в словаре.
weights[0] array([ 0.02083263, 0.00343355, 0.03133059, 0.04064811, 0.02139286, 0.01668987, -0.01700681, 0.03104338, 0.00513292, 0.01149722, 0.00156037, 0.04110433, -0.02908002, -0.02072917, -0.04493903, -0.03360658, 0.02354895, 0.02986685, 0.01450031, -0.00434611, 0.02604233, 0.00688297, -0.00568321, 0.02448267, -0.04282743, 0.01752845, 0.02333864, -0.03737045, -0.03860588, 0.03164918, -0.03887875, 0.03344462, -0.04599243, -0.00912831, -0.03298129, -0.02165511, 0.00222781, -0.01334076, 0.03560077, -0.01657902, -0.04948949, 0.00923187, 0.03645227, 0.00624547, -0.00375736, 0.03080207, -0.03460135, 0.00123183, 0.0317348 , -0.03172968, -0.01598473, 0.03343581, 0.03939797, 0.01271281, 0.01737561, -0.04787338, 0.03081578, 0.02194339, 0.00668417, 0.0198779 , -0.03545182, 0.03608498, 0.03983852, 0.01381046, 0.02620314, -0.01378284, 0.04695277, -0.0301432 , -0.01917797, 0.03523597, 0.03922388, 0.02773141, 0.00329931, 0.02588192, 0.03493189, -0.02089679, 0.04374716, -0.03882134, -0.02024856, 0.04483554, -0.03621026, -0.04145117, -0.03030737, -0.02996567, -0.00220994, 0.0392569 , 0.03163559, -0.02619413, 0.04448912, -0.01938783, 0.02185104, 0.01294803, -0.01223926, -0.02752018, 0.02359452, 0.01469387, 0.01765844, -0.00813044, -0.04376047, -0.01028157, 0.00078993, -0.01525372, -0.0381612 , -0.00429031, 0.01438124, 0.03173996, 0.02320362, -0.03639726, -0.01158337, 0.04985858, 0.03488507, 0.0025389 , 0.03290978, 0.02607682, 0.04781124, 0.00342916, -0.03108559, 0.0361053 , 0.02612146, -0.00554097, -0.03796817, 0.03855484, -0.03623279, 0.0217861 , -0.01969334, -0.03057173, 0.03088465, -0.02974273], dtype=float32)
Заключение
Вложения слов — это широко используемое представление слов, которое фиксирует сходство между данным словом и другими словами в корпусе. В отличие от Bag of Words (BOW), вложения слов имеют дополнительное преимущество в том, что они плотные. Вложения слов можно получить, обучив модель Word2Vec и взглянув на веса. Существует два варианта модели Word2Vec: skip-gram и CBOW.