手把手教你NumPy来实现Word2vec

Word2Vec被认为是自然语言处理(NLP)领域中最大、最新的突破之一。其的概念简单,优雅,(相对)容易掌握。Google一下就会找到一堆关于如何使用诸如Gensim和TensorFlow的库来调用Word2Vec方法的结果。另外,对于那些好奇心强的人,可以查看Tomas Mikolov基于C语言的原始实现。原稿也可以在这里找到。

 

本文的主要重点是详细介绍Word2Vec。为此,我在Python上使用Numpy(在其他教程的帮助下)实现了Word2Vec,还准备了一个Google Sheet来展示计算结果。以下是代码和Google Sheet的链接。

 

 

图1.一步一步来介绍Word2Vec。由代码和Google Sheet呈现

 

    直观上看

 

Word2Vec的目标是生成带有语义的单词的向量表示,用于进一步的NLP任务。每个单词向量通常有几百个维度,语料库中每个唯一的单词在空间中被分配一个向量。例如,单词“happy”可以表示为4维向量[0.24、0.45、0.11、0.49],“sad”具有向量[0.88、0.78、0.45、0.91]。

 

这种从单词到向量的转换也被称为单词嵌入(word embedding)。这种转换的原因是机器学习算法可以对数字(在向量中的)而不是单词进行线性代数运算。

 

为了实现Word2Vec,有两种风格可以选择,Continuous Bag-of-Words(CBOW)或Skip-gram(SG)。简单来说,CBOW尝试从相邻单词(上下文单词)猜测输出(目标单词),而Skip-Gram从目标单词猜测上下文单词。实际上,Word2Vec是基于分布假说,其认为每个单词的上下文都在其附近的单词中。因此,通过查看它的相邻单词我们可以尝试对目标单词进行预测。

 

根据Mikolov(引用于这篇文章),以下是Skip-gram和CBOW之间的区别:

 

Skip-gram:能够很好地处理少量的训练数据,而且能够很好地表示不常见的单词或短语

 

CBOW:比skip-gram训练快几倍,对出现频率高的单词的准确度稍微更好一些

 

更详细地说,由于Skip-gram学习用给定单词来预测上下文单词,所以万一两个单词(一个出现频率较低,另一个出现频率较高)放在一起,那幺当最小化loss值时,两个单词将进行有相同的处理,因为每个单词都将被当作目标单词和上下文单词。与CBOW相比,不常见的单词将只是用于预测目标单词的上下文单词集合的一部分。因此,该模型将给不常现的单词分配一个低概率。

 

 

图2—Word2Vec—CBOW和skip-gram模型架构。感谢:IDIL

 

    实现过程

 

在本文中,我们将实现Skip-gram体系结构。为了便于阅读,内容分为以下几个部分:

 

1.数据准备——定义语料库、整理、规范化和分词

 

2.超参数——学习率、训练次数、窗口尺寸、嵌入(embedding)尺寸

 

3.生成训练数据——建立词汇表,对单词进行one-hot编码,建立将id映射到单词的字典,以及单词映射到id的字典

 

4.模型训练——通过正向传递编码过的单词,计算错误率,使用反向传播调整权重和计算loss值

 

5.结论——获取词向量,并找到相似的词

 

6.进一步的改进 —— 利用Skip-gram负采样(Negative Sampling)和Hierarchical Softmax提高训练速度

 

1.数据准备

 

首先,我们从以下语料库开始:

 

natural language processing and machine learning is fun and exciting

 

简单起见,我们选择了一个没有标点和大写的橘子。而且,我们没有删除停用词“and”和“is”。

 

实际上,文本数据是非结构化的,甚至可能很“很不干净”清理它们涉及一些步骤,例如删除停用词、标点符号、将文本转换为小写(实际上取决于你的实际例子)和替换数字等。KDnuggets 上有一篇关于这个步骤很棒的文章。另外,Gensim也提供了执行简单文本预处理的函数——gensim.utils.simple_preprocess,它将文档转换为由小写的词语(Tokens )组成的列表,并忽略太短或过长的词语。

 

 

在预处理之后,我们开始对语料库进行分词。我们按照单词间的空格对我们的语料库进行分词,结果得到一个单词列表:

 

[“natural”, “language”, “processing”, “ and”, “ machine”, “ learning”, “ is”, “ fun”, “and”, “ exciting”]

 

2.超参数

 

在进入word2vec的实现之前,让我们先定义一些稍后需要用到的超参数。

 

 

[window_size/窗口尺寸]:如之前所述,上下文单词是与目标单词相邻的单词。但是,这些词应该有多远或多近才能被认为是相邻的呢?这里我们将窗口尺寸定义为2,这意味着目标单词的左边和右边最近的2个单词被视为上下文单词。参见下面的图3,可以看到,当窗口滑动时,语料库中的每个单词都会成为一个目标单词。

 

 

图3,在window_size为2的情况下,目标单词用橙色高亮显示,上下文单词用绿色高亮显示

 

[n]:这是单词嵌入(word embedding)的维度,通常其的大小通常从100到300不等,取决于词汇库的大小。超过300维度会导致效益递减(参见图2(a)的1538页)。请注意,维度也是隐藏层的大小。

 

[epochs]  :表示遍历整个样本的次数。在每个epoch中,我们循环通过一遍训练集的样本。

 

[learning_rate/学习率]:学习率控制着损失梯度对权重进行调整的量。

 

3.生成训练数据

 

在本节中,我们的主要目标是将语料库转换one-hot编码表示,以方便Word2vec模型用来训练。从我们的语料库中,图4中显示了10个窗口(#1到#10)中的每一个。每个窗口都由目标单词及其上下文单词组成,分别用橙色和绿色高亮显示。

 

 

图4,每个目标单词及其上下文单词的one hot编码

 

第一个和最后一个训练窗口中的第一个和最后一个元素的示例如下所示:

 

# 1 [目标单词(natural)], [上下文单词 (language, processing)][list([1, 0, 0, 0, 0, 0, 0, 0, 0])

 

list([[0, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 1, 0, 0, 0, 0, 0, 0]])]

 

*****#2 to #9 省略****#10

 

[ 目标单词   (exciting)], [ 上下文单词  (fun, and)]

 

[list([0, 0, 0, 0, 0, 0, 0, 0, 1])

 

list([[0, 0, 0, 0, 0, 0, 0, 1, 0], [0, 0, 0, 1, 0, 0, 0, 0, 0]])]

 

为了生成one-hot训练数据,我们首先初始化word2vec()对象,然后使用对象w2v通过settings 和corpus 参数来调用函数generate_training_data。

 

 

在函数generate_training_data内部,我们进行以下操作:

 

 

self.v_count: 词汇表的长度(注意,词汇表指的就是语料库中不重复的单词的数量)

 

self.words_list: 在词汇表中的单词组成的列表

 

self.word_index: 以词汇表中单词为key,索引为value的字典数据

 

self.index_word: 以索引为key,以词汇表中单词为value的字典数据

 

for循环给用one-hot表示的每个目标词和其的上下文词添加到training_data中,one-hot编码用的是word2onehot函数。

 

 

 

 

4.模型训练

 

 

图5,Word2Vec——skip-gram的网络结构

 

拥有了training_data,我们现在可以准备训练模型了。训练从w2v.train(training_data)开始,我们传入训练数据,并执行train函数。

 

Word2Vec2模型有两个权重矩阵(w1和w2),为了展示,我们把值初始化到形状分别为(9×10)和(10×9)的矩阵。这便于反向传播误差的计算,这部分将在后文讨论。在实际的训练中,你应该随机初始化这些权重(比如使用np.random.uniform())。想要这幺做,把第九第十行注释掉,把11和12行取消注释就好。

 

 

训练——向前传递

 

接下来,我们开始用第一组训练样本来训练第一个epoch,方法是把w_t 传入forward_pass 函数,w_t 是表示目标词的one-hot向量。在forward_pass 函数中,我们执行一个w1 和w_t 的点乘积,得到h (原文是24行,但图中实际是第22行)。然后我们执行w2和h 点乘积,得到输出层的u( 原文是26行,但图中实际是第24行  )。最后,在返回预测向量y_pred和隐藏层h 和输出层u 前,我们使用softmax把u 的每个元素的值映射到0和1之间来得到用来预测的概率(第28行)。

 

 

我附上一些截图展示第一窗口(#1)中第一个训练样本的计算,其中目标词是“natural”,上下文单词是“language”和“processing”。可以在这里查看Google Sheet中的公式。

 

 

图6,计算隐藏层,输出层和softmax

 

训练——误差,反向传播和损失(loss)

 

误差——对于y_pred、h 和u,我们继续计算这组特定的目标词和上下文词的误差。这是通过对y_pred 与在w_c 中的每个上下文词之间的差的加合来实现的。

 

 

图7,计算误差——上下文单词是“language”和“processing”

 

反向传播——接下来,我们使用反向传播函数backprop ,通过传入误差EI 、隐藏层h 和目标字w_t 的向量,来计算我们所需的权重调整量。

 

为了更新权重,我们将权重的调整量(dl_dw1 和dl_dw2 )与学习率相乘,然后从当前权重(w1 和w2 )中减去它。

 

 

图8,反向传播——计算W1和W2的增量

 

 

图9,反向传播——调整权重以得到更新后的W1和W2

 

 

损失——最后,根据损失函数计算出每个训练样本完成后的总损失。注意,损失函数包括两个部分。第一部分是输出层(在softmax之前)中所有元素的和的负数。第二部分是上下文单词的数量乘以在输出层中所有元素(在 exp之后)之和的对数。

 

 

图10,Skip-gram的损失函数。

 

引用至:https://arxiv.org/pdf/1411.2738.pdf

 

5. 推论和总结(Inferencing)

 

既然我们已经完成了50个epoch的训练,两个权重(w1和w2)现在都准备好执行推论了。

 

获取单词的向量

 

有了一组训练后的权重,我们可以做的第一件事是查看词汇表中单词的词向量。我们可以简单地通过查找单词的索引来对训练后的权重(w1)进行查找。在下面的示例中,我们查找单词“machine”的向量。

 

 

> print(w2v.word_vec("machine"))
[ 0.76702922 -0.95673743  0.49207258  0.16240808 -0.4538815  
-0.74678226  0.42072706 -0.04147312  0.08947326 -0.24245257]

 

查询相似的单词

 

我们可以做的另一件事就是找到类似的单词。即使我们的词汇量很小,我们仍然可以通过计算单词之间的余弦相似度来实现函数vec_sim 。

 

 

> w2v.vec_sim("machine", 3)
machine 1.0
fun 0.6223490454018772
and 0.5190154215400249

 

6.进一步改进

 

如果你还在读这篇文章,做得好,谢谢!但这还没结束。正如你在上面的反向传播步骤中可能已经注意到的,我们需要调整训练样本中没有涉及的所有其他单词的权重。如果词汇量很大(例如数万),这个过程可能需要很长时间。

 

为了解决这个问题,您可以在Word2Vec中实现以下两个特性,以加快速度:

 

Skip-gram Negative Sampling (SGNS)  有助于加快训练时间,提高最终的词向量的质量。这是通过训练网络只修改一小部分的权重而不是全部的权重来实现。回想一下上面的示例,我们对每一个词的权重都进行更新,若词汇库的尺寸很大,这可能需要很长时间。对于SGNS,我们只需要更新目标词和少量(例如,5到20)随机“否定”单词的权重。

 

Hierarchical Softmax是用来替换原始softmax加速训练的另一个技巧。其主要思想是,不需要对所有输出节点进行评估来获得概率分布,只需要评估它的对数个数(基为2)。使用二叉树(Huffman编码树)表示,其中输出层中的节点表示为叶子,其节点由与其子节点的相应的概率表示。

 

 

图11,Hierarchical二叉树,被高亮的为从根到W2的路径

 

除此之外,为什幺不尝试调整代码来实现Continuous Bag-of-Words(Continuous Bag-of-Words,CBOW)构架呢??

 

    结论

 

本文是对Word2Vec的介绍,并解除了单词嵌入(word embedding)的世界。另外还值得注意的是,有预训练的嵌入可用,如GloVe、fastText和ELMo,你可以直接下载和使用。此外还有Word2Vec的扩展,如Doc2Vec和最近的Code2Vec,在这俩方法中文档和代码被转换成向量。

 

最后,我要感谢Ren Jie Tan、Raimi 和Yuxin抽出时间来阅读和评论本文的草稿。

 

    参考

 

nathanrooy/word2vec-from-scratch-with-python
A very simple, bare-bones, inefficient, implementation of skip-gram word2vec from scratch with Python …github.com

 

Word2vec from Scratch with Python and NumPy
TL;DR – word2vec is awesome, it’s also really simple. Learn how it works, and implement your own version. Since joining…nathanrooy.github.io

 

Why word2vec maximizes the cosine similarity between semantically similar words
Thanks for contributing an answer to Cross Validated! Some of your past answers have not been well-received, and you’re…stats.stackexchange.com

 

Hierarchical softmax and negative sampling: short notes worth telling
Thanks to unexpected and very pleasant attention the audience has paid to my last (and the only) post here dedicated to…towardsdatascience.com

 

感谢 Ren Jie Tan 和 Raimi Bin Karim.

原标题 :An implementation guide to Word2Vec using NumPy and Google Sheets

 

作者 |  Derek Chia 翻译 | mui 校对 | 酱番梨       整理 | 菠萝妹

 

原文链接:https://medium.com/@derekchia/an-implementation-guide-to-word2vec-using-numpy-and-google-sheets-13445eebd281

发表评论

电子邮件地址不会被公开。 必填项已用*标注