在上一篇文章中,我们开始研究旨在提升神经网络收敛性的方法,并体验了其中一种减少特征协适应的舍弃(Dropout)方法。 我们来继续这个话题,并掌握常规化方法。
1. 常规化的理论前提
在神经网络应用实践中运用了多种数据常规化方法。 然而,它们的作用均是为了令训练样本数据和神经网络隐藏层的输出保持在一定范围内,并具有某些样本统计特征,如方差和中位数。 这一点很重要,因为网络神经元在训练过程中利用线性变换将样本朝逆梯度偏移。
参考一个含有两个隐藏层的全连接感知器。 在前馈验算过程中,每一层都会生成一个特定的数据集,作为下一层的训练样本。 输出层的结果与参考数据进行比较。 然后,在反馈验算过程中,误差梯度自输出层穿过隐藏层朝向初始数据传播。 每个神经元接收到误差梯度后,我们更新权重系数,为最后一次前馈验算的训练样本调整神经网络。 此处会产生一个冲突:第二个隐藏层(下图中的 H2)会基于第一个隐藏层(图中的 H1)输出的数据样本进行调整,而通过改变第一个隐藏层的参数,我们已更改了数据数组。 换言之,我们调整第二个隐藏层,其数据样本不再存在。 类似的状况也发生在输出层,因第二个隐藏层输出业已变化,故它也会被调整。 如果我们参考第一和第二隐藏层之间的失真,误差尺度会更大。 神经网络越深,影响越强。 这种现象被称为内部协变量偏移。
实现
1 为我们的模型创建新的类
我们已经研究过理论方面,现在我们在函数库中实现它。 我们创建一个新类 CNeuronBatchNormOCL 来实现该算法。
class CNeuronBatchNormOCL : public CNeuronBaseOCL { protected: CNeuronBaseOCL *PrevLayer; ///< Pointer to the object of the previous layer uint iBatchSize; ///< Batch size CBufferDouble *BatchOptions; ///< Container of method parameters ///\ingroup neuron_base_ff virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); ///< \brief Feed Forward method of calling kernel ::BatchFeedForward().@param NeuronOCL Pointer to previous layer. ///\ingroup neuron_base_opt virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); ///< Method for updating weights.\details Calling one of kernels ::UpdateBatchOptionsMomentum() or ::UpdateBatchOptionsAdam() in depends on optimization type (#ENUM_OPTIMIZATION).@param NeuronOCL Pointer to previous layer. public: /** Constructor */CNeuronBatchNormOCL(void); /** Destructor */~CNeuronBatchNormOCL(void); //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons, uint batchSize, 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] numNeurons Number of neurons in layer @param optimization_type Optimization type (#ENUM_OPTIMIZATION)@return Boolen result of operations. //--- virtual int getOutputIndex(void) { return (iBatchSize>1 ? Output.GetIndex() : PrevLayer.getOutputIndex()); } ///< Get index of output buffer @return Index virtual int getGradientIndex(void) { return (iBatchSize>1 ? Gradient.GetIndex() : PrevLayer.getGradientIndex()); } ///< Get index of gradient buffer @return Index //--- virtual int getOutputVal(double &values[]) { return (iBatchSize>1 ? Output.GetData(values) : PrevLayer.getOutputVal(values)); } ///< Get values of output buffer @param[out] values Array of data @return number of items virtual int getOutputVal(CArrayDouble *values) { return (iBatchSize>1 ? Output.GetData(values) : PrevLayer.getOutputVal(values)); } ///< Get values of output buffer @param[out] values Array of data @return number of items virtual int getGradient(double &values[]) { return (iBatchSize>1 ? Gradient.GetData(values) : PrevLayer.getGradient(values)); } ///< Get values of gradient buffer @param[out] values Array of data @return number of items virtual CBufferDouble *getOutput(void) { return (iBatchSize>1 ? Output : PrevLayer.getOutput()); } ///< Get pointer of output buffer @return Pointer to object virtual CBufferDouble *getGradient(void) { return (iBatchSize>1 ? Gradient : PrevLayer.getGradient()); } ///< Get pointer of gradient buffer @return Pointer to object //--- virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL); ///< Method to transfer gradient to previous layer by calling kernel ::CalcHiddenGradientBatch(). @param NeuronOCL Pointer to next layer. //--- 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 //--- virtual int Type(void) const { return defNeuronBatchNormOCL; }///< Identificator of class.@return Type of class };
新类将自 CNeuronBaseOCL 基类继承。 如同 CNeuronDropoutOCL 类一样外推,我们添加 PrevLayer 变量。 当指定的批次尺寸小于 “2” 时,将应用上一篇文章中展示的数据缓冲区替换方法,将其保存到 iBatchSize 变量之中。
批次常规化算法需要保存一些参数,这些参数对于已常规化层的每个神经元都是独立的。 出于避免为每个单独的参数生成过多的单独缓冲区,我们将为这些参数创建含有以下结构的单一 BatchOptions 缓冲区。
添加图片注释,不超过 140 字(可选)
从呈现的结构可以看出,参数缓冲区的大小将取决于所采用的参数优化方法,因此其会在类初始化方法中创建。
类方法的集合都已经标准化。 我们来查看它们。 在类构造函数中,我们重置指向对象的指针,并将批次尺寸设置为 1,其实质是把层从网络操作中排除,直至它被初始化。