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

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

MetaTrader 5交易系统 | 24 十月 2022, 09:56
1 465 0
Dmitriy Gizlyk
Dmitriy Gizlyk

内容


    概述

    我们继续研究无监督学习方法。 在上一篇文章中,我们熟悉了自动编码器。 自动编码器的主题很广泛,在一篇文章中无法容纳得下。 我愿意继续这个话题,并向您介绍一种自动编码器的修改版 — 变分自动编码器。


    1. 变分自动编码器的架构

    在继续研究变分自动编码器的体系结构之前,我们回到我们上一篇文章中发掘出的要点。

    • 自动编码器是经由反向传播方法训练的一种神经网络。
    • 任何自动编码器都由编码器和解码器模块构成。
    • 编码器源数据层和解码器结果层包含相同数量的元素。
    • 编码器和解码器由潜伏状态的“瓶颈”连接,该“瓶颈”包含有关初始状态的压缩信息。


    在训练过程中,我们的目标是解码器的潜伏状态解码结果与原始数据之间的最大相似性。 在这种情况下,我们可以断言有关原始数据的最大信息已基于潜伏状态进行了加密。 而且这些数据有足够的概率能恢复原始数据。 但自动编码器的使用范围更广泛,远不止数据压缩问题。

    现在,我们将应付自动编码器生成图像时发现的问题。 我们的初始数据由某个云表示。 在训练期间,我们的模型学会了完美地恢复 2 个随机选择的对象 AB。 简单推出,编码器和解码器同意为处于潜伏状态的对象 A 指定 1,为对象 B 指定 5。 在解决数据压缩问题时,这没有什么坏处。 与之对比,对象最好可以分离,且模型可以恢复它们。

    数据云

    但当研究人员尝试利用自动编码器生成图像时,2 个物体之间潜伏状态值的缺口可证明这是一个问题。 实验表明,当潜伏状态值在靠近对象的区域从对象 A 演变为对象 B 时,解码器会恢复指示的对象,但会出现一些失真。 但是在间隔的中间,解码器生成了一些非原始数据特有的东西。

    换句话说,自动编码器的潜伏状态,即其原始数据被编码和压缩,可以是非连续的,亦或可以允许一些插值。 这是自动编码器在应用中,生成某些数据时出现的基本问题。

    我们当然不会继续生成任何数据。 但不要忘记世界在不断演变。 在研究市场状况的过程中,将来从训练集合中提炼出拥有数学准确性的形态的概率极其渺茫。 但是,我们想得到的是拥有正确处理市场状况,并产生足够结果的模型。 故此,我们需要为我们的应用领域以及生成模型找出这个问题的解决方案。

    对于这个问题没有简单的解决方案。 训练样本的增加,和各种潜伏状态正则化方法的运用,都会导致问题伸缩。 例如,通过应用正则化,我们减少了对象潜伏状态向量之间的距离。 我们可以说,对于我们的例子,这些是数字 12。 但可能会出现一个编码为 1.5 的对象。 这会令我们的解码器感到迷惑。 越靠近重叠点,就越难以分离对象。

    增加训练样本具有类似的效果,因为每个状态都是离散的。 甚至,训练样本的增加会导致花费在训练上的时间和资源增加。 与此同时,自动编码器尝试选择源数据的每个独立形态,将努力把最近邻域状态的距离最大化。

    与我们的模型不同,我们知道我们的每个离散状态都是对象的某一类代表。 在我们的源数据云中,这些对象彼此靠近,并根据一定的分布规律进行分布。 我们将先验知识添加到模型中。

    数据云

    但我们如何令模型返回整个数值范围而不是单个数值呢? 请注意,该数值范围在离散值的数量及其分布上可能有所不同。 这也许会让您回想起聚类分析问题。 但是我们不知道类的具体数量。 该数字也许会因所采用的源数据样本而异。 我们需要一个更通用的数据表述模型。

    如前所述,每个类的对象在我们的源数据云中的定位会受到分布的一些影响。 可能最常用的一个是正态分布。 因此,我们假设在编码器输出端,在潜伏状态里的每个特征都与正态分布相对应。 正态分布是由两个参数判定:数学期望值和标准差。 我们寻求编码器为每个特征返回不只一个离散值,而是两个:数学期望值(平均值)和所分析源数据形态所属的分布的标准偏差。

    但无论我们如何调用编码器输出端的述值,因为解码器仍会将它们视为数字。 这就是变分自动编码器的体系结构。 在其架构中,编码器和解码器之间并未直接传输数值。 与之对比,我们从编码器中获取分布参数,从指定的分布中采样一个随机值,并将其输入到解码器之中。 因此,经由编码器处理相同的源数据形态其结果就是,解码器输入也许具有不同的数值向量,但它总是遵从于相同的正态分布。


    如您所见,如此这般操作的结果就是,解码器的输入值将始终比编码器的输出少 2 倍。

    但在此,我们面临着模型训练的问题。 训练模型采用反向传播方法。 这种方法的主要需求之一是沿误差梯度路径的所有函数的可辨性。 不幸的是,对于随机数生成器来说,情况并非如此。

    但这个问题也被解决了。 我们仔细看看正态分布的属性,以及描述它的参数。 正态分布是以数学期望点为中心的数学概率分布。 68% 的值与分布中心之间的距离不超过标准偏差。 因此,数学期望的变化会偏移分布中心。 而更改标准偏差尺度时,数值分布会围绕中心。

    因此,为了从拥有给定参数的正态分布中获取单个值,我们能够生成一个数值,对应数学期望值为 “0”,且标准偏差为 “1 ”的标准正态分布。 然后将结果值乘以给定的标准偏差,并添加到给定的数学期望值之中。 此方式被称为重新参数化技巧

    重新参数化技巧

    结果就是,我们按照正向验算的标准正态分布生成一个随机值,并保存它。 然后,我们将具有指定参数的校正向量输入到解码器之中。 在反向传播验算中,我们通过加法和乘法运算轻松地将误差梯度传递给编码器,这很容易区分。 我们的模型中未用到非微分的随机值生成器。

    看起来我们已经把拼图放在一起,绕过了所有的陷阱。 但实际验证表明,该模型并不会遵照我们的新规则玩游戏。 取而采用新输入学习更复杂的规则 ,自动编码器在学习过程中将标准偏差特征降低到 0。 乘以 0,我们的随机变量就没有了影响,解码器接收数学期望的离散值作为输入。 通过将标准偏差特征降低到 0,该模型抵消了上述所有影响,并回到编码器和解码器之间交换离散值。

    为了令模型遵照我们的规则工作,我们需要引入额外的规则和限制。 首先,我们向模型指出,数学期望值和标准偏差特征应尽可能与标准正态分布的参数相对应。 我们可以通过添加额外的偏差惩罚来实现这一点。 库尔巴克 — 莱布勒(Kullback–Leibler)背离被选为这种偏差的衡量标准。 我们现在不会深入探讨数学计算。 因此,这是经验值偏离正态分布参数的误差结果。 我们将采用此函数来正则化潜伏状态值。 在实践中,我们会将其数值添加到潜伏状态误差之中。

    标准分布的库尔巴克 — 莱布勒(Kullback–Leibler)背离

    因此,每次当特征参数偏离参考(在本例中为标准分布)时会惩罚模型,我们将强制模型引导每个特征的分布参数更接近标准分布的参数(数学期望值为 0,标准偏差为 1)。

    于此必须说,在编码器输出端的这样特征“拉取”,将与主要问题背道而驰 — 提取单个对象的特征。 添加的正则化将以相同的力度将所有特征拉向参考值。 即,它将尝试令参数相同。 与此同时,解码器误差梯度将尽可能地尝试分离出不同对象的特征。 已执行的 2 项任务之间显然存在利益冲突。 故此,模型必须在解决问题时找到平衡。 但这种平衡并不总是符合我们的期望。 为了控制这个均衡点,我们将在模型中引入一个额外的超参数。 它将控制库尔巴克-莱布勒背离对整体结果的影响。


    2. 实现

    在研究了变分自动编码器算法的理论层面之后,我们就可进入实施部分。 为了实现变分自动编码器的编码器和解码器,我们将再次取用之前创建的函数库中的完全连接神经层。 为了实现一个成熟的变分自动编码器,我们需要一个操控潜伏状态的模块。 在此模块中,我们将实现变分自动编码器的上述所有创新。

    为了在我们的函数库中保留组织神经网络的常用方法,我们将整个潜伏状态处理算法包装在单独的神经层 CVAE 之中。 在继续实现该类之前,我们创建内核来实现设备端 OpenCL 的功能。

    我们从前馈内核开始。 我们层参数中输入潜伏状态特征的正态分布描述。 有一点要告诫。 数学期望可以取任何数值。 但标准偏差只能取非负值。 如果我们生成参数所用的神经层不同,我们就要采用不同的神经元激活函数。 但我们的函数库架构只允许创建线性模型。 与此同时,一个神经层中只能使用一个激活函数。

    再次,模型不关心数值如何调用。 它只简单地执行数学公式。 这对我们来说很重要,因为它能够正确构建模型。 请注意上面的库尔巴克-莱布勒背离公式。 它采用方差及其对数。 分布的方差等于标准偏差的平方,且也许只能是非负数。 它的对数可以取正值和负值。 看一下平方参数的自然对数图:横坐标线与函数图的交点正好在 1 处。 该值是标准偏差的目标。 甚而,对于从 -1 到 1 的函数值区间,函数参数采用从 0.6 到 1.6 的值,这样就满足了我们对标准偏差的期望。

    自然对数 x^2

    因此,我们将指示模型编码器输出数学期望值和分布方差的自然对数。 我们可用双曲切线作为神经层的激活函数,因为它的数值范围即满足了我们对分布的数学期望,亦满足了其方差对数的期望。

    故此,该概念性方法很清晰了。 现在,我们进入到函数编程。 我们将从前馈内核 VAE_FeedForward 开始。 该内核在参数中接收指向三个数据缓冲区的指针。 其中两个包含原始数据,一个是结果缓冲区。 至于 OpenCL 方面,没有伪随机数生成器。 因此,我们将在主程序的端针对标准分布元素进行采样。 然后,我们把它们经由 "random" 缓冲区传递到前馈内核。

    第二个源数据缓冲区将包含编码器的结果。 正如您可能已经猜到的那样,数学期望向量和方差对数向量将包含在同一缓冲区 "inputs" 当中。

    现在,我们只需要在内核主体中实现重新参数化技巧。 不要忘记,编码器提供的是离散的对数,取代标准偏差。 因此,在运用技巧之前,我们需要获得标准偏差值。

    自然对数的逆函数是指数函数。 我们可以由此函数找到方差。 通过提取方差的平方根,我们得到标准偏差。 或者,使用幂的性质,我们可以简单地取方差对数的一半指数,这也能给出我们标准差。

    幂的性质

    在前馈内核的主体中,我们首先判定当前线程的标识符,和正在运行的线程总数,这些都将用作源缓冲区和结果缓冲区中指向所需单元格的指针。 然后依据从方差对数获得的标准偏差执行重新参数化技巧。 将结果写入结果缓冲区的相应元素,并退出内核。

    __kernel void VAE_FeedForward(__global float* inputs,
                                  __global float* random,
                                  __global float* outputs
                                 )
      {
       uint i = (uint)get_global_id(0);
       uint total = (uint)get_global_size(0);
       outputs[i] = inputs[i] + exp(0.5f * inputs[i + total]) * random[i];
      }
    
    

    因此,前馈内核算法非常简单。 接下来,我们继续在 OpenCL 关联端组织反向传播传递。 我们的变分自动编码器的潜伏状态层将不包含可训练的参数。 故此,整个反向传播过程将由规划的从解码器到编码器的误差梯度转移组成。 这将在 VAE_CalcHiddenGradient 内核中实现。

    当实现该内核时,请记住,在前馈验算期间,我们从编码器结果向量中获取两个元素,并且在重新参数化技巧之后,将一个特征作为输入传递到解码器中。 因此,我们必须从解码器中获取一个误差梯度,并将其分派给两个相应的编码器元素。

    好吧,对于数学期望,一切都很简单(当添加时,误差梯度完全转移到这两处)。 但对于方差对数,我们正在应对复函数的导数。

    方差对数的导数

    但就像硬币也有另一面。 为了令模型遵照我们的规则操作,我们引入了库尔巴克-莱布勒背离。 现在,我们将把分布参数偏差与标准分布参考值的误差梯度添加到从解码器接收的误差梯度中。

    我们看一下 VAE_CalcHiddenGradient 内核的实现。 内核在参数中接收指向四个数据缓冲区和一个常量的指针。 接收的三个缓冲区携带原始信息,一个缓冲区用于记录梯度的结果,并将其传输到编码器层级。

    • inputs 是编码器前馈的结果。 缓冲区包含数学期望值和特征方差的对数。
    • random — 前馈验算中使用的标准偏差元素的值
    • gradient — 从解码器接收的误差梯度
    • inp_grad — 传递给编码器的误差梯度写入到结果缓冲区
    • kld_mult — 库尔巴克-莱布勒背离对总结果的影响系数的离散值

    在内核主体中,我们首先判定当前线程的序列号,以及正在内核运行的线程总数。 这些值都作为指针,指向输入缓冲区和结果缓冲区的所需元素。

    接下来,判定库尔巴克-莱布勒背离值。 请注意,我们努力将经验值分布和参考分布之间的距离最小化,即尽量将其减至 0。 请注意,我们努力将经验分布和参考分布之间的距离最小化,即将其减小到0。 为了剔除不必要的操作,简单地删除判定偏差的公式前面的减号即可。 通过背离对结果的影响系数来调整该值。

    接下来,我们将误差梯度传递到编码器层级。 在此,根据上述函数的导数,我们将传递每个分布参数的两个梯度之和。

    __kernel void VAE_CalcHiddenGradient(__global float* inputs,
                                         __global float* inp_grad,
                                         __global float* random,
                                         __global float* gradient,
                                         const float kld_mult
                                        )
      {
       uint i = (uint)get_global_id(0);
       uint total = (uint)get_global_size(0);
       float kld = kld_mult * 0.5f * (inputs[i + total] - exp(inputs[i + total]) - pow(inputs[i], 2.0f) + 1);
       inp_grad[i] = gradient[i] + kld * inputs[i];
       inp_grad[i + total] = 0.5f * (gradient[i] * random[i] * exp(0.5f * inputs[i + total]) -
                                     kld * (1 - exp(inputs[i + total]))) ;
      }
    
    

    我们采用 OpenCL 程序完成操作,然后继续在主程序端实现功能。 我们首先创建一个新的神经层类 CVAE,该类派生自神经层基类 CNeuronBaseOCL。

    在该类中,我们添加一个变量 m_fKLD_Mult 来存储库尔巴克-莱布勒对整体结果的影响系数,并添加 SetKLDMult 方法来指定它。 我们还创建了一个额外的缓冲区 m_cRandom 来写入标准偏差的随机值。 这些值将利用统计和数学运算的标准库 "Math\Stat\Normal.mqh"“ 进行采样。

    此外,为了实现我们的功能,我们将覆盖前馈和反向传播方法。 此外,我们还将覆盖处理文件的方法。

    class CVAE : public CNeuronBaseOCL
      {
    protected:
       float             m_fKLD_Mult;
       CBufferDouble*    m_cRandom;
    
       virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL); 
       virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) { return true; } 
    
    public:
                         CVAE();
                        ~CVAE();
       virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                              uint numNeurons, ENUM_OPTIMIZATION optimization_type, uint batch);
       //---
       virtual void      SetKLDMult(float value) { m_fKLD_Mult = value;}
       virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);      
       //---
       virtual bool      Save(int const file_handle);
       virtual bool      Load(int const file_handle);
       //---
       virtual int       Type(void)        const                      {  return defNeuronVAEOCL; }
      };
    
    

    类的构造函数和析构函数非常简单。 在第一个当中,我们只需为新变量设置初始值,并初始化数据缓冲区实例,以便操控一连串随机变量。

    CVAE::CVAE()   : m_fKLD_Mult(0.01f)
      {
       m_cRandom = new CBufferDouble();
      }
    
    

    在类的析构函数当中,我们删除在构造函数中创建的缓冲区对象。

    CVAE::~CVAE()
      {
       if(!!m_cRandom)
          delete m_cRandom;
      }
    
    

    类实例初始化方法并不复杂。 实际上,几乎所有的对象初始化功能都是由父类的方法实现的。 它实现了初始化继承对象所需的所有控制和功能。 因此,我们只在变分编码器类方法中调用父类方法即可。 成功执行后,初始化缓冲区,以便操控随机序列。 我们在 OpenCL 关联环境内存中为它创建一个缓冲区。

    bool CVAE::Init(uint numOutputs,
    
                    uint myIndex,
                    COpenCLMy *open_cl,
    
                    uint numNeurons,
                    ENUM_OPTIMIZATION optimization_type,
    
                    uint batch)
      {
       if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, numNeurons, optimization_type, batch))
          return false;
    //---
       if(!m_cRandom)
         {
          m_cRandom = new CBufferDouble();
          if(!m_cRandom)
             return false;
         }
       if(!m_cRandom.BufferInit(numNeurons, 0.0))
          return false;
       if(!m_cRandom.BufferCreate(OpenCL))
          return false;
    //---
       return true;
      }
    
    

    我们从前馈验算 CVAE::feedForward 开始实现类的主要功能。 与其它神经层方法类似,该方法在参数中接收指向前一个神经层对象的指针。 后跟一个控制模块。 它主要检查指向所用对象指针的有效性。 之后,检查所接收初始数据的大小。 前一层结果缓冲区中的元素数量必须是 2 的倍数,并且必须比正在创建的神经层的结果缓冲区大两倍。 变分自动编码器的体系结构需要这种严格的对应关系。 编码器应为每个特征返回两个值,分别描述每个特征分布的数学期望值和标准偏差。

    bool CVAE::feedForward(CNeuronBaseOCL *NeuronOCL)
      {
       if(!OpenCL || !NeuronOCL || !m_cRandom)
          return false;
       if(NeuronOCL.Neurons() % 2 != 0 ||
          NeuronOCL.Neurons() / 2 != Neurons())
          return false;
    
    

    成功检查后,实现标准偏差的随机值进行采样,并将其值传输到相应的缓冲区。

       double random[];
       if(!MathRandomNormal(0, 1, m_cRandom.Total(), random))
          return false;
       if(!m_cRandom.AssignArray(random))
          return false;
       if(!m_cRandom.BufferWrite())
          return false;
    
    

    将生成的数值传递到 OpenCL 关联环境内存,以便进一步处理。

    接下来,我们实现调用相应的内核。 首先,我们传递内核所要用到的数据缓冲区指针。 请注意,我们只将生成的事例缓冲区传递到关联内存。 我们期望所有其它用到的数据缓冲区都已在关联内存之中。 如果您以前从未在关联内存中创建缓冲区,或者曾对主程序端的缓冲区数据进行了任何更改,那么在将缓冲区指针传递给内核参数之前,必须将数据传递到 OpenCL 关联内存。 您应该始终记住,OpenCL 程序仅在其关联内存上运行,而不会访问计算机的全局内存。 即便您所用的是运行在集成显卡或处理器的 OpenCL 库。

       if(!OpenCL.SetArgumentBuffer(def_k_VAEFeedForward, def_k_vaeff_inputs, NeuronOCL.getOutput().GetIndex()))
          return false;
       if(!OpenCL.SetArgumentBuffer(def_k_VAEFeedForward, def_k_vaeff_random, m_cRandom.GetIndex()))
          return false;
       if(!OpenCL.SetArgumentBuffer(def_k_VAEFeedForward, def_k_vaeff_outputd, Output.GetIndex()))
          return false;
    
    

    在方法的末尾,指定任务的维度、每个维度的偏移量,并调用该方法将内核排队等待执行。

       uint off_set[] = {0};
       uint NDrange[] = {Neurons()};
       if(!OpenCL.Execute(def_k_VAEFeedForward, 1, off_set, NDrange))
          return false;
    //---
       return true;
      }
    
    

    不要忘记在每个步骤中都要检查结果。 

    成功完成操作后,以 true 退出该方法。

    前馈验算之后是反向传播。 以前,我们曾按照多种方法实现了后向验算。 首先,我们调用 calcOutputGradientscalcHiddenGradients 和 calcInputGradients 来实现误差梯度的计算,以及从神经输出层到输入数据层,贯穿整个模型来传递这些误差梯度。 然后,我们调用 updateInputWeights  将更改训练的参数朝向反梯度。

    我们的神经层是为了操控变分自动编码器潜伏层,故不包含可训练参数。 因此,我们将用存根覆盖最后一种参数优化方法,该存根在每次调用该方法时始终返回 true

    实际上,对于类中反向验算过程的正常实现,我们只应重新定义 calcInputGradients 方法。 尽管在功能上,正向和反向验算方法都具有逆数据流方向,但这些方法的内容非常相似。 这是因为算法的功能是在 OpenCL 关联站点上实现的。 在主程序一端,我们只是在做调用内核的准备工作。 它们将根据单个模板进行调用。

    与前馈方法一样,我们首先检查指向正在使用对象指针的有效性。 我们不会将数据重新传递到 OpenCL 关联内存。 但如果您还不确定所有必要的信息是否都在关联内存之中,则最好立即将其再次传递到 OpenCL 关联内存。 之后,我们就可以将参数传递给内核。

    参数传输成功后,有一个操作模块可启动内核执行。 首先,设置问题的大小,以及沿每个维度的偏移量。 然后调用将内核放入执行队列的方法。

    bool CVAE::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
      {
       if(!OpenCL || !NeuronOCL)
          return false;
    //---
       if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_input,
                                                            NeuronOCL.getOutput().GetIndex()))
          return false;
       if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_inp_grad,
                                                            NeuronOCL.getGradient().GetIndex()))
          return false;
       if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_random, Weights.GetIndex()))
          return false;
       if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_gradient, Gradient.GetIndex()))
          return false;
       if(!OpenCL.SetArgument(def_k_VAECalcHiddenGradient, def_k_vaehg_kld_mult, m_fKLD_Mult))
          return false;
       int off_set[] = {0};
       int NDrange[] = {Neurons()};
       if(!OpenCL.Execute(def_k_VAECalcHiddenGradient, 1, off_set, NDrange))
          return false;
    //---
       return true;
      }
    
    

    我们检查所有操作的结果,并退出该方法。

    该类的主要功能的实现至此完毕。 但还有另一个重要的功能 — 文件操作。 因此,我们将为该类补充这些功能的方法。 在继续编写类方法之前,我们研究一下需要保存哪些信息才能成功恢复模型性能。 在该类中,我们只创建了一个变量和一个数据缓冲区。 缓冲区内容在每次正向验算时用随机值填充。 因此,无需替我们保存此数据。 变量的值是一个超参数,我们需要保存它。

    因此,我们的对象保存方法将仅包含 2 个操作:

    • 调用父类中的类似方法,执行所有必要的控制,并保存继承的对象
    • 保存库尔巴克-莱布勒背离对整体结果影响的超参数。

    bool CVAE::Save(const int file_handle)
      {
    //---
       if(!CNeuronBaseOCL::Save(file_handle))
          return false;
       if(FileWriteFloat(file_handle, m_fKLD_Mult) < sizeof(m_fKLD_Mult))
          return false;
    //---
       return true;
      }
    
    

    不要忘记检查操作结果。 成功完成所有操作后,以 true 结果退出该方法。

    为了恢复模型性能,严格按照数据写入顺序,从文件中读取保存的数据。 我们首先调用父类中的一个类似方法。 它应包含所有必要的控制,并加载继承的对象。

    bool CVAE::Load(const int file_handle)
      {
       if(!CNeuronBaseOCL::Load(file_handle))
          return false;
       m_fKLD_Mult=FileReadFloat(file_handle);
    
    

    成功执行父类方法后,从文件中读取超参数值,并将其值写入相应的变量。 但与数据保存方法不同,数据加载方法并未在此处结束。 没错,文件中没有加载更多信息到该类中。 但若要为其规划正确的操作,我们需要初始化缓冲区,以便操控正确大小的随机变量。 创建一个缓冲区,其大小等于当前加载的神经层结果缓冲区(它由父类方法加载)。 还要在 OpenCL 关联内存中创建相关的缓冲区。

       if(!m_cRandom)
         {
          m_cRandom = new CBufferDouble();
          if(!m_cRandom)
             return false;
         }
       if(!m_cRandom.BufferInit(Neurons(), 0.0))
          return false;
       if(!m_cRandom.BufferCreate(OpenCL))
          return false;
    //---
       return true;
      }
    
    

    成功完成所有操作后,以 true 结果退出该方法。

    变分自动编码器潜伏状态处理类至此完毕。 下面的附件中提供了所有方法和类的完整代码。

    我们的新类已准备就绪。 但我们规划神经网络操作的调度类仍然对此一无所知。 如此,请转至 NeuroNet.mqh,并找到 CNet 类。

    首先,转到类构造函数,并定义创建新神经层的过程。 另外,增加所用 OpenCL 内核的数量,并声明两个新内核。

    CNet::CNet(CArrayObj *Description)  :  recentAverageError(0),
                                           backPropCount(0)
      {
      .................
      .................
    //---
       for(int i = 0; i < total; i++)
         {
      .................
      .................
          if(CheckPointer(opencl) != POINTER_INVALID)
            {
             CNeuronBaseOCL *neuron_ocl = NULL;
             CNeuronConvOCL *neuron_conv_ocl = NULL;
             CNeuronProofOCL *neuron_proof_ocl = NULL;
             CNeuronAttentionOCL *neuron_attention_ocl = NULL;
             CNeuronMLMHAttentionOCL *neuron_mlattention_ocl = NULL;
             CNeuronDropoutOCL *dropout = NULL;
             CNeuronBatchNormOCL *batch = NULL;
             CVAE *vae = NULL;
             switch(desc.type)
               {
      .................
      .................
                //---
                case defNeuronVAEOCL:
                   vae = new CVAE();
                   if(!vae)
                     {
                      delete temp;
                      return;
                     }
                   if(!vae.Init(outputs, 0, opencl, desc.count, desc.optimization, desc.batch))
                     {
                      delete vae;
                      delete temp;
                      return;
                     }
                   if(!temp.Add(vae))
                     {
                      delete vae;
                      delete temp;
                      return;
                     }
                   vae = NULL;
                   break;
                default:
                   return;
                   break;
               }
            }
          else
             for(int n = 0; n < neurons; n++)
               {
      .................
      .................
               }
          if(!layers.Add(temp))
            {
             delete temp;
             delete layers;
             return;
            }
         }
    //---
       if(CheckPointer(opencl) == POINTER_INVALID)
          return;
    //--- create kernels
       opencl.SetKernelsCount(32);
      .................
      .................
       opencl.KernelCreate(def_k_VAEFeedForward, "VAE_FeedForward");
       opencl.KernelCreate(def_k_VAECalcHiddenGradient, "VAE_CalcHiddenGradient");
    //---
       return;
      }
    
    

    针对模型加载方法 CNet::Load 实现类似的修改。 我不再重复本文中的代码。 文后附件中提供了整个代码。

    接下来,在 CLayer::CreateElement 和 CLayer::Load 方法中添加指向新类的指针。

    最后,将新的类指针添加到基准神经层 CNeuronBaseOCL FeedForward calcHiddenGradientsUpdateInputWeights 方法当中。

    在完成了所有必要的补充之后,我们就可开始实现和测试模型。 附件中提供了所有类及其方法的完整代码。


    3. 测试

    为了测试变分自动编码器的操作,我们将取用之前文章中的模型。 将其保存为新文件“ "vae.mq5"。 在该模型中,编码器在第 5 个神经层上返回 2 个值。 为了正确规划变分自动编码器的操作,我将编码器输出端的层大小增加到 4 个神经元。 我还插入了新神经层,将变分自动编码器的潜伏状态作为第 6 个神经元。 该模型基于 EURUSD 数据,和 H1 时间帧进行了训练,未更改参数。 模型训练的时间区间取过去 15 年。 多层和变分自动编码器的学习动态的比较图如下图所示。

    比较学习结果 

    如您所见,根据模型训练的结果,变分自动编码器在整个训练期间显示的数据恢复误差显著降低。 此外,变分自动编码器展示出更高的误差降低态势。

    基于测试结果,我们可以得出结论,以 EURUSD 价格动态为例,解决提取时间序列特征的问题,变分自动编码器在提取单个形态描述特征方面具有巨大的潜力。


    结束语

    在本文中,我们熟悉了 变分自动编码器算法。 我们构建了一个类来实现变分自动编码器算法。 我们还依据真实历史数据,针对变分自动编码器模型进行了训练测试。 测试结果证明,针对旨在提取描述市场状况的单个特征的模型,经该变分自动编码器初步训练后,与测试结果具有一致性。 这种训练的结果可用于创建交易形态,亦可运用监督学习方法进一步训练。


    参考文献列表

    1. 神经网络变得轻松(第十四部分):数据聚类
    2. 神经网络变得轻松(第十五部分):利用 MQL5 进行数据聚类
    3. 神经网络变得轻松(第十六部分):聚类运用实践
    4. 神经网络变得轻松(第十七部分):降低维度
    5. 神经网络变得轻松(第十八部分):关联规则
    6. 神经网络变得轻松(第十九部分):使用 MQL5 的关联规则
    7. 神经网络变得轻松(第二十部分):自动编码器
    8. 变分自动编码器教程
    9. 直观理解变分自动编码器
    10. 教程 — 什么是变分自动编码器?



    本文中用到的程序

    # 名称 类型 说明
    1 vae.mq5 EA   变分自动编码器学习智能系统
    2 vae2.mq5 EA 准备可视化数据的 EA 
    3 VAE.mqh 类库 变分自动编码器潜伏层类库
    4 NeuroNet.mqh 类库 用于创建神经网络的类库
    5 NeuroNet.cl 代码库 OpenCL 程序代码库


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

    附加的文件 |
    MQL5.zip (68.61 KB)
    从头开始开发智能交易系统(第 24 部分):提供系统健壮性(I) 从头开始开发智能交易系统(第 24 部分):提供系统健壮性(I)
    在本文中,我们将令系统更加可靠,来确保健壮和安全的使用。 实现所需健壮性的途径之一是尝试尽可能多地重用代码,从而能在不同情况下不断对其进行测试。 但这只是其中一种方式。 另一个是采用 OOP。
    学习如何基于标准偏差设计交易系统 学习如何基于标准偏差设计交易系统
    此为我们该系列中的一篇新文章,介绍如何利用 MetaTrader 5 交易平台中最受欢迎的技术指标来设计交易系统。 在这篇新文章中,我们将学习如何运用标准偏差指标设计交易系统。
    DoEasy. 控件 (第 12 部分): 基准列表对象、ListBox 和 ButtonListBox WinForms 对象 DoEasy. 控件 (第 12 部分): 基准列表对象、ListBox 和 ButtonListBox WinForms 对象
    在本文中,我将继续创建 WinForms 对象列表的基准对象,以及两个新对象:ListBox 和 ButtonListBox。
    神经网络实验(第 2 部分):智能神经网络优化 神经网络实验(第 2 部分):智能神经网络优化
    在本文中,我将利用实验和非标准方法开发一个可盈利的交易系统,并验证神经网络是否对交易者有任何帮助。 若在交易中运用神经网络的话, MetaTrader 5 完全可作为一款自给自足的工具。