Rabbit House

時間の中 泳ぎ続ける

@leo9年前

2016-10-26
18:19
Tech

Note for word2vec Source Code

Reference:
https://github.com/zhangchen-qinyinghua/word2vector/blob/master/word2vec_full_chinese_comment.c

#include <stdio.h> // 文件输入输入使用的标准化函数
#include <stdlib.h> 
// C 标准函数库的头文件,声明了数值与字符串转换函数,伪随机数生成函数,动态内存分配函数,
// 进程控制函数等公共函数。 
#include <string.h>
// 涉及字符串和大量内存处理函数
#include <math.h>
// 提供用于常用高级数学运算的运算函数
#include <pthread.h>
// POSIX的线程标准,定义了创建和操纵线程的一套API

#define MAX_STRING 100             // 字符串最大字符数
#define EXP_TABLE_SIZE 1000        // sigmoid函数表格
#define MAX_EXP 6              // sigmoid函数只计算-6~6 
#define MAX_SENTENCE_LENGTH 1000   // 句子最大长度,以换行符为界
#define MAX_CODE_LENGTH 40     // 哈夫曼编码的最大码长

const int vocab_hash_size = 30000000;
// maximum 30*0.7=21M words in the vocabulary
// 定义了字典里词的最大数目

typedef float real;

struct vocab_word {
    long long cn;
    // 词频数
    int *point;
    // point表示这个词汇对应的辅助向量列的index序列,其长度是codelen
    // 辅助向量定义在syn1中
    char *word, *code, codelen;
    // *word是词本身
    // *code是词对应的哈夫曼编码
    // codelen是code的长度
    // 参见hierarchical softmax原理
}

char train_file[MAX_STRING], output_file[MAX_STRING];
// 定义了输入输出文件(文件名),以字符数组形式储存
char save_vocab_file[MAX_STRING], read_vocab_file[MAX_STRING];
// 定义了存取词典和读词典文件(文件名)
struct vocab_word *vocab;
// 定义了动态变化的词典结构,定义见第*行
int binary=0, cbow=1, debug_mode=2, window=5, min_count=5, num_threads=12, min_reduce=1;
// 定义默认参数
// binary: 是否使用二进制
// cbow: 是否使用cbow,否则使用skip-gram
// window: 窗长,默认为5
// min_count: 出现频次小于一定值的词被舍弃
// min_reduce: 参见ReduceVocab
int *vocab_hash;
// 词典词的哈希值,一个数组
long long vocab_max_size=1000, vocab_size=0, layer1_size=100;
// 词典词数最大值为1000,之后不够用再加,当前词数为0
// layer1_size指的应该是embedding vector的维数为100,即cbow第一层的节点数
long long train_words=0, word_count_actual=0, iter=5, file_size=0, classes=0;
// 词频统计
// train_words是所有有效词汇的频率之和?
// word_count_actual ?
// iter应该是迭代次数 ?
// file_size定义文件最大体积,用于并行化时负载均衡
// classes ?
real alpha=0.025, starting_alpha, sample=1e-3;
// alpha是学习率learning rate
// starting_alpha 初始学习率,可指定
// sample参见词汇表初始化部分
real *syn0, *syn1, *syn1neg, *expTable;
// *syn0: word2vec的最终结果,大小是vector维数×输出词汇数目,以*point作为index
// *syn1: 全部辅助变量,个数比*syn0少一个 ?
// *syn1neg: 随机负采样得到的向量表
// *expTable是预先计算的sigmoid函数值,参见main函数底部
clock_t start;  // 计时器,考察程序时间复杂度

int hs=0, negative=5;  // 定义hierarchical softmax和negative sampling默认参数,前者默认关
// negative不为零时表示每一个word和上下文要负采样的个数
const int table_size=1e8;
int *table;
// 用于负采样,初始化负采样辅助table,保证一个单词被选为负样本的概率d1为
// (词频数)^0.75/所有词[(词频数)^0.75]之和
void InitUnigramTable() {
    int a,i;
    double train_word_pow=0;
    double d1, power=0.75;      // 为什么选择0.75作为指数尚不清楚
    table=(int *)malloc(table_size * sizeof(int));
    for (a=0;a<vocab_size;a++) train_words_pow+=pow(vocab[a].cn, power);    // ?
    i=0;
    d1=pow(vocab[i].cn, power)/train_word_pow;
    for (a=0;a<vocab_size;a++) {
        table[a]=i;
        if (a/(double)table_size>d1) {
            i++;
            d1+=pow(vocab[i].cn, power)/train_word_pow;
        }
        if (i>=vocab_size) i=vocab_size-1;
    }
}

// Reads a single word from a file, assuming space+tab+EOL to be word boundaries
// 从文件中读取单个词,以空格、制表符和EOL(EOF和'\n')作为边界
// char(13)是回车('\r)
// 不同的操作系统中换行符不同,win是\r\n, unix是\n, mac是\r,需统一转换为'\n'换行
// 不认为'\r'是有效符号
// 单个换行符号'\n'被认为是有效符号,并被记为词"</s>"
// 遇到换行符作为结尾,换行符将被回退到输入流,形成一个单个换行符,下一次读为"</s>"
void ReadWord(char *word, FILE *fin) {
    int a=0, ch;
    while (!feof(fin)) {
        ch=fgetc(fin);
        if(ch==13) continue;
        if((ch==' ')||(ch=='\t')||(ch=='\n')) {
            if (a>0) {
                if (ch=='\n') ungetc(ch,fin); // 换行符被回缩到输入流中
                break;
            }
            if(ch=='\n') {
                strcpy(word,(char*)"</s>");
                return;
            } else continue;
        }
        word[a]=ch;
        a++;
        if (a>=MAX_STRING-1) a--;  // Truncate too long words 
        // 单个词汇太长时需要截断
    }
    word[a]=0;
}

// Return hash value of a word
// 计算词的hash值
int GetWordHash(char *word) {
    unsigned long long a, hash=0;
    for (a=0;a<strlen(word);a++) hash=hash*257+word[a];
    hash=hash%vocab_hash_size;
    return hash;
}

// Return position of a word in the vocabulary; if the word is not found, return -1
// 根据hash表查找词在词典里的位置
int SearchVocab(char *word) {
    unsigned int hash=GetWordHash(word);
    while(1) {
        if (vocab_hash[hash]==-1) return -1;
        if (!strcmp(word, vocab[vocab_hash[hash]].word) return vocab_hash[hash];
        hash=(hash+1)%vocab_hash_size;
    }
    return -1;
}

// Reads a word and returns its index in the vocabulary
// 从输入文件中读取一个单词,返回其在词典中的位置
int ReadWordIndex(FILE *fin) {
    char word[MAX_STRING];
    ReadWord(word, fin);
    if (feof(fin)) return -1;
    return SearchVocab(word);
}

// Adds a word to the vocabulary
int AddWordToVocab(char *word) {
    unsigned int hash, length=strlen(word)+1;
    if (length>MAX_STRING) length=MAX_STRING; // 截断词使其长度不超过MAX_STRING
    vocab[vocab_size].word=(char *)calloc(length, sizeof(char));
    strcpy(vocab[vocab_size].word, word);
    vocab[vocab_size].cn=0;
    vocab_size++;
    // Reallocate memory if needed
    if (vocab_size+2>=vocab_max_size) {
        vocab_max_size+=1000;
        vocab=(struct vocab_word *)realloc(vocab, vocab_max_size*sizeof(struct vocab_word));
    }
    hash=GetWordHash(word);     // 获取词的hash
    while (vocab_hash[hash]!=-1) hash=(hash+1)%vocab_hash_size;
    // 在hash表中线性探测,如果有位置为-1(即没有词)就将当前词插入到这个位置
    vocab_hash[hash]=vocab_size-1;
    return vocab_size-1;
}

// Used later for sorting by word counts
// 比较词频
int VocabCompare(count void *a, const void *b) {
    return ((struct vocab_word *)b)->cn - ((struct vocab_word *)a)->cn;
}

// Sorts the vocabulary by frequency using word counts
// 重排词典
void SortVocab() {
    int a, size;
    unsigned int hash;
    // Sort the vocabulary and keep </s> at the first position
    qsort(&vocab[1], vocab_size-1, sizeof(struct vocab_word), VocabCompare);
    // 用快速排序,按照词频对词典里的词进行排序,词频大的排前面
    for (a=0;a<vocab_hash_size;a++) vocab_hash[a]=-1;       // 重置hash表为空
    size=vocab_size;
    train_words=0;
    for (a=0;a<size;a++) {
        // Words occuring less than min_count times will be discarded from the vocab
        // 词频小于某一个阈值的词,丢弃
        if ((vocab[a].cn<min_count) && (a!=0)) {
            vocab_size--;
            free(vocab[a].word);
        } else {
            // Hash will be recomputed, as after the sorting it is not actual
            // 剩下的词重新排列在hash表中
            hash=GetWordHash(vocab[a].word);
            while(vocab_hash[hash]!=-1) hash=(hash+1)%vocab_hash_size;
            vocab_hash[hash]=a;
            train_words+=vocab[a].cn;
            // train_words是词典中所有有效词的词频之和
        }
    }
    // 精简词典占用的内存
    vocab=(struct vocab_word *)realloc(vocab,(vocab_size+1)*sizeof(struct vocab_word));
    // Allocate memory for the binary tree construction
    // 为哈夫曼树分配内存
    for (a=0;a<vocab_size;a++) {
        vocab[a].code=(char *)calloc(MAX_CODE_LENGTH,sizeof(char));
        vocab[a].point=(int *)calloc(MAX_CODE_LENGTH,sizeof(int));
    }
}

// Reduces the vocabulary by removing infrequent tokens
void ReduceVocab() {
    int a,b=0;
    unsigned int hash;
    // 对于词a,若词频大于阈值,则移动到词典位置b,否则舍弃
    for (a=0;a<vocab_size;a++) {
        if (vocab[a].cn > min_reduce) {
            vocab[b].cn=vocab[a].cn;
            vocab[b].word=vocab[a].word;
            b++
        } else free(vocab[a].word);
        vocab_size=b;
    }
    for (a=0;a<vocab_hash_size;a++) vocab_hash[a]=-1;
    for (a=0;a<vocab_size;a++) {
        // Hash will be recomputed, as it is not actual
        // 重构hash表
        hash=GetWordHash(vocab[a].word);
        while(vocab_hash[hash]!=-1) hash=(hash+1)%vocab_hash_size;
        vocab_hash[hash]=a;
    }
    fflush(stdout);
    min_reduce++;       // 每执行一次,舍弃阈值+1 why?
}

// Create binary Huffman tree using the word counts
// Frequent words will have short unique binary codes
// 构造哈夫曼树,给每个词赋予一个哈夫曼编码,词频高者码长短
// 哈夫曼树的构建使用贪心法
/*
Huffman树:最优二叉树,WPL=∑码长x权值 最低
贪心法构造Huffman树的过程:
对于已知的一组叶子节点,权值分别为W1,W2,...,Wn,约定权值小的为左子树,大的为右子树

(1)首先把n个叶子节点看成n棵树(仅有一个节点的二叉树),把它们看做一个森林

    例:   2 4 5 8

(2)合并森林中最小和次小的两棵树,该树根节点的权值为两棵子树之和,此时森林中还有n-1棵树

    i.    6    5   8
         / \
        2   4

(3)重复第(2)步直到森林中只有一棵树为止

    ii.      11        8
             /\
            5  6
              / \
             2   4

    iii.           19
                  /  \
                 8   11
                     /\
                    5  6
                      / \
                     2   4
    (done)

*/
void CreateBinaryTree() {
    long long a,b,i,min1i,min2i,pos1,pos2,point[MAX_CODE_LENGTH];
    // min1i,min2i是两个权值最小的节点,权值为vocab[a].cn即每个词的词频
    char code[MAX_CODE_LENGTH];
    long long *count=(long long *)calloc(vocab_size*2+1,sizeof(long long));
    // 每个词的词频
    long long *binary=(long long *)calloc(vocab_size*2+1,sizeof(long long));
    // 每个词的Huffman编码,为了对齐用了更大的长度(最大码长为MAX_CODE_LENGTH=40)
    long long *parent_node=(long long *)calloc(vocab_size*2+1,sizeof(long long));
    // 记录所有子树的父节点
    for (a=0;a<vocab_size;a++) count[a]=vocab[a].cn;
    for (a=vocab_size;a<vocab_size*2;a++) count[a]=1e15;
    // 剩下节点赋权一个大树,方便获取两个最小的权值
    pos1=vocab_size-1;
    pos2=vocab_size;
    // Following algorithm constructs the Huffman tree by adding one node at a time
    // 每次合并两棵权值最小的子树,直到合并vocab_size-1次
    for (a=0;a<vocab_size-1;a++) {
        // First, find two smallest nodes 'min1,min2'
        if (pos1>=0) {
            if (count[pos1]<count[pos2]) {
                min1i=pos1; pos1--;
            } else {
                min1i=pos2; pos2++;
            }
        } else {
            min1i=pos2; pos2++;
        }
        if (pos1>=0) {
            if (count[pos1]<count[pos2]) {
                min2i=pos1; pos1--;
            } else {
                min1i=pos2; pos2++;
            }

        } else {
            min2i=pos2; pos2++;
        }
        count[vocab_size+a]=count[min1i]+count[min2i];
        parent_node[min1i]=vocab_size+a;
        parent_node[min2i]=vocab_size+a;
        binary[min2i]=1;
        // binary[min1i]=0, 即左子树编码0,右子树编码1
    }
    // Now assign binary code to each vocabulary word 
    for (a=0;a<vocab_size;a++) {
        b=a;
        i=0;
        while(1) {
            code[i]=binary[b];
            point[i]=b;
            i++;
            b=parent_node[b];
            if (b==vocab_size*2-2) break;
        }
        vocab[a].codelen=i;
        vocab[a].point[0]=vocab_size-2;
        for (b=0;b<i;b++) {
            vocab[a].code[i-b-1]=code[b];
            vocab[a].point[i-b]=point[b]-vocab_size;
        }
    }
    free(count);
    free(binary);
    free(parent_node);
}

// 从训练文件中获取词语,同时读取训练文件大小用于并行计算时负载均衡
void LearnVocabFromTrainFile() {
    char word[MAX_STRING];
    FILE *fin;
    long long a,i;
    for (a=0;a<vocab_hash_size;a++) vocab_hash[a]=-1;   // 初始化hash表
    fin=fopen(train_file,"rb");
    if (fin==NULL) {
        printf("ERROR: training data file not found!\n");
        exit(1);
    }
    vocab_size=0;
    AddWordToVocab((char *)"</s>");
    // 首先加入一个特殊词汇</s>, 这个词表示换行符,用于分隔行的标记
    while (1) {
        ReadWord(word,fin);     // 读入一个词
        if(feof(fin)) break;
        train_words++;          // train_words是词典中词的数目
        if ((debug_mode)>1 && (train_words%100000==0)) {
            printf("%lldK%c",train_words/1000,13);
            fflush(stdout);
        }
        i=SearchVocab(word);
        if(i==-1) {
            a=AddWordToVocab(word);
            vocab[a].cn=1;
        }
        else vocab[i].cn++;
        if (vocab_size>vocab_hash_size*0.7) ReduceVocab();
        // 如果词典中词汇数目达到一定数量,则清理掉一些词频较小的词
    }
    SortVocab();        // 排序
    if(debug_mode>0) {
        printf("Vocab size: %lld\n", vocab_size);
        printf("Words in train file: %lld\n", train_words);
    }
    file_size=ftell(fin);
    fclose(fin);
}

// 写入词汇到文件save_vocab_file
// 格式:词汇 词频\n
void SaveVocab() {
    long long i;
    FILE *fo=fopen(save_vocab_file,"wb");
    for(i=0;i<vocab_size;i++) fprintf(fo, "%s %lld\n",vocab[i].word, vocab[i].cn);
    fclose(fo);
}

// 从格式为“词汇 词频\n”的文件中读取词汇
void ReadVocab() {
    long long a,i=0;
    char c;
    char word[MAX_STRING];
    FILE *fin=fopen(read_vocab_file,"rb");
    if (fin==NULL) {
        printf("Vocabulary file not found\n");
        exit(1);
    }
    for (a=0;a<vocab_hash_size;a++) vocab_hash[a]=-1;
    vocab_size=0;
    while(1) {
        ReadWord(word,fin);
        if(feof(fin)) break;
        a=AddWordToVocab(word);
        fscanf(fin,"%lld%c", &vocab[a].cn, &c);
         // 读入词频,由于fscanf和ReadWord的问题,需要退掉词频后面表示边界的字符,否则scanf不会
         // 读入这些符号,ReadWord会读入
        i++;
    }
    SortVocab();        // 重排词典
    if (debug_mode>0) {
        printf("Vocab size: %lld\n", vocab_size);
        printf("Words in train file: %lld\n", train_words);
    }
    fin=fopen(train_file,"rb");
    if (fin==NULL) {
        printf("ERROR: training data file not found!\n");
        exit(1);
    }
    fseek(fin,0,SEEK_END);
    file_size=ftell(fin);
    fclose(fin);
}

void InitNet() {
    long long a,b;
    a=posix_memalign((void **)&syn0, 128, (long long)vocab_size*layer1_size*sizeof(real));
    // 内存动态对齐
    // syn0就是word2vec的计算结果,即词的向量化表示,向量维数为100,alignment=128要求为2的整数次幂
    if (syn1==NULL) {
        printf("Memory allocation failed\n");
        exit(1);
    }

    //hierarchical softmax 
    if (hs) {
        a=posix_memalign((void **)&syn1, 128, (long long)vocab_size*layer1_size*sizeof(real));
        // syn1是辅助向量,大小为layer1_size,数量比syn0少一个,全零初始化
        if (syn1==NULL) {
            printf("Memory allocation failed\n");
            exit(1);
        }
        for(b=0;b<layer1_size;b++) {
            for(a=0;a<vocab_size;a++) syn1[a*layer1_size+b]=0;
        }
    }

    // negative sampling 
    if (negative>0) {
        a=posix_memalign((void **)&syn1neg,128, (long long)vocab_size*layer1_size*sizeof(real));
        // 每个词一个负采样向量
        if (syn1neg==NULL) {
            printf("Memory allocation failed\n");
            exit(1);
        }
        for(b=0;b<layer1_size;b++) {
            for(a=0;a<vocab_size;a++) syn1neg[a*layer1_size+b]=0;
        }
    }

    // 所有词的对应向量初始化,随机数,范围在(-0.5/维度)~(0.5/维度)上均匀分布
    for(b=0;b<layer1_size;b++) {
        for(a=0;a<vocab_size;a++) {
            syn0[a*layer1_size+b]=(rand()/(real)RAND_MAX-0.5)/layer1_size;
        }
    }

    //构造Huffman树
    CreateBinaryTree();
}

// word2vec模型训练的主要过程
void *TrainModelThread(void *id) {
    long long a,b,d,word,last_word,sentence_length=0,sentence_position=0;
    // word是词在词典中的位置
    long long word_count=0, last_word_count=0, sen[MAX_SENTENCE_LENGTH+1];
    // word_count统计学习了多少词,每学一个就加一
    // last_word_count的作用是动态变化学习率
    // 每学习10000个样本,学习率就变小一次,直到变小到starting_alpha*0.0001
    long long l1,l2,c,target,label,local_iter=iter;
    // local_iter ?
    unsigned long long next_random=(long long)id;
    // 随机数以公式 next_random=next_random*(unsigned long long)*25214903917+11 产生
    // 25214903917是一个大质数
    real f,g;
    clock_t now;
    real *neu1=(real *)calloc(layer1_size, sizeof(real));
    // neu1是所有输入的上下文向量之和
    // neu1e是一轮迭代之后每一个word对应的向量的增量
    real *neu1e=(real *)calloc(layer1_size, sizeof(real));
    FILE *f1=fopen(train_file,"rb");
    fseek(f1,file_size/(long long)num_threads*(long long)id, SEEK_SET();    // 并行计算
    while(1) {
        // 学习率更新,新句子读取,根据一个词及其上下文进行模型学习

        // 学习率更新 
        if (word_count-last_word_count>10000) {
            // 每学习10000个词,学习率就变小一次
            word_count_actual += word_count+last_word_count;
            last_word_count=word_count;
            if ((debug_mode)>1)) {
                now=clock();
                printf("%cAlpha: %f Progress: %.2f%% Words/thread/sec: %.2fk  ",13,alpha,
                  word_count_actual/(real)(iter*train_words+1)*100,
                  word_count_actual/((real)(now-start+1)/(real)CLOCKS_PER_SEC*1000));
                fflush(stdout);
            }
            alpha=starting_alpha*(1-word_count_actual/(real)(iter*train_words+1));
            // 学习率变化公式
            if(alpha<starting_alpha*0.0001) alpha=starting_alpha*0.0001;
        }

        // 新句子读取 
        if(sentence_length==0) {
            while(1) {
                word=ReadWordIndex(fi);     // 读入一个新句子
                if(feof(fi)) break;
                if(word==-1) continue;
                word_count++;   // 读到一个有效词,word_count增加1
                if(word==0) break;
                // The subsampling randomly discards frequent words while 
                // keeping the ranking same
                if(sample>0) {
                    // 高频词亚采样
                    real ran=(sqrt(vocab[word].cn/(sample*train_words))+1)*
                             (sample*train_words)/vocab[word].cn;
                    // ran是高频词不被舍弃的概率, sample是一个参数
                    // ran=sqrt(f/s)*s/f=sqrt(s/f)=sqrt(sample/frequency)
                    // 其中s为采样数sample*train_words, f=vocab[word].cn
                    // 分母用s+1只是为了防止其为零,实际几乎不影响数值
                    // 出现频率越高的词,被舍弃的概率越大
                    // 于是高频词被舍弃的概率为max{0,1-ran} (?)
                    next_random=next_random*(unsigned long long)25214903917+11;
                    if(ran<(next_random&0xFFFF)/(real)65536) continue;
                    // 将next_random缩放到(0,1)
                    // 如果这个随机数大于ran(即被舍弃概率1-ran),即被舍弃
                }
                sen[sentence_length]=word;
                sentence_length++;
                if(sentence_length>=MAX_SENTENCE_LENGTH) break;
                // 如果句子过长,则只取前MAX_SENTENCE_LENGTH
            }
            sentence_position=0;
        }

        // 学习一个词及其上下文
        if(feof(fi)||(word_count>train_words/num_threads)) {
            word_count_actual += word_count-last_word_count;
            local_iter--;   // 这段的意思应该是重复迭代学习一定次数
            if(local_iter==0) break;
            word_count=0;
            last_word_count=0;
            sentence_length=0;
            fseek(fi,file_size/(long long)num_threads*(long long)id, SEEK_SET);
            continue;
        }
        word=sen[sentence_position];
        // 从sentence_position=0开始一个一个词学习
        if(word==-1) continue;      // ?
        for(c=0;c<layer1_size;c++) neu1[c]=0;       // 初始化 
        for(c=0;c<layer1_size;c++) neu1e[c]=0;      // 初始化 
        next_random=next_random*(unsigned long long)25214903917+11;
        b=next_random%window;
        // b是0到window-1的数
        // 选取的上下文是(sentence_position-(window-b),sentence_postion+(window-b))的去心集合
        // -----------------------------------------------------------------------------------------
        // 以下部分自己敲了一遍,看了一遍Reference提供的注释,但是还是不太懂,还需要边读papaer边做笔记
        // 目前找到的打算读的paper:
        //
        // [1] Goldberg Y, Levy O. word2vec Explained: deriving Mikolov et al.'s negative-sampling 
        //     word-embedding method[J]. arXiv preprint arXiv:1402.3722, 2014.
        // [2] Mikolov T, Sutskever I, Chen K, et al. Distributed representations of words and 
        //     phrases and their compositionality[C]//Advances in neural information processing
        //     systems. 2013: 3111-3119.
        // [3] Morin F, Bengio Y. Hierarchical Probabilistic Neural Network Language Model[C]//
        //     Aistats. 2005, 5: 246-252.
        // [4] Mikolov T, Chen K, Corrado G, et al. Efficient estimation of word representations 
        //     in vector space[J]. arXiv preprint arXiv:1301.3781, 2013.
        //
        // -----------------------------------------------------------------------------------------
        if(cbow) {
            // train the cbow architeture
            // in->hidden
            // CBOW模型,根据上下文学习
            // 首先获取word的上下文,加和到neu1
            // 然后根据是否采用hierarchical softmax和negative sampling使用不同的梯度公式
            // 依次根据两种梯度公式更新辅助向量syn1,然后再更新word向量syn0
            //
            // CBOW:                        Skip-Gram:
            //  input projection output        input   projection  output
            //  w(t-2)                                           w(t-2)
            // w(t-1) ╲                                    ↗  w(t-1)
            //       ╲ ╲                                     ╱  ↗
            //         ↘ ↘                               ╱  ╱  
            //           [sum] ─→ w(t)          w(t) ─→  [     ]
            //         ↗  ↗                            ╲  ╲
            //       ╱  ╱                                    ╲  ↘
            // w(t+1)  ╱                                       ↘  w(t+1)
            //   w(t+2)                                       w(t+2)
            //

            cw=0;
            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++) neu1[c]+=syn0[c+last_word*layer1_size];
                cw++;
            }
            if(cw) {
                for(c=0;c<layer1_size;c++) neu1[c]/=cw;
                if(hs) for(d=0;d<vocab[word].codelen;d++) {
                    f=0;
                    l2=vocab[word].point[d]*layer1_size;
                    // 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))];
                    // 'g' is the gradient multiplied by learning rate
                    g=(1-vocab[word].code[d]-f)*alpha;
                    // Propagate errors output -> hidden 
                    for(c=0;c<layer1_size;c++) neu1e[c]+=g*syn1[c+l2];
                    // Learn weights hedden -> output 
                    for(c=0;c<layer1_size;c++) syn1[c+l2]+=g*neu1[c];
                }
                // Negative Sampling
                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];
                }
            }
        } else {
            // train skip-gram
            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;
                l1=last_word*layer1_size;
                for(c=0;c<layer1_size;c++) neu1e[c]=0;
                // hierarchical softmax
                if (hs) for (d=0;d<vocab[word].codelen;d++) {
                    f=0;
                    l2=vocab[word].point[d]*layer1_size;
                    // Propagate hidden -> output
                    for(c=0;c<layer1_size;c++) f+=syn0[c+l1]*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))];
                    // 'g' is the gradient multiplied by the learning rate
                    g=(1-vocab[word].code[d]-f)*alpha;
                    // Propagate errors output -> hidden
                    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*syn0[c+l1];
                }
                // Negative Sampling
                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+=syn0[c+l1]*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*syn0[c+l1];
                }
                // Learn weights input -> hidden
                for(c=0;c<layer1_size;c++) syn0[c+l1]+=neu1e[c];
            }
        }
        sentence_position++;
        if(sentence_position>=sentence_length) {
            sentence_length=0;
            continue;
        }
    }
    fclose(fi);
    free(neu1);
    free(neu1e);
    pthread_exit(NULL);
}

[1] Goldberg Y, Levy O. word2vec Explained: deriving Mikolov et al.’s negative-sampling
word-embedding method[J]. arXiv preprint arXiv:1402.3722, 2014.

Note for word2vec Source Code