Word2Vec源码解析

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

源码:http://pan.baidu.com/s/1o6KddOI 

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()会根据情况自动扩空间。

亚系字符,因为编码默认使用UTF8,也可以被Hash。但是需要分词。

1.3 排序与词剪枝

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

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

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

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

1.4 Huffman树

在Hierarchical Softmax优化方法中使用。

直接计算Softmax函数是不切实际的,P(Wt|WtN....,Wt1,Wt+1....,Wt+N)=eWtX+bti=1VeWiX+bi

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

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

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

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

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

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

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

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

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

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

1.5 网络参数、初始化

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

syn1数组存着Hierarchical Softmax的参数,大小|V||M|,初始化全为0,经验规则。实际使用|V1|组。

syn1neg数组存着Negative Sampling的参数,大小|V||M|,初始化全为0,经验规则。

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个词各个维度求和,并且取平均,构成一个新的平均词向量Wx~

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

对于单个句子,需要优化的目标函数:argmaxVec&W1Tt=1TlogP(Wt|Wx~)

T为滑动窗口数。

2.3.1 Hierarchical Softmax近似优化求解P(Wobj|Wx~)

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

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

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

Softmax的K组θ,现在变成了K-1组,代表着二叉树的K-1个非叶结点。在Word2Vec中,由syn1数组存放,。

范围[0layerSize(vocabSize2)layerSize]。因为Huffman树是倒着编码的,所以数组尾正好是树的头。

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

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

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

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

Step1: P(Node2=0|Wx~,θ1)=σ(θ1Wx~)

Step2: P(Node3=1|Wx~,θ2)=1σ(θ2Wx~)

P(Wlove|Wx~)=P(Node2=0|Wx~,θ1)P(Node3=1|Wx~,θ2)

将每个Node写成完整的判别模型概率式:  P(NodeWx~,θ)=σ(θWx~)1y(1σ(θWx~))y

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

P(Wobj|Wx~)=i=0len(Code)1σ(θiWx~)1y(1σ(θiWx~))yy{0,1}andy=HuffmanCode

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

intpointNodecharcodeHuffmanCode

一个容易混掉的地方:

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

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

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

正好错开了,这样就可以一步计算出 P(NodeWx~,θ)=σ(θWx~)1y(1σ(θWx~))y

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

2.3.2 Hierarchical Softmax的随机梯度更新

判别模型 P(Wobj|Wx~) 需要更新的是 Wx~,由于 Wx~ 是个平均项。

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

先对目标函数取对数:

ζ=1Tt=1Ti=0len(Code)1(1y)log[σ(θiWx~)]+ylog[1σ(θiWx~)]

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

ζ=i=0len(Code)1(1y)log[σ(θiWx~)]+ylog[1σ(θiWx~)]

Wx~的梯度:

ζWx~=i=0len(Code)1[(1y)log[σ(θiWx~)]+ylog[1σ(θiWx~)]]Wx~=i=0len(Code)1(1y)θi[(1σ(θiWx~)]yθiσ(θiWx~)=i=0len(Code)1(1yσ(θiWx~))θi

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

ζθi=(1yσ(θiWx~))Wx~

更新流程:

UPDATE_CBOW_HIERARCHICALSOFTMAX(Wt)neu1e=0Wx~Sum&Avg(Wtc...,Wt1,Wt+1...,Wt+c)fori=0tolen(Wt.code1)f=σ(Wx~θi)g=(1codef)LearningRateneu1e=neu1e+gθiθi=θi+gWx~forWin(Wtc...,Wt1,Wt+1...,Wt+c)W=W+neu1e

2.4.1 Negative Sampling简述

引入噪声的Negative Examples,可以用来替代P(Wt|WtN....,Wt1,Wt+1....,Wt+N)[licstar的Blog]

最早这么做的是[Collobert&Weston08]中的,替代Bengio的Softmax的单个词打分目标函数:

xXwDmax{0,1f(x)+f(x(w))}

他们的做法是,对于一个N-Gram的输入x,枚举整个语料库D, 每次取出一个词w,替换N-Gram的中心词,build成x(w)

这样,每次为正样本打f(x)分,噪声负样本打f(x(w)),训练使得,正样本得分高,负样本得分低而负分滚粗。

理论上的工作,来自[Gutmann12],论文提出NCE方法,用原始集X,生成噪声集Y,通过Logistic回归训练出概率密度函数之比:

pdpnwheredPositive,nNegative

pdpnSoftmax  (个人理解,[Gutmann12]中并没有这么说,但是[Mikolov13]中一提而过)

[Gutmann12]中,Logistic回归整体的目标函数为:

J(θ)=1Td{t=1Tdln[h(xt;θ)]+t=1Tnln[1h(yt;θ)]]}whereTd=Num(Positive),Tn=vTd=Num(Negative)

对于Softmax函数而言,每次只需要一个给定的预测正样本Wt,数个随机抽取的词作为预测负样本(源码里默认设定是5)。

2.4.2 采样Negative Examples

随机抽取负样本是件苦差事。

随机数生成满足是均匀分布,而取词概率可不是均匀分布,其概率应当随着词频大小变化。

词频高的词容易被随机到,而词频低的词不容易被随机到。

按照 peghoty 中的理解,源码中做法是将词频转换为线段长度:

len(W)=W.cntUVocabU.cnt

这样就生成了一条词频线段,接下来就是随机Roll点了:

源码中有几点说明:

①词频默认被幂了3/4这样缩短了每个词线段的长度,增强了Roll出的每个点的分布性。

当然,幂不能过小,否则会导致词线段长度整体不够,后面的Roll出点都映射到最后一个词线段上。

②没有刻意去连接所有词线段,而是动态连接。一旦Roll点进度超出词线段总长度,就扩展一条词线段。

Roll点长度=当前Roll点序号/全部Roll点数(VocabSize)

2.4.3 Negative Sampling的随机梯度更新

源码中的syn1neg数组存着负采样的参数,该参数和HS的数组syn1是独立的。

用6个Logistic回归退化Softmax后,得到:

P(Wobj|Wx~)i=05P(WSamplingi|Wx~){WSamplingi=Positive(i=0)WSamplingi=Negative(i>0)

接下来的Logistic回归就比较简单了,由于此时不是Huffman编码,所以标签不用颠倒了,有单样本对数似然函数:

ζ=i=05ylog[σ(θnegiWx~)]+(1y)log[1σ(θnegiWx~)]

如果你熟悉Logistic回归,应该已经熟记Logistic回归参数梯度的优美式子。

Wx~ 的梯度

ζWx~=i=05[yσ(θnegiWx~)]θnegi

θnegi 的梯度

ζθnegi=[yσ(θnegiWx~)]Wx~

更新流程:

UPDATE_CBOW_NEGATIVESAMPLING(Wt)neu1e=0Wx~Sum&Avg(Wtc...,Wt1,Wt+1...,Wt+c)fori=0tonegativef=σ(Wx~θnegi)g=(labelf)LearningRateneu1e=neu1e+gθnegiθnegi=θnegi+gWx~forWin(Wtc...,Wt1,Wt+1...,Wt+c)W=W+neu1e

Word2Vec:Skip-Gram 模型

3.1 起源

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

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

需要优化目标函数:argmaxVec&W1Tt=1Tc<=j<=c,j0logP(Wt+j|Wt)

3.2 握手游戏

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

你可能会不屑一笑:"啊,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_HIERARCHICALSOFTMAX(Wt)forWin(Wtc...,Wt1,Wt+1...,Wt+c)neu1e=0fori=0tolen(Wt.Code)1f=σ(Wθi)g=(1codef)LearningRateneu1e=neu1e+gθiθi=θi+gWW=W+neule

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

UPDATE_SKIPGRAM_HIERARCHICALSOFTMAX(Wt)forWin(Wtc...,Wt1,Wt+1...,Wt+c)neu1e=0fori=0tolen(W.Code)1f=σ(Wtθi)g=(1codef)LearningRateneu1e=neu1e+gθiθi=θi+gWtWt=Wt+neule

至于作者 peghoty 为什么会犯这样的错误,正如前面所说:

大家都被P(Wt+j|Wt)给误导了,认为是中心词预测两侧词,所以每次就更新中心词。这是错误的。

引用我在原文里的评论:

回复neopenx:补充一下,LZ的方法主要错在,梯度更新顺序错了。
主要原因是word2vec源码中使用了随机梯度训练,之所以不采样完全梯度,是因为梯度矩阵太难算(Theano等可以直接一步求导,但是没法做HS)
正常情况下,如果是完全梯度的话,需要等这个句子跑完之后,再更新。所以对于一个句子,先算P(4|3)还是P(3|4)其实无所谓,反正都没更新。
也就是说P(w|context(w))=P(context(w)|w)是可以颠倒的。
但是随机梯度则是在每跑一个pos后更新,本身就是一种近似。
LZ的做法是对于每个窗口,总是在更新当前pos的词向量。如P(2|4)、P(3|4)、P(5|4)、P(6|4),更新的都是4,这是一种DP思想,关键真的无后效性嘛?
明显P(5|4)时,5就没更新,却还是算了,这是错误的。
源码中则是依次更新P(4|2)、P(4|3)、P(4|5)、P(4|6),这样,对于每个pos,保证窗口词都能更新一遍,而不是盯在一个词上,有点负载均衡的味道。
最 后就是,不能简单认为Skip-Gram就是CBOW的颠倒。其实两者的差别主要在于,CBOW利用词袋模型的贝叶斯独立性假设,近似将n-gram中的 n个单词sum&avg看成一个。而Skip-Gram则是看成n个。如果完全梯度,至于P(中心|两侧)还是P(两侧|中心)其实无所谓,反正 都是一对一,和握手游戏一样,最后是对称的,肯定全被覆盖到。
如果随机梯度,那么必须先算P(中心|两侧),P(两侧|中心)是没有道理的。

对于批梯度来说,其实先更新谁都无所谓。但是如果是随机梯度,应该同样按照CBOW中的做法:

对于每个t,每次应该把握手的9个词给更新掉,不然就有点负载不均衡了。

这样,更新的时候,实际上用的是P(Wt|Wt+j),而不是P(Wt+j|Wt)

有趣的是,这却恰恰和目标函数相反,然而目前网上居然还没人从这个角度理解源码。

3.4 Negative Sampling的随机梯度更新

在Negative Sampling中, peghoty 还是没有意识到他的错误,他误认为:

源码没有按照目标函数编程,然后Balabala一大堆,实际上,NS只是在HS基础上把Huffman树去掉,主要是他HS理解错了。

但是这次却写对了算法流程。

源码的算法流程:

UPDATE_SKIPGRAM_NEGATIVESAMPLING(Wt)forWin(Wtc...,Wt1,Wt+1...,Wt+c)neu1e=0fori=0tonegativef=σ(Wθnegi)g=(labelf)LearningRateneu1e=neu1e+gθnegiθnegi=θnegi+gWW=W+neule

posted @   Physcal  阅读(12213)  评论(9编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
点击右上角即可分享
微信分享提示