word2vec源码浅析

word2vec

大概花了半个月的时间来看word2vec的源码,意外的发现看源码其实是一个很有意思的事情。在阅读的过程中,你可以明显感受到别人的编程水准和自己不是同一个层次,可以说一个大师的代码看起来就是格外的赏心悦目。除了编程风格,你还可以学到各种各样稀奇古怪的库的使用,各种复杂功能的简洁实现

整体架构

关于word2vec的部分主要分为了3块内容,构造词表、训练、以及计算分数。其中计算分数不改变训练的结果,且计算过程与训练部分大同小异,所以就不进行介绍了,下面详细介绍一下构造词表与训练部分。

构造词表

  • 构造词表中包含3个小块的函数:

    • scan_vocab 的作用是扫描整体的sentence,初步的统计词频,将结果存在一个raw_vacab的字典中,key是词的名称,value则是词出现的频率。另外还统计了total_word(总词数)、corpus_count(语料库大小)。

    • scale_vocab 的作用则是针对外界输入的最小词频对词典进行过滤,并且,通过外界输入的采样率对每个词进行取词概率的计算,具体公式为: s a m p l e i n t = v / ( t C + 1 ) ∗ ( t C / v ) ∗ 2 32 sample_{int}=\sqrt{v/(t_C+1)}* (t_C/v) *2^{32} sampleint=v/(tC+1) (tC/v)232 其中,tc 为采样率与过滤后剩余总词数的乘积,v是当前词的词频——即出现词数,sample_int则代表当前词被取概率乘上2^32。

    • finalize_vocab 的作用是构造出最终的词典。其中包含两种方式去构建,第一种是构建huffman树,其二则是构造负采样表。这里主要介绍一下第一种。

      • 首先对原始词典vocab建堆,比较大小在Vocab类中有定义,为其中存储的词频。

      • 每次从队中取出词频最小的两个vocab,min1 和min2。

        wv.vocab是字典{word:Vocab(count=,sample_int=,index=)} , 这里的index可以判断是否是叶子节点、count则被用来各种比较大小,他们都存储于定义好的Vocab类。

        用这两个vocab构造出他们的父节点,count为两者之和,索引为range(len(wv.vocab)-1) + len(wv.vocab). 即非叶子节点他们的索引总是大于len(wv.vocab)的。另外,在Vocab中增加定义左孩子为min1,右孩子为min2。

      • 重复上面的步骤,最后得到的是1个根节点的Vocab。将这个Vocab用栈进行操作,通过index进行判断是否已经到了叶子节点,最后得到huffman树每个节点的路径、节点位置、深度信息。

      除了构建词表之外,finalize_vocab还调用了reset_weight()函数,将权重按需重置为0和1.

训练

  • 训练部分可以说是整体的一大块,包含了训练的完整流程,拆分来看,其中有几个主要的函数。

    • job_producer(): 该函数是根据不同的batch_size(外界输入),把sentence拆分成多个不同的job。同时,根据给定的start α \alpha α 和end α \alpha α 来更新next α \alpha α.
    • worker_loop(): 该函数为训练的开端,获取job_producer()函数传到job_queue队列中的job,解包后传到do_train_job()函数中去,进行下一步训练操作。源码为了加速计算,将do_train_job 中的train_batch_sg()函数以及之后的流程进行了cython的重写,然后编译成了.c 和 .so,然后在python中直接调用的这个库。
    • 在word2vec的python版本中(虽然慢但是还是有的),train_batch_sg()函数的主要作用是根据滑动窗口w来训练单个句子,由于是skip-gram模型,所以它把窗口内的背景词逐个和要训练的词放入train_sg_pair()中去,进行下一步的训练。
    • 与论文不同的地方在于源码中对于被训练词V(word)的更新是每训练一个词就更新一次,而论文则是说所有词 context(w) 都处理完之后才刷新V(word)。下面给出训练部分的伪代码

    • 其中源码中的syn0对应的是v(w),syn1对应的是 θ j − 1 u \theta_{j-1}^{u} θj1u,neu1e对应的是e。这个更新完全是根据对条件概率的公式使用梯度上升法得到的最终结果来进行的。

源码调度结构

废弃部分word2vec

在2018年更新后的gensim中,model目录下有deprecated word2vec和重写之后的word2vec两份源码,单看word2vec的话,废弃部分的源码相对来说更容易理解,但是从复用的角度或者继承、多态的角度来看是不友好的,所以该部分主要是介绍一下新版word2vec的代码框架

新版word2vec

  • 总体来说分为了3个类,从下往上表示了继承关系,子类中的函数表示在子类中有重写。
  • 除此之外还有构建词表的部分,这个是在word2vec.py中另写了一个Word2VecVocab类,里面重写了_scan_vocab,prepare_vocab等等跟词表构建相关的函数,该部分没有画在图里。
  • word2vec训练部分全部使用cython重写了然后编译成了动态链接库(.so)以及c源码(.c),该部分cython可以在word2vec_inner.pyx中看到,word2vec_inner.c则是c源码

阅读源码过程中可能会遇到的问题

该部分是本人在阅读过程中遇到的一些麻烦,其中网上查阅过一些资料,也咨询过同事,或多或少增长了一些奇怪的知识,感觉并不能保证完全正确。

  1. wv.vocab、Vocab、vocab 傻傻分不清楚。

    • wv.vocab是模型里边定义的一个字典,其中key是单个的词,value是Vocab()

    • Vocab如上所述存在于wv.vocab中,是定义在keyedvectors.py文件中的一个类,其中定义了 __lt__()函数:

      def __lt__(self, other):  # used for sorting in a priority queue
          return self.count < other.count
      

      该部分(个人理解)是用在建堆时,看似比较Vocab,实则比较里面的count

    • vocab是在扫描所有sentence的时候临时建的defaultdict,其中key是单个词,value是词频。

  2. 代码中经常会出现一些完全不知道有什么用的东西,比如is_ft,word2vec中的取值都是false,然而却有为true的代码。这个is_ft它没有做解释,其实ft是fasttext的缩写,而fasttext是继承的word2vec,在word2vec的子类里才会有is_ft为true的地方,所以其实is_ft在word2vec中并没有使用。

  3. 源码的计算部分往往和理论方面相差甚远,因为在word2vec源码中实现的训练仅仅是理论推导的结果,你会发现计算中完全没有求梯度之类的操作,条件概率更是完全没有涉及,有的仅仅是你的参数如何更新,所以单看训练部分很难看出一个算法的思路。大概这也是为什么网上那么多原理解析的文章的原因吧。

参考资料

《word2vec中的数学》

的参数如何更新,所以单看训练部分很难看出一个算法的思路。大概这也是为什么网上那么多原理解析的文章的原因吧。

参考资料

《word2vec中的数学》

  • 这本书原理部分十分详细。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值