Word2vec源码分析

Published: 04 May 2016 Category: Codes

Word2vec源码分析


Word2vec为一类双层神经元网络,用来产生词嵌入的模型。在bag-of-words的假设下,词的顺序是不重要的,通过训练,word2vec 模型可用来映射每个词到一个高维向量,可用来表示词对词之间的关系,例如用余弦相似度计算词与词之间的距离。

Word2vec的实现主要包括continuous-bag-of-words(CBOW)和skip-grams两种,其区别在于神经元网络结构的输入和输出对调。

Word2vec的源码地址为:
http://code.google.com/p/word2vec/

类似的词嵌入模型和算法还有GloVe等,地址为:
http://nlp.stanford.edu/projects/glove/

据说经测评,GloVe比Word2vec更胜一筹,但由于Word2vec先于GloVe出现,应用更为主流。

Word2vec原理解释

词袋模型(BOW),通过将重复的词叠加,以此来增加句子的似然概率。它基于朴素贝叶斯的独立性假设,将不同位置出现的相同词视为等价的,无视语义、语法等关联性。在单输入单输出的情况下(可视为窗口大小为1),神经元网络结构如下。

此处输入图片的描述

由One-hot编码的词向量经过隐含层编码降维,再解码升维后近似为原始编码,因此,需要最大化如下的似然。

此处输入图片的描述

由神经元网络经典的反传误差的算法推导,网络结构参数的递推公式如下。

此处输入图片的描述

此处输入图片的描述

此处输入图片的描述

考虑到每个词的context,即窗口大小内共现的其他词,Word2vec可分为两种算法BOW和skip-grams,每个词根据当前的context,其词向量是窗口内所有词的词向量的均值。

此处输入图片的描述

BOW和skip-grams的神经元网络结构如下,由此看出两者的区别在于输入和输出结构对调。

此处输入图片的描述

此处输入图片的描述

直接计算网络参数,由之前反传误差递推公式的推导,每次都需要更新所有词的词向量,计算效率低,提高计算效率,有两种方法,一种是通过建立Hierarchical Softmax,其核心思想是建立Huffman树,一种是通过Negative Sampling,减小所需更新的负样本的数量。

Huffman树如下所示,由于Huffman树某条支路上结点具有父子关系,相关性强,因此仅更新某条支路上所有结点的词向量,有效减小了计算复杂度。同时,根据Huffman树的特性,词频越大的词距离根结点越近,左右孩子结点的区分通过将词频大的结点作为左孩子结点,词频小的结点作为右孩子结点实现。由于Huffman树对应的Huffman编码具有不等长性质,因此更新次数能够得到优化。

此处输入图片的描述

同样,为了减小需要更新词向量的词数,还能通过负采样,仅更新负样本的对应的词向量,减小了计算复杂度,其原理在于每个词的context中其他词出现的概率不同,只有那些共现概率高的词的影响更为强烈,对应需要抽样出的负样本,似然及参数更新计算过程如下。

此处输入图片的描述

此处输入图片的描述

Word2vec核心函数解释

Word2vec的训练语料输入后按行进行训练。函数ReadWord读入流字符,通过判断换行符"\n"对样例进行划分。

void ReadWord(char *word, FILE *fin)  

Word2vec通过建立哈希表vocab_hash对字符进行编码映射。函数AddWordToVocab视情况动态扩展空间。

const int vocab_hash_size = 30000000;  
int *vocab_hash;  

相关的util函数包括:

// Returns hash value of a word  
int GetWordHash(char *word)  
// Returns position of a word in the vocabulary; if the word is not found, returns -1  
int SearchVocab(char *word)  
// Reads a word and returns its index in the vocabulary  
int ReadWordIndex(FILE *fin)  
// Adds a word to the vocabulary  
int AddWordToVocab(char *word)  
// Sorts the vocabulary by frequency using word counts  
void SortVocab()  
// Reduces the vocabulary by removing infrequent tokens  
void ReduceVocab()  

前面提到,为了优化算法,提高计算效率,有两种方法,一种是通过建立Hierarchical Softmax,其核心思想是建立Huffman树,一种是Negative Sampling,减小更新负样本的数量。其中建立Huffman树的函数是CreateBinaryTree,其原理是根据词频创建Huffman树,词频越大的单词对应越短的Huffman编码。

// Create binary Huffman tree using the word counts  
// Frequent words will have short uniqe binary codes  
void CreateBinaryTree()  

为了对负样本进行抽样,实现过程中建立了每个单词的能量分布表table,在负样本抽样中用到,此处直接看源码可能难以理解,相关注释如下。

void InitUnigramTable() {  
  int a, i;  
  long long train_words_pow = 0;  
  real d1, power = 0.75;  
  table = (int *)malloc(table_size * sizeof(int));  
  for (a = 0; a < vocab_size; a++) //遍历词汇表,统计能量总值train_words_pow,power为指数系数。  
      train_words_pow += pow(vocab[a].cn, power);  
  i = 0;  
  d1 = pow(vocab[i].cn, power) / (real)train_words_pow; //已遍历词的能量值占能量总值的比例  
  for (a = 0; a < table_size; a++) { //遍历table。a表示table的位置,i表示词汇表的位置  
    table[a] = i; //单词i占据table的位置a  
    //table反映的了单词能量的分布,单词能量越大对应占用table中的位置越多,越容易被抽样到  
    if (a / (real)table_size > d1) {  
      i++;
      d1 += pow(vocab[i].cn, power) / (real)train_words_pow;  
    }  
    if (i >= vocab_siInitNetze) i = vocab_size - 1;  
  }  
}  

神经元网络参数保存在syn0、syn1、syn1neg,其中syn0数组存着Vocab的全部词向量,syn1数组存着Hierarchical Softmax的参数,syn1neg数组存着Negative Sampling的参数。

real *syn0, *syn1, *syn1neg, *expTable;  

初始化和更新通过函数InitNet和TrainModelThread,其中TrainModelThread是线程调用函数,Word2vec的执行可基于多核,每个线程对应一段文本,根据线程id找到自己负责的文本的初始位置,线程函数执行之前,首先需要计算出按照词频排序的词汇表,以及每个单词的Huffman编码。

void InitNet()  
void *TrainModelThread(void *id)  
void TrainModel()  

多线程创建调用函数如下。

for (a = 0; a < num_threads; a++) pthread_create(&pt[a], NULL, TrainModelThread, (void *)a);  
for (a = 0; a < num_threads; a++) pthread_join(pt[a], NULL);  

训练的具体过程如下,根据参数设定,确定是否建立Haffman树以及是否采用负采样进行优化,然后根据传播误差算法更新参数。

for (a = b; a < window * 2 + 1 - b; a++) if (a != window) //扫描目标单词的左右单词  
{  
    c = sentence_position - window + a;  
    if (c < 0) continue;  
    if (c >= sentence_length) continue;  
    last_word = sen[c];  
    if (last_word == -1) continue;  
    for (c = 0; c < layer1_size; c++) //layer1_size为词向量的维度  
        neu1[c] += syn0[c + last_word * layer1_size]; //计算向量和  
    }  
    if (hs) for (d = 0; d < vocab[word].codelen; d++) //开始遍历Huffman树  
    {  
        f = 0;  
        l2 = vocab[word].point[d] * layer1_size;  //point记录Huffman的路径,找到当前节点,并算出偏移  
        // Propagate hidden -> output  
        for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1[c + l2];//计算内积  
        if (f <= -MAX_EXP) continue; //内积不在范围内直接丢弃  
        else if (f >= MAX_EXP) continue;  
        else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];  //内积之后sigmoid函数  
        // 'g' is the gradient multiplied by the learning rate  
        g = (1 - vocab[word].code[d] - f) * alpha;//偏导数的一部分  
        // Propagate errors output -> hidden 反向传播误差,从Huffman树传到隐藏  
        for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];  
        // Learn weights hidden -> output 更新当前内节点的向量  
        for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * neu1[c];  
    }  
    if (negative > 0) for (d = 0; d < negative + 1; d++) {  
        if (d == 0) {  
            target = word;  
            label = 1;  
        } else {  
            next_random = next_random * (unsigned long long)25214903917 + 11;  
            target = table[(next_random >> 16) % table_size];  
            if (target == 0) target = next_random % (vocab_size - 1) + 1;  
            if (target == word) continue;  
            label = 0;  
        }  
        l2 = target * layer1_size;  
        f = 0;  
        for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1neg[c + l2];  
        if (f > MAX_EXP) g = (label - 1) * alpha;  
        else if (f < -MAX_EXP) g = (label - 0) * alpha;  
        else g = (label - expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))]) * alpha;  
        for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1neg[c + l2];  
        for (c = 0; c < layer1_size; c++) syn1neg[c + l2] += g * neu1[c];  
    }  
    // hidden -> in  
    for (a = b; a < window * 2 + 1 - b; a++) if (a != window) {  
        c = sentence_position - window + a;  
        if (c < 0) continue;  
        if (c >= sentence_length) continue;  
        last_word = sen[c];  
        if (last_word == -1) continue;  
        for (c = 0; c < layer1_size; c++) syn0[c + last_word * layer1_size] += neu1e[c];  
    }  
}  

相关连接

  • http://code.google.com/p/word2vec/
  • http://nlp.stanford.edu/projects/glove/
comments powered by Disqus