之前写过一篇博文介绍如何使用word2vec,最近老板让我讲一讲word2vec,显然光讲word2vec的使用是不够的,更重要的是介绍原理。这篇文章就写写自己对于word2vec的一些理解吧。
背景介绍
Word2vec是google在2013年开源的一款将词表征为实数值向量的高效工具,一般被认为是一个深度学习模型。Word2vec的赞誉极高,被称为2013年最重要的自然语言处理工具,相信搞NLP的没有不知道word2vec的。在我看来,Word2vec最重要的贡献是提供了一个基础,也就是把词转换为实数值向量,在这个基础上可以玩很多花样。当然,可以站在一个更高的角度来看,这里的词其实并不一定真的就是单词,完全可以是具有一定意义的单元块,比如国外音乐网站就用word2vec来训练用户的听歌记录,这里的单元块就是歌曲编号,如果用户对音乐类型的喜好是一致的,那么训练后就能找到与某个歌曲相似的歌曲,这样就能给用户进行推荐了,相信类似这样的例子还有很多。
下面是word2vec最基本的查找相近词效果:
从上面两幅图可以看出word2vec的效果还是不错。
原理介绍
Word2vec的原理主要涉及到统计语言模型(包括N-gram模型和神经网络语言模型),continuousbag-of-words模型以及continuous skip-gram模型。下面分别进行介绍:
统计语言模型
统计语言模型就是用一个机率分布来表示一段语句:
一般的语言模型可以用各个词语的条件概率表示:
每个词的出现概率与其上下文有关(确切的说是在前面出现的词)。
N-gram模型
N-gram的意思就是每个词出现只看其前面的n个词,可以对每个词出现的概率进行近似
比如当n=2的时候
其实n=2时模型一般被称为Bigram,n=3是则被称为Trigram,这两者算是最常用的模型。
N-gram的效果还是不错的,否则不会这么多人用,但是其存在一些问题:
-无法建模更远的关系。当n取值较大时计算量会大大增加,故一般只取2,3
-无法建模词之间的相似度。比如对” Thecat is walking in the bedroom.” ”A dog was running in a room.” 这两句话,无法识别出其实dog和cat是类似的,bedroom和room也是类似的,walking和running也是类似的,如果能够识别出这些相似特征,那么2*2*2就可以组合出8句话了,这将大大丰富语料,可惜N-gram做不到。
-未出现的n元组概率为0。对于没有出现过的,肯定就求不出来概率了,不过可以用平滑法(出现次数都增加1,1为基准值)或回退法(用n-1元概率模拟)来处理这种情况。
神经网络语言模型(NNLM)
为了避免N-gram模型中的那些问题,可以考虑用神经网络来构建语言模型。该模型用特征向量来表征每个词各个方面的特征,把词转换为向量后一个词就对应为特征空间中的一个点。同时,每个词的特征维度少于词表的总数,这算是NNLM的一大优势。
NNLM的基础是一个联合概率
其中g(.)就代表一个神经网络。
神经网络的目的是学习得到一个模型
并且对任意的满足
该模型可以进一步分解
-字典集合V中的词i到其向量C(i)的映射函数C,C是一个|V|*m的矩阵
-用C表示词之间的概率函数
g(.)的输出是一个向量,第i维表示。
神经网络结构如下:
神经网络中的参数为第二个参数为g函数的参数
训练的目的是最大化似然函数
输出时采用了softmax函数
最后要训练的参数为
接下来用梯度下降的方法求解theta即可
以上就是神经网络语言模型的基本思想。
Continuous Bag-of-Words模型
Continuous Bag-of-Words(CBOW)模型与NNLM类似,不同点在于CBOW去除了最耗时的非线性隐层,让所有词共享隐层,所有词都被映射到同一个位置。
结构如下
从图中能够看到CBOW是通过上下文来预测中间的词,如果窗口大小为k,则模型预测
同时CBOW采用了层次softmax算法,该算法结合了Huffman编码,每个词 w 都可以从树的根结点root沿着唯一一条路径被访问到,其路径也就形成了其编码code。假设 n(w, j)为这条路径上的第 j 个结点,且 L(w)为这条路径的长度, j 从 1 开始编码,即 n(w, 1)=root,n(w, L(w)) = w。对于第 j 个结点,层次 Softmax 定义的Label 为 1 - code[j]。
取一个适当大小的窗口当做语境,输入层读入窗口内的词,将它们的向量(K维,初始随机)加和在一起,形成隐藏层K个节点。输出层是一个巨大的二叉树,叶节点代表语料里所有的词(语料含有V个独立的词,则二叉树有|V|个叶节点)。而这整颗二叉树构建的算法就是Huffman树。这样,对于叶节点的每一个词,就会有一个全局唯一的编码,形如"010011",不妨记左子树为1,右子树为0。接下来,隐层的每一个节点都会跟二叉树的内节点有连边,于是对于二叉树的每一个内节点都会有K条连边,每条边上也会有权值。结构如下
在给定上下文时,对于一个要预测的词(这应该算是一个正样本,该词是预先知道的),这时就让预测词的二进制编码概率最大即可(采用logistic函数计算概率),例如如果一个词是“010001”,我们求解第一位为0的概率,第二位为1的概率等等。而一个词在当前网络中的概率就是从根结点到该词路径上的概率的乘积。于是就能够得到样本差值,接下来用梯度下降的方法求解参数即可。很显然,神经网络就是用正负样本不断训练,求解输出值与真实值误差,然后用梯度下降的方法求解各边权重参数值的。这里采用二叉树的方式是为了降低时间复杂度
除了层次softmax之外,CBOW还有另一种方法negative sampling,也就是随机生成负例来训练神经网络,思想差不多,这里省略。
Continuous skip-gram模型
Skip-gram模型与CBOW正好相反,是通过中间词来预测前后词,一般可以认为位置距离接近的词之间的联系要比位置距离较远的词的联系紧密。
其预测概率
目标为最大化
结构为
值得一提的是,skip-gram中的skip的是指在一个窗口内词两两之间都会计算概率,无论它们之间是否间隔词,这样“白色汽车”和“白色的汽车”会被识别为相同的短语。在skip-gram中也用到层次softmax算法和negative sampling,和CBOW模型中的类似,这里省略。
参数介绍
Word2vec中的参数有不少,我修改了一部分代码,让其在聚类时也能够输出词向量,得到的参数列表如下
其中重要的几个参数如下
–size:向量维数(一般200足够)
–window:上下文窗口大小(介于5-10之间)
–sample:高频词亚采样的阈值
–hs:是否采用层次softmax
–negative:负例数目(用于negativesampling)
–min-count:被截断的低频词阈值
–alpha:开始的 learning rate
–cbow:使用CBOW算法(0为不使用)
应用介绍
前面已经提到使用Word2vec得到的词向量是一个基础,在上面可以玩很多花样,下面介绍自己做过的几个。
同义词查找
这里是查找同义词,也就是要比词聚类中同一类中的词更接近。比如下面这几组词:
l 中国科学院 中科院
l 院所 科研单位 科研机构
l 院士 中科院院士
l 科研项目 科研工作 科研
l 两会 全国两会
l 三公 三公经费
l 中央经济工作会议 经济工作会议
l 高考加分政策 高考加分
l 公车改革 车改
l ……
可以看到大部分是简称,不过也算是同义词了。具体方法是对词先进行聚类(用word2vec自带的即可),然后进行层次聚类(更细粒度的聚类),最后就能找到意思最为接近的词对了,同义词基本都在其中。
文本聚类
对文本进行聚类时可以考虑使用词向量的特征,只需要考虑如何用词来表征文本即可,最容易想到的就是用关键词来代替文本,依据关键词聚类的结果就能够得到文本的聚类结果。关键词提取用TF-IDF,然后用word2vec训练得到关键词向量,再用k-means聚类,最后文本就能够以关键词的类别进行分类了。下面就是同一个类别中的文本
文本类别投递
对文章进行指定类别的分类也可以考虑使用词向量,首先需要有一批标记好的数据
这些词是人工标记出来的,其中包含了该词属于各个类别的概率。在这个基础上,结合词向量的特征,可以求出全体词属于各个类别的概率。具体方法是对每个词求出其在样本词表中最接近的K个词(比如十个),统计这K个词属于各个类别的概率,然后就能够得到这个词的类别信息了。下图就是以微信为例的结果
接下来,文本就可以按照其关键词的类别信息进行类别的投递了。具体参看这里:http://blog.csdn.net/zhaoxinfan/article/details/17170049
以上就是我对word2vec的一些思考,希望老板会满意吧。本文中参考了其他一些人的资料,例如有道的那篇word2vec文章,word2vec傻瓜剖析等等,很感谢他们,这里不再一一列出了。这应该算是我关于word2vec写的最后一篇文章,也可能是与NLP相关的最后一篇文章了。
最后再次感谢晓阳童鞋去年向我推荐word2vec这个工具,如今他已经是百度自然语言处理部门的研发工程师了,祝他在NLP领域取得更大的成绩。
-
看了word2vec中虽然对霍夫曼原理有所了解。但是没有找到使用霍夫曼编码的原理。在google上搜到这篇文章,感觉写的很不错,果断转了http://xiaoquanzi.net/?p=156
2013年末,Google发布的word2vec引起了一帮人的热捧,各种兴奋。时至今日,各地讨论的也不似如此频繁,也是时候写一下个人对它的理解,亦可避免被真正的有识之士鄙视。
在大量赞叹word2vec的微博或者短文中,几乎都认为它是深度学习在自然语言领域的一项了不起的应用,各种欢呼“深度学习在自然语言领域开始发力了”。但实际上,简单看看代码就知道它实际上只是一个三层网络,压根算不上所谓的深层网络,学习过程也很简单,并未用太玄妙的东西,以至于在了解完整以后对它的简单叹为观止。
笔者其实也是门外汉,幸好周围有一些高人,几经指点,自认为大体了解,作此鄙文,记录一下。
首先,它的结构就是一个三层网络——输入层、隐层(也可称为映射层),输出层。
其次,代码中让人费解(没学过神经网络,是以费解)的主要是hierarchical softmax。得同事J指导,和同事S讨论,终于弄明白其网络结果,如下图所示:输入层读入窗口内的词,将它们的向量(K维,初始随机)加和在一起,形成隐藏层K个节点。输出层是一个巨大的二叉树,叶节点代表语料里所有的词(语料含有V个独立的词,则二叉树有|V|个叶节点)。而这整颗二叉树构建的算法就是Huffman树。这样,对于叶节点的每一个词,就会有一个全局唯一的编码,形如"010011"。我们可以记左子树为1,右子树为0。接下来,隐层的每一个节点都会跟二叉树的内节点有连边,于是对于二叉树的每一个内节点都会有K条连边,每条边上也会有权值。
这样,整体的结构就清晰了。在训练阶段,当给定一个上下文,要预测后面的词(Wn)的时候(word2vec的CBOW和Skip-gram都不是预测后面的词,都是在中间的词上做文章,但是本文这么写并不影响理解),实际上我们知道要的是哪个词(Wn),而Wn是肯定存在于二叉树的叶子节点的,因此它必然有一个二进制编号,如"010011",那么接下来我们就从二叉树的根节点一个个地去便利,而这里的目标就是预测这个词的二进制编号的每一位!即对于给定的上下文,我们的目标是使得预测词的二进制编码概率最大。形象地说,我们希望在根节点,词向量和与根节点相连经过logistic计算得到的概率尽量接近0(即预测目标是bit=1);在第二层,希望其bit是1,即概率尽量接近1……这么一直下去,我们把一路上计算得到的概率相乘,即得到目标词Wn在当前网络下的概率(P(Wn)),那么对于当前这个sample的残差就是1-P(Wn)。于是就可以SGD优化各种权值了。
那么hs(hierarchical softmax)如何保证叶节点输出的概率值(即我们一路沿二进制编号乘下去的概率)是归一化的呢(否则,所谓的残差1-P(Wn)就没什么意义了)?这点其实很简单,请看下图:
从根节点开始,对于一个sample而言,目标词是W2,二进制编码是"110"。我们在根节点计算得到它的第一位是'1'的概率是P,那么它第一位是'0'的概率就是1-P;在左子树里,第二位是"1"的概率是P',那么第二位是"0"的概率就是1-P',而在右子树里,第二位是"1"的概率是P'',那么第二位是"0"的概率就是1-P'';第三位亦如此。为方便表示记,我们只写到第二层。这样,在第二层,整个概率之和就是
(P*(P') + P*(1-P')) + ((1-P)*(P'') + (1-P)*(1-P'')) = P + (1-P) = 1
即按照目标词的二进制编码计算到最后的概率值就是归一化的,这也是为啥它被称作hierarchical softmax的原因。
如果没有使用这种二叉树,而是直接从隐层直接计算每一个输出的概率——即传统的softmax,就需要对|V|中的每一个词都算一遍,这个过程时间复杂度是O(|V|)的。而使用了二叉树(如word2vec中的Huffman树),其时间复杂度就降到了O(log2(|V|)),速度大大地加快了。
不过虽然hierarchical softmax一般被认为只是用于加速,但是仍然可以感性地理解一下为啥它会奏效:二叉树里面的每一个内节点实际上是一种隐含概念的分类器(二元分类器,因为二进制编码就是0/1),它的输出值的大小预示着当前上下文能够表达该隐含概念的概率,而一个词的编码实际上是一堆隐含概念的表达(注意,这个隐含概念的表达和词向量的维度所表达的隐含概念是不一样的)。我们的目标就在于找到这些当前上下文对于这些概念分类的最准确的那个表达(即目标词向量)。由于概念之间实际上是有互斥关系的(二叉树保证),即在根节点如果是"1",即可以表达某一概念,那么该上下文是绝对不会再有表达根节点是"0"的其他情况的概念了,因此就不需要继续考虑根节点是"0"的情况了。因此,整个hierarchical softmax可以被看作完全不同于传统softmax的一套。
写到这里,感觉没有想象的那么明白。sigh,果然下笔难成书。再慢慢润色吧。
感谢同事JCH,SC,ZQQ以及软件所LSW博士的指导。
分类:版权声明:本文为博主原创文章,未经博主允许不得转载。
系列所有帖子
自己动手写word2vec (一):主要概念和流程
自己动手写word2vec (二):统计词频
自己动手写word2vec (三):构建Huffman树
自己动手写word2vec (四):CBOW和skip-gram模型
CBOW和skip-gram应该可以说算是word2vec的核心概念之一了。这一节我们就来仔细的阐述这两个模型。其实这两个模型有很多的相通之处,所以这里就以阐述CBOW模型为主,然后再阐述skip-gram与CBOW的不同之处。这一部分的代码放在pyword2vec.py文件中
1.CBOW模型
之前已经解释过,无论是CBOW模型还是skip-gram模型,都是以Huffman树作为基础的。而Huffman树的构建在前一节已经讲过咯,这里就不再重复。值得注意的是,Huffman树中非叶节点存储的中间向量的初始化值是零向量,而叶节点对应的单词的词向量是随机初始化的。
1.1 训练的流程
那么现在假设我们已经有了一个已经构造好的Huffman树,以及初始化完毕的各个向量,可以开始输入文本来进行训练了。
训练的过程如下图所示,主要有输入层(input),映射层(projection)和输出层(output)三个阶段。
输入层即为某个单词A周围的n-1个单词的词向量。如果n取5,则词A(可记为w(t))前两个和后两个的单词为w(t-2),w(t-1),w(t+1),w(t+2)。相对应的,那4个单词的词向量记为v(w(t-2)),v(w(t-1)),v(w(t+1)),v(w(t+2))。从输入层到映射层比较简单,将那n-1个词向量相加即可。而从映射层到到输出层则比较繁琐,下面单独讲
1.2 从映射层到输出层
要完成这一步骤,需要借助之前构造的Huffman树。从根节点开始,映射层的值需要沿着Huffman树不断的进行logistic分类,并且不断的修正各中间向量和词向量。
举个例子, 比如说有下图所示的Huffman树
此时中间的单词为w(t),而映射层输入为
pro(t)=v(w(t-2))+v(w(t-1))+v(w(t+1))+v(w(t+2))假设此时的单词为“足球”,即w(t)=“足球”,则其Huffman码可知为d(t)=”1001”(具体可见上一节),那么根据Huffman码可知,从根节点到叶节点的路径为“左右右左”,即从根节点开始,先往左拐,再往右拐2次,最后再左拐。
既然知道了路径,那么就按照路径从上往下依次修正路径上各节点的中间向量。在第一个节点,根据节点的中间向量Θ(t,1)和pro(t)进行Logistic分类。如果分类结果显示为0,则表示分类错误(应该向左拐,即分类到1),则要对Θ(t,1)进行修正,并记录误差量。
接下来,处理完第一个节点之后,开始处理第二个节点。方法类似,修正Θ(t,2),并累加误差量。接下来的节点都以此类推。
在处理完所有节点,达到叶节点之后,根据之前累计的误差来修正词向量v(w(t))。
这样,一个词w(t)的处理流程就结束了。如果一个文本中有N个词,则需要将上述过程在重复N遍,从w(0)~w(N-1)。
1.3 CBOW模型的伪代码描述
将模型形象化的描述过以后,还需要以更精确的方式将模型的流程确定下来。
首先,我们需要先引入一些符号以便于更清晰的表达。那么根据word2vec中的数学,流程可以表述为
其中σ表示sigmoid函数,η表示学习率。学习率越大,则判断错误的惩罚也越大,对中间向量的修正跨度也越大。
1.4 CBOW模型的代码描述
为了提高复用性,代码主要由两部分组成,分别是__Deal_Gram_CBOW和__GoAlong_Huffman。后者负责最核心部分,也就是与huffman相关的部分,前者负责剩下的功能,包括修正词向量等
def __Deal_Gram_CBOW(self,word,gram_word_list): if not self.word_dict.__contains__(word): return word_huffman = self.word_dict[word]['Huffman'] gram_vector_sum = np.zeros([1,self.vec_len]) for i in range(gram_word_list.__len__())[::-1]: item = gram_word_list[i] if self.word_dict.__contains__(item): gram_vector_sum += self.word_dict[item]['vector'] #将周围单词的词向量相加 else: gram_word_list.pop(i) if gram_word_list.__len__()==0: return e = self.__GoAlong_Huffman(word_huffman,gram_vector_sum,self.huffman.root) #与Huffman相关方法 for item in gram_word_list: self.word_dict[item]['vector'] += e self.word_dict[item]['vector'] = preprocessing.normalize(self.word_dict[item]['vector']) #修正词向量
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
def __GoAlong_Huffman(self,word_huffman,input_vector,root): node = root #从root开始 自顶向下 e = np.zeros([1,self.vec_len]) #将误差初始化为零向量 for level in range(word_huffman.__len__()): # 一层层处理 huffman_charat = word_huffman[level] # 根据Huffman码获知当前节点应该将输入分到哪一边 q = self.__Sigmoid(input_vector.dot(node.value.T)) grad = self.learn_rate * (1-int(huffman_charat)-q) # 计算当前节点的误差 e += grad * node.value # 累加误差 node.value += grad * input_vector #修正当前节点的中间向量 node.value = preprocessing.normalize(node.value) # 归一化 if huffman_charat=='0': #将当前节点切换到路径上的下一节点 node = node.right else: node = node.left return e
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
2. skip-gram模型
skip-gram与CBOW相比,只有细微的不同。skip-gram的输入是当前词的词向量,而输出是周围词的词向量。也就是说,通过当前词来预测周围的词。如下图所示
由于输出有n-1个词,所以要对于一个词来讲,上述沿着huffman树从顶到底的过程要循环n-1遍。。。其伪码描述如下其代码描述如下,与huffman有关的代码上面已经贴过了,就不再重复
def __Deal_Gram_SkipGram(self,word,gram_word_list): if not self.word_dict.__contains__(word): return word_vector = self.word_dict[word]['vector'] for i in range(gram_word_list.__len__())[::-1]: if not self.word_dict.__contains__(gram_word_list[i]): gram_word_list.pop(i) if gram_word_list.__len__()==0: return for u in gram_word_list: u_huffman = self.word_dict[u]['Huffman'] e = self.__GoAlong_Huffman(u_huffman,word_vector,self.huffman.root) self.word_dict[word]['vector'] += e self.word_dict[word]['vector'] = preprocessing.normalize(self.word_dict[word]['vector'])