word2vec 番外篇 2—— 在 TensorFlow 中实现 softmax Word2Vec 方法 (持续更新)

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_15192373/article/details/90031406

本文代码

1. word2vec 结构

与其他机器学习模型一样,该网络也有两个组件:

1. 一个用于将所有 数据转换 为 可用格式

2. 一个用于对数据进行 训练、验证和测试

2. 本文内容

1. 介绍如何 将数据收集成可用的格式

2. 对模型的 TensorFlow 图进行讨论

3. 代码解读

   3.1 下载文本数据

    TensorFlow 有几个函数,可用于提取文本数据库并对其进行转换,在此基础上我们可以小批量(mini-batch)提取 输入词 及其 相关gram,进而用于 训练 Word2Vec 系统。下面的内容会依次介绍这些函数:

   (1) maybe_download() 下载原始单词数据

    该函数用于 检查是否已经从提供的 URL 下载了文件(代码中的 filename):

  • 如果没有,使用 urllib.request Python 模块(该模块可从给定的 url 中检索文件),并将该文件下载到本地代码目录中。
  • 如果文件已经存在(即 os.path.exists(filename)返回结果为真),那么函数不会再下载文件。

def maybe_download(filename, expected_bytes):
  """Download a file if not present, and make sure it's the right size."""
  if not os.path.exists(filename):
    filename, _ = urllib.request.urlretrieve(url + filename, filename)
  statinfo = os.stat(filename)
  if statinfo.st_size == expected_bytes:
    print('Found and verified', filename)
  else:
    print(statinfo.st_size)
    raise Exception(
        'Failed to verify ' + filename + '. Can you get to it with a browser?')
  return filename

原始下载数据文件可视化结果:   

(2) expected_bytes 检查文件

   接下来,expected_bytes 函数会 对文件大小进行检查,以确保下载文件与预期的文件大小一致。如果一切正常,将返回至用于提取数据的文件对象。代码如下: 

url = 'http://mattmahoney.net/dc/'
filename = maybe_download('text8.zip', 31344016)

 (3) Python zipfile 提取数据

    接下来,取用已下载文件的文件,并使用 Python zipfile 模块 提取数据。

# Read the data into a list of strings.
def read_data(filename):
  """Extract the first file enclosed in a zip file as a list of words."""
  with zipfile.ZipFile(filename) as f:
    data = tf.compat.as_str(f.read(f.namelist()[0])).split()
  return data

    zipfile.ZipFile() : 读取文件。使用 zipfile 模块中的读取器功能

    namelist() : 检索该档案中的所有成员。在本例中只有一个成员,所以可以使用 0 索引对其进行访问

    read(): 函数读取文件中的所有文本,并传递给 TensorFlow 的 as_str 函数

    as_str: 确保文本保存为字符串数据类型。

    split() :最后,使用 split() 函数创建一个列表,该列表包含 文本文件中所有的单词,并用空格字符分隔。内容展示语句,和列表部分内容如下:

vocabulary = read_data(filename)
print(vocabulary[:7])

['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse']

   如上述返回值所见,返回的词汇数据包含 一个清晰的单词列表,将其按照原始文本文件的句子排序。 

展示列表长度:

vocabulary = read_data(filename)
print('Data size', len(vocabulary))

列表可视化的内容:

全部是单词,之间有空格

3.2 建立 skip-gram 原始数据

现在已经 提取了所有的单词并置入列表,需要对其进行进一步的处理以 创建 skip-gram 批量数据。处理步骤如下:

1. 创建嵌入向量。提取前 10000 个最常用的单词,置入嵌入向量;

具体:设置一个「计数器」列表。该列表中存储在数据集中找到一个单词的次数。由于词汇量仅限于 10,000 个单词,因此,不包括在前 10,000 个最常用单词中的任何单词都将标记为「UNK」,表示「未知」。

2. 创建独热码。汇集所有单独的单词,并用唯一的整数对它们进行索引。这一步等同于为单词 创建独热码。将使用一个字典来完成这一步;

具体:对计数列表进行扩展,返回 n 个最常见的单词。使用 Python 集合模块和 Counter()类以及关联的 most_common()函数 对已初始化的计数列表进行扩展。这些设置用于计算给定参数(单词)中的单词数量,然后以列表格式返回 n 个最常见的单词。

如何返回 n 个最常见的单词?

该函数的下一部分创建了一个字典,名为 dictionary,该字典由关键词进行填充,这些关键词与每个独一无二的词相对应。分配给每个独一无二的关键词的值只是简单地将字典的大小以整数形式进行递增。例如,将 1 赋值给第一常用的单词,2 赋值给第二常用的词,3 赋值给第三常用的词,依此类推(整数 0 被分配给「UNK」词)。这一步给词汇表中的每个单词赋予了唯一的整数值,表示最常用的等级。

3. 循环遍历。循环遍历数据集中的每个单词(词汇变量),并将其分配给在步骤 2 中创建的独一无二的整数。这使在单词数据流中进行查找或处理操作变得更加容易。

具体: 该函数 对数据集中的每个单词进行循环遍历。该数据集是由 read_data()函数输出的。经过这一步,我们创建了一个叫做「data」的列表,该列表长度与单词量相同。但该列表不是由独立单词组成的单词列表,而是个整数列表。在字典里由分配给该单词的唯一整数表示每一个单词。因此,对于数据集的第一个句子['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse'],现在在数据变量中是这样的:[5242,3083,12,6,195,2,3136]。这解决了上述第三步。

4. 建立检索单词的方法。最后,该函数创建了一个名为 reverse_dictionary 的字典,它允许 根据其唯一的整数标识符来查找单词,而非根据单词查找标识符。

def build_dataset(words, n_words):
  """Process raw inputs into a dataset."""
  count = [['UNK', -1]]
  count.extend(collections.Counter(words).most_common(n_words - 1))
  dictionary = dict()
  for word, _ in count:
    dictionary[word] = len(dictionary)
  data = list()
  unk_count = 0
  for word in words:
    if word in dictionary:
      index = dictionary[word]
    else:
      index = 0  # dictionary['UNK']
      unk_count += 1
    data.append(index)
  count[0][1] = unk_count
  reversed_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
  return data, count, dictionary, reversed_dictionary

原始数据可视化结果:

numpy 格式

3.3 建立训练数据及其标签。(包含 输入词相关 gram 的数据集)

建立数据的最后一点,现在要创建一个包含输入词和相关 gram 的数据集,用于训练 Word2Vec 嵌入系统。

执行这一步操作的代码如下:

# Step 3: Function to generate a training batch for the skip-gram model.
def generate_batch(batch_size, num_skips, skip_window):
  global data_index
  assert batch_size % num_skips == 0
  assert num_skips <= 2 * skip_window
  batch = np.ndarray(shape=(batch_size), dtype=np.int32)
  labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
  span = 2 * skip_window + 1  # [ skip_window target skip_window ]
  buffer = collections.deque(maxlen=span)
  for _ in range(span):
    buffer.append(data[data_index])
    data_index = (data_index + 1) % len(data)
  for i in range(batch_size // num_skips):
    target = skip_window  # target label at the center of the buffer
    targets_to_avoid = [skip_window]
    for j in range(num_skips):
      while target in targets_to_avoid:
        target = random.randint(0, span - 1)
      targets_to_avoid.append(target)
      batch[i * num_skips + j] = buffer[skip_window]
      labels[i * num_skips + j, 0] = buffer[target]
    buffer.append(data[data_index])
    data_index = (data_index + 1) % len(data)
  # Backtrack a little bit to avoid skipping words in the end of a batch
  data_index = (data_index + len(data) - span) % len(data)
  return batch, labels

        该函数会生成用于训练的小批量数据。这些小 batch 包括输入词(存储在batch中)和 gram 中随机关联的上下文单词,这些 batch 将作为标签对结果进行预测(存储在上下文中)。例如,在 gram 为 5 的「the cat sat on the」中,输入词即中心词,也就是「sat」,并且将被预测的上下文将从这一 gram 的剩余词中随机抽取:[「the 」,「cat」,「on」,「the」]。在该函数中,

num_skips :定义 从上下文中随机抽取的 单词数量。该函数会使用 skip_window 定义输入词周围抽取的上下文单词的窗口大小。在上述例子(「the cat sat on the」)中,输入词「sat」周围的 skip_window 的宽度为 2。

batch_size 的变量:batch 和 输出标签

span size:定义其广度的大小,即为要提取的输入词和上下文的单词列表的大小。在上述例子的子句「the cat on the」中,广度是 5 = 2 * skip window + 1。

(1) 创建一个缓冲区: 

这个缓冲区将会最大程度地保留 span 元素,还是一种用于采样的移动窗口。每当有新的单词索引添加至缓冲区时,最左方的元素将从缓冲区中排出,以便为新的单词索引腾出空间。输入文本流中的缓冲器被存储在全局变量 data_index 中,每当缓冲器中有新的单词进入时,data_index 递增。如果到达文本流的末尾,索引更新的「%len(data)」组件会将计数重置为 0。

buffer = collections.deque(maxlen=span)
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)

(2) 填写批量处理和上下文变量:

for i in range(batch_size // num_skips):
    target = skip_window  # target label at the center of the buffer
    targets_to_avoid = [skip_window]
    for j in range(num_skips):
      while target in targets_to_avoid:
        target = random.randint(0, span - 1)
      targets_to_avoid.append(target)
      batch[i * num_skips + j] = buffer[skip_window]
      labels[i * num_skips + j, 0] = buffer[target]
    buffer.append(data[data_index])
    data_index = (data_index + 1) % len(data)
  # Backtrack a little bit to avoid skipping words in the end of a batch
  data_index = (data_index + len(data) - span) % len(data)

选择的第一个词「target」是单词表最中间的词,因此这是输入词。然后从单词的 span 范围中随机选择其他单词,确保 上下文中不包含输入词 且 每个上下文单词都是唯一的。batch 变量会反映出重复的输入词(buffer [skip_window]),这些输入词会与 context 中的每个上下文单词进行匹配。

然后返回 batch 变量和 context 变量,现在我们有了从数据集中分出 batch 的方法。

batch和labels可视化结果:

3.4 建立 TensorFlow 模型

现在,需要建立之前提出的神经网络,该网络在 TensorFlow 中使用 词嵌入矩阵 作为 隐藏层,还包括一个输出 softmax 层。通过训练该模型,将得到最好的词嵌入矩阵。因此我们将通过学习得到一个简化的、保留了上下文的 单词到向量的映射

(1) 建立验证集

在 TensorFlow 中训练 Word2Vec 的代码之前,要先建立一个用于测试模型表现的验证集。

方法一:从词汇表最常用的词中随机提取验证单词,代码如下所示:

# We pick a random validation set to sample nearest neighbors. Here we limit the
# validation samples to the words that have a low numeric ID, which by
# construction are also the most frequent.
valid_size = 16     # Random set of words to evaluate similarity on.
valid_window = 100  # Only pick dev samples in the head of the distribution.
valid_examples = np.random.choice(valid_window, valid_size, replace=False)
num_sampled = 64    # Number of negative examples to sample.

graph = tf.Graph()

上面的代码从 0 到 100 中随机选择了 16 个整数,这些整数与文本数据中最常用的 100 个单词的整数索引相对应。将通过考察这些词语,来评估 相关单词向量空间 相关联的过程 在我们的学习模型中进行得如何。

方法二:通过测量 向量空间中 最接近的向量 来建立验证集,并使用英语知识以确保这些词确实是相似的。

(2) 设置变量

batch_size = 128
embedding_size = 128  # Dimension of the embedding vector.
skip_window = 1       # How many words to consider left and right.
num_skips = 2         # How many times to reuse an input to generate a label.

(3) 设置 TensorFlow 占位符

设置一些 TensorFlow 占位符,这些占位符会保存 输入词(的整数索引)和准备 预测的上下文单词

还需要创建一个常量来保存 TensorFlow 中的验证集索引

  # Input data.
  train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
  train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
  valid_dataset = tf.constant(valid_examples, dtype=tf.int32)

(4) 设置嵌入矩阵变量或张量

这里使用 TensorFlow 中 embedding_lookup() 函数最直接的方法。创建嵌入变量,这实际上是 线性隐藏层 连接的权重

# Look up embeddings for inputs.
    embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
    embed = tf.nn.embedding_lookup(embeddings, train_inputs)

1. 对变量初始化:用 -1.0 到 1 的随机均匀分布对变量进行初始化。

2. 变量大小:包括 vocabulary_size 和 embedding_size。vocabulary_size 是上一节中用来设置数据的 10,000 个单词。这是我们输入的独热向量,在向量中仅有一个值为「1」的元素是当前的输入词,其他值都为「0」。embedding_size 是隐藏层的大小,也是新的更小的单词表示的长度。

3. 看作查找表:该文也考虑了可以把这个张量看作一个大的查找表。行是词汇表中的每个词,列是每个词的新的向量表示。以下一个简化的例子(使用虚拟值),其中 vocabulary_size = 7,embedding_size = 3:

正如我们所见,「anarchism」(实际上由一个整数或独热向量表示)现在表示为 [0.5,0.1,-0.1]。我们可以通过查找其整数索引、搜索嵌入行查找嵌入向量的方法「查找」anarchism:[0.5,0.1,-0.1]。

备注:本博客到目前为止展示了数据的预处理,数据的嵌入过程。

4. 上面的代码涉及到 tf.nn.embedding_lookup () 函数:

输入:一个整数索引向量(独热编码)。在本例中是训练输入词的张量 train_input,并在已给的嵌入张量中「查找」这些索引。

输出:每个给定输入词的当前嵌入向量。训练批次中每个给定输入词的当前嵌入向量,完整的嵌入张量将在训练过程中进行优化。

(5) 连接输出 softmax 层

接下来,创建一些权重和偏差值来连接输出 softmax 层,并对其进行运算。如下所示:

# Construct the variables for the softmax
weights = tf.Variable(tf.truncated_normal([vocabulary_size, embedding_size],
stddev=1.0 / math.sqrt(embedding_size)))
biases = tf.Variable(tf.zeros([vocabulary_size]))
hidden_out = tf.matmul(embed, tf.transpose(weights)) + biases

1. 因为权重变量连接着隐藏层和输出层,因此其大小 size(out_layer_size,hidden_layer_size)=(vocabulary_size,embedding_size)。

2. 一如以往,偏差值是一维的,且大小与输出层一致。

3.将嵌入变量与权重相乘(嵌入),再与偏差值相加。

(6) 做 softmax 运算

接下来可以做 softmax 运算,并通过 交叉熵损失函数 来优化模型的权值、偏差值和嵌入。

1. 首先将上下文单词和整数索引 转换成 独热向量。

2. 使用 TensorFlow 中的 softmax_cross_entropy_with_logits () 函数简化这个过程。

下面的代码不仅执行了这两步操作,还对梯度下降进行了优化:

# convert train_context to a one-hot format
train_one_hot = tf.one_hot(train_context, vocabulary_size)
cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=hidden_out,
labels=train_one_hot))# Construct the SGD optimizer using a learning rate of 1.0.
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(cross_entropy)

(7) 确定哪些词彼此相似

接下来,需要执行 相似性评估 以检查模型训练时的表现。

1. 测量不同词的词嵌入向量间的「距离」。为了确定哪些词彼此相似,需要执行某种操作,来测量不同词的 词嵌入向量间的「距离」。

2. 在本例中,计算了 余弦相似度 以度量 不同向量间的距离。定义如下:

粗体字母**A**和**B**:需要测量距离的两个向量。

|| A || 2 的双平行线:向量的 L2 范数。将向量的每个维数(在这种情况下,n = 300,嵌入向量的宽度)平方对其求和后再取平方根。

3.  在 TensorFlow 中 计算余弦相似度 的最好方法是对每个向量进行归一化,如下所示:

  (1) 首先,分别使用 tf.square(),tf.reduce_sum() 和 tf.sqrt() 函数分别计算 每个向量的 L2 范数的平方和 以及 平方根

  # 计算两个不同单词的词嵌入向量之间的距离.
  norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
  normalized_embeddings = embeddings / norm

  (2) 然后使用 tf.nn.embedding_lookup() 函数查找之前提到的 验证向量 或 验证词

  valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings, valid_dataset)

  (3) 向 embedding_lookup() 函数提供了一个整数列表(该列表与我们的 验证词汇表 相关联),该函数对 normalized_embedding 张量 按行进行查找,返回一个 归一化嵌入 的验证集的子集。现在我们有了 归一化的验证集张量 valid_embeddings,可将其嵌入 完全归一化的词汇表(normalized_embedding)以完成相似性计算:

similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True)

该操作将返回一个(validation_size, vocabulary_size)大小的张量,该张量

行: 指代一个验证词,

列: 验证词 和 词汇表中其他词 的相似度。

猜你喜欢

转载自blog.csdn.net/qq_15192373/article/details/90031406