系列前言
参考文献:
- 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);
}
由于本篇长度差不多了,下一篇继续函数实现的分析