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

概述

在上一篇名为神经网络变得轻松的文章中,我们曾研究过利用 赫兹量化交易软件配合完全连接的神经网络一起操作的 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 函数。

 

猜你喜欢

转载自blog.csdn.net/herzqthz/article/details/131582180