word2vec

在 TensorFlow.org 上查看 在 Google Colab 中运行 在 Github 上查看源代码 {img1下载笔记本

word2vec 不是单一算法,而是一系列模型架构和优化,可用于从大型数据集中学习单词嵌入向量。通过 word2vec 学习到的嵌入向量已被证明在各种下游自然语言处理任务上取得了成功。

注:本教程基于 Efficient estimation of word representations in vector spaceDistributed representations of words and phrases and their compositionality。本教程不是上述论文的精确实现,而旨在阐明关键思想。

上述论文提出了两种学习单词表示的方法:

  • 连续词袋模型:根据周围的上下文单词预测中间单词。上下文由当前(中间)单词前后的几个单词组成。这种架构被称为词袋模型,因为上下文中的单词顺序并不重要。
  • 连续跳字模型:预测同一句子中当前单词前后一定范围内的单词。下面给出了一个工作示例。

您将在本教程中使用跳字方式。首先,您将使用一个句子来探索跳字和其他概念。接下来,您将在一个小型数据集上训练自己的 word2vec 模型。本教程还包含用于导出经过训练的嵌入向量并在 TensorFlow Embedding Projector 中可视化它们的代码。

跳字和负采样

词袋模型能够在给定相邻上下文的情况下预测单词,而跳字模型能够在给定单词本身的情况下预测单词的上下文(或邻居)。该模型在跳字上训练,它是允许跳过词例的 n 元语法(请参阅下图的示例)。一个单词的上下文可以通过一组 (target_word, context_word) 的跳字对来表示,其中 context_word 出现在 target_word 的相邻上下文中。

考虑以下由八个单词组成的句子:

The wide road shimmered in the hot sun.

这句话的 8 个单词中,每一个单词的上下文单词由一个窗口大小定义。窗口大小决定了 target_word 可以被视为 context word 的单词跨度。下面是基于不同窗口大小的目标词的跳字表。

注:对于本教程,n 的窗口大小表示每边有 n 个单词,每个单词的总窗口跨度为 2*n+1 个单词。

word2vec_skipgrams

跳字模型的训练目标是在给定目标词的情况下最大化预测上下文词的概率。对于单词序列 w1,w2,... wT,目标可写为平均对数概率

word2vec_skipgram_objective

其中,c 是训练上下文的大小。基本的跳字公式使用 Softmax 函数定义该概率。

word2vec_full_softmax

其中,vv' 是单词的目标和上下文向量表示,W 是词汇量。

计算这个公式的分母涉及对整个词汇表执行完整的 Softmax,其通常是很大的 (105-107) 项。

噪声对比估计 (NCE) 损失函数是完整 Softmax 的有效近似。为了学习单词嵌入向量而不是对单词分布进行建模,NCE 损失可以简化为使用负采样。

目标单词的简化负采样目标是将上下文单词与从单词的噪声分布 Pn(w) 中抽取的 num_ns 负样本区分开来。更准确地说,对于一个跳字对,词汇表上的完整 Softmax 的有效近似是将目标单词的损失作为上下文单词和 num_ns 负样本之间的分类问题。

负样本定义为 (target_word, context_word) 对,这样 context_word 就不会出现在 target_wordwindow_size 邻域中。对于例句,这些是一些潜在的负样本(当 window_size2 时)。

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

在下一部分中,您将为单个句子生成跳字和负样本。您还将在本教程后面学习二次采样技术并为正负训练样本训练分类模型。

设置

import io
import re
import string
import tqdm

import numpy as np

import tensorflow as tf
from tensorflow.keras import layers
# 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))

创建一个词汇表来保存从词例到整数索引的映射:

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)

创建一个反向词汇表来保存从整数索引到词例的映射:

inverse_vocab = {index: token for token, index in vocab.items()}
print(inverse_vocab)

向量化您的句子:

example_sequence = [vocab[word] for word in tokens]
print(example_sequence)

从一个句子生成跳字

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

打印几个正跳字:

for target, context in positive_skip_grams[:5]:
  print(f"({target}, {context}): ({inverse_vocab[target]}, {inverse_vocab[context]})")

对某个跳字进行负采样

skipgrams 函数通过在给定的窗口跨度上滑动来返回所有正的跳字对。要生成额外的跳字对作为训练的负样本,您需要从词汇表中随机抽取单词。使用 tf.random.log_uniform_candidate_sampler 函数在窗口中对给定目标单词采样 num_ns 个负样本。您可以在一个跳字的目标单词上调用该函数,并将上下文单词作为 true 类传递,以将其排除在采样之外。

要点:[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