Word2Vec源码分析

Reference:http://blog.csdn.net/itplus/article/details/37969519  (Word2Vec解析(部分有错))

源码:https://github.com/danielfrg/word2vec  (Python封装C版,原Code.Google被墙了)

Word2Vec中的Coding技巧

1.1 ReadWord()

训练语料每个句子呈一行。ReadWord()逐个对输入流读字符。

特判的换行符,第一次遇到换行符,会把换行符退流。这样下一次单独遇到换行符,

此时a=0,直接生成结尾符单词$</s>$,这个词在Hash表中处于0号位置。

只要Hash到0,说明一个句子处理完了,跳过这个词。进入下一个句子。

1.2 Hash表

为了执行速度,Word2Vec放弃了C++,因而不能用map做Hash。

因而里面手写了Hash表进行词Hash。使用线性探测,默认Hash数组30M。

AddWordToVocab()会根据情况自动扩空间。

如果要做中文词向量,需要对Unicode进行Hash,首先你需要一个支持Unicode的类库。

然后用map来Hash QString(Qt)、CString(MFC)。

最后你要把源码Re-Written一遍。

1.3 排序与词剪枝

由于构建Huffman树的需要,做好Vocab库后,对Hash表,按词频从大到小排序。

并剔除低频词(min_count=5),重新调整Hash表空间。

除此之外,在从文件创建Vocab时候,会自动用ReduceVocab()剪枝。

当VocabSize达到Hash表70%容量时,先剪掉频率1的全部词。然后下次再负荷,则对频率2下手。

1.4 Huffman树

在Hierarchical Softmax优化方法中使用。

计算传统Softmax函数是不切实际的,$P(t|t-1,t-2...t-N)=\frac{e^{W_{t}X+b_{t}}}{\sum_{i=1}^{V}e^{W_{i}X+b_{i}}}$

底部的归一化因子直接关系到Vocab大小,有10^5,每算一个概率都要计算10^5次矩阵乘法,不现实。

最早的Solution是Hierarchical Softmax,即把Vocab做成一颗二叉树,这样计算一次概率,最坏要跑$O(logV)$结点(矩阵乘法)。

至于为什么采用Huffman树,原因有二:

①Huffman树是满二叉树,从BST角度来看,平衡性最好。

②Huffman树可以构成优先队列,对于非随机访问往往奇效。

为什么是非随机访问?因为根据生活常识,高频词经常被访问(废话=。=)

这样,按照词频降序建立Huffman树,保证了高频词接近Root,这样高频词计算少,低频词计算多,贪心优化思想。

Word2Vec的构建代码非常巧妙,利用数组下标的移动就完成了构建、编码。

它最重要的是只用了parent_node这个数组来标记生成的Parent结点(范围$[VocabSize,VocabSize*2-2]$)

最后对Parent结点减去VocabSize,得到从0开始的Point路径数组。剩余细节在下文描述。

1.5 网络参数、初始化

syn0数组存着Vocab的全部词向量,大小$|V|*|M|$,初始化范围$[\frac{-0.5}{M},\frac{0.5}{M}]$,经验规则。

syn1数组存着Softmax用的临时词向量,大小$|V|*|M|$,初始化全为0,经验规则。也就是Softmax分成$|V|$类的参数了。

Word2Vec:CBOW(Continues Bag of Word)模型

2.1 One-Hot Represention with BOW

在One-Hot Represention中,如果一个句子中出现相同的词,那么只用0/1来编码明显不准确。

词袋模型(BOW),允许将重复的词叠加,就像把重复的词装进一个袋子一样,以此来增加句子的信度。

它基于朴素贝叶斯的独立性假设,将不同位置出现的相同词,看作是等价的,但是仍然无视了语义、语法、关联性。

2.2 Distributed Represention with CBOW

Bengio的模型中,为了让词向量得到词序信息,输入层对N-Gram的N个词进行拼接,增加了计算压力。

CBOW中取消了训练词序信息,将N个词各个维度求和,并且取平均,构成一个新的平均词向量$W_{{\tilde{x}}}$。

同时,为了得到更好的语义、语法信息,采用窗口扫描上下文法,即预测第$i$个词,不仅和前N个词有关,还和后N个词有关。

对于单个句子,需要优化的目标函数:$arg\max \limits_{Vec\&W}\frac{1}{T}\sum_{t=1}^{T}logP(W_{t}|W_{{\tilde{x}}})$

T为滑动窗口数。

2.3.1 Hierarchical Softmax近似优化求解$P(W_{obj}|W_{{\tilde{x}}})$

传统的Softmax可以看成是一个线性表,平均查找时间$O(n)$

HS方法将Softmax做成一颗平衡的满二叉树,维护词频后,变成Huffman树。

这样,原本的Softmax问题,被近似退化成了近似$log(K)$个Logistic回归组合成决策树。

Softmax的K组$\theta$,现在变成了K-1组,在Word2Vec中,由syn1数组存放,。

范围$[0*layerSize\sim(vocabSize - 2)*layerSize]$。因为Huffman树是倒着编码的,所以数组尾正好是树的头。

如:syn1数组中,$syn1[(vocabSize - 2)*layerSize]$就是Root的参数$\theta$。(不要问我为什么要-2,因为下标从零开始)

Word2Vec规定,每次Logistic回归,$Label=1-HuffmanCode$,Label和编码正好是相反的。

比如现在要利用$W_{{\tilde{x}}}$预测$love$这个词, 从Root到love这个词的路径上,有三个结点(Node 1、2、3),两个编码01。

那么(注意,Node指的是Huffman编码,而后面Sigmoid算出的是标签,所以和Logistic回归正好相反):

Step1: $P(Node_{2}=0|W_{{\tilde{x}}},\theta_{1})=\sigma(\theta_{1}W_{{\tilde{x}}})$

Step2: $P(Node_{3}=1|W_{{\tilde{x}}},\theta_{2})=1-\sigma(\theta_{2}W_{{\tilde{x}}})$

则$P(W_{love}|W_{{\tilde{x}}})=P(Node_{2}=0|W_{{\tilde{x}}},\theta_{1}) \cdot P(Node_{3}=1|W_{{\tilde{x}}},\theta_{2})$

将每个Node写成完整的判别模型概率式:  $P(Node\mid W_{{\tilde{x}}},\theta)=\sigma(\theta W_{{\tilde{x}}})^{1-y} \cdot (1-\sigma(\theta W_{{\tilde{x}}}))^{y}$

将路径上所有Node连锁起来,得到概率积:

$P(W_{obj}|W_{{\tilde{x}}})=\prod_{i=0}^{len(Code)-1}\sigma(\theta_{i}W_{{\tilde{x}}})^{1-y} \cdot (1-\sigma(\theta_{i}W_{{\tilde{x}}}))^{y}\qquad y\propto \begin{Bmatrix}{{0,1}}\end{Bmatrix}\quad and \quad y=HuffmanCode$

Word2Vec中vocab_word结构体有两个数组变量负责这部分:

$int *point\quad--\quad Node\\char *code\quad--\quad HuffmanCode$

一个容易混掉的地方:

$vocab[word].code[d]$ 指的是,当前单词word的,第d个编码,编码不含Root结点

$vocab[word].point[d]$ 指的是,当前单词word,第d个编码下,前置结点。

比如$vocab[word].point[0]$ 肯定是Root结点,而 $vocab[word].code[0]$ 肯定是Root结点走到下一个点的编码。

正好错开了,这样就可以一步计算出 $P(Node\mid W_{{\tilde{x}}},\theta)=\sigma(\theta W_{{\tilde{x}}})^{1-y} \cdot (1-\sigma(\theta W_{{\tilde{x}}}))^{y}$

这种避免回溯搜索对应路径的预处理trick在 $CreateBinaryTree()$ 函数中实现。

2.3.2 Hierarchical Softmax的随机梯度更新

判别模型 $P(W_{obj}|W_{{\tilde{x}}})$ 需要更新的是 $W_{{\tilde{x}}}$,由于 $W_{{\tilde{x}}}$ 是个平均项。

源码中的做法是对于原始SUM的全部输入,逐一且统一更新 $W_{{\tilde{x}}}$ 的梯度项。(注意,这种近似不是一个好主意)

先对目标函数取对数:

$\zeta =\frac{1}{T}\sum_{t=1}^{T}\sum_{i=0}^{len(Code)-1}\quad(1-y)\cdot\log[\sigma(\theta_{i}W_{{\tilde{x}}})]+y\cdot\log[1-\sigma(\theta_{i}W_{{\tilde{x}}})]$

当然,Word2Vec中没有去实现麻烦的批梯度更新,而是对于每移动到一个中心词t,就更新一下,单样本目标函数:

$\zeta^{‘} =\sum_{i=0}^{len(Code)-1}\quad(1-y)\cdot\log[\sigma(\theta_{i}W_{{\tilde{x}}})]+y\cdot\log[1-\sigma(\theta_{i}W_{{\tilde{x}}})]$

对$W_{{\tilde{x}}}$的梯度:

$\frac{\partial \zeta^{‘}}{W_{{\tilde{x}}}}=\,\sum_{i=0}^{len(Code)-1}\frac{\partial [\,(1-y)\cdot\log[\sigma(\theta_{i}W_{{\tilde{x}}})]+y\cdot\log[1-\sigma(\theta_{i}W_{{\tilde{x}}})]\,]}{\partial W_{{\tilde{x}}}}\\\\\quad\,\, =\,\sum_{i=0}^{len(Code)-1}(1-y)\cdot\theta_{i}\cdot[(1-\sigma(\theta_{i}W_{{\tilde{x}}})]-y\cdot\theta_{i}\cdot\sigma(\theta_{i}W_{{\tilde{x}}})\\\\\quad\,\,=\,\sum_{i=0}^{len(Code)-1}(1-y-\sigma(\theta_{i}W_{{\tilde{x}}}))\cdot\theta_{i}$

对$\theta$的梯度,和上面有点对称,只不过没有 $\sum$ 了,所以源码里,每pass一个Node,就更新:

$\frac{\partial \zeta^{‘}}{\partial \theta}=\,(1-y-\sigma(\theta_{i}W_{{\tilde{x}}}))\cdot W_{{\tilde{x}}}$

更新流程:

$UPDATE\_CBOW\_HIERARCHICAL\,SOFTMAX(W_{t})\\1. \quad neu1e=0\\2. \quad W_{{\tilde{x}}}\leftarrow Sum\&Avg(W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\3. \quad for \quad i=0 \quad to \quad len(Code)-1\\\qquad\qquad f=\sigma(W_{{\tilde{x}}}\theta_{i})\\\qquad\qquad g=(1-code-f)\cdot LearningRate\\\qquad\qquad neu1e=neu1e+g\cdot\theta_{i}\\\qquad\qquad \theta_{i}=\theta_{i}+g\cdot W_{{\tilde{x}}}\\4. \quad for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\ \qquad\qquad W=W+neu1e$

Word2Vec:Skip-Gram 模型

3.1 起源

Skip-Gram是Mikolov在Word2Vec中祭出的大杀器,用于解决CBOW的精度问题。

它不再将输入求和平均化,而是对称预测

需要优化目标函数:$arg\max \limits_{Vec\&W}\frac{1}{T}\sum_{t=1}^{T}\sum_{-c<=j<=c,j\neq 0}logP(W_{t+j}|W_{t})$

3.2 握手游戏

Skip-Gram是个容易让人误解的模型,主要是因为 $P(W_{t+j}|W_{t})$ ,以及上面那张图。

你可能会不屑一笑:"啊,Skip-Gram不就是用中心词预测两侧词嘛,不就是CBOW的颠倒版!”

实际上,Skip-Gram可不是简单的颠倒版,它是用 每个词,预测窗口内,除它以外的词。

先让我们看一下二年级小朋友写的作文,~Link~

握手游戏

二年级108班 朱紫曦

今天上数学课我们学了《数学广角》。下课的时候,钟老师带我们做握手游戏。首先,我和廖志杰、董恬恬,还有钟老师,我们四个人来握手。我和他们3个人每人 握了一次手就是3次。钟老师笑眯眯地和廖志杰、董恬恬每人握了一次手。最后,董恬恬和廖志杰友好地我了一次手。钟老师问同学们:“我们4人每两人握一次手 一共握了几次手?”大家齐声说:“6次!”接着,老师让我们5个人,6个人……都来做握手游戏,并能说出每两人握一次手一共握了几次手。在快乐的游戏中钟 老师还教我们一个计算握手次数的计算方法。4个人每两个人握一次手,握手次数是:1+2+3;5个人这样握手次数是:1+2+3+4:;10个人这样握手 次数是1+2+3+4+5+6+7+8+9;100个这样握手次数是1+2+3+4……+98+99。

在这个数学握手游戏,不仅让我们开学,还让我们学到了知识。

Skip-Gram模型是握手游戏的规则修改版,它假设A和B握手、B与A握手是不同的( 贝叶斯条件概率公式不同 )

仔细回想一下目标函数中的t循环了一个句子中的每个词,对于每个t,它和其他的词都握了一次手。

假设一个句子有10个词,第一个词和剩余9个词握手,第二个词和剩余9个词握手.....,请问一共握了多少次手?

噢哟,10*9=90次嘛!对,目标函数里就有90个条件概率。

3.3 Hierarchical Softmax的随机梯度更新

迷人的Skip-Gram,以至于让 http://blog.csdn.net/itplus/article/details/37969979 这篇非常棒的文章都写了。

直接贴算法流程了:

$UPDATE\_SKIPGRAM\_HIERARCHICAL\,SOFTMAX(W_{t})\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\\qquad neu1e=0\\\qquad for \quad i=0 \quad to \quad len(Code)-1\\\qquad\qquad f=\sigma(W\theta_{i})\\\qquad\qquad g=(1-code-f)\cdot LearningRate\\\qquad\qquad neu1e=neu1e+g\cdot\theta_{i}\\\qquad\qquad \theta_{i}=\theta_{i}+g\cdot W\\\qquad W=W+neule$

再贴下作者 peghoty 理解错误的算法流程:

$UPDATE\_SKIPGRAM\_HIERARCHICAL\,SOFTMAX(W_{t})\\for \quad W \quad in \quad (W_{t-c}...,W_{t-1},W_{t+1}...,W_{t+c})\\\qquad neu1e=0\\\qquad for \quad i=0 \quad to \quad len(Code)-1\\\qquad\qquad f=\sigma(W_{t}\theta_{i})\\\qquad\qquad g=(1-code-f)\cdot LearningRate\\\qquad\quad neu1e=neu1e+g\cdot\theta_{i}\\\qquad\qquad \theta_{i}=\theta_{i}+g\cdot W\\\qquad W_{t}=W_{t}+neule$

至于作者 peghoty 为什么会犯这样的错误,正如前面所说,大家都被$P(W_{t+j}|W_{t})$给误导了,认为是中心词预测两侧词,所以每次就更新中心词。

时间: 2024-10-12 14:06:22

Word2Vec源码分析的相关文章

FastText总结,fastText 源码分析

文本分类单层网络就够了.非线性的问题用多层的. fasttext有一个有监督的模式,但是模型等同于cbow,只是target变成了label而不是word. fastText有两个可说的地方:1 在word2vec的基础上, 把Ngrams也当做词训练word2vec模型, 最终每个词的vector将由这个词的Ngrams得出. 这个改进能提升模型对morphology的效果, 即"字面上"相似的词语distance也会小一些. 有人在question-words数据集上跑过fastT

机器学习算法实现解析——word2vec源码解析

在阅读本文之前,建议首先阅读"简单易学的机器学习算法--word2vec的算法原理",掌握如下的几个概念: 什么是统计语言模型 神经概率语言模型的网络结构 CBOW模型和Skip-gram模型的网络结构 Hierarchical Softmax和Negative Sampling的训练方法 Hierarchical Softmax与Huffman树的关系 有了如上的一些概念,接下来就可以去读word2vec的源码.在源码的解析过程中,对于基础知识部分只会做简单的介绍,而不会做太多的推导

TeamTalk源码分析之login_server

login_server是TeamTalk的登录服务器,负责分配一个负载较小的MsgServer给客户端使用,按照新版TeamTalk完整部署教程来配置的话,login_server的服务端口就是8080,客户端登录服务器地址配置如下(这里是win版本客户端): 1.login_server启动流程 login_server的启动是从login_server.cpp中的main函数开始的,login_server.cpp所在工程路径为server\src\login_server.下表是logi

Android触摸屏事件派发机制详解与源码分析二(ViewGroup篇)

1 背景 还记得前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>中关于透过源码继续进阶实例验证模块中存在的点击Button却触发了LinearLayout的事件疑惑吗?当时说了,在那一篇咱们只讨论View的触摸事件派发机制,这个疑惑留在了这一篇解释,也就是ViewGroup的事件派发机制. PS:阅读本篇前建议先查看前一篇<Android触摸屏事件派发机制详解与源码分析一(View篇)>,这一篇承接上一篇. 关于View与ViewGroup的区别在前一篇的A

HashMap与TreeMap源码分析

1. 引言     在红黑树--算法导论(15)中学习了红黑树的原理.本来打算自己来试着实现一下,然而在看了JDK(1.8.0)TreeMap的源码后恍然发现原来它就是利用红黑树实现的(很惭愧学了Java这么久,也写过一些小项目,也使用过TreeMap无数次,但到现在才明白它的实现原理).因此本着"不要重复造轮子"的思想,就用这篇博客来记录分析TreeMap源码的过程,也顺便瞅一瞅HashMap. 2. 继承结构 (1) 继承结构 下面是HashMap与TreeMap的继承结构: pu

Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)(Linux-3.0 ARMv7)【转】

原文地址:Linux内核源码分析--内核启动之(5)Image内核启动(rest_init函数)(Linux-3.0 ARMv7) 作者:tekkamanninja 转自:http://blog.chinaunix.net/uid-25909619-id-4938395.html 前面粗略分析start_kernel函数,此函数中基本上是对内存管理和各子系统的数据结构初始化.在内核初始化函数start_kernel执行到最后,就是调用rest_init函数,这个函数的主要使命就是创建并启动内核线

Spark的Master和Worker集群启动的源码分析

基于spark1.3.1的源码进行分析 spark master启动源码分析 1.在start-master.sh调用master的main方法,main方法调用 def main(argStrings: Array[String]) { SignalLogger.register(log) val conf = new SparkConf val args = new MasterArguments(argStrings, conf) val (actorSystem, _, _, _) =

Solr4.8.0源码分析(22)之 SolrCloud的Recovery策略(三)

Solr4.8.0源码分析(22)之 SolrCloud的Recovery策略(三) 本文是SolrCloud的Recovery策略系列的第三篇文章,前面两篇主要介绍了Recovery的总体流程,以及PeerSync策略.本文以及后续的文章将重点介绍Replication策略.Replication策略不但可以在SolrCloud中起到leader到replica的数据同步,也可以在用多个单独的Solr来实现主从同步.本文先介绍在SolrCloud的leader到replica的数据同步,下一篇

zg手册 之 python2.7.7源码分析(4)-- pyc字节码文件

什么是字节码 python解释器在执行python脚本文件时,对文件中的python源代码进行编译,编译的结果就是byte code(字节码) python虚拟机执行编译好的字节码,完成程序的运行 python会为导入的模块创建字节码文件 字节码文件的创建过程 当a.py依赖b.py时,如在a.py中import b python先检查是否有b.pyc文件(字节码文件),如果有,并且修改时间比b.py晚,就直接调用b.pyc 否则编译b.py生成b.pyc,然后加载新生成的字节码文件 字节码对象