概述
在“神经网络变得简单(第八部分):关注机制”一文中,我们研究了自关注机制,及其实现的变体。 实际当中,现代神经网络体系结构会采用多目击者关注。 这种机制意味着将并行启动多个具有不同权重的自关注线程。 这样的解决方案应该能更好地揭示序列中各个元素之间的联系。 我们来尝试实现类似的体系结构,并比较这两种方法的效果。
1. 多目击者关注
自关注算法采用三个已训练的权重矩阵(Wq,Wk 和 Wv)。 矩阵数据用于获取 3 个实体:Query, Key 和 Value。 前两个实体定义了序列元素之间的配对关系,最后一个实体定义了所分析元素的上下文。
情况并非总是一目了然,这并非什么秘密。 与之对比,似乎在大多数情况下,一种状况可从不同的观点来阐释。 如此,根据选择的观点,结论可能完全相反。 重要的是要在这种情况下考虑所有可能的变体,并且只有在仔细分析后才能做出决策。 已经提议采用多目击者关注机制来解决这类问题。 每个“目击者”都有自己的见解,而决策则是由平衡投票制定。
多目击者关注体系结构意味着并行利用具有不同权重的多个自我关注线程,从而模仿针对某状况的多方分析。 若干自关注线程的操作结果被串联到一个张量之中。 通过将张量乘以 W0 矩阵来找到算法的最终结果,该矩阵的参数是在神经网络训练过程中选择的。 整个体系结构在变换器体系结构的编码器和解码器中取代了的“自关注”模块。
2. 一点数学
下面的公式可以提供对自关注算法的数学描述:
,
其中 'Q' 是 Query 张量,'K' 是 Key 张量,'V' 是 Values 张量,'d' 是一个 key 向量的维数。
反过来
和 ,
其中 X1 和 X2 是序列的元素; Wq 和 Wk 分别是 Queries 和 keys 的权重矩阵。 因此,我们得到以下内容:
通过矩阵的关联性,我们可以首先将权重矩阵 Wq 和 Wk 相乘。 如您所见,权重矩阵的乘积不依赖于输入序列,并且对于特定的自关注块的所有迭代都是相同的(当然,直到下一次更新矩阵参数时,一直为真)。 因此,为了减少运算,我们按照特定方式一次性计算中间矩阵,然后将其用于其他计算。
我们可以走得更远,仅训练一个矩阵即可替代两个矩阵。 然而,令人迷惑的的是,并非总是能够仅训练一个矩阵就能减少运算次数。 例如,对于较大维度的输入序列向量,可把矩阵 Wq 和 Wk 降维。 在这种情况下,如果输入向量 X1 和 X2 的长度为 100 个元素,则单个矩阵将包含 10000 个元素(100*100)。 如果矩阵 Wq 和 Wk 降维 10 倍,我们将得到两个矩阵,每个矩阵包含 1000 个元素(100*10)。 因此,您应该考虑到网络性能及其运行结果的品质,仔细选择解决方案。
3. 位置编码
还有,在操控时间序列时,请注意序列中元素之间的距离。 关注算法需针对序列元素之间的依赖性进行配对验证,且序列的所有元素均使用相同的矩阵。 于此同时,时间序列元素的相互影响强烈取决于它们之间的时间间隔。 因此,另一个急迫的问题是添加位置编码算法。
理想的位置编码算法应满足若干准则:
- 序列中的每个元素必须接收一个唯一的代码
- 任何两个连续元素之间的步长必须恒定
- 该模型应易于调整,并可泛用于任意长度的序列
- 该模型必须是确定性的
变换器体系结构的作者建议不要采用单独元素来为序列编码,但整个矢量的维数应等于输入序列元素维度。 在此,正弦用来描述矢量的偶数元素,而余弦用于奇数元素。 请注意,序列元素不是特定的数组元素,而是描述单个位置状态的向量。 在我们的例子中,它是描述一根烛条的向量。
,
其中 “pos” 是序列元素的位置,“i” 是某个元素在向量中的位置,“d” 是一个序列元素的向量维数。
该解决方案能够为序列的每个元素设置位置,并判断它们之间的距离。
直接在变换器体系结构中,位置编码在其范围之外。 执行该操作,需在向首个编码器输入数据之前,将位置编码张量加入到输入序列张量。 出现两个问题:
- 为什么用附加取代向量级联?
- 张量的增加会令原始数据失真多少?
串联将增加数据维数,并因此增加迭代次数。 而这将降低系统的整体性能。 这种解决方案的第二方面,是向量的添加不仅能够定位单个序列元素的向量,而且还能够定位向量的每个元素。 假设,这不仅可以分析序列元素之间的依赖关系,还可以分析其各个组成部分之间的依赖关系。
至于数据失真,神经网络对每个元素的含义一无所知,并依据附加了编码的数据进行训练,即它不会单独分析每个元素及其位置。 例如,如果我们在第二和第二十的位置看到相同的十字星,那么我们可能会优先选择最近的十字星。 对于含有位置编码的神经网络,这些信号将是完全不同的信号,并将根据训练过程中累积的数据进行处理。
4. 实现
我们来研究上述解决方案的实现。 在以前的自关注算法实现中,Queries 和 Keys 向量的维数与输入序列相似。 因此,我首先要重建算法,以便训练一个矩阵。
4.1. 消除密钥张量
实际的解决方案十分简单。 在 CNeuronAttentionOCL::feedForward 方法里,我已经为调用 Key 卷积层的类似方法进行了注释。 我还在 Score 计算内核调用中,替换了带有以前神经层的 Key 卷积层。 方法代码中的修改在下面高亮显示。
bool CNeuronAttentionOCL::feedForward(CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer)==POINTER_INVALID) return false; //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=1; OpenCL.SetArgumentBuffer(def_k_Normilize,def_k_norm_buffer,prevLayer.getOutputIndex()); OpenCL.SetArgument(def_k_Normilize,def_k_norm_dimension,prevLayer.Neurons()); if(!OpenCL.Execute(def_k_Normilize,1,global_work_offset,global_work_size)) { printf("Error of execution kernel Normalize: %d",GetLastError()); return false; } if(!prevLayer.Output.BufferRead()) return false; } //--- if(CheckPointer(Querys)==POINTER_INVALID || !Querys.FeedForward(prevLayer)) return false; //if(CheckPointer(Keys)==POINTER_INVALID || !Keys.FeedForward(prevLayer)) // return false; if(CheckPointer(Values)==POINTER_INVALID || !Values.FeedForward(prevLayer)) return false; //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_AttentionScore,def_k_as_querys,Querys.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionScore,def_k_as_keys,prevLayer.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionScore,def_k_as_score,Scores.GetIndex()); OpenCL.SetArgument(def_k_AttentionScore,def_k_as_dimension,iWindow); if(!OpenCL.Execute(def_k_AttentionScore,1,global_work_offset,global_work_size)) { printf("Error of execution kernel AttentionScore: %d",GetLastError()); return false; } if(!Scores.BufferRead()) return false; } //--- Further code has no changes
在反向传播方法 CNeuronAttentionOCL::calcInputGradients 里也实现了类似的修改。 请注意,由于很早以前误差梯度的第一部分就写入了先前的层缓冲区,因此梯度累积过程早就开始。 所有修改在以下代码中以高亮显示。
bool CNeuronAttentionOCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer)==POINTER_INVALID) return false; //--- if(!FF2.calcInputGradients(FF1)) return false; if(!FF1.calcInputGradients(AttentionOut)) return false; //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,AttentionOut.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,Gradient.GetIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,AttentionOut.getGradientIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,0.5); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } double temp[]; if(AttentionOut.getGradient(temp)<=0) return false; } //--- { uint global_work_offset[2]={0,0}; uint global_work_size[2]; global_work_size[0]=iUnits; global_work_size[1]=iWindow; OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_gradient,AttentionOut.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_keys,prevLayer.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_keys_g,prevLayer.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_querys,Querys.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_querys_g,Querys.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_values,Values.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_values_g,Values.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_scores,Scores.GetIndex()); if(!OpenCL.Execute(def_k_AttentionGradients,2,global_work_offset,global_work_size)) { printf("Error of execution kernel AttentionGradients: %d",GetLastError()); return false; } double temp[]; if(Querys.getGradient(temp)<=0) return false; } //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,AttentionOut.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,prevLayer.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,AttentionOut.getGradientIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,1.0); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } double temp[]; if(AttentionOut.getGradient(temp)<=0) return false; } //--- if(!Querys.calcInputGradients(prevLayer)) return false; //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,AttentionOut.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,prevLayer.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,AttentionOut.getGradientIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,1.0); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } double temp[]; if(AttentionOut.getGradient(temp)<=0) return false; } --- // if(!Keys.calcInputGradients(prevLayer)) // return false; --- // { // uint global_work_offset[1]={0}; // uint global_work_size[1]; // global_work_size[0]=iUnits; // OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,AttentionOut.getGradientIndex()); // OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,prevLayer.getGradientIndex()); // OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,AttentionOut.getGradientIndex()); // OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); // OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,1.0); // if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) // { // printf("Error of execution kernel MatrixSum: %d",GetLastError()); // return false; // } // double temp[]; // if(AttentionOut.getGradient(temp)<=0) // return false; // } //--- Further code has no changes
我还为 CNeuronAttentionOCL::updateInputWeights 方法中 Key 卷积层权重的更新进行了注释,以及该对象的声明。
附件中提供了所有方法和函数的完整代码。
4.2. 多目击者关注类
多目击者关注的构建是在单独的 CNeuronMHAttentionOCL 类中实现的,其基于 CNeuronAttentionOCL 父类。 在受保护模块里,依据关注目击者的数量,声明卷积层 Querys 和 Values 的附加实例。 在本示例中用到了四个目击者。 另外,为每个关注目击者添加 Scores 缓冲区和完全连接 AttentionOut 层。 再有,我们需要一个完全连接层来连接关注目击者的数据- AttentionConcatenate - 和卷积层 Weights0 ,其能够模拟加权投票,并降低结果张量的维数。
class CNeuronMHAttentionOCL : public CNeuronAttentionOCL { protected: CNeuronConvOCL *Querys2; ///< Convolution layer for Querys Head 2 CNeuronConvOCL *Querys3; ///< Convolution layer for Querys Head 3 CNeuronConvOCL *Querys4; ///< Convolution layer for Querys Head 4 CNeuronConvOCL *Values2; ///< Convolution layer for Values Head 2 CNeuronConvOCL *Values3; ///< Convolution layer for Values Head 3 CNeuronConvOCL *Values4; ///< Convolution layer for Values Head 4 CBufferDouble *Scores2; ///< Buffer for Scores matrix Head 2 CBufferDouble *Scores3; ///< Buffer for Scores matrix Head 3 CBufferDouble *Scores4; ///< Buffer for Scores matrix Head 4 CNeuronBaseOCL *AttentionOut2; ///< Layer of Self-Attention Out CNeuronBaseOCL *AttentionOut3; ///< Layer of Self-Attention Out CNeuronBaseOCL *AttentionOut4; ///< Layer of Self-Attention Out CNeuronBaseOCL *AttentionConcatenate;///< Layer of Concatenate Self-Attention Out CNeuronConvOCL *Weights0; ///< Convolution layer for Weights0 //--- virtual bool feedForward(CNeuronBaseOCL *prevLayer); ///< Feed Forward method.@param prevLayer Pointer to previous layer. virtual bool updateInputWeights(CNeuronBaseOCL *prevLayer); ///< Method for updating weights.@param prevLayer Pointer to previous layer. /// Method to transfer gradients inside Head Self-Attention virtual bool calcHeadGradient(CNeuronConvOCL *query, CNeuronConvOCL *value, CBufferDouble *score, CNeuronBaseOCL *attention, CNeuronBaseOCL *prevLayer); public: /** Constructor */CNeuronMHAttentionOCL(void){}; /** Destructor */~CNeuronMHAttentionOCL(void); virtual bool Init(uint numOutputs,uint myIndex,COpenCLMy *open_cl, uint window, uint units_count, ENUM_OPTIMIZATION optimization_type); ///< Method of initialization class.@param[in] numOutputs Number of connections to next layer.@param[in] myIndex Index of neuron in layer.@param[in] open_cl Pointer to #COpenCLMy object.@param[in] window Size of in/out window and step.@param[in] units_countNumber of neurons.@param[in] optimization_type Optimization type (#ENUM_OPTIMIZATION)@return Boolean result of operations. virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer); ///< Method to transfer gradients to previous layer @param[in] prevLayer Pointer to previous layer. //--- virtual int Type(void) const { return defNeuronMHAttentionOCL; }///< Identificator of class.@return Type of class //--- methods for working with files virtual bool Save(int const file_handle); ///< Save method @param[in] file_handle handle of file @return logical result of operation virtual bool Load(int const file_handle); ///< Load method @param[in] file_handle handle of file @return logical result of operation };
这套类方法重写了父类的虚方法。 可能,它已经被称为标准。 唯一的例外是 calcHeadGradient 方法,它描述了误差梯度传播迭代,针对每个目击者重复进行。
将类构造函数留空,然后将新对象的初始化移至 Init 初始化方法。 在类的析构函数中,删除该类在 “protected” 模块中声明并已创建的对象实例。
CNeuronMHAttentionOCL::~CNeuronMHAttentionOCL(void) { if(CheckPointer(Querys2)!=POINTER_INVALID) delete Querys2; if(CheckPointer(Querys3)!=POINTER_INVALID) delete Querys3; if(CheckPointer(Querys4)!=POINTER_INVALID) delete Querys4; if(CheckPointer(Values2)!=POINTER_INVALID) delete Values2; if(CheckPointer(Values3)!=POINTER_INVALID) delete Values3; if(CheckPointer(Values4)!=POINTER_INVALID) delete Values4; if(CheckPointer(Scores2)!=POINTER_INVALID) delete Scores2; if(CheckPointer(Scores3)!=POINTER_INVALID) delete Scores3; if(CheckPointer(Scores4)!=POINTER_INVALID) delete Scores4; if(CheckPointer(Weights0)!=POINTER_INVALID) delete Weights0; if(CheckPointer(AttentionOut2)!=POINTER_INVALID) delete AttentionOut2; if(CheckPointer(AttentionOut3)!=POINTER_INVALID) delete AttentionOut3; if(CheckPointer(AttentionOut4)!=POINTER_INVALID) delete AttentionOut4; if(CheckPointer(AttentionConcatenate)!=POINTER_INVALID) delete AttentionConcatenate; }
Init 方法的构建由父类方法类推而来。 在方法的开头,调用父类的相关方法。
bool CNeuronMHAttentionOCL::Init(uint numOutputs,uint myIndex,COpenCLMy *open_cl,uint window,uint units_count,ENUM_OPTIMIZATION optimization_type) { if(!CNeuronAttentionOCL::Init(numOutputs,myIndex,open_cl,window,units_count,optimization_type)) return false;
然后,初始化 Querys 卷积层的实例。 请注意,我们从第二个目击者开始初始化对象,因为第一个目击者的所有对象实例都是在父类中被初始化。
if(CheckPointer(Querys2)==POINTER_INVALID) { Querys2=new CNeuronConvOCL(); if(CheckPointer(Querys2)==POINTER_INVALID) return false; if(!Querys2.Init(0,6,open_cl,window,window,window,units_count,optimization_type)) return false; Querys2.SetActivationFunction(None); } //--- if(CheckPointer(Querys3)==POINTER_INVALID) { Querys3=new CNeuronConvOCL(); if(CheckPointer(Querys3)==POINTER_INVALID) return false; if(!Querys3.Init(0,7,open_cl,window,window,window,units_count,optimization_type)) return false; Querys3.SetActivationFunction(None); } //--- if(CheckPointer(Querys4)==POINTER_INVALID) { Querys4=new CNeuronConvOCL(); if(CheckPointer(Querys4)==POINTER_INVALID) return false; if(!Querys4.Init(0,8,open_cl,window,window,window,units_count,optimization_type)) return false; Querys4.SetActivationFunction(None); }
类似地,为 AttentionOut 初始化类实例的 Values,Scores。
if(CheckPointer(Values2)==POINTER_INVALID) { Values2=new CNeuronConvOCL(); if(CheckPointer(Values2)==POINTER_INVALID) return false; if(!Values2.Init(0,9,open_cl,window,window,window,units_count,optimization_type)) return false; Values2.SetActivationFunction(None); } //--- if(CheckPointer(Values3)==POINTER_INVALID) { Values3=new CNeuronConvOCL(); if(CheckPointer(Values3)==POINTER_INVALID) return false; if(!Values3.Init(0,10,open_cl,window,window,window,units_count,optimization_type)) return false; Values3.SetActivationFunction(None); } //--- if(CheckPointer(Values4)==POINTER_INVALID) { Values4=new CNeuronConvOCL(); if(CheckPointer(Values4)==POINTER_INVALID) return false; if(!Values4.Init(0,11,open_cl,window,window,window,units_count,optimization_type)) return false; Values4.SetActivationFunction(None); } //--- if(CheckPointer(Scores2)==POINTER_INVALID) { Scores2=new CBufferDouble(); if(CheckPointer(Scores2)==POINTER_INVALID) return false; } if(!Scores2.BufferInit(units_count*units_count,0.0)) return false; if(!Scores2.BufferCreate(OpenCL)) return false; //--- if(CheckPointer(Scores3)==POINTER_INVALID) { Scores3=new CBufferDouble(); if(CheckPointer(Scores3)==POINTER_INVALID) return false; } if(!Scores3.BufferInit(units_count*units_count,0.0)) return false; if(!Scores3.BufferCreate(OpenCL)) return false; //--- if(CheckPointer(Scores4)==POINTER_INVALID) { Scores4=new CBufferDouble(); if(CheckPointer(Scores4)==POINTER_INVALID) return false; } if(!Scores4.BufferInit(units_count*units_count,0.0)) return false; if(!Scores4.BufferCreate(OpenCL)) return false; //--- if(CheckPointer(AttentionOut2)==POINTER_INVALID) { AttentionOut2=new CNeuronBaseOCL(); if(CheckPointer(AttentionOut2)==POINTER_INVALID) return false; if(!AttentionOut2.Init(0,12,open_cl,window*units_count,optimization_type)) return false; AttentionOut2.SetActivationFunction(None); } //--- if(CheckPointer(AttentionOut3)==POINTER_INVALID) { AttentionOut3=new CNeuronBaseOCL(); if(CheckPointer(AttentionOut3)==POINTER_INVALID) return false; if(!AttentionOut3.Init(0,13,open_cl,window*units_count,optimization_type)) return false; AttentionOut3.SetActivationFunction(None); } //--- if(CheckPointer(AttentionOut4)==POINTER_INVALID) { AttentionOut4=new CNeuronBaseOCL(); if(CheckPointer(AttentionOut4)==POINTER_INVALID) return false; if(!AttentionOut4.Init(0,14,open_cl,window*units_count,optimization_type)) return false; AttentionOut4.SetActivationFunction(None); return true; }
附件中提供了所有方法和函数的完整代码。
4.3. 前馈
前馈算法主要采用较早创建的 OpenCL 程序构建。 唯一的例外是创建了一个内核,将来自每个关注目击者的 4 个张量的数据串联到单一张量中。 内核在参数中接收以下内容:指向数据缓冲区的指针,和每个缓冲区窗口大小,以及指向结果张量的指针。 已添加输入数据缓冲区的详细窗口大小,以便能够将不同大小的张量与不同的窗口大小串联在一起。
__kernel void ConcatenateBuffers(__global double *input1, int window1, __global double *input2, int window2, __global double *input3, int window3, __global double *input4, int window4, __global double *output)
在内核主体中,从输入数组逐个复制数据元素到输出数组。 该算法非常简单,故我认为所附代码应很容易理解。
在 CNeuronMHAttentionOCL 类中,前馈是在 feedForward 方法里实现的。 在该方法的开头,检查接收到的前一层链接的有效性,并对输入数据进行规范化。
bool CNeuronMHAttentionOCL::feedForward(CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer)==POINTER_INVALID) return false; //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=1; OpenCL.SetArgumentBuffer(def_k_Normilize,def_k_norm_buffer,prevLayer.getOutputIndex()); OpenCL.SetArgument(def_k_Normilize,def_k_norm_dimension,prevLayer.Neurons()); if(!OpenCL.Execute(def_k_Normilize,1,global_work_offset,global_work_size)) { printf("Error of execution kernel Normalize: %d",GetLastError()); return false; } if(!prevLayer.Output.BufferRead()) return false; }
然后调用相应的卷积层方法,并为所有关注目击者计算 Querys 和 Values 张量的值。
if(CheckPointer(Querys)==POINTER_INVALID || !Querys.FeedForward(prevLayer)) return false; if(CheckPointer(Querys2)==POINTER_INVALID || !Querys2.FeedForward(prevLayer)) return false; if(CheckPointer(Querys3)==POINTER_INVALID || !Querys3.FeedForward(prevLayer)) return false; if(CheckPointer(Querys4)==POINTER_INVALID || !Querys4.FeedForward(prevLayer)) return false; if(CheckPointer(Values)==POINTER_INVALID || !Values.FeedForward(prevLayer)) return false; if(CheckPointer(Values2)==POINTER_INVALID || !Values2.FeedForward(prevLayer)) return false; if(CheckPointer(Values3)==POINTER_INVALID || !Values3.FeedForward(prevLayer)) return false; if(CheckPointer(Values4)==POINTER_INVALID || !Values4.FeedForward(prevLayer)) return false;
接下来,重新计算每个目击者的关注。 该算法与第八篇文章中描述的父类相似。 下面是一个关注目击者的代码。 其他目击者的代码是相似的,只是指向相应关注目击者对象的指针是相似的。
//--- Scores Head 1 { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_AttentionScore,def_k_as_querys,Querys.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionScore,def_k_as_keys,prevLayer.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionScore,def_k_as_score,Scores.GetIndex()); OpenCL.SetArgument(def_k_AttentionScore,def_k_as_dimension,iWindow); if(!OpenCL.Execute(def_k_AttentionScore,1,global_work_offset,global_work_size)) { printf("Error of execution kernel AttentionScore: %d",GetLastError()); return false; } if(!Scores.BufferRead()) return false; } //--- { uint global_work_offset[2]={0,0}; uint global_work_size[2]; global_work_size[0]=iUnits; global_work_size[1]=iWindow; OpenCL.SetArgumentBuffer(def_k_AttentionOut,def_k_aout_scores,Scores.GetIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionOut,def_k_aout_inputs,prevLayer.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionOut,def_k_aout_values,Values.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionOut,def_k_aout_out,AttentionOut.getOutputIndex()); if(!OpenCL.Execute(def_k_AttentionOut,2,global_work_offset,global_work_size)) { printf("Error of execution kernel Attention Out: %d",GetLastError()); return false; } double temp[]; if(!AttentionOut.getOutputVal(temp)) return false; }
每个关注目击者计算完毕之后,利用先前编写的内核将结果串联到单一张量之中。
{ uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_ConcatenateMatrix,def_k_conc_input1,AttentionOut.getOutputIndex()); OpenCL.SetArgument(def_k_ConcatenateMatrix,def_k_conc_window1,iWindow); OpenCL.SetArgumentBuffer(def_k_ConcatenateMatrix,def_k_conc_input2,AttentionOut2.getOutputIndex()); OpenCL.SetArgument(def_k_ConcatenateMatrix,def_k_conc_window2,iWindow); OpenCL.SetArgumentBuffer(def_k_ConcatenateMatrix,def_k_conc_input3,AttentionOut3.getOutputIndex()); OpenCL.SetArgument(def_k_ConcatenateMatrix,def_k_conc_window3,iWindow); OpenCL.SetArgumentBuffer(def_k_ConcatenateMatrix,def_k_conc_input4,AttentionOut4.getOutputIndex()); OpenCL.SetArgument(def_k_ConcatenateMatrix,def_k_conc_window4,iWindow); OpenCL.SetArgumentBuffer(def_k_ConcatenateMatrix,def_k_conc_out,AttentionConcatenate.getOutputIndex()); if(!OpenCL.Execute(def_k_ConcatenateMatrix,1,global_work_offset,global_work_size)) { printf("Error of execution kernel Concatenate Matrix: %d",GetLastError()); return false; } double temp[]; if(!AttentionConcatenate.getOutputVal(temp)) return false; }
将张量串联结果传递到 Weights0 卷积层,从而降低多目击者关注操作结果的大小。
if(CheckPointer(Weights0)==POINTER_INVALID || !Weights0.FeedForward(AttentionConcatenate)) return false;
然后,将得到的结果与前一层的数据求平均值,并将结果进行常规化。
{ uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,Weights0.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,prevLayer.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,Weights0.getOutputIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,0.5); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } if(!Output.BufferRead()) return false; } //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=1; OpenCL.SetArgumentBuffer(def_k_Normilize,def_k_norm_buffer,Weights0.getOutputIndex()); OpenCL.SetArgument(def_k_Normilize,def_k_norm_dimension,Weights0.Neurons()); if(!OpenCL.Execute(def_k_Normilize,1,global_work_offset,global_work_size)) { printf("Error of execution kernel Normalize: %d",GetLastError()); return false; } double temp[]; if(!Weights0.getOutputVal(temp)) return false; }
然后,与父类类似,将结果传递给 FeedForward 块。
if(!FF1.FeedForward(Weights0)) return false; if(!FF2.FeedForward(FF1)) return false; //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,Weights0.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,FF2.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,Output.GetIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,0.5); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } if(!Output.BufferRead()) return false; } //--- return true; }
附件中提供了所有方法和函数的完整代码。
4.4. 反馈
前馈过程包含两个子过程:将误差梯度向下传递一个级别,并更新权重矩阵。 权重则利用先前创建的 OpenCL 内核进行更新,而对于误差反馈传播过程,我们需要进行一些修改。
首先,我们需要由关注目击者来传播误差梯度。 为了执行此函数,创建 DeconcatenateBuffers 内核。 向内核输入指向梯度传播缓冲区的指针,每个缓冲区的窗口大小,以及从上次迭代接收的指向梯度的缓冲区指针。
__kernel void DeconcatenateBuffers(__global double *output1, int window1, __global double *output2, int window2, __global double *output3, int window3, __global double *output4, int window4, __global double *inputs)
在内核伊始,定义序列元素的序号,以及原始张量和第一个关注目击者张量的第一个位置偏移。
{ int n=get_global_id(0); int shift=n*(window1+window2+window3+window4); int shift_out=n*window1;
接着,在一个循环中,移动第一个关注目击者的误差梯度向量。
for(int i=0;i<window1;i++) output1[shift_out+i]=inputs[shift+i];
一旦循环结束,调整在原始张量的指针位置,并确判第二个关注目击者的缓冲区中的第一个位置偏移。 然后为第二个关注目击者运行数据复制循环。 每个关注目击者均要重复该操作。
//--- Head 2 shift+=window1; shift_out=n*window2; for(int i=0;i<window2;i++) output2[shift_out+i]=inputs[shift+i]; //--- Head 3 shift+=window2; shift_out=n*window3; for(int i=0;i<window3;i++) output3[shift_out+i]=inputs[shift+i]; //--- Head 4 shift+=window3; shift_out=n*window4; for(int i=0;i<window4;i++) output4[shift_out+i]=inputs[shift+i]; }
稍后,为每个关注目击者计算误差梯度之后,有必要将梯度组合到神经网络前一层的单一数据缓冲区之中。 技术上,我们可通过添加所有关注目击者的成对梯度来使用 SumMatrix 内核。 但此解决方案在性能方面并非最佳。 因此,我们创建另一个内核 - Sum5Matrix。 在内核参数中,传递指向数据缓冲区(5 个输入和 1 个输出)的指针,数据窗口的大小,和乘数(累加校正因子)。 也许,我需要解释为什么会有 5 个接收缓冲区和 4 个关注目击者。 第五个缓冲器用来传递误差梯度,以便让梯度衰落的风险降至最小。
__kernel void Sum5Matrix(__global double *matrix1, ///<[in] First matrix __global double *matrix2, ///<[in] Second matrix __global double *matrix3, ///<[in] Third matrix __global double *matrix4, ///<[in] Fourth matrix __global double *matrix5, ///<[in] Fifth matrix __global double *matrix_out, ///<[out] Output matrix int dimension, ///< Dimension of matrix double multiplyer ///< Multiplyer for output )
在内核主体中,定义序列中已处理矢量的第一个元素的移位,并开始循环累加梯度。 误差梯度之和乘以 0.2,能够在神经网络前一层基础上将传输误差值进行平均。 反过来,在参数中有意实现乘法器,如此在调整算法时能够选择其数值。
{ const int i=get_global_id(0)*dimension; for(int k=0;k<dimension;k++) matrix_out[i+k]=(matrix1[i+k]+matrix2[i+k]+matrix3[i+k]+matrix4[i+k]+matrix5[i+k])*multiplyer; }
在 CNeuronMHAttentionOCL 类中,每个子流程都接受其方法。 误差梯度传播是由 calcInputGradients 方法执行的。 该方法在参数中接收指向先前的神经网络层对象的指针。 在方法伊始检查指针的有效性。
bool CNeuronMHAttentionOCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer)==POINTER_INVALID) return false;
然后,调用卷积层 FF1 和 FF2 的相应方法,计算经过前馈块的误差梯度。
if(!FF2.calcInputGradients(FF1)) return false; if(!FF1.calcInputGradients(Weights0)) return false;
传递附近的 FeedForward 块误差梯度。 将平均误差值保存在 Weights0 层的梯度缓冲区之中。
{ uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,Weights0.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,Gradient.GetIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,Weights0.getGradientIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,0.5); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } double temp[]; if(Weights0.getGradient(temp)<=0) return false; }
现在,轮到关注目击者误差传播的时候了。 我们需要把梯度张量的大小增加到已串联关注缓冲区的大小。 为此,调用卷积层相应方法,传递穿过 Weights0 卷积层的误差梯度。
if(!Weights0.calcInputGradients(AttentionConcatenate)) return false;
接收到足够大的误差梯度张量之后,我们可通过关注目击者的缓冲区分配误差。 利用上面创建的内核串联拆解。
{ uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_DeconcatenateMatrix,def_k_dconc_output1,AttentionOut.getGradientIndex()); OpenCL.SetArgument(def_k_DeconcatenateMatrix,def_k_dconc_window1,iWindow); OpenCL.SetArgumentBuffer(def_k_DeconcatenateMatrix,def_k_dconc_output2,AttentionOut2.getGradientIndex()); OpenCL.SetArgument(def_k_DeconcatenateMatrix,def_k_dconc_window2,iWindow); OpenCL.SetArgumentBuffer(def_k_DeconcatenateMatrix,def_k_dconc_output3,AttentionOut3.getGradientIndex()); OpenCL.SetArgument(def_k_DeconcatenateMatrix,def_k_dconc_window3,iWindow); OpenCL.SetArgumentBuffer(def_k_DeconcatenateMatrix,def_k_dconc_output4,AttentionOut4.getGradientIndex()); OpenCL.SetArgument(def_k_DeconcatenateMatrix,def_k_dconc_window4,iWindow); OpenCL.SetArgumentBuffer(def_k_DeconcatenateMatrix,def_k_dconc_inputs,AttentionConcatenate.getGradientIndex()); if(!OpenCL.Execute(def_k_DeconcatenateMatrix,1,global_work_offset,global_work_size)) { printf("Error of execution kernel Deconcatenate Matrix: %d",GetLastError()); return false; } double temp[]; if(AttentionConcatenate.getGradient(temp)<=0) return false; }
在关注目击者内部的误差梯度计算是在单独的方法 calcHeadGradient 里实现的。 在此,我们针对每个关注线程调用该方法。
if(!calcHeadGradient(Querys,Values,Scores,AttentionOut,prevLayer)) return false; if(!calcHeadGradient(Querys2,Values2,Scores2,AttentionOut2,prevLayer)) return false; if(!calcHeadGradient(Querys3,Values3,Scores3,AttentionOut3,prevLayer)) return false; if(!calcHeadGradient(Querys4,Values4,Scores4,AttentionOut4,prevLayer)) return false;
在该方法的末尾,累加来自所有关注目击者的误差梯度,并将结果传递到神经网络的前一层。
{ uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_Matrix5Sum,def_k_sum5_matrix1,AttentionOut.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_Matrix5Sum,def_k_sum5_matrix2,AttentionOut2.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_Matrix5Sum,def_k_sum5_matrix3,AttentionOut3.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_Matrix5Sum,def_k_sum5_matrix4,AttentionOut4.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_Matrix5Sum,def_k_sum5_matrix5,Weights0.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_Matrix5Sum,def_k_sum5_matrix_out,prevLayer.getGradientIndex()); OpenCL.SetArgument(def_k_Matrix5Sum,def_k_sum5_dimension,iWindow); OpenCL.SetArgument(def_k_Matrix5Sum,def_k_sum5_multiplyer,0.2); if(!OpenCL.Execute(def_k_Matrix5Sum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel Matrix5Sum: %d",GetLastError()); return false; } double temp[]; if(prevLayer.getGradient(temp)<=0) return false; } //--- return true; }
我们来看一下 calcHeadGradient 方法。 该方法在参数中接收指向内部神经层里与所研究关注目击者相关的 'query','value','score','attention' 的指针,以及指向之前神经层的指针。
bool CNeuronMHAttentionOCL::calcHeadGradient(CNeuronConvOCL *query,CNeuronConvOCL *value,CBufferDouble *score,CNeuronBaseOCL *attention,CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer)==POINTER_INVALID) return false;
方法主体从检查指向之前神经层的指针有效性开始。 为了在内部层上分配误差梯度,调用 AttentionInsideGradients 内核,该内核已在第八篇文章中进行了讨论。
{ uint global_work_offset[2]={0,0}; uint global_work_size[2]; global_work_size[0]=iUnits; global_work_size[1]=iWindow; OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_gradient,attention.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_keys,prevLayer.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_keys_g,prevLayer.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_querys,query.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_querys_g,query.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_values,value.getOutputIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_values_g,value.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_AttentionGradients,def_k_ag_scores,score.GetIndex()); if(!OpenCL.Execute(def_k_AttentionGradients,2,global_work_offset,global_work_size)) { printf("Error of execution kernel AttentionGradients: %d",GetLastError()); return false; } double temp[]; if(query.getGradient(temp)<=0) return false; }
此示例展示训练一个矩阵,没有划分 'query' 和 'key'。 因此,指定之前的层缓冲区替换 key 层缓冲区。 为了不覆盖在上一层上获得的误差梯度,在计算其他内层时,将数据传输给当前关注目击者的 AttentionOut 张量。 我并未在缓冲区之间复制数据时提供单独的张量。 利用内核 SumMatrix 执行两个矩阵相加操作。 由于我们只有一个矩阵,在两个张量的指针中指示上一层。 为避免数值重复,用到一个 0.5 的乘数。
{ uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,prevLayer.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,prevLayer.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,attention.getGradientIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,0.5); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } double temp[]; if(attention.getGradient(temp)<=0) return false; }
接下来,调用 “query” 层的相应方法来计算穿过 query 层的误差梯度。 将结果与前一次迭代获得的梯度相加。 在此步骤中乘数等于 1。 增加的梯度将在下一步进行平均。
if(!query.calcInputGradients(prevLayer)) return false; //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,attention.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,prevLayer.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,attention.getGradientIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,1.0); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } double temp[]; if(attention.getGradient(temp)<=0) return false; }
再一次,在该方法的末尾,计算穿过 “value” 层的梯度,并与先前获得的梯度求和。 覆盖关注目击者的梯度总体上可用 0.33 的乘数进行平均。
if(!value.calcInputGradients(prevLayer)) return false; //--- { uint global_work_offset[1]={0}; uint global_work_size[1]; global_work_size[0]=iUnits; OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix1,attention.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix2,prevLayer.getGradientIndex()); OpenCL.SetArgumentBuffer(def_k_MatrixSum,def_k_sum_matrix_out,attention.getGradientIndex()); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_dimension,iWindow+1); OpenCL.SetArgument(def_k_MatrixSum,def_k_sum_multiplyer,0.33); if(!OpenCL.Execute(def_k_MatrixSum,1,global_work_offset,global_work_size)) { printf("Error of execution kernel MatrixSum: %d",GetLastError()); return false; } double temp[]; if(prevLayer.getGradient(temp)<=0) return false; } //--- return true; }
重新计算误差梯度后,更新所有内层的权重。 在 updateInputWeights 方法里,编写顺序调用所有内部神经层相关方法。
bool CNeuronMHAttentionOCL::updateInputWeights(CNeuronBaseOCL *prevLayer) { if(!Querys.UpdateInputWeights(prevLayer) || !Querys2.UpdateInputWeights(prevLayer) || !Querys3.UpdateInputWeights(prevLayer) || !Querys4.UpdateInputWeights(prevLayer)) return false; //--- if(!Values.UpdateInputWeights(prevLayer) || !Values2.UpdateInputWeights(prevLayer) || !Values3.UpdateInputWeights(prevLayer) || !Values4.UpdateInputWeights(prevLayer)) return false; if(!Weights0.UpdateInputWeights(AttentionConcatenate)) return false; if(!FF1.UpdateInputWeights(Weights0))