神经网络变得轻松(第二部分):网络训练和测试

26 十二月 2020, 10:19
Dmitriy Gizlyk
5
1 180

内容

概述

在上一篇名为神经网络变得轻松的文章中,我们曾研究过利用 MQL5 配合完全连接的神经网络一起操作的 CNet 构造原理。 在本文中,我将演示一个示例,说明如何在 EA 中利用该类,并在实际条件下评估该类。


1. 定义问题

在开始创建智能交易系统之前,必须定义将为新神经网络设定的目标。 当然,金融市场上所有智能交易系统的共同目标是获利。 然而,此目的太笼统宽泛。 我们需要为神经网络指定更具体的任务。 甚至,我们需要了解如何评估神经网络的未来结果。

另一个重要的时刻是,先前创建的 CNet 类使用了监督学习的原理,因此它需要标记数据作为训练集合。

分形

如果您查看价格图表,自然会希望在价格峰值时执行交易操作,这可以通过标准的比尔·威廉姆斯(Bill Williams)分形指标来示意。 而指标的问题在于,它会判断 3 根烛条的峰值,且产生的信号始终会延迟 1 根烛条,而这可能会产生相反的信号。 如果我们设置神经网络以判定第三根烛条形成之前的枢轴点,该怎么办? 这种方法至少会在交易方向移动有一根之前走势的烛条。

这是指训练集合:

  • 在正向递进中,我们会将当前行情状况输入到神经网络,并输出最后一根收盘烛条上提取的形成概率的评估。
  • 对于逆向递进中,在下一根烛条形成之后,我们将检查前一根烛条上是否存在分形,并将输入结果按权重调整。 

为了评估网络运算的结果,我们可以使用均方预测误差,正确的分形预测的百分比,和无法识别的分形的百分比。

现在我们需要判定哪些数据应输入到我们的神经网络中。 您还记得,当您尝试根据图表评估行情状况时所做的事情吗?

首先,建议交易新手从图表中直观评估趋势方向。 因此,我们必须将有关价格变动的信息数字化,并将其输入到神经网络中。 我建议输入有关的开盘价和收盘价、最高价和最低价、交易量和形成时间的数据。 

另一种判定趋势的流行方法是使用振荡器指标。 此类指标操作很方便,因为指标会输出标准化数据。 我决定为本次实验准备四个标准指标:RCI,CCI,ATR 和 MACD,所有指标均带采用标准参数。 我在选择指标及其参数时,没有进行任何其他分析。

有人可能会说利用指标是没有意义的,因为指标的数据是通过重新计算烛条的价格数据而建立的,我们已经将其输入到神经网络当中。 但这并非完全正确。 指标值是通过计算来自多根烛条的数据来判定的,从而可对所分析的样本进行一定程度的扩展。 神经网络训练过程将判定它们如何影响结果。

为了能够评估行情动态,我们在一定的历史时期内将全部信息输入到神经网络之中。

2. 神经网络模型项目

2.1. 判定输入层中神经元的数量

此处,我们需要知晓输入层中神经元的数量。 为此,请评估每根烛条上的初始信息,然后将其乘以分析历史记录的深度。

由于指标数据已经标准化,且指标缓冲区的相关数量已知,因此无需预处理指标数据(上述 4 个指标总共有 5 个值)。 因此,若要在输入层中接收这些指标,我们需要为每根所分析烛条创建 5 个神经元。

烛条价格数据的情况略有不同。 从图表直观地判定趋势方向和强度时,我们首先分析烛条方向和大小。 只有在此之后,当我们要判定趋势方向,和可能的枢轴点时,我们要注意所分析品种的价位。 因此,有必要在把该数据输入到神经网络之前对其进行标准化。 我个人输入了所述烛条的开盘价与收盘价、最高价和最低价的差值。 在这种方法中,定义三个神经元就足够了,其中第一个神经元的符号判定烛条方向。

有许多不同的资料论述了各种时间因素对货币波动的影响。 例如,季度线、周线和日线的动态差异,以及欧洲、美洲和亚洲的交易时段,均以不同的方式影响货币汇率。 若要分析这些因素,将烛条形成的月份、时刻和星期几输入进神经网络。 我特意将烛条形成的时间和日期分为几个部分,因为这可令神经网络能够泛化,并找到依赖性。

另外,我们来包含有关成交量的信息。 如果您的经纪商提供真实的交易量数据,则指明这些交易量;否则指定即时报价的交易量。

故此,为了应对每根烛条,我们需要 12 个神经元。 将此数字乘以所分析的历史深度,您可得到神经网络输入层的大小。

2.2. 设计隐藏层

下一步是准备神经网络的隐藏层。 网络结构(层数和神经元数)的选择是最困难的任务之一。 单层感知器善于类的线性分离。 双层网络可跟踪非线性边界。 三层网络可以描述复杂的多连接区域。 当我们增加层数时,功能类别会扩展,但这会导致收敛性变差,和训练成本增加。 每层当中,神经元的数量必须满足功能的预期变化。 实际上,非常简单的网络无法在实际条件下按要求的精度模拟行为,而过于复杂的网络不仅会重复训练目标函数,还有噪声。

在首篇文章中,我提到了 “5 个为什么” 方法。 现在,我建议继续此实验,并创建一个包含 4 个隐藏层的网络。 我将首个隐藏层中的神经元数量设置为 1000。 不过,也有可能根据分析周期的深度建立一些依赖关系。 遵照帕累托(Pareto)规则,我们将每个后续层中的神经元数量减少 70%。 此外,将遵循如下限制:隐藏层中的神经元数量不得少于 20。

2.3. 判定输出层中神经元的数量

输出层当中神经元的数量取决于任务,及其解决方案。 若要解决回归问题,只需要一个神经元就能产生期望值即可。 为了解决分类问题,我们需要与期望的类数量相等的神经元 - 每个神经元将为分配给每个类的原始对象生成概率。 而在实际中,对象的类别由最大概率判定。

对于我们的情况,我建议创建 2 个神经网络变体,并评估它们在实践中应对我们之问题的适用性。 在第一种情况下,输出层仅有一个神经元。 数值在 0.5...1.0 范围内与买入分形对应,而数值在 -0.5..-1.0 范围内与卖出信号对应,数值在 -0.5...0.5 范围内表示没有信号。 在此解决方案中,双曲正切用作激活函数 - 它的输出值范围为 -1.0 到 +1.0。

在第二种情况下,将在输出层中创建 3 个神经元(买、卖、无信号)。 在这个变体中,我们来训练神经网络,从而获得范围为 0.0...1.0 的结果。 在此,结果就是分形出现的概率。信号将依据最大概率来判定,并根据含有最高概率的神经元的索引来判定信号的方向。

3. 编程

3.1. 准备工作

现在,到编程的时候了。 首先,加入所需的函数库:

  • NeuroNet.mqh — 前一篇文章中创建神经网络的函数库
  • SymbolInfo.mqh — 接收品种数据的标准库
  • TimeSeries.mqh — 处理时间序列的标准库
  • Volumes.mqh — 接收交易量数据的标准库
  • Oscilators.mqh — 含有振荡器类的标准库

#include "NeuroNet.mqh"
#include <Trade\SymbolInfo.mqh>
#include <Indicators\TimeSeries.mqh>
#include <Indicators\Volumes.mqh>
#include <Indicators\Oscilators.mqh>

下一步是编写程序参数,通过它们来设置神经网络和指标参数。

//+------------------------------------------------------------------+
//|   input parameters                                               |
//+------------------------------------------------------------------+
input int                  StudyPeriod =  10;            //Study period, years
input uint                 HistoryBars =  20;            //Depth of history
ENUM_TIMEFRAMES            TimeFrame   =  PERIOD_CURRENT;
//---
input group                "---- RSI ----"
input int                  RSIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   RSIPrice    =  PRICE_CLOSE;   //Applied price
//---
input group                "---- CCI ----"
input int                  CCIPeriod   =  14;            //Period
input ENUM_APPLIED_PRICE   CCIPrice    =  PRICE_TYPICAL; //Applied price
//---
input group                "---- ATR ----"
input int                  ATRPeriod   =  14;            //Period
//---
input group                "---- MACD ----"
input int                  FastPeriod  =  12;            //Fast
input int                  SlowPeriod  =  26;            //Slow
input int                  SignalPeriod=  9;             //Signal
input ENUM_APPLIED_PRICE   MACDPrice   =  PRICE_CLOSE;   //Applied price

接下来,声明全局变量 - 稍后会讲解它们的用法。

CSymbolInfo         *Symb;
CiOpen              *Open;
CiClose             *Close;
CiHigh              *High;
CiLow               *Low;
CiVolumes           *Volumes;
CiTime              *Time;
CNet                *Net;
CArrayDouble        *TempData;
CiRSI               *RSI;
CiCCI               *CCI;
CiATR               *ATR;
CiMACD              *MACD;
//---
double               dError;
double               dUndefine;
double               dForecast;
double               dPrevSignal;
datetime             dtStudied;
bool                 bEventStudy;

准备工作至此完成。 现在继续进行类的初始化。

3.2 初始化类

类的初始化将在 OnInit 函数中执行。 首先,我们创建处理品种的 CSymbolInfo 类的实例,并更新有关图表品种的数据。

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
  {
//---
   Symb=new CSymbolInfo();
   if(CheckPointer(Symb)==POINTER_INVALID || !Symb.Name(_Symbol))
      return INIT_FAILED;
   Symb.Refresh();

然后创建时间序列实例。 在您每次创建类实例时,请检查它是否已成功创建,并初始化。 如果发生错误,则以 INIT_FAILED 作为结果退出函数。

   Open=new CiOpen();
   if(CheckPointer(Open)==POINTER_INVALID || !Open.Create(Symb.Name(),TimeFrame))
      return INIT_FAILED;
//---
   Close=new CiClose();
   if(CheckPointer(Close)==POINTER_INVALID || !Close.Create(Symb.Name(),TimeFrame))
      return INIT_FAILED;
//---
   High=new CiHigh();
   if(CheckPointer(High)==POINTER_INVALID || !High.Create(Symb.Name(),TimeFrame))
      return INIT_FAILED;
//---
   Low=new CiLow();
   if(CheckPointer(Low)==POINTER_INVALID || !Low.Create(Symb.Name(),TimeFrame))
      return INIT_FAILED;
//---
   Volumes=new CiVolumes();
   if(CheckPointer(Volumes)==POINTER_INVALID || !Volumes.Create(Symb.Name(),TimeFrame,VOLUME_TICK))
      return INIT_FAILED;
//---
   Time=new CiTime();
   if(CheckPointer(Time)==POINTER_INVALID || !Time.Create(Symb.Name(),TimeFrame))
      return INIT_FAILED;

在此示例中采用了即时报价交易量。 若您希望采用真实交易量,则在调用 Volumes.Creare 方法时将 “VOLUME_TICK” 替换为 “VOLUME_REAL”。

在声明了时间序列之后,创建类的实例,从而以类似方式使用指标。

   RSI=new CiRSI();      
   if(CheckPointer(RSI)==POINTER_INVALID || !RSI.Create(Symb.Name(),TimeFrame,RSIPeriod,RSIPrice))
      return INIT_FAILED;
//---
   CCI=new CiCCI();      
   if(CheckPointer(CCI)==POINTER_INVALID || !CCI.Create(Symb.Name(),TimeFrame,CCIPeriod,CCIPrice))
      return INIT_FAILED;
//---
   ATR=new CiATR();      
   if(CheckPointer(ATR)==POINTER_INVALID || !ATR.Create(Symb.Name(),TimeFrame,ATRPeriod))
      return INIT_FAILED;
//---
   MACD=new CiMACD();      
   if(CheckPointer(MACD)==POINTER_INVALID || !MACD.Create(Symb.Name(),TimeFrame,FastPeriod,SlowPeriod,SignalPeriod,MACDPrice))
      return INIT_FAILED;

现在我们可以直接利用神经网络类运作了。 首先,创建一个类的实例。 在 CNet 类初始化期间,构造函数参数会将引用传递给含有网络结构规范的数组。 请注意,网络训练过程当中会消耗计算资源,且会花费大量时间。 因此,每次重启之后,网络都是不正确的,需要训练。 此处是我如何操作的:首先,我在声明网络实例时未指定结构,然后尝试从本地存储加载先前已训练过的网络(文件名在 #define 中提供)。

#define FileName        Symb.Name()+"_"+EnumToString((ENUM_TIMEFRAMES)Period())+"_"+IntegerToString(HistoryBars,3)+"fr_ea"
...
...
...
...
   Net=new CNet(NULL);
   ResetLastError();
   if(CheckPointer(Net)==POINTER_INVALID || !Net.Load(FileName+".nnw",dError,dUndefine,dForecast,dtStudied,false))
     {
      printf("%s - %d -> Error of read %s prev Net %d",__FUNCTION__,__LINE__,FileName+".nnw",GetLastError());

如果无法加载先前已训练的数据,则会将消息打印到日志,指示错误代码,然后开始创建新的未经训练的网络。 首先,声明 CArrayInt 类的实例,并指定神经网络的结构。 元素的数量表示神经网络层的数量,而元素的数值表示相应层中神经元的数量。

      CArrayInt *Topology=new CArrayInt();
      if(CheckPointer(Topology)==POINTER_INVALID)
         return INIT_FAILED;

正如早前所提到的,我们在输入层中需要 12 个神经元来应对每根烛条。 因此,在第一个数组元素中,用 12 乘以所分析历史记录的深度。

      if(!Topology.Add(HistoryBars*12))
         return INIT_FAILED;

然后定义隐藏层。 我们已判定在第一个隐藏层中将包括 4 个含 1000 个神经元的隐藏层。 然后,在后续的每个层中,神经元的数量将减少 70%,但每一层至少含有 20 个神经元。 数据将循环添加到数组当中。

      int n=1000;
      bool result=true;
      for(int i=0;(i<4 && result);i++)
        {
         result=(Topology.Add(n) && result);
         n=(int)MathMax(n*0.3,20);
        }
      if(!result)
        {
         delete Topology;
         return INIT_FAILED;
        }

在输出层中指示 1 来构建回归模型。

      if(!Topology.Add(1))
         return INIT_FAILED;

如果我们采用分类模型,则需要为输出神经元指定 3。

接下来,删除先前创建的 CNet 类实例,并创建一个新实例,并在其中指明要创建的神经网络的结构。 创建新的神经网络实例后,删除网络结构的类,因为以后不会再用到它。

      delete Net;
      Net=new CNet(Topology);
      delete Topology;
      if(CheckPointer(Net)==POINTER_INVALID)
         return INIT_FAILED;

设置变量的初始值,以便收集统计数据:

  • dError - 标准偏差(误差)
  • dUndefine - 未定义分形的百分比
  • dForecast - 正确预测分形的百分比
  • dtStudied — 最后一根已训练烛条的日期。

      dError=-1;
      dUndefine=0;
      dForecast=0;
      dtStudied=0;
     }

不要忘记,只当没有先前训练过的神经网络,无需从本地存储加载的情况下,我们才需要设置神经网络结构,创建神经网络类的新实例,并初始化统计变量。
在 OnInit 函数的末尾,创建 CArrayDouble() 类的实例,该实例用来与神经网络交换数据,并开始神经网络训练过程。

我想在这里分享另一种解决方案。 MQL5 不支持异步函数调用。 如果我们从 OnInit 函数显式调用学习函数,则终端将误认为程序初始化过程尚未完成,直到训练完成。 这就是为什么我们要创建一个自定义事件,从 OnChartEvent 函数调用该训练函数,替代直接调用该函数的原因。 创建事件时,请在 lparam 参数中指定训练开始日期。 这种方法可令我们调用函数,并完成 OnInit 函数。

   TempData=new CArrayDouble();
   if(CheckPointer(TempData)==POINTER_INVALID)
      return INIT_FAILED;
//---
   bEventStudy=EventChartCustom(ChartID(),1,(long)MathMax(0,MathMin(iTime(Symb.Name(),PERIOD_CURRENT,(int)(100*Net.recentAverageSmoothingFactor*(dForecast>=70 ? 1 : 10))),dtStudied)),0,"Init");
//---
   return(INIT_SUCCEEDED);
  }
//+------------------------------------------------------------------+
//| ChartEvent function                                              |
//+------------------------------------------------------------------+
void OnChartEvent(const int id,
                  const long &lparam,
                  const double &dparam,
                  const string &sparam)
  {
//---
   if(id==1001)
     {
      Train(lparam);
      bEventStudy=false;
      OnTick();
     }
  }

不要忘记清除 OnDeinit 函数中的内存。

//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {
//---
   if(CheckPointer(Symb)!=POINTER_INVALID)
      delete Symb;
//---
   if(CheckPointer(Open)!=POINTER_INVALID)
      delete Open;
//---
   if(CheckPointer(Close)!=POINTER_INVALID)
      delete Close;
//---
   if(CheckPointer(High)!=POINTER_INVALID)
      delete High;
//---
   if(CheckPointer(Low)!=POINTER_INVALID)
      delete Low;
//---
   if(CheckPointer(Time)!=POINTER_INVALID)
      delete Time;
//---
   if(CheckPointer(Volumes)!=POINTER_INVALID)
      delete Volumes;
//---
   if(CheckPointer(RSI)!=POINTER_INVALID)
      delete RSI;
//---
   if(CheckPointer(CCI)!=POINTER_INVALID)
      delete CCI;
//---
   if(CheckPointer(ATR)!=POINTER_INVALID)
      delete ATR;
//---
   if(CheckPointer(MACD)!=POINTER_INVALID)
      delete MACD;
//---
   if(CheckPointer(Net)!=POINTER_INVALID)
      delete Net;
   if(CheckPointer(TempData)!=POINTER_INVALID)
      delete TempData;
  }

3.3. 训练神经网络

为了训练神经网络,创建 Train 函数。 训练期的开始日期应作为参数传递给函数。

void Train(datetime StartTrainBar=0)

在函数伊始声明局部变量:

  • count - 学习期计数
  • prev_un - 前一学习期内未识别分形的百分比
  • prev_for - 前一学习期内分形正确“预测”的百分比
  • prev_er - 前一个学习期的误差
  • bar_time - 重新计算酒吧日期
  • stop - 用于跟踪强制程序终止调用的标志。

   int count=0;
   double prev_up=-1;
   double prev_for=-1;
   double prev_er=-1;
   datetime bar_time=0;
   bool stop=IsStopped();
   MqlDateTime sTime;

接下来,检查在函数参数中获得的日期是否未超过最初指定的训练周期。

   MqlDateTime start_time;
   TimeCurrent(start_time);
   start_time.year-=StudyPeriod;
   if(start_time.year<=0)
      start_time.year=1900;
   datetime st_time=StructToTime(start_time);
   dtStudied=MathMax(StartTrainBar,st_time);

神经网络训练将在 do-while 循环语句中实现。 在循环开始时,重新计算训练神经网络所需历史柱线数量,并保存先前的递次统计信息。

   do
     {
      int bars=(int)MathMin(Bars(Symb.Name(),TimeFrame,dtStudied,TimeCurrent())+HistoryBars,Bars(Symb.Name(),TimeFrame));
      prev_un=dUndefine;
      prev_for=dForecast;
      prev_er=dError;
      ENUM_SIGNAL bar=Undefine;

然后,调整缓冲区的大小,并加载必要的历史数据。

      if(!Open.BufferResize(bars) || !Close.BufferResize(bars) || !High.BufferResize(bars) || !Low.BufferResize(bars) || !Time.BufferResize(bars) ||
         !RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars) || !Volumes.BufferResize(bars))
         break;
      Open.Refresh(OBJ_ALL_PERIODS);
      Close.Refresh(OBJ_ALL_PERIODS);
      High.Refresh(OBJ_ALL_PERIODS);
      Low.Refresh(OBJ_ALL_PERIODS);
      Volumes.Refresh(OBJ_ALL_PERIODS);
      Time.Refresh(OBJ_ALL_PERIODS);
      RSI.Refresh(OBJ_ALL_PERIODS);
      CCI.Refresh(OBJ_ALL_PERIODS);
      ATR.Refresh(OBJ_ALL_PERIODS);
      MACD.Refresh(OBJ_ALL_PERIODS);

更新跟踪强制程序终止的标志,并声明一个新的标志,指示学习期已经过去(add_loop)。

      stop=IsStopped();
      bool add_loop=false;

遍历所有历史数据来组织嵌套的训练循环。 在循环的伊始,检查历史数据是否已触及末尾。 如有必要,更改 add_loop 标志。 另外,在图表上用注释显示神经网络训练的当前状态。 这将有助于监视训练过程。

      for(int i=(int)(bars-MathMax(HistoryBars,0)-1); i>=0 && !stop; i--)
        {
         if(i==0)
            add_loop=true;
         string s=StringFormat("Study -> Era %d -> %.2f -> Undefine %.2f%% foracast %.2f%%\n %d of %d -> %.2f%% \nError %.2f\n%s -> %.2f",count,dError,dUndefine,dForecast,bars-i+1,bars,(double)(bars-i+1.0)/bars*100,Net.getRecentAverageError(),EnumToString(DoubleToSignal(dPrevSignal)),dPrevSignal);
         Comment(s);

然后检查循环的上一步是否已计算出预测的系统状态。 如果有,则沿正确值的方向调整权重。 为此,清除 TempData 数组的内容,检查在上一根烛条上是否分形已形成,然后向 TempData 数组添加正确的值(以下是在输出层中含有一个神经元的回归神经网络的代码)。 之后,调用神经网络的 backProp 方法,把 TempData 数组的引用作为参数传递。 更新 dForecast(正确预测分形的百分比)和 dUndefine(无法识别分形的百分比)中的统计数据。

         if(i<(int)(bars-MathMax(HistoryBars,0)-1) && i>1 && Time.GetData(i)>dtStudied && dPrevSignal!=-2)
           {
            TempData.Clear();
            bool sell=(High.GetData(i+2)<High.GetData(i+1) && High.GetData(i)<High.GetData(i+1));
            bool buy=(Low.GetData(i+2)<Low.GetData(i+1) && Low.GetData(i)<Low.GetData(i+1));
            TempData.Add(buy && !sell ? 1 : !buy && sell ? -1 : 0);
            Net.backProp(TempData);
            if(DoubleToSignal(dPrevSignal)!=Undefine)
              {
               if(DoubleToSignal(dPrevSignal)==DoubleToSignal(TempData.At(0)))
                  dForecast+=(100-dForecast)/Net.recentAverageSmoothingFactor;
               else
                  dForecast-=dForecast/Net.recentAverageSmoothingFactor;
               dUndefine-=dUndefine/Net.recentAverageSmoothingFactor;
              }
            else
              {
               if(sell || buy)
                  dUndefine+=(100-dUndefine)/Net.recentAverageSmoothingFactor;
              }
           }

调整神经网络权重系数后,计算在当前历史柱线上出现分形的概率(如果等于 0,则计算在当前柱线上形成分形的概率)。 为此,清除 TempData 数组,并将神经网络输入层的当前数据添加到其中。 如果数据添加失败或没有足够的数据,则退出循环。

         TempData.Clear();
         int r=i+(int)HistoryBars;
         if(r>bars)
            continue;
//---
         for(int b=0; b<(int)HistoryBars; b++)
           {
            int bar_t=r+b;
            double open=Open.GetData(bar_t);
            TimeToStruct(Time.GetData(bar_t),sTime);
            if(open==EMPTY_VALUE || !TempData.Add(Close.GetData(bar_t)-open) || !TempData.Add(High.GetData(bar_t)-open) || !TempData.Add(Low.GetData(bar_t)-open) ||
               !TempData.Add(Volumes.Main(bar_t)/1000) || !TempData.Add(sTime.mon) || !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) ||
               !TempData.Add(RSI.Main(bar_t)) ||
               !TempData.Add(CCI.Main(bar_t)) || !TempData.Add(ATR.Main(bar_t)) || !TempData.Add(MACD.Main(bar_t)) || !TempData.Add(MACD.Signal(bar_t)))
                  break;
           }
         if(TempData.Total()<(int)HistoryBars*12)
            break;

准备好初始数据后,运行 feedForward 方法,并将神经网络结果写入 dPrevSignal 变量。 以下是在输出层中含有一个神经元的回归神经网络的代码。 在输出层中含有三个神经元的分类神经网络的代码随附于后。

         Net.feedForward(TempData);
         Net.getResults(TempData);
         dPrevSignal=TempData[0];

为了在图表上可视化神经网络的操作,显示最后 200 根蜡烛的预测分形的标签。

         bar_time=Time.GetData(i);
         if(i<200)
           {
            if(DoubleToSignal(dPrevSignal)==Undefine)
               DeleteObject(bar_time);
            else
               DrawObject(bar_time,dPrevSignal,High.GetData(i),Low.GetData(i));
           }

在历史数据周期结束时,更新强制程序终止的标志。

         stop=IsStopped();
        }

一旦在所有可用的历史数据上针对神经网络进行了训练,就可以增加训练期的计数,并将神经网络的当前状态保存到本地文件之中。 下次启动神经网络数据时,我们将会用到此数据。

      if(add_loop)
         count++;
      if(!stop)
        {
         dError=Net.getRecentAverageError();
         if(add_loop)
           {
            Net.Save(FileName+".nnw",dError,dUndefine,dForecast,dtStudied,false);
            printf("Era %d -> error %.2f %% forecast %.2f",count,dError,dForecast);
           }
         }

最后,指定退出训练循环的条件。 条件可以如下:接收信号表示所达目标高于预定级别以上的概率;达到目标误差参数;或在训练期之后,统计数据没有变化,或变化不大(训练停止在局部最小值处)。 您可以自行定义退出训练过程的条件。 

     }
   while((!(DoubleToSignal(dPrevSignal)!=Undefine || dForecast>70) || !(dError<0.1 && MathAbs(dError-prev_er)<0.01 && MathAbs(dUndefine-prev_up)<0.1 && MathAbs(dForecast-prev_for)<0.1)) && !stop);

在退出训练函数之前,请保存最后一根已训练烛条的时间。

   if(count>0)
     {
      dtStudied=bar_time;
     }
  }

3.4. 改进梯度计算方法

我想提请您注意,我在测试过程中发现的如下几点。 当训练神经网络时,在某些情况下,隐藏层神经元的权重系数不受控制地增加,这是因为超出了最大允许变量值,结果导致整个神经网络瘫痪了。 当随后的层错误要求神经元输出的数值,超出了激活函数可能的数值范围时,就会发生这种情况。 我找到的解决方案是标准化神经元的目标值。 经调整后的梯度计算方法代码如下所示。

void CNeuron::calcOutputGradients(double targetVals)
  {
   double delta=(targetVals>1 ? 1 : targetVals<-1 ? -1 : targetVals)-outputVal;
   gradient=(delta!=0 ? delta*CNeuron::activationFunctionDerivative(targetVals) : 0);
  }

附件中提供了所有方法和函数的完整代码。

4. 测试

在 H1 时间帧内,在 EURUSD 对上进行了神经网络的测试训练。 将 20 根烛条的数据输入到神经网络。 针对最近两年进行了训练。 为了检查结果,我在同一终端的两张图表上同时启动了这个智能交易系统:一个 EA 具有回归神经网络(分形 - 输出层中有 1 个神经元),另一个则是分类神经网络(Fractal_2 - 输出层中有 3 个神经元的分类)。

第一个训练期是 12432 根柱线,历时 2 小时 20 分钟。 两种 EA 的表现相似,命中率刚超过 6%。

回归神经网络的第一个训练期的结果(1 个输出神经元) 分类神经网络的第一个训练期的结果(3 个输出神经元)

第一个训练期强烈依赖于在初始阶段随机选择的神经网络的权重。

经过 35 期的训练,统计数据的差异略有增加 - 回归神经网络模型的效果更好:

数值 回归神经网络 分类神经网络
根均方误差 0.68 0.78
命中率 12.68% 11.22%
未识别的分形 20.22% 24.65%

回归神经网络第 35 个训练期的结果(1 个输出神经元) 分类神经网络的第 35 个训练期的结果(3 个输出神经元)

测试结果表明,两种神经网络组织变体在训练时间和预测准确性方面产生的结果相似。 于此同时,获得的结果表明神经网络需要额外的时间和资源进行训练。 如果您希望分析神经网络的学习动态,请查看附件中每个学习期的屏幕截图。

结束语

在本文中,我们研究了神经网络创建、训练和测试的过程。 获得的结果表明,这种技术有潜在的利用价值。 然而,神经网络训练过程会消耗大量计算资源,并会花费大量时间。

本文中用到的程序

# 发行 类型 说明
Experts\NeuroNet_DNG\
1 Fractal.mq5   智能交易系统  一款含有回归神经网络(输出层中有 1 个神经元)的智能交易系统
2 Fractal_2.mq5  智能交易系统  一款含有分类神经网络的智能交易系统(输出层中有 3 个神经元)
3 NeuroNet.mqh 类库 创建神经网络(感知器)的类库
  Files\    
4  Fractal  目录  包含显示回归神经网络测试的屏幕截图
 Fractal_2  目录  包含显示分类神经网络测试的屏幕截图

本文译自 MetaQuotes Software Corp. 撰写的俄文原文
原文地址: https://www.mql5.com/ru/articles/8119

附加的文件 |
MQL5.zip (2005.76 KB)
最近评论 | 前往讨论 (5)
Zhiqiang Zhu
Zhiqiang Zhu | 26 12月 2020 在 10:54
12.68%的命中率?有实际意义吗?我投硬币也50%左右的概率了。这么低的概率,不知道这个东西有何用?
Shi Chao Ma
Shi Chao Ma | 6 1月 2021 在 21:54
Zhiqiang Zhu:
12.68%的命中率?有实际意义吗?我投硬币也50%左右的概率了。这么低的概率,不知道这个东西有何用?
交易性金融中的神经网络压根不是它这么用的,学习什么呢找规律求万能公式,逻辑本身就很荒谬。真正的交易性金融人工智能是不做判断的,而是立足五维对四维混沌系统进行降维打击。
Shi Chao Ma
Shi Chao Ma | 6 1月 2021 在 22:00
Zhiqiang Zhu:
12.68%的命中率?有实际意义吗?我投硬币也50%左右的概率了。这么低的概率,不知道这个东西有何用?
交易算法分三个等级,一级技术指标型(垃圾),二级大数据战略型(垃圾),三级逻辑必然型(无敌)。我的算法就是第三等级,安全性100%五个交易日加个零。
fAmAccount
fAmAccount | 9 1月 2021 在 17:50
Shi Chao Ma:
交易性金融中的神经网络压根不是它这么用的,学习什么呢找规律求万能公式,逻辑本身就很荒谬。真正的交易性金融人工智能是不做判断的,而是立足五维对四维混沌系统进行降维打击。
想和前辈学习一下 五维到四维的降维
fAmAccount
fAmAccount | 9 1月 2021 在 17:51
Shi Chao Ma:
交易算法分三个等级,一级技术指标型(垃圾),二级大数据战略型(垃圾),三级逻辑必然型(无敌)。我的算法就是第三等级,安全性100%五个交易日加个零。
我也认为交易逻辑是EA最重要 最有灵魂的部分,所以请多多指教
自定义品种(符号):实践基础 自定义品种(符号):实践基础

本文专门介绍了程序化生成自定义品种(符号),这些自定义品种可用来演示一些显示报价的流行方法。 它描述的是一种建议的微创智能交易系统改编方案,可用在派生的自定义品种图表上,如同真实品种一样。 MQL 源代码随附于文后。

什么是趋势,行情结构是基于趋势还是横盘? 什么是趋势,行情结构是基于趋势还是横盘?

交易者经常谈论趋势和横盘,但很少有人真正了解趋势/横盘是什么,甚至很少能够清楚地解释这些概念。 讨论这些基本术语通常会受到一系列顽固偏见和误解的困扰。 然而,如果我们想赚钱,就需要了解这些概念的数学和逻辑含义。 在本文中,我将仔细研究趋势和横盘的本质,并尝试定义行情结构是基于趋势/横盘,亦或其他。 我还将研究在趋势和横盘行情上获利的最佳策略。

DoEasy 函数库中的时间序列(第四十八部分):在单一子窗口里基于一个缓冲区的多周期、多品种指标 DoEasy 函数库中的时间序列(第四十八部分):在单一子窗口里基于一个缓冲区的多周期、多品种指标

本文研究了一个示例,该示例使用单个指标缓冲区来创建多品种、多周期标准指标,以便在指标子窗口中进行构造和操作。 我会准备库类,以便在程序主窗口中与标准指标一起操作,并有多个缓冲区来显示其数据。

连续前行优化 (第八部分): 程序改进和修复 连续前行优化 (第八部分): 程序改进和修复

根据本系列文章的用户和读者的评论和要求,程序已进行了修改。 本文包含一个自动优化器的新版本。 该版本实现了所需的功能,并提供了其他改进,这些是我运用该程序操作时发现的。