Mikolov RNNLM Code
这篇博客基于Mikolov大神的博士论文Statistical Language Models based on Neural Networks、相应的RNNLM(Recurrent Neural Network Language Model)公开源码以及Mikolov在google做的slide。
另外,得益于国内前辈们的工作,才不至于筚路蓝缕、以启山林。已经有过源码的分析解读,这篇博客也只能在此基础上想想有什么能添砖加瓦的了。
代码版本 - rnnlm-0.4b
rnnlmlib.h
一些主要的宏定义
/* 防止 _RNNLMLIB_H_ 被重复include */ #ifndef _RNNLMLIB_H_ #define _RNNLMLIB_H_ /* 字符串长度的限制 */ #define MAX_STRING 100 /* 防止 WEIGHTTYPE 被重复include 设定RNNLM中的权值值为double类型,正是论文中 到的一些实用的建议 */ #ifndef WEIGHTTYPE #define WEIGHTTYPE double #endif
神经网络数据类型的一些设定
/* 权值设定 定义了神经元的激活值权值类型为 real,最大熵 中输入层直接到输出层的权值类型为 direct_t */ typedef WEIGHTTYPE real; // NN weights typedef WEIGHTTYPE direct_t; // ME weights /* ac是神经元中的激活值,er为误差值 */ struct neuron { real ac; //actual value stored in neuron real er; //error value in neuron, used by learning algorithm }; /* 突触,非常形象,是一个神经元细胞连接另一个神经元 细胞的组织。 实际上就是一个 real(double) 类型的浮点数。 */ struct synapse { real weight; //weight of synapse };
关于neuron, synapse 的初始化与赋值情况,在 rnnlmlib.cpp 的初始化函数中
一个神经元细胞
void CRnnLM::initNet() { /* skip */ neu0=(struct neuron *)calloc(layer0_size, sizeof(struct neuron)); /* 神经元的初始化,相应的,还有 neu1, neuc, neu2, skip */ syn0=(struct synapse *)calloc(layer0_size*layer1_size, sizeof(struct synapse)); if (layerc_size==0) // 如果压缩层大小为零 syn1=(struct synapse *)calloc(layer1_size*layer2_size, sizeof(struct synapse)); else { // 压缩层大小非零 syn1=(struct synapse *)calloc(layer1_size*layerc_size, sizeof(struct synapse)); sync=(struct synapse *)calloc(layerc_size*layer2_size, sizeof(struct synapse)); } /* 内存分配的管理,skip */ neu0b=(struct neuron *)calloc(layer0_size, sizeof(struct neuron)); /* 神经元的初始化,相应的,还有 neu1b, neucb, neu1b2, neu2b 和第一组神经元不同之处在于"b",所以是做back备份之用,在 函数 void CRnnLM::saveWeights() 和 void CRnnLM::restoreWeights() 中使用 在CRnnLM类定义的最后几个成员变量里有提及 */ syn0b=(struct synapse *)calloc(layer0_size*layer1_size, sizeof(struct synapse)); /* 同上, skip */ for (a=0; a < layer0_size; a++) { neu0[a].ac=0; neu0[a].er=0; } /* 对一系列神经元的赋值,ac 与 er 都赋值为零, skip */ for (b=0; b < layer1_size; b++) for (a=0; a < layer0_size; a++) { syn0[a+b*layer0_size].weight=random(-0.1, 0.1)+random(-0.1, 0.1)+random(-0.1, 0.1); } /* 对突触的赋值,例如对 layer0 的第 a 个神经元 到 layer1 的第 b 个神经元 之间的突触赋值,突触的 序数就为 a+b*layer0_size ,即在矩阵[b行, a列]中 按行优先的方式储存,并随机赋值。 为什么采用三个 random(-0.1, 0.1) 相加,我也不是 很懂。 */ /* bptt的初始化, skip */ saveWeights(); /* 备份 neu 和 syn 的数据到 neub 和 synb */ }
回到 rnnlmlib.h
/* word的结构定义 */ struct vocab_word { /* 表示该词在train_file中出现的频数 */ int cn; /* 作为字符串的单词,字符串长度有限制 */ char word[MAX_STRING]; /* 单词的概率 */ real prob; /* 单词的类别 */ int class_index; }; /* 储存质数的数组,用来做hash函数 */ const unsigned int PRIMES[]={108641969, 116049371, 125925907, 133333309, 145678979, 175308587, 197530793, 234567803, 251851741, 264197411, 330864029, 399999781, 407407183, 459258997, 479012069, 545678687, 560493491, 607407037, 629629243, 656789717, 716048933, 718518067, 725925469, 733332871, 753085943, 755555077, 782715551, 790122953, 812345159, 814814293, 893826581, 923456189, 940740127, 953085797, 985184539, 990122807}; /* 质数数组 PRIMES[] 的元素个数 */ const unsigned int PRIMES_SIZE=sizeof(PRIMES)/sizeof(PRIMES[0]); /* 最大阶数,用来限制ME模型的N元模型的N */ const int MAX_NGRAM_ORDER=20; /* 文件储存类型: TEXT - ASCII储存,在储存网络权值时比较浪费空间 BINARY - 二进制储存,不利于阅读 */ enum FileTypeEnum {TEXT, BINARY, COMPRESSED}; //COMPRESSED not yet implemented
类 CRnnLM 的成员变量设定及其在构造函数中的赋值
class CRnnLM{ protected: /* 训练集、验证集、测试集、储存文件、其他模型的生成文件 {train, valid, test, rnnlm}_file[0] = 0 */ char train_file[MAX_STRING]; char valid_file[MAX_STRING]; char test_file[MAX_STRING]; char rnnlm_file[MAX_STRING]; char lmprob_file[MAX_STRING]; /* 随机数种子 构造函数中 = 1 */ int rand_seed; /* 不同的debug_mode输出不同详细程度的信息 构造函数中 = 0 输出比较简略的debug信息 */ int debug_mode; /* rnn toolkit 版本号 构造函数中 = 10 */ int version; /* 储存模型参数的文件类型 TEXT/BINARY 构造函数中 = TEXT */ int filetype; /* user_lmprob == 1 使用其他LM 构造函数中 = 0,也就是使用RNN */ int use_lmprob; /* user_lmprob == 1 时其他LM与RNN的插值系数 构造函数中 = 0.75 */ real lambda; /* 防止梯度爆炸的截断阈值,在矩阵乘法的函数中被使用 构造函数中 = 15 也就是Mikolov在论文中提到的 |\mathbf{e}| \in (-15, 15) */ real gradient_cutoff; /* dynamic > 0: 边测试边学习 构造函数中 dynamic = 0 不选择动态测试学习 */ real dynamic; /* 学习率 构造函数中 = 0.1 */ real alpha; /* 训练初始的学习率 */ real starting_alpha; /* alpha_divid == 0: 不将alpha减半 构造函数中 = 0 */ int alpha_divide; /* 累计对数概率 logp = \sum_{i} log10(w_i) 构造函数中 = 0 llogp 指 last logp 构造函数中 = -100000000 负无穷大 */ double logp, llogp; /* 最小增长倍数 构造函数中 = 1.003 */ float min_improvement; /* 训练集的训练次数 构造函数中 = 0 */ int iter; /* 词汇集的最大容量,并且在代码中动态增加 构造函数中 = 100 */ int vocab_max_size; /* 词汇集的实际大小 构造函数中 = 0 */ int vocab_size; /* 训练集中的词汇量 构造函数中 = 0 */ int train_words; /* 当前词在训练集中的位置,是第几个词 构造函数中 = 0 */ int train_cur_pos; int counter; /* one_iter == 1 只训练一遍 构造函数中 = 0 不选用 */ int one_iter; /* 训练集的最大训练次数 构造函数中 = 0 */ int maxIter; /* 每次训练的单词数,会被保存到rnnlm_file */ int anti_k; /* L2正则化系数 */ real beta; /* 单词分类的种类 构造函数中 = 100 */ int class_size; /* 二维指针class_words[i-1][j-1]指第i类的第j个词 在词汇集vocab中的下标 */ int **class_words; /* 词汇类计数,在第i类中有class_cn[i-1]个单词 */ int *class_cn; /* 第i类中最多有class_max_cn[i-1]个单词 */ int *class_max_cn; /* 选择分类词的算法 构造函数中 = 0 */ int old_classes; /* 不重复的词汇集,数据类型见前 */ struct vocab_word *vocab; /* 根据词频vocab.cn对vocab[1: vocab_size-1]选择排序 */ void sortVocab(); /* 存放word在词汇集vocab中的下标,下标由hash函数映射得到 构造函数中: vocab_hash_size=100000000; hash数值小于100000000 vocab_hash=(int *)calloc(vocab_hash_size, */ int *vocab_hash; int vocab_hash_size; /* 输入层 */ int layer0_size; /* 隐藏层 构造函数中 = 30 */ int layer1_size; /* 压缩层 */ int layerc_size; /* 输出层 */ int layer2_size; /* ME模型中输入层到输出层的直连 构造函数中 = 0 */ long long direct_size; /* ME模型采用的特征阶数 构造函数中 = 0 */ int direct_order; /* history存放单词: history[0]存放w_t,history[1]存放w_{t-1},类推 */ int history[MAX_NGRAM_ORDER]; /* bptt <= 0="" 1="" 为常规的bptt,从s_{t}展开到s_{t-1}="" 构造函数中="0,为常规的bptt" *="" int="" bptt;="" 每次训练bptt_block个单词,使用bptt="" bptt_block;="" 也存放单词,下标从0开始存放w_{t},="" w_{t-1}="" 空指针="" *bptt_history;="" 存放隐藏层的状态,下标从0开始存放s_{t},="" s_{t-1}="" neuron="" *bptt_hidden;="" 输入层到隐藏层的权值,bptt中使用="" struct="" synapse="" *bptt_syn0;="" gen;="" 句子的独立训练="" independent="" !="0" 要求每个句子独立训练="" 上一个句子对下一个句子的训练算历史信息="" independent;="" 下列一系列指针在构造函数中="NULL" neurons="" in="" input="" layer="" *neu0;="" hidden *neu1;="" *neuc;="" output="" *neu2;="" weights="" between="" and="" *syn0;="" (or="" compression="" if="">0) */ struct synapse *syn1; /* weights between hidden and compression layer */ struct synapse *sync; /* direct parameters between input and output layer (similar to Maximum Entropy model parameters) */ direct_t *syn_d; //backup used in training: /* 数据备份 */ struct neuron *neu0b; struct neuron *neu1b; struct neuron *neucb; struct neuron *neu2b; struct synapse *syn0b; struct synapse *syn1b; struct synapse *syncb; direct_t *syn_db; //backup used in n-bset rescoring: struct neuron *neu1b2; =>
以上是CRnnLM的成员变量以及构造函数中的赋值情况。
rnnlmlib.cpp
CRnnLM除了成员变量以外,还有成员函数。Mikolov在组织文件时,自然所有的函数声明在类CRnnLM内,然后除了构造函数和析构函数以外,其他成员函数在rnnlmlib.cpp中实现。
/* 返回一个大小在 min 到 max 之间的 real 类型的浮点数 */ real CRnnLM::random(real min, real max) { return rand()/(real)RAND_MAX*(max-min)+min; } /* 设置训练集的文件名 */ void CRnnLM::setTrainFile(char *str) { strcpy(train_file, str); } /* 设置验证集、测试集、模型储存的文件名,代码基本相同 */ void CRnnLM::setValidFile(char *str); void CRnnLM::setTestFile(char *str); void CRnnLM::setRnnLMFile(char *str); /* 中间若干 void set 函数都是通过 public 的成员函数 来修改 protected 的成员变量,故此略去 */
词汇集 vocab 的操作。其中几个hash方面的函数主要是为最大熵模型 - Maximum Entropy 服务的。
/* 从文件 fin 中读取格式化单词到 word */ void CRnnLM::readWord(char *word, FILE *fin); /* 返回单词的hash值 */ int CRnnLM::getWordHash(char *word); /* 在词汇集中搜索word的索引 */ int CRnnLM::searchVocab(char *word); /* 给出当前文件指针所指的单词在vocab中的索引 */ int CRnnLM::readWordIndex(FILE *fin); /* 将形参加入vocab */ int CRnnLM::addWordToVocab(char *word); /* vocab的选择排序 */ void CRnnLM::sortVocab(); /* 从训练集中读取词汇到vocab词汇集 */ void CRnnLM::learnVocabFromTrainFile() /***************************************/ /* 从文件 fin 中读取格式化单词到 word */ void CRnnLM::readWord(char *word, FILE *fin) { int a=0, ch; while (!feof(fin)) { // 遍历文件 fin 中的字符 ch=fgetc(fin); if (ch==13) continue; /* 遇到回车键时本应结束,但仍然继续读文件 */ /* 如果遇到了单词的分隔符 */ if ((ch==' ') || (ch=='\t') || (ch=='\n')) { if (a > 0) { if (ch=='\n') ungetc(ch, fin); /* 把字符 ch 退回到输入流 fin */ break; } if (ch=='\n') { strcpy(word, (char *)" < /s > "); return; } else continue; } word[a]=ch; // 将输入流中读到的字符复制到word a++; if (a > =MAX_STRING) { // 字符串长度溢出 //printf("Too long word found!\n"); //truncate too long words a--; } } word[a]=0; // 用0作为字符串的截止符 } /***************************************/ /* 返回单词的hash值 */ int CRnnLM::getWordHash(char *word) { unsigned int hash, a; hash=0; /* 根据字符串每个字符的ASCII进行计算 */ for (a=0; a < strlen(word); a++) hash=hash*237+word[a]; hash=hash%vocab_hash_size; return hash; } /***************************************/ /* 在词汇集中搜索word的索引 */ int CRnnLM::searchVocab(char *word) { int a; unsigned int hash; hash=getWordHash(word); // 计算hash if (vocab_hash[hash]==-1) return -1; /* 在hash之后的词汇集中根据hash值查询对应的单词 */ if (!strcmp(word, vocab[vocab_hash[hash]].word)) return vocab_hash[hash]; for (a=0; a < vocab_size; a++) { //search in vocabulary if (!strcmp(word, vocab[a].word)) { vocab_hash[hash]=a; return a; } } return -1; //return OOV if not found } /***************************************/ /* 给出当前文件指针所指的单词在vocab中的索引 */ int CRnnLM::readWordIndex(FILE *fin) { char word[MAX_STRING]; readWord(word, fin) // 读取当前文件指针所指的单词 if (feof(fin)) return -1; return searchVocab(word) // 返回该单词在词汇集中的索引 } /***************************************/ /* 将形参加入vocab */ int CRnnLM::addWordToVocab(char *word) { unsigned int hash; strcpy(vocab[vocab_size].word, word); vocab[vocab_size].cn=0; vocab_size++; // 像队列一样,vocab增长一个词汇 /* vocab增长到max_size满了,再分配更多的空间 */ if (vocab_size+2 > =vocab_max_size) { //reallocate memory if needed vocab_max_size+=100; vocab=(struct vocab_word *)realloc(vocab, vocab_max_size * sizeof(struct vocab_word)); } /* 同时把word加入散列表中 */ hash=getWordHash(word); vocab_hash[hash]=vocab_size-1; return vocab_size-1; } /***************************************/ /* vocab的选择排序 */ void CRnnLM::sortVocab() { int a, b, max; vocab_word swap; 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; } } /***************************************/ /* 从训练集中读取词汇到vocab词汇集 */ void CRnnLM::learnVocabFromTrainFile() //assumes that vocabulary is empty { char word[MAX_STRING]; FILE *fin; int a, i, train_wcn; /* 先将hash词汇集填满-1 以免无法填满散列表,并且标识空散列的位置 */ for (a=0; a < vocab_hash_size; a++) vocab_hash[a]=-1; fin=fopen(train_file, "rb"); /* 刚初始化的词汇表为空 */ vocab_size=0; addWordToVocab((char *)" < /s > "); train_wcn=0; // 训练集中的单词数量 /* 将文件指针遍历文件 不断将训练集中的单词添加到词汇集vocab */ while (1) { readWord(word, fin); if (feof(fin)) break; train_wcn++; /* 根据单词频数选择排序 */ i=searchVocab(word); if (i==-1) { // 如果word没有在词汇集中出现过 a=addWordToVocab(word); vocab[a].cn=1; } else vocab[i].cn++; // 如果出现过 } sortVocab(); // 根据单词的频数进行选择排序 if (debug_mode > 0) { printf("Vocab size: %d\n", vocab_size); printf("Words in train file: %d\n", train_wcn); } train_words=train_wcn; fclose(fin); }
保存与恢复的函数,neu和syn系列的成员变量与neub和synb系列的备份变量相互赋值
void CRnnLM::saveWeights(); //saves current weights and unit activations void CRnnLM::restoreWeights(); //restores current weights and unit activations from backup copy void CRnnLM::saveContext(); //useful for n-best list processing void CRnnLM::saveContext2(); void CRnnLM::restoreContext2();
初始化设定,相似的代码都略过
/* 网络初始化 */ void CRnnLM::initNet() { int a, b, cl; layer0_size=vocab_size+layer1_size; layer2_size=vocab_size+class_size; /* 为输入层、隐藏层、压缩层、输出层分配空间 */ neu0=(struct neuron *)calloc(layer0_size, sizeof(struct neuron)); /* 为相应的突触分配空间 */ syn0=(struct synapse *)calloc(layer0_size*layer1_size, sizeof(struct synapse)); /* 为输入层、隐藏层、压缩层、输出层的备份神经元分配空间 */ neu0b=(struct neuron *)calloc(layer0_size, sizeof(struct neuron)); /* 为相应的突触备份分配空间 */ syn0b=(struct synapse *)calloc(layer0_size*layer1_size, sizeof(struct synapse)); /* 将输入层、隐藏层、压缩层、输出层所有的神经元的实值、误差值赋值为零 */ for (a=0; a < layer0_size; a++) { neu0[a].ac=0; neu0[a].er=0; } /* 为相应的突触赋值 */ for (b=0; b < layer1_size; b++) for (a=0; a < layer0_size; a++) { syn0[a+b*layer0_size].weight=random(-0.1, 0.1)+random(-0.1, 0.1)+random(-0.1, 0.1); } /* ME模型中直连的突触赋值 */ long long aa; for (aa=0; aa < direct_size; aa++) syn_d[aa]=0; /* 选择bptt方式训练 */ if (bptt > 0) { bptt_history=(int *)calloc((bptt+bptt_block+10), sizeof(int)); for (a=0; a < bptt+bptt_block; a++) bptt_history[a]=-1; // bptt_hidden=(neuron *)calloc((bptt+bptt_block+1)*layer1_size, sizeof(neuron)); for (a=0; a < (bptt+bptt_block)*layer1_size; a++) { bptt_hidden[a].ac=0; bptt_hidden[a].er=0; } // bptt_syn0=(struct synapse *)calloc(layer0_size*layer1_size, sizeof(struct synapse)); if (bptt_syn0==NULL) { printf("Memory allocation failed\n"); exit(1); } } saveWeights(); // 将权值备份储存
下面一段仍然是初始化函数 - initNet() 的一部分,是有关分类优化的处理,所以特别提出来展示。Mikolov提到,这部分运算是在训练之前实现的,所以相关的代码出现在initNet()内:
/* 输出层分解 - Factorization of the output layer frequency binning */ double df, dd; int i; df=0; // 遍历到当前word的频率加总 dd=0; a=0; // 分类的class序数,从0开始生长 b=0; // vocab中单词频数加总 if (old_classes) { // 选择一种词的分类算法 /* b 加总vocab词汇集中所有单词的频数 */ for (i=0; i < vocab_size; i++) b+=vocab[i].cn; for (i=0; i < vocab_size; i++) { /* 加总频率 */ df+=vocab[i].cn/(double)b; /* 保证频率之和必定==1,规避一些计算错误 */ if (df > 1) df=1; /* 根据遍历到当前的word频率加总与代表当前class 的a比较。也就是说直到df累加到超过a所占的比率, class前进一位 */ if (df > (a+1)/(double)class_size) { vocab[i].class_index=a; if (a < class_size-1) a++; } else { vocab[i].class_index=a; } } } else { /* 另一种算法 */ } //allocate auxiliary class variables //(for faster search when normalizing probability //at output layer) class_words=(int **)calloc(class_size, sizeof(int *)); class_cn=(int *)calloc(class_size, sizeof(int)); class_max_cn=(int *)calloc(class_size, sizeof(int)); for (i=0; i < class_size; i++) { class_cn[i]=0; class_max_cn[i]=10; /* 二维数组int **class_words;进一步分配空间 */ class_words[i]=(int *)calloc(class_max_cn[i], sizeof(int)); } /* 按照class_words[cl][class_cn[cl]]来赋值,skip */ }
下面是神经网络对文件的输入输出方面的函数,也不必深究。
/* 将网络模型的所有信息fprintf打印储存到rnnlm_file文件中 虽然这个函数本身的对网络训练没什么助益,但是可以通过 fprintf打印的内容与格式来理解不同变量在程序中的意义, 对理解程序帮助较大。 */ void saveNet(); /* 在文件中找到ASCII == delim的字符,使文件指针fi指向 这个字符 - delim 出现的下一个字符位置 这个函数是为了下一个加载信息 restoreNet()服务的 */ void goToDelimiter(int delim, FILE *fi); /* 从rnnlm_file文件中读取所有的网络信息,通过goToDelimiter() 进行定位,将从文件中读取到的网络信息加载到成员变量的 指针地址上,从而根据rnnlm_file恢复网络 */ void restoreNet(); /* 清除所有的激活值和误差向量,将神经元的ac, er置零 */ void netFlush(); /* 将隐藏层的ac值置一 neu1[].ac = 1, bptt+history置零 */ void netReset();
接下来是正儿八经的网络训练部分的函数了。需要注意的是,这个神经网络的训练是有class层的,可以参看论文部分的博文,当然更好的是直接看Mikolov的博士论文,更更好的是顺带读一读列出的参考论文。但是需要注意的是代码中的网络结构并非与论文中描述的一模一样,具体可以看下面这张图:
在训练过程中并没有采用两个单独的矩阵( \mathbf{U}, \mathbf{W} ) 而是只用了一个 syn0. 同时,也没有分为 ( \mathbf{w}(t), \mathbf{s}(t-1) ) 进行输入,而是将它们并括为neu0.
/* 突触矩阵与神经元向量乘积的函数 主要用于下面传播过程中的各种计算 */ void CRnnLM::matrixXvector(struct neuron *dest, struct neuron *srcvec, struct synapse *srcmatrix, int matrix_width, int from, int to, int from2, int to2, int type);
前向传播过程
void CRnnLM::computeNet(int last_word, int word) { /* 形参相当于论文中的 w_{t-1}, w_{t} */ int a, b, c; real val; /* sum is used for normalization: it's better to have larger precision as many numbers are summed together here */ double sum; /* 如果上一个单词不是终止符 将neu0激活成在last_word位置one-hot 的词向量 */ if (last_word!=-1) neu0[last_word].ac=1; /******************************************/ /* START 从输入层传播到隐藏层 */ /* 将所有隐藏层的神经元ac值置零 */ for (a=0; a < layer1_size; a++) neu1[a].ac=0; /* 将所有class层神经元的ac值置零 */ for (a=0; a < layerc_size; a++) neuc[a].ac=0; /* 进行矩阵运算,注意看这里:实际上matrixXvector() 函数并没有直接计算 neu1 = syn0 * neu0,而是计算 [from : to]的部分,也就是说实际上这里的调用计算的 是: neu0[0size - 1size : 0size] * syn0[0 : 1size][0size - 1size : 0size] 也就是说输入层计算的神经元实际上是最后layer1_size个, 也就是 s(t-1) 的部分 */ matrixXvector(neu1, neu0, syn0, layer0_size, 0, layer1_size, layer0_size-layer1_size, layer0_size, 0); /* 再将neu0中 onehot 词向量部分的矩阵运算加上去 */ for (b=0; b < layer1_size; b++) { a=last_word; if (a!=-1) neu1[b].ac += neu0[a].ac * syn0[a+b*layer0_size].weight; } /* 通过 sigmoid 激活到隐藏层 */ for (a=0; a < layer1_size; a++) { /* 为了数据稳定性,将ac值限定在[-50, 50]以内 */ if (neu1[a].ac > 50) neu1[a].ac=50; if (neu1[a].ac < -50) neu1[a].ac=-50; val=-neu1[a].ac; neu1[a].ac=1/(1+fasterexp(val)); } /* END 从输入层传播到隐藏层 */ /******************************************/ /* */ /******************************************/ /* START 从隐藏层传播到压缩层 */ if (layerc_size > 0) { /* 计算压缩层 */ /* 计算 neuc = neu1[0 : 1size] * syn1[0 : csize][0 : 1size] */ matrixXvector(neuc, neu1, syn1, layer1_size, 0, layerc_size, 0, layer1_size, 0); /* sigmoid与数值稳定性 */ for (a=0; a < layerc_size; a++) { if (neuc[a].ac > 50) neuc[a].ac=50; if (neuc[a].ac < -50) neuc[a].ac=-50; val=-neuc[a].ac; neuc[a].ac=1/(1+fasterexp(val)); } } /* END 从隐藏层传播到压缩层 */ /******************************************/ /* */ /******************************************/ /* START 从隐藏层传播到输出层 - class */ for (b=vocab_size; b < layer2_size; b++) neu2[b].ac=0; if (layerc_size > 0) { /* 中间经过压缩层 */ matrixXvector(neu2, neuc, sync, layerc_size, vocab_size, layer2_size, 0, layerc_size, 0); } else /* 不计算压缩层,直接从隐藏层计算到输出层 */ { matrixXvector(neu2, neu1, syn1, layer1_size, vocab_size, layer2_size, 0, layer1_size, 0); } /* 最大熵模型 - Maxmium Entropy Model 对neu2的class部分采用直连的方法 */ if (direct_size > 0) { /* 使用ME模型 */ unsigned long long hash[MAX_NGRAM_ORDER]; //this will hold pointers to syn_d that contains hash parameters for (a=0; a < direct_order; a++) hash[a]=0; for (a=0; a < direct_order; a++) { b=0; if (a > 0) if (history[a-1]==-1) break; /* if OOV was in history, do not use this N-gram feature and higher orders */ hash[a]=PRIMES[0]*PRIMES[1]; for (b=1; b < =a; b++) hash[a]+= PRIMES[(a*PRIMES[b]+b)%PRIMES_SIZE] *(unsigned long long)(history[b-1]+1); /* update hash value based on words from the history */ hash[a]=hash[a]%(direct_size/2); /* make sure that starting hash index is in the first half of syn_d (second part is reserved for history- > words features) */ } for (a=vocab_size; a < layer2_size; a++) { for (b=0; b < direct_order; b++) if (hash[b]) { neu2[a].ac+=syn_d[hash[b]]; //apply current parameter and move to the next one hash[b]++; } else break; } } /* softmax的计算 这里的softmax计算采用了一个小技巧,以防在计算exp时 指数爆炸,就是softmax分子分母同时除以e^{maxAc}: \frac{e^{ac}}{\sum_{i} e^{ac_{i}}} = \frac{e^{ac - maxAc}}{\sum_{i} e^{ac_{i} - maxAc}} */ sum=0; real maxAc=-FLT_MAX; for (a=vocab_size; a < layer2_size; a++) if (neu2[a].ac > maxAc) maxAc=neu2[a].ac; //this prevents the need to check for overflow for (a=vocab_size; a < layer2_size; a++) sum+=fasterexp(neu2[a].ac-maxAc); for (a=vocab_size; a < layer2_size; a++) neu2[a].ac=fasterexp(neu2[a].ac-maxAc)/sum; if (gen > 0) return; /* if we generate words, we don't know what current word is - > only classes are estimated and word is selected in testGen() */ /* END 从隐藏层传播到输出层 - class */ /******************************************/ /* */ /******************************************/ /* START 从隐藏层传播到输出层 - word */ if (word!=-1) { /* word所在的class的所有词的ac值被置零 */ for (c=0; c < class_cn[vocab[word].class_index]; c++){ neu2[class_words[vocab[word].class_index][c]].ac=0; } if (layerc_size > 0) { /* 计算word所在class的词从压缩层传播到输出层 */ matrixXvector(neu2, neuc, sync, layerc_size, class_words[vocab[word].class_index][0], class_words[ vocab[word].class_index][0] +class_cn[vocab[word].class_index], 0, layerc_size, 0); } else /* 否则word所在class内的词直接从隐藏层传播到输出层 */ { matrixXvector(neu2, neu1, syn1, layer1_size, class_words[vocab[word].class_index][0], class_words[ vocab[word].class_index][0] +class_cn[vocab[word].class_index], 0, layer1_size, 0); } } /* 最大熵模型 - Maxmium Entropy Model 对neu2的word部分采用直连的方法 skip */ /* softmax skip */ /* END 从隐藏层传播到输出层 - word */ /******************************************/ /* */ }
反向传播过程
void CRnnLM::learnNet(int last_word, int word)
未完