在“神经网络变得简单(第八部分):关注机制”一文中,赫兹量化研究了自关注机制,及其实现的变体。 实际当中,现代神经网络体系结构会采用多目击者关注。 这种机制意味着将并行启动多个具有不同权重的自关注线程。 这样的解决方案应该能更好地揭示序列中各个元素之间的联系。 我们来尝试实现类似的体系结构,并比较这两种方法的效果。
8 o1 {' ]0 d- t1. 多目击者关注自关注算法采用三个已训练的权重矩阵(Wq,Wk 和 Wv)。 矩阵数据用于获取 3 个实体:Query, Key 和 Value。 前两个实体定义了序列元素之间的配对关系,最后一个实体定义了所分析元素的上下文。
8 d" r$ |2 G3 ~8 J) j, n1 g$ |$ [; P5 N# C9 J' o$ N: c4 C
编辑添加图片注释,不超过 140 字(可选)
- v6 j- ~) n5 M, Y. H }# V* L情况并非总是一目了然,这并非什么秘密。 与之对比,似乎在大多数情况下,一种状况可从不同的观点来阐释。 如此,根据选择的观点,结论可能完全相反。 重要的是要在这种情况下考虑所有可能的变体,并且只有在仔细分析后才能做出决策。 已经提议采用多目击者关注机制来解决这类问题。 每个“目击者”都有自己的见解,而决策则是由平衡投票制定。
& p! O% K! {( V" v6 f, F4 D+ n2 P* ^0 Z5 h# W7 y( U3 I! c
编辑切换为居中添加图片注释,不超过 140 字(可选)
- U& w% [/ Z; B, k# ? K6 W5 y多目击者关注体系结构意味着并行利用具有不同权重的多个自我关注线程,从而模仿针对某状况的多方分析。 若干自关注线程的操作结果被串联到一个张量之中。 通过将张量乘以 W0 矩阵来找到算法的最终结果,该矩阵的参数是在神经网络训练过程中选择的。 整个体系结构在变换器体系结构的编码器和解码器中取代了的“自关注”模块。5 R6 Q' }0 o1 G
2. 一点数学下面的公式可以提供对自关注算法的数学描述:/ L/ |% p- U1 g2 g; C3 g
$ v4 w3 E# k$ ? G
编辑添加图片注释,不超过 140 字(可选),其中 'Q' 是 Query 张量,'K' 是 Key 张量,'V' 是 Values 张量,'d' 是一个 key 向量的维数。反过来 1 v' s4 G. Y: G# Z4 r, q
( P/ I! m2 H4 a( ~2 O6 X
添加图片注释,不超过 140 字(可选) 和 6 @1 |+ T" U2 u& t9 |4 z% y& N
编辑添加图片注释,不超过 140 字(可选),其中 X1 和 X2 是序列的元素; Wq 和 Wk 分别是 Queries 和 keys 的权重矩阵。 因此,我们得到以下内容:. H- X5 F f( k7 |+ O
( X. }- D' H- s- ]! Z' W
编辑添加图片注释,不超过 140 字(可选)通过矩阵的关联性,赫兹量化可以首先将权重矩阵 Wq 和 Wk 相乘。 如您所见,权重矩阵的乘积不依赖于输入序列,并且对于特定的自关注块的所有迭代都是相同的(当然,直到下一次更新矩阵参数时,一直为真)。 因此,为了减少运算,我们按照特定方式一次性计算中间矩阵,然后将其用于其他计算。我们可以走得更远,仅训练一个矩阵即可替代两个矩阵。 然而,令人迷惑的的是,并非总是能够仅训练一个矩阵就能减少运算次数。 例如,对于较大维度的输入序列向量,可把矩阵 Wq 和 Wk 降维。 在这种情况下,如果输入向量 X1 和 X2 的长度为 100 个元素,则单个矩阵将包含 10000 个元素(100*100)。 如果矩阵 Wq 和 Wk 降维 10 倍,赫兹量化将得到两个矩阵,每个矩阵包含 1000 个元素(100*10)。 因此,您应该考虑到网络性能及其运行结果的品质,仔细选择解决方案。
8 T6 J V& O# |! u7 H: ~3. 位置编码还有,在操控时间序列时,请注意序列中元素之间的距离。 关注算法需针对序列元素之间的依赖性进行配对验证,且序列的所有元素均使用相同的矩阵。 于此同时,时间序列元素的相互影响强烈取决于它们之间的时间间隔。 因此,另一个急迫的问题是添加位置编码算法。理想的位置编码算法应满足若干准则:. G2 }: [: }' Z+ w# V
5 p4 n3 t' T$ |. m序列中的每个元素必须接收一个唯一的代码
D @0 D/ ~2 z/ t$ X任何两个连续元素之间的步长必须恒定& y4 Z/ n/ W: c) P3 V1 }5 j
该模型应易于调整,并可泛用于任意长度的序列9 R/ y+ r1 s( v2 f( U
该模型必须是确定性的
' A, B0 k0 ~% p$ p+ A
- @! F' n% q8 K) \# m变换器体系结构的作者建议不要采用单独元素来为序列编码,但整个矢量的维数应等于输入序列元素维度。 在此,正弦用来描述矢量的偶数元素,而余弦用于奇数元素。 请注意,序列元素不是特定的数组元素,而是描述单个位置状态的向量。 在赫兹量化的例子中,它是描述一根烛条的向量。
* ?0 {! J& k$ t @6 ~7 X1 P" T" z7 e& x9 L$ `2 x0 ]# K
编辑添加图片注释,不超过 140 字(可选),其中 “pos” 是序列元素的位置,“i” 是某个元素在向量中的位置,“d” 是一个序列元素的向量维数。该解决方案能够为序列的每个元素设置位置,并判断它们之间的距离。直接在变换器体系结构中,位置编码在其范围之外。 执行该操作,需在向首个编码器输入数据之前,将位置编码张量加入到输入序列张量。 出现两个问题:1 Q) X0 q/ L/ A+ w8 E
3 U5 n& `( t0 I" c) p4 u为什么用附加取代向量级联?
d3 S1 @# r+ B& S张量的增加会令原始数据失真多少?
& h# {- Q% Z }) i- z$ p+ u1 ?, C6 V! [. J8 t" S- O0 I& k
串联将增加数据维数,并因此增加迭代次数。 而这将降低系统的整体性能。 这种解决方案的第二方面,是向量的添加不仅能够定位单个序列元素的向量,而且还能够定位向量的每个元素。 假设,这不仅可以分析序列元素之间的依赖关系,还可以分析其各个组成部分之间的依赖关系。至于数据失真,神经网络对每个元素的含义一无所知,并依据附加了编码的数据进行训练,即它不会单独分析每个元素及其位置。 例如,如果赫兹量化在第二和第二十的位置看到相同的十字星,那么我们可能会优先选择最近的十字星。 对于含有位置编码的神经网络,这些信号将是完全不同的信号,并将根据训练过程中累积的数据进行处理。 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。 |