Word2Vec是一组用来产生词嵌入的模型,包括两种主要的模型:skip-gram和CBOW。
Skip-gram vs CBOW 算法层面上, 两种模型很相似,CBOW模型是从两边预测中心词,skip-gram模型是中心词预测两边。比如,对于“The quick brown fox jumps”,CBOW模型从上下文"the, “quick”, “fox”, 和"jumps"预测中心词“brown”,skip-gram模型从“brown”预测上下文"the, “quick”, “fox”, 和"jumps"。 统计层面上,CBOW模型通过将上下文看做一个样本组成,将分布信息做了平滑处理。大多数情况下,cbow模型适用于小型数据集;skip-gram模型将每个上下文-中心词对看做一个样本,用在大型数据集上表现更好。
在本文中,我们使用skip-gram模型来构建word2vec。为了得到词嵌入向量,我们需要构建一个单隐藏层的神经网络,然后用来执行特定任务来完成训练;但是训练得到的模型并不是我们需要的。我们只关注隐藏层的权重,这些权重就是词嵌入向量。
上面的特定任务是指给定中心词预测上下文。对于句子中的某个词,在词的上下文中随机选择一个词;网络模型可以输出整个词典中每个词是中心词上下文的概率。
Softmax、Negative Sampling & Noise Contrastive Estimation(NCE)
为了得到某个词的所有可能上下文词的概率分布,我们使用softmax来实现。softmax函数根据输入xix_ixi输出一个概率值pip_ipi。在这里xix_ixi表示中心词的一个可能的上下文: softmax(xi)=exp(xi)∑jexp(xj) softmax(x_i) = frac{exp(x_i)}{sum_j exp(x_j)} softmax(xi)=∑jexp(xj)exp(xi) 但是,softmax用于归一化的分母的计算需要遍历整个词典,通常情况下词典长度在百万级别,而且指数的计算也比较耗时,这就导致了softmax的计算量进一步加大。
为了规避这个计算瓶颈,我们可以使用分层softmax(hierarchical softmax)和基于采样的softmax。论文Distributed Representations of Words and Phrases and their Compositionality 指出训练skip-gram模型,和分层softmax方法相比,使用negative sampling的方法训练速度更快,得到的词向量更好。
Negative Sampling(负采样)是基于采样方法的一种。基于采样的方法也包括重要性采样(importance sampling)和目标采样(target sampling)。负采样方法是NCE的简化版:负采样对噪声样本(负样本)的采样数量k以及噪声数据服从的分布Q做了假定,kQ(w)=1
。
负采样方法用于学习词嵌入表示,并不能保证其梯度值和softmax函数梯度值相近;而NCE方法随着负样本采样数的增加其提取值也愈来愈逼近于softmax的梯度值。Mnih and Teh(2012)表明使用25个噪声样本的计算结果与softmax的计算值差距不大,而且运算速度能加快45倍。因此,我们使用NCE来实现word2vec。
基于采样的方法,无论是负采样还是NCE方法,只适用于训练阶段;在应用阶段还需要执行softmax来得到正则化的概率结果。
数据介绍
2006年3月3日的维基百科文本的100MB数据text8。
100MB数据训练得到的词嵌入虽然表现不太好,但是从中也能看到一些有趣的现象。使用空格切分数据后,文本包括17005207个词。为了得到更好的词嵌入,需要使用更大的数据集。
Overview
使用TensorFlow实现模型,需要景观两个阶段:定义计算图以及图的运行。
阶段一:图定义
- 导入数据(tf.data 、placeholders)
- 定义权重
- 定义模型
- 定义损失函数loss
- 定义优化器
阶段二:执行运算图
- 变量初始化
- 初始化迭代器/向模型传送数据
- 执行前向计算
- 计算cost
- 梯度计算来调整模型参数
阶段一:图定义
1. 创建dataset,生成样本
skip-gram模型的输入为(中心词,上下文词)pair对。数据传送到模型之前,需要将字符串类型转换成indices表示,如果“computer”是词典中第100个单词,那么对象下标为99。
每一个样本数据是一个标量,BATCH_SIZE个输入样本的构成tensor的shape 为[BATCH_SIZE]
,输出样本的shape为[BATCH_sIZE, 1]
.
2.定义权重
在embedding矩阵中每一行表示一个词的向量表示。如果词向量长度为EMBED_SIZE,embedding矩阵的shape为[VOCAB_SIZE, EMBED_SIZE]
。
3. Inference
为了从embed_matrix
中得到对应输入的词向量表示,我们可以使用tf.nn.embedding_lookup
来实现:
这个函数相当于一个查表操作,根据输入ids在params找到对应的向量。
如果输入是one_hot表示,向量乘以矩阵可以很快地找到one_hot非零值对应的向量(one_hot中非零值为第4个,相乘后结果就是矩阵的第4行);使用相乘方法,由于one_hot表示有很多0值进而会产生许多不必要的计算;使用tf.nn.lookup
就可以节省这些不必要的计算。
为了得到中心词的embedding表示,
代码语言:javascript复制embed = tf.nn.embedding_lookup(embed_matrix, center_words, name='embed')
4. 定义损失函数
TensorFlow已经为我们实现了NCE损失函数:
代码语言:javascript复制tf.nn.nce_loss(
weights,
biases,
labels,
inputs,
num_sampled,
num_classes,
num_true=1,
sampled_values=None,
remove_accidental_hits=False,
partition_strategy='mod',
name='nce_loss'
)
为了计算NCE loss,需要定义计算loss的隐藏层权重weights和偏置biases。在训练过程中这些参数会自动更新、优化。在采样之后,最终结果的计算过程如下:
代码语言:javascript复制tf.matmul(embed, tf.transpose(nce_weight)) nce_bias
这项计算包含在tf.nn_nce_loss
的计算过程中。
nce_weight = tf.get_variable('nce_weight', shape=[VOCAB_SIZE, EMBED_SIZE], initializer=tf.truncated_normal_initializer(stddev=1.0 / (EMBED_SIZE ** 0.5)))
nce_bias = tf.get_variable('nce_bias', initializer=tf.zeros([VOCAB_SIZE]))
损失函数定义如下:
代码语言:javascript复制loss = tf.reduce_mean(tf.nn.nce_loss(weights=nce_weight,
biases=nce_bias,
labels=target_words,
inputs=embed,
num_sampled=NUM_SAMPLED,
num_classes=VOCAB_SIZE))
5. 定义优化器
代码语言:javascript复制optimizer = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(loss)
阶段二:图的执行
创建一个会话来执行优化op。
代码语言:javascript复制with tf.Session() as sess:
# 迭代器初始化
sess.run(iterator.initializer)
# 变量初始化
sess.run(tf.global_variables_initializer())
writer = tf.summary.FileWriter('graphs/word2vec_simple', sess.graph)
for index in range(NUM_TRAIN_STEPS):
try:
# 执行优化,计算loss
loss_batch, _ = sess.run([loss, optimizer])
except tf.errors.OutOfRangeError:
sess.run(iterator.initializer)
writer.close()
完整代码地址:ClickMe
Reference
Stanford CS 20: Tensorflow for Deep Learning Research