在本教程中,我们看一下Mikolov等人的word2vec模型,该模型用于学习单词的向量表示,称为”word embeddings”。
画重点
- 我们首先给出了为什么我们想要将单词表示为向量的动机。
- 我们观察模型背后的直觉以及如何训练(用数学方法来衡量)。
- 我们还在TensorFlow中展示了一个简单的模型实现。
- 最后,我们看看如何让初级版本更好地扩展。
在本教程中我们稍后介绍代码,但是如果您希望直接进入,请随时查看简单的实现tensorflow/examples/tutorials/word2vec/word2vec_basic.py 这个基本的例子包含了下载一些数据所需的代码,对它进行一些训练并将结果可视化。一旦你阅读和运行了基本版本,你可以毕业了models/tutorials/embedding/word2vec.py这是一个更严格的实现,展示了一些更高级的TensorFlow原理,如何有效地使用线程将数据移动到文本模型中,如何在训练过程中设置检查点等。
但首先,我们来看看为什么我们想要学习单词嵌入(word embedding)。如果你是一个Embedding专家,你可以跳过这个部分。
动机:为什么学习Word嵌入?
图像和音频处理系统在大量且高维的数据集一起工作,该高维数据集被编码为用于图像数据的各个原始像素向量,或者例如音频数据的功率谱密度系数。对于像图像物体或语音识别这样的任务,我们知道成功执行任务所需的所有信息都被编码在数据中(因为人类可以从原始数据执行这些任务)。然而,自然语言处理系统传统上把单词当作离散的原子符号,因此’cat’可以表示为Id537
,而’dog’为Id143
。这些编码是任意的,并且不提供关于各个符号之间可能存在的关系。这意味着模型在处理有关’dogs'(例如动物,four-legged,宠物等)的数据时不能充分利用’cats’的相关知识。将单词表示为唯一的离散标识还会导致数据稀疏,通常意味着我们可能需要更多数据才能成功地训练统计模型。使用矢量表示可以部分克服这些障碍。
向量空间模型(VSM)将单词表示为(嵌入)到连续向量空间,其中语义上相似的单词被映射到附近的点(‘彼此嵌入在一起’)。 VSM在NLP中有着悠久而丰富的历史,但是所有的方法都依赖于某种方式分布假设,其中指出,出现在相同语境中的词语具有相同语义意义。利用这一原则的不同方法可以分为两类:count-based方法(例如。潜在的语义分析)和预测方法(例如。神经概率语言模型)。
这个区别更详细的阐述参考Baroni等人,但简而言之:Count-based方法计算一个词与其相邻词在一个大文本语料库中共现频率的统计,然后将每个词的这些count-statistics映射到小而密集向量。预测模型直接从邻居单词预测当前词,从而尝试学习小而密嵌入向量(考虑模型的参数)。
Word2vec是一个特别的可高效计算的预测模型,用于从原始文本中学习单词嵌入。它有两种方式,连续Bag-of-Words模型(CBOW)和Skip-Gram模型(第3.1和3.2节Mikolov等人)。在算法上,这些模型是类似的,除了CBOW从源上下文词(‘cat坐在’)预测目标词(例如’mat’),而skip-gram做相反的并且从目标词预测上下文单词。这种倒置可能看起来像是一种任意的选择,但从统计上来看,CBOW具有平滑许多分布信息的效果(通过将整个上下文视为一个观察)。大部分情况下,这对于较小的数据集是一个有用的东西。但是,skip-gram将每个上下文目标对视为一个新的观测值,而当我们有更大的数据集时,这往往会更好。我们将在本教程的其余部分重点介绍skip-gram模型。
扩大Noise-Contrastive训练
神经概率语言模型传统上使用的训练依据是最大似然(ML)的原则,以最大化给出前面的单词h(对于”history”)时下一个单词wt的概率(对于”target”),采用的方式是SOFTMAX函数,
其中score(wt, h)计算word wt与上下文h的兼容性(通常使用点积)。我们通过最大化log-likelihood来训练这个模型。在训练集上,即通过最大化
这产生了一个适当的规范化的语言建模概率模型。然而,这是非常昂贵的,因为在每一个训练阶段, 我们需要使用当前上下文h中的所有其他V个词w’的分数来计算和归一化每个概率。
另一方面,对于word2vec中的特征学习,我们不需要完全的概率模型。 CBOW和skip-gram模型是使用二元分类目标(逻辑回归),在相同的上下文中区分来自k虚数(噪声)单词和实际目标单词wt。我们在下面对CBOW模型进行说明。对于skip-gram,方向是简单的倒转。
在数学上,目标(对于每个示例)是最大化的
其中Qθ(D = 1 | w, h)是数据集D中上下文h,根据学习的嵌入向量θ计算。在实践中,我们通过从噪声分布中取出k对比词来近似期望(即,我们计算a蒙特卡洛平均)。
当模型将高概率分配给真实的词时,这个目标是最大化的,对噪声词的概率低。从技术上讲,这被称为负采样,并且使用这个损失函数有很好的数学动机:它提出的更新近似于极限情况下softmax函数的更新。并且从计算角度来看,这是非常有吸引力的,因为计算损失函数现在只能随着噪音词数量的变化而变化,即我们选择的(k),而不是词汇(V)表中的所有单词。这使得训练要快得多。实际上我们会利用非常相似的noise-contrastive估计(NCE)损失,为此TensorFlow有一个方便的帮手功能tf.nn.nce_loss()
。
让我们直观地感受一下,在实践中这将如何工作!
Skip-gram模型
作为一个例子,我们来考虑一下数据集
the quick brown fox jumped over the lazy dog
我们首先构造一个数据集,它包括单词及其出现的上下文。我们可以用任何有意义的方式定义上下文’context’,事实上人们已经研究过句法上下文(即当前目标单词的句法依赖,参见例如Levy等人),目标左边的单词,目标右边的单词,等等。让我们锁定在普通定义上,界定’context’为目标单词上的一个窗口,它包含左边和右边的单词。使用1的窗口大小,我们有数据集
([the, brown], quick), ([quick, fox], brown), ([brown, jumped], fox), ...
的(context, target)
对。回想一下,skip-gram颠倒上下文和目标,并试图从目标字中预测每个上下文字,所以任务变为预测来自’quick’的’the’和’brown’,来自’brown’的’quick’和’fox’等。因此,我们的数据集变成
(quick, the), (quick, brown), (brown, quick), (brown, fox), ...
的(input, output)
对。目标函数是在整个数据集上定义的,但是我们通常使用随机梯度下降(SGD)对这个函数进行优化,一次使用一个示例(或’minibatch’,batch_size
例子,典型的16 <= batch_size <= 512
)。所以让我们来看看这个过程的一个步骤。
让我们想象在训练步骤t我们观察上面的第一个训练案例,目标是从quick
预测the
。我们选择通过从一些噪声分布(通常是单字符分布)中提取噪声(对比)样本,数量为num_noise
。(P(w))。为了简单,让我们设置num_noise=1
我们选择sheep
作为一个噪声样本。接下来,我们计算这对观察到的和有噪声的例子的损失,即在时间步骤t处的目标变成
目标是对嵌入参数θ进行更新以改进(在这种情况下,最大化)这个目标函数。我们通过推导相对于嵌入参数的损失梯度来实现,即(Tensorflow有简单的辅助功能来做到这一点!)。然后,我们通过向梯度方向迈出一小步来更新嵌入。当这个过程在整个训练集上重复时,这将对每个单词产生移动(‘moving’)嵌入向量的效果,直到模型成功识别真实单词与噪声单词为止。
我们可以通过使用例如t-SNE降维技术类似的东西,将它们投影到2维来可视化所学习的向量。当我们检查这些可视化时,显然这些向量捕捉到了一些一般的,实际上相当有用的,关于单词的语义信息及相互之间的关系。非常有趣的是,向量空间中的某些方向专注于特定的语义关系。例如,男性-女性,动词时态,乃至国家-首都,如下图所示(另见例如Mikolov等,2013)。
这解释了为什么这些矢量也可以用作许多典型NLP预测任务的特征,例如词类标记或命名实体识别(例如,参见Collobert等人,2011(PDF格式),或follow-up工作Turian等人,2010)。
但现在,让我们用它们来绘制美丽的图画!
建立图表
首先,来定义我们的embedding矩阵。作为开始,这只是一个很大的随机矩阵,我们将使用均匀分布初始化单位立方体中的值。
embeddings = tf.Variable(
tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
noise-contrastive评估损失是根据逻辑回归模型定义的。为此,我们需要为词汇中的每个单词定义权重和偏差
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]))
现在我们已经有了参数,我们可以定义我们的skip-gram模型图。为了简单起见,假设我们已经将文本语料库与词汇表进行了整合,以便将每个词表示为一个整数(参见tensorflow/examples/tutorials/word2vec/word2vec_basic.py的细节)。 skip-gram模型需要两个输入。一个是一批代表源语境词汇的整数,另一个是针对目标词汇的整数。让我们为这些输入创建占位符节点,以便稍后输入数据。
# Placeholders for inputs
train_inputs = tf.placeholder(tf.int32, shape=[batch_size])
train_labels = tf.placeholder(tf.int32, shape=[batch_size, 1])
现在我们需要做的是查找batch中每个源词的向量。 TensorFlow有方便的帮手,使这个实现起来很简单。
embed = tf.nn.embedding_lookup(embeddings, train_inputs)
好的,现在我们已经为每个单词Embedding了,我们想用noise-contrastive训练目标来预测目标单词。
# Compute the NCE loss, using a sample of the negative labels each time.
loss = tf.reduce_mean(
tf.nn.nce_loss(weights=nce_weights,
biases=nce_biases,
labels=train_labels,
inputs=embed,
num_sampled=num_sampled,
num_classes=vocabulary_size))
现在我们有一个损失节点,我们需要添加计算梯度所需的节点并更新参数等等。为此,我们将使用随机梯度下降,并且TensorFlow也有方便的帮助器来使这一点变得容易。
# We use the SGD optimizer.
optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0).minimize(loss)
训练模型
训练模型就是简单地使用feed_dict
将数据推送到占位符和调用tf.Session.run
。
for inputs, labels in generate_batch(...):
feed_dict = {train_inputs: inputs, train_labels: labels}
_, cur_loss = session.run([optimizer, loss], feed_dict=feed_dict)
请参阅完整的示例代码tensorflow /示例/教程/word2vec /word2vec_basic.py。
Embedding的可视化
训练完成后,我们可以使用t-SNE可视化已学习的Embedding。
瞧!如预期的那样,相似的单词最终彼此聚集在一起。对于展示更多TensorFlow高级功能的更重量级的word2vec实现,请参阅models/tutorials/embedding/word2vec.py。
评估Embedding:类比推理(Analogical Reasoning)
Embedding对于NLP中的各种预测任务是有用的。在训练full-blown词类模型或命名实体模型之后,评估Embedding的一种简单方法是直接使用它们来预测句法和语义关系king is to queen as father is to ?
。这就是所谓的类比推理,此任务是由Mikolov和同事引入的。可从下载该任务的数据集。
要知道如何做这个评估,看看build_eval_graph()
和eval()
函数,它们在models/tutorials/embedding/word2vec.py.。
超参数的选择可以强烈影响此任务的准确性。为了在这个任务上实现state-of-the-art性能,需要对一个非常大的数据集进行训练,仔细调整超参数并利用诸如子采样数据这样的技巧,这超出了本教程的范围。
优化实现
我们的普通实施展示了TensorFlow的灵活性。例如,改变训练目标就像把调用tf.nn.nce_loss()
换成现成的tf.nn.sampled_softmax_loss()
一样简单。如果您对损失函数有新的想法,则可以在TensorFlow中手动为新目标编写一个表达式,然后让优化器计算其导数。这种灵活性在机器学习模型开发的探索阶段是非常有价值的,在这个阶段我们正在尝试几个不同的想法并且快速迭代。
一旦你有一个满意的模型结构,就可以考虑优化你的实现从而更有效地运行(在更短的时间内覆盖更多的数据)。例如,我们在本教程中使用的朴素代码将会有不利影响,因为我们使用Python来读取和提供数据项 – 而这在TensorFlow后端上只需要很少的工作。如果您发现您的模型对输入数据有严重的瓶颈,您可能需要为您的问题实现自定义数据读取器,如新的数据格式。对于Skip-Gram建模的例子,我们实际上已经为你做了这个例子 models/tutorials/embedding/word2vec.py。
如果您的模型不再受I/O限制,但您仍希望获得更高的性能,则可以通过编写自己的TensorFlow Ops来进一步处理,如添加一个新的操作。我们再次为Skip-Gram案例提供了一个例子models/tutorials/embedding/word2vec_optimized.py。可以随意对这些对方进行基准测试,以衡量每个阶段的性能改进。
结论
在本教程中,我们介绍了word2vec模型,这是一个用于学习单词嵌入的高效计算模型。我们激发了为什么嵌入是有用的,讨论了高效的训练技术,并展示了如何在TensorFlow中实现所有这些功能。