自从本系列文章开始以来,我们已在研究各种神经网络模型方面取得了长足的进步。 但学习过程总是在没有我们参与的情况下进行的。 与此同时,总是希望以某种方式帮助神经网络改进训练效果,这也可能会设计神经网络收敛。 在本文中,我们将研究一种名为舍弃的方法。
在训练神经网络时,会将大量特征馈入每个神经元,且很难评估每个独立特征的影响。 结果就是,某些神经元的误差会被其他神经元的调整值抹平,这些误差从而会在神经网络输出处累积。 这会导致训练在某个局部最小值处停止,且误差较大。 这种效应涉及特征检测器的协同适应,其中每个特征的影响会随环境而变化。 当环境分解成单独的特征,且可以分别评估每个特征的影响时,很可能会有相反的效果。
2012年,多伦多大学的一组科学家提议从学习过程中随机排除一些神经元,作为复杂协同适应问题的解决方案 [12]。 训练中减少特征的数量,会增加每个特征的重要性,且特征的数量和质量构成的持续变化降低了它们协同适应的风险。 此方法称为舍弃。 有时拿这种方法的应用与决策树进行比较:通过舍弃一些神经元,我们在每次训练迭代中获得一个含有其自身权重的新神经网络。 根据组合规则,这样的网络具有很大的可变性。
在神经网络操作期间评估所有特征和神经元,从而我们能得到所分析环境当前状态的最准确和独立的评估。
作者在他们的文章(12)中谈及使用该方法来提高预训练模型品质的可能性。
从数学的角度来看,我们可以这样描述这个过程:以给定的概率 p 从过程中舍弃每个独立的神经元。 换句话说,神经元能够参与神经网络学习过程的概率为 q = 1-p 。
由含有正态分布的伪随机数生成器来判定将被排除的神经元列表。 这种方式可以实现最大程度地统一排除神经元。 我们将生成一个练习向量,其大小与输入序列相等。 向量中的 "1" 将会参与训练,且 "0" 则为排除元素。
然而,排除已分析特征无疑会导致神经元激活函数输入量的减少。 为了补偿这种影响,我们将每个特征的值乘以系数 1/q 。 该系数将提升该数值,因为概率 q 始终在 0 到 1 之间。
,
其中:
d — 舍弃结果向量的元素,
q — 在训练过程中用到的神经元概率,
x — 掩码向量的元素,
n — 输入序列的元素.
在学习过程中的前馈验算过程中,误差梯度乘以上述函数的导数。 如您所见,在舍弃的情况下,反馈验算与前馈验算类似,均采用前馈验算的掩码向量。
在神经网络的操作过程中,掩码向量用 “1” 填充,这允许数值在两个方向上平滑传递。
实际上,系数 1/q 在整个训练期间都是恒定的,因此我们可以轻松地一次性计算该系数,然后将其代替 “1” 写入掩码张量当中。 因此,在每次训练迭代中,我们可以排除系数的重新计算操作,并将其乘以掩码 “1”。
如今,我们已研究过理论方面,我们来继续研究如何在函数库中实现此方法的变体。 我们遇到的第一件事是实现两种不同算法。 其一在训练过程需要,而第二个则用于生产。 相应地,我们需要根据每种独立情况,为神经元明确指出应采用的算法。 为此目的,我们将在基准神经元级别引入 bTrain 标志。 该标志值对于训练 应设为 true,而对于测试 则设为 false。
class CNeuronBaseOCL : public CObject
{
protected:
bool bTrain; ///< Training Mode Flag
以下辅助方法将控制该标志值。
virtual void TrainMode(bool flag) { bTrain=flag; }///< Set Training Mode Flag
virtual bool TrainMode(void) { return bTrain; }///< Get Training Mode Flag
该标志特意在基准神经元级别实现。 如此在以后开发时能够启用舍弃相关的代码。
为了实现舍弃算法,我们来创建新的 CNeuronDropoutOCL 类,它将包含在我们的模型当中作为单独的层。 新类将直接继承自 CNeuronBaseOCL 基准神经元类。 在受保护模块中声明变量:
另外,声明两个指向类的指针:
class CNeuronDropoutOCL : public CNeuronBaseOCL
{
protected:
CNeuronBaseOCL *PrevLayer;
double OutProbability;
double OutNumber;
CBufferDouble *DropOutMultiplier;
double dInitValue;
//---
virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); ///<brief Feed Forward method of calling kernel ::FeedForward().@param NeuronOCL Pointer to previous layer.
virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) {return true;} ///< Method for updating weights.@param NeuronOCL Pointer to previous layer.
//---
int RND(void) { xor128; return (int)((double)(Neurons()-1)/UINT_MAX*rnd_w); } ///< Generates a random neuron position to turn off
public:
CNeuronDropoutOCL(void);
~CNeuronDropoutOCL(void);
//---
virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons,double out_prob, 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[in] out_prob Probability of neurons shutdown @param optimization_type Optimization type (#ENUM_OPTIMIZATION)@return Boolen result of operations.
//---
virtual int getOutputIndex(void) { return (bTrain ? Output.GetIndex() : PrevLayer.getOutputIndex()); } ///< Get index of output buffer @return Index
virtual int getGradientIndex(void) { return (bTrain ? Gradient.GetIndex() : PrevLayer.getGradientIndex()); } ///< Get index of gradient buffer @return Index
//---
virtual int getOutputVal(double &values[]) { return (bTrain ? 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 (bTrain ? 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 (bTrain ? 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 (bTrain ? Output : PrevLayer.getOutput()); } ///< Get pointer of output buffer @return Pointer to object
virtual CBufferDouble *getGradient(void) { return (bTrain ? 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 ::CalcHiddenGradient(). @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 defNeuronDropoutOCL; }///< Identificator of class.@return Type of class
};
您必须熟悉类方法的清单,因为它们都会覆盖父类的方法。 唯一排除在外的是 RND 方法,它用来生成均匀分布的伪随机数。 在文章的第十三部分中已讲述过该方法的算法。 在我们的神经网络里,为了确保所有对象中数值的最大可能随机性,伪随机序列生成器在实现时以宏替换来定义全局变量。
#define xor128 rnd_t=(rnd_x^(rnd_x<<11));
rnd_x=rnd_y;
rnd_y=rnd_z;
rnd_z=rnd_w;
rnd_w=(rnd_w^(rnd_w>>19))^(rnd_t^(rnd_t>>8))
uint rnd_x=MathRand(), rnd_y=MathRand(), rnd_z=MathRand(), rnd_w=MathRand(), rnd_t=0;
所提议算法将生成一个范围在 [0,UINT_MAX=4294967295] 内的整数序列。 因此,在伪随机序列生成器方法中,宏替换执行之后,将结果值常规化为序列的大小。
int RND(void) { xor128; return (int)((double)(Neurons()-1)/UINT_MAX*rnd_w); }
如果您阅读过本系列中的早前文章,您可能已经注意到,在以前的版本中,我们没有覆盖来自其他对象的操控类数据缓冲区的方法。 当神经元访问上一层或下一层的数据时,这些方法可在神经网络的各层之间交换数据。
选择该解决方案是为了在实际应用中优化神经网络的运行。 不要忘记仅在神经网络训练时才会用到舍弃层。 在测试和以后的应用期间,会禁用此算法。 通过覆盖数据缓冲区的访问方法,我们启用略过舍弃层。 所有被覆盖的方法都应遵循相同的原理。 取代复制数据,我们实现了用上一层缓冲区替换舍弃层缓冲区。 因此,在以后的操作期间,含有舍弃层的神经网络在速度上可比没有舍弃层的类似网络,而我们在训练阶段已获得了神经元舍弃的所有优势。
virtual int getOutputIndex(void) { return (bTrain ? Output.GetIndex() : PrevLayer.getOutputIndex()); }
在附件中可找到所有类方法的完整代码。
传统上,我们在 feedForward 方法中实现前馈验算。 在方法伊始,检查接收到的指向神经网络上一层的指针,和指向 OpenCL 对象的指针的有效性。 此后,保存上一层所用的激活函数,和指向上一层对象的指针。 对于神经网络实际操作模式,舍弃层的前馈验算到此结束。 以后尝试从下一层访问该层将激活上述替换数据缓冲区的机制。
bool CNeuronDropoutOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
{
if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID)
return false;
//---
activation=(ENUM_ACTIVATION)NeuronOCL.Activation();
PrevLayer=NeuronOCL;
if(!bTrain)
return true;
后续迭代仅与神经网络训练模式相关。 首先,生成一个掩码向量,在其中,我们需定义在此步骤中舍弃的神经元。 将掩码写入 DropOutMultiplier 缓冲区中,检查之前创建对象的可用性,并在必要时创建一个新对象。 用初始值初始化缓冲区。 为了降低计算量,我们以递增的因子 1/q 来初始化缓冲区。
if(CheckPointer(DropOutMultiplier)==POINTER_INVALID)
DropOutMultiplier=new CBufferDouble();
if(!DropOutMultiplier.BufferInit(NeuronOCL.Neurons(),dInitValue))
return false;
for(int i=0;i
缓冲区初始化后,规划一个循环,而其重复次数等于要舍弃的神经元数量。 缓冲区中随机选择的元素将以零值替换。 为避免在一个单元内两次写入 “0” 的风险,在循环内部实现额外检查。
生成掩码后,直接在 GPU 内存中创建一个缓冲区,并传输数据。
if(!DropOutMultiplier.BufferCreate(OpenCL))
return false;
现在,我们需要将两个向量的元素逐个相乘。 此操作的结果将成为舍弃层的输出。 向量乘法运算将在 GPU 上利用 OpenCL 实现。 元素相乘的最有效方法是采用向量运算。 我在 OpenCL 内核里采用 double4 类型的变量, 即 四个元素的向量。 因此,启动线的程数量将比向量中元素的数量少 4 倍。
uint global_work_offset[1]= {0};
uint global_work_size[1];
int i=Neurons()%4;
global_work_size[0]=(Neurons()-i)/4+(i>0 ? 1 : 0);
接下来,指示初始数据缓冲区和变量,并启动内核加以执行。
if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_input,NeuronOCL.getOutputIndex()))
return false;
if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_map,DropOutMultiplier.GetIndex()))
return false;
if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_out,Output.GetIndex()))
return false;
if(!OpenCL.SetArgument(def_k_Dropout,def_k_dout_dimension,Neurons()))
return false;
ResetLastError();
if(!OpenCL.Execute(def_k_Dropout,1,global_work_offset,global_work_size))
{
printf("Error of execution kernel Dropout: %d",GetLastError());
return false;
}
在方法的最后得到内核执行操作的结果。 在此,掩码缓冲区已从 GPU 内存中删除。
if(!Output.BufferRead())
return false;
DropOutMultiplier.BufferFree();
//---
return true;
}
完成操作后,以 true 退出方法。
如果不考虑 GPU 端的操作,前馈方法的描述将是不完整的。 这是内核代码。
__kernel void Dropout (__global double *inputs, ///<[in] Input matrix
__global double *map, ///<[in] Dropout map matrix
__global double *out, ///<[out] Output matrix
int dimension ///< Dimension of matrix
)
内核从参数里接收指向两个含有初始数据的输入张量的指针,和结果张量,以及向量的大小。
在内核代码中,根据线程编号判定需要相乘的元素。 之后,代码被分为两条分支。 第一条分支是主要分支:运用向量运算将四个连续的元素相乘,并将得到的数据写入结果缓冲区的相应元素。
{
const int i=get_global_id(0)*4;
if(i+3
仅当张量中的元素数量并非 4 的倍数,且在循环中剩余元素相乘时,第二条分支才被激活。 这样的循环不会超过 3 次迭代,故此它不是时间紧迫的。
附件中提供了所有类及其方法的完整代码。
在之前研究过的所有神经元反馈验算分为两种方法:
在舍弃的情况下,我们没有权重张量。 然而,为了保持对象的一般结构,我们将覆盖 updateInputWeights 方法 - 但在这种情况下,它将始终返回 true。
virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) {return true;} ///< Method for updating weights.
研究 calcInputGradients方法的实现。 该方法从参数中接收指向上一层的指针。 在方法开始时,检查接收到的指针和指向 OpenCL 对象的指针的有效性。 然后,与前馈验算一样,将算法划分为训练和操作过程。 在测试或操作模式下,我们退出该方法,因为由于数据缓冲区替换,下一个神经层已将梯度直接写入了前一层的缓冲区,从而避免了舍弃层中不必要的迭代。
bool CNeuronDropoutOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
{
if(CheckPointer(OpenCL)==POINTER_INVALID || CheckPointer(NeuronOCL)==POINTER_INVALID)
return false;
//---
if(!bTrain)
return true;
在训练模式下,梯度将以不同的方式传播。 以下算法仅与神经网络训练过程有关。 如同前馈方法一样,检查指向掩码缓冲区 DropOutMultiplier 指针的有效性。 不过,与前馈验算不同,验证错误不会导致创建新的缓冲区 - 在这种情况下,我们将以 false 退出该方法。 这是因为反馈验算使用前馈通道生成的掩码。 这种方法可确保数据的可比性,以及神经元之间误差梯度的正确分布。
if(CheckPointer(DropOutMultiplier)==POINTER_INVALID)
return false;
//---
if(!DropOutMultiplier.BufferCreate(OpenCL))
return false;
在成功验证 DropOutMultiplier 对象之后,在 GPU 内存中创建一个缓冲区,并向其中填充数据。
现在我们需要将两个向量元素逐个相乘。 您对此是否似曾相识? 在上面前馈传递的讲述中,曾给出完全相同的句子。 确实是的。 在理论部分,我们已经看到数学函数 Dropout 的导数等于递增系数。 因此,在反馈验算中,我们还将下一层的梯度乘以 DropOutMultiplier 掩码缓冲区中写入的递增系数。 因此,若前馈和反馈采用相同的内核,则 CNeuronDropoutOCL 类是一种独有的情况,但在这些情况下将馈送不同的输入数据:对于前馈验算,它是神经元的输出数据;对于反馈验算是误差梯度。
因此,我们需指定数据缓冲区,并调用内核执行。 该代码与前馈代码相似,因此务须赘述。
uint global_work_offset[1]= {0};
uint global_work_size[1];
int i=Neurons()%4;
global_work_size[0]=(Neurons()-i)/4+(i>0 ? 1 : 0);
if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_input,Gradient.GetIndex()))
return false;
if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_map,DropOutMultiplier.GetIndex()))
return false;
if(!OpenCL.SetArgumentBuffer(def_k_Dropout,def_k_dout_out,NeuronOCL.getGradientIndex()))
return false;
if(!OpenCL.SetArgument(def_k_Dropout,def_k_dout_dimension,Neurons()))
return false;
ResetLastError();
if(!OpenCL.Execute(def_k_Dropout,1,global_work_offset,global_work_size))
{
printf("Error of execution kernel Dropout: %d",GetLastError());
return false;
}
if(!NeuronOCL.getGradient().BufferRead())
return false;
DropOutMultiplier.BufferFree();
//---
return true;
}
附件中提供了所有类及其方法的完整代码。
我们来看一下保存和加载舍弃神经层对象的方法。 无需保存掩码缓冲区对象,因为在每个训练周期都会生成新的掩码。 仅在 CNeuronDropoutOCL 类的初始化方法中添加了一个变量:应加以保存的排除神经元的概率。
在 Save 方法中,我们将调用父类的相关方法。 成功完成后,我们将保存给定的神经元舍弃概率。
bool CNeuronDropoutOCL::Save(const int file_handle)
{
if(!CNeuronBaseOCL::Save(file_handle))
return false;
//---
if(FileWriteDouble(file_handle,OutProbability)<=0)
return false;
//---
return true;
}
在 Load 方法中,我们将从硬盘读取数据,并还原该类的所有元素。 因此,该方法算法比 Save 算法复杂一些。
与类的保存方法相似,我们调用父类当中的同名方法。 完成后,计算神经元舍弃的概率。 如此即完成了保存方法,但是我们需要复原缺失的元素。 根据神经元舍弃的概率,我们计算需要排除的神经元数量,和递增系数值,该值也用作初始化掩蔽向量的值。
bool CNeuronDropoutOCL::Load(const int file_handle)
{
if(!CNeuronBaseOCL::Load(file_handle))
return false;
//---
OutProbability=FileReadDouble(file_handle);
OutNumber=(int)(Neurons()*OutProbability);
dInitValue=1/(1-OutProbability);
if(CheckPointer(DropOutMultiplier)==POINTER_INVALID)
DropOutMultiplier=new CBufferDouble();
if(!DropOutMultiplier.BufferInit(Neurons()+1,dInitValue))
return false;
//---
return true;
}
现在,计算完毕之后,我们可以复原掩码向量。 检查指向 DropOutMultiplier 中数据缓冲区对象的指针的有效性,并在必要时创建一个新对象。 然后采用初始值来初始化掩码缓冲区。
同样,应将新类正确添加到函数库操作中。 我们从声明操控新内核的宏替换开始。 另外,我们需要为新类设置标识常量。
#define def_k_Dropout 23 ///< Index of the kernel for Dropout process (#Dropout)
#define def_k_dout_input 0 ///< Inputs Tensor
#define def_k_dout_map 1 ///< Map Tensor
#define def_k_dout_out 2 ///< Out Tensor
#define def_k_dout_dimension 3 ///< Dimension of Inputs
#define defNeuronDropoutOCL 0x7890 ///
然后,在神经层描述方法中,我们添加一个新变量来记录神经元舍弃的概率。
class CLayerDescription : public CObject
{
public:
/** Constructor */
CLayerDescription(void);
/** Destructor */~CLayerDescription(void) {};
//---
int type; ///< Type of neurons in layer (ref ObjectTypes)
int count; ///< Number of neurons
int window; ///< Size of input window
int window_out; ///< Size of output window
int step; ///< Step size
int layers; ///< Layers count
ENUM_ACTIVATION activation; ///< Type of activation function (#ENUM_ACTIVATION)
ENUM_OPTIMIZATION optimization; ///< Type of optimization method (#ENUM_OPTIMIZATION)
double probability; ///< Probability of neurons shutdown, only Dropout used
};
在神经网络方法创建方法 CNe ::CNet 里,在层创建和初始化模块中,我们添加初始化新层的代码(在下面的代码中高亮显示)。
for(int i=0; i
不要忘记在同一方法里声明一个新内核。
opencl.SetKernelsCount(24);
opencl.KernelCreate(def_k_FeedForward,"FeedForward");
opencl.KernelCreate(def_k_CalcOutputGradient,"CalcOutputGradient");
opencl.KernelCreate(def_k_CalcHiddenGradient,"CalcHiddenGradient");
opencl.KernelCreate(def_k_UpdateWeightsMomentum,"UpdateWeightsMomentum");
opencl.KernelCreate(def_k_UpdateWeightsAdam,"UpdateWeightsAdam");
opencl.KernelCreate(def_k_AttentionGradients,"AttentionInsideGradients");
opencl.KernelCreate(def_k_AttentionOut,"AttentionOut");
opencl.KernelCreate(def_k_AttentionScore,"AttentionScore");
opencl.KernelCreate(def_k_CalcHiddenGradientConv,"CalcHiddenGradientConv");
opencl.KernelCreate(def_k_CalcInputGradientProof,"CalcInputGradientProof");
opencl.KernelCreate(def_k_FeedForwardConv,"FeedForwardConv");
opencl.KernelCreate(def_k_FeedForwardProof,"FeedForwardProof");
opencl.KernelCreate(def_k_MatrixSum,"SumMatrix");
opencl.KernelCreate(def_k_Matrix5Sum,"Sum5Matrix");
opencl.KernelCreate(def_k_UpdateWeightsConvAdam,"UpdateWeightsConvAdam");
opencl.KernelCreate(def_k_UpdateWeightsConvMomentum,"UpdateWeightsConvMomentum");
opencl.KernelCreate(def_k_Normilize,"Normalize");
opencl.KernelCreate(def_k_NormilizeWeights,"NormalizeWeights");
opencl.KernelCreate(def_k_ConcatenateMatrix,"ConcatenateBuffers");
opencl.KernelCreate(def_k_DeconcatenateMatrix,"DeconcatenateBuffers");
opencl.KernelCreate(def_k_MHAttentionGradients,"MHAttentionInsideGradients");
opencl.KernelCreate(def_k_MHAttentionScore,"MHAttentionScore");
opencl.KernelCreate(def_k_MHAttentionOut,"MHAttentionOut");
opencl.KernelCreate(def_k_Dropout,"Dropout");
必须添加相同的新内核声明,以便从硬盘读取预训练神经网络的方法 - CNet::Load。
关注加载预训练的神经网络的过程,我们还需要添加相关代码来调整 CLayer::CreateElement 方法,以便创建神经网络层,从而创建舍弃元素。 修改高亮在下面。
bool CLayer::CreateElement(int index)
{
if(index>=m_data_max)
return false;
//---
bool result=false;
CNeuronBase *temp=NULL;
CNeuronProof *temp_p=NULL;
CNeuronBaseOCL *temp_ocl=NULL;
CNeuronConvOCL *temp_con_ocl=NULL;
CNeuronAttentionOCL *temp_at_ocl=NULL;
CNeuronMLMHAttentionOCL *temp_mlat_ocl=NULL;
CNeuronDropoutOCL *temp_drop_ocl=NULL;
if(iFileHandle<=0)
{
temp=new CNeuron();
if(CheckPointer(temp)==POINTER_INVALID || !temp.Init(iOutputs,index,SGD))
return false;
result=true;
}
else
{
int type=FileReadInteger(iFileHandle);
switch(type)
{
case defNeuron:
temp=new CNeuron();
if(CheckPointer(temp)==POINTER_INVALID)
result=false;
result=temp.Init(iOutputs,index,ADAM);
break;
case defNeuronProof:
temp_p=new CNeuronProof();
if(CheckPointer(temp_p)==POINTER_INVALID)
result=false;
if(temp_p.Init(iOutputs,index,1,1,1,ADAM))
{
temp=temp_p;
result=true;
}
break;
case defNeuronConv:
temp_p=new CNeuronConv();
if(CheckPointer(temp_p)==POINTER_INVALID)
result=false;
if(temp_p.Init(iOutputs,index,1,1,1,ADAM))
{
temp=temp_p;
result=true;
}
break;
case defNeuronLSTM:
temp_p=new CNeuronLSTM();
if(CheckPointer(temp_p)==POINTER_INVALID)
result=false;
if(temp_p.Init(iOutputs,index,1,1,1,ADAM))
{
temp=temp_p;
result=true;
}
break;
case defNeuronBaseOCL:
if(CheckPointer(OpenCL)==POINTER_INVALID)
return false;
temp_ocl=new CNeuronBaseOCL();
if(CheckPointer(temp_ocl)==POINTER_INVALID)
result=false;
if(temp_ocl.Init(iOutputs,index,OpenCL,1,ADAM))
{
m_data[index]=temp_ocl;
return true;
}
break;
case defNeuronConvOCL:
if(CheckPointer(OpenCL)==POINTER_INVALID)
return false;
temp_con_ocl=new CNeuronConvOCL();
if(CheckPointer(temp_con_ocl)==POINTER_INVALID)
result=false;
if(temp_con_ocl.Init(iOutputs,index,OpenCL,1,1,1,1,ADAM))
{
m_data[index]=temp_con_ocl;
return true;
}
break;
case defNeuronAttentionOCL:
if(CheckPointer(OpenCL)==POINTER_INVALID)
return false;
temp_at_ocl=new CNeuronAttentionOCL();
if(CheckPointer(temp_at_ocl)==POINTER_INVALID)
result=false;
if(temp_at_ocl.Init(iOutputs,index,OpenCL,1,1,ADAM))
{
m_data[index]=temp_at_ocl;
return true;
}
break;
case defNeuronMHAttentionOCL:
if(CheckPointer(OpenCL)==POINTER_INVALID)
return false;
temp_at_ocl=new CNeuronMHAttentionOCL();
if(CheckPointer(temp_at_ocl)==POINTER_INVALID)
result=false;
if(temp_at_ocl.Init(iOutputs,index,OpenCL,1,1,ADAM))
{
m_data[index]=temp_at_ocl;
return true;
}
break;
case defNeuronMLMHAttentionOCL:
if(CheckPointer(OpenCL)==POINTER_INVALID)
return false;
temp_mlat_ocl=new CNeuronMLMHAttentionOCL();
if(CheckPointer(temp_mlat_ocl)==POINTER_INVALID)
result=false;
if(temp_mlat_ocl.Init(iOutputs,index,OpenCL,1,1,1,1,0,ADAM))
{
m_data[index]=temp_mlat_ocl;
return true;
}
break;
case defNeuronDropoutOCL:
if(CheckPointer(OpenCL)==POINTER_INVALID)
return false;
temp_drop_ocl=new CNeuronDropoutOCL();
if(CheckPointer(temp_drop_ocl)==POINTER_INVALID)
result=false;
if(temp_drop_ocl.Init(iOutputs,index,OpenCL,1,0.1,ADAM))
{
m_data[index]=temp_drop_ocl;
return true;
}
break;
default:
result=false;
break;
}
}
if(result)
m_data[index]=temp;
//---
return (result);
}
新类应添加到 CNeuronBaseOCL 基类的调度程序方法当中。
前馈验算 CNeuronBaseOCL::FeedForward。
bool CNeuronBaseOCL::FeedForward(CObject *SourceObject)
{
if(CheckPointer(SourceObject)==POINTER_INVALID)
return false;
//---
CNeuronBaseOCL *temp=NULL;
switch(SourceObject.Type())
{
case defNeuronBaseOCL:
case defNeuronConvOCL:
case defNeuronAttentionOCL:
case defNeuronMHAttentionOCL:
case defNeuronMLMHAttentionOCL:
case defNeuronDropoutOCL:
temp=SourceObject;
return feedForward(temp);
break;
}
//---
return false;
}
误差梯度传播方法 CNeuronBaseOCL::calcHiddenGradients。
bool CNeuronBaseOCL::calcHiddenGradients(CObject *TargetObject)
{
if(CheckPointer(TargetObject)==POINTER_INVALID)
return false;
//---
CNeuronBaseOCL *temp=NULL;
CNeuronAttentionOCL *at=NULL;
CNeuronMLMHAttentionOCL *mlat=NULL;
CNeuronConvOCL *conv=NULL;
CNeuronDropoutOCL *dropout=NULL;
switch(TargetObject.Type())
{
case defNeuronBaseOCL:
temp=TargetObject;
return calcHiddenGradients(temp);
break;
case defNeuronConvOCL:
conv=TargetObject;
temp=GetPointer(this);
return conv.calcInputGradients(temp);
break;
case defNeuronAttentionOCL:
case defNeuronMHAttentionOCL:
at=TargetObject;
temp=GetPointer(this);
return at.calcInputGradients(temp);
break;
case defNeuronMLMHAttentionOCL:
mlat=TargetObject;
temp=GetPointer(this);
return mlat.calcInputGradients(temp);
break;
case defNeuronDropoutOCL:
dropout=TargetObject;
temp=GetPointer(this);
return dropout.calcInputGradients(temp);
break;
}
//---
return false;
}
而且,令人惊讶的是,此处是权重更新方法 CNeuronBaseOCL::UpdateInputWeights。
bool CNeuronBaseOCL::UpdateInputWeights(CObject *SourceObject)
{
if(CheckPointer(SourceObject)==POINTER_INVALID)
return false;
//---
CNeuronBaseOCL *temp=NULL;
switch(SourceObject.Type())
{
case defNeuronBaseOCL:
case defNeuronConvOCL:
case defNeuronAttentionOCL:
case defNeuronMHAttentionOCL:
case defNeuronMLMHAttentionOCL:
case defNeuronDropoutOCL:
temp=SourceObject;
return updateInputWeights(temp);
break;
}
//---
return false;
}
即使上述修改看起来很小或微不足道,但即使缺少了其中之一,也会导致整个神经网络的错误操作。
附件中提供了所有类及其方法的完整代码。
为了保持一致,我们将借用文章第十一部分中的智能交易系统,并在其中添加了 4 个舍弃层:
下面的代码描述了神经网络的结构。
//--- 0
CLayerDescription *desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=(int)HistoryBars*12;
desc.type=defNeuronBaseOCL;
desc.optimization=ADAM;
desc.activation=TANH;
if(!Topology.Add(desc))
return INIT_FAILED;
//--- 1
desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=(int)HistoryBars*12;
desc.type=defNeuronDropoutOCL;
desc.probability=0.2;
desc.optimization=ADAM;
desc.activation=TANH;
if(!Topology.Add(desc))
return INIT_FAILED;
//--- 2
desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=(int)HistoryBars;
desc.type=defNeuronConvOCL;
desc.window=12;
desc.step=12;
desc.window_out=24;
desc.optimization=ADAM;
desc.activation=SIGMOID;
if(!Topology.Add(desc))
return INIT_FAILED;
//--- 3
desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=(int)HistoryBars;
desc.type=defNeuronDropoutOCL;
desc.probability=0.2;
desc.optimization=ADAM;
desc.activation=SIGMOID;
if(!Topology.Add(desc))
return INIT_FAILED;
//--- 4
desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=(int)HistoryBars;
desc.type=defNeuronMLMHAttentionOCL;
desc.window=24;
desc.window_out=4;
desc.step=8; //heads
desc.layers=5;
desc.optimization=ADAM;
desc.activation=SIGMOID;
if(!Topology.Add(desc))
return INIT_FAILED;
//--- 5
desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=(int)HistoryBars;
desc.type=defNeuronDropoutOCL;
desc.probability=0.2;
desc.optimization=ADAM;
desc.activation=SIGMOID;
if(!Topology.Add(desc))
return INIT_FAILED;
//--- 6
desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=200;
desc.type=defNeuron;
desc.activation=TANH;
desc.optimization=ADAM;
if(!Topology.Add(desc))
return INIT_FAILED;
//--- 7
desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=200;
desc.type=defNeuronDropoutOCL;
desc.probability=0.2;
desc.optimization=ADAM;
desc.activation=TANH;
if(!Topology.Add(desc))
return INIT_FAILED;
//--- 8
desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=200;
desc.type=defNeuron;
desc.activation=TANH;
desc.optimization=ADAM;
if(!Topology.Add(desc))
return INIT_FAILED;
//--- 9
desc=new CLayerDescription();
if(CheckPointer(desc)==POINTER_INVALID)
return INIT_FAILED;
desc.count=3;
desc.type=defNeuron;
desc.activation=SIGMOID;
desc.optimization=ADAM;
智能交易系统已基于 EURUSD,H1 时间帧进行了测试,最后 20 根烛条的历史数据被输入到神经网络中。 基于相似数据集测试所有体系结构,可以最大程度地减少外部因素的影响,并可评估相似条件下各种体系结构的性能。
通过比较含有舍弃和不含有舍弃的两个神经网络学习图表,我们可看到神经网络误差曲线的前 30 个迭代几乎是平行的,而没有舍弃的神经网络则展示出更好的结果。 但是在第 33 个迭代之后,采用舍弃的智能交易系统有所降低。 在第 35 迭代之后,舍弃表现出最好的结果,其有误差降低的趋势。 没有舍弃的智能交易系统会把误差持续保持在同一水平。
错失的形态图表还表明,采用舍弃技术的智能交易系统的效果更好。 该图表提供了更多详细信息。 采用舍弃的智能交易系统立即展示出差距缩小的趋势。 与之对比,没有舍弃的智能交易系统会逐渐增加错失形态区域。
两款智能交易系统的预测命中图表非常接近。 经过 44 个迭代的训练,带有舍弃的 EA 仅提升了 0.5%。
在本文中,我们开始研究提升神经网络收敛性的方法,并体验其中一种方法,即舍弃。 该方法已被加入我们以前的智能交易系统之一。 此方法的效率已展示在 EA 测试当中。 当然,利用这种方法会增加神经网络的训练成本。 但这些成本会被最终结果的效率提高所掩盖。
我邀请所有人尝试这种方法,并评估其有效性。
…
# | 发行 | 类型 | 说明 |
1 | Fractal_OCL_AttentionMLMH.mq5 | 智能交易系统 | 采用 GTP 架构的分类神经网络(输出层中有 3 个神经元)和 5 个关注层的智能交易系统 |
2 | Fractal_OCL_AttentionMLMH_d.mq5 | 智能交易系统 | 采用 GTP 架构的分类神经网络(输出层中有 3 个神经元)和 5 个关注层的智能交易系统 + 舍弃 |
3 | NeuroNet.mqh | 类库 | 用于创建神经网络的类库 |
4 | NeuroNet.cl | 代码库 | OpenCL 程序代码库 |
5 | NN.chm | HTML 帮助 | 一个编译后的函数库帮助 CHM 文件。 |
页面更新:2024-04-29
本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828
© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号