本文详细介绍了 word2vector 模型的模型架构,以及 TensorFlow 的实现过程,包括数据准备、建立模型、构建验证集,并给出了运行结果示例。
GitHub 链接:https://github.com/adventuresinML/adventures-in-ml-code
Word2Vec softmax 训练器
在接下来的教程中,我将解决的问题是该如何建立一个深度学习模型预测文本序列。然而,在建立模型之前,我们必须理解一些关键的自然语言处理(NLP)的思想。NLP 的关键思想之一是如何有效地将单词转换为数字向量,然后将这些数字向量「馈送」到机器学习模型中进行预测。本教程将对现在使用的主要技术,即「Word2Vec」进行介绍。在讨论了相关的背景材料之后,我们将使用 TensorFlow 实现 Word2Vec 嵌入。要快速了解 TensorFlow,请查看我的 TensorFlow 教程:http://adventuresinmachinelearning.com/python-tensorflow-tutorial/
我们为什么需要 Word2Vec
如果我们想把单词输入机器学习模型,除非使用基于树的方法,否则需要把单词转换成一些数字向量。一种直接的方法是使用「独热编码」方法将单词转换为稀疏表示,向量中只有一个元素设置为 1,其余为 0。我们构建分类任务也采用了相同的方法——详情请参考该教程:http://adventuresinmachinelearning.com/neural-networks-tutorial/#setting-up-output
所以,我们可以使用如下的向量表示句子「The cat sat on the mat」:
我们在此将一个六个字的句子转换为一个 6*5 的矩阵,其中 5 是词汇量(「the」有重复)。然而,在实际应用中,我们希望深度学习模型能够在词汇量很大(10,000 字以上)的情况下进行学习。从这里能看到使用「独热码」表示单词的效率问题——对这些词汇建模的任何神经网络的输入层至少都有 10,000 个节点。不仅如此,这种方法剥离了单词的所有局部语境——也就是说它会去掉句子中(或句子之间)紧密相连的单词的信息。
例如,我们可能想看到「United」和「States」靠得很近,或者是「Soviet」和「Union」,或者「食物」和「吃」等等。如果我们试图以这种方法对自然语言建模,会丢失所有此类信息,这将是一个很大的疏漏。因此,我们需要使用更高效的方法表示文本数据,而这种方法可以保存单词的上下文的信息。这是 Word2Vec 方法发明的初衷。
Word2Vec 方法
如上文所述,Word2Vec 方法由两部分组成。首先是将高维独热形式表示的单词映射成低维向量。例如将 10,000 列的矩阵转换为 300 列的矩阵。这个过程被称为词嵌入。第二个目标是在保留单词上下文的同时,从一定程度上保留其意义。在 Word2Vec 方法中实现这两个目标的方法之一是,输入一个词,然后试着估计其他词出现在该词附近的概率,称为 skip-gram 方法。还有一种与此相反的被称为连续词袋模型(Continuous Bag Of Words,CBOW)的方法——CBOW 将一些上下文词语作为输入,并通过评估概率找出最适合(概率最大)该上下文的词。在本教程中,我们将重点介绍 skip-gram 方法。
什么是 gram?gram 是一个有 n 个单词的组(group),其中 n 是 gram 的窗口大小(window size)。因此,对「The cat sat on the mat」这句话来说,这句话用 3 个 gram 表示的话,是「The cat sat」、「cat sat on」、「sat on the」、「on the mat」。「skip」指一个输入词在不同的上下文词的情况下,在数据集中重复的次数(这点会在稍后陈述)。这些 gram 被输入 Word2Vec 上下文预测系统。举个例子,假设输入词是「cat」——Word2Vec 试图从提供的输入字中预测上下文(「the」,「sat」)。Word2Vec 系统将遍历所有给出的 gram 和输入的单词,并尝试学习适当的映射向量(嵌入),这些映射向量保证了在给定输入单词的情况下,正确的上下文单词能得到更高概率。
什么是 Word2Vec 预测系统?不过是一种神经网络。
softmax Word2Vec 方法
从下图考虑——在这种情况下,我们将假设「The cat sat on the mat」这个句子是一个文本数据库的一部分,而这个文本数据库的词汇量非常大——有 10,000 个字。我们想将其减少到长度为 300 的嵌入。
Word2Vec softmax 训练器
如上表所示,如果我们取出「cat」这个词,它将成为 10,000 个词汇中的一个单词。因此我们可以将它表示成一个长度为 10,000 的独热向量。然后将这个输入向量连接到一个具有 300 个节点的隐藏层。连接这个图层的权重将成为新的词向量。该隐藏层中的节点的激活是加权输入的线性总和(不会使用如 sigmoid 或 tanh 这样的非线性激活函数)。此后这些节点会馈送到 softmax 输出层。在训练过程中,我们想要改变这个神经网络的权重,使「cat」周围的单词在 softmax 输出层中输出的概率更高。例如,如果我们的文本数据集有许多苏斯博士(Dr.Seuss)的书籍,我们希望通过神经网络,像「the」,「sat」和「on」这样的词能得到更高概率(给出很多诸如「the cat sat on the mat」这样的句子)。
通过训练这个网络,我们将创建一个 10,000*300 的权重矩阵,该矩阵使用有 300 个节点的隐藏层与长度为 10,000 的输入相连接。该矩阵中的每一行都与有 10,000 词汇的词汇表的一个单词相对应——我们通过这种方式有效地将表示单词的独热向量的长度由 10,000 减少至 300。实际上,该权重矩阵可以当做查找或编码单词的总表。不仅如此,由于我们采用这种方式训练网络,这些权值还包含了上下文信息。一旦我们训练了网络,就意味着我们放弃了 softmax 层并使用 10,000 x 300 的权重矩阵作为我们的嵌入式查找表。
如何用代码实现上述想法?
在 TensorFlow 中实现 softmax Word2Vec 方法
与其他机器学习模型一样,该网络也有两个组件——一个用于将所有数据转换为可用格式,另一个则用于对数据进行训练、验证和测试。在本教程中,我首先会介绍如何将数据收集成可用的格式,然后对模型的 TensorFlow 图进行讨论。请注意,在 Github 中可找到本教程的完整代码。在本例中,大部分代码都是以这里的 TensorFlow Word2Vec 教程(https://github.com/tensorflow/tensorflow/blob/r1.2/tensorflow/examples/tutorials/word2vec/word2vec_basic.py)为基础,并对其进行了一些个人修改。
准备文本数据
前面提到的 TensorFlow 教程有几个函数,这些函数可用于提取文本数据库并对其进行转换,在此基础上我们可以小批量(mini-batch)提取输入词及其相关 gram,进而用于训练 Word2Vec 系统。下面的内容会依次介绍这些函数:
def maybe_download(filename, url, 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
该函数用于检查是否已经从提供的 URL 下载了文件(代码中的 filename)。如果没有,使用 urllib.request Python 模块(该模块可从给定的 url 中检索文件),并将该文件下载到本地代码目录中。如果文件已经存在(即 os.path.exists(filename)返回结果为真),那么函数不会再下载文件。接下来,expected_bytes 函数会对文件大小进行检查,以确保下载文件与预期的文件大小一致。如果一切正常,将返回至用于提取数据的文件对象。为了在本例所用数据集中调用该函数,我们执行了下面的代码:
url = 'http://mattmahoney.net/dc/'
filename = maybe_download('text8.zip', url, 31344016)
接下来我们要做的是取用指向已下载文件的文件对象,并使用 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 函数,以确保文本保存为字符串数据类型。最后,我们使用 split()函数创建一个列表,该列表包含文本文件中所有的单词,并用空格字符分隔。我们可以在这里看到一些输出:
vocabulary = read_data(filename)print(vocabulary[:7])['anarchism', 'originated', 'as', 'a', 'term', 'of', 'abuse']
如我们所见,返回的词汇数据包含一个清晰的单词列表,将其按照原始文本文件的句子排序。现在我们已经提取了所有的单词并置入列表,需要对其进行进一步的处理以创建 skip-gram 批量数据。处理步骤如下:
1. 提取前 10000 个最常用的单词,置入嵌入向量;
2. 汇集所有单独的单词,并用唯一的整数对它们进行索引——这一步等同于为单词创建独热码。我们将使用一个字典来完成这一步;
3. 循环遍历数据集中的每个单词(词汇变量),并将其分配给在步骤 2 中创建的独一无二的整数。这使在单词数据流中进行查找或处理操作变得更加容易。
实现上述行为的代码如下所示:
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 = 0for 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
第一步是设置一个「计数器」列表,该列表中存储在数据集中找到一个单词的次数。由于我们的词汇量仅限于 10,000 个单词,因此,不包括在前 10,000 个最常用单词中的任何单词都将标记为「UNK」,表示「未知」。然后使用 Python 集合模块和 Counter()类以及关联的 most_common()函数对已初始化的计数列表进行扩展。这些设置用于计算给定参数(单词)中的单词数量,然后以列表格式返回 n 个最常见的单词。
该函数的下一部分创建了一个字典,名为 dictionary,该字典由关键词进行填充,而这些关键词与每个独一无二的词相对应。分配给每个独一无二的关键词的值只是简单地将字典的大小以整数形式进行递增。例如,将 1 赋值给第一常用的单词,2 赋值给第二常用的词,3 赋值给第三常用的词,依此类推(整数 0 被分配给「UNK」词)。这一步给词汇表中的每个单词赋予了唯一的整数值——完成上述过程的第二步。
接下来,该函数将对数据集中的每个单词进行循环遍历——该数据集是由 read_data()函数输出的。经过这一步,我们创建了一个叫做「data」的列表,该列表长度与单词量相同。但该列表不是由独立单词组成的单词列表,而是个整数列表——在字典里由分配给该单词的唯一整数表示每一个单词。因此,对于数据集的第一个句子 [『anarchism』, 『originated』, 『as』, 『a』, 『term』, 『of』, 『abuse』],现在在数据变量中是这样的:[5242,3083,12,6,195,2,3136]。这解决了上述第三步。
最后,该函数创建了一个名为 reverse_dictionary 的字典,它允许我们根据其唯一的整数标识符来查找单词,而非根据单词查找标识符。
建立数据的最后一点在于,现在要创建一个包含输入词和相关 gram 的数据集,这可用于训练 Word2Vec 嵌入系统。执行这一步操作的代码如下:
data_index = 0# generate batch datadef generate_batch(data, batch_size, num_skips, skip_window):global data_index
assert batch_size % num_skips == 0assert num_skips <= 2 * skip_window
batch = np.ndarray(shape=(batch_size), dtype=np.int32)
context = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
span = 2 * skip_window + 1 # [ skip_window input_word 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 # input word 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] # this is the input word
context[i * num_skips + j, 0] = buffer[target] # these are the context words
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, context
该函数会生成小批量数据用于我们的训练中(可在此了解小批量训练:http://adventuresinmachinelearning.com/stochastic-gradient-descent/)。这些小批量包括输入词(存储在批量中)和 gram 中随机关联的上下文单词,这些批量将作为标签对结果进行预测(存储在上下文中)。例如,在 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 的变量。然后定义其广度的大小(span size),这基本上就是我们要提取输入词和上下文的单词列表的大小。在上述例子的子句「the cat on the」中,广度是 5 = 2 * skip window + 1。此后还需创建一个缓冲区:
buffer = collections.deque(maxlen=span)for _ in range(span):
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
这个缓冲区将会最大程度地保留 span 元素,还是一种用于采样的移动窗口。每当有新的单词索引添加至缓冲区时,最左方的元素将从缓冲区中排出,以便为新的单词索引腾出空间。输入文本流中的缓冲器被存储在全局变量 data_index 中,每当缓冲器中有新的单词进入时,data_index 递增。如果到达文本流的末尾,索引更新的「%len(data)」组件会将计数重置为 0。
填写批量处理和上下文变量的代码如下所示:
for i in range(batch_size // num_skips):
target = skip_window # input word 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] # this is the input word
context[i * num_skips + j, 0] = buffer[target] # these are the context words
buffer.append(data[data_index])
data_index = (data_index + 1) % len(data)
选择的第一个词「target」是单词表最中间的词,因此这是输入词。然后从单词的 span 范围中随机选择其他单词,确保上下文中不包含输入词且每个上下文单词都是唯一的。batch 变量会反映出重复的输入词(buffer [skip_window]),这些输入词会与 context 中的每个上下文单词进行匹配。
然后返回 batch 变量和 context 变量——现在我们有了从数据集中分出批量数据的方法。我们现在可以在 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)
上面的代码从 0 到 100 中随机选择了 16 个整数——这些整数与文本数据中最常用的 100 个单词的整数索引相对应。我们将通过考察这些词语来评估相关单词与向量空间相关联的过程在我们的学习模型中进行得如何。到现在为止,我们可以建立 TensorFlow 模型了。
建立 TensorFlow 模型
接下来我将介绍在 TensorFlow 中建立 Word2Vec 词嵌入器的过程。这涉及到什么内容呢?简单地说,我们需要建立我之前提出的神经网络,该网络在 TensorFlow 中使用词嵌入矩阵作为隐藏层,还包括一个输出 softmax 层。通过训练该模型,我们将通过学习得到最好的词嵌入矩阵,因此我们将通过学习得到一个简化的、保留了上下文的单词到向量的映射。
首先要做的是设置一些稍后要用的变量——设置这些变量的目的稍后会变得清楚:
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 context.
接下来,我们设置一些 TensorFlow 占位符,这些占位符会保存输入词(的整数索引)和我们准备预测的上下文单词。我们还需要创建一个常量来保存 TensorFlow 中的验证集索引:
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)
接下来,我们需要设置嵌入矩阵变量或张量——这是使用 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.0 到 1 的随机均匀分布对变量进行初始化。变量大小包括 vocabulary_size 和 embedding_size。vocabulary_size 是上一节中用来设置数据的 10,000 个单词。这是我们输入的独热向量,在向量中仅有一个值为「1」的元素是当前的输入词,其他值都为「0」。embedding_size 是隐藏层的大小,也是新的更小的单词表示的长度。我们也考虑了可以把这个张量看作一个大的查找表——行是词汇表中的每个词,列是每个词的新的向量表示。以下一个简化的例子(使用虚拟值),其中 vocabulary_size = 7,embedding_size = 3:
正如我们所见,「anarchism」(实际上由一个整数或独热向量表示)现在表示为 [0.5,0.1,-0.1]。我们可以通过查找其整数索引、搜索嵌入行查找嵌入向量的方法「查找」anarchism:[0.5,0.1,-0.1]。
下面的代码涉及到 tf.nn.embedding_lookup()函数,在 TensorFlow 的此类任务中该函数是一个很有用的辅助函数:它取一个整数索引向量作为输入——在本例中是训练输入词的张量 train_input,并在已给的嵌入张量中「查找」这些索引。
因此,该命令将返回训练批次中每个给定输入词的当前嵌入向量。完整的嵌入张量将在训练过程中进行优化。
接下来,我们必须创建一些权重和偏差值来连接输出 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
因为权重变量连接着隐藏层和输出层,因此其大小 size(out_layer_size,hidden_layer_size)=(vocabulary_size,embedding_size)。一如以往,偏差值是一维的,且大小与输出层一致。然后,我们将嵌入变量与权重相乘(嵌入),再与偏差值相加。接下来可以做 softmax 运算,并通过交叉熵损失函数来优化模型的权值、偏差值和嵌入。我们将使用 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)
接下来,我们需要执行相似性评估以检查模型训练时的表现。为了确定哪些词彼此相似,我们需要执行某种操作来测量不同词的词嵌入向量间的「距离」。在本例中,我们计算了余弦相似度以度量不同向量间的距离。定义如下:
公式中粗体字母**A**和**B**是需要测量距离的两个向量。具有 2 个下标(|| A || 2)的双平行线是指向量的 L2 范数。为了得到向量的 L2 范数,可以将向量的每个维数(在这种情况下,n = 300,我们的嵌入向量的宽度)平方对其求和后再取平方根:
在 TensorFlow 中计算余弦相似度的最好方法是对每个向量进行归一化,如下所示:
然后,我们可以将这些归一化向量相乘得到余弦相似度。我们将之前提过的验证向量或验证词与嵌入向量中所有的单词相乘,然后我们可以将之按降序进行排列,以得到与验证词最相似的单词。
首先,我们分别使用 tf.square(),tf.reduce_sum()和 tf.sqrt()函数分别计算每个向量的 L2 范数的平方、和以及平方根:
# Compute the cosine similarity between minibatch examples and all embeddings.
norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
normalized_embeddings = embeddings / norm
然后我们就可以使用 tf.nn.embedding_lookup()函数查找之前提到的验证向量或验证词:
valid_embeddings = tf.nn.embedding_lookup(
normalized_embeddings, valid_dataset)
我们向 embedding_lookup()函数提供了一个整数列表(该列表与我们的验证词汇表相关联),该函数对 normalized_embedding 张量按行进行查找,返回一个归一化嵌入的验证集的子集。现在我们有了归一化的验证集张量 valid_embeddings,可将其嵌入完全归一化的词汇表(normalized_embedding)以完成相似性计算:
similarity = tf.matmul(
valid_embeddings, normalized_embeddings, transpose_b=True)
该操作将返回一个(validation_size, vocabulary_size)大小的张量,该张量的每一行指代一个验证词,列则指验证词和词汇表中其他词的相似度。
运行 TensorFlow 模型
下面的代码对变量进行了初始化并在训练循环中将初始化的变量馈送入每个数据批次中,每迭代 2,000 次后输出一次平均损失值。如果在这段代码中有不能理解的地方,请查看我的 TensorFlow 教程。
with tf.Session(graph=graph) as session:# We must initialize all variables before we use them.
init.run()print('Initialized')
average_loss = 0for step in range(num_steps):
batch_inputs, batch_context = generate_batch(data,
batch_size, num_skips, skip_window)
feed_dict = {train_inputs: batch_inputs, train_context: batch_context}# We perform one update step by evaluating the optimizer op (including it# in the list of returned values for session.run()
_, loss_val = session.run([optimizer, cross_entropy], feed_dict=feed_dict)
average_loss += loss_val
if step % 2000 == 0:if step > 0:
average_loss /= 2000# The average loss is an estimate of the loss over the last 2000 batches.print('Average loss at step ', step, ': ', average_loss)
average_loss = 0
接下来,我们想要输出与验证词相似程度最高的单词——这一步需要通过调用上面定义的相似性运算以及对结果进行排序来达成(注意,由于计算量大,因此每迭代 10,000 次执行一次该操作):
# Note that this is expensive (~20% slowdown if computed every 500 steps)if step % 10000 == 0:
sim = similarity.eval()for i in range(valid_size):
valid_word = reverse_dictionary[valid_examples[i]]
top_k = 8 # number of nearest neighbors
nearest = (-sim[i, :]).argsort()[1:top_k + 1]
log_str = 'Nearest to %s:' % valid_word
for k in range(top_k):
close_word = reverse_dictionary[nearest[k]]
log_str = '%s %s,' % (log_str, close_word)print(log_str)
该函数首先计算相似性,即给每个验证词返回一组余弦相似度的值。然后我们遍历验证集中的每一个词,使用 argsort()函数输入相似度的负值,取前 8 个最接近的词并按降序进行排列。打印出这 8 个词的代码,我们就可以看到嵌入过程是如何执行的了。
最后,在完成所有的训练过程的所有迭代之后,我们可以将最终的嵌入结果定为一个单独的张量供以后使用(比如其他深度学习或机器学习过程):
final_embeddings = normalized_embeddings.eval()
现在我们完成了——真的完成了吗?Word2Vec 的这个 softmax 方法的代码被放在了 Github 上——你可以试着运行它,但我并不推荐。为什么?因为它真的很慢。
提速——「真正的」Word2Vec 方法
事实上,使用 softmax 进行评估和更新一个有 10,000 词的输出或词汇表的权值是非常慢的。我们从 softmax 的定义考虑:
在我们正在处理的内容中,softmax 函数将预测哪些词在输入词的上下文中具有最高的可能性。为了确定这个概率,softmax 函数的分母必须评估词汇表中所有可能的上下文单词。因此,我们需要 300 * 10,000 = 3M 的权重,所有这些权重都需要针对 softmax 输出进行训练。这会降低速度。
NCE(Noise Contrastive Estimation,噪声对比估计,http://papers.nips.cc/paper/5021-distributed-representations-of-words-and-phrases-and-their-compositionality.pdf)的速度更快,可以作为替代方案。这个方法不是用上下文单词相对于词汇表中所有可能的上下文单词的概率,而是随机抽样 2-20 个可能的上下文单词,并仅从这些单词中评估概率。在此不对细节进行描述,但可以肯定的是,该方法可用于训练模型,且可大大加快训练进程。
TensorFlow 已经在此帮助过我们,并为我们提供了 NCE 损失函数,即 tf.nn.nce_loss()。我们可以将权重和偏差变量输入 tf.nn.nce_loss()。使用该函数和 NCE,迭代 100 次的时间从 softmax 的 25 秒减少到不到 1 秒。用以下内容替换 softmax:
# Construct the variables for the NCE loss
nce_weights = tf.Variable(
tf.truncated_normal([vocabulary_size, embedding_size],
stddev=1.0 / math.sqrt(embedding_size)))
nce_biases = tf.Variable(tf.zeros([vocabulary_size]))
nce_loss = tf.reduce_mean(
tf.nn.nce_loss(weights=nce_weights,
biases=nce_biases,
labels=train_context,
inputs=embed,
num_sampled=num_sampled,
num_classes=vocabulary_size))
optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(nce_loss)
现在我们可以运行代码了。如上所述,每迭代 10,000 次代码输出验证词和 Word2Vec 系统得出的相似词。您可以在下面看到随机初始化和 50,000 次迭代标记之间的某些选定验证词的改进:
开始:
最接近 nine 的词:heterosexual, scholarly, scandal, serves, humor, realized, cave, himself
最接近 this 的词:contains, alter, numerous, harmonica, nickname, ghana, bogart, Marxist
迭代 10,000 次后:
最接近 nine 的词:zero, one, and, coke, in, UNK, the, jpg
最接近 this 的词:the, a, UNK, killing, meter, afghanistan, ada, Indiana
50,000 次迭代后的最终结果:
最接近 nine 的词:eight, one, zero, seven, six, two, five, three
最接近 this 的词:that, the, a, UNK, one, it, he, an
通过查看上面的输出,我们可以首先看到「nine」这个词与其他数字的关联性越来越强(「eight」,「one」,「seven」等)这是有一定道理的。随着迭代次数的增加,「this」这个词在句子中起到代词和定冠词的作用,与其他代词(「he」,「it」)和其他定冠词(「the」,「that」等)关联在一起。
总而言之,我们已经学会了如何使用 Word2Vec 方法将大的独热单词向量减少为小得多的词嵌入向量,这些向量保留了原始单词的上下文和含义。这些词嵌入向量可以作为构建自然语言模型的深度学习技术的更加高效和有效的输入。诸如循环神经网络这样的深度学习技术,将在未来占据主要地位。
原文链接:http://adventuresinmachinelearning.com/word2vec-tutorial-tensorflow/