Recurrent neural network language modeling toolkit 源码走读(七)

系列前言
参考文献:
  1. RNNLM - Recurrent Neural Network  Language Modeling Toolkit(点此阅读)
  2. Recurrent neural network based language model(点此阅读)
  3. EXTENSIONS OF RECURRENT NEURAL NETWORK LANGUAGE MODEL(点此阅读)
  4. Strategies for Training Large Scale Neural Network  Language Models(点此阅读)
  5. STATISTICAL LANGUAGE MODELS BASED ON NEURAL  NETWORKS(点此阅读)
  6. A guide to recurrent neural networks and backpropagation(点此阅读)
  7. A Neural Probabilistic Language Model(点此阅读)
  8. Learning Long-Term Dependencies with Gradient Descent is Difficult(点此阅读)
  9. Can Artificial Neural Networks Learn Language Models?(点此阅读)

前一篇是网络的前向计算,这篇是网络的学习算法,学习算法我在rnnlm原理及BPTT数学推导中介绍了。学习算法主要更新的地方在网络中的权值,这个最终版本的网络的权值大体可以分为三个部分来看:第一个是网络中类似输入到隐层的权值,隐层到输出层的权值。第二个是网络中ME的部分,即输入层到输出层的权值部分。第三个来看是BPTT的部分。我先把整个网络的ME+Rnn图放上来,然后再贴带注释的源码,结构图如下:

技术分享

下面代码还是分成两部分来看,一部分是更新非BPTT部分,一个是更新BPTT部分,如下:
//反传误差,更新网络权值
void CRnnLM::learnNet(int last_word, int word)
{
	//word表示要预测的词,last_word表示当前输入层所在的词
    int a, b, c, t, step;
    real beta2, beta3;
	
	//alpha表示学习率,初始值为0.1, beta初始值为0.0000001;
    beta2=beta*alpha;
	//这里注释不懂,希望明白的朋友讲下~
    beta3=beta2*1;	//beta3 can be possibly larger than beta2, as that is useful on small datasets (if the final model is to be interpolated wich backoff model) - todo in the future
	
    if (word==-1) return;
	
    //compute error vectors,计算输出层的(只含word所在类别的所有词)误差向量
    for (c=0; c<class_cn[vocab[word].class_index]; c++) {
		a=class_words[vocab[word].class_index][c];
        neu2[a].er=(0-neu2[a].ac);
    }
    neu2[word].er=(1-neu2[word].ac);	//word part
	
    //flush error
    for (a=0; a<layer1_size; a++) neu1[a].er=0;
    for (a=0; a<layerc_size; a++) neuc[a].er=0;
	
	//计算输出层的class部分的误差向量
    for (a=vocab_size; a<layer2_size; a++) {
        neu2[a].er=(0-neu2[a].ac);
    }
    neu2[vocab[word].class_index+vocab_size].er=(1-neu2[vocab[word].class_index+vocab_size].ac);	//class part
    
    //计算特征所在syn_d中的下标,和上面一样,针对ME中word部分
    if (direct_size>0) {	//learn direct connections between words
		if (word!=-1) {
			unsigned long long hash[MAX_NGRAM_ORDER];
			
			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;
				hash[a]=PRIMES[0]*PRIMES[1]*(unsigned long long)(vocab[word].class_index+1);
				
				for (b=1; b<=a; b++) hash[a]+=PRIMES[(a*PRIMES[b]+b)%PRIMES_SIZE]*(unsigned long long)(history[b-1]+1);
				hash[a]=(hash[a]%(direct_size/2))+(direct_size)/2;
			}
			
			//更新ME中的权值部分,这部分是正对word的
			for (c=0; c<class_cn[vocab[word].class_index]; c++) {
				a=class_words[vocab[word].class_index][c];
				
				//这里的更新公式很容易推导,利用梯度上升,和RNN是一样的
				//详见我的这篇文章,另外这里不同的是权值是放在一维数组里面的,所以程序写法有点不同
				for (b=0; b<direct_order; b++) if (hash[b]) {
					syn_d[hash[b]]+=alpha*neu2[a].er - syn_d[hash[b]]*beta3;
					hash[b]++;
					hash[b]=hash[b]%direct_size;
				} else break;
			}
		}
    }
    
	//计算n元模型特征,这是对class计算的
    //learn direct connections to classes
    if (direct_size>0) {	//learn direct connections between words and classes
		unsigned long long hash[MAX_NGRAM_ORDER];
		
		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;
			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);
			hash[a]=hash[a]%(direct_size/2);
		}
		
		//和上面一样,更新ME中权值部分,这是对class的
		for (a=vocab_size; a<layer2_size; a++) {
			for (b=0; b<direct_order; b++) if (hash[b]) {
				syn_d[hash[b]]+=alpha*neu2[a].er - syn_d[hash[b]]*beta3;
				hash[b]++;
			} else break;
		}
    }
    //
    
    //含压缩层的情况,更新sync, syn1
    if (layerc_size>0) {
		//将输出层的误差传递到压缩层,即计算部分V × eo^T(符号含义表示见图)
		matrixXvector(neuc, neu2, 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, 1);
		
		//这里把一维的权值转换为二维的权值矩阵好理解一些
		//注意这里的t相当于定位到了参数矩阵的行,下面的下标a相当于列
		t=class_words[vocab[word].class_index][0]*layerc_size;
		for (c=0; c<class_cn[vocab[word].class_index]; c++) {
			b=class_words[vocab[word].class_index][c];
			//这里的更新公式见我写的另一篇文章的rnn推导
			//并且每训练10次才L2正规化一次 
			if ((counter%10)==0)	//regularization is done every 10. step
				for (a=0; a<layerc_size; a++) sync[a+t].weight+=alpha*neu2[b].er*neuc[a].ac - sync[a+t].weight*beta2;
				else
					for (a=0; a<layerc_size; a++) sync[a+t].weight+=alpha*neu2[b].er*neuc[a].ac;
					//参数矩阵下移动一行
					t+=layerc_size;
		}
		
		//将输出层的误差传递到压缩层,即计算部分V × eo^T(符号含义表示见图)
		//这里计算输出层中class部分到压缩层
		matrixXvector(neuc, neu2, sync, layerc_size, vocab_size, layer2_size, 0, layerc_size, 1);		//propagates errors 2->c for classes
		
		//这里同样相当于定位的权值矩阵中的行,下面的a表示列
		c=vocab_size*layerc_size;
		
		//更新和上面公式是一样的,不同的是更新的权值数组中不同的部分
		for (b=vocab_size; b<layer2_size; b++) {
			if ((counter%10)==0) {	
				for (a=0; a<layerc_size; a++) sync[a+c].weight+=alpha*neu2[b].er*neuc[a].ac - sync[a+c].weight*beta2;	//weight c->2 update
			}
			else {
				for (a=0; a<layerc_size; a++) sync[a+c].weight+=alpha*neu2[b].er*neuc[a].ac;	//weight c->2 update
			}
			//下一行
			c+=layerc_size;
		}
		
		//这里是误差向量,即论文中的e hj (t)
		for (a=0; a<layerc_size; a++) neuc[a].er=neuc[a].er*neuc[a].ac*(1-neuc[a].ac);    //error derivation at compression layer
		
		////
		//下面都是同理,将误差再往下传,计算syn1(二维) × ec^T, ec表示压缩层的误差向量
		matrixXvector(neu1, neuc, syn1, layer1_size, 0, layerc_size, 0, layer1_size, 1);		//propagates errors c->1
		
		//更新syn1,相应的见公式
		for (b=0; b<layerc_size; b++) {
			for (a=0; a<layer1_size; a++) syn1[a+b*layer1_size].weight+=alpha*neuc[b].er*neu1[a].ac;	//weight 1->c update
		}
    }
    else		//无压缩层的情况,更新syn1
    {
		//下面的情况和上面类似,只是少了一个压缩层
		matrixXvector(neu1, neu2, 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, 1);
		
		t=class_words[vocab[word].class_index][0]*layer1_size;
		for (c=0; c<class_cn[vocab[word].class_index]; c++) {
			b=class_words[vocab[word].class_index][c];
			if ((counter%10)==0)	//regularization is done every 10. step
				for (a=0; a<layer1_size; a++) syn1[a+t].weight+=alpha*neu2[b].er*neu1[a].ac - syn1[a+t].weight*beta2;
				else
					for (a=0; a<layer1_size; a++) syn1[a+t].weight+=alpha*neu2[b].er*neu1[a].ac;
					t+=layer1_size;
		}
		//
		matrixXvector(neu1, neu2, syn1, layer1_size, vocab_size, layer2_size, 0, layer1_size, 1);		//propagates errors 2->1 for classes
		
		c=vocab_size*layer1_size;
		for (b=vocab_size; b<layer2_size; b++) {
			if ((counter%10)==0) {	//regularization is done every 10. step
				for (a=0; a<layer1_size; a++) syn1[a+c].weight+=alpha*neu2[b].er*neu1[a].ac - syn1[a+c].weight*beta2;	//weight 1->2 update
			}
			else {
				for (a=0; a<layer1_size; a++) syn1[a+c].weight+=alpha*neu2[b].er*neu1[a].ac;	//weight 1->2 update
			}
			c+=layer1_size;
		}
    }
    
    //
    //到这里,上面部分把到隐层的部分更新完毕
    ///////////////
	
	//这里就是最常规的RNN,即t时刻往前只展开了s(t-1)
    if (bptt<=1) {		//bptt==1 -> normal BP
		
		//计算隐层的误差,即eh(t)
		for (a=0; a<layer1_size; a++) neu1[a].er=neu1[a].er*neu1[a].ac*(1-neu1[a].ac);    //error derivation at layer 1
		
		//这部分更新隐层到word部分的权值
		//注意由于word部分是1-of-V编码
		//所以这里更新的循环式上有点不同,并且下面的更新都是每10次训练更新才进行一次L2正规化
		a=last_word;
		if (a!=-1) {
			if ((counter%10)==0)			
				for (b=0; b<layer1_size; b++) syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac - syn0[a+b*layer0_size].weight*beta2;
				else
					//我们可以把这里的权值看做一个矩阵
					//b表示行,a表示列,非L2正规化的完整更新式如下:
					/*for (b=0; b<layer1_size; b++) {
					for (k=0; k<vocab_size; k++) {
					syn0[k+b*layer0_size].weight+=alpha*neu1[b].er*neu0[k].ac;
					}
		}*/
		//但由于neu0[k]==1只有当k==a时,所以为了加快循环计算的速度,更新就变为如下了
		//下面的代码是类似的道理,其实这里的neu0[a].ac可以直接省略掉,因为本身值为1				
		for (b=0; b<layer1_size; b++) syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac;
		}
		
		//这部分更新隐层到s(t-1)的权值
		if ((counter%10)==0) {
			for (b=0; b<layer1_size; b++) for (a=layer0_size-layer1_size; a<layer0_size; a++) syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac - syn0[a+b*layer0_size].weight*beta2;
		}
		else {
			for (b=0; b<layer1_size; b++) for (a=layer0_size-layer1_size; a<layer0_size; a++) syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac;
		}
    }

下面是更新BPTT的部分, 还是先把BPTT算法部分的数据结构图上来,这样更方便对照:

技术分享

else		//BPTT
    {
		//BPTT部分权值就不在用syn0了,而是用bptt_syn0
		//神经元部分的存储用bptt_hidden
		
		//将隐层的神经元信息复制到bptt_hidden,这里对neu1做备份
		for (b=0; b<layer1_size; b++) bptt_hidden[b].ac=neu1[b].ac;
		for (b=0; b<layer1_size; b++) bptt_hidden[b].er=neu1[b].er;
		
		//bptt_block第一个条件是控制BPTT的,因为不能每训练一个word都用BPTT来深度更新参数
		//这样每进行一次训练计算量太大,过于耗时
		//word==0表示当前一个句子结束
		//或者当independent开关不为0时,下一个词是句子的结束,则进行BPTT
		if (((counter%bptt_block)==0) || (independent && (word==0))) {
			
			//step表示
			for (step=0; step<bptt+bptt_block-2; step++) {
				
				//计算隐层的误差向量,即eh(t)
				for (a=0; a<layer1_size; a++) neu1[a].er=neu1[a].er*neu1[a].ac*(1-neu1[a].ac);    //error derivation at layer 1
				
				//更新的是输入层中vocab_size那部分权值
				//bptt_history下标从0开始依次存放的是wt, wt-1, wt-2...
				//比如当step == 0时,相当于正在进行普通的BPTT,这时bptt_history[step]表示当前输入word的在vocab中的索引
				a=bptt_history[step];	
				if (a!=-1)
					for (b=0; b<layer1_size; b++) {
						//由于输入层word部分是1-of-V编码,所以neu0[a].ac==1 所以这里后面没有乘以它
						//在step == 0时,bptt_syn0相当于原来的syn0
						bptt_syn0[a+b*layer0_size].weight+=alpha*neu1[b].er;
					}
					
					//为从隐层往状态层传误差做准备
					for (a=layer0_size-layer1_size; a<layer0_size; a++) neu0[a].er=0;
					
					//从隐层传递误差到状态层
					matrixXvector(neu0, neu1, syn0, layer0_size, 0, layer1_size, layer0_size-layer1_size, layer0_size, 1);		//propagates errors 1->0
					
					//更新隐层到状态层的权值
					//之所以先计算误差的传递就是因为更新完权值会发生改变
					for (b=0; b<layer1_size; b++) for (a=layer0_size-layer1_size; a<layer0_size; a++) {
						//neu0[a].er += neu1[b].er * syn0[a+b*layer0_size].weight;
						bptt_syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac;
					}
					
					//todo这里我理解是把论文中的s(t-1)复制到s(t),准备下一次循环
					for (a=0; a<layer1_size; a++) {		//propagate error from time T-n to T-n-1
						//后面为什么会加bptt_hidden呢
						neu1[a].er=neu0[a+layer0_size-layer1_size].er + bptt_hidden[(step+1)*layer1_size+a].er;
					}
					
					//历史中的s(t-2)复制到s(t-1)
					if (step<bptt+bptt_block-3)
						for (a=0; a<layer1_size; a++) {
							neu1[a].ac=bptt_hidden[(step+1)*layer1_size+a].ac;
							neu0[a+layer0_size-layer1_size].ac=bptt_hidden[(step+2)*layer1_size+a].ac;
						}

到这里,无奈再把代码打断一下,把上面两个关键循环按照step的增加走一下,并假设当前时刻为t,展开一下上面的循环:
step = 0:
第一个for循环完成:
neu1.er = neu0.er + btpp_hidden.er -> s(t) = s(t-1) + s(t-1)
第二个for循环完成:
neu1.ac = bptt_hidden.ac -> s(t) = s(t-1)
neu0.ac = bptt_hidden.ac -> s(t-1) = s(t-2)
step = 1:
第一个for循环完成:
neu1.er = neu0.er + btpp_hidden.er -> s(t-1) = s(t-2) + s(t-2)
第二个for循环完成:
neu1.ac = bptt_hidden.ac -> s(t-1) = s(t-2)
neu0.ac = bptt_hidden.ac -> s(t-2) = s(t-3)

通过上面列出的步骤,大概能知道原理,其中neu0,和bptt_hidden所对应的s(t-1)有不同,前者的er值隐层传下来的 后者的er值是在t-1时刻,压缩层传下来的,感觉直接让neu0.er覆盖neu1.er即可,这里不太明白为什么要将两者加起来希望明白的朋友告知一下哈,我的一个想法是bptt_hidden对应的误差是从输出传到隐层的,neu0的er是输出传到输入层的两者加起来更利于整个网络的优化,其二由于在循环网络中,误差梯度向量会随着网络深度增加而消失,加上bptt_hidden或许可以某种程度上预防这种情况的发生,也不知道是压根就没理解清楚上面的循环而瞎想还是其他什么问题,总之欢迎懂的朋友告知~

然后继续被打断的代码:
}
			
			//清空历史信息中的er值
			for (a=0; a<(bptt+bptt_block)*layer1_size; a++) {
				bptt_hidden[a].er=0;
			}
			
			//这里恢复neu1,因为neu1反复在循环中使用
			for (b=0; b<layer1_size; b++) neu1[b].ac=bptt_hidden[b].ac;		//restore hidden layer after bptt
			
			
			//将BPTT后的权值改变作用到syn0上
			for (b=0; b<layer1_size; b++) {		//copy temporary syn0
				
				//bptt完毕,将bptt_syn0的权值复制到syn0中来,这是复制状态层部分
				if ((counter%10)==0) {
					for (a=layer0_size-layer1_size; a<layer0_size; a++) {
						syn0[a+b*layer0_size].weight+=bptt_syn0[a+b*layer0_size].weight - syn0[a+b*layer0_size].weight*beta2;
						bptt_syn0[a+b*layer0_size].weight=0;
					}
				}
				else {
					for (a=layer0_size-layer1_size; a<layer0_size; a++) {
						syn0[a+b*layer0_size].weight+=bptt_syn0[a+b*layer0_size].weight;
						bptt_syn0[a+b*layer0_size].weight=0;
					}
				}
				
				//bptt完毕,将bptt_syn0的权值复制到syn0中来,这是word部分
				if ((counter%10)==0) {
					for (step=0; step<bptt+bptt_block-2; step++) if (bptt_history[step]!=-1) {
						syn0[bptt_history[step]+b*layer0_size].weight+=bptt_syn0[bptt_history[step]+b*layer0_size].weight - syn0[bptt_history[step]+b*layer0_size].weight*beta2;
						bptt_syn0[bptt_history[step]+b*layer0_size].weight=0;
					}
				}
				else {
					//因为输入的word层是1-of-V编码,并不是全部的权值改变
					//这样写可以加快计算速度,避免不必要的循环检索
					for (step=0; step<bptt+bptt_block-2; step++) if (bptt_history[step]!=-1) {
						syn0[bptt_history[step]+b*layer0_size].weight+=bptt_syn0[bptt_history[step]+b*layer0_size].weight;
						bptt_syn0[bptt_history[step]+b*layer0_size].weight=0;
					}
				}
			}
		}
    }	
}

//将隐层神经元的ac值复制到输出层后layer1_size那部分
void CRnnLM::copyHiddenLayerToInput()
{
    int a;
	
    for (a=0; a<layer1_size; a++) {
        neu0[a+layer0_size-layer1_size].ac=neu1[a].ac;
    }
}

最后仍然和前面一样,把整个函数完整的注释版贴在下面:
//反传误差,更新网络权值
void CRnnLM::learnNet(int last_word, int word)
{
	//word表示要预测的词,last_word表示当前输入层所在的词
    int a, b, c, t, step;
    real beta2, beta3;
	
	//alpha表示学习率,初始值为0.1, beta初始值为0.0000001;
    beta2=beta*alpha;
	//这里注释不懂,希望明白的朋友讲下~
    beta3=beta2*1;	//beta3 can be possibly larger than beta2, as that is useful on small datasets (if the final model is to be interpolated wich backoff model) - todo in the future
	
    if (word==-1) return;
	
    //compute error vectors,计算输出层的(只含word所在类别的所有词)误差向量
    for (c=0; c<class_cn[vocab[word].class_index]; c++) {
		a=class_words[vocab[word].class_index][c];
        neu2[a].er=(0-neu2[a].ac);
    }
    neu2[word].er=(1-neu2[word].ac);	//word part
	
    //flush error
    for (a=0; a<layer1_size; a++) neu1[a].er=0;
    for (a=0; a<layerc_size; a++) neuc[a].er=0;
	
	//计算输出层的class部分的误差向量
    for (a=vocab_size; a<layer2_size; a++) {
        neu2[a].er=(0-neu2[a].ac);
    }
    neu2[vocab[word].class_index+vocab_size].er=(1-neu2[vocab[word].class_index+vocab_size].ac);	//class part
    
    //计算特征所在syn_d中的下标,和上面一样,针对ME中word部分
    if (direct_size>0) {	//learn direct connections between words
		if (word!=-1) {
			unsigned long long hash[MAX_NGRAM_ORDER];
			
			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;
				hash[a]=PRIMES[0]*PRIMES[1]*(unsigned long long)(vocab[word].class_index+1);
				
				for (b=1; b<=a; b++) hash[a]+=PRIMES[(a*PRIMES[b]+b)%PRIMES_SIZE]*(unsigned long long)(history[b-1]+1);
				hash[a]=(hash[a]%(direct_size/2))+(direct_size)/2;
			}
			
			//更新ME中的权值部分,这部分是正对word的
			for (c=0; c<class_cn[vocab[word].class_index]; c++) {
				a=class_words[vocab[word].class_index][c];
				
				//这里的更新公式很容易推导,利用梯度上升,和RNN是一样的
				//详见我的这篇文章,另外这里不同的是权值是放在一维数组里面的,所以程序写法有点不同
				for (b=0; b<direct_order; b++) if (hash[b]) {
					syn_d[hash[b]]+=alpha*neu2[a].er - syn_d[hash[b]]*beta3;
					hash[b]++;
					hash[b]=hash[b]%direct_size;
				} else break;
			}
		}
    }
    
	//计算n元模型特征,这是对class计算的
    //learn direct connections to classes
    if (direct_size>0) {	//learn direct connections between words and classes
		unsigned long long hash[MAX_NGRAM_ORDER];
		
		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;
			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);
			hash[a]=hash[a]%(direct_size/2);
		}
		
		//和上面一样,更新ME中权值部分,这是对class的
		for (a=vocab_size; a<layer2_size; a++) {
			for (b=0; b<direct_order; b++) if (hash[b]) {
				syn_d[hash[b]]+=alpha*neu2[a].er - syn_d[hash[b]]*beta3;
				hash[b]++;
			} else break;
		}
    }
    //
    
    //含压缩层的情况,更新sync, syn1
    if (layerc_size>0) {
		//将输出层的误差传递到压缩层,即计算部分V × eo^T(符号含义表示见图)
		matrixXvector(neuc, neu2, 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, 1);
		
		//这里把一维的权值转换为二维的权值矩阵好理解一些
		//注意这里的t相当于定位到了参数矩阵的行,下面的下标a相当于列
		t=class_words[vocab[word].class_index][0]*layerc_size;
		for (c=0; c<class_cn[vocab[word].class_index]; c++) {
			b=class_words[vocab[word].class_index][c];
			//这里的更新公式见我写的另一篇文章的rnn推导
			//并且每训练10次才L2正规化一次 
			if ((counter%10)==0)	//regularization is done every 10. step
				for (a=0; a<layerc_size; a++) sync[a+t].weight+=alpha*neu2[b].er*neuc[a].ac - sync[a+t].weight*beta2;
				else
					for (a=0; a<layerc_size; a++) sync[a+t].weight+=alpha*neu2[b].er*neuc[a].ac;
					//参数矩阵下移动一行
					t+=layerc_size;
		}
		
		//将输出层的误差传递到压缩层,即计算部分V × eo^T(符号含义表示见图)
		//这里计算输出层中class部分到压缩层
		matrixXvector(neuc, neu2, sync, layerc_size, vocab_size, layer2_size, 0, layerc_size, 1);		//propagates errors 2->c for classes
		
		//这里同样相当于定位的权值矩阵中的行,下面的a表示列
		c=vocab_size*layerc_size;
		
		//更新和上面公式是一样的,不同的是更新的权值数组中不同的部分
		for (b=vocab_size; b<layer2_size; b++) {
			if ((counter%10)==0) {	
				for (a=0; a<layerc_size; a++) sync[a+c].weight+=alpha*neu2[b].er*neuc[a].ac - sync[a+c].weight*beta2;	//weight c->2 update
			}
			else {
				for (a=0; a<layerc_size; a++) sync[a+c].weight+=alpha*neu2[b].er*neuc[a].ac;	//weight c->2 update
			}
			//下一行
			c+=layerc_size;
		}
		
		//这里是误差向量,即论文中的e hj (t)
		for (a=0; a<layerc_size; a++) neuc[a].er=neuc[a].er*neuc[a].ac*(1-neuc[a].ac);    //error derivation at compression layer
		
		////
		//下面都是同理,将误差再往下传,计算syn1(二维) × ec^T, ec表示压缩层的误差向量
		matrixXvector(neu1, neuc, syn1, layer1_size, 0, layerc_size, 0, layer1_size, 1);		//propagates errors c->1
		
		//更新syn1,相应的见公式
		for (b=0; b<layerc_size; b++) {
			for (a=0; a<layer1_size; a++) syn1[a+b*layer1_size].weight+=alpha*neuc[b].er*neu1[a].ac;	//weight 1->c update
		}
    }
    else		//无压缩层的情况,更新syn1
    {
		//下面的情况和上面类似,只是少了一个压缩层
		matrixXvector(neu1, neu2, 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, 1);
		
		t=class_words[vocab[word].class_index][0]*layer1_size;
		for (c=0; c<class_cn[vocab[word].class_index]; c++) {
			b=class_words[vocab[word].class_index][c];
			if ((counter%10)==0)	//regularization is done every 10. step
				for (a=0; a<layer1_size; a++) syn1[a+t].weight+=alpha*neu2[b].er*neu1[a].ac - syn1[a+t].weight*beta2;
				else
					for (a=0; a<layer1_size; a++) syn1[a+t].weight+=alpha*neu2[b].er*neu1[a].ac;
					t+=layer1_size;
		}
		//
		matrixXvector(neu1, neu2, syn1, layer1_size, vocab_size, layer2_size, 0, layer1_size, 1);		//propagates errors 2->1 for classes
		
		c=vocab_size*layer1_size;
		for (b=vocab_size; b<layer2_size; b++) {
			if ((counter%10)==0) {	//regularization is done every 10. step
				for (a=0; a<layer1_size; a++) syn1[a+c].weight+=alpha*neu2[b].er*neu1[a].ac - syn1[a+c].weight*beta2;	//weight 1->2 update
			}
			else {
				for (a=0; a<layer1_size; a++) syn1[a+c].weight+=alpha*neu2[b].er*neu1[a].ac;	//weight 1->2 update
			}
			c+=layer1_size;
		}
    }
    
    //
    //到这里,上面部分把到隐层的部分更新完毕
    ///////////////
	
	//这里就是最常规的RNN,即t时刻往前只展开了s(t-1)
    if (bptt<=1) {		//bptt==1 -> normal BP
		
		//计算隐层的误差,即eh(t)
		for (a=0; a<layer1_size; a++) neu1[a].er=neu1[a].er*neu1[a].ac*(1-neu1[a].ac);    //error derivation at layer 1
		
		//这部分更新隐层到word部分的权值
		//注意由于word部分是1-of-V编码
		//所以这里更新的循环式上有点不同,并且下面的更新都是每10次训练更新才进行一次L2正规化
		a=last_word;
		if (a!=-1) {
			if ((counter%10)==0)			
				for (b=0; b<layer1_size; b++) syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac - syn0[a+b*layer0_size].weight*beta2;
				else
					//我们可以把这里的权值看做一个矩阵
					//b表示行,a表示列,非L2正规化的完整更新式如下:
					/*for (b=0; b<layer1_size; b++) {
					for (k=0; k<vocab_size; k++) {
					syn0[k+b*layer0_size].weight+=alpha*neu1[b].er*neu0[k].ac;
					}
		}*/
		//但由于neu0[k]==1只有当k==a时,所以为了加快循环计算的速度,更新就变为如下了
		//下面的代码是类似的道理,其实这里的neu0[a].ac可以直接省略掉,因为本身值为1				
		for (b=0; b<layer1_size; b++) syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac;
		}
		
		//这部分更新隐层到s(t-1)的权值
		if ((counter%10)==0) {
			for (b=0; b<layer1_size; b++) for (a=layer0_size-layer1_size; a<layer0_size; a++) syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac - syn0[a+b*layer0_size].weight*beta2;
		}
		else {
			for (b=0; b<layer1_size; b++) for (a=layer0_size-layer1_size; a<layer0_size; a++) syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac;
		}
    }
    else		//BPTT
    {
		//BPTT部分权值就不在用syn0了,而是用bptt_syn0
		//神经元部分的存储用bptt_hidden
		
		//将隐层的神经元信息复制到bptt_hidden,这里对neu1做备份
		for (b=0; b<layer1_size; b++) bptt_hidden[b].ac=neu1[b].ac;
		for (b=0; b<layer1_size; b++) bptt_hidden[b].er=neu1[b].er;
		
		//bptt_block第一个条件是控制BPTT的,因为不能每训练一个word都用BPTT来深度更新参数
		//这样每进行一次训练计算量太大,过于耗时
		//word==0表示当前一个句子结束
		//或者当independent开关不为0时,下一个词是句子的结束,则进行BPTT
		if (((counter%bptt_block)==0) || (independent && (word==0))) {
			
			//step表示
			for (step=0; step<bptt+bptt_block-2; step++) {
				
				//计算隐层的误差向量,即eh(t)
				for (a=0; a<layer1_size; a++) neu1[a].er=neu1[a].er*neu1[a].ac*(1-neu1[a].ac);    //error derivation at layer 1
				
				//更新的是输入层中vocab_size那部分权值
				//bptt_history下标从0开始依次存放的是wt, wt-1, wt-2...
				//比如当step == 0时,相当于正在进行普通的BPTT,这时bptt_history[step]表示当前输入word的在vocab中的索引
				a=bptt_history[step];	
				if (a!=-1)
					for (b=0; b<layer1_size; b++) {
						//由于输入层word部分是1-of-V编码,所以neu0[a].ac==1 所以这里后面没有乘以它
						//在step == 0时,bptt_syn0相当于原来的syn0
						bptt_syn0[a+b*layer0_size].weight+=alpha*neu1[b].er;
					}
					
					//为从隐层往状态层传误差做准备
					for (a=layer0_size-layer1_size; a<layer0_size; a++) neu0[a].er=0;
					
					//从隐层传递误差到状态层
					matrixXvector(neu0, neu1, syn0, layer0_size, 0, layer1_size, layer0_size-layer1_size, layer0_size, 1);		//propagates errors 1->0
					
					//更新隐层到状态层的权值
					//之所以先计算误差的传递就是因为更新完权值会发生改变
					for (b=0; b<layer1_size; b++) for (a=layer0_size-layer1_size; a<layer0_size; a++) {
						//neu0[a].er += neu1[b].er * syn0[a+b*layer0_size].weight;
						bptt_syn0[a+b*layer0_size].weight+=alpha*neu1[b].er*neu0[a].ac;
					}
					
					//todo这里我理解是把论文中的s(t-1)复制到s(t),准备下一次循环
					for (a=0; a<layer1_size; a++) {		//propagate error from time T-n to T-n-1
						//后面为什么会加bptt_hidden呢
						neu1[a].er=neu0[a+layer0_size-layer1_size].er + bptt_hidden[(step+1)*layer1_size+a].er;
					}
					
					//历史中的s(t-2)复制到s(t-1)
					if (step<bptt+bptt_block-3)
						for (a=0; a<layer1_size; a++) {
							neu1[a].ac=bptt_hidden[(step+1)*layer1_size+a].ac;
							neu0[a+layer0_size-layer1_size].ac=bptt_hidden[(step+2)*layer1_size+a].ac;
						}
					
			}
			
			//清空历史信息中的er值
			for (a=0; a<(bptt+bptt_block)*layer1_size; a++) {
				bptt_hidden[a].er=0;
			}
			
			//这里恢复neu1,因为neu1反复在循环中使用
			for (b=0; b<layer1_size; b++) neu1[b].ac=bptt_hidden[b].ac;		//restore hidden layer after bptt
			
			
			//将BPTT后的权值改变作用到syn0上
			for (b=0; b<layer1_size; b++) {		//copy temporary syn0
				
				//bptt完毕,将bptt_syn0的权值复制到syn0中来,这是复制状态层部分
				if ((counter%10)==0) {
					for (a=layer0_size-layer1_size; a<layer0_size; a++) {
						syn0[a+b*layer0_size].weight+=bptt_syn0[a+b*layer0_size].weight - syn0[a+b*layer0_size].weight*beta2;
						bptt_syn0[a+b*layer0_size].weight=0;
					}
				}
				else {
					for (a=layer0_size-layer1_size; a<layer0_size; a++) {
						syn0[a+b*layer0_size].weight+=bptt_syn0[a+b*layer0_size].weight;
						bptt_syn0[a+b*layer0_size].weight=0;
					}
				}
				
				//bptt完毕,将bptt_syn0的权值复制到syn0中来,这是word部分
				if ((counter%10)==0) {
					for (step=0; step<bptt+bptt_block-2; step++) if (bptt_history[step]!=-1) {
						syn0[bptt_history[step]+b*layer0_size].weight+=bptt_syn0[bptt_history[step]+b*layer0_size].weight - syn0[bptt_history[step]+b*layer0_size].weight*beta2;
						bptt_syn0[bptt_history[step]+b*layer0_size].weight=0;
					}
				}
				else {
					//因为输入的word层是1-of-V编码,并不是全部的权值改变
					//这样写可以加快计算速度,避免不必要的循环检索
					for (step=0; step<bptt+bptt_block-2; step++) if (bptt_history[step]!=-1) {
						syn0[bptt_history[step]+b*layer0_size].weight+=bptt_syn0[bptt_history[step]+b*layer0_size].weight;
						bptt_syn0[bptt_history[step]+b*layer0_size].weight=0;
					}
				}
			}
		}
    }	
}


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。