一、word2vec訓(xùn)練參數(shù)
先根據(jù)輸入的train_file文件創(chuàng)建兩個(gè)數(shù)組,vocab和vocab_hash,vocab是詞庫(kù)數(shù)組,一維數(shù)組,每一個(gè)對(duì)象都是vocab_word類型;vocab_hash是詞庫(kù)的hash表,將詞按hash映射到詞庫(kù),vocab_hash[word_hash] = 詞在詞庫(kù)的位置,在從語(yǔ)料文件建立詞庫(kù)時(shí),對(duì)讀入的詞快速定位到其在詞庫(kù)中的位置。
struct vocab_word { // 詞的結(jié)構(gòu)體,存儲(chǔ)包括詞本身、詞頻、哈夫曼編碼、編碼長(zhǎng)度、哈夫曼路徑
long long cn;// 詞頻
int *point;// 哈夫曼樹中從根節(jié)點(diǎn)到該詞的路徑,路徑的索引要特別注意,在下面的構(gòu)建哈夫曼樹中會(huì)說(shuō)明
char *word, *code, codelen;// 分別是:詞,哈夫曼編碼,編碼長(zhǎng)度
};
//訓(xùn)練參數(shù)格式
-train text8 -output vectors.bin -cbow 0 -size 200 -window 5 -negative 0 -hs 1 -sample 1e-3 -threads 12 -binary
//訓(xùn)練參數(shù)意義
size: 對(duì)應(yīng)代碼中l(wèi)ayer1_size, 表示詞向量的維度,默認(rèn)值是100。
train: 對(duì)應(yīng)代碼中train_file, 表示語(yǔ)料庫(kù)文件路徑。
save-vocab: 對(duì)應(yīng)代碼中save_vocab_file, 詞匯表保存路徑。
read-vocab: 對(duì)應(yīng)代碼中read_vocab_file, 表示已有的詞匯表文件路徑,直接讀取,不用從語(yǔ)料庫(kù)學(xué)習(xí)得來(lái)。
debug: 對(duì)應(yīng)代碼中debug_mode, 表示是否選擇debug模型,值大于1表示開啟,默認(rèn)是2。開啟debug會(huì)打印一些信息。
binary: 對(duì)應(yīng)代碼中全局變量binary,表示文件保存方式,1表示按二進(jìn)制保存,0表示按文本保存,默認(rèn)是0.
cbow: 對(duì)應(yīng)代碼中cbow, 1表示按cbow模型訓(xùn)練, 0表示按skip模式訓(xùn)練,默認(rèn)是1。
alpha: 對(duì)應(yīng)代碼中alpha,表示學(xué)習(xí)率。skip模式下默認(rèn)為0.025, cbow模式下默認(rèn)是0.05。
output: 對(duì)應(yīng)代碼中output_file, 表示詞向量保存路徑。
window: 對(duì)應(yīng)代碼中window,表示訓(xùn)練窗口大小。默認(rèn)是5
sample: 對(duì)應(yīng)代碼中sample,表示下采樣閥值。
hs: 對(duì)應(yīng)代碼中hs, 表示按huffman softmax模式訓(xùn)練。默認(rèn)是0, 表示不使用hs。
negative: 對(duì)應(yīng)代碼中negative, 表示按負(fù)采樣模式訓(xùn)練, 默認(rèn)是5。值為0表示不采用負(fù)采樣訓(xùn)練;如果使用,值一般為3到10。
threads: 對(duì)應(yīng)代碼中num_threads,訓(xùn)練線程數(shù),一般為12。
iter: 對(duì)應(yīng)代碼中iter,訓(xùn)練迭代次數(shù),默認(rèn)是5.
min-count: 對(duì)應(yīng)代碼中min_count,表示最小出現(xiàn)頻率,低于這個(gè)頻率的詞會(huì)被移除詞匯表。默認(rèn)值是5
classes: 對(duì)應(yīng)代碼中classes,表示聚類中心數(shù), 默認(rèn)是0, 表示不啟用聚類。
min-count: read
二、總體流程

main函數(shù)的流程

訓(xùn)練部分的流程
2.1 TrainModel()函數(shù)
void TrainModel() {
long a, b, c, d;
FILE *fo;
//創(chuàng)建多線程,num_threads 為
pthread_t *pt = (pthread_t *)malloc(num_threads * sizeof(pthread_t));
printf("Starting training using file %s\n", train_file);
starting_alpha = alpha;//初始學(xué)習(xí)速率
//詞匯文件是否存在,存在則直接讀取,不存在則學(xué)習(xí),得到vocab和vocab_hash
if (read_vocab_file[0] != 0){
printf("ReadVocab\n");
ReadVocab();
}
else{
printf("LearnVocabFromTrainFile\n");//詞匯文件不存在,read_vocab_file[0]=0
LearnVocabFromTrainFile();
}
if (save_vocab_file[0] != 0) SaveVocab();//根據(jù)需要,可以將詞表中的詞和詞頻輸出到文件
if (output_file[0] == 0) return;//訓(xùn)練后詞向量的輸出文件,由binary和classes共同控制輸出結(jié)果,沒有的話直接返回
InitNet(); // 網(wǎng)絡(luò)結(jié)構(gòu)初始化
if (negative > 0) InitUnigramTable();//暫時(shí)略過(guò)
start = clock();//開始計(jì)時(shí)
//其實(shí)網(wǎng)絡(luò)的實(shí)現(xiàn)都是在TrainModelThread中,神經(jīng)網(wǎng)絡(luò)分成多線程計(jì)算,
//計(jì)算完成之后再進(jìn)行k-mean聚類。TrainModel生成線程,配置線程。
//TrainModelThread的詳細(xì)解析在第五部分。
for (a = 0; a < num_threads; a++) pthread_create(&pt[a], NULL, TrainModelThread, (void *)a);
//讓主線程等待子線程執(zhí)行結(jié)束后,主線程再結(jié)束。
//這樣,防止主線程很快執(zhí)行完后,退出,上一行創(chuàng)建的子線程沒有機(jī)會(huì)執(zhí)行。
for (a = 0; a < num_threads; a++) pthread_join(pt[a], NULL);
fo = fopen(output_file, "wb");//打開輸出文件,
if (classes == 0) {//不用聚類,輸出詞向量到文件中
// 首先打印出詞匯表的大小,再打印出詞向量維度
fprintf(fo, "%lld %lld\n", vocab_size, layer1_size);
for (a = 0; a < vocab_size; a++) {
fprintf(fo, "%s ", vocab[a].word);//按照詞匯表輸出詞匯
//binary: 對(duì)應(yīng)代碼中全局變量binary,表示文件保存方式,1表示按二進(jìn)制保存,0表示按文本保存,默認(rèn)是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");
}
}
}
三、構(gòu)建詞庫(kù)和hash
word2vec支持兩種數(shù)據(jù),一種是已經(jīng)統(tǒng)計(jì)好詞頻的形式,如“中國(guó) 6 地球 8”;另外一種是只做了分詞,并沒有統(tǒng)計(jì)詞頻的形式,如“我 愛 北京 天安門”。在TrainModel()這個(gè)函數(shù)中我們可以看到
// 如果是帶有頻率的詞表則讀入詞表和頻率,否則讀詞表并統(tǒng)計(jì)頻率
if (read_vocab_file[0] != 0) ReadVocab(); else LearnVocabFromTrainFile();
3.1 LearnVocabFromTrainFile()這個(gè)函數(shù)是讀取不帶頻率的文件所用的
/*相關(guān)變量說(shuō)明*/
const int vocab_hash_size = 30000000;
int *vocab_hash;//詞庫(kù)的hash表,將詞按hash映射到詞庫(kù),vocab_hash[word_hash] = 詞在詞庫(kù)的位置,
//在從語(yǔ)料文件建立詞庫(kù)時(shí),對(duì)讀入的詞快速定位到其在詞庫(kù)中的位置,訓(xùn)練時(shí)不會(huì)用到
//從訓(xùn)練文件中獲取所有詞匯并構(gòu)建詞表和hash
void LearnVocabFromTrainFile() {
char word[MAX_STRING];//MAX_STRING為一個(gè)word的最大長(zhǎng)度
FILE *fin;
long long a, i;
for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1;// 0~30000000
fin = fopen(train_file, "rb");//打開訓(xùn)練文件 train_file(作為訓(xùn)練參數(shù)輸入)
if (fin == NULL) {//找不到該文件,直接退出
printf("ERROR: training data file not found!\n");
exit(1);
}
vocab_size = 0;//詞庫(kù)中實(shí)際的詞個(gè)數(shù),初始為0
// 將</s>添加到詞表最開始的位置
AddWordToVocab((char *)"</s>");//添加字符</s>,該函數(shù)的定義請(qǐng)看4.2部分,添
//加一個(gè)單詞到詞表尾部,并返回該單詞所在的位置
while (1) {
ReadWord(word, fin);//ReadWord()函數(shù)參看4.4部分,讀取訓(xùn)練文件
if (feof(fin)) break;//讀取到文件末尾
//讀到每一個(gè)word
train_words++;//要訓(xùn)練的詞總數(shù)
// debug_mode模式打印進(jìn)度,暫時(shí)不管
if ((debug_mode > 1) && (train_words % 100000 == 0)) {
printf("%lldK%c", train_words / 1000, 13);
fflush(stdout);
}
i = SearchVocab(word);//查找詞在詞庫(kù)中的位置,詞庫(kù)中存在該詞,返回詞的位置,否則返回-1,參看4.5部分
if (i == -1) {//不在詞庫(kù)中
a = AddWordToVocab(word);//添加進(jìn)去
vocab[a].cn = 1;//詞頻唯一
} else vocab[i].cn++;//不用添加,增加詞頻即可
/**
* 如果詞表的規(guī)模大于哈??臻g的70%,則移除一部分低頻詞
* 每添加一個(gè)詞,判斷一次詞庫(kù)大小,若大于填充因子上限,刪除一次低頻詞
* 注意,這里有一個(gè)問題,在刪除低頻詞時(shí),語(yǔ)料文件可能是未處理完的,只是讀取了一部分詞
* 所以詞庫(kù)當(dāng)前狀態(tài)下的詞頻信息是局部的,不是訓(xùn)練文件全局的
* 這時(shí)刪除低頻詞時(shí),是把局部的低頻詞刪除,但局部低頻詞未必是全局低頻詞,例如,一個(gè)詞在訓(xùn)練文件的前一部分少量出現(xiàn),但在后面部分,大量出現(xiàn)
* 不過(guò)考慮到詞庫(kù)規(guī)模(千萬(wàn)級(jí)),這個(gè)問題也不太可能出現(xiàn)
*/
if (vocab_size > vocab_hash_size * 0.7) ReduceVocab();//去除低頻詞,參看4.6部分
}
SortVocab();// 把詞表中的單詞按詞頻排序 ,參看4.7部分
if (debug_mode > 0) {
printf("Vocab size: %lld\n", vocab_size);
printf("Words in train file: %lld\n", train_words);
}
file_size = ftell(fin);//訓(xùn)練文件大小,ftell得到,多線程訓(xùn)練時(shí)會(huì)對(duì)文件進(jìn)行分隔,
//用于定位每個(gè)訓(xùn)練線程開始訓(xùn)練的文件位置
printf("file_size: %lld\n", file_size);
fclose(fin);
}
3.2 AddWordToVocab()是添加一個(gè)單詞到詞表尾部
//詞表結(jié)構(gòu)
struct vocab_word *vocab;
vocab = (struct vocab_word *)calloc(vocab_max_size, sizeof(struct vocab_word));
struct vocab_word {
long long cn;//詞頻,從訓(xùn)練集中計(jì)數(shù)得到或直接提供詞頻文件
int *point;//Haffman樹中從根節(jié)點(diǎn)到該詞的路徑,存放的是路徑上每個(gè)節(jié)點(diǎn)的索引
//word為該詞的字面值,code為該詞的haffman編碼,codelen為該詞haffman編碼的長(zhǎng) 度
char *word, *code, codelen;
};
/*相關(guān)變量說(shuō)明*/
long long vocab_max_size = 1000;//vocab_max_size詞庫(kù)規(guī)模(詞庫(kù)容量),在建立詞庫(kù)的過(guò)程中,
//當(dāng)詞庫(kù)規(guī)模到達(dá)vocab_max_size時(shí)會(huì)對(duì)詞庫(kù)擴(kuò)容,每次擴(kuò)增vocab_max_size個(gè)容量
//for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1;// 0~30000000
//添加一個(gè)單詞到詞表尾部
int AddWordToVocab(char *word) {
unsigned int hash, length = strlen(word) + 1;
if (length > MAX_STRING) length = MAX_STRING;//保證單詞的長(zhǎng)度不超過(guò)MAX_STRING
// vocab_size為詞庫(kù)中實(shí)際的詞個(gè)數(shù)
vocab[vocab_size].word = (char *)calloc(length, sizeof(char));
strcpy(vocab[vocab_size].word, word);
vocab[vocab_size].cn = 0;//詞頻暫時(shí)記為0
vocab_size++;//詞個(gè)數(shù)增加
// 如果詞表快到上限了,為詞表重新申請(qǐng)空間
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);//獲取word的hash值,GetWordHash()請(qǐng)看4.3部分
printf("%lld\n",hash);
while (vocab_hash[hash] != -1) hash = (hash + 1) % vocab_hash_size;//30000000;開放定址法解決沖突,
//哈希沖突時(shí)線性探測(cè)繼續(xù)順序往下查找空白位置;vocab_hash[hash] != -1,則說(shuō)明已經(jīng)有這個(gè)hash了
vocab_hash[hash] = vocab_size - 1;// 記錄在詞匯表中的存儲(chǔ)位置
return vocab_size - 1;// 返回添加的單詞在詞匯表中的存儲(chǔ)位置,末尾前一個(gè)
}
3.3 GetWordHash()是添加一個(gè)單詞到詞表尾部
//對(duì)一個(gè)單詞進(jìn)行哈希得到它的哈希值
int GetWordHash(char *word) {
unsigned long long a, hash = 0;//打印long long類型:printf("%lld\n",word[1]);
/*計(jì)算hash值*/
for (a = 0; a < strlen(word); a++) hash = hash * 257 + word[a];
hash = hash % vocab_hash_size;//詞庫(kù)大小取模
return hash;
}
3.4 ReadWord()是從文件中讀取單個(gè)單詞
// 從文件中讀取單個(gè)單詞,假設(shè)單詞之間通過(guò)空格或者tab鍵或者EOL鍵進(jìn)行分割的
void ReadWord(char *word, FILE *fin) {
int a = 0, ch;//a - 用于向word中插入字符的索引;ch - 從fin中讀取的每個(gè)字符
//整個(gè)循環(huán)就是為了讀取一個(gè)單詞
while (!feof(fin)) {//如果fp文件指針沒有到達(dá)文件尾
ch = fgetc(fin);//讀取一個(gè)詞,準(zhǔn)確地說(shuō)是一個(gè)字符
if (ch == 13) continue;//回車,開始新的一行,重新開始while循環(huán)讀取下一個(gè)字符
//當(dāng)遇到space(' ') + tab(\t) + EOL(\n)時(shí),認(rèn)為word結(jié)束,UNIX/Linux中‘\n’為一行的結(jié)束符號(hào),
//windows中為:“<回車><換行>”,即“\r\n”;Mac系統(tǒng)里,每行結(jié)尾是“<回車>”,即“\r”
if ((ch == ' ') || (ch == '\t') || (ch == '\n')) {//word結(jié)束
if (a > 0) {
if (ch == '\n') ungetc(ch, fin);//退回到流中
break;//如果讀到了單詞就直接退出
}
if (ch == '\n') {//最后還是換行符,說(shuō)明文件為空,直接退出
strcpy(word, (char *)"</s>");//將</s>賦予給word
return;
} else continue;//此時(shí)a=0,且遇到的為\t or ' ',直接跳過(guò)取得下一個(gè)字符
}
word[a] = ch;
a++;
if (a >= MAX_STRING - 1) a--; // MAX_STRING為一個(gè)word的最大長(zhǎng)度,超過(guò)了就直接截?cái)? }
word[a] = 0;//最后一個(gè)字符是'\0'
printf(word);
putchar('|');
}
3.5 SearchVocab()查找詞在詞庫(kù)中的位置,詞庫(kù)中存在該詞,返回詞的位置,否則返回-1
// 查找詞在詞庫(kù)中的位置,詞庫(kù)中存在該詞,返回詞的位置,否則返回-1
int SearchVocab(char *word) {
unsigned int hash = GetWordHash(word);//獲取hash值
while (1) {
if (vocab_hash[hash] == -1) return -1;//詞庫(kù)中不存在該詞
if (!strcmp(word, vocab[vocab_hash[hash]].word)) return vocab_hash[hash];//vocab_hash[hash] != -1且word一樣,直接返回位置
hash = (hash + 1) % vocab_hash_size;//vocab_hash[hash] != -1且word不一樣,沖突策略
}
return -1;
}
3.6 ReduceVocab() 刪除低頻詞
/*變量說(shuō)明*/
// 從語(yǔ)料文件建立詞庫(kù)時(shí),為了控制詞庫(kù)規(guī)模,會(huì)在詞庫(kù)的hash表達(dá)到填充因子上限時(shí),調(diào)用該方法刪除一些低頻詞
void ReduceVocab() {
int a, b = 0;
unsigned int hash;
//vocab_size為詞庫(kù)里的實(shí)際詞匯數(shù)
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;//實(shí)際詞匯數(shù)變了
//處理hash映射
for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1;//重新全部為-1
for (a = 0; a < vocab_size; a++) {//實(shí)際詞匯數(shù)
// Hash will be re-computed, as it is not actual
hash = GetWordHash(vocab[a].word);//重新計(jì)算hash
//沖突了就重新尋址
while (vocab_hash[hash] != -1) hash = (hash + 1) % vocab_hash_size;
vocab_hash[hash] = a;//沒沖突這就是位置
}
fflush(stdout);
min_reduce++;//閾值變大
}
3.7 SortVocab() 把詞表中的單詞按詞頻排序
// 比較兩個(gè)詞的詞頻大小,用于對(duì)詞庫(kù)排序
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;
// 快排排序,按照詞頻從大到小排
qsort(&vocab[1], vocab_size - 1, sizeof(struct vocab_word), VocabCompare);
for (a = 0; a < vocab_hash_size; a++) vocab_hash[a] = -1;//清楚原來(lái)的hash映射
size = vocab_size;
train_words = 0;
for (a = 0; a < size; a++) {
// 如果當(dāng)前單詞詞頻小于規(guī)定的閾值,則刪除詞表最后一個(gè)詞 (因?yàn)橐呀?jīng)從大到小排序了)
if (vocab[a].cn < min_count) {
vocab_size--;
free(vocab[vocab_size].word);//刪除最后一個(gè)詞
} else {
// 排序以后詞表的序改變了,需要重新計(jì)算哈希值
hash=GetWordHash(vocab[a].word);//計(jì)算hash值
//說(shuō)明hash沖突了,線性尋址
while (vocab_hash[hash] != -1) hash = (hash + 1) % vocab_hash_size;
vocab_hash[hash] = a;//找到位置
train_words += vocab[a].cn;//重新計(jì)算train_words
}
}
// 因?yàn)閯h了一些詞,所以重新定義詞表大小
vocab = (struct vocab_word *)realloc(vocab, (vocab_size + 1) * sizeof(struct vocab_word));
// Allocate memory for the binary tree construction
// 給每一個(gè)詞匯的Huffman編碼和路徑申請(qǐng)空間
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));
}
}
3.8 ReadVocab() 讀取詞頻文件,過(guò)程和LearnVocabFromTrainFile差不多
// 如果是帶有頻率的詞表則讀入詞表和頻率,否則讀詞表并統(tǒng)計(jì)頻率
if (read_vocab_file[0] != 0) ReadVocab(); else LearnVocabFromTrainFile();
//因此ReadVocab的過(guò)程和LearnVocabFromTrainFile差不多
//從詞匯表文件中讀詞并構(gòu)建詞表和hash表
//由于詞匯表中的詞語(yǔ)不存在重復(fù),因此與LearnVocabFromTrainFile相比沒有做重復(fù)詞匯的檢測(cè)
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;//實(shí)際詞匯數(shù)初始化
//開始處理詞匯表文件
while (1) {
ReadWord(word, fin);//從文件中讀入一個(gè)詞
if (feof(fin)) break;
//將該詞添加到詞表中,創(chuàng)建其在hash表中的值,并通過(guò)輸入的詞匯表文件中的值
//來(lái)更新這個(gè)詞的詞頻
//不存在重復(fù)的問題,所以不用search
a = AddWordToVocab(word);//返回在詞匯表中的位置
fscanf(fin, "%lld%c", &vocab[a].cn, &c);//讀取詞頻,并設(shè)置詞頻
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");// 還得打開以下訓(xùn)練文件好知道文件大小是多少
if (fin == NULL) {
printf("ERROR: training data file not found!\n");
exit(1);
}
fseek(fin, 0, SEEK_END);//將文件指針移至文件末尾
file_size = ftell(fin);//得到訓(xùn)練文件大小
fclose(fin);
}
3.9 SaveVocab() 輸出單詞和詞頻到文件(Vocab.txt)
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);
}
四、初始化網(wǎng)絡(luò)結(jié)構(gòu)
4.1 初始化網(wǎng)絡(luò)參數(shù)
void InitNet() {
long long a, b;
unsigned long long next_random = 1;
//syn0存儲(chǔ)的是詞表中每個(gè)詞的詞向量,源碼中使用一個(gè)real(float)類型的一維數(shù)組表 示,注意是一個(gè)一維數(shù)組!
//容量大小為vocab_size * layer1_size(詞向量的維度大小,也是隱藏層的大小),即 詞匯量 * 詞向量維度
//調(diào)用posiz_memalign來(lái)為syn0分配內(nèi)存,對(duì)齊的內(nèi)存,大小為vocab_size * layer1_size * sizeof(real),
//也就是每個(gè)詞匯對(duì)應(yīng)一個(gè)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);}//沒有分配到內(nèi)存,退出程序
if (hs) {//基于hierarchical softmax的模型
//syn1存儲(chǔ)的是Haffman樹中每個(gè)非葉節(jié)點(diǎn)的向量
a = posix_memalign((void **)&syn1, 128, (long long)vocab_size * layer1_size * sizeof(real));
if (syn1 == NULL) {printf("Memory allocation failed\n"); exit(1);}//未分配到內(nèi)存,退出程序
for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++)
syn1[a * layer1_size + b] = 0;////初始化syn1為0,初始化非葉節(jié)點(diǎn)的向量為0
}
if (negative>0) {//基于negative Sampling模型
//負(fù)采樣時(shí),存儲(chǔ)每個(gè)樣本對(duì)應(yīng)的詞向量,一維數(shù)組,第i個(gè)詞的詞向量
//為syn1neg[i * layer1_size, (i + 1) * layer1_size - 1]
a = posix_memalign((void **)&syn1neg, 128, (long long)vocab_size * layer1_size * sizeof(real));
if (syn1neg == NULL) {printf("Memory allocation failed\n"); exit(1);}//未分配到內(nèi) 存,退出程序
for (a = 0; a < vocab_size; a++) for (b = 0; b < layer1_size; b++)
syn1neg[a * layer1_size + b] = 0;
}
//初始化詞向量syn0,每一維的值為[-0.5, 0.5]/layer1_size范圍內(nèi)的隨機(jī)數(shù)
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;
}
CreateBinaryTree(); //創(chuàng)建Haffman二叉樹
}
4.2 構(gòu)建哈夫曼樹
- 哈夫曼樹
一般得到霍夫曼樹后我們會(huì)對(duì)葉子節(jié)點(diǎn)進(jìn)行霍夫曼編碼,由于權(quán)重高的葉子節(jié)點(diǎn)越靠近根節(jié)點(diǎn),而權(quán)重低的葉子節(jié)點(diǎn)會(huì)遠(yuǎn)離根節(jié)點(diǎn),這樣我們的高權(quán)重節(jié)點(diǎn)編碼值較短,而低權(quán)重值編碼值較長(zhǎng)。這保證的樹的帶權(quán)路徑最短,也符合我們的信息論,即我們希望越常用的詞擁有更短的編碼。如何編碼呢?在word2vec中,約定編碼方式和上面的例子相反,即約定左子樹編碼為1,右子樹編碼為0,同時(shí)約定左子樹的權(quán)重不小于右子樹的權(quán)重。在有n個(gè)葉子節(jié)點(diǎn)的哈夫曼樹中,其節(jié)點(diǎn)總數(shù)為2n-1Word2Vec中的Huffman樹.png -
哈夫曼編碼
哈夫曼編碼.png
哈夫曼編碼.png
//利用統(tǒng)計(jì)到的詞頻構(gòu)建Haffman二叉樹
//根據(jù)Haffman樹的特性,出現(xiàn)頻率越高的詞其二叉樹上的路徑越短,即二進(jìn)制編碼越短
void CreateBinaryTree() {
//用來(lái)暫存一個(gè)詞到根節(jié)點(diǎn)的Haffman樹路徑
//MAX_CODE_LENGTH是point域和code域大小 ,路徑長(zhǎng)度其實(shí)就是haffman編碼
的函數(shù)
//int *point; 霍夫曼樹中從根節(jié)點(diǎn)到該詞的路徑,存放路徑上每個(gè)非葉結(jié)點(diǎn)的索引
long long a, b, i, min1i, min2i, pos1, pos2, point[MAX_CODE_LENGTH];
char code[MAX_CODE_LENGTH];//用來(lái)暫存一個(gè)詞的Haffman編碼
//內(nèi)存分配,Haffman二叉樹中,若有n個(gè)葉子節(jié)點(diǎn),則一共會(huì)有2n-1個(gè)節(jié)點(diǎn)
//count數(shù)組前vocab_size個(gè)元素為Haffman樹的葉子節(jié)點(diǎn),初始化為詞表中所有詞的詞頻, count數(shù)組后vocab_size個(gè)元素為Haffman書中即將生成的非葉子節(jié)點(diǎn)(合并節(jié)點(diǎn))的詞頻,初始化為一個(gè)大值1e15
long long *count = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long));
//binary數(shù)組中前vocab_size存儲(chǔ)的是每一個(gè)詞的對(duì)應(yīng)的二進(jìn)制編碼,后面初始化的是0,用來(lái)存儲(chǔ)生成節(jié)點(diǎn)的編碼
long long *binary = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long));
//parent_node數(shù)組中前vocab_size存儲(chǔ)的是每一個(gè)詞的對(duì)應(yīng)的父節(jié)點(diǎn),后面初始化的是0,用來(lái)存儲(chǔ)生成節(jié)點(diǎn)的父節(jié)點(diǎn)
long long *parent_node = (long long *)calloc(vocab_size * 2 + 1, sizeof(long long));
//count數(shù)組的初始化
for (a = 0; a < vocab_size; a++) count[a] = vocab[a].cn;
for (a = vocab_size; a < vocab_size * 2; a++) count[a] = 1e15;
//以下部分為創(chuàng)建Haffman樹的算法,默認(rèn)詞表已經(jīng)按詞頻由高到低排序
//</s>詞也包含在樹內(nèi)
pos1 = vocab_size - 1;//末尾
pos2 = vocab_size;//末尾后面一個(gè)
//構(gòu)建霍夫曼樹,最多進(jìn)行vocab_size-1次循環(huán)操作,每次添加一個(gè)節(jié)點(diǎn),即可構(gòu)成完整的樹
for (a = 0; a < vocab_size - 1; a++) {
// 'min1, min2'分別用于存儲(chǔ)最小和次小節(jié)點(diǎn),注意vocab中的詞是已經(jīng)按照cn排好序的了,是按照降序排列的
//pos1表示取最原始的詞對(duì)應(yīng)的詞頻,而pos2表示取合并最小值形成的詞頻
//連續(xù)兩次取,兩次取的時(shí)候代碼操作時(shí)一模一樣的
//第一個(gè)if查找最小的權(quán)重位置
if (pos1 >= 0) {
if (count[pos1] < count[pos2]) { //如果count[pos1]比較小,則pos1左移,反之pos2右移
min1i = pos1;
pos1--;
} else {
min1i = pos2;
pos2++;
}
} else {//pos1<0,說(shuō)明葉子節(jié)點(diǎn)已經(jīng)被合并完了,只能往右邊找非葉子節(jié)點(diǎn)進(jìn)行合并
min1i = pos2;
pos2++;
}
//第二個(gè)if查找最小的權(quán)重位置
if (pos1 >= 0) {
if (count[pos1] < count[pos2]) { //如果count[pos1]比較小,則pos1左移,反之pos2右移
min2i = pos1;
pos1--;
} else {
min2i = pos2;
pos2++;
}
} else {//pos1<0,說(shuō)明葉子節(jié)點(diǎn)已經(jīng)被合并完了,只能往右邊找非葉子節(jié)點(diǎn)進(jìn)行合并
min2i = pos2;
pos2++;
}
//在count數(shù)組的后半段存儲(chǔ)合并節(jié)點(diǎn)的詞頻(即最小count[min1i]和次小 count[min2i]詞頻之和)
count[vocab_size + a] = count[min1i] + count[min2i];//a
parent_node[min1i] = vocab_size + a;//父節(jié)點(diǎn)的位置
parent_node[min2i] = vocab_size + a;//父節(jié)點(diǎn)的位置
binary[min2i] = 1;//詞頻較大的為1,左為1,右為0(因?yàn)橛薪M合,所以有編碼)
}
// 建好了hufuman樹之后,就需要分配code了,注意這個(gè)hufuman樹
//是用一個(gè)數(shù)組來(lái)存儲(chǔ)的,并不是我們常用的指針式鏈表
// 順著父子關(guān)系找回編碼,vocab_size個(gè)詞匯
for (a = 0; a < vocab_size; a++) {
b = a;//從第一個(gè)葉子節(jié)點(diǎn)開始
i = 0;
//找到一個(gè)word的huffman編碼
while (1) {
code[i] = binary[b];//這個(gè)位置對(duì)應(yīng)的編碼
point[i] = b;//在point數(shù)組中增加路徑節(jié)點(diǎn)的編號(hào)
i++;//Haffman編碼的當(dāng)前長(zhǎng)度,從葉子結(jié)點(diǎn)到當(dāng)前節(jié)點(diǎn)的深度
b = parent_node[b];//找到父節(jié)點(diǎn)位置
if (b == vocab_size * 2 - 2) break; //由于Haffman樹一共有vocab_size*2-1個(gè)
// 節(jié)點(diǎn),所以vocab_size*2-2為根節(jié)點(diǎn)
}
// 以下要注意的是,同樣的位置,point總比code深一層
vocab[a].codelen = i;//編碼長(zhǎng)度為i,編碼長(zhǎng)度賦值,少1,沒有算根節(jié)點(diǎn)
//Haffman編碼和路徑都應(yīng)該是從根節(jié)點(diǎn)到葉子結(jié)點(diǎn)的,因此需要對(duì)之前得到的code和point進(jìn)行反向
vocab[a].point[0] = vocab_size - 2;// // 逆序,把第一個(gè)賦值為root(即2*vocab_size - 2 - vocab_size)
for (b = 0; b < i; b++) {
vocab[a].code[i - b - 1] = code[b];//逆序,換成vocab[a].code[i - 1-b] = code[b]更 好理解
//路徑逆序,point的長(zhǎng)度比code長(zhǎng)1,即根節(jié)點(diǎn),point數(shù)組“最后”一個(gè)值是負(fù)的,point路徑是為了定位節(jié)點(diǎn)位置,葉子節(jié)點(diǎn)即是詞本身,不用定位,所以訓(xùn)練時(shí)這個(gè)負(fù)數(shù)是用不到的
vocab[a].point[i - b] = point[b] - vocab_size;//注意這個(gè)索引對(duì)應(yīng)的是huffman樹中的非葉子節(jié)點(diǎn),對(duì)應(yīng)syn1中的索引, 因?yàn)榉侨~子節(jié)點(diǎn)都是在vocab_size * 2 + 1 的后(vocab_size + 1)個(gè)
}
}
//釋放內(nèi)存,point、code、codelen都已經(jīng)得到
free(count);
free(binary);
free(parent_node);
}

第一個(gè)for循環(huán).png
五、模型訓(xùn)練
5.1 預(yù)生成expTable
word2vec計(jì)算過(guò)程中用上下文預(yù)測(cè)中心詞或者用中心詞預(yù)測(cè)上下文,都需要進(jìn)行預(yù)測(cè);而word2vec中采用的預(yù)測(cè)方式是邏輯回歸分類,需要用到sigmoid函數(shù),具體函數(shù)形式為:

sigmoid函數(shù)

sigmoid函數(shù)圖像
在訓(xùn)練過(guò)程中需要用到大量的sigmoid值計(jì)算,如果每次都臨時(shí)去算ex的值,將會(huì)影響性能;當(dāng)對(duì)精度的要求不是很嚴(yán)格的時(shí)候,我們可以采用近似的運(yùn)算。在word2vec中,將區(qū)間-MAX_EXP, MAX_EXP等距劃分為EXP_TABLE_SIZE(默認(rèn)值1000)等份,并將每個(gè)區(qū)間的sigmoid值計(jì)算好存入到expTable中。在需要使用時(shí),只需要確定所屬的區(qū)間,屬于哪一份,然后直接去數(shù)組中查找。expTable初始化代碼如下:
expTable = (real *)malloc((EXP_TABLE_SIZE + 1) * sizeof(real)); //初始化expTable,近似逼近sigmoid(x)值,x區(qū)間為[-MAX_EXP, MAX_EXP],分成EXP_TABLE_SIZE份
//將[-MAX_EXP, MAX_EXP]分成EXP_TABLE_SIZE份
for (i = 0; i < EXP_TABLE_SIZE; i++) {
//注意:在代碼中,作者使用的是小于EXP_TABLE_SIZE,實(shí)際的區(qū)間是[?6,6)[?6,6)
expTable[i] = exp((i / (real)EXP_TABLE_SIZE * 2 - 1) * MAX_EXP); // Precompute the exp() table
expTable[i] = expTable[i] / (expTable[i] + 1); // Precompute f(x) = x / (x + 1)
}
5.2 CBOW模型
在CBOW模型中,總共有三層,分別是輸入層,映射層和輸出層。如下圖所示:

CBOW模型.png
hs模式和negative模式中,輸入層到映射層的處理是一樣的,僅僅是映射層到輸出層的處理不一致。輸入層到映射層的具體操作是:將上下文窗口中的每個(gè)詞向量求和,然后再平均,得到一個(gè)和詞向量一樣維度的向量,假設(shè)叫上下文向量,這個(gè)向量就是映射層的向量。

CBOW模型算法總體過(guò)程.png

CBOW模型算法總體過(guò)程.png
5.3 Skip-Gram模型


skip-gram模型總體算法.png
5.4 TrainModelThread()函數(shù),實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò),進(jìn)行訓(xùn)練
//實(shí)現(xiàn)神經(jīng)網(wǎng)絡(luò),進(jìn)行訓(xùn)練
void *TrainModelThread(void *id) {
// word: 在提取句子時(shí)用來(lái)表示當(dāng)前詞在詞表中的索引,也就是說(shuō)向sen中添加單詞用,句子完成后表示句子中的當(dāng)前單詞
// last_word 上一個(gè)單詞,輔助掃描窗口,記錄當(dāng)前掃描到的上下文單詞
// sentence_length 當(dāng)前處理的句子長(zhǎng)度,當(dāng)前句子的長(zhǎng)度(單詞數(shù))
// sentence_position 當(dāng)前處理的單詞在當(dāng)前句子中的位置(index)
//cw:窗口長(zhǎng)度(中心詞除外)
long long a, b, d, cw, word, last_word, sentence_length = 0, sentence_position = 0;
//word_count: 當(dāng)前線程當(dāng)前時(shí)刻已訓(xùn)練的語(yǔ)料的長(zhǎng)度
//last_word_count: 當(dāng)前線程上一次記錄時(shí)已訓(xùn)練的語(yǔ)料長(zhǎng)度
// last_word_count:保存值,以便在新訓(xùn)練語(yǔ)料長(zhǎng)度超過(guò)某個(gè)值時(shí)輸出信息
// sen 單詞數(shù)組,表示句子,//sen:當(dāng)前從文件中讀取的待處理句子,存放的是每個(gè) 詞在詞表中的索引
long long word_count = 0, last_word_count = 0, sen[MAX_SENTENCE_LENGTH + 1];
//l1:在skip-gram模型中,在syn0中定位當(dāng)前詞詞向量的起始位置
//l2:在syn1或syn1neg中定位中間節(jié)點(diǎn)向量或負(fù)采樣向量的起始位置
long long l1, l2, c, target, label, local_iter = iter;
unsigned long long next_random = (long long)id;
real f, g;// f e^x / (1/e^x),fs中指當(dāng)前編碼為是0(父親的左子節(jié)點(diǎn)為0,右為1)的概率,ns中指label是1的概率
// g 誤差(f與真實(shí)值的偏離)與學(xué)習(xí)速率的乘積
clock_t now;// 當(dāng)前時(shí)間,和start比較計(jì)算算法效率
//neu1:輸入詞向量,在CBOW模型中是Context(x)中各個(gè)詞的向量和,在skip-gram模型中是中心詞的詞向量
real *neu1 = (real *)calloc(layer1_size, sizeof(real));
//neuele:累計(jì)誤差項(xiàng)
real *neu1e = (real *)calloc(layer1_size, sizeof(real));
FILE *fi = fopen(train_file, "rb");//打開訓(xùn)練文件
//多線程模型訓(xùn)練:利用多線程對(duì)訓(xùn)練文件劃分,每個(gè)線程訓(xùn)練一部分的數(shù)據(jù)
//每個(gè)進(jìn)程對(duì)應(yīng)一段文本,根據(jù)當(dāng)前線程的id找到該線程對(duì)應(yīng)文本的初始位置
//file_size就是之前LearnVocabFromTrainFile和ReadVocab函數(shù)中獲取的訓(xùn)練文件的大小
fseek(fi, file_size / (long long)num_threads * (long long)id, SEEK_SET);// 將文件內(nèi)容分配給各個(gè)線程
while (1) {//對(duì)每一個(gè)詞,應(yīng)用四種模型進(jìn)行訓(xùn)練
//每訓(xùn)練10000個(gè)詞時(shí),打印已訓(xùn)練數(shù)占所有需要訓(xùn)練數(shù)比例,以及打印訓(xùn)練時(shí)間; 然后更新學(xué)習(xí)率
if (word_count - last_word_count > 10000) {//word_count表示當(dāng)前線程當(dāng)前時(shí)刻已 訓(xùn)練的語(yǔ)料的長(zhǎng)度
//last_word_count當(dāng)前線程上一次記錄時(shí)已訓(xùn)練的語(yǔ)料長(zhǎng)度
//word_count_actual是全局變量
word_count_actual += word_count - last_word_count;//word_count_actual是所有線程總共當(dāng)前處理的詞數(shù)
last_word_count = word_count;//更新last_word_count的值
// debug_mode 大于0,加載完畢后輸出匯總信息,大于1,加載訓(xùn)練詞匯的時(shí)候輸出信息,訓(xùn)練過(guò)程中輸出信息
if ((debug_mode > 1)) {//debug模式下輸出一些訓(xùn)練信息
//輸出信息包括:
//當(dāng)前的學(xué)習(xí)率alpha;
//訓(xùn)練總進(jìn)度(當(dāng)前訓(xùn)練的總詞數(shù)/(迭代次數(shù)*訓(xùn)練樣本總詞數(shù))+1);
//每個(gè)線程每秒處理的詞數(shù)
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);
}
//在初始學(xué)習(xí)率的基礎(chǔ)上,隨著實(shí)際訓(xùn)練詞數(shù)的上升,逐步降低當(dāng)前學(xué)習(xí)率(自適應(yīng)調(diào)整學(xué)習(xí)率)
alpha = starting_alpha * (1 - word_count_actual / (real)(iter * train_words + 1));//自動(dòng)調(diào)整學(xué)習(xí)率
if (alpha < starting_alpha * 0.0001) alpha = starting_alpha * 0.0001;// 調(diào)整的過(guò)程中保證學(xué)習(xí)率不低于starting_alpha * 0.0001
}//以上一整塊都在基礎(chǔ)配置階段
//從訓(xùn)練樣本中取出一個(gè)句子,句子間以回車分割(只取出一個(gè)句子,因?yàn)橥饷孢€有一層循環(huán))
if (sentence_length == 0) {// 如果當(dāng)前句子長(zhǎng)度為0 ,從訓(xùn)練樣本中取出一個(gè)句子,句子間以回車分割
//sentence_length被初始值為0,所以一定進(jìn)入該語(yǔ)句塊
while (1) {
//word用來(lái)表示當(dāng)前詞在詞表中的索引
word = ReadWordIndex(fi);//從文件中讀入一個(gè)詞,并返回這個(gè)詞在詞匯表中的位置,關(guān)于該函數(shù),請(qǐng)參看5.2部分。
if (feof(fi)) break;//文件結(jié)束,退出最近的循環(huán)
if (word == -1) continue;
word_count++;//當(dāng)前線程當(dāng)前時(shí)刻已訓(xùn)練的語(yǔ)料的長(zhǎng)度
if (word == 0) break;//如果讀到的時(shí)回車,表示句子結(jié)束,退出當(dāng)前循環(huán),讀下一個(gè)句子,</s>的索引為0
// 這里的亞采樣是指 Sub-Sampling,Mikolov 在論文指出這種亞采樣能夠帶來(lái) 2 到 10 倍的性能提升,并能夠提升低頻詞的表示精度。
// 低頻詞被丟棄概率低,高頻詞被丟棄概率高
//對(duì)高頻詞進(jìn)行隨機(jī)下采樣,丟棄掉一些高頻詞,能夠使低頻詞向量更加準(zhǔn)確,同時(shí)加快訓(xùn)練速度
//可以看作是一種平滑方法
if (sample > 0) {// sample 亞采樣概率的參數(shù),亞采樣的目的是以一定概率拒絕高頻詞,使得低頻詞有更多出鏡率,默認(rèn)為0,即不進(jìn)行亞采樣
real ran = (sqrt(vocab[word].cn / (sample * train_words)) + 1) * (sample * train_words) / vocab[word].cn;//詞頻高的詞ran值低,容易被舍棄
next_random = next_random * (unsigned long long)25214903917 + 11;
//以1-ran的概率舍棄高頻詞
if (ran < (next_random & 0xFFFF) / (real)65536) continue;
}
sen[sentence_length] = word;//句子里面存放的是詞在詞匯表里面的索引
sentence_length++;//當(dāng)前處理的單詞在當(dāng)前句子中的位置
if (sentence_length >= MAX_SENTENCE_LENGTH) break;//如果句子長(zhǎng)度超出最大長(zhǎng)度則截?cái)?超過(guò)1000個(gè)單詞則截?cái)唷? }
sentence_position = 0;// 定位到句子頭,表示當(dāng)前單詞在當(dāng)前句中的index,起始值為0
}
//如果當(dāng)前線程處理的詞數(shù)超過(guò)了它應(yīng)該處理的最大值,那么開始新一輪迭代
//如果迭代數(shù)超過(guò)上限,則停止迭代
if (feof(fi) || (word_count > train_words / num_threads)) {//讀到末尾或者數(shù)據(jù)超過(guò)最大處理值
word_count_actual += word_count - last_word_count;//更新word_count_actual,所有線程總共當(dāng)前處理的詞數(shù)
local_iter--;//迭代總次數(shù)減少一次
if (local_iter == 0) break;//迭代超過(guò)上限,停止迭代,只有這里才是跳出最外層循環(huán)的地方
word_count = 0;
last_word_count = 0;
sentence_length = 0;
fseek(fi, file_size / (long long)num_threads * (long long)id, SEEK_SET);//將文件讀指針重新移到到此線程所處理詞的開頭
continue;//重新開始循環(huán)
}
word = sen[sentence_position];// 取句子中的第一個(gè)單詞,開始運(yùn)行BP算法
if (word == -1) continue;// 如果沒有這個(gè)單詞,則繼續(xù),有點(diǎn)疑問???
for (c = 0; c < layer1_size; c++) neu1[c] = 0;//初始化輸入詞向量
for (c = 0; c < layer1_size; c++) neu1e[c] = 0;//初始化累計(jì)誤差項(xiàng)
//生成一個(gè)[0, window-1]的隨機(jī)數(shù),用來(lái)確定|context(w)|窗口的實(shí)際寬度
next_random = next_random * (unsigned long long)25214903917 + 11;
b = next_random % window;//b的大小在[0, window-1]之間
/*然后就開始訓(xùn)練了,先初始化了neu1和neu1e的值。并且確定了窗口的起始位 置,
通過(guò)b = next_random % window來(lái)確定,理論上,我們?cè)谥行脑~左右都是取大小為 window個(gè)上下文詞,
但是在代碼中,并不是保證左右都是window個(gè),而是左邊為(window - b)個(gè),
右邊為(window + b)個(gè),總數(shù)仍然是2 * window個(gè)*/
/********如果使用的是CBOW模型:輸入是某單詞周圍窗口單詞的詞向量,來(lái)預(yù)測(cè)該中心單詞本身*********/
if (cbow) { //CBOW模型訓(xùn)練
// 輸入層 -> 隱藏層
cw = 0;
//這個(gè)循環(huán)主要是為了求neu1:輸入詞向量,在CBOW模型中是Context(x)中各個(gè)詞的向量和
//一個(gè)詞的窗口為[setence_position - window + b, sentence_position + window - b]
for (a = b; a < window * 2 + 1 - b; a++) if (a != window) {//去除窗口的中心詞,這是我們要預(yù)測(cè)的內(nèi)容,僅僅提取上下文
c = sentence_position - window + a;//一個(gè)詞的窗口為[setence_position - window + b, sentence_position + window - b]
//不符合條件,則繼續(xù)取
if (c < 0) continue;
if (c >= sentence_length) continue;
last_word = sen[c]; //sen數(shù)組中存放的是句子中的每個(gè)詞在詞表中的索引
if (last_word == -1) continue;//不存在該詞,則繼續(xù),有點(diǎn)疑惑???
//累加詞對(duì)應(yīng)的向量。雙重循環(huán)下來(lái)就是窗口額定數(shù)量的詞每一維對(duì)應(yīng)的向量累加。
//累加后neu1的維度依然是layer1_size。
//從輸入層過(guò)度到隱含層。
//neu1:輸入詞向量,在CBOW模型中是Context(x)中各個(gè)詞的向量和,在skip-gram模型中是中心詞的詞向量
//real *neu1 = (real *)calloc(layer1_size, sizeof(real));
//syn0 表示: 存儲(chǔ)詞典中每個(gè)詞的詞向量
//real *syn0,syn0存儲(chǔ)的是詞表中每個(gè)詞的詞向量,源碼中使用一個(gè)real(float)類型的一維數(shù)組表示,注意是一個(gè)一維數(shù)組!
for (c = 0; c < layer1_size; c++) neu1[c] += syn0[c + last_word * layer1_size];//計(jì)算窗口中詞向量的和
//注意syn0是一維數(shù)組,不是二維的,所以通過(guò)last_word * layer1_size來(lái)定位某個(gè)詞對(duì)應(yīng)的向量位置, last_word表示上下文中上一個(gè)詞
cw++;//進(jìn)入隱含層的詞個(gè)數(shù)。
}
if (cw) {//有效次數(shù)大于1
for (c = 0; c < layer1_size; c++) neu1[c] /= cw;//歸一化處理,求平均值, neu1是投影層的向量和
//注意hs模式下,syn1存的是非葉子節(jié)點(diǎn)對(duì)應(yīng)的向量,并不是詞匯表中的詞對(duì)應(yīng)的另一個(gè)向量;
//而negative模型下,syn1neg存的是詞的另一個(gè)向量,需要注意
//如果采用分層softmax優(yōu)化
//根據(jù)Haffman樹上從根節(jié)點(diǎn)到當(dāng)前詞的葉節(jié)點(diǎn)的路徑,遍歷所有經(jīng)過(guò)的中間節(jié)點(diǎn)
if (hs) for (d = 0; d < vocab[word].codelen; d++) {//codelen是編碼長(zhǎng)度,word是當(dāng)前詞的索引
f = 0;
// 霍夫曼樹中從根節(jié)點(diǎn)到該詞的路徑,存放路徑上每個(gè)非葉結(jié)點(diǎn)的索引
l2 = vocab[word].point[d] * layer1_size;//l2為當(dāng)前遍歷到的中間節(jié)點(diǎn)的向量在syn1中的起始位置,syn1 用于表示hs(hierarchical softmax)算法中霍夫曼編碼樹非葉結(jié)點(diǎn)的權(quán)重,syn1也是一個(gè)一維向量
// 隱藏層 -> 輸出層
for (c = 0; c < layer1_size; c++) f += neu1[c] * syn1[c + l2];//f為輸入向量neu1與中間結(jié)點(diǎn)向量的內(nèi)積
//檢測(cè)f有沒有超出Sigmoid函數(shù)表的范圍
if (f <= -MAX_EXP) continue;
else if (f >= MAX_EXP) continue;
//如果沒有超出范圍則對(duì)f進(jìn)行Sigmoid變換
//sigmod函數(shù), f=expTab[(int)((f+6)*1000/12)] (計(jì)算出屬于哪一份)
else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];//f計(jì)算出來(lái)了
//g是梯度和學(xué)習(xí)率的乘積
//注意!word2vec中將Haffman編碼為1的節(jié)點(diǎn)定義為負(fù)類,而將編碼為0的節(jié)點(diǎn)定義為正類
//即一個(gè)節(jié)點(diǎn)的label = 1 - d
g = (1 - vocab[word].code[d] - f) * alpha;
// 根據(jù)計(jì)算得到的修正量g和中間節(jié)點(diǎn)的向量更新累計(jì)誤差
for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];
//根據(jù)計(jì)算得到的修正量g和輸入向量更新中間節(jié)點(diǎn)的向量值
for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * neu1[c];
}
// 負(fù)采樣,暫時(shí)略過(guò)
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];
}
//根據(jù)獲得的的累計(jì)誤差,更新context(w)中每個(gè)詞的詞向量
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;//一般不存在這種情況
//以上部分都與前面一致
//neu1e存放的是累計(jì)誤差向量,syn0存放的是詞匯表中每個(gè)詞匯的詞向量
for (c = 0; c < layer1_size; c++) syn0[c + last_word * layer1_size] += neu1e[c];
}
}
} else { //skip-gram 模型,skip-gram其實(shí)沒有投影層
//因?yàn)樾枰A(yù)測(cè)Context(w)中的每個(gè)詞,因此需要循環(huán)2window - 2b + 1次遍歷整個(gè)窗口
//每個(gè)上下文都要有一次循環(huán)
for (a = b; a < window * 2 + 1 - b; a++) if (a != window) {//遍歷時(shí),略過(guò)中心單詞
c = sentence_position - window + a;//last_word為當(dāng)前待預(yù)測(cè)的上下文單詞
//不符合條件,則繼續(xù)
if (c < 0) continue;
if (c >= sentence_length) continue;
last_word = sen[c]; //取出該詞在詞匯表中的索引
if(last_word ==-1)continue;//一般不會(huì)出現(xiàn)這種情況
l1 = last_word * layer1_size; //l1為當(dāng)前單詞的詞向量在syn0中的起始位置,syn中存的是詞匯表中詞的向量
for (c = 0; c < layer1_size; c++) neu1e[c] = 0;//初始化累計(jì)誤差
// HIERARCHICAL SOFTMAX huffman樹
if (hs) for (d = 0; d < vocab[word].codelen; d++) {//d代表公式里的l
f = 0;//初始化為0
//l2為當(dāng)前遍歷到的中間節(jié)點(diǎn)的向量在syn1中的起始位置,syn1 用于表示hs(hierarchical softmax)算法
//中霍夫曼編碼樹非葉結(jié)點(diǎn)的權(quán)重,syn1也是一個(gè)一維向量
l2 = vocab[word].point[d] * layer1_size;
// 從隱藏層到輸出層
for (c = 0; c < layer1_size; c++) f += syn0[c + l1] * syn1[c + l2];//f為與當(dāng)前上下文向量與中間結(jié)點(diǎn)向量的內(nèi)積
//檢測(cè)f有沒有超出Sigmoid函數(shù)表的范圍
if (f <= -MAX_EXP) continue;
else if (f >= MAX_EXP) continue;
//如果沒有超出范圍則對(duì)f進(jìn)行Sigmoid變換
else f = expTable[(int)((f + MAX_EXP) * (EXP_TABLE_SIZE / MAX_EXP / 2))];//f計(jì)算出來(lái)了
//注意!這里用到了模型對(duì)稱:p(u|w) = p(w|u),其中w為中心詞,u為context(w)中每個(gè)詞
//也就是skip-gram雖然是給中心詞預(yù)測(cè)上下文,真正訓(xùn)練的時(shí)候還是用上下文預(yù)測(cè)中心詞
//與CBOW不同的是這里的u是單個(gè)詞的詞向量,而不是窗口向量之和
//算法流程基本和CBOW的hs一樣
// g是梯度和學(xué)習(xí)率的乘積
g = (1 - vocab[word].code[d] - f) * alpha;
// 計(jì)算累計(jì)誤差值
for (c = 0; c < layer1_size; c++) neu1e[c] += g * syn1[c + l2];
// 根據(jù)計(jì)算得到的修正量g和輸入向量更新中間節(jié)點(diǎn)的向量值
for (c = 0; c < layer1_size; c++) syn1[c + l2] += g * syn0[c + l1];
}
// 負(fù)采樣,暫時(shí)略過(guò)
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];
}
// 處理完一個(gè)詞,及時(shí)去更新他的詞向量,用累計(jì)誤差來(lái)更新。
for (c = 0; c < layer1_size; c++) syn0[c + l1] += neu1e[c];
//每個(gè)窗口向量都要遍歷一遍,遍歷完成以后,讀取下一個(gè)詞。
}
}
//都是為了跳到外面的循環(huán)進(jìn)入下一步
sentence_position++;//完成了一個(gè)詞的訓(xùn)練,句子中位置往后移一個(gè)詞
if (sentence_position >= sentence_length) {//處理完一句句子后,將句子長(zhǎng)度置為零,進(jìn)入循環(huán)
//重新讀取句子并進(jìn)行逐詞計(jì)算,即讀取下一個(gè)句子
sentence_length = 0;//sentence_length 設(shè)置為0以后就
continue;
}
}
fclose(fi);//關(guān)閉文件
free(neu1);//釋放內(nèi)存
free(neu1e);//釋放內(nèi)存
pthread_exit(NULL);//退出線程
}
//3.1.詞向量的初始化:首先,生成一個(gè)很大的next_random的數(shù),
//通過(guò)與“0xFFFF”進(jìn)行與運(yùn)算截?cái)?,再除?5536得到[0,1]之間的數(shù),
//最終,得到的初始化的向量的范圍為:[-0.5/layer1_size,0.5/layer1_size],其中l(wèi)ayer1_size為詞向量的長(zhǎng)度
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; // 隨機(jī)初始化word vectors
}
5.4 ReadWordIndex()從文件流中讀取一個(gè)詞,并返回這個(gè)詞在詞匯表中的位置
//構(gòu)建詞庫(kù)的過(guò)程:從文件流中讀取一個(gè)詞,并返回這個(gè)詞在詞匯表中的位置
int ReadWordIndex(FILE *fin) {
char word[MAX_STRING];
ReadWord(word, fin);//讀取一個(gè)詞
if (feof(fin)) return -1;
return SearchVocab(word);//返回該詞在詞匯表中的位置
}


