Word2Vec原始碼詳細解析(上)
阿新 • • 發佈:2018-12-31
相關連結:
1、Word2Vec原始碼最詳細解析(上)
2、Word2Vec原始碼最詳細解析(下)
Word2Vec原始碼最詳細解析(上)
在這一部分中,主要介紹的是Word2Vec原始碼中的主要資料結構、各個變數的含義與作用,以及所有演算法之外的輔助函式,包括如何從訓練檔案中獲取詞彙、構建詞表、hash表、Haffman樹等,為演算法實現提供資料準備。而演算法部分的程式碼實現將在《Word2Vec原始碼最詳細解析(下)》一文中,重點分析。
該部分程式碼分析如下:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <math.h> #include <pthread.h> #define MAX_STRING 100 #define EXP_TABLE_SIZE 1000 #define MAX_EXP 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; // Precision of float numbers //每個詞的基本資料結構 struct vocab_word { long long cn; //詞頻,從訓練集中計數得到或直接提供詞頻檔案 int *point; //Haffman樹中從根節點到該詞的路徑,存放的是路徑上每個節點的索引 //word為該詞的字面值 //code為該詞的haffman編碼 //codelen為該詞haffman編碼的長度 char *word, *code, codelen; }; 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; //詞hash表,該陣列的下標為每個詞的hash值,由詞的字面值ASCII碼計算得到。vocab_hash[hash]中儲存的是該詞在詞表中的索引 int *vocab_hash; //vocab_max_size是一個輔助變數,每次當詞表大小超出vocab_max_size時,一次性將詞表大小增加1000 //vocab_size為訓練集中不同單詞的個數,即詞表的大小 //layer1_size為詞向量的長度 long long vocab_max_size = 1000, vocab_size = 0, layer1_size = 100; long long train_words = 0, word_count_actual = 0, iter = 5, file_size = 0, classes = 0; real alpha = 0.025, starting_alpha, sample = 1e-3; //syn0儲存的是詞表中每個詞的詞向量 //syn1儲存的是Haffman樹中每個非葉節點的向量 //syn1neg是負取樣時每個詞的輔助向量 //expTable是提前計算好的Sigmond函式表 real *syn0, *syn1, *syn1neg, *expTable; clock_t start; int hs = 0, negative = 5; const int table_size = 1e8; int *table; //計算每個函式的能量分佈表,在負取樣中用到 void InitUnigramTable() { int a, i; long long train_words_pow = 0; real d1, power = 0.75; //為能量表table分配記憶體空間,共有table_size項,table_size為一個既定的數1e8 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:表示已遍歷詞的能量值佔總能量的比 d1 = pow(vocab[i].cn, power) / (real)train_words_pow; //a:能量表table的索引 //i:詞表的索引 for (a = 0; a < table_size; a++) { //i號單詞佔據table中a位置 <span style="white-space:pre"> </span>table[a] = i; <span style="white-space:pre"> </span>//能量表反映的是一個單詞的能量分佈,如果該單詞的能量越大,所佔table的位置就越多 <span style="white-space:pre"> </span>//如果當前單詞的能量總和d1小於平均值,i遞增,同時更新d1;反之如果能量高的話,保持i不變,以佔據更多的位置 if (a / (real)table_size > d1) { i++; d1 += pow(vocab[i].cn, power) / (real)train_words_pow; } <span style="white-space:pre"> </span>//如果詞表遍歷完畢後能量表還沒填滿,將能量表中剩下的位置用詞表中最後一個詞填充 if (i >= vocab_size) i = vocab_size - 1; } } //從檔案中讀入一個詞到word,以space' ',tab'\t',EOL'\n'為詞的分界符 //截去一個詞中長度超過MAX_STRING的部分 //每一行的末尾輸出一個</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; } //返回一個詞的hash值,由詞的字面值計算得到,可能存在不同詞擁有相同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; } //返回一個詞在詞表中的位置,若不存在則返回-1 //先計算詞的hash值,然後在詞hash表中,以該值為下標,檢視對應的值 //如果為-1說明這個詞不存在索引,即不存在在詞表中,返回-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; } //從檔案中讀入一個詞,並返回這個詞在詞表中的位置,相當於將之前的兩個函式包裝了起來 int ReadWordIndex(FILE *fin) { char word[MAX_STRING]; ReadWord(word, fin); if (feof(fin)) return -1; return SearchVocab(word); } //為一個詞構建一個vocab_word結構物件,並新增到詞表中 //詞頻初始化為0,hash值用之前的函式計算, //返回該詞在詞表中的位置 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; vocab_size++; //每當詞表數目即將超過最大值時,一次性為其申請新增一千個詞結構體的記憶體空間 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值與其他詞產生衝突,則使用開放地址法解決衝突(為這個詞尋找一個hash值空位) while (vocab_hash[hash] != -1) hash = (hash + 1) % vocab_hash_size; //將該詞在詞表中的位置賦給這個找到的hash值空位 vocab_hash[hash] = vocab_size - 1; return vocab_size - 1; } //按照詞頻從大到小排序 int VocabCompare(const void *a, const void *b) { return ((struct vocab_word *)b)->cn - ((struct vocab_word *)a)->cn; } //統計詞頻,按照詞頻對詞表中的項從大到小排序 void SortVocab() { int a, size; unsigned int hash; //對詞表進行排序,將</s>放在第一個位置 qsort(&vocab[1], vocab_size - 1, sizeof(struct vocab_word), VocabCompare); //充值hash表 for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1; size = vocab_size; train_words = 0; for (a = 0; a < size; a++) { //將出現次數小於min_count的詞從詞表中去除,出現次數大於min_count的重新計算hash值,更新hash詞表 if ((vocab[a].cn < min_count) && (a != 0)) { vocab_size--; free(vocab[a].word); } else { //hash值計算 hash=GetWordHash(vocab[a].word); //hash值衝突解決 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)); // 為Haffman樹的構建預先申請空間 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)); } } //從詞表中刪除出現次數小於min_reduce的詞,沒執行一次該函式min_reduce自動加一 void ReduceVocab() { int a, b = 0; 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; //重置hash表 for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1; //更新hash表 for (a = 0; a < vocab_size; a++) { //hash值計算 hash = GetWordHash(vocab[a].word); //hash值衝突解決 while (vocab_hash[hash] != -1) hash = (hash + 1) % vocab_hash_size; vocab_hash[hash] = a; } fflush(stdout); min_reduce++; } //利用統計到的詞頻構建Haffman二叉樹 //根據Haffman樹的特性,出現頻率越高的詞其二叉樹上的路徑越短,即二進位制編碼越短 void CreateBinaryTree() { long long a, b, i, min1i, min2i, pos1, pos2; //用來暫存一個詞到根節點的Haffman樹路徑 long long point[MAX_CODE_LENGTH]; //用來暫存一個詞的Haffman編碼 char code[MAX_CODE_LENGTH]; //記憶體分配,Haffman二叉樹中,若有n個葉子節點,則一共會有2n-1個節點 //count陣列前vocab_size個元素為Haffman樹的葉子節點,初始化為詞表中所有詞的詞頻 //count陣列後vocab_size個元素為Haffman書中即將生成的非葉子節點(合併節點)的詞頻,初始化為一個大值1e15 long long *count = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long)); //binary陣列記錄各節點相對於其父節點的二進位制編碼(0/1) long long *binary = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long)); //paarent陣列記錄每個節點的父節點 long long *parent_node = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long)); //count陣列的初始化 for (a = 0; a < vocab_size; a++) count[a] = vocab[a].cn; for (a = vocab_size; a < vocab_size * 2; a++) count[a] = 1e15; //以下部分為建立Haffman樹的演算法,預設詞表已經按詞頻由高到低排序 //pos1,pos2為別為詞表中詞頻次低和最低的兩個詞的下標(初始時就是詞表最末尾兩個) //</s>詞也包含在樹內 pos1 = vocab_size - 1; pos2 = vocab_size; //最多進行vocab_size-1次迴圈操作,每次新增一個節點,即可構成完整的樹 for (a = 0; a < vocab_size - 1; a++) { //比較當前的pos1和pos2,在min1i、min2i中記錄當前詞頻最小和次小節點的索引 //min1i和min2i可能是葉子節點也可能是合併後的中間節點 if (pos1 >= 0) { //如果count[pos1]比較小,則pos1左移,反之pos2右移 if (count[pos1] < count[pos2]) { min1i = pos1; pos1--; } else { min1i = pos2; pos2++; } } else { min1i = pos2; pos2++; } if (pos1 >= 0) { //如果count[pos1]比較小,則pos1左移,反之pos2右移 if (count[pos1] < count[pos2]) { min2i = pos1; pos1--; } else { min2i = pos2; pos2++; } } else { min2i = pos2; pos2++; } //在count陣列的後半段儲存合併節點的詞頻(即最小count[min1i]和次小count[min2i]詞頻之和) count[vocab_size + a] = count[min1i] + count[min2i]; //記錄min1i和min2i節點的父節點 parent_node[min1i] = vocab_size + a; parent_node[min2i] = vocab_size + a; //這裡令每個節點的左右子節點中,詞頻較低的為1(則詞頻較高的為0) binary[min2i] = 1; } //根據得到的Haffman二叉樹為每個詞(樹中的葉子節點)分配Haffman編碼 //由於要為所有詞分配編碼,因此迴圈vocab_size次 for (a = 0; a < vocab_size; a++) { b = a; i = 0; while (1) { //不斷向上尋找葉子結點的父節點,將binary陣列中儲存的路徑的二進位制編碼增加到code陣列末尾 code[i] = binary[b]; //在point陣列中增加路徑節點的編號 point[i] = b; //Haffman編碼的當前長度,從葉子結點到當前節點的深度 i++; b = parent_node[b]; //由於Haffman樹一共有vocab_size*2-1個節點,所以vocab_size*2-2為根節點 if (b == vocab_size * 2 - 2) break; } //在詞表中更新該詞的資訊 //Haffman編碼的長度,即葉子結點到根節點的深度 vocab[a].codelen = i; //Haffman路徑中儲存的中間節點編號要在現在得到的基礎上減去vocab_size,即不算葉子結點,單純在中間節點中的編號 //所以現在根節點的編號為(vocab_size*2-2) - vocab_size = vocab_size - 2 vocab[a].point[0] = vocab_size - 2; //Haffman編碼和路徑都應該是從根節點到葉子結點的,因此需要對之前得到的code和point進行反向。 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); } //從訓練檔案中獲取所有詞彙並構建詞表和hash比 void LearnVocabFromTrainFile() { char word[MAX_STRING]; FILE *fin; long long a, i; //初始化hash詞表 for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1; //開啟訓練檔案 fin = fopen(train_file, "rb"); if (fin == NULL) { printf("ERROR: training data file not found!\n"); exit(1); } //初始化詞表大小 vocab_size = 0; //將</s>新增到詞表的最前端 AddWordToVocab((char *)"</s>"); //開始處理訓練檔案 while (1) { //從檔案中讀入一個詞 ReadWord(word, fin); if (feof(fin)) break; //對總詞數加一,並輸出當前訓練資訊 train_words++; if ((debug_mode > 1) && (train_words % 100000 == 0)) { printf("%lldK%c", train_words / 1000, 13); fflush(stdout); } //搜尋這個詞在詞表中的位置 i = SearchVocab(word); //如果詞表中不存在這個詞,則將該詞新增到詞表中,建立其在hash表中的值,初始化詞頻為1;反之,詞頻加一 if (i == -1) { a = AddWordToVocab(word); vocab[a].cn = 1; } else vocab[i].cn++; //如果詞表大小超過上限,則做一次詞表刪減操作,將當前詞頻最低的詞刪除 if (vocab_size > vocab_hash_size * 0.7) ReduceVocab(); } //對詞表進行排序,剔除詞頻低於閾值min_count的值,輸出當前詞表大小和總詞數 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); } //從詞彙表文件中讀詞並構建詞表和hash表 //由於詞彙表中的詞語不存在重複,因此與LearnVocabFromTrainFile相比沒有做重複詞彙的檢測 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); } //初始化hash詞表 for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1; vocab_size = 0; //開始處理詞彙表文件 while (1) { //從檔案中讀入一個詞 ReadWord(word, fin); if (feof(fin)) break; //將該詞新增到詞表中,建立其在hash表中的值,並通過輸入的詞彙表文件中的值來更新這個詞的詞頻 a = AddWordToVocab(word); fscanf(fin, "%lld%c", &vocab[a].cn, &c); i++; } //對詞表進行排序,剔除詞頻低於閾值min_count的值,輸出當前詞表大小和總詞數 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); }