这篇文章分析word2vec.c的源代码。word2vec.c和word2pharse一样都要过两遍语料,第一遍建立字典,这部分的代码两个文件完全是一样的。word2vec.c第二遍过语料就在训练词向量了。
word2vec实际上是包含了四种模型,根据对上下文的不同的定义方式,可以分为cbow和sg,根据对上下文和中心单词不同的建模方式,分为负采样和哈夫曼树。所以一组合就有四种模型。负采样英文缩写是ns(negative sampling)。目前最普遍的模型是sg和ns的组合。即sgns。这篇文中我就以sgns模型为例过一遍word2vec.c的代码。
word2vec.c中的单词用结构体vocab_word表示
struct vocab_word {
long long cn;
int *point;
char *word, *code, codelen;
};
由于word2vec中用到了哈夫曼树的数据结果,所以多存了一个point,一个code和一个codelen。由于我们这里只讲sgns,所以也不会涉及这些变量。
先分析几个函数的源码。首先是InitUnigramTable函数,这个函数是做negative sampling用的。语料给出了单词的概率分布,下面的代码就是模拟这样一个分布。比如the的频数高,被抽到概率就大。当然这里对分布进行了一个改动,power=0.75。所有单词的频数的0.75次方构成分布,这样能抑制高频词被采样的几率。
void InitUnigramTable() {
int a, i;
double train_words_pow = 0;
double d1, power = 0.75;
table = (int *)malloc(table_size * sizeof(int));//从table中采样,table_size是一个很大的数
for (a = 0; a < vocab_size; a++) train_words_pow += pow(vocab[a].cn, power);//所有单词的频数(的power次方)之和,分母
i = 0;
d1 = pow(vocab[i].cn, power) / train_words_pow;//按照单词分布往table中填单词id
for (a = 0; a < table_size; a++) {
table[a] = i;
if (a / (double)table_size > d1) {
i++;
d1 += pow(vocab[i].cn, power) / train_words_pow;
}
if (i >= vocab_size) i = vocab_size - 1;
}
}
InitNet函数用来初始化word2vec中所有的参数,word2vec中的参数有两部分,分别是词向量和上下文向量。他们的参数个数都是|V|*d。其中|V|是字典中单词的个数,d是词向量的维度,比如是300。由于有两种建模方式:哈夫曼(hs)还是负采样(ns),所以有可以有两份上下文向量。我们这里就考虑ns,所以只有一份儿上下文向量
void InitNet() {
long long a, b;
unsigned long long next_random = 1;
a = posix_memalign((void **)&syn0, 128, (long long)vocab_size * layer1_size * sizeof(real));//初始化词向量
if (syn0 == NULL) {printf("Memory allocation failed\n"); exit(1);}
if (hs) {
a = posix_memalign((void **)&syn1, 128, (long long)vocab_size * layer1_size * sizeof(real));
if (syn1 == NULL) {printf("Memory allocation failed\n"); exit(1);}
for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++)
syn1[a * layer1_size + b] = 0;
}
if (negative>0) {
//初始化上下文向量,用0初始化
a = posix_memalign((void **