系列前言
参考文献:
- RNNLM - Recurrent Neural Network Language Modeling Toolkit(点此阅读)
- Recurrent neural network based language model(点此阅读)
- EXTENSIONS OF RECURRENT NEURAL NETWORK LANGUAGE MODEL(点此阅读)
- Strategies for Training Large Scale Neural Network Language Models(点此阅读)
- STATISTICAL LANGUAGE MODELS BASED ON NEURAL NETWORKS(点此阅读)
- A guide to recurrent neural networks and backpropagation(点此阅读)
- A Neural Probabilistic Language Model(点此阅读)
- Learning Long-Term Dependencies with Gradient Descent is Difficult(点此阅读)
- Can Artificial Neural Networks Learn Language Models?(点此阅读)
这一篇开始介绍函数实现,对.cpp文件的函数内部语句分别进行走读,函数的顺序我没去组织,就按照文件的顺序进行。如果需要对某个函数配图说明,我会在下面注明,由于自己知识面比较窄,难免很多错误之处,欢迎看到的朋友指出~
好了,我们看一下开头的这部分,内容如下:
#ifdef USE_BLAS extern "C" { #include <cblas.h> } #endif
其中有一个cblas.h的头文件,blas的全称是basic linear algebra subprograms,用于向量和矩阵计算的高性能数学库, blas本身是Fortran写的,cblas是blas的c语言接口库,rnnlmlib.cpp文件本身是用c++写的,需要调用c语言的cblas,所以需要用extern
"C"来表明{}里面的内容需要按c语言的规范进行编译和链接,这是因为C++和C程序编译完成后在目标代码中命名规则不同,extern "C"实现了c和c++的混合编程。更详细的可以参考这篇博文C++中extern
“C”含义深层探索,以及CBLAS的安装与使用,通过这两篇文章能了解更多。
下面继续看第一个函数,一个生成随机小数的函数,如下:
real CRnnLM::random(real min, real max) { return rand()/(real)RAND_MAX*(max-min)+min; }
这里RAND_MAX是VC中stdlib.h中宏定义的一个字符常量,#define RAND_MAX 0x7FFF,其值为32767,通常在产生随机小数时可以使用RAND_MAX。里面的rand()返回值在[0, RAND_MAX],[]表示闭区间,即能取到边界值。这样return返回值范围在[min,
max]。如果我们返回[min, max)之间数,可以用下面语句:
return rand() / (real)(RAND_MAX+1) * (max - min) + min;
若要返回随机的整数,可以用rand() % 整数 来获取。
下面几个设置文件名的函数,很容易理解。为了完整性,还是贴出来,如下
//设置训练数据的文件名 void CRnnLM::setTrainFile(char *str) { strcpy(train_file, str); } //设置验证数据集的文件名 void CRnnLM::setValidFile(char *str) { strcpy(valid_file, str); } //设置测试集的文件名 void CRnnLM::setTestFile(char *str) { strcpy(test_file, str); } //设置模型保存文件,即该文件用来存储模型的信息,以及各类参数 void CRnnLM::setRnnLMFile(char *str) {
下面这个函数是一个基本的函数,在其他函数里面会反复的用到,功能是从文件中读取一个单词到word,但要注意两点:
1.单词最长不能超过99(最后一个字符得为‘\0‘),否则会被截断
2.训练集中每个句子结尾都会自动生成</s>作为一个单独的词,被复制到word返回,这在后面也是用来判断一个句子是否结束的标志。
void CRnnLM::readWord(char *word, FILE *fin) { int a=0, ch; //feof(FILE *stream)当到达文件末尾时返回一个非0数 while (!feof(fin)) { //从流中读取一个字符到ch ch=fgetc(fin); //ascii为13表示回车,\r,即回到一行的开头 //注意\r与\n不同,后者是换行 // \r\n主要是在文本文件中出现的 if (ch==13) continue; if ((ch==' ') || (ch=='\t') || (ch=='\n')) { if (a>0) { //将'\n'送回到字符流中,下次读取时还会读取到,这里标记一个句子的结束 if (ch=='\n') ungetc(ch, fin); break; } //如果a=0的情况就遇到换行,即上一个句子的结束,这里把句子的结束标记为</s>单独作为一个word if (ch=='\n') { strcpy(word, (char *)"</s>"); return; } else continue; } word[a]=ch; a++; //过长的单词会被截断,过长的结果word[99] = '\0' if (a>=MAX_STRING) { //printf("Too long word found!\n"); //truncate too long words a--; } } //字符串结尾用'\0',其ascii码为0 word[a]=0; }
下面这个函数查找word,找到返回word在vocab中的索引,没找到返回-1,有关前面变量只是做了简要的解释,这里简要的理解一下word,getWordHash(word),vocab_hash[],vocab[]的关系,见图。观察图,看到给定一个word,可以在O(1)的时间内得到word在vocab中的索引:vacab[vocab_hash[getWordHash(word)]],这里应用哈希映射是典型的用空间换时间的方法,但是哈希映射有个问题就是冲突,所以这里加了三层查找,如果发生了冲突,那么就在vocab中顺序查找,时间复杂度为O(vocab_size)。
//返回单词的哈希值 int CRnnLM::getWordHash(char *word) { unsigned int hash, a; hash=0; //单词哈希值的计算方式 for (a=0; a<strlen(word); a++) hash=hash*237+word[a]; //vocab_hash_size在CRnnLm的构造函数初始化为1亿即100000000 hash=hash%vocab_hash_size; return hash; }
<span style="line-height: 36px;">int CRnnLM::searchVocab(char *word) { int a; unsigned int hash; hash=getWordHash(word); //第一层查找,vocab_hash[hash]==-1表示当前word不在vocab中 if (vocab_hash[hash]==-1) return -1; //第二层查找,这里确认当前word并未被其他word给冲突掉 if (!strcmp(word, vocab[vocab_hash[hash]].word)) return vocab_hash[hash]; //第三层查找,走到了这里,说明当前word与其他word的哈希值有冲突,直接线性查找 for (a=0; a<vocab_size; a++) { if (!strcmp(word, vocab[a].word)) { //这里把查找到的当前词的哈希值覆盖,这样vocab_hash总是保持最近查找词的hash值 //越是频繁查找的词,通过这种方式即便冲突了,下次也会在O(1)的时间内查找到! vocab_hash[hash]=a; return a; } } //没找到,即该词不在vocab内,即out-of-vocabulary return -1; }</span>
下面这个函数读取当前文件指针所指的单词,并返回该单词在vocab中的索引,注意无论是训练数据、验证数据、测试数据文件的格式都是文件末尾空行,所以按照文件内容顺序查找,查找到文件末尾一定是</s>,然后fin就到文件末尾了。
int CRnnLM::readWordIndex(FILE *fin) { char word[MAX_STRING]; readWord(word, fin); if (feof(fin)) return -1; return searchVocab(word); }
接下来这个函数将word添加到vocab中,并且返回刚添加word在vocab中的索引,并将word与vocab_hash与vocab通过word哈希值关联起来,可以看到这的内存是动态管理的,代码注释如下:
int CRnnLM::addWordToVocab(char *word) { unsigned int hash; strcpy(vocab[vocab_size].word, word); vocab[vocab_size].cn=0; vocab_size++; //vocab是动态管理的,当数组内存快不够了,再扩大数组内存,每次增加100个单位,每个单位是vocab_word类型 if (vocab_size+2>=vocab_max_size) { vocab_max_size+=100; //realloc是用来扩大或缩小内存的,扩大时原来的内容不变,系统直接 //在后面找空闲内存,如果没找到,则会把前面的数据重新移动到一个够大的地方 //即realloc可能会导致数据的移动,这算自己顺便看源码边复习一些c的知识吧 vocab=(struct vocab_word *)realloc(vocab, vocab_max_size * sizeof(struct vocab_word)); } //将word的哈希值作为vocab_hash的下标,下标所对应的整型值为vocab中对该word的索引 hash=getWordHash(word); vocab_hash[hash]=vocab_size-1; return vocab_size-1; }
下面是一个选择排序的算法,将vocab[1]到vocab[vocab_size-1]按照他们出现的频数从大到小排序。
void CRnnLM::sortVocab() { int a, b, max; vocab_word swap; //注意这里下标是从1开始,并未把vocab[0]考虑进来 //实际上vocab[0]是存放的</s>,从后面的learnVocabFromTrainFile()可以看到 for (a=1; a<vocab_size; a++) { max=a; for (b=a+1; b<vocab_size; b++) if (vocab[max].cn<vocab[b].cn) max=b; swap=vocab[max]; vocab[max]=vocab[a]; vocab[a]=swap; } }
然后这个函数是从train_file中读数据,相关数据会装入vocab,vocab_hash,这里假设vocab是空的。
void CRnnLM::learnVocabFromTrainFile() { char word[MAX_STRING]; FILE *fin; int a, i, train_wcn; //这里对vocab_hash的初始化说明不在vocab中的word,其vocab_hash[getWordHash(word)]为-1 for (a=0; a<vocab_hash_size; a++) vocab_hash[a]=-1; //以二进制模式读取文件 //关于二进制和文本文件的区别,可以参考这篇博文:http://www.cnblogs.com/flying-roc/articles/1798817.html //当train_file是文本文件存储时,即句子结尾是\r\n,前面readWord()函数有一个条件语句if处理掉了\r //如果train_file是二进制存储时,句子结尾只有\n,所以对于字符组成的文件来说两者差别不大 fin=fopen(train_file, "rb"); vocab_size=0; //也就是vocab[0]是存放的</s> addWordToVocab((char *)"</s>"); //记录train_file中tokens数量 train_wcn=0; while (1) { readWord(word, fin); if (feof(fin)) break; train_wcn++; //vocab存放的word不会重复,重复的word让其词频加1 i=searchVocab(word); if (i==-1) { a=addWordToVocab(word); vocab[a].cn=1; } else vocab[i].cn++; } //注意这里在读入train_file后,会将vocab排序,后面会看到对词语分类有帮助 sortVocab(); //select vocabulary size /*a=0; while (a<vocab_size) { a++; if (vocab[a].cn==0) break; } vocab_size=a;*/ if (debug_mode>0) { printf("Vocab size: %d\n", vocab_size); printf("Words in train file: %d\n", train_wcn); } //train_words表示训练文件中的词数 train_words=train_wcn; fclose(fin); }
由于本篇长度差不多了,下一篇继续函数实现的分析