word2vec源码下载地址:https://github.com/tmikolov/word2vec

本文对word2vec源码进行分析,在源码中进行了注释。在阅读源码之前,建议先阅读以下两篇博文,加深对word2vec的理解。

《word2vec中的数学原理详解》

《word2vec数学分析》

以下给出word2vec源码和注释:

//  Copyright 2013 Google Inc. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <pthread.h>#define MAX_STRING 100                                                                                // 一个词的最大字符长度(英语:单词的字符个数,汉语:词中字个数)
#define EXP_TABLE_SIZE 1000                                                                         // 对sigmoid函数值进行缓存,存储1000个,需要用的时候查表,x范围是[-MAX_EXP, MAX_EXP]
#define MAX_EXP 6                                                                                   // sigmoid函数缓存的计算范围,最大计算到6 (exp^6 / (exp^6 + 1)),最小计算到-6 (exp^-6 / (exp^-6 + 1))
#define MAX_SENTENCE_LENGTH 1000                                                                    // 定义句子的最大长度(最大词个数)
#define MAX_CODE_LENGTH 40                                                                          // 最长的哈夫曼编码长度和路径长度,vocab_word中point域和code域最大大小const int vocab_hash_size = 30000000;                                                             // Maximum 30 * 0.7 = 21M words in the vocabulary,词库哈希表大小,装填系数为0.7,用线性探测解决哈希冲突typedef float real;                                                                                 // Precision of float numbersstruct vocab_word {                                                                                    // 词的结构体,存储包括词本身、词频、哈夫曼编码、编码长度、哈夫曼路径long long cn;                                                                                    // 词频int *point;                                                                                        // 哈夫曼树中从根节点到该词的路径,路径的索引要特别注意,在下面的构建哈夫曼树中会说明char *word, *code, codelen;                                                                       // 分别是:词,哈夫曼编码,编码长度
};/*** train_file                   要训练的语料文件,以句子为单位进行训练,会从该文件读取词以建立词库,训练时从该文件读入句子* output_file                 训练后词向量的输出文件,由binary和classes共同控制输出结果*/
char train_file[MAX_STRING], output_file[MAX_STRING];/*** save_vocab_file               当提供该参数时,建立词库后,会把词库保存到该文件* read_vocab_file             当提供该参数时,从该文件读入词库;若不提供该参数时,从训练语料文件(train_file)读入并建立词库*/
char save_vocab_file[MAX_STRING], read_vocab_file[MAX_STRING];/*** 词库数组,一维数组,每一个对象都是vocab_word类型*/
struct vocab_word *vocab;/*** binary                        词向量输出控制,classes和binary共同控制。若classes = 0,由binary控制,binary = 1时二进制输出,binary = 0时文本格式输出;若classes > 0,聚类输出(只输出词和聚类类别,不输出词向量)* cbow                         1:使用CBOW模型,0:使用Skip-gram模型* debug_mode                 用于输出一些进度信息* window                      上下文窗口大小,实际使用的是动态窗口,动态大小为[0, window]* min_count                    最小词频* num_threads                   训练线程数,多线程训练* min_reduce                  最小词频*/
int binary = 0, cbow = 1, debug_mode = 2, window = 5, min_count = 5, num_threads = 12, min_reduce = 1;/*** vocab_hash                    词库的hash表,将词按hash映射到词库,vocab_hash[word_hash] = 词在词库的位置,在从语料文件建立词库时,对读入的词快速定位到其在词库中的位置,训练时不会用到*/
int *vocab_hash;/*** vocab_max_size             词库规模(词库容量),在建立词库的过程中,当词库规模到达vocab_max_size时会对词库扩容,每次扩增vocab_max_size个容量* vocab_size                  词库中实际的词个数* layer1_size                  词向量的维度大小(也是隐藏层的大小)*/
long long vocab_max_size = 1000, vocab_size = 0, layer1_size = 100;/*** train_words                  要训练的词总数(词频累加,但不包含低频词),在多线程训练时,每个线程要训练的词数是train_words / num_threads* word_count_actual            已经训练完的词频总数,因为训练迭代次数为iter次,所以最终word_count_actual = iter * train_words* iter                           每个训练线程训练迭代的次数,即每个线程对各自分配到的语料上迭代训练iter次* file_size                    训练文件大小,ftell得到,多线程训练时会对文件进行分隔,用于定位每个训练线程开始训练的文件位置* classes                     词向量输出控制,classes和binary共同控制。若classes = 0,由binary控制,binary = 1时二进制输出,binary = 0时文本格式输出;若classes > 0,聚类输出(只输出词和聚类类别,不输出词向量)*/
long long train_words = 0, word_count_actual = 0, iter = 5, file_size = 0, classes = 0;/*** alpha                      学习速率,会根据训练进度衰减* starting_alpha               初始alpha值* sample                        亚采样概率,会以一定的概率过滤高于这个值的高频词,加速训练,也提高相对低频词的精度*/
real alpha = 0.025, starting_alpha, sample = 1e-3;/*** syn0                           存储词库中每个词的词向量(哈夫曼树中叶子节点的词向量),一维数组,第i个词的词向量为syn0[i * layer1_size, (i + 1) * layer1_size - 1]* syn1                           哈夫曼树中非叶子节点的辅助向量,一维数组,第i个辅助向量为syn1[i * layer1_size, (i + 1) * layer1_size - 1]* syn1neg                       负采样时,存储每个样本对应的词向量,一维数组,第i个词的词向量为syn1neg[i * layer1_size, (i + 1) * layer1_size - 1]* expTable                     预先存储sigmod函数结果,训练中直接查表*/
real *syn0, *syn1, *syn1neg, *expTable;/*** start                       训练开始时间,用于输出训练进度*/
clock_t start;/*** hs                           hierarchical softmax标志* negative                        negative sampling标志,也是负采样数,当negative和hs同时大于0时,这两个方法是混合使用的*/
int hs = 0, negative = 5;/*** table_size                  负采样表的大小*/
const int table_size = 1e8;/*** table                      负采样表,大小为table_size,每一个采样点对应的是词在词库的位置*/
int *table;/*** 根据词频大小进行负采样** 可以这么理解:将词频视为一段线段,长度为词频大小,依次首尾相连后可以形成一段长度为1的线段,在这个线段内均匀采点table_size次,则采集到的点对应的词即组成负样本集*/
void InitUnigramTable() {int a, i;double train_words_pow = 0;                                                                      // 线段总长double d1, power = 0.75;                                                                        // d1用于迭代累加每个词线段的长度(其实是比例),power用于计算每个词的线段长度(词频的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;                                                                                           // i用于遍历词库,指示当前词位置d1 = pow(vocab[i].cn, power) / train_words_pow;                                                   // 第一个词的线段比例for (a = 0; a < table_size; a++) {                                                                // 遍历采样点/*** 收集当前词到负样本集* * 注意:在数学上,该行代码逻辑不严谨,逻辑上并没有做到均匀采样,但可以采集到在词库上从左到右扫描过的每一个词,包括低频词* * 当词i的词频比例很小时,小到没有任何采样点落在该词上,这时本不应该采样该词,但该行代码也会将其采样* 当词库和负采样规模相当,甚至更大时,该问题会凸显,当负采样规模远大于词库规模时,该问题可以得到控制,即增大了采样密度,放大了词的分辨率** 若要严格均匀采样,该行代码应该改为是:if (a / (double) table_size < d1) table[a] = i;  即保证采样点落在该词内才将其采样(边缘上为下一个词)*/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;                                                   // 采样点落在词库外,这时采集最后一个词,词频计算时可能精度丢失,导致词频总和不严格等于1,导致最后的少量采样点落在词库外}
}/*** Reads a single word from a file, assuming space + tab + EOL to be word boundaries* 每文件fin中读取一个词* @param word* @param fin* @param eof*/
void ReadWord(char *word, FILE *fin, char *eof) {int a = 0, ch;while (1) {                                                                                     // 循环读入字符,直到遇到空白符或文件尾ch = fgetc_unlocked(fin);                                                                  // 读入一个字符if (ch == EOF) {                                                                         // 读到了文件尾,读取完成,退出循环*eof = 1;break;}if (ch == 13) continue;                                                                     // 读到了回车(计算机中回车和换行的意义是不一样的),读下一行if ((ch == ' ') || (ch == '\t') || (ch == '\n')) {                                         // 读到了空白符,这时要判断该空白符是当前词的结束还是上一个词的结束if (a > 0) {                                                                           // 读到的空白符是当前词的结束if (ch == '\n') ungetc(ch, fin);                                                    // 读到了换行符,换行符在word2vec中是一个特殊的词,所以将'\n'回退给fin,会在下一次读入break;}if (ch == '\n') {                                                                     // 读到了换行符,因为换行符是一个特殊的词,在词库中用'</s>'表示,所以这里做了一个转换strcpy(word, (char *) "</s>");return;} else continue;                                                                       // 读到的空白符是上一个词的结束符,即本次还没有读到任务有意义的字符,所以继续读入字符}word[a] = ch;                                                                               // 读到了有意义的字符,将该字符添加到词上a++;if (a >= MAX_STRING - 1) a--;                                                                // Truncate too long words,词太长,截断}word[a] = 0;                                                                                   // 字符串以0结束
}/*** Returns hash value of a word* 计算词的hash值* @param word* @return*/
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;
}/*** Returns position of a word in the vocabulary; if the word is not found, returns -1* 查找词在词库中的位置,词库中存在该词,返回词的位置,否则返回-1* @param word* @return*/
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* 从文件流中读入一个词,并返回这个词在词库中的位置,这个方法是在训练时被调用,用于读入句子* @param fin* @param eof* @return*/
int ReadWordIndex(FILE *fin, char *eof) {char word[MAX_STRING], eof_l = 0;ReadWord(word, fin, &eof_l);                                                                 // 读入一个词if (eof_l) {                                                                                    // 读到了文件尾,返回-1*eof = 1;return -1;}return SearchVocab(word);                                                                     // 返回该词在词库中的位置
}/*** Adds a word to the vocabulary* 将一个词添加到词库中,并返回该词在词库中的位置,这时是将词添加到词库尾* @param word* @return*/
int AddWordToVocab(char *word) {unsigned int hash, length = strlen(word) + 1;if (length > MAX_STRING) length = MAX_STRING;                                                    // 词太长,截断vocab[vocab_size].word = (char *) calloc(length, sizeof(char));                                    // 分配词结构体存储空间strcpy(vocab[vocab_size].word, word);                                                          // 复制词vocab[vocab_size].cn = 0;                                                                        // 这里词频记为0,当从训练语料读入词时,会在调用方赋值为1;当从之前保存的词库文件读入时,会在调用方读入词频vocab_size++;                                                                                 // 词库现有词总数加1// Reallocate memory if neededif (vocab_size + 2 >= vocab_max_size) {                                                          // 持续读入词时,词库数组的容量不够了,对词库数组进行扩容1000个词位vocab_max_size += 1000;vocab = (struct vocab_word *) realloc(vocab, vocab_max_size * sizeof(struct vocab_word));}/*** 这三行代码是对该词建立hash映射,哈希冲突时线性探测继续顺序往下查找空白位置*/hash = GetWordHash(word);while (vocab_hash[hash] != -1) hash = (hash + 1) % vocab_hash_size;vocab_hash[hash] = vocab_size - 1;return vocab_size - 1;                                                                           // 返回添加的词在词库中的存储位置,即最后一个
}/*** Used later for sorting by word counts* 比较两个词的词频大小,用于对词库排序* @param a* @param b* @return*/
int VocabCompare(const void *a, const void *b) {long long l = ((struct vocab_word *) b)->cn - ((struct vocab_word *) a)->cn;if (l > 0) return 1;if (l < 0) return -1;return 0;
}/*** 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 positionqsort(&vocab[1], vocab_size - 1, sizeof(struct vocab_word), VocabCompare);                      // 词库第一个词是'</s>'不参与排序,保持在第一位,对其他词进行排序for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1;                                     // 词库排序后,哈希映射被打乱,清除size = vocab_size;train_words = 0;                                                                               // 记录在词库中出现了的词的词频总数,多线程训练时,用于给每个线程平均分配要训练的词数for (a = 0; a < size; a++) {                                                                    // 删除低频词,并重建hash映射// Words occuring less than min_count times will be discarded from the vocabif ((vocab[a].cn < min_count) && (a != 0)) {                                               // 删除低频词,但不能把'</s>'删除了,其实'</s>'词的词频字段为0,因为没给其赋值vocab_size--;                                                                           // 删除一个低频词,词库中的词总数减1free(vocab[a].word);                                                                 // 释放该词的存储空间} else {                                                                                    // 重新计算hash映射// Hash will be re-computed, 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;                                                             // 词频累加}}vocab = (struct vocab_word *) realloc(vocab, (vocab_size + 1) * sizeof(struct vocab_word));      // 删除低频词后,重新分配vocab的内存大小,以释放一些不必要的存储空间,原词库中vocab_size + 1后的低频词全部被删除// Allocate memory for the binary tree constructionfor (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* 删除低频词,* 从语料文件建立词库时,为了控制词库规模,会在词库的hash表达到填充因子上限时,调用该方法删除一些低频词*/
void ReduceVocab() {int a, b = 0;                                                                                  // a:遍历词库,b:用于规整词库,将右侧的非低频词移到词库左侧unsigned int hash;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;                                                                                    // 删除低频词后的词库大小,最后剩下b个词,词频均大于min_reducefor (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1;                                        // 规整词库后,hash映射被打乱,清除for (a = 0; a < vocab_size; a++) {                                                             // 重建hash映射// Hash will be re-computed, as it is not actualhash = GetWordHash(vocab[a].word);while (vocab_hash[hash] != -1) hash = (hash + 1) % vocab_hash_size;                            // 哈希冲突,线性探测继续顺序往下查找vocab_hash[hash] = a;}fflush(stdout);min_reduce++;                                                                                    // 每次删除低频词后,词库中的词频都已经大于min_reduce了,若下次还要删除低频词,必须删除更大词频的词了,因此min_reduce加1
}/*** Create binary Huffman tree using the word counts* Frequent words will have short uniqe binary codes** 构建哈夫曼树,就是按词频从小到大依次构建哈夫曼树的节点,同时得到每个节点的哈夫曼编码* 函数涉及三个一维数组:* 1、count                       存储哈夫曼树每个节点的权重值(权重值用词频来表示)* 2、binary                   存储哈夫曼树每个节点的哈夫曼编码(每个节点的编码为0或1)* 3、parent_node              存储哈夫曼树每个节点的父节点位置** 词库vocab是一个一维数组,词库大小为vocab_size,按词频从大到小排列词** 哈夫曼树的构建过程如下:* 首先创建一个一维数组:count,大小为:2 * vocab_size + 1,用于存储哈夫曼树每个节点的权重。其实哈夫曼树的节点总数 = 叶子节点个数 + 非叶子节点个数 = 叶子节点个数 + (叶子节点个数 - 1) = 2 * vocab_size - 1*     1. 将词库中每个词的词频依次写进count的[0,vocab_size - 1]位置中,相当于每个叶子节点的权重,这部分权重是非递增的*     2. 将count数组[vocab_size, 2 * vocab_size]位置用1e15填充,相当于每个非叶子节点的权重,每构建一个非叶子节点,会将非叶子节点的权重写进第一个1e15的位置,这部分权重是非递减的,因此count数组里的权重值是中间小,两边大* 接下来构建哈夫曼树主要围绕权重数组count,从count数组的中间位置[vocab_size - 2, vocab_size - 1]开始,向两边查找count数组中两个权重最小的且未处理过的节点,找到这两个权重最小的位置。* 之后合并这两个最小的权重,构建一个父节点,这个父节点的权重等于这两个最小权重的和,将这个权重值写进count数组[vocab_size, 2 * vocab_size]部分的第一个1e15的位置上,而这两上最小权重节点与新构建的父节点就构成了父子关系* 循环这个过程,直到构建根节点* 注意,在构建哈夫曼树的同时,会记录每个节点的哈夫曼编码(根节点除外),在父节点的两个子节点中,权重大的编码为1,代表负类,权重小的编码为0,代表正类;同时也会在子节点位置上记录其父节点的位置,并将这个关系记录到parent_node数组中*** 最后是构建词库中每个词(对应哈夫曼树的叶子节点)的哈夫曼编码和路径* 遍历词库中的每个词,从parent_node数组中可以一路找到根节点,这个路径的逆序就是词的哈夫曼路径,而这个路径上的每个节点的编码(除根节点)就构成了该词的哈夫曼编码*/
void CreateBinaryTree() {/*** min1i                 最小权重节点的位置* min2i                    次小权重节点的位置* pos1                     叶子节点部分([0,vocab_size - 1])权重最小的位置,用于查找min1i和min2i,从右向左移动* pos2                       非叶子节点部分([vocab_size, 2 * vocab_size])权重最小的位置,用于查找min1i和min2i,从左向右移动* point                  记录从根节点到词的路径*/long long a, b, i, min1i, min2i, pos1, pos2, point[MAX_CODE_LENGTH];char code[MAX_CODE_LENGTH];                                                                        // 记录词的哈夫曼编码long long *count = (long long *) calloc(vocab_size * 2 + 1, sizeof(long long));                   // 储哈夫曼树每个节点的权重值,大小为:2 * vocab_size + 1long long *binary = (long long *) calloc(vocab_size * 2 + 1, sizeof(long long));                // 储哈夫曼树每个节点的哈夫曼编码,大小为:2 * vocab_size + 1long long *parent_node = (long long *) calloc(vocab_size * 2 + 1, sizeof(long long));         // 存储哈夫曼树每个节点的父节点位置,大小为:2 * vocab_size + 1for (a = 0; a < vocab_size; a++) count[a] = vocab[a].cn;                                        // 将词库中每个词的词频依次写进count的[0,vocab_size - 1]位置中for (a = vocab_size; a < vocab_size * 2; a++) count[a] = 1e15;                                    // 将count数组[vocab_size, 2 * vocab_size]位置用1e15填充pos1 = vocab_size - 1;                                                                         // 设置pos1为左侧(叶子节点)权重最小的位置,从右向左移动,初始值为最后一个词的下标,即vocab_size - 1pos2 = vocab_size;                                                                             // 设置pos2为右侧(非叶子节点)权重最小的位置,从左向右移动,初始时没有非叶子节点,初值为vocab_size// Following algorithm constructs the Huffman tree by adding one node at a timefor (a = 0; a < vocab_size - 1; a++) {                                                            // 在哈夫曼树中,有vocab_size - 1个非叶子节点,因此要循环vocab_size - 1次,每一次循环会合并两个最小的权重,构建一个非叶子节点// First, find two smallest nodes 'min1, min2'/*** 第一个if查找最小的权重位置*/if (pos1 >= 0) {                                                                         // 叶子节点未遍历完if (count[pos1] < count[pos2]) {                                                      // 叶子节点的权重小于非叶子节点时,最小权重为叶子节点,将pos1赋值给min1i后左移一个min1i = pos1;pos1--;} else {                                                                              // 非叶子节点的权重小于叶子节点时,最小权重为非叶子节点,将pos2赋值给min1i后右移一个min1i = pos2;pos2++;}} else {                                                                                  // 叶子节点已经遍历完,最小的权重位于右侧的非叶子节点,向右找最小权重的下标min1i = pos2;pos2++;}/*** 第二个if查找次小的权重位置*/if (pos1 >= 0) {                                                                          // 叶子节点未遍历完if (count[pos1] < count[pos2]) {                                                      // 叶子节点的权重小于非叶子节点时,次小权重为叶子节点,将pos1赋值给min2i后左移一个min2i = pos1;pos1--;} else {                                                                              // 非叶子节点的权重小于叶子节点时,次小权重为非叶子节点,将pos2赋值给min2i后右移一个min2i = pos2;pos2++;}} else {                                                                                  // 叶子节点已经遍历完,次小的权重位于右侧的非叶子节点,向右找次小权重的下标min2i = pos2;pos2++;}count[vocab_size + a] = count[min1i] + count[min2i];                                        // 合并找到的两个最小的权重,存储到右侧第一个1e15的位置上,其实这个位置就是vocab_size + a,即在构建第a(a从0编号)个非叶子节点,存储过程即可理解成构建非叶子节点parent_node[min1i] = vocab_size + a;                                                       // 最小权重节点(min1i)的父节点为新构建的非叶子节点parent_node[min2i] = vocab_size + a;                                                      // 次小权重节点(min2i)的父节点为新构建的非叶子节点binary[min2i] = 1;                                                                         // 哈夫曼树中,权重大的子节点(即min2i)的编码为1,代表负类,权重小的子节点(即min1i)的编码为0,代表正类,数组中默认为0,因此没有赋值语句}// Now assign binary code to each vocabulary word/*** 遍历词库中每个词,沿着父节点路径,构建词的哈夫曼编码和路径*/for (a = 0; a < vocab_size; a++) {b = a;                                                                                      // b:从当前词(叶节点)开始记录每一个父节点,即当前词的哈夫曼路径上的每一个节点i = 0;                                                                                       // i:记录哈夫曼编码长度,编码不包括根节点while (1) {                                                                                    // 沿沿着父节点路径,记录哈夫曼编码和路径,直到根节点code[i] = binary[b];                                                                 // 记录哈夫曼编码,注意,这时code数组里记录的编码是逆序的point[i] = b;                                                                            // 记录路径,注意,这时point数组里记录的路径是逆序的,point[0] = a < vocab_size,point[i] = b >= vocab_sizei++;                                                                                  // 编码长度加1b = parent_node[b];                                                                       // 找下一个父节点if (b == vocab_size * 2 - 2) break;                                                     // vocab_size * 2 - 2为根节点位置,找到根节点时,该词的哈夫曼编码和路径即已记录到code和point中,但顺序是逆序的}vocab[a].codelen = i;                                                                      // 记录词的哈夫曼编码的长度,编码不包括根节点vocab[a].point[0] = vocab_size - 2;                                                         // 根节点位置,point[0]即是根节点位置(vocab_size * 2 - 2 - vocab_size),路径的长度比编码的长度大1,因此这里在point数组里记录了根节点for (b = 0; b < i; b++) {                                                                   // 逆序处理vocab[a].code[i - b - 1] = code[b];                                                     // 编码逆序,没有根节点,所以i - b减去1/*** 路径逆序,point的长度比code长1,即根节点,point数组“最后”一个值是负的,point路径是为了定位节点位置,叶子节点即是词本身,不用定位,所以训练时这个负数是用不到的* * 在count数组中,非叶子节点的下标范围为[vocab_size, vocab_size * 2 - 2]* 但在词向量训练时,非叶子节点词向量是存储在syn1数组中的,虽然syn1数组的大小为vocab_size * layer1_size,但可以理解成vocab_size个向量,即大小为vocab_size* 所以这里做了一个映射,把count数组非叶子节点的范围[vocab_size, vocab_size * 2 - 2]顺序映射到syn1数组的向量范围的[0, vocab_size - 2]* 所以这里用point[b]减去了vocab_size,包括上面根节点的位置也是减去了vocab_size*/vocab[a].point[i - b] = point[b] - vocab_size;}}free(count);free(binary);free(parent_node);
}/*** 从原始语料训练文件读入词,构建词库*/
void LearnVocabFromTrainFile() {char word[MAX_STRING], eof = 0;FILE *fin;long long a, i, wc = 0;                                                                          // wc:在debug_mode模式中,每读入1000000个词输出一次信息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;                                                                                 // 初始词库大小为0AddWordToVocab((char *) "</s>");                                                             // 将'</s>'加入到词库的第一个位置while (1) {                                                                                        // 从语料文件中读入每个词,添加到词库中,在读完语料文件之前,词库是未按词频排序的ReadWord(word, fin, &eof);                                                                   // 读入一个词if (eof) break;                                                                             // 文件结束train_words++;                                                                             // 词频总数加1wc++;if ((debug_mode > 1) && (wc >= 1000000)) {                                                   // debug_mode模式打印进度printf("%lldM%c", train_words / 1000000, 13);fflush(stdout);wc = 0;}i = SearchVocab(word);                                                                       // 查找该词是否已经在词库中,若不存在,添加;若已经存在,词频加1if (i == -1) {                                                                              // 添加新词,词频为1a = AddWordToVocab(word);vocab[a].cn = 1;} else vocab[i].cn++;                                                                       // 词已经在词库中,词频加1/*** 每添加一个词,判断一次词库大小,若大于填充因子上限,删除一次低频词* * 注意,这里有一个问题,在删除低频词时,语料文件可能是未处理完的,只是读取了一部分词* 所以词库当前状态下的词频信息是局部的,不是训练文件全局的* 这时删除低频词时,是把局部的低频词删除,但局部低频词未必是全局低频词,例如,一个词在训练文件的前一部分少量出现,但在后面部分,大量出现* 不过考虑到词库规模(千万级),这个问题也不太可能出现*/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);
}/*** 保存词库到文件,词 + 词频*/
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);
}/*** 从之前保存的词库文件中读入词,构建词库* 词库文件每行按(词 词频)的格式存储。** 并获取训练语料的语料文件的文件大小,注意,read_vocab_file文件是之保存的词库文件;train_file是要训练的语料文件,从该文件中读入句子,训练词向量*/
void ReadVocab() {long long a, i = 0;char c, eof = 0;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;                                      // 清空词的hash映射vocab_size = 0;                                                                                   // 初始词库大小为0while (1) {                                                                                      // 从词库文件中读入每个词,添加到词库中ReadWord(word, fin, &eof);                                                                  // 读入一个词到if (eof) break;                                                                                // 文件结束a = AddWordToVocab(word);                                                                   // 添加到词库中,并返回该词在词库中的位置fscanf(fin, "%lld%c", &vocab[a].cn, &c);                                                 // 读入词频,注意这里的&c,该变量用于读入每行最后的换行符,利于读取下一行的词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);
}/*** 初始化** 为词向量数组分配存储空间,并进行初始化* 若使用hierarchical softmax,为哈夫曼树非叶子节点辅助向量数组分配存储空间,并初始化为0* 若使用negative sampling,为负采样样本向量数组分配存储空间,并初始化为0** 最后构建建哈夫曼树*/
void InitNet() {long long a, b;unsigned long long next_random = 1;/*** vocab_size个词,每个词的向量维度为layer1_size*/a = posix_memalign((void **) &syn0, 128, (long long) vocab_size * layer1_size * sizeof(real));if (syn0 == NULL) {printf("Memory allocation failed\n");exit(1);}/*** hierarchical softmax*/if (hs) {/*** vocab_size个词,每个词的向量维度为layer1_size*/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;}/*** negative sampling*/if (negative > 0) {/*** vocab_size个词,每个词的向量维度为layer1_size*/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 (a = 0; a < vocab_size; a++)for (b = 0; b < layer1_size; b++)syn1neg[a * layer1_size + b] = 0;}/*** 每个词向量每个维度用[-0.5/layer1_size, 0.5/layer1_size]范围的数初始化*/for (a = 0; a < vocab_size; a++)for (b = 0; b < layer1_size; b++) {next_random = next_random * (unsigned long long) 25214903917 + 11;syn0[a * layer1_size + b] = (((next_random & 0xFFFF) / (real) 65536) - 0.5) / layer1_size;}/*** 创建哈夫曼树* 当只使用negative sampling训练词向量时,构建哈夫曼树多余了*/CreateBinaryTree();
}/*** 训练词向量* @param id 线程编号[0, num_threads - 1],不是线程号,表示num_threads个线程中的第几个线程,用于分割语料文件,确定本线程从语料文件读取句子的开始位置* @return** 多线程训练,将训练语料分成与线程个数相等的份数,每个线程训练其中一份,每个线程对其分配到的语料子集迭代训练iter次* 训练按句子进行,每次从语料文件中读取一个句子,如果句子超长(超过MAX_SENTENCE_LENGTH个词),则截断,剩余的被当作下一个句子** 根据cbow参数决定选择CBOW模型还是Skip-gram模型,cbow=1:使用CBOW模型;cbow=0:使用Skip-gram模型。但是要注意的是不管选择哪个模型都可以混合使用hierarchical softmax和negative sampling** CBOW模型的训练思想是根据上下文预测当前词,得到一个预测误差,将该误差反向更新到每一个上下文词上,让这些上下文词向更正确的预测当前词的方向更新* Skip-gram模型的训练思想与CBOW模型相反,是根据当前词预测上下文,得到一个预测误差,将该误差反向更新到当前词上,让当前词向更正确的预测上下文的方向更新** 但是在word2vec中,Skip-gram模型的并没有按上面的思想进行训练,而是借鉴了CBOW的思想,其思路是用上下文中的每一个词来预测当前词,得到一个预测误差,再将该误差反向更新到该上下文词上** 注意,word2vec是多线程训练,全局训练参数是共享的,这一点要特别注意,下面的注释中没有刻意说明这一点** word2vec训练的数学推导可以参照CSDN博文:https://blog.csdn.net/sealir/article/details/85269567*/
void *TrainModelThread(void *id) {/*** a                        用于遍历对象* b                       动态上下文窗口大小,动态大小为[0, window]* d                        用于遍历对象* word                        当前词,注意:word的值其实是词在词库中的位置,为了注释方便,以下注释中提到词时都是指词在词库中的位置,辅助向量也是一样* last_word             用于遍历上下文* sentence_length            当前训练句子的长度(词数)* sentence_position      当前词在句子中的位置*/long long a, b, d, cw, word, last_word, sentence_length = 0, sentence_position = 0;/*** word_count                已经训练的词总数(词频累加)* last_word_count           上一次记录的已经训练的词频总数,与word_count一起用于衰减学习率,每训练10000个词衰减一次* sen                      当前待训练的句子,存储的是每个词在词库中的位置*/long long word_count = 0, last_word_count = 0, sen[MAX_SENTENCE_LENGTH + 1];/*** l1                      在Skip-gram模型中,用于定位上下文词的词向量在syn0中的起始位置* l2                        hierarchical softmax时,用于定位非叶子节点辅助向量在syn1中的起始位置;negative sampling时,用于定位采样点(包括正负样本)辅助向量在syn1neg中的起始位置* c                       用于遍历对象* target                  在negative sampling中,表示采样点(包括正负样本)词* label                  在negative sampling中,表示样本标签,1:正样本;0:负样本* local_iter               训练剩余迭代次数,一共迭代iter次*/long long l1, l2, c, target, label, local_iter = iter;unsigned long long next_random = (long long) id;                                             // 用于生成随机数char eof = 0;                                                                                    // 训练文件结束符标志real f, g;clock_t now;real *neu1 = (real *) calloc(layer1_size, sizeof(real));                                     // 用于CBOW模型,表示上下文各词的词向量的加和real *neu1e = (real *) calloc(layer1_size, sizeof(real));                                     // 累加词向量的修正量,用neu1e修正词向量,即词向量 += neu1eFILE *fi = fopen(train_file, "rb");                                                                // 打开训练文件fseek(fi, file_size / (long long) num_threads * (long long) id, SEEK_SET);                     // 定位当前线程开始训练的文件位置/*** 当选择CBOW模型时,用全体上下文词来预测当前词,再反向修正上下文的词的词向量* 当选择Skip-gram模型时,用每一个上下文词来预测当前词,再反向修正该上下文词的词向量** 每次读取一个句子(句子过长时截断),按句子为单位进行训练* 对分配给当前线程的全部句子迭代训练iter次*/while (1) {/*** 每训练10000个词,衰减一次学习率*/if (word_count - last_word_count > 10000) {word_count_actual += word_count - last_word_count;                                        // word_count_actual词频累加,全部线程都累加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;}/*** 当前句子训练完成,读取下一个句子(每一次训练迭代开始,sentence_length也是为0)*/if (sentence_length == 0) {while (1) {                                                                               // 读取句子,直到遇到文件尾、换行符或被截断word = ReadWordIndex(fi, &eof);                                                      // 读取一个词,返回词在词库中的索引if (eof) break;                                                                       // 遇到文件尾if (word == -1) continue;                                                         // 词库中不存在的词,跳过word_count++;                                                                        // 词频累加if (word == 0) break;                                                              // 遇到换行符'</s>',词库中第一个词是'</s>',即word = 0// 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;next_random = next_random * (unsigned long long) 25214903917 + 11;if (ran < (next_random & 0xFFFF) / (real) 65536) continue;}sen[sentence_length] = word;                                                        // 将该词加入到句子中sentence_length++;                                                                    // 句子长度加1if (sentence_length >= MAX_SENTENCE_LENGTH) break;                                 // 截断超长句子,剩余的被视为下一个句子}sentence_position = 0;                                                                    // 句子读取完成,设置句子的初始训练位置为第一个词}/*** 当前线程遇到文件尾,或者分配给该线程的全部词已完成了一次训练,设置一些初始值后进行下一次迭代训练*/if (eof || (word_count > train_words / num_threads)) {word_count_actual += word_count - last_word_count;                                        // word_count_actual词频累加local_iter--;                                                                           // 剩余训练次数减1if (local_iter == 0) break;                                                                // 没有训练次数了,已经训练完成word_count = 0;                                                                            // 每次迭代开始,当前线程训练的词频总数设置为0last_word_count = 0;                                                                   // 每次迭代开始,上一次记录的已经训练的词频总数设置为0sentence_length = 0;                                                                   // 每次迭代开始,句子长度设置为0fseek(fi, file_size / (long long) num_threads * (long long) id, SEEK_SET);             // 每次迭代开始,重新定位当前线程开始训练的文件位置continue;}word = sen[sentence_position];                                                             // word为当前词if (word == -1) continue;for (c = 0; c < layer1_size; c++) neu1[c] = 0;                                             // CBOW模型中,重置上下文词向量加和向量为0for (c = 0; c < layer1_size; c++) neu1e[c] = 0;                                              // 重置词向量的修正量为0next_random = next_random * (unsigned long long) 25214903917 + 11;b = next_random % window;                                                                    // 生成动态上下文窗口大小,动态窗口在句子中的范围是sen[sentence_position - window + b, sentence_position + window - b],注意可能会超出句子范围/*** CBOW模型,用全体上下文词来预测当前词,再反向修正上下文的词的词向量*/if (cbow) {  //train the cbow architecture// in -> hiddencw = 0;                                                                                  // 上下文窗口内的词数,不计当前词for (a = b; a < window * 2 + 1 - b; a++)                                                // 累加上下文词的词向量if (a != window) {                                                                    // 不计当前词c = sentence_position - window + a;                                               // c用于定位上下文词在句子中的真实位置,sentence_position位置是当前词if (c < 0) continue;                                                         // 上下文窗口超出句子范围if (c >= sentence_length) continue;                                               // 上下文窗口超出句子范围last_word = sen[c];                                                              // last_word表示上下文词if (last_word == -1) continue;for (c = 0; c < layer1_size; c++) neu1[c] += syn0[c + last_word * layer1_size];  // 上下文各词的词向量加和cw++;                                                                           // 上下文窗口内的词数加1}if (cw) {                                                                                // 当前词有上下文,有上下文时才进行训练for (c = 0; c < layer1_size; c++) neu1[c] /= cw;                                 // 将上下文各词词向量加和的和向量平均化/*** hierarchical softmax,用哈夫曼树进行训练* 遍历当前词的哈夫曼路径,在每个非叶子节点进行一次训练,预测一次子节点,其实就是对子节点做一次二分类,累加每次预测的误差*/if (hs)for (d = 0; d < vocab[word].codelen; d++) {                                      // 从根节点开始,遍历路径上的每一个非叶子节点,注意是非叶子节点,根据父节点对子节点进行预测f = 0;l2 = vocab[word].point[d] * layer1_size;                                    // l2用于定位当前节点的辅助向量在syn1的开始位置// Propagate hidden -> outputfor (c = 0; c < layer1_size; c++) f += neu1[c] * syn1[c + l2];             // 将上下文词的加和向量(已被平均) 与 当前节点的辅助向量 做内积/*** 从缓存中读sigmod值,可以理解成预测子节点的类别,word2vec中0表示正类,1表示负类,注意在以下代码中,当f值超出范围时,不对该节点进行预测,直接跳过该节点*/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/*** f                                  表示根据当前节点对其子节点进行分类时得到的子节点的分类标签* 1 - vocab[word].code[d]              表示在哈夫曼树中当前节点的子节点的真实分类标签(哈夫曼树中,编码0表示正类,编码1表示负类,(1 - 编码)即为分类标签)* 1 - vocab[word].code[d] - f           即真实分类标签与预测分类标签之间的差值*/g = (1 - vocab[word].code[d] - f) * alpha;// Propagate errors output -> hiddenfor (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];             // 累加词向量的修正量,这里遍历了路径上的每个节点,先累加修正量,下面会更新到上下文每个词的词向量中// Learn weights hidden -> outputfor (c = 0; c < layer1_size; c++) syn1[c + l2] += g * neu1[c];             // 更新当前节点的辅助向量}// NEGATIVE SAMPLING/*** negative sampling,用负采样进行训练* negative也表示也采样次数,即采样negative个负样本,这样就收集了一个正样本和negative个负样本,在每个样本上进行一次训练*/if (negative > 0)for (d = 0; d < negative + 1; d++) {                                         // 在每一个样本上进行一次训练,累计训练误差if (d == 0) {target = word;                                                            // 当前词为正样本label = 1;                                                               // 正样本标签为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;                                                               // 负样本标签为0}l2 = target * layer1_size;                                                  // l2用于定位样本辅助向量在syn1neg的开始位置f = 0;for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1neg[c + l2];           // 将上下文词的加和向量(已被平均) 与 负样本辅助向量 做内积/*** 从缓存中读sigmod值,可以理解成预测负样本的标签,label为样本的真实分类,1,0,expTable(x)为预测分类,相减等到分类误差*/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];}}}/*** Skip-gram模型,用每一个上下文词来预测当前词,再反向修正该上下文词的词向量*/else {  //train skip-gramfor (a = b; a < window * 2 + 1 - b; a++)                                                // 遍历上下文中的每个词,做一次训练(当前词不用做训练)if (a != window) {                                                                   // 跳过当前词c = sentence_position - window + a;                                               // c用于定位上下文词在句子中的真实位置,sentence_position位置是当前词if (c < 0) continue;                                                         // 上下文窗口超出句子范围if (c >= sentence_length) continue;                                               // 上下文窗口超出句子范围last_word = sen[c];                                                              // last_word表示上下文词if (last_word == -1) continue;l1 = last_word * layer1_size;                                                    // l1用于定位上下文词的词向量在syn0中的起始位置for (c = 0; c < layer1_size; c++) neu1e[c] = 0;                                  // 重置词向量的修正量为0// HIERARCHICAL SOFTMAX/*** hierarchical softmax,用哈夫曼树进行训练* 遍历当前词的哈夫曼路径,在每个非叶子节点进行一次训练,预测一次子节点,其实就是对子节点做一次二分类,累加每次预测的误差*/if (hs)for (d = 0; d < vocab[word].codelen; d++) {                                  // 从根节点开始,遍历路径上的每一个非叶子节点,注意是非叶子节点,根据父节点对子节点进行预测f = 0;l2 = vocab[word].point[d] * layer1_size;                                // l2用于定位当前节点的辅助向量在syn1的开始位置// Propagate hidden -> outputfor (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1[c + l2];       // 将该上下文词词向量 与 当前节点辅助向量 做内积/*** 从缓存中读sigmod值,可以理解成预测子节点的类别,word2vec中0表示正类,1表示负类,注意在以下代码中,当f值超出范围时,不对该节点进行预测,直接跳过该节点*/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/*** f                                表示根据当前节点对其子节点进行分类时得到的子节点的分类标签* 1 - vocab[word].code[d]          表示在哈夫曼树中当前节点的子节点的真实分类标签(哈夫曼树中,编码0表示正类,编码1表示负类,(1 - 编码)即为分类标签)* 1 - vocab[word].code[d] - f       即真实分类标签与预测分类标签之间的差值*/g = (1 - vocab[word].code[d] - f) * alpha;// Propagate errors output -> hiddenfor (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];         // 累加词向量的修正量,这里遍历了路径上的每个节点,先累加修正量,下面会更新到该上下文词的词向量中// Learn weights hidden -> outputfor (c = 0; c < layer1_size; c++) syn1[c + l2] += g * syn0[c + l1];        // 更新当前节点的辅助向量}// NEGATIVE SAMPLING/*** negative sampling,用负采样进行训练* negative也表示也采样次数,即采样negative个负样本,这样就收集了一个正样本和negative个负样本,在每个样本上进行一次训练*/if (negative > 0)for (d = 0; d < negative + 1; d++) {                                     // 在每一个样本上进行一次训练,累计训练误差if (d == 0) {target = word;                                                        // 当前词为正样本label = 1;                                                           // 正样本标签为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;                                                          // 负样本标签为0}l2 = target * layer1_size;                                              // l2用于定位样本辅助向量在syn1neg的开始位置f = 0;for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1neg[c + l2]; // 将该上下文词词的词向量 与 负样本辅助向量 做内积/*** 从缓存中读sigmod值,可以理解成预测负样本的标签,label为样本的真实分类,1,0,expTable(x)为预测分类,相减等到分类误差*/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 -> hiddenfor (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、从词库文件或语料文件中构建词库,并根据参数是否保存词库* 2、调用初始化函数初始化训练参数* 3、若使用negative sampling,初始化负采样表* 4、创建num_threads个训练线程,启动并等待全部线程训练结束(注意:在多线程训练时,训练参数是线程共享的)* 5、输出词向量*/
void TrainModel() {long a, b, c, d;FILE *fo;pthread_t *pt = (pthread_t *) malloc(num_threads * sizeof(pthread_t));                         // 修建一个线程数组,大小为num_threadsprintf("Starting training using file %s\n", train_file);starting_alpha = alpha;                                                                         // 记录初始学习率,学习率会随着训练进度衰减,衰减到一定程度后不再衰减if (read_vocab_file[0] != 0) ReadVocab(); else LearnVocabFromTrainFile();                        // 若提供了词库文件,则从词库文件中构建词库,否则从训练语料文件中构建词库if (save_vocab_file[0] != 0) SaveVocab();                                                      // 若提供词库输出文件,保存词库到文件if (output_file[0] == 0) return;                                                               // 未设置词向量输出文件,直接退出InitNet();                                                                                     // 训练参数初始化,词向量、辅助向量、负样本向量、哈夫曼树if (negative > 0) InitUnigramTable();                                                           // 若使用negative sampling,初始化负采样表start = clock();                                                                             // 记录CPU时间,用于输出训练进度for (a = 0; a < num_threads; a++) pthread_create(&pt[a], NULL, TrainModelThread, (void *) a);   // 创建num_threads个训练线程,线程执行函数的参数是线程编号(不是线程号),用于逻辑分割语料文件for (a = 0; a < num_threads; a++) pthread_join(pt[a], NULL);                                    // 开始训练并等待每个线程结束,全部线程结束即训练完成/*** 全部线程训练完成后,将训练好的词向量输出到output_file文件* 输出由classes和binary两个参数共同控制,classes表示是否进行聚类输出,binary表示在不聚类输出时,以二进制输出还是文本输出*/fo = fopen(output_file, "wb");                                                                 // 打开词向量输出文件if (classes == 0) {                                                                               // 不聚类输出// Save the word vectorsfprintf(fo, "%lld %lld\n", vocab_size, layer1_size);for (a = 0; a < vocab_size; a++) {                                                          // 遍历词库的每个词,将词的词向量输出到文件fprintf(fo, "%s ", vocab[a].word);/*** 输出每个词向量每个维度值* binary用于判断以是否以二进制输出,1:二进制输出,0:文本输出*/if (binary) for (b = 0; b < layer1_size; b++) fwrite(&syn0[a * layer1_size + b], sizeof(real), 1, fo);else for (b = 0; b < layer1_size; b++) fprintf(fo, "%lf ", syn0[a * layer1_size + b]);fprintf(fo, "\n");}} else {                                                                                      // 先按K均值聚类后,再输出词向量,classes:表示聚类个数,这时不输出词向量,而是输出聚类类别// Run K-means on the word vectorsint clcn = classes, iter = 10, closeid;                                                       // clcn:聚类个数,iter:迭代次数,closeid:表示某个词最近的类别编号int *centcn = (int *) malloc(classes * sizeof(int));                                     // 存储每个类别的词个数,一维数组,大小为classesint *cl = (int *) calloc(vocab_size, sizeof(int));                                          // 每个词对应的类别编号,一维数组,大小为vocab_sizereal closev, x;                                                                               // x:词向量和聚类中心的内积(余弦距离),值越大说明距离越近;closev:最大的内积,这时距离最近real *cent = (real *) calloc(classes * layer1_size, sizeof(real));                            // 每个类别的聚类中心,一维数组,第i个类的中心为cent[i * layer1_size, (i + 1) * layer1_size - 1]for (a = 0; a < vocab_size; a++) cl[a] = a % clcn;                                          // 初始化每个词的类编号,即初始聚类(这跟打牌时的发牌过程很相似哦)for (a = 0; a < iter; a++) {                                                              // 迭代iter次,每次迭代重新进行一次聚类,并计算新的聚类中心for (b = 0; b < clcn * layer1_size; b++) cent[b] = 0;                                 // 每次迭代开始,设置每个聚类中心为0for (b = 0; b < clcn; b++) centcn[b] = 1;                                             // 每次迭代开始,设置每个类别的词汇个数为1/*** 重新计算每个类别的聚类中心和词汇个数,这个“聚类中心”并不是真正的中心,这一阶段是累加“中心”的每个分量,将“聚类中心”的每个分量除以类别中词汇个数才是真正的中心*/for (c = 0; c < vocab_size; c++) {for (d = 0; d < layer1_size; d++) cent[layer1_size * cl[c] + d] += syn0[c * layer1_size + d];centcn[cl[c]]++;}/*** 将上面累加的“聚类中心”除以各个类别的词汇个数,得到真正的聚类中心,并将中心向量归一化*/for (b = 0; b < clcn; b++) {closev = 0;for (c = 0; c < layer1_size; c++) {cent[layer1_size * b + c] /= centcn[b];                                           // “聚类中心”的每个分量除以词汇个数得到真正的聚类中心closev += cent[layer1_size * b + c] * cent[layer1_size * b + c];               // 累加聚类中心每个分量的平方,用于计算聚类中心向量的长度}closev = sqrt(closev);                                                               // 计算计算聚类中心向量的长度for (c = 0; c < layer1_size; c++) cent[layer1_size * b + c] /= closev;              // 聚类中心向量归一化,方便计算余弦距离}/*** 计算每个词向量到每个聚类中心的内积(余弦距离),内积最大表示该词向量到该聚类中心最近,因此更新该词向量的聚类类别*/for (c = 0; c < vocab_size; c++) {closev = -10;closeid = 0;for (d = 0; d < clcn; d++) {x = 0;for (b = 0; b < layer1_size; b++) x += cent[layer1_size * d + b] * syn0[c * layer1_size + b];if (x > closev) {                                                             // 找最近的聚类中心closev = x;closeid = d;}}cl[c] = closeid;                                                                 // 重新计算词的最近聚类中心后, 更新词向量的聚类类别}}// Save the K-means classes/*** 以文本格式保存词向量和聚类类别*/for (a = 0; a < vocab_size; a++) fprintf(fo, "%s %d\n", vocab[a].word, cl[a]);free(centcn);free(cent);free(cl);}fclose(fo);
}int ArgPos(char *str, int argc, char **argv) {int a;for (a = 1; a < argc; a++)if (!strcmp(str, argv[a])) {if (a == argc - 1) {printf("Argument missing for %s\n", str);exit(1);}return a;}return -1;
}int main(int argc, char **argv) {int i;if (argc == 1) {printf("WORD VECTOR estimation toolkit v 0.1c\n\n");printf("Options:\n");printf("Parameters for training:\n");printf("\t-train <file>\n");printf("\t\tUse text data from <file> to train the model\n");printf("\t-output <file>\n");printf("\t\tUse <file> to save the resulting word vectors / word clusters\n");printf("\t-size <int>\n");printf("\t\tSet size of word vectors; default is 100\n");printf("\t-window <int>\n");printf("\t\tSet max skip length between words; default is 5\n");printf("\t-sample <float>\n");printf("\t\tSet threshold for occurrence of words. Those that appear with higher frequency in the training data\n");printf("\t\twill be randomly down-sampled; default is 1e-3, useful range is (0, 1e-5)\n");printf("\t-hs <int>\n");printf("\t\tUse Hierarchical Softmax; default is 0 (not used)\n");printf("\t-negative <int>\n");printf("\t\tNumber of negative examples; default is 5, common values are 3 - 10 (0 = not used)\n");printf("\t-threads <int>\n");printf("\t\tUse <int> threads (default 12)\n");printf("\t-iter <int>\n");printf("\t\tRun more training iterations (default 5)\n");printf("\t-min-count <int>\n");printf("\t\tThis will discard words that appear less than <int> times; default is 5\n");printf("\t-alpha <float>\n");printf("\t\tSet the starting learning rate; default is 0.025 for skip-gram and 0.05 for CBOW\n");printf("\t-classes <int>\n");printf("\t\tOutput word classes rather than word vectors; default number of classes is 0 (vectors are written)\n");printf("\t-debug <int>\n");printf("\t\tSet the debug mode (default = 2 = more info during training)\n");printf("\t-binary <int>\n");printf("\t\tSave the resulting vectors in binary moded; default is 0 (off)\n");printf("\t-save-vocab <file>\n");printf("\t\tThe vocabulary will be saved to <file>\n");printf("\t-read-vocab <file>\n");printf("\t\tThe vocabulary will be read from <file>, not constructed from the training data\n");printf("\t-cbow <int>\n");printf("\t\tUse the continuous bag of words model; default is 1 (use 0 for skip-gram model)\n");printf("\nExamples:\n");printf("./word2vec -train data.txt -output vec.txt -size 200 -window 5 -sample 1e-4 -negative 5 -hs 0 -binary 0 -cbow 1 -iter 3\n\n");return 0;}/*** 文件名均空*/output_file[0] = 0;save_vocab_file[0] = 0;read_vocab_file[0] = 0;/*** 读入参数*/if ((i = ArgPos((char *) "-size", argc, argv)) > 0) layer1_size = atoi(argv[i + 1]);if ((i = ArgPos((char *) "-train", argc, argv)) > 0) strcpy(train_file, argv[i + 1]);if ((i = ArgPos((char *) "-save-vocab", argc, argv)) > 0) strcpy(save_vocab_file, argv[i + 1]);if ((i = ArgPos((char *) "-read-vocab", argc, argv)) > 0) strcpy(read_vocab_file, argv[i + 1]);if ((i = ArgPos((char *) "-debug", argc, argv)) > 0) debug_mode = atoi(argv[i + 1]);if ((i = ArgPos((char *) "-binary", argc, argv)) > 0) binary = atoi(argv[i + 1]);if ((i = ArgPos((char *) "-cbow", argc, argv)) > 0) cbow = atoi(argv[i + 1]);if (cbow) alpha = 0.05;                                                                          // 采用CBOW模型时,学习率 = 0.05if ((i = ArgPos((char *) "-alpha", argc, argv)) > 0) alpha = atof(argv[i + 1]);if ((i = ArgPos((char *) "-output", argc, argv)) > 0) strcpy(output_file, argv[i + 1]);if ((i = ArgPos((char *) "-window", argc, argv)) > 0) window = atoi(argv[i + 1]);if ((i = ArgPos((char *) "-sample", argc, argv)) > 0) sample = atof(argv[i + 1]);if ((i = ArgPos((char *) "-hs", argc, argv)) > 0) hs = atoi(argv[i + 1]);if ((i = ArgPos((char *) "-negative", argc, argv)) > 0) negative = atoi(argv[i + 1]);if ((i = ArgPos((char *) "-threads", argc, argv)) > 0) num_threads = atoi(argv[i + 1]);if ((i = ArgPos((char *) "-iter", argc, argv)) > 0) iter = atoi(argv[i + 1]);if ((i = ArgPos((char *) "-min-count", argc, argv)) > 0) min_count = atoi(argv[i + 1]);if ((i = ArgPos((char *) "-classes", argc, argv)) > 0) classes = atoi(argv[i + 1]);vocab = (struct vocab_word *) calloc(vocab_max_size, sizeof(struct vocab_word));vocab_hash = (int *) calloc(vocab_hash_size, sizeof(int));expTable = (real *) malloc((EXP_TABLE_SIZE + 1) * sizeof(real));for (i = 0; i < EXP_TABLE_SIZE; i++) {                                                            // 预处理,提前计算sigmod值,并保存起来expTable[i] = exp((i / (real) EXP_TABLE_SIZE * 2 - 1) * MAX_EXP);                            // 计算e^xexpTable[i] = expTable[i] / (expTable[i] + 1);                                                // f(x) = 1 / (1 + e^(-x)) = e^x / (1 + e^x)}TrainModel();return 0;
}

word2vec源码分析相关推荐

  1. Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树

    Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树 文章目录 Alink漫谈(十六) :Word2Vec源码分析 之 建立霍夫曼树 0x00 摘要 0x01 背景概念 1.1 词向量 ...

  2. 机器学习算法实现解析——word2vec源码解析

    在wrod2vec工具中,有如下的几个比较重要的概念: CBOW Skip-Gram Hierarchical Softmax Negative Sampling 其中CBOW和Skip-Gram是w ...

  3. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  4. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  5. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  6. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  7. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

  8. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

  9. View的Touch事件分发(二.源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,先来看简单的View的touch事件分发. 主要分析View的dispatchTouchEvent()方法和onTou ...

最新文章

  1. 字符设备驱动程序框架
  2. jre for mac 删除_在 Mac 的 Docker Desktop 中运行 K8s
  3. 使用鸿蒙系统的家电厂商,除了华为外!谁还会用鸿蒙系统?米OV们不可能:这些巨头才会用...
  4. Java中字节输入输出流
  5. 黑客发现瑞士电子选举系统中的多个漏洞并获奖2.7万美元
  6. js控制layui radio button选中
  7. 太强了 GitHub中文开源项目榜单出炉,暴露了程序员的硬性需求
  8. linux五笔输入法制作_在linux下制作拼音五笔输入法
  9. 最近整理的Android学习笔记
  10. 微云百度云等资源 至少1M/s下载提速 无需会员
  11. 操作系统笔记(一)——操作系统的定义及作用
  12. 火车票飞机票,点击切换按钮切换出发城市和到达城市
  13. C语言——运算符优先级
  14. 如何从缓存白嫖网易云音乐
  15. 写一个Singleton模式的例子
  16. 机器视觉相机类型以及接口标准详解
  17. 弱势运营商校园市场竞争策略
  18. php做网站步骤_php建一个网站步骤
  19. AIX 用户使用的系统资源限制包括两个概念 --- 硬限制(hard limits) 和软限制(soft limits)
  20. 今日一起喝汤啦,‘不醉不归’——意志篇

热门文章

  1. OpenCV-Python学习 <三> 颜色空间及其转换
  2. webpack前端应用之基础打包
  3. 洛谷 P4233 射命丸文的笔记 题解
  4. 视频转换格式,用DOS命令
  5. 洛谷刷题C语言:距离函数、闰年展示、计算阶乘、猴子吃桃、培训
  6. 人力资源杂志人力资源杂志社人力资源编辑部2022年第20期目录
  7. samba搭建办公室共享打印机
  8. k8s Nodeport方式下service访问,iptables处理逻辑(转)
  9. 瑞友客户端 提示:连接远程服务器遇到错误
  10. 【错误汇总】zabbix 监控偶遇问题一记