指数
介绍
人工智能越来越涵盖我们生活的各个方面。出现了许多新出版物,声称“神经网络已经被训练为……”,然而,人工智能仍然与奇妙的事物相关联。这个想法似乎非常复杂,超自然和莫名其妙。因此,下一代奇迹只能由一组科学家创造。似乎无法使用我们的家用计算机开发类似的程序。但是相信我,这并不困难。让我们尝试了解什么是神经网络以及如何将其应用于交易中。
1.建立AI网络的原则
在 维基百科 提供了以下神经网络定义:
人工神经网络(ANN)是由构成动物大脑的生物神经网络松散启发的计算机系统。 RNA基于称为人工神经元的连接单元或节点的集合,该单元可以自由地模拟生物大脑中的神经元。
换句话说,神经网络是由人工神经元组成的实体,其中存在有组织的关系。这些关系类似于生物大脑。
下图显示了一个简单的神经网络图。在此,圆圈表示神经元,而线则可视化了神经元之间的连接。神经元位于分为三组的层中。蓝色表示输入神经元的层,表示来自源的信息输入。绿色和蓝色是输出神经元,它们发出神经网络操作的结果。其中,灰色神经元形成隐藏层。
尽管有这些层,但整个网络是由相同的神经元构成的,其中几个元素用于输入信号,而一个元素仅用于结果。输入数据在神经元内部进行处理,然后生成简单的逻辑结果。例如,可以为是或否。当应用于交易时,结果可以作为交易信号或交易方向发布。
初始信息被插入到输入神经元层中,然后对其进行处理,处理的结果将用作下一层神经元的信息源。从一层到另一层重复操作,直到到达输出神经元层。因此,对初始数据进行处理并将其从一层过滤到另一层,然后生成结果。
根据任务的复杂性和创建的模型,每一层中神经元的数量可以变化。某些网络版本可能包含多个隐藏层。更高级的神经网络可以解决更复杂的问题。但是,这将需要更多的计算资源。
因此,在创建神经网络模型时,有必要定义要处理的数据量和所需的结果。这会影响模型层所需的神经元数量。
如果需要在神经网络中插入10个元素的矩阵,则输入网络的层必须包含10个神经元。这将允许接受数据数组的所有10个元素。额外的输入神经元将过多。
输出神经元的质量由预期结果决定。为了获得明确的逻辑结果,输出神经元就足够了。如果要接收几个问题的答案,请为每个问题创建一个神经元。
隐藏层充当分析中心,用于处理和分析收到的信息。因此,该层中神经元的数量取决于前一层数据的可变性,也就是说,每个神经元都暗示了某种事件假设。
隐藏层的数量由源数据和预期结果之间的因果关系确定。例如,如果我们要为“ 5个为什么”技术创建模型,则逻辑解决方案是使用4个隐藏层,这些隐藏层与输出层一起可以对源数据提出5个问题。
恢复:
- 用相同的神经元建立神经网络;因此,一类神经元足以建立模型。
- 模型中的神经元是分层组织的;
- 神经网络中的数据流被实现为模型从输入神经元到输出神经元的所有层之间的串行传输。
- 输入神经元的数量取决于每遍分析的数据量,而输出神经元的数量取决于生成的数据量;
- 由于在输出端形成了逻辑结果,因此提供给神经网络的问题必须提供给出明确答案的可能性。
2.人工神经元的结构
现在我们已经考虑了神经网络的结构,让我们继续创建人工神经元模型。所有数学计算和决策都在该神经元内执行。这里出现一个问题:我们如何才能基于相同的源数据并使用相同的公式来实现许多不同的解决方案?解决方案是改变神经元之间的连接。确定每个连接的权重系数。此权重定义输入值将对结果产生多大影响。
神经元的数学模型包含两个功能。首先根据输入数据乘积的权重系数对其进行合成。
根据接收到的值,在调用激活函数时计算结果。实际上,使用激活功能的不同变体。最常用的是:
- Sigmoid函数-返回值范围为“ 0”至“ 1”
- 双曲正切-返回值范围为-1到1
激活功能的选择取决于要解决的问题。例如,如果我们期望由于处理源数据而产生逻辑响应,则首选S型函数。出于交易目的,我更喜欢使用双曲正切。值“ -1”对应于卖出信号,值“ 1”对应于买入信号。平均结果表明不确定性。
3.网络培训
如上所述,每个神经元和整个神经网络结果的可变性取决于为神经元之间的连接选择的权重。权重选择问题称为神经网络学习。
可以使用以下几种算法和方法来训练网络:
- 监督学习;
- 无监督学习;
- 强化学习。
学习方法取决于源数据和为神经网络定义的任务。
当有足够的初始数据集且具有对应于所问问题的正确答案时,将使用监督学习。在学习过程中,会将初始数据插入网络,并使用正确的已知答案验证输出。之后,调整权重以减少误差。
当存在初始数据集而没有相应的正确答案时,将使用无监督学习。在这种方法中,神经网络查找相似的数据集,并允许将源数据分为相似的组。
在没有正确答案的情况下使用强化学习,但我们了解所需的结果。在学习过程中,源数据被插入到网络中,试图解决该问题。检查结果后,将发送“反馈”作为一定的奖励。在学习期间,网络尝试获得最大的奖励。
在本文中,我们将使用监督学习。例如,我使用反向传播算法。这种方法允许实时连续训练神经网络。
该方法基于使用神经网络输出误差来校正其权重。学习算法包括两个阶段。首先,网络根据输入数据计算结果值,将其与参考值进行比较并计算出误差。然后,进行反向传递,并通过调整所有加权因子,将误差从网络输出传播到其输入。这是一种交互式方法,并且逐步地训练网络。在学习了如何使用历史数据之后,可以在线模式对网络进行进一步的培训。
反向传播方法使用随机的向下梯度,可以实现最小的可接受误差。在线模式下进一步培训网络的可能性允许长时间维持此最低水平。
4.使用MQL构建我们自己的神经网络
现在,让我们进入本文的实际部分。为了更好地可视化神经网络(RNA)的操作,我们将创建一个仅使用MQL5语言而不使用第三方库的示例。让我们从创建存储有关神经元之间基本连接数据的类开始。
4.1。连接数
首先,我们创建了СConnection类来存储连接的权重系数。它被创建为CObject类的子级。该类将包含两个类型为double的变量:weight以存储weight和deltaWeight,在其中我们将存储权重最近一次变化的值(用于学习)。为了避免使用其他方法来处理变量,我们将其公开。变量的初始值在类构造函数中定义。
class СConnection : public CObject { public: double weight; double deltaWeight; СConnection(double w) { weight=w; deltaWeight=0; } ~СConnection(){}; //--- methods for working with files virtual bool Save(const int file_handle); virtual bool Load(const int file_handle); };
为了能够存储有关连接的更多信息,我们将创建一种方法来将数据保存在文件中(保存)并读取该数据(加载)。这些方法基于经典方案:在方法的参数中接收文件标识符,然后对其进行验证,然后写入数据(或在Load方法中读取数据)。
bool СConnection::Save(const int file_handle) { if(file_handle==INVALID_HANDLE) return false; //--- if(FileWriteDouble(file_handle,weight)<=0) return false; if(FileWriteDouble(file_handle,deltaWeight)<=0) return false; //--- return true; }
下一步是创建一个存储权重的数组:基于CArrayObj的CArrayCon。在这里,我们替换了两个虚拟方法CreateElement和Type。第一个将用于创建新元素,第二个将标识我们的类。
class CArrayCon : public CArrayObj { public: CArrayCon(void){}; ~CArrayCon(void){}; //--- virtual bool CreateElement(const int index); virtual int Type(void) const { return(0x7781); } };
在创建新元素的CreateElement方法的参数中,我们将传递该新元素的索引。我们检查该方法的有效性,数据存储矩阵的大小,并在必要时调整其大小。然后,我们创建СConnection类的新实例,并为其分配初始随机权重。
bool CArrayCon::CreateElement(const int index) { if(index<0) return false; //--- if(m_data_max<index+1) { if(ArrayResize(m_data,index+10)<=0) return false; m_data_max=ArraySize(m_data)-1; } //--- m_data[index]=new СConnection(MathRand()/32767.0); if(!CheckPointer(m_data[index])!=POINTER_INVALID) return false; m_data_total=MathMax(m_data_total,index); //--- return (true); }
4.2。神经元
下一步是创建人工神经元。如前所述,我将双曲正切值用作神经元的激活函数。结果值的范围在-1和1之间。 “ -1”表示卖出信号,“ 1”表示买入信号。
像之前的CConnection元素一样,CNeuron人工神经元类继承自CObject类。但是,它的结构稍微复杂一些。
class CNeuron : public CObject { public: CNeuron(uint numOutputs,uint myIndex); ~CNeuron() {}; void setOutputVal(double val) { outputVal=val; } double getOutputVal() const { return outputVal; } void feedForward(const CArrayObj *&prevLayer); void calcOutputGradients(double targetVals); void calcHiddenGradients(const CArrayObj *&nextLayer); void updateInputWeights(CArrayObj *&prevLayer); //--- methods for working with files virtual bool Save(const int file_handle) { return(outputWeights.Save(file_handle)); } virtual bool Load(const int file_handle) { return(outputWeights.Load(file_handle)); } private: double eta; double alpha; static double activationFunction(double x); static double activationFunctionDerivative(double x); double sumDOW(const CArrayObj *&nextLayer) const; double outputVal; CArrayCon outputWeights; uint m_myIndex; double gradient; };
在类构造函数参数中,我们传递输出神经元的连接数和该层中神经元的序数(它将用于随后的神经元识别)。在方法的主体中,我们定义常量,保存接收到的数据并创建传出连接的数组。
CNeuron::CNeuron(uint numOutputs, uint myIndex) : eta(0.15), // net learning rate alpha(0.5) // momentum { for(uint c=0; c<numOutputs; c++) { outputWeights.CreateElement(c); } m_myIndex=myIndex; }
setOutputVal和getOutputVal方法用于访问神经元的结果值。使用feedForward方法计算得出的神经元值。插入神经元的前层作为该方法的参数。
void CNeuron::feedForward(const CArrayObj *&prevLayer) { double sum=0.0; int total=prevLayer.Total(); for(int n=0; n<total && !IsStopped(); n++) { CNeuron *temp=prevLayer.At(n); double val=temp.getOutputVal(); if(val!=0) { СConnection *con=temp.outputWeights.At(m_myIndex); sum+=val * con.weight; } } outputVal=activationFunction(sum); }
方法主体包含一个穿过前层所有神经元的环。由神经元产生的值和权重的乘积也被添加到方法主体中。在计算出总和后,可以通过activationFunction方法计算神经元的结果值(如在另一种方法中那样实现神经元激活函数)。
double CNeuron::activationFunction(double x) { //output range [-1.0..1.0] return tanh(x); }
下一个方法块用于学习ANN。我们创建了一种方法来计算激活函数的导数activationFunctionDerivative。这允许确定求和函数的必要变化以补偿神经元结果值的误差。
double CNeuron::activationFunctionDerivative(double x) { return 1/MathPow(cosh(x),2); }
接下来,我们创建了两种计算梯度以调整权重的方法。我们需要创建2种方法,因为对于输出层和隐藏层的神经元,结果值的误差计算方式不同。对于输出层,将误差计算为结果值与参考值之间的差。对于隐藏层中的神经元,将误差计算为基于神经元之间连接权重的下一层中所有神经元的梯度总和。此计算被实现为单独的sumDOW方法。
void CNeuron::calcHiddenGradients(const CArrayObj *&nextLayer) { double dow=sumDOW(nextLayer); gradient=dow*CNeuron::activationFunctionDerivative(outputVal); } //+------------------------------------------------------------------+ //| | //+------------------------------------------------------------------+ void CNeuron::calcOutputGradients(double targetVals) { double delta=targetVals-outputVal; gradient=delta*CNeuron::activationFunctionDerivative(outputVal); }
然后通过将误差乘以激活函数的导数来确定梯度。
让我们更详细地考虑为隐藏层确定神经元错误的sumDOW方法。该方法接收指向下一个神经元层的指针作为参数。在方法主体中,我们首先将结果总和值设置为零,然后遍历下一层中的所有神经元,并添加神经元的梯度与其连接权重的乘积。
double CNeuron::sumDOW(const CArrayObj *&nextLayer) const { double sum=0.0; int total=nextLayer.Total()-1; for(int n=0; n<total; n++) { СConnection *con=outputWeights.At(n); CNeuron *neuron=nextLayer.At(n); sum+=con.weight*neuron.gradient; } return sum; }
完成上述准备工作后,我们只需创建updateInputWeights方法即可重新计算权重。在我的模型中,神经元存储输出权重,因此权重更新方法接收参数中神经元的前一层。
void CNeuron::updateInputWeights(CArrayObj *&prevLayer) { int total=prevLayer.Total(); for(int n=0; n<total && !IsStopped(); n++) { CNeuron *neuron= prevLayer.At(n); СConnection *con=neuron.outputWeights.At(m_myIndex); con.weight+=con.deltaWeight=eta*neuron.getOutputVal()*gradient + alpha*con.deltaWeight; } }
方法主体包含一个遍历前层中所有神经元的回路,权重的调整指示了对当前神经元的影响。
请注意,权重调整是使用两个系数执行的:eta(以减少对电流偏差的反应)和alpha(惯性系数)。该方法允许若干后续学习迭代的影响的一定平均值,并过滤掉噪声数据。
4.3。神经网络
创建人造神经元后,我们需要将创建的对象组合到单个实体即神经网络中。生成的对象必须是灵活的,并允许创建不同配置的神经网络。这将使我们能够将生成的解决方案用于各种任务。
如上所述,神经网络由神经元层组成。因此,第一步是将神经元合并为一层。让我们创建CLayer类。它的基本方法是从CArrayObj继承的。
class CLayer: public CArrayObj { private: uint iOutputs; public: CLayer(const int outputs=0) { iOutputs=outpus; }; ~CLayer(void){}; //--- virtual bool CreateElement(const int index); virtual int Type(void) const { return(0x7779); } };
在CLayer类的初始化方法的参数中,我们定义下一层的元素数。此外,我们将重写两个虚拟方法:CreateElement(创建新层神经元)和Type(对象标识方法)。
创建新的神经元时,我们在方法参数中指定其索引。在该方法的主体中验证了接收到的索引的有效性。接下来,我们检查矩阵的大小以存储指向神经元对象实例的指针,并在必要时增加矩阵的大小。之后,我们创建了神经元。如果成功创建了新的神经元实例,我们将定义其初始值并更改矩阵中的对象数量。因此,我们将该方法保留为“ true”。
bool CLayer::CreateElement(const uint index) { if(index<0) return false; //--- if(m_data_max<index+1) { if(ArrayResize(m_data,index+10)<=0) return false; m_data_max=ArraySize(m_data)-1; } //--- CNeuron *neuron=new CNeuron(iOutputs,index); if(!CheckPointer(neuron)!=POINTER_INVALID) return false; neuron.setOutputVal((neuronNum%3)-1) //--- m_data[index]=neuron; m_data_total=MathMax(m_data_total,index); //--- return (true); }
使用类似的方法,我们创建了CArrayLayer类以将指针存储在我们的网络层。
class CArrayLayer : public CArrayObj { public: CArrayLayer(void){}; ~CArrayLayer(void){}; //--- virtual bool CreateElement(const uint neurons, const uint outputs); virtual int Type(void) const { return(0x7780); } };
与上一类的区别出现在CreateElement方法中,该方法在数组中创建一个新元素。在此方法的参数中,我们指定要创建的当前层和后层中神经元的数量。在方法主体中,我们检查层中神经元的数量。如果在创建的层中没有神经元,则以“ false”离开。接下来,我们检查是否有必要调整数组的存储指针的大小。之后,可以创建对象:我们创建一个新层并实现一个循环以创建神经元。我们检查在每个阶段创建的对象。如果发生错误,我们将其保留为'false'。创建所有元素之后,我们将指针保存在从矩阵创建的图层上,并以“ true”退出。
bool CArrayLayer::CreateElement(const uint neurons, const uint outputs) { if(neurons<=0) return false; //--- if(m_data_max<=m_data_total) { if(ArrayResize(m_data,m_data_total+10)<=0) return false; m_data_max=ArraySize(m_data)-1; } //--- CLayer *layer=new CLayer(outputs); if(!CheckPointer(layer)!=POINTER_INVALID) return false; for(uint i=0; i<neurons; i++) if(!layer.CreatElement(i)) return false; //--- m_data[m_data_total]=layer; m_data_total++; //--- return (true); }
为层和层矩阵创建单独的类可以创建具有不同配置的多个神经网络,而无需更改类。这是一个灵活的实体,可让您输入所需的层数和每层神经元。
现在让我们考虑创建一个神经网络的CNet类。
class CNet { public: CNet(const CArrayInt *topology); ~CNet(){}; void feedForward(const CArrayDouble *inputVals); void backProp(const CArrayDouble *targetVals); void getResults(CArrayDouble *&resultVals); double getRecentAverageError() const { return recentAverageError; } bool Save(const string file_name, double error, double undefine, double forecast, datetime time, bool common=true); bool Load(const string file_name, double &error, double &undefine, double &forecast, datetime &time, bool common=true); //--- static double recentAverageSmoothingFactor; private: CArrayLayer layers; double recentAverageError; };
我们已经实现了上述类中所需的大部分内容,因此,神经网络类本身包含最少的变量和方法。除了指向包含网络层的“层”矩阵的指针之外,类代码仅包含两个统计变量以计算和存储平均误差(recentAverageSmoothingFactor和最近使用平均错误)。
让我们更详细地考虑此类的方法。指向int类型数据数组的指针在该类的构造函数参数中传递。矩阵中的元素数量表示层数,而矩阵中的每个元素都包含适当层中神经元的数量。因此,该通用类可用于创建任何复杂程度的神经网络。
CNet::CNet(const CArrayInt *topology) { if(CheckPointer(topology)==POINTER_INVALID) return; //--- int numLayers=topology.Total(); for(int layerNum=0; layerNum<numLayers; layerNum++) { uint numOutputs=(layerNum==numLayers-1 ? 0 : topology.At(layerNum+1)); if(!layers.CreateElement(topology.At(layerNum), numOutputs)) return; } }
在方法的主体中,我们检查传输的指针的有效性,并实现循环以在神经网络中创建图层。为传出级别指定了零个传出连接值。
feedForward方法用于计算神经网络的值。在参数中,该方法接收输入值的数组,基于该值将计算神经网络的结果值。
void CNet::feedForward(const CArrayDouble *inputVals) { if(CheckPointer(inputVals)==POINTER_INVALID) return; //--- CLayer *Layer=layers.At(0); if(CheckPointer(Layer)==POINTER_INVALID) { return; } int total=inputVals.Total(); if(total!=Layer.Total()-1) return; //--- for(int i=0; i<total && !IsStopped(); i++) { CNeuron *neuron=Layer.At(i); neuron.setOutputVal(inputVals.At(i)); } //--- total=layers.Total(); for(int layerNum=1; layerNum<total && !IsStopped(); layerNum++) { CArrayObj *prevLayer = layers.At(layerNum - 1); CArrayObj *currLayer = layers.At(layerNum); int t=currLayer.Total()-1; for(int n=0; n<t && !IsStopped(); n++) { CNeuron *neuron=currLayer.At(n); neuron.feedForward(prevLayer); } } }
在方法的主体中,我们检查接收指针和网络零层的有效性。然后,我们将初始值定义为零层神经元产生的值,并实现一个双循环,对从第一个隐藏层到输出神经元的整个神经网络中神经元产生的值进行阶段性重新计算。
使用getResults方法获得结果,该方法包含一个循环,该循环收集输出层中神经元的结果值。
void CNet::getResults(CArrayDouble *&resultVals) { if(CheckPointer(resultVals)==POINTER_INVALID) { resultVals=new CArrayDouble(); } resultVals.Clear(); CArrayObj *Layer=layers.At(layers.Total()-1); if(CheckPointer(Layer)==POINTER_INVALID) { return; } int total=Layer.Total()-1; for(int n=0; n<total; n++) { CNeuron *neuron=Layer.At(n); resultVals.Add(neuron.getOutputVal()); } }
神经网络学习过程在backProp方法中实现。该方法接收参数参考值的数组。在方法主体中,我们检查接收到的矩阵的有效性,并计算所得层的均方误差。然后,在循环中,我们重新计算所有层中神经元的梯度。之后,在该方法的最后一层,我们根据先前计算的梯度更新神经元之间连接的权重。
void CNet::backProp(const CArrayDouble *targetVals) { if(CheckPointer(targetVals)==POINTER_INVALID) return; CArrayObj *outputLayer=layers.At(layers.Total()-1); if(CheckPointer(outputLayer)==POINTER_INVALID) return; //--- double error=0.0; int total=outputLayer.Total()-1; for(int n=0; n<total && !IsStopped(); n++) { CNeuron *neuron=outputLayer.At(n); double delta=targetVals[n]-neuron.getOutputVal(); error+=delta*delta; } error/= total; error = sqrt(error); recentAverageError+=(error-recentAverageError)/recentAverageSmoothingFactor; //--- for(int n=0; n<total && !IsStopped(); n++) { CNeuron *neuron=outputLayer.At(n); neuron.calcOutputGradients(targetVals.At(n)); } //--- for(int layerNum=layers.Total()-2; layerNum>0; layerNum--) { CArrayObj *hiddenLayer=layers.At(layerNum); CArrayObj *nextLayer=layers.At(layerNum+1); total=hiddenLayer.Total(); for(int n=0; n<total && !IsStopped();++n) { CNeuron *neuron=hiddenLayer.At(n); neuron.calcHiddenGradients(nextLayer); } } //--- for(int layerNum=layers.Total()-1; layerNum>0; layerNum--) { CArrayObj *layer=layers.At(layerNum); CArrayObj *prevLayer=layers.At(layerNum-1); total=layer.Total()-1; for(int n=0; n<total && !IsStopped(); n++) { CNeuron *neuron=layer.At(n); neuron.updateInputWeights(prevLayer); } } }
为了避免在程序重新启动时需要对系统进行重新培训,我们将创建“保存”方法以将数据保存到本地文件,并创建“加载”方法以载入保存在文件中的数据。
所有类方法的完整代码均作为附件提供。
结论
本文的目的是展示如何在家中创建神经网络。当然,这只是冰山一角。本文只考虑了其中一种可能的版本,即由弗兰克·罗森布拉特(Frank Rosenblatt)于1957年推出的感知器。自引入该模型以来已经过去了60多年,并且出现了其他几种模型。但是,感知器模型仍然可行,并且可以产生良好的结果-您可以自己测试模型。那些希望加深人工智能概念的人应该阅读相关材料,因为即使在一系列文章中也无法涵盖所有内容。
参考文献
本文中使用的程序
# | 名称 | 类型 | 描述 |
---|---|---|---|
1 | NeuroNet.mqh | 类库 | 用于创建神经网络(感知器)的类库 |
由MetaQuotes Software Corp.从俄语翻译而来。
来源文章: //www.tbxfkj.com/ru/articles/7447
附加的文件 |
NeuroNet.mqh
(38.34 KB)