English Русский Español Deutsch 日本語 Português
preview
神经网络变得轻松(第二十部分):自动编码器

神经网络变得轻松(第二十部分):自动编码器

MetaTrader 5交易系统 | 17 十月 2022, 10:36
1 440 0
Dmitriy Gizlyk
Dmitriy Gizlyk

内容


      概述

      我们继续研究无监督学习方法。 在前几篇文章中,我们已经分析了聚类、数据压缩和关联规则挖掘算法。 但之前研究的无监督算法并未用到神经网络。 在本文中,我们回到研究神经网络。 这一次,我们将看到自动编码器。


      1. 自动编码器架构

      在继续讲述自动编码器架构之前,我们回顾一下具有监督学习算法的神经网络训练方法。 在研究算法时,我们采用形态和目标结果组成的成对标记数据。 我们曾优化过权重,如此可把神经网络运算结果与目标值之间的误差最小化。

      在这种情况下,神经网络可以学习什么? 它将学习我们对她的确切期望。 也就是说,它将找到影响目标结果的特征。 然而,网络将对不影响目标结果,或效果不显著的特征赋予零权重。 故此,模型是在一个非常狭窄的预方向上训练。 这没什么过错。 这个模型完美达到我们的所求。

      但如同硬币都有另一面。 我们已经遇到了转移学习的概念,它意味着采用预先训练的模型来解决新问题。 在这种情况下,只有当以前的目标值和新的目标值依赖于相同的特征时,才能获得良好的结果。 否则,模型性能可能会受到在前一个训练阶段被归零的缺失特征的影响。

      这将如何影响任务解决方案? 我们的世界不是静止的,它在不断变化。 今天的市场驱动因素可能在明天就会失去影响力。 市场又会受到其它力量的推动。 这限制了我们模型的寿命。 这很明显。 例如,交易员不时地审查他们的策略。 采用经典策略描述方法构建的算法交易机器人,其盈利能力也验证了这一点。

      当我们开始研究神经网络时,我们预期人工智能的运用能延长模型的寿命。 此外,通过不时进行附加的训练,我们也能够在很长一段时间内产生利润。

      为了把与上述资产相关的风险最小化,我们将使用表示或特征学习,这是无监督学习的一个领域。 表示学习结合了一组自动从原始输入数据中提取特征的算法。 我们前面讨论的聚类和降维算法也涉及表示学习。 我们在这些算法中采用了线性变换。 自动编码器可以研究更复杂的形式。

      一般情况下,自动编码器是由两个编码器和解码器模块组成的神经网络。 编码器源数据层和解码器结果层包含相同数量的元素。 它们之间有一个隐藏层,通常比源数据小。 在学习过程中,这一层的神经元形成一种潜在(隐藏)状态,能以压缩形式描述源数据。


      这类似于我们采用主成分分析方法解决的数据压缩问题。 不过,稍后我们讨论的方法中会存在一些差异。

      如上所述,自动编码器是一种神经网络。 它通过反向传播方法进行训练。 诀窍在于,因为我们用的是未标记的数据,所以我们首先训练模型,再用编码器将数据压缩到潜在状态的大小。 然后,在解码器中,模型在数据恢复到原始状态时信息损失将会最小。

      因此,我们采用已知的反向传播方法来训练自动编码器。 训练样本本身被用作目标结果。

      神经层的结构可以不同。 在最简单的版本中,这些层可以是完全连接层。 卷积模型被广泛用于从图像中提取特征。

      递归模型和关注度算法可用于处理序列。 所有这些细节将在以后的文章中讨论。


      2. 自动编码器解决的经典问题

      尽管训练自动编码器的方法十分不标准,但它们可以用于解决各种各样的问题。 首先,这些是数据压缩和预处理任务...

      数据压缩算法可以分为两种类型:

      • 有损压缩
      • 无损压缩

      有损数据压缩的一个例子是我们在前一篇文章中讨论过的主成分分析(PCA)。 当选择组件时,我们要注意信息损失的最大程度。

      无损数据压缩的一个例子是各种归档器和压缩器。 我们不想在解压缩后损失任何数据。

      理论上,自动编码器可以依据任何数据进行训练。 编码器可用于压缩数据,解码器则用于恢复原始数据。 这将基于传递的潜在状态进行。 根据自动编码器模型的复杂程度,可以分有损压缩和无损压缩。 当然,无损数据压缩需要更复杂的模型。 它广泛应用于电信领域,在提高数据传输品质同时,减少所用的通信带宽。 这最终会增加所用网络的吞吐量。

      压缩之后下一步来到源数据的预处理。 例如,自动编码器的数据压缩用于解决所谓的维数灾难。 许多机器学习方法对低维数据的学习效果更好、速度更快。 因此,输入数据维数较小的神经网络将包含较少的可训练权重。 这意味着它们能更快地学习和工作,并降低过度匹配的风险。

      自动编码器解决的另一个任务是清除源数据中的噪声。 解决这个问题亦有两种方法。 与 PCA 一样,第一种是有损数据压缩。 然而,我们预计噪声将在压缩过程中丢失。

      第二种方式广泛用于图像处理。 我们获取高质量(无噪声)的源图像,并添加各种失真(伪影、噪声、等等)。 失真的图像会被送入自动编码器。 对模型进行训练,从而获得与原始高品质图像的最大相似性。 不过,我们应小心行事,特别是在选择扭曲时。 它们应与自然噪音相当。 否则,模型很可能无法在真实条件下正确工作。

      除了从图像中剔除噪声外,还可以利用自动编码器删除或向图像中添加对象。 例如,如果我们取两张图像,彼此仅有一个对象不同,并将它们输入编码器,则两个图像的潜在状态之间的差别向量仅与其中一张图像上存在的对象将相对应。 因此,通过把结果向量添加到任何其它图像的潜在状态,我们可以将对象添加到图像之中。 类似地,从潜在状态中减去向量,我们就可从图像中删除对象。

      独立的自动编码器学习技术能够将潜在状态分离为内容和图像样式。 保留样式的内容替换,允许在解码器输出时获得一个新图像 — 该图像将把一个图像的内容和另一图像的样式相结合。 这些实验的开发,允许用经过训练的自动编码器来生成图像。

      一般来说,自动编码器可用来解决的任务范围相当广泛。 但并非所有这些都可以应用于交易。 至少,我看不出现在如何使用生成能力。 也许有人会提出一些非标准的想法,并可能实现它们。


      3. 自动编码器与 PCA 的比较

      如上所述,自动编码器解决的任务与前面所研究的算法部分重叠。 特别是,自动编码器和主成分分析方法可以压缩数据(降低维数),并从源数据中去除噪声。 既如此,为什么我们需要另一种工具来解决同样的问题呢? 我们看看这些方法及其性能之间的差异。

      首先,我们来回顾一下主成分分析方法算法的细节。 这是一种基于严格数学公式的纯数学方法。 它用来提取主成分。 当针对同一源数据使用该方法时,我们会始终获得相同的结果。 而自动编码器并非如此。

      自动编码器是一种神经网络。 它在初始化时配合随机等待,并采用逐级递减法进行迭代训练。 训练采用相同的数学公式。 然而,由于某些原因,针对同一源数据进行相同模型的不同训练,也可能会产生完全不同的结果。 它们能够提供类似的精度,但仍然有所不同。

      第二个方面与转换的类型有关。 在 PCA 中,我们采用矩阵乘法形式的线性变换。 而在神经网络中,我们通常使用非线性激活函数。 这意味着自动编码器中的转换将更加复杂。

      好吧,我们可以拿主成分分析方法与没有激活函数的三层自动编码器进行比较。 但即使在这种情况下,即使隐藏层元素的数量等于主成分的数量,也不能保证相同的结果。 与之对比,在 PCA 情况下,就可以保证产生更好的结果。


      此外,主成分的计算将比训练自动编码器模型快得多。 因此,如果数据中存在线性关系,最好采用主成分分析法对其进行压缩。 自动编码器更适合复杂度更高的任务。


      4. 自动编码器在交易中的潜在用途

      现在我们已经研究了自动编码器算法的理论部分,我们来看看如何在交易策略中使用它们的功能。 第一个可能的想法是数据预处理:数据压缩和噪声消除。 早些时候,我们采用主成分分析方法执行了类似实验。 因此,我们可以进行比较分析。

      现在我很难想象我们如何利用自动编码器的生成能力。 此外,虚假图表的数值也值得怀疑。 当然,您可以尝试训练自动编码器,从而获得的解码器结果能稍微领先一点时间。 但这与前面讨论的监督学习方法没有太大区别。 无论如何,这种方法的价值只能通过实验来评估。

      我们还可以尝试评估市场形势变化的动态。 因为交易一般基于对市场形势变化的监测,和对未来走势的预测。 我已经讲述了采用潜在状态可以在图像中添加或删除对象的方法。 我们为什么不利用这项财产呢? 但我们不会在解码器输出端扭曲市场状况。 我们将尝试根据两个连续的潜在状态之间的差异向量来评估市场动态。

      我们还将使用转移学习。 这就是我们的文章开始的地方。 自动编码器学习技术可用于训练模型,以便从源数据中提取特征。 然后,我们将只用编码器,为其添加若干个决策层,并用监督学习训练模型来解决我们的任务。 当我们训练一个自动编码器时,它的潜在状态包含了源数据的所有特征。 因此,只要训练一次编码器,我们就可以用它来解决各种问题。 当然,前提是它们依据相同的源数据。

      我们概述了一系列实验任务。 请注意,它们的整体篇幅超出了一篇文章的范畴。 但我们不怕困难。 那么,我们开始实践部分。


      5. 实践性实验

      现在是实施实验的时候了。 首先,我们将采用完全连接的层创建和训练一个简单的自动编码器。 为了建立自动编码器模型,我们将采用我们在研究监督学习方法时创建的神经层函数库。

      在直接创建代码之前,我们研究一下我们将如何训练自动编码器。 我们之前曾讨论过编码器,并理清了它们返回的源数据。 那么我们为什么要问这个问题呢? 实际上,当我们处理同质数据时,一切都很清楚。 在这种情况下,我们简单地训练模型,以便返回原始数据。 但我们的原始数据并不一致。 我们可以给模型饲喂价格数据,以及指标读数。 各种指标的读数也提供了不同的数据。 当我们研究监督学习算法时,已经提到了这一点。 在早前的文章中,我们注意到不同振幅的数据对模型结果有不同的影响。 但现在问题变得更加复杂,因为在解码器结果层,我们必须指定激活函数。 该激活函数必须能够返回不同初始值的整个范围。

      我的解决方案与监督学习方法相同,即规范化源数据。 这可以作为单独的流程或使用批量规范化层来实现。

      我们的自动编码器的第一个隐藏层就是批量规范化层。 我们将训练自动编码器,以便解码器返回规范化数据。 对于解码器结果层,我们将采用双曲正切作为激活函数。 而这允许在 -1 和 1 之间针对结果进行规范化。

      这是理论上的解决方案。 为了在实践中实现它,在每次模型训练迭代中,我们都需要访问模型第一个隐藏层的结果。 我们还没有看到我们的模型内部。 我们的神经网络的隐藏状态一直是一个“黑箱”。 这一次,我们需要打开它,来规划学习过程。 为此,我们转到我们的 CNet 类来规划神经网络操作,并添加 GetLayerOutput 方法,以便获取任何隐藏层的结果缓冲区的数值。

      在这个新方法的参数中,我们将传递所需层的序号,和一个指向缓冲区的指针,以便写入结果。

      不要忘记在方法主体中加入结果检查模块。 在本例中,我们检查有效的模型层缓冲区存在与否。 此外,我们还要检查所需神经层的指定序号是否落在模型的神经层数范围之内。 请注意,这并不是检查可能的指定负值层数的错误。 取而代之,我们采用无符号整数变量来获取参数。 如此这般,其值将始终为非负数。 因此,在控制模块中,我们只需简单地检查模型中神经层数的上限。

      在成功传递控件模块后,我们得到指向指定神经层的指针,并放入局部变量之中。 立即检查所接收指针的有效性。

      在该方法的下一步中,我们检查参数中指向结果接收缓冲区的指针的有效性。 如有必要,发起创建新的数据缓冲区。

      之后,从相应的神经层请求结果缓冲区中的数值。 不要忘记在每个步骤中都要检查结果。

      bool CNet::GetLayerOutput(uint layer, CBufferDouble *&result)
        {
         if(!layers || layers.Total() <= (int)layer)
            return false;
         CLayer *Layer = layers.At(layer);
         if(!Layer)
            return false;
      //---
         if(!result)
           {
            result = new CBufferDouble();
            if(!result)
               return false;
           }
      //---
         CNeuronBaseOCL *temp = Layer.At(0);
         if(!temp || temp.getOutputVal(result) <= 0)
            return false;
      //---
         return true;
        }
      
      

      准备工作至此完毕。 现在,我们能够着手构建我们的第一个自动编码器了。 为了实现它,我们将创建一个智能交易系统,并将其命名为 ae.mq5。 它是基于监督学习模型的 EA。

      源数据是价格报价和四个指标的读数:RSI、CCI、ATR 和 MACD。 用相同的数据测试所有以前的模型。 所有指标参数均在 EA 的外部参数中指定。 而在 OnInit 函数中,我们初始化操作所需指标的对象实例。

      int OnInit()
        {
      //---
         Symb = new CSymbolInfo();
         if(CheckPointer(Symb) == POINTER_INVALID || !Symb.Name(_Symbol))
            return INIT_FAILED;
         Symb.Refresh();
      //---
         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;
      
      

      接下来,我们需要指定编码器的体系结构。 神经网络构建算法和原理与我们构建监督学习模型的算法和原理完全一致。 仅有的区别是神经网络的架构。

      为了把神经网络架构传递给我们的初始化模型,我们需创建一个动态对象数组 CArrayObj。 该数组中的每个对象都描述一个神经层。 它们在数组中的序列则对应于模型中的神经层序列。 为了描述神经层架构,我们将采用为此专门创建的 CLayerDescription 对象。

      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
         int               batch;         ///< Batch Size
         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
        };
      
      

      第一层是源数据层,其被声明为完全连接层。 每根烛条我们都需要 12 个元素来描述。 因此,图层大小将是一个形态的历史深度的 12 倍。 我们不会针对源数据层使用激活函数。

         Net = new CNet(NULL);
         ResetLastError();
         double temp1, temp2;
         if(CheckPointer(Net) == POINTER_INVALID || !Net.Load(FileName + ".nnw", dError, temp1, temp2, dtStudied, false))
           {
            printf("%s - %d -> Error of read %s prev Net %d", __FUNCTION__, __LINE__, FileName + ".nnw", GetLastError());
            CArrayObj *Topology = new CArrayObj();
            if(CheckPointer(Topology) == POINTER_INVALID)
               return INIT_FAILED;
            //--- 0
            CLayerDescription *desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            int prev = desc.count = (int)HistoryBars * 12;
            desc.type = defNeuronBaseOCL;
            desc.activation = None;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
      

      一旦神经层的架构描述完毕,我们将其添加到模型架构描述的动态数组之中。

      下一层是批量规范化层。 我们之前讨论了创建它的必要性。 批量规范化层中的元素数量等于前一层中的神经元数量。 此处,我们也不会用到激活函数。 我们指明规范化批量大小等于 1000 个元素,以及训练参数的优化方法。 此外,我们将另一个神经层的描述添加到模型架构描述的动态数组当中。

            //--- 1
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = prev;
            desc.batch = 1000;
            desc.type = defNeuronBatchNormOCL;
            desc.activation = None;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
      

      请记住,我们的自动编码器体系结构中规范化层的索引。

      接下来,开始构建自动编码器的编码器。 在编码器中,我们逐渐将神经层的大小缩减到潜在状态的 2 个元素。 它的架构类似于漏斗。

      编码器的所有神经层都采用双曲切线作为激活函数。 为了激活潜在状态,我采用了希格玛函数。

      当构建自动编码器时,对神经层数及所采用的激活函数没有特殊要求。 故此,我们应用了构建任何神经网络模型时所采用的相同原则。 我建议您在构建自动编码器模型时尝试不同的体系结构。

            //--- 2
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            prev = desc.count = (int)HistoryBars;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 3
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            prev = desc.count = prev / 2;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 4
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            prev = desc.count = prev / 2;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 5
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = 2;
            desc.type = defNeuronBaseOCL;
            desc.activation = SIGMOID;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
      

      接下来,我们指定解码器的体系结构。 这次,我们将逐渐增加神经层中元素的数量。 通常,解码器的体系结构是编码器的镜像。 但我决定改变神经网络的数量,以及其中包含的神经元。 不过,我们必须确保批量规范化层中的神经元数量等于解码器结果层的数量。

            //--- 6
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = (int) HistoryBars;
            desc.type = defNeuronBaseOCL;
            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 = (int) HistoryBars * 4;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
            //--- 8
            desc = new CLayerDescription();
            if(CheckPointer(desc) == POINTER_INVALID)
               return INIT_FAILED;
            desc.count = (int) HistoryBars * 12;
            desc.type = defNeuronBaseOCL;
            desc.activation = TANH;
            desc.optimization = ADAM;
            if(!Topology.Add(desc))
               return INIT_FAILED;
      
      

      在模型架构的描述创建完毕后,我们可以继续创建自动编码器的神经网络。 我们创建一个神经网络对象的新实例,并将自动编码器的描述传递给其构造函数。

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

      在 EA 初始化函数完成之前,我们来创建一个临时数据的缓冲区,和一个启动模型训练的事件。

         TempData = new CBufferDouble();
         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 * 10)),
                                        dtStudied)), 0, "Init");
      //---
         return(INIT_SUCCEEDED);
        }
      
      

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

      所创建的自动编码器需要进行训练。 我们的 EA 模板调用 Train 函数来训练模型。 该函数在参数中接收训练的开始日期。 在函数主体中,我们创建局部变量,并定义学习周期。

      void Train(datetime StartTrainBar = 0)
        {
         int count = 0;
      //---
         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);
         ulong last_tick = 0;
      
         double prev_er = DBL_MAX;
         datetime bar_time = 0;
         bool stop = IsStopped();
         CArrayDouble *loss = new CArrayDouble();
         MqlDateTime sTime;
      
      

      之后,加载历史数据以便训练模型。

         int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);
         prev_er = dError;
      //---
         if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
           {
            ExpertRemove();
            return;
           }
         if(!ArraySetAsSeries(Rates, true))
           {
            ExpertRemove();
            return;
           }
         RSI.Refresh(OBJ_ALL_PERIODS);
         CCI.Refresh(OBJ_ALL_PERIODS);
         ATR.Refresh(OBJ_ALL_PERIODS);
         MACD.Refresh(OBJ_ALL_PERIODS);
      
      

      模型会在嵌套循环系统中进行训练。 外层循环将计算训练周期。 内层循环将迭代学习世代期内的历史数据。

      在外层循环主体中,我们将存储来自上一个训练世代的误差值。 它将用于控制学习动态。 如果完成下一个学习世代后的误差变化动态不明显,那么学习过程就会被中断。 此外,我们需要检查指明用户停止程序的标志。 后随一个嵌套循环。

         int total = (int)(bars - MathMax(HistoryBars, 0));
         do
           {
            //---
            stop = IsStopped();
            prev_er = dError;
            for(int it = total - 1; it >= 0 && !stop; it--)
              {
               int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
               if((GetTickCount64() - last_tick) >= 250)
                 {
                  com = StringFormat("Study -> Era %d -> %.6f\n %d of %d -> %.2f%% \nError %.5f",
                                     count, prev_er, bars - it + 1, bars,
                                     (double)(bars - it + 1.0) / bars * 100, Net.getRecentAverageError());
                  Comment(com);
                  last_tick = GetTickCount64();
                 }
      
      

      在嵌套循环的主体中,我们显示有关学习过程的信息 — 该信息将在图表上作为注释显示。 然后随机判定下一个形态来训练模型。 其后,以历史数据填充临时缓冲区。

               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 = Rates[bar_t].open;
                  TimeToStruct(Rates[bar_t].time, sTime);
                  double rsi = RSI.Main(bar_t);
                  double cci = CCI.Main(bar_t);
                  double atr = ATR.Main(bar_t);
                  double macd = MACD.Main(bar_t);
                  double sign = MACD.Signal(bar_t);
                  if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE ||
                     macd == EMPTY_VALUE || sign == EMPTY_VALUE)
                     continue;
                  //---
                  if(!TempData.Add(Rates[bar_t].close - open) || !TempData.Add(Rates[bar_t].high - open) ||
                     !TempData.Add(Rates[bar_t].low - open) || !TempData.Add((double)Rates[bar_t].tick_volume / 1000.0) ||
                     !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
                     !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
                     break;
                 }
               if(TempData.Total() < (int)HistoryBars * 12)
                  continue;
      
      

      在收集历史数据后,调用自动编码器的前馈方法。 收集来的历史数据则于方法参数中进行传递。

               Net.feedForward(TempData, 12, true);
               TempData.Clear();
      
      

      在下一步中,我们需要调用模型的反向传播方法。 之前,我们在方法参数中传递的是目标结果的缓冲区。 现在,编码器的目标结果是规范化的源数据。 为此,我们需要首先获取批量规范化层的结果,然后将其传递给模型的反向传播方法。 正如我们已知的,在我们的模型中,批量规范化层的索引是 “1”。

               if(!Net.GetLayerOutput(1, TempData))
                  break;
               Net.backProp(TempData);
               stop = IsStopped();
              }
      
      

      当反向传播方法完成后,检查程序执行是否由用户中断的标志,然后跳转到嵌套循环的下一次迭代。

      训练世代完成后,保存当前模型的训练结果。 当前模型的误差值会通知给用户,并将其保存到训练动态缓冲区中。

      在启动新的学习世代之前,需检查进一步训练的可行性。

            if(!stop)
              {
               dError = Net.getRecentAverageError();
               Net.Save(FileName + ".nnw", dError, 0, 0, dtStudied, false);
               printf("Era %d -> error %.5f %%", count, dError);
               loss.Add(dError);
               count++;
              }
           }
         while(!(dError < 0.01 && (prev_er - dError) < 0.01) && !stop);
      
      

      而当训练完成后,将整个模型训练过程的误差动态保存到一个文件之中,并调用一个函数强制 EA 终止。

         Comment("Write dynamic of error");
         int handle = FileOpen("ae_loss.csv", FILE_WRITE | FILE_CSV | FILE_ANSI, ",", CP_UTF8);
         if(handle == INVALID_HANDLE)
           {
            PrintFormat("Error of open loss file: %d", GetLastError());
            delete loss;
            return;
           }
         for(int i = 0; i < loss.Total(); i++)
            if(FileWrite(handle, loss.At(i)) <= 0)
               break;
         FileClose(handle);
         PrintFormat("The dynamics of the error change is saved to a file %s\\%s",
                     TerminalInfoString(TERMINAL_DATA_PATH), "ae_loss.csv");
         delete loss;
         Comment("");
         ExpertRemove();
        }
      
      

      在如上版本中,我调用 ExpertRemove 函数来完成 EA 操作,因为它的目的只是训练模型。 如果您的 EA 有其它用意,请从代码中删除此函数。 可选项,您也可将其移动到末尾,以便在 EA 执行所有赋予的任务后再执行。

      在文后附件中可找到 EA 和全部所用类的完整代码。

      接下来,我们能够用采用真实数据测试所创建的 EA。 自动编码器已经基于 EURUSD 过去 15 年的 H1 时间帧数据进行了训练。 如此,自动编码器已在超过 92,000 个形态,40 根蜡条的训练集合上进行了训练。 学习误差动态如下图所示。

      如您所见,在 10 个世代中,均方根误差的值下降到 0.28,然后继续缓慢下降。 而这意味着,自动编码器能够将信息从 480 个特征(40 根蜡烛 * 每根蜡烛 12 个特征)压缩到两个元素的潜在状态,同时保留 78% 的信息。 如果您还记得,在用 PCA 时,前两个组件上仅保留不到 25% 的类似数据。

      我故意采用等于 2 个元素的潜在状态大小。 这样就能令其可视化,并可与我们运用主成分分析方法获得的类似表现进行比较。 为了准备这样的数据,我们要稍微修改一下上述的 EA。 主要变化将影响模型的训练函数 Train。 该函数的开头不会更改 — 它包括训练样本的创建过程。

      创建训练样本后,我们立刻加入主成分分析方法进行训练。

      void Train(datetime StartTrainBar = 0)
        {
      //---
          The process of creating a training sample has not changed
      //---
         if(!PCA.Study(data))
           {
            printf("Runtime error %d", GetLastError());
            return;
           }
      
      

      在上述 EA 中,我们创建了一个由两个嵌套循环组成的系统来训练模型。 现在我们不会重新训练自动编码器,但我们将采用先前训练的模型。 因此,我们不需要嵌套循环系统。 我们只需要一个遍历训练样本元素的循环。 此外,我们无需可视化所有 92,000 个形态的潜在状态。 而这将令信息难以理解。 故我决定只可视化 1000 个形态。 您可以基于任何所需数量的形态重复我的实验,以便进行可视化。

      鉴于我决定不可视化整个样本,故我会从训练样本中随机选择一种形态来可视化。 如此,我们可用所选形态的特征填充临时缓冲区。

           {
            //---
            stop = IsStopped();
            bool add_loop = false;
            for(int it = 0; i < 1000 && !stop; i++)
              {
               if((GetTickCount64() - last_tick) >= 250)
                 {
                  com = StringFormat("Calculation -> %d of %d -> %.2f%%", it + 1, 1000, (double)(it + 1.0) / 1000 * 100);
                  Comment(com);
                  last_tick = GetTickCount64();
                 }
               int i = (int)((MathRand() * MathRand() / MathPow(32767, 2)) * (total));
               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 = Rates[bar_t].open;
                  TimeToStruct(Rates[bar_t].time, sTime);
                  double rsi = RSI.Main(bar_t);
                  double cci = CCI.Main(bar_t);
                  double atr = ATR.Main(bar_t);
                  double macd = MACD.Main(bar_t);
                  double sign = MACD.Signal(bar_t);
                  if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
                     continue;
                  //---
                  if(!TempData.Add(Rates[bar_t].close - open) || !TempData.Add(Rates[bar_t].high - open) ||
                     !TempData.Add(Rates[bar_t].low - open) || !TempData.Add((double)Rates[bar_t].tick_volume / 1000.0) ||
                     !TempData.Add(sTime.hour) || !TempData.Add(sTime.day_of_week) || !TempData.Add(sTime.mon) ||
                     !TempData.Add(rsi) || !TempData.Add(cci) || !TempData.Add(atr) || !TempData.Add(macd) || !TempData.Add(sign))
                     break;
                 }
               if(TempData.Total() < (int)HistoryBars * 12)
                  continue;
      
      

      接收到有关形态的信息后,调用自动编码器的前馈方法,并运用主成分分析方法压缩数据。 然后,我们得到了自动编码器潜在状态的结果缓冲区的数值。

               Net.feedForward(TempData, 12, true);
               data = PCA.ReduceM(TempData);
               TempData.Clear();
               if(!Net.GetLayerOutput(5, TempData))
                  break;
      
      

      之前,当测试模型时,我们检查了它们预测分形形成的能力。 这次,为了可视地分离形态,我们将在图表上用颜色区分形态。 因此,我们需要指定所渲染形态属于哪种类型。 为了理解这一点,我们需要检查形态之后形成的分形。

               bool sell = (Rates[i - 1].high <= Rates[i].high && Rates[i + 1].high < Rates[i].high);
               bool buy = (Rates[i - 1].low >= Rates[i].low && Rates[i + 1].low > Rates[i].low);
               if(buy && sell)
                  buy = sell = false;
      
      

      接收到的数据将保存到文件中,以便将来可视化。 然后我们移至下一个形态。

               FileWrite(handle, (buy ? DoubleToString(TempData.At(0)) : " "), (buy ? DoubleToString(TempData.At(1)) : " "),
                         (sell ? DoubleToString(TempData.At(0)) : " "), (sell ? DoubleToString(TempData.At(1)) : " "),
                         (!(buy || sell) ? DoubleToString(TempData.At(0)) : " "),
                         (!(buy || sell) ? DoubleToString(TempData.At(1)) : " "),
                         (buy ? DoubleToString(data[0, 0]) : " "), (buy ? DoubleToString(data[0, 1]) : " "),
                         (sell ? DoubleToString(data[0, 0]) : " "), (sell ? DoubleToString(data[0, 1]) : " "),
                         (!(buy || sell) ? DoubleToString(data[0, 0]) : " "),
                         (!(buy || sell) ? DoubleToString(data[0, 1]) : " "));
               stop = IsStopped();
              }
           }
      
      

      全部循环迭代完毕后,清除图表上的注释字段,并关闭 EA。

         Comment("");
         ExpertRemove();
        }
      
      

      完整的 EA 代码可在附件中找到。

      智能交易系统的操作结果就是,我们有了 AE_latent.csv 文件,其中包含自动编码器潜在状态的数据,和相应形态的前两个主成分。 这两个图形采用来自文件中的数据进行构造。

      自动编码器潜在状态的可视化 两个最先主成分的可视化

      如您所见,所示的图形都没有将形态明确划分到所期望的分组。 然而,自动编码器在两个数轴上的延迟数据都接近 0.5。 这次我们采用希格玛作为潜在状态神经层的激活函数。 该函数始终返回从 0 到 1 范围内的数值。 因此,所得到的分布中心与函数值范围的中心接近。

      采用主成分分析方法进行数据压缩可提供相当大的值。 沿轴的值相差 6-7 倍。 其分布中心大致在 [18000, 130000]。 该范围还具有明显的线性上限和下限。

      基于针对所呈现图形的分析,在数据输入到制定决策的神经网络之前,我会选择一个自动编码器进行数据预处理。


      结束语

      在本文中,我们熟悉了广泛用于解决各种问题的自动编码器。 我们采用全连接层构建了第一个自动编码器,并将其性能与主成分分析进行了比较。 测试结果表明,在求解非线性问题时,采用自编码器更具有优势。 但自动编码器的主题非常广泛,无法在一篇文章里容纳。 在下一篇文章中,我会提议研究各种启发式方法,从而提高自动编码器的效率。

      我很乐意在文章的论坛帖子中回答您的所有问题。


      参考文献列表

      1. 神经网络变得轻松(第十四部分):数据聚类
      2. 神经网络变得轻松(第十五部分):利用 MQL5 进行数据聚类
      3. 神经网络变得轻松(第十六部分):聚类运用实践
      4. 神经网络变得轻松(第十七部分):降低维度
      5. 神经网络变得轻松(第十八部分):关联规则
      6. 神经网络变得轻松(第十九部分):使用 MQL5 的关联规则


      本文中用到的程序

      # 发行 类型 说明
      1 ae.mq5 智能交易系统   自动编码器学习智能系统 
      2 ae2.mq5 EA 准备可视化数据的 EA 
      2 NeuroNet.mqh 类库 用于创建神经网络的类库
      3 NeuroNet.cl 代码库 OpenCL 程序代码库


      本文由MetaQuotes Ltd译自俄文
      原文地址: https://www.mql5.com/ru/articles/11172

      附加的文件 |
      MQL5.zip (67.49 KB)
      DoEasy. 控件 (第 11 部分): WinForms 对象 — 群组,CheckedListBox WinForms 对象 DoEasy. 控件 (第 11 部分): WinForms 对象 — 群组,CheckedListBox WinForms 对象
      本文将讨论 WinForms 对象群组,及创建 CheckBox 对象列表对象。
      从头开始开发智能交易系统(第 23 部分):新订单系统 (VI) 从头开始开发智能交易系统(第 23 部分):新订单系统 (VI)
      我们将会令订单系统更加灵活。 在此,我们将研究代码的修改,令其更加灵活,而这也让我们能够更快地修改持仓破位价。
      神经网络实验(第 2 部分):智能神经网络优化 神经网络实验(第 2 部分):智能神经网络优化
      在本文中,我将利用实验和非标准方法开发一个可盈利的交易系统,并验证神经网络是否对交易者有任何帮助。 若在交易中运用神经网络的话, MetaTrader 5 完全可作为一款自给自足的工具。
      DoEasy. 控件 (第 10 部分): WinForms 对象 — 动画界面 DoEasy. 控件 (第 10 部分): WinForms 对象 — 动画界面
      现在是时候实现动画图形界面功能,方便用户与对象的交互了。 为了让更复杂的对象能正确工作,还需要新功能。