English Русский Español Deutsch 日本語 Português
preview
交易中的神经网络:定向扩散模型(DDM)

交易中的神经网络:定向扩散模型(DDM)

MetaTrader 5交易系统 |
29 0
Dmitriy Gizlyk
Dmitriy Gizlyk

概述

利用扩散模型进行无监督表征学习,已成为计算机视觉的一个关键研究领域。来自不同研究人员的实验结果证实了扩散模型在学习有意义的视觉表征方面的有效性。因不同等级噪声而扭曲的数据重造,为模型掌握复杂的视觉概念提供了合适的基础。甚至,在训练期间优先参考某些噪声等级,而非其它因素,已被证明可以提升扩散模型的性能。

论文《图形表征学习之定向扩散模型》的作者提议运用扩散模型进行无监督图形表征学习。然而,他们在实践中遇到了“雏形”扩散模型的局限性。他们的实验揭示,图形结构中的数据往往展现出明显的各向异性和方向性形态,而在图像数据中却不太明显。传统的扩散模型依赖于各向同性正向扩散过程,往往会遭遇内部信噪比(SNR)的快速下降,这令它们在捕获各向异性结构方面效率较低。为了解决这个问题,作者引入了能够有效捕获这般定向结构的新方式。其中包括定向扩散模型,其可缓解信噪比极速恶化的问题。所拟议框架将数据相关和方向偏置噪声纳入正向扩散过程。去噪模型产生的中间激活有效地捕获到对于下游任务至关重要的有价值的语义和拓扑信息。

如是结果,定向扩散模型为图形表征学习提供了一种有前途的生成方式。作者的实验结果表明,这些模型对比学习和传统生成方法更出彩。值得注意的是,对于图形分类任务,定向扩散模型甚至超过了基线监督学习模型,凸显了基于扩散的方法在图形表征学习中的巨大潜力。

在交易境况下应用扩散模型为强化市场数据的表征和分析开辟了新的可能性。特别是定向扩散模型,由于它们具备解释各向异性数据结构的能力,或许特别实用。金融市场通常以不对称和方向性走势为特征,结合方向性噪声的模型能更有效地识别趋势及调整阶段的结构性形态。该能力可以识别隐藏的依赖关系、和季节性趋势。


1. DDM 算法

在图形和图像之间看到的数据存在显著的结构差异。在雏形前向正向扩散过程中,各向同性高斯噪声被迭代添加到原始数据之中,直到数据完全转化为白噪声。当数据遵循各向同性分布时,这种方式是合适的,在于它会逐渐将数据点降级为噪声,同时跨越宽泛信噪比(SNR)生成噪声样本。然而,对于各向异性数据分布,添加各向同性噪声会迅速破坏底层结构,导致信噪比迅速下降到零。

如是结果,降噪模型无法学习有意义、且具区分性的特征表示,而这些能有效地用于下游任务。对比之下,定向扩散模型(DDM)结合了数据依赖性、和定向前向扩散过程,以较慢的速率降低信噪比。这种更渐进的退化允许提取不同信噪比等级的细粒度特征表示,从而保留有关各向异性结构的关键信息。然后,提取的信息可用于下游任务,例如图形和节点分类。

定向噪声的产生涉及经由两个附加约束,将初始各向同性高斯噪声转换为各向异性噪声。这些约束对于提高扩散模型的性能至关重要。

Gt = (A, Xt) 表示前向扩散过程第 t 步的工作状态,其中 𝐗t = {xt,1, xt,2, …, xt,N} 表示正在研究的特征。

此处,x0,i 是节点 i 的原生特征向量,μ∈ℛ 和 σ∈ℛ 分别表示所有 N 个节点上特征维度 d 的平均值,和标准差张量。⊙ 表示元素级乘法。在小批量训练期间,在批次内使用图形计算 μσ。参数 ɑt 表示固定方差计划,并按递减序列 {β ∈ (0, 1)} 参数化。

相比雏形扩散过程,定向扩散模型施加了两个关键约束:一种是将与数据无关的高斯噪声转换为各向异性的、批量特异性噪声。在该约束下,噪声向量的每个坐标都被迫与实际数据中相应坐标的经验均值和标准差相匹配。这将扩散过程限制在批量局部邻域,防止过度发散,并保持局部相干性。另一个约束引入了一个角度方向,它将噪声 ε 旋转到物体 x0,i 的相同超平面,从而保留其方向属性。这有助于在整个前向扩散过程中保持数据的内在结构。

这两个约束协同工作,从而确保前向扩散过程遵循底层数据结构,并防止信号快速衰减。如是结果,信噪比衰减得更慢,允许定向扩散模型在一系列 SNR 等级上提取有意义的特征表示。通过提供更健壮、及信息更丰富的嵌入来提升下游任务的性能。

该方法作者遵循与雏形扩散模型相同的训练策略,训练一个去噪模型 fθ 来近似逆向扩散过程。然而,鉴于具有定向噪声的前向过程的逆过程不能以闭合形式表示,因此降噪模型 fθ 经训练后直接预测原始序列。

下面提供了作者提议的定向扩散模型框架的原始可视化。



2. 利用 MQL5 实现

在研究过定向扩散模型方法的理论层面之后,我们转到文章的实践部分,利用 MQL5 实现所提议方法。

我们将工作划分为两个主要部分。在第一阶段,我们把定向噪声加进正在分析的数据 ,在第二阶段,我们将在单一类结构中实现该框架。

2.1添加定向噪声


在开始之前,我们讨论一下生成定向噪声的动作算法。首先,我们需要正态分布噪声,我们可用标准 MQL5 函数库轻松获得。

接下来,遵循框架作者概括的方法,我们必须将这种各向同性噪声转换为各向异性、与数据相关的噪声。为此,我们需要计算每个特征的均值和方差。仔细观察,这类似于我们在开发批量归一化层 CNeuronBatchNormOCL 时已解决的任务。批量归一化算法将数据标准化为零均值和单位方差。不过,在移位和缩放期间,数据分布会发生变化。理论上,我们可从归一化层本身中提取这些统计信息。事实上,我们之前在开发逆归一化类 CNeuronRevINDenormOCL 时曾实现了一个程序来获取原始分布的参数。但这种方式会约束我们框架的灵活性和通用性。

为了克服这一限制,我们采取了一种更加综合的方式。我们结合了定向噪声加上数据归一化过程本身。这就浮现出一个重要问题:应该在哪个点添加噪声?

我们可在归一化之前添加噪声。但这会扭曲归一化过程本身。添加噪声会改变数据分布。因此,据先前计算的均值和方差应用归一化,将导致分布偏差。这是一个不能忍受的结果。

第二种选项是在归一化层的输出端添加噪声。在这种情况下,我们需要通过缩放和移位因子来调整高斯噪声。但如果您看看原始算法的上述公式,您能看到这种调整引入了偏差,且噪声漂向均值偏移。因此,随着偏移的增加,我们会得到偏斜的、非对称噪声。再者,这是不可取的。

在权衡利弊后,我们选择了不同的策略:我们在归一化步骤、和缩放/偏移应用之间添加噪声。该方式假设归一化数据已含有零均值和单位方差。这正是我们用来生成噪声的分布。然后,我们将噪声数据投喂到缩放和移位阶段,令模型能够学习相应的参数。

策略将会这样实现。我们可以继续工作的实践部分。该算法将在 OpenCL 端实现。为此,我们将创建一个名为 BatchFeedForwardAddNoise 的新内核。值得注意的是,这个内核的逻辑很大程度上是基于批量归一化层的前馈通验。然而,我们通过添加高斯噪声数据的缓冲区和偏差的缩放因子(表示为 ɑ)来扩展它。

__kernel void BatchFeedForwardAddNoise(__global const float *inputs, __global float *options,
                                       __global const float *noise, __global float *output,
                                       const int batch, const int optimization, 
                                       const int activation, const float alpha)
  {
   if(batch <= 1)
      return;
   int n = get_global_id(0);
   int shift = n * (optimization == 0 ? 7 : 9);

在方法主体中,我们首先检查归一化批量的大小,必须大于“1”。然后,我们基于当前线程 ID 判定数据缓冲区的偏移量。

接下来,我们检查归一化参数缓冲区是否包含实数。我们将用零值替换不正确的元素。

   for(int i = 0; i < (optimization == 0 ? 7 : 9); i++)
     {
      float opt = options[shift + i];
      if(isnan(opt) || isinf(opt))
         options[shift + i] = 0;
     }

然后,我们根据基本内核算法对原始数据进行归一化。

   float inp = inputs[n];
   float mean = (batch > 1 ? (options[shift] * ((float)batch - 1.0f) + inp) / ((float)batch) : inp);
   float delt = inp - mean;
   float variance = options[shift + 1] * ((float)batch - 1.0f) + pow(delt, 2);
   if(batch > 0)
      variance /= (float)batch;
   float nx = (variance > 0 ? delt / sqrt(variance) : 0);

在该阶段,我们获得均值和单位方差为零的归一化初始数据。于此,我们添加噪声,之前已调整过其方向。

   float noisex = sqrt(alpha) * nx + sqrt(1-alpha) * fabs(noise[n]) * sign(nx);

然后我们执行缩放和移位算法,将结果保存在相应的数据缓冲区之中,类似于供体内核的实现。但这次,我们对噪声值应用缩放和偏移。

   float gamma = options[shift + 3];
   if(gamma == 0 || isinf(gamma) || isnan(gamma))
     {
      options[shift + 3] = 1;
      gamma = 1;
     }
   float betta = options[shift + 4];
   if(isinf(betta) || isnan(betta))
     {
      options[shift + 4] = 0;
      betta = 0;
     }
//---
   options[shift] = mean;
   options[shift + 1] = variance;
   options[shift + 2] = nx;
   output[n] = Activation(gamma * noisex + betta, activation);
  }

我们已实现了前馈通验算法。反向传播通验呢?此处应注意,为了执行反向传播操作,我们决定使用批量归一化层算法的完整实现。事实上,我们并未训练噪音本身。因此,会直接传递误差梯度、以及整体到原始输入数据。我们早之前讲述的缩放因子 ɑ 只是当作稍微模糊原始数据周围的区域。由此,我们可以忽略这个因素,并完全按照标准批量归一化算法将误差梯度转发到输入。

因此,我们实现 OpenCL 端的工作已经完成。附件中提供了完整的源代码。我们现在转到 MQL5 端的实现。此处,我们将创建一个名为 CNeuronBatchNormWithNoise 的新类。顾名思义,大部分核心功能都是直接从批量归一化类继承而来的。唯一需要覆盖的方法是前馈通验。新类结构如下所示。

class CNeuronBatchNormWithNoise  :  public CNeuronBatchNormOCL
  {
protected:
   CBufferFloat      cNoise;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   
public:
                     CNeuronBatchNormWithNoise(void) {};
                    ~CNeuronBatchNormWithNoise(void) {};
   //---
   virtual int       Type(void) const   {  return defNeuronBatchNormWithNoise;    }
  };

您或许已注意到,我们尝试令新类 CNeuronBatchNormWithNoise 的开发尽可能直截了当。无论如何,为了启用所需的功能,我们需要一个缓冲区来传送噪声,其将在主端生成,并传递到 OpenCL 关联环境中。我们特意选择不重写对象初始化方法或文件方法。没有实际理由保留随机产生的噪声。取而代之,所有相关操作都在 feedForward 方法中实现。该方法接收指向输入数据对象的指针作为参数。

bool CNeuronBatchNormWithNoise::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!bTrain)
      return CNeuronBatchNormOCL::feedForward(NeuronOCL);

注意,噪音仅在训练阶段添加。这将有助于模型学习输入数据中有意义的结构。在实况运用时,我们打算模型充当过滤器,从可能固有的噪声、或不一致的真实世界数据中恢复有意义的形态。因此,现阶段未添加人工噪声。取而代之,我们通过父类功能执行标准归一化。

以下代码仅在模型训练期间执行。我们首先检查接收指针与源数据对象的相关性。

if(!OpenCL || !NeuronOCL)
   return false;

然后我们把它保存在一个内部变量之中。

PrevLayer = NeuronOCL;

之后,我们检查归一化封包的大小。如果它不大于 1,那么我们只需同步激活函数,并以正结果终止该方法。因为在这种情况下,归一化算法的结果将等于原始数据。为了消除额外的操作,我们简单地将接收到的初始数据传递到下一层。

if(iBatchSize <= 1)
  {
   activation = (ENUM_ACTIVATION)NeuronOCL.Activation();
   return true;
  }

如果上述所有检查点都成功通过,我们首先据正态分布生成噪声。

double random[];
if(!Math::MathRandomNormal(0, 1, Neurons(), random))
   return false;

之后,我们需要将其传递到 OpenCL 关联环境。但我们并未重写对象初始化方法。故此,我们首先检查我们的数据缓冲区,以确保它有足够的元素,并且之前创建的缓冲区在关联环境中。

if(cNoise.Total() != Neurons() ||
   cNoise.GetOpenCL() != OpenCL)
  {
   cNoise.BufferFree();
   if(!cNoise.AssignArray(random))
      return false;
   if(!cNoise.BufferCreate(OpenCL))
      return false;
  }

当我们在其中一个检查点获得负值时,我们更改缓冲区大小,并在 OpenCL 关联环境中创建一个新指针。

否则,我们只需将数据复制到缓冲区中,并将其移到 OpenCL 关联环境内存中即可。

else
  {
   if(!cNoise.AssignArray(random))
      return false;
   if(!cNoise.BufferWrite())
      return false;
  }

接下来,我们调整实际批量大小,并随机判定原始数据的噪声等级。

iBatchCount = MathMin(iBatchCount, iBatchSize);
float noise_alpha = float(1.0 - MathRand() / 32767.0 * 0.01);

现在我们已准备好所有必要的数据,我们只需将其传递到我们刚刚创建的内核参数。

uint global_work_offset[1] = {0};
uint global_work_size[1];
global_work_size[0] = Neurons();
int kernel = def_k_BatchFeedForwardAddNoise;
ResetLastError();
if(!OpenCL.SetArgumentBuffer(kernel, def_k_normwithnoise_inputs, NeuronOCL.getOutputIndex()))
  {
   printf("Error of set parameter kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), 
                                                                    GetLastError(), __LINE__);
   return false;
  }
if(!OpenCL.SetArgumentBuffer(kernel, def_k_normwithnoise_noise, cNoise.GetIndex()))
  {
   printf("Error of set parameter kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), 
                                                                    GetLastError(), __LINE__);
   return false;
  }
if(!OpenCL.SetArgumentBuffer(kernel, def_k_normwithnoise_options, BatchOptions.GetIndex()))
  {
   printf("Error of set parameter kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), 
                                                                    GetLastError(), __LINE__);
   return false;
  }
if(!OpenCL.SetArgumentBuffer(kernel, def_k_normwithnoise_output, Output.GetIndex()))
  {
   printf("Error of set parameter kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), 
                                                                    GetLastError(), __LINE__);
   return false;
  }
if(!OpenCL.SetArgument(kernel, def_k_normwithnoise_activation, int(activation)))
  {
   printf("Error of set parameter kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), 
                                                                    GetLastError(), __LINE__);
   return false;
  }
if(!OpenCL.SetArgument(kernel, def_k_normwithnoise_alpha, noise_alpha))
  {
   printf("Error of set parameter kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), 
                                                                    GetLastError(), __LINE__);
   return false;
  }
if(!OpenCL.SetArgument(kernel, def_k_normwithnoise_batch, iBatchCount))
  {
   printf("Error of set parameter kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), 
                                                                    GetLastError(), __LINE__);
   return false;
  }
if(!OpenCL.SetArgument(kernel, def_k_normwithnoise_optimization, int(optimization)))
  {
   printf("Error of set parameter kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), 
                                                                    GetLastError(), __LINE__);
   return false;
  }
//---
if(!OpenCL.Execute(kernel, 1, global_work_offset, global_work_size))
  {
   printf("Error of execution kernel %s: %d; line %d", OpenCL.GetKernelName(kernel), 
                                                                GetLastError(), __LINE__);
   return false;
  }
   iBatchCount++;
//---
   return true;
  }

我们将内核放入执行队列之中。我们还控制每一步的运营。在方法的末尾,我们将操作的逻辑结果返回给调用者。

我们的新类 CNeuronBatchNormWithNoise 到此完结。其完整代码在附件中提供。

2.2DDM 框架类


我们已实现了一个对象,用于向原始输入数据添加定向噪声。现在我们转到构建定向扩散模型框架的解释。

我们采用框架作者索提议方式的结构。不过,我们允许针对具体问题的背景下有一些偏差。在我们的实现中,我们还用到该方法作者提议的 U-形架构,但将图形神经网络(GNN)替换为变换器编码器模块。此外,该方法作者把已有的噪声投喂到模型之中,而我们在模型本身内添加噪声。但特事特例。

为了实现我们的方案,我们创建了一个名为 CNeuronDiffusion 的新类。作为父对象,我们使用 U-形变换器。新类结构如下所示。

class CNeuronDiffusion  : public CNeuronUShapeAttention
  {
protected:
   CNeuronBatchNormWithNoise  cAddNoise;
   CNeuronBaseOCL             cResidual;
   CNeuronRevINDenormOCL      cRevIn;
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   virtual bool      calcInputGradients(CNeuronBaseOCL *prevLayer);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);

public:
                     CNeuronDiffusion(void) {};
                    ~CNeuronDiffusion(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                          uint window, uint window_key, uint heads, uint units_count, 
                          uint layers, uint inside_bloks, 
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuronDiffusion;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau);
   virtual void      SetOpenCL(COpenCLMy *obj);
  };

在所呈现的类结构中,我们声明了三个新的静态对象,我们将在类方法的实现过程中领略它们的用途。为了构建噪声过滤模型的基本架构,我们将使用继承的对象。

所有对象都声明为静态,这允许我们将类构造函数和析构函数留空。对象的初始化在 Init 方法中执行。

在方法参数中,我们接收确定所创建对象架构的主要常量。应当说,在这种情况下,我们已完全从父类方法中转移了参数的结构,未经任何更改。

bool CNeuronDiffusion::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                            uint window, uint window_key, uint heads, 
                            uint units_count, uint layers, uint inside_bloks, 
                            ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch))
      return false;

不过,在构建新算法时,我们将稍微修改继承对象的顺序。因此,在方法主体中,我们调用了基类的相关方法,其中仅初始化主接口。

接下来,我们通过添加噪声来初始化原始输入数据的归一化对象。我们将用该对象进行输入数据初始处理。

if(!cAddNoise.Init(0, 0, OpenCL, window * units_count, iBatch, optimization))
   return false;

然后我们构建 U-形变换器结构。在此,我们首先使用多头注意力模块。

if(!cAttention[0].Init(0, 1, OpenCL, window, window_key, heads, units_count, layers, optimization, iBatch))
   return false; 

接下来是降维的卷积层。

if(!cMergeSplit[0].Init(0, 2, OpenCL, 2 * window, 2 * window, window, (units_count + 1) / 2, optimization, 
                                                                                                  iBatch))
   return false;

然后我们重复形成颈部对象。

 if(inside_bloks > 0)
   {
    CNeuronDiffusion *temp = new CNeuronDiffusion();
    if(!temp)
       return false;
    if(!temp.Init(0, 3, OpenCL, window, window_key, heads, (units_count + 1) / 2, layers, inside_bloks - 1, 
                                                                                     optimization, iBatch))
      {
       delete temp;
       return false;
      }
    cNeck = temp;
   }
 else
   {
   CNeuronConvOCL *temp = new CNeuronConvOCL();
   if(!temp)
      return false;
   if(!temp.Init(0, 3, OpenCL, window, window, window, (units_count + 1) / 2, optimization, iBatch))
     {
      delete temp;
         return false;
     }
   cNeck = temp;
     }

注意,我们稍微复杂化了模型架构。这也令模型正在解决的问题变得复杂。关键是,作为颈部对象,我们反复添加类似的定向扩散对象。这意味着每个新层都会往原始输入数据里添加噪声。因此,该模型学会工作,并从具有大量噪声的数据中恢复数据。

该方式与扩散模型的概念并不矛盾,扩散模型本质上是生成式模型。创建它们是为了从噪声中迭代生成数据。不过,也可在模型颈部使用父类对象。

接下来,我们在降噪模型中添加第二个注意力模块。

if(!cAttention[1].Init(0, 4, OpenCL, window, window_key, heads, (units_count + 1) / 2, layers, optimization,
                                                                                                     iBatch))
   return false;

我们还添加了一个卷积层,以便将维度恢复到输入数据级别。

if(!cMergeSplit[1].Init(0, 5, OpenCL, window, window, 2 * window, (units_count + 1) / 2, optimization, iBatch))
   return false;

根据 U-型变换器架构,我们用残差连接补充获得的结果。为了编写它们,我们将创建一个基本神经层。

if(!cResidual.Init(0, 6, OpenCL, Neurons(), optimization, iBatch))
   return false;
if(!cResidual.SetGradient(cMergeSplit[1].getGradient(), true))
   return false;

之后,我们同步残差连接、与维度恢复层的梯度缓冲区。

接下来,我们添加一个逆向归一化层,作者并未提到它,而是我们从方法逻辑中得出。

if(!cRevIn.Init(0, 7, OpenCL, Neurons(), 0, cAddNoise.AsObject()))
   return false;

事实上,框架的原始版本并未用到数据归一化。据信,该算法采用准备好的图形数据,皆经图形网络处理。故此,在模型的输出中,期待原始降噪数据。在训练期间,数据恢复误差最小化。在我们的方案中,我们用到了数据归一化。因此,为了比较结果与真实数值,我们需要将数据返回至原始表示。该操作由逆归一化层执行。

现在我们需要替换数据缓冲区,从而剔除不必要的复制操作,并将方法操作的逻辑结果返回给调用程序。

   if(!SetOutput(cRevIn.getOutput(), true))
      return false;
//---
   return true;
  }

不过,要注意,在这种状况下,我们只是替换了输出缓冲区指针。误差梯度缓冲区不受影响。我们将在检查反向传播算法时,再讨论这一决定的原因。

但首先,我们研究 feedForward 方法。

bool CNeuronDiffusion::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!cAddNoise.FeedForward(NeuronOCL))
      return false;

在方法参数中,我们接收一个指向输入数据对象的指针,我们立即将其传递给内部噪声添加层的同名方法。

添加的噪声被投喂到第一个注意力模块之中。

if(!cAttention[0].FeedForward(cAddNoise.AsObject()))
   return false;

之后,我们更改数据维度,并将其传递给颈部对象。

if(!cMergeSplit[0].FeedForward(cAttention[0].AsObject()))
   return false;
if(!cNeck.FeedForward(cMergeSplit[0].AsObject()))
   return false;

从颈部获得的结果被投喂到第二个注意力模块之中。

if(!cAttention[1].FeedForward(cNeck))
   return false;

之后,我们将数据维度恢复到原始维度,并将其与添加了噪声的数据相加。

if(!cMergeSplit[1].FeedForward(cAttention[1].AsObject()))
   return false;
if(!SumAndNormilize(cAddNoise.getOutput(), cMergeSplit[1].getOutput(), cResidual.getOutput(),
                                                                        1, true, 0, 0, 0, 1))
   return false;

在方法结束时,我们将数据返回至原始分布子空间。

   if(!cRevIn.FeedForward(cResidual.AsObject()))
      return false;
//---
   return true;
  }

此后,我们只需将操作执行的逻辑结果返回给调用函数即可。

我相信前馈方法的逻辑相当直白。不过,若搭配梯度传播方法 calcInputGradients,事情就变得更加复杂。这是我们必须记住的地方,即我们正在与扩散模型打交道。

bool CNeuronDiffusion::calcInputGradients(CNeuronBaseOCL *prevLayer)
  {
   if(!prevLayer)
      return false;

就像在前馈通验中一样,该方法接收指向源数据对象的指针。然而,这一次,我们需要根据输入数据对于模型输出的影响,将误差梯度传回。我们首先验证接收到的指针,否则进一步的操作将毫无意义。

我还要提醒您,在初始化期间,我们有意不去替换梯度缓冲区指针。此刻,来自下一层的误差梯度仅存在于相应的接口缓冲区当中。选择这样设计令我们能够实现我们的第二个主要目标 — 训练扩散模型。正如本文的理论章节所议,扩散模型经过训练,可从噪声中重构输入数据。因此,我们计算前向通验的输出、与原始输入数据(无噪声)之间的偏差。

float error = 1;
if(!cRevIn.calcOutputGradients(prevLayer.getOutput(), error) ||
   !SumAndNormilize(cRevIn.getGradient(), Gradient, cRevIn.getGradient(), 1, false, 0, 0, 0, 1))
   return false;

不过,我们希望配置一个能够在主要任务上下文中提取有意义结构的过滤器。因此,为了重构梯度,我们加上沿主路径接收的误差梯度,其指示主模型的预测误差。

接下来,我们将组合误差梯度向下传播到残差连接层。

if(!cResidual.calcHiddenGradients(cRevIn.AsObject()))
   return false;

在该阶段,我们用到缓冲区替换,并继续经由第二个注意力模块反向传播梯度。

if(!cAttention[1].calcHiddenGradients(cMergeSplit[1].AsObject()))
   return false;

从那里,我们继续传播误差梯度至网络的其余部分:颈部、降维层、第一个注意力模块、最后是噪声注入层。

if(!cNeck.calcHiddenGradients(cAttention[1].AsObject()))
   return false;
if(!cMergeSplit[0].calcHiddenGradients(cNeck.AsObject()))
   return false;
if(!cAttention[0].calcHiddenGradients(cMergeSplit[0].AsObject()))
   return false;
if(!cAddNoise.calcHiddenGradients(cAttention[0].AsObject()))
   return false;

此处我们需要停止,并添加残差连接误差梯度。

if(!SumAndNormilize(cAddNoise.getGradient(), cResidual.getGradient(), cAddNoise.getGradient(), 1, 
                                                                             false, 0, 0, 0, 1))
   return false;

最后,我们将梯度传播回输入层,并将操作结果返回给调用函数。

   if(!prevLayer.calcHiddenGradients(cAddNoise.AsObject()))
      return false;
//---
   return true;
  }

我们审查定向扩散框架类方法的算法实现至此完结。您可在附件中找到所有方法的完整源代码。训练和环境互动程序也包括在内,这些都是从我们之前的工作中继承下来的,未经修改。

模型架构本身也是从上一篇文章中借鉴而来。仅有的修改是环境编码器中的自适应图形表示层,已被可训练的定向扩散层取代。

//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDiffusion;
   descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.window_out = BarDescr;
   descr.layers=2;
   descr.step=3;
   {
      int temp[] = {4};                                  // Heads
      if(ArrayCopy(descr.heads, temp) < (int)temp.Size())
         return false;
   }
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

您可在附件中找到模型的完整架构。

现在,我们转入工作的最后阶段 — 采用真实世界的数据评估所实现方式的有效性。



3. 测试

我们投入了大量精力来利用 MQL5 实现定向扩散方法。现在是时候评估它们在真实交易场景中的表现了。为此,我们依据 EURUSD 的 2023 年真实数据训练我们按拟议方式构建的模型。至于训练过程,我们采用 H1 时间帧内的历史数据。

如前工作,我们采用离线训练策略,并定期更新训练数据集,以便令其与参与者的当前政策保持一致。

如前所述,新状态编码器的架构很大程度上基于我们上一篇文章中讲述的模型。为了进行公平的性能比较,我们保持新模型的测试参数与基线的测试参数相同。2024 年前三个月的评估结果如下。

在测试期间,该模型仅执行了 10 笔交易。这是一个非常低的频率。甚至,这些交易中仅有 4 笔是盈利的。不是一个令人印象深刻的结果。然而,每笔获胜交易的平均盈利、及最大盈利,大约是亏损交易的五倍。如是结果,该模型实现了 3.28 的盈利因子。

总的来说,该模型展现出良好的盈亏比,不过,有限的交易数量建议我们需要增加交易频率。理想情况下,不会影响交易品质。



结束语

定向扩散模型DDM)为在交易应用程序中分析和表示市场数据,提供了一种很有前途的工具。由于复杂的结构性关系,及外部宏观经济驱动因素,金融市场往往展现出各向异性、和方向性形态。基于各向同性过程的传统扩散模型,或许无法有效捕捉这些细微差别。另一方面,DDM 通过运用定向噪声来适配数据的方向性,即使在高噪声、高波动性环境中也能更好地识别关键形态和趋势。

在实践部分,我们利用 MQL5 实现了我们对所提议方式的愿景。我们依据真实历史市场数据训练模型,并评估它们在样本外数据上的性能。基于实验结果,我们得出结论,DDMs 展现出强大的潜力。不过,我们目前的实现仍需进一步优化。


参考

文章中所用程序

# 名称 类型 说明
1 Research.mq5 智能系统 收集样本的 EA
2 ResearchRealORL.mq5
智能系统
利用 Real ORL方法收集样本的 EA
3 Study.mq5 智能系统 模型训练 EA
4 Test.mq5 智能系统 模型测试 EA
5 Trajectory.mqh 类库 系统状态描述结构
6 NeuroNet.mqh 类库 创建神经网络的类库
7 NeuroNet.cl 函数库 OpenCL 程序代码库

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

附加的文件 |
MQL5.zip (2075.11 KB)
在Python中使用Numba对交易策略进行快速测试 在Python中使用Numba对交易策略进行快速测试
本文实现了一个快速策略测试器,它使用Numba对机器学习模型进行快速策略测试。它的速度比纯 Python 策略回测器快 50 倍。作者推荐使用该库来加速数学计算,尤其是那些涉及循环的计算。
开发回放系统(第 69 部分):取得正确的时间(二) 开发回放系统(第 69 部分):取得正确的时间(二)
今天我们将看看为什么我们需要 iSpread 功能。同时,我们将了解当没有可用的分时报价时,系统如何通知我们柱形的剩余时间。此处提供的内容仅用于教育目的。在任何情况下,除了学习和掌握所提出的概念外,都不应出于任何目的使用此应用程序。
基于交易量的神经网络分析:未来趋势的关键 基于交易量的神经网络分析:未来趋势的关键
本文探讨了通过将技术分析原理与 LSTM 神经网络架构相结合,基于交易量分析来改进价格预测准确性的可能性。文章特别关注异常交易量的检测与解读、聚类方法的使用,以及基于交易量的特征创建及其在机器学习背景下的定义。
原子轨道搜索(AOS)算法 原子轨道搜索(AOS)算法
本文探讨了原子轨道搜索(Atomic Orbital Search,AOS)算法,该算法运用原子轨道模型的概念来模拟解的搜索过程。此算法基于概率分布以及原子内相互作用的动力学原理。本文详细阐述了关于AOS算法的数学层面,包括候选解位置的更新方式,以及能量吸收与释放的机制。AOS算法通过为计算问题提供一种创新的优化方法,为将量子原理应用于计算问题开辟了新思路。