English Русский Español Deutsch 日本語 Português
preview
神经网络在交易中的应用:市场异常的自适应检测(DADA)

神经网络在交易中的应用:市场异常的自适应检测(DADA)

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

概述

随着技术的进步和流程的自动化,时间序列已成为金融市场分析不可或缺的一部分。有效检测市场数据中的异常情况,能够及时识别出潜在威胁,如价格剧烈波动、资产操纵和流动性变化等。这对于算法交易、风险管理和金融系统稳定性评估尤为重要。波动性突然飙升、交易量出现偏差或资产之间出现异常相关性,可能预示着市场失灵、投机活动,甚至市场危机。

基于深度学习的现代异常检测方法已取得显著成功,但它们也存在局限性。大多数情况下,这些方法需要对每个新数据集进行单独训练,这阻碍了它们在现实世界中的应用。金融数据瞬息万变,其历史模式并非总是重现。

主要问题之一是不同市场中的数据结构各不相同。由于异常情况很少发生,现代算法通常使用自编码器来“记忆”正常的市场行为。然而,如果一个模型保留了过多的信息,它就会开始考虑市场噪声,从而降低异常检测的准确性。相反,过度压缩可能会导致重要模式的丢失。大多数方法采用固定的压缩比,这限制了模型适应不同市场条件的能力。

另一个挑战是异常情况的多样性。许多模型仅在正常数据上进行训练,但如果不了解异常本身,就很难检测到异常。例如,价格急剧上涨在某一市场可能是异常现象,但在另一市场则可能是正常现象。在某些资产中,异常情况与突然的流动性激增相关,而在其他资产中,则与意外的相关性相关。因此,模型可能会遗漏重要信号,或者产生过多错误信号。

为了解决这些问题,论文“Towards a General Time Series Anomaly Detector with Adaptive Bottlenecks and Dual Adversarial Decoders”(迈向具有自适应瓶颈和双重对抗解码器的通用时间序列异常检测器)的作者提出了一种新的框架 DADA,该框架采用自适应信息压缩和两个独立的解码器。与传统方法不同,DADA能够灵活适应不同的数据。它没有采用固定的压缩级别,而是提供了多种选项,并为每种情况选择最合适的选项。这有助于更好地捕捉市场数据的特征,并保留重要的模式。

在模型输出端,使用了两个解码器。一个解码器用于处理正常数据,而另一个解码器则用于异常数据的重构。第一个解码器学习重构时间序列,而第二个解码器则基于异常样本进行训练。这使模型能够清晰地区分正常行为与异常行为,同时也降低了误报的可能性。


DADA 算法

时间序列是指随时间变化的数据序列。任何偏离正常数据行为的情况都可能预示着危机、故障或欺诈活动。为了有效识别此类异常,DADA 框架(具有自适应瓶颈和双对抗解码器的检测器)采用深度学习方法进行自适应时间序列分析和异常模式检测。DADA 的一个关键特性是其通用性。它无需事先针对特定领域进行适配,即可处理各种输入数据。

DADA 框架基于使用掩码进行数据重构的思想,这使其成为分析时间依赖性和识别异常情况的有效工具。这种方法不仅能让模型记住数据中的模式,还能通过重构缺失或损坏的部分来学习理解其结构。

训练过程涉及处理两种类型的序列:正常序列和异常序列。与需要手动标记异常数据的传统方法不同,DADA 框架的作者采用了一种生成式方法,即在原始时间序列中加入人工噪声。这种方法不仅通过消除人工劳动简化了数据准备过程,还使模型更具通用性。它能够学习检测各种类型的偏差:峰值、异常值、趋势变化、波动性变化以及其他模式。

在第一阶段,原始数据被划分为若干片段(块),并对这些片段应用随机掩码。这对于训练模型以重建数据的缺失部分是必要的。它增强了检测异常情况和隐藏模式的能力。

接下来,这些片段被输入到编码器中,在那里它们被转换为紧凑的潜在表示。编码器学习提取时间序列的关键特征,同时忽略噪声和无关紧要的细节。这种方法使模型能够更好地泛化,并适用于不同性质的数据,无论是金融市场的价格图表、交易量时间序列,还是其他指标。

该模型的关键组成部分之一是自适应瓶颈机制,该机制根据数据的结构与质量来调节信息压缩的程度。当数据包含有意义的信号时,模型会保留更多细节;当信息冗余或噪声严重时,压缩率会增加,有助于减少干扰并提高异常检测能力。

自适应瓶颈模块(AdaBN)能够动态调整数据压缩的程度。该机制由一组类似于自编码器的小型模型组成。它们各自都蕴含着不同大小的潜在表征:

其中,DownNeti(•) 对分析后的数据进行压缩,UpNeti(•) 对其进行重建。

自适应路由器根据对输入数据的分析来选择最优路径:

其中 WrouterWnoise 是可训练矩阵。

为了压缩每个片段,我们使用了 k 条具有最高 R(z) 值的最优路径。

编码后,潜在表征被传递给两个并行的解码器。其中一个被设计用于重建正常数据,并经过训练以最小化重建误差。第二个旨在通过在重构值和原始值之间产生最大差异来检测异常。这种对抗性过程使模型能够有效区分标准模式和意外偏差。

在测试和实际部署期间,禁用异常解码器,仅使用正常解码器进行评估。如果模型能够以高精度重建数据,那么时间序列就对应于正常行为。如果重建过程中出现显著错误,则表明可能存在异常。

下面给出了作者绘制的 DADA 框架示意图。



使用 MQL5 实现

在回顾了 DADA 框架的理论方面后,我们继续讨论工作的实践部分,即考虑在 MQL5 中实现我们对所提方法的自身诠释。该框架的关键要素是自适应瓶颈模块。我们从它的构建开始我们的工作。

我相信,并非只有我一人注意到了它与之前实现的 Mixture of Experts 模块的相似之处。然而,这里有一个重要的区别。在我们构建的 CNeuronMoE 对象中,我们假设使用具有相同架构的小型模型。然而,在这种情况下,我们需要为每个模型调整潜在状态层的大小,以使其适应不同的数据特征。此选项无法再像以前那样使用卷积层对象来实现。当然,每个模型都可以单独创建,并依次将数据传递给它们。然而,这会导致硬件效率降低,训练和部署成本增加。

为了解决这些问题,决定为多窗口卷积层开发一个新对象。它基于同时使用几种不同卷积窗口大小的思想。这使得该模型能够在并行计算流中分析不同粒度级别的数据。这种方法使架构更加灵活,提高了输入数据处理的质量,并实现了计算资源的更高效利用。因此,该模型能够更好地适应输入数据中的各种时间结构,从而确保高准确性和高性能。

在 OpenCL 程序端构建算法


与往常一样,大部分数学运算都转移到了 OpenCL 环境中。在这里,我们创建 FeedForwardMultWinConv 内核,并在其中组织我们新层的前向传播。

__kernel void FeedForwardMultWinConv(__global const float *matrix_w,
                                     __global const float *matrix_i,
                                     __global float *matrix_o,
                                     __global const int *windows_in,
                                     const int inputs,
                                     const int windows_total,
                                     const int window_out,
                                     const int activation
                                    )
  {
   const size_t i = get_global_id(0);
   const size_t v = get_global_id(1);
   const size_t outputs = get_global_size(0);

内核参数包括指向四个数据缓冲区的指针以及四个常量,这些常量定义了输入数据和结果的结构。

请注意,其中一个全局缓冲区(windows_in)包含整数值。它存储卷积窗口的大小。假设输入数据缓冲区(matrix_i)包含一系列数据段。在每个段内,每个卷积窗口的数据是按顺序排列的。

我们计划在二维任务空间中调用此内核。第一维的大小表示每个单变量序列在结果缓冲区中的值的数量,而第二维则表示此类单变量序列的数量。

需要明确的是,第一维度特指结果缓冲区中的值数量,而非单变量序列中的元素数量。换言之,第一维的大小等于单变量序列中分析的段数与所用卷积核和卷积窗口数量的乘积。同时,无论卷积窗口大小如何,每个元素使用的卷积核数量都是相同的。这对于确保从压缩表示中重构的数据格式的一致性是必要的。

在内核体中,我们首先在二维任务空间中为每个维度标识当前线程。

接下来,有必要确定全局数据缓冲区中的偏移量,以便访问所需的元素。显然,第一维中的线程标识符指向已分析单变量序列中结果缓冲区的一个元素。然而,确定剩余数据缓冲区中的偏移量需要额外的工作。

首先,我们确定元素在分析段中的位置。为此,我们取第一维度线程标识符除以单个段的结果缓冲区中元素总数的余数。

   const int id = i % (window_out * windows_total);

然后,我们准备几个局部变量来临时存储中间值。

   int step = 0;
   int shift_in = 0;
   int shift_weight = 0;
   int window_in = 0;
   int window = 0;

接下来,我们组织一个循环来遍历卷积窗口缓冲区中的所有值。

#pragma unroll
   for(int w = 0; w < windows_total; w++)
     {
      int win = windows_in[w];
      step += win;

在循环中,我们计算所有卷积窗口的总和,这给出了输入数据缓冲区中单个段的大小。此外,在此循环中,我们确定当前段内到所需卷积窗口的偏移量(shift_in)、分析的卷积窗口的大小(window_in),以及可训练参数缓冲区中到所需卷积窗口矩阵元素开头的偏移量(shift_weight)。

      if((w * window_out) < id)
        {
         shift_in = step;
         window_in = win;
         shift_weight += (win + 1) * window_out;
        }
     }

接下来,我们确定结果缓冲区中当前元素之前的完整段数(steps),并在输入数据缓冲区中加上相应的偏移量,以访问所需的段。

   int steps = (int)(i / (window_out * windows_total));
   shift_in += steps * step + v * inputs;

针对可训练参数缓冲区中的偏移,我们为相应的卷积核添加了校正。为此,我们取结果缓冲区当前段中分析元素位置的除法余数。这为我们提供了当前卷积窗口结果中元素的索引。本质上,这个值标识了所需的过滤器。每个卷积核中可训练参数的数量等于卷积窗口的大小加上偏置元素。因此,通过将卷积核索引乘以可训练参数的数量,我们即可得到所需的偏移量。   

shift_weight += (id % window_out) * (window_in+1);

在完成预备步骤后,我们组织一个循环,将当前元素的值计算到一个局部变量中。

   float sum = matrix_w[shift_weight + window_in];
#pragma unroll
   for(int w = 0; w < window_in; w++)
      if((shift_in + w) < inputs)
         sum += IsNaNOrInf(matrix_i[shift_in + w], 0) * matrix_w[shift_weight + w];

然后,使用激活函数对得到的值进行调整,并将其存储到全局结果缓冲区的对应元素中。

 matrix_o[v * outputs + i] = Activation(sum, activation);
}

构建完前向传播算法后,我们继续组织反向传播过程。在这里,我们首先创建 CalcHiddenGradientMultWinConv 内核,将误差梯度向下分布到输入数据的级别。该内核的参数结构与前向传播内核的参数结构基本相同。我们只添加指向相应梯度缓冲区的指针。

__kernel void CalcHiddenGradientMultWinConv(__global const float *matrix_w,
                                            __global const float *matrix_i,
                                            __global float *matrix_ig,
                                            __global const float *matrix_og,
                                            __global const int *windows_in,
                                            const int outputs,
                                            const int windows_total,
                                            const int window_out,
                                            const int activation
                                           )
  {
   const size_t i = get_global_id(0);
   const size_t v = get_global_id(1);
   const size_t inputs = get_global_size(0);

该内核也在二维任务空间中运行。然而,这一次,第一个维度表示输入数据缓冲区中的偏移量,因为正是在输入数据层面,我们需要从所有卷积核中聚合梯度值。

与往常一样,内核首先在任务空间的所有维度上识别线程。然后,我们组织一个循环来对所有卷积窗口进行求和,以确定输入数据缓冲区中单个段的大小。

   int step = 0;
#pragma unroll
   for(int w = 0; w < windows_total; w++)
      step += windows_in[w];

这使我们能够确定与分析元素相对应的段落的索引以及该段落内的偏移量。

int steps = (int)(i / step);
int id = i % step;

接下来,我们声明几个局部变量用于临时数据存储,并组织另一个循环。在此循环中,我们确定分析的卷积窗口的大小(window_in)、卷积窗口的索引(window)以及当前段内距当前卷积窗口起始位置的偏移量(before)。

   int window = 0;
   int before = 0;
   int window_in = 0;
#pragma unroll
   for(int w = 0; w < windows_total; w++)
     {
      window_in = windows_in[w];
      if((before + window_in) >= id)
         break;
      window = w + 1;
      before += window_in;
     }

这些值使我们能够确定结果缓冲区(shift_out)和参数张量(shift_weight)中的偏移量。

int shift_weight = (before + window) * window_out + id - before;
int shift_out = (steps * windows_total + window) * window_out + v * outputs;

至此,准备阶段已经完成,我们已掌握足够的信息来累积误差梯度。我们构建了另一个循环,在该循环中,我们从所有卷积核中收集梯度值,同时考虑相应的权重。

   float sum = 0;
#pragma unroll
   for(int w = 0; w < window_out; w++)
      sum += IsNaNOrInf(matrix_og[shift_out + w], 0) * matrix_w[shift_weight + w * (window_in + 1)];

使用输入层激活函数的导数对结果值进行调整,并将结果存储到全局梯度缓冲区的对应元素中。

 matrix_ig[v * inputs + i] = Deactivation(sum, matrix_i[v * inputs + i], activation);
}

我们工作的第三阶段是构建将误差梯度分配到权重系数级别并更新它们以最小化整体模型误差的过程。在这项工作中,我们在 UpdateWeightsMultWinConvAdam 内核中实现了 Adam 优化算法。

为了正确构建此算法,我们通过添加特定常数和两个全局缓冲区来扩展核参数的数量,以用于计算矩。

__kernel void UpdateWeightsMultWinConvAdam(__global float *matrix_w,
                                           __global const float *matrix_og,
                                           __global const float *matrix_i,
                                           __global float *matrix_m,
                                           __global float *matrix_v,
                                           __global const int *windows_in,
                                           const int windows_total,
                                           const int window_out,
                                           const int inputs,
                                           const int outputs,
                                           const float l,
                                           const float b1,
                                           const float b2
                                          )
  {
   const size_t i = get_global_id(0);  // weight shift
   const size_t v = get_local_id(1);   // variable
   const size_t variables = get_local_size(1);

该内核也适用于二维任务空间。这一次,第一个维度表示可训练参数全局缓冲区中的优化元素。然而,这里有一个重要的细微差别。在处理多维时间序列时,每个单变量序列都使用共享的可训练参数进行分析。因此,在此阶段,我们需要汇总所有单变量序列的误差梯度。为了组织对单个单变量序列的并行处理,我们将它们沿着任务空间的第二维度进行分布,同时将它们分组到工作组中以实现数据交换。正是为了在工作组内进行数据交换,我们在 OpenCL 上下文的本地内存中创建了一个数组。

__local float temp[LOCAL_ARRAY_SIZE];

接下来,我们进入准备阶段,在此阶段我们将确定数据缓冲区中的偏移量。最简单的步骤或许是确定结果缓冲区中的步长( step_out )。它等于每个分段中的卷积窗口数量与卷积核数量的乘积。

int step_out = window_out * windows_total;

要获得其余参数,还需要进行额外的工作。首先,我们声明局部变量来存储中间结果。

int step_in = 0;
int shift_in = 0;
int shift_out = 0;
int window = 0;
int number_w = 0;

然后,我们组织一个循环来遍历卷积窗口大小的全局缓冲区中的值。

#pragma unroll
   for(int w = 0; w < windows_total; w++)
     {
      int win = windows_in[w];
      if((step_in + w)*window_out <= i &&
         (step_in + win + w + 1)*window_out > i)
        {
         shift_in = step_in;
         shift_out = (step_in + w + 1) * window_out;
         window = win;
         number_w = w;
        }
      step_in += win;
     }

在此循环中,我们确定输入数据缓冲区(shift_in)和结果缓冲区(shift_out)中所需卷积窗口的偏移量、卷积窗口的大小(window)以及其在缓冲区中的索引(number_w)。此外,我们计算所有卷积窗口(step_in)的总和,这表示了分段的大小。该值也用作输入数据缓冲区的步长。

值得注意的是,由于 bias 元素的存在,并非每个可训练参数都与输入数据缓冲区相关联。因此,我们引入了一个标记来检测这些元素。

bool bias = ((i - (shift_in + number_w) * window_out) % (window + 1) == window);

接下来,我们将偏移量调整到结果缓冲区中所需的元素位置。

int t = (i - (shift_in + number_w) * window_out) / (window + 1);
shift_out += t + v * outputs;

执行类似的操作来调整全局输入数据缓冲区中的偏移量。

   shift_in += (i - (shift_in + number_w) * window_out) % (window + 1) + v * inputs;

至此,准备阶段已经完成,我们将直接进入确定分析参数梯度的阶段。为此,我们组织了一个循环,该循环从结果缓冲区的所有元素中收集梯度值,这些元素在当前线程中的计算涉及到了正在优化的参数。

   float grad = 0;
   int total = (inputs + step_in - 1) / step_in;
#pragma unroll
   for(int t = 0; t < total; t++)
     {
      int sh_out = t * step_out + shift_out;
      if(bias && sh_out < outputs)
        {
         grad += IsNaNOrInf(matrix_og[sh_out], 0);
         continue;
        }

对于偏置元素,我们只需将梯度值相加;而对于其他参数,我们根据输入数据的相应元素进行调整。

 int sh_in = t * step_in + shift_in;
 if(sh_in >= inputs)
    break;
 grad += IsNaNOrInf(matrix_og[sh_out] * matrix_i[sh_in], 0);
}

需要注意的是,在这个循环中,我们仅收集单个单变量序列中的误差值。然而,如前所述,优化后的参数被应用于多维时间序列的所有序列中。因此,在进行参数优化之前,我们必须汇总工作组内计算的所有单变量序列的值。为了实现这一目标,在第一阶段,我们将各个值累加到局部数组的元素中。

//--- sum
   const uint ls = min((uint)variables, (uint)LOCAL_ARRAY_SIZE);
#pragma unroll
   for(int s = 0; s < (int)variables; s += ls)
     {
      if(v >= s && v < (s + ls))
         temp[v % ls] = (i == 0 ? 0 : temp[v % ls]) + grad;
      barrier(CLK_LOCAL_MEM_FENCE);
     }

然后,我们对局部数组元素中累积的值进行求和。

   uint count = ls;
#pragma unroll
   do
     {
      count = (count + 1) / 2;
      if(v < ls)
         temp[v] += (v < count && (v + count) < ls ? temp[v + count] : 0);
      if(v + count < ls)
         temp[v + count] = 0;
      barrier(CLK_LOCAL_MEM_FENCE);
     }
   while(count > 1);

从工作组中的所有线程获取总误差梯度后,我们可以更新被分析参数的值。此操作只需要一个线程。

   if(v == 0)
     {
      grad = temp[0];
      float mt = IsNaNOrInf(clamp(b1 * matrix_m[i] + (1 - b1) * grad, -1.0e5f, 1.0e5f), 0);
      float vt = IsNaNOrInf(clamp(b2 * matrix_v[i] + (1 - b2) * pow(grad, 2), 1.0e-6f, 1.0e6f), 1.0e-6f);
      float weight = clamp(matrix_w[i] + IsNaNOrInf(l * mt / sqrt(vt), 0), -MAX_WEIGHT, MAX_WEIGHT);
      matrix_w[i] = weight;
      matrix_m[i] = mt;
      matrix_v[i] = vt;
     }
  }

通过这些操作,我们将更新全局数据缓冲区中被分析参数的值和相应的矩。

至此,我们完成了 OpenCL 程序中多窗口卷积层算法的构建。完整的源代码见附录。

多窗口卷积层对象


我们工作的下一阶段是将之前构建的多窗口卷积层算法集成到主程序中。为此,我们创建了一个新的对象 CNeuronMultiWindowsConvOCL ,其中我们组织了用于管理在 OpenCL 上下文中创建的内核的进程。新对象的结构如下所示。

class CNeuronMultiWindowsConvOCL    :  public CNeuronConvOCL
  {
protected:
   int               aiWindows[];
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL);
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL);
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);

public:
                     CNeuronMultiWindowsConvOCL(void) {  activation = SoftPlus;  iWindow = -1; }
                    ~CNeuronMultiWindowsConvOCL(void) {};
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint &windows[], uint window_out, uint units_count, uint variables,
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int       Type(void)   const   {  return defNeuronMultiWindowsConvOCL;   }
   //--- methods for working with files
   virtual bool      Save(int const file_handle);
   virtual bool      Load(int const file_handle);
   //---
   virtual void      SetOpenCL(COpenCLMy *obj);
  };

从本质上讲,新的 CNeuronMultiWindowsConvOCL 对象代表了标准卷积层的修改版本。因此,从父类派生它是合乎逻辑的。这使我们能够继承基本的卷积逻辑,并避免代码重复。

在所展示的结构中,我们观察到了熟悉的重写虚方法集。然而,新对象的主要区别在于其能够同时使用多个卷积窗口大小进行操作。这需要创建额外的数据存储元素和接口,以便将它们传输到 OpenCL 上下文中。为了实现这一点,我们声明了一个额外的数组 aiWindows ,并修改了对象初始化方法 Init 的参数。

bool CNeuronMultiWindowsConvOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                                      uint &windows[], uint window_out, uint units_count,
                                      uint variables, ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(windows.Size() <= 0 || ArrayCopy(aiWindows, windows) < int(windows.Size()))
      return false;

需要强调的是,尽管对 CNeuronMultiWindowsConvOCL 算法进行了各种更改,但我们已尽一切努力保留父类的逻辑和功能。这不仅简化了新对象与现有架构的集成,还允许重用已经测试和调试过的机制。

对象的初始化算法首先检查参数中接收到的卷积窗口数组的大小,并将其值复制到一个专门创建的内部数组中。

接下来,我们计算所有卷积窗口的总和,并在每个卷积窗口中加上一个偏置元素。

int window = 0;
for(uint i = 0; i < aiWindows.Size(); i++)
   window += aiWindows[i] + 1;

这看似是一个不显眼但必要的操作。原因在于,对于每个卷积窗口,我们需要生成一个大小为(Windowi + 1)*Filters 的权重矩阵。因此,参数缓冲区的总大小将为:

可以将卷积核数量的公共变量从求和中分离出来:

如果我们用总值来代替窗口的总和,那么我们就得到了确定单个卷积窗口可训练参数数量的公式。但是,在父类中,只能添加一个偏置元素,而不是像这里要求的那样,每个卷积窗口添加一个偏置元素。因此,我们为每个窗口的总和添加一个偏置元素,然后将得到的值减一,并将其作为窗口大小和卷积步长传递给父类的初始化方法。

window--;
if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, window, window, window_out,
                         units_count * aiWindows.Size()*variables, 1, ADAM, batch))
   return false;

由于我们计划对所有单变量序列使用相同的参数,因此我们将参数数量设置为 1。同时,我们通过将序列中的元素数量乘以卷积窗口的数量和单变量序列的数量,来增加序列中的元素数量。

这种方法使我们能够以所需的大小初始化所有继承的数据缓冲区,包括用随机值初始化可训练参数的缓冲区。

接下来,我们创建一个全局数据缓冲区,用于将卷积窗口数组传输到 OpenCL 上下文中。正如预期的那样,此缓冲区的值在对象初始化期间设置,并在训练和模型运行期间保持不变。因此,缓冲区仅在 OpenCL 上下文中创建,而我们的对象仅存储一个指向该缓冲区的指针。

   iVariables = variables;
   iWindow = OpenCL.AddBufferFromArray(aiWindows, 0, aiWindows.Size(), CL_MEM_READ_ONLY);
   if(iWindow < 0)
      return false;
//---
   return true;
  }

我们使用返回的句柄验证全局缓冲区创建的正确性,并完成新对象的初始化方法,同时向调用程序返回操作结果的布尔值。

初始化新对象后,我们继续重写前馈传递方法 CNeuronMultiWindowsConvOCL::feedForward 。正如你可能已经猜到的那样,这里是之前创建的 FeedForwardMultWinConv 内核排队等待执行的地方。然而,尽管此类情况采用的是标准程序,但仍有一些细微之处值得注意。

bool CNeuronMultiWindowsConvOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!OpenCL || !NeuronOCL)
      return false;

该方法参数包括一个指向输入数据对象的指针,会立即检查该指针的有效性。

验证检查成功通过后,我们初始化任务空间数组。

uint global_work_offset[2] = {0, 0};
uint global_work_size[2] = {Neurons() / iVariables, iVariables};

如内核描述中所述,第二维度对应于输入数据中的单变量序列数量。第一维的线程数量是通过将对象结果缓冲区中的总元素数除以单变量序列的数量来确定的。

接下来,数据会被传递给内核参数。

   ResetLastError();
   int kernel = def_k_FeedForwardMultWinConv;
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_ffmwc_matrix_i, NeuronOCL.getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_ffmwc_matrix_o, getOutputIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_ffmwc_matrix_w, WeightsConv.GetIndex()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgumentBuffer(kernel, def_k_ffmwc_windows_in, iWindow))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }

值得注意的是,之前存储的句柄被作为卷积窗口大小缓冲区进行传递。同时,输入数据序列的维度是通过将输入数据缓冲区的大小除以单变量序列的数量来确定的。

   if(!OpenCL.SetArgument(kernel, def_k_ffmwc_inputs, NeuronOCL.Neurons() / iVariables))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(kernel, def_k_ffmwc_window_out, iWindowOut))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(kernel, def_k_ffmwc_windows_total, (int)aiWindows.Size()))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.SetArgument(kernel, def_k_ffmwc_activation, (int)activation))
     {
      printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__);
      return false;
     }
   if(!OpenCL.Execute(kernel, 2, global_work_offset, global_work_size))
     {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      return false;
     }
//---
   return true;
  }

在成功传递所有参数后,我们将内核加入执行队列并完成方法,向调用程序返回一个布尔结果。

同样地,用于组织反向传播过程的内核也被放入队列中。唯一的区别在于,在分配误差梯度时,我们指定输入层的激活函数;而在参数优化操作中,我们确保在任务空间的第二维度内创建工作组。

bool CNeuronMultiWindowsConvOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   if(!OpenCL || !NeuronOCL)
      return false;
//---
   uint global_work_offset[2] = {0, 0};
   uint global_work_size[2] = {WeightsConv.Total(), iVariables};
   uint local_work_size[2] = {1, iVariables};
//---
.........
.........
.........
//---
   if(!OpenCL.Execute(kernel, 2, global_work_offset, global_work_size, local_work_size))
     {
      printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
      return false;
     }
//---
   return true;
  }

至此,我们完成了对构建多窗口卷积层算法的讨论。CNeuronMultiWindowsConvOCL 对象及其所有方法的完整代码在附录中提供。

文章即将结束,但我们的工作还没有结束。让我们稍作休息,在下一篇文章中继续阐述我们对 DADA 框架作者提出的方法的理解。



结论

现代金融市场的特点不仅在于数据量巨大,还在于数据波动性高。这使得异常检测成为一项极具挑战性的任务。DADA 框架提出了一种全新的方法,将自适应瓶颈和双并行解码器相结合,以实现更精确的时间序列分析。其关键优势在于能够动态适应不同的数据结构,无需事先定制,从而使其成为一款通用工具。

在本文的实践部分,我们开始使用 MQL5 实现我们对 DADA 框架作者提出的方法的解释。然而,我们的工作尚未完成,我们将在下一篇文章中继续探讨。


参考


本文中用到的程序

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

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

附加的文件 |
MQL5.zip (2565.37 KB)
交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
数据科学与机器学习(第四十二部分):使用Python中的ARIMA模型进行外汇时间序列预测 —— 您需要了解的一切 数据科学与机器学习(第四十二部分):使用Python中的ARIMA模型进行外汇时间序列预测 —— 您需要了解的一切
ARIMA,全称为自回归积分移动平均模型,是一种效果出色的传统时间序列预测模型。该模型能够捕捉时间序列数据中的突增与波动,可对后续数值做出精准预测。在本文中,我们将了解什么是ARIMA、如何运行,以及如何利用它高精度预测市场下一期价格,还有更多相关实用内容。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
博弈论方法在交易算法中的应用 博弈论方法在交易算法中的应用
我们正在基于深度Q网络(DQN)机器学习技术,结合多维因果推理,开发一款自适应、自学习的交易 EA。该 EA 将能够同时成功交易 7 个货币对。不同货币对的智能体之间会相互交换信息。