English Русский Español Português
preview
神经网络在交易中的应用:用于多元时间序列预测的 LSTM 优化(DA-CG-LSTM)

神经网络在交易中的应用:用于多元时间序列预测的 LSTM 优化(DA-CG-LSTM)

MetaTrader 5交易系统 |
38 2
Dmitriy Gizlyk
Dmitriy Gizlyk

概述

金融市场不仅仅是屏幕上的数字。它们构成了一个动态的环境,在这个环境中,每一个价格跳动、每一根蜡烛图以及交易量的每一次变化都反映了人类的情绪、期望、恐惧和希望。理解这种节奏并学会预测价格走势,是交易者们不断努力解决的难题。

重点在于多元时间序列 — 市场数据的经典表现形式:随时间变化的资产价格、交易量、技术指标和新闻信息流。所有这些都是可以进行分析、建模并最终用于预测的数据形式。

直到最近,该行业仍主要依赖经过时间检验的经典方法,如 ARIMA、SARIMA 及相关模型。这些模型实用且易于解释,同时不需要大量的计算资源。在处理季节性和线性依赖关系时,尤其是在稳定的市场条件下,它们的表现相当不错。然而,金融市场并非平稳运行。新闻影响预期,投资者情绪可能在几秒钟内发生变化,算法交易会产生共振效应,所有这些都导致了复杂、非线性且往往混乱的关系。传统模型或许能指出大致方向,但无法捕捉到更精细的细节。

深度学习的出现从根本上改变了这一格局。循环神经网络( RNN )使模型能够考虑历史动态,但它们远非通用解决方案。它们的主要局限性是所谓的“短记忆”问题。换句话说,这类模型只能处理有限的时间上下文。随着输入序列变长,它们会迅速丢失时间序列早期部分的重要信息。

为了解决这个问题,人们开发了改进的架构,例如 LSTM长短期记忆网络)和GRU门控循环单元)。这些模型赋予了神经网络记忆能力,即在较长的时间段内保留重要信息的能力。

然而,这些架构也有其自身的局限性。尽管它们能够捕捉到更长期的依赖关系,但它们对输入数据的质量仍然高度敏感。特别是,它们在短期市场尖峰波动起决定性作用的情况下会遇到困难。这种短期变化通常不会被模型保留在长期记忆中,尤其是当它们没有伴随长期背景的重大变化时。

为了克服这些缺点,引入了注意力机制。它使模型能够有选择性地关注时间序列中最相关的部分,而不管这些部分与当前时间步的距离如何。与 LSTM 不同,注意力机制不需要对每个时间步进行顺序处理,可以立即集中精力于最具信息量的元素。这显著提高了神经网络捕捉长距离依赖关系的能力,尤其是在复杂的多元时间序列中。

然而,这种方法也有一个缺点:这类模型擅长捕捉长期依赖关系,但往往忽视可能至关重要的短期信号。金融市场无法容忍任何延误 — 仅仅一条新闻标题就可能使价格飙升或暴跌。如果一个模型未能对这些事件做出反应,那么它就失去了及时调整持仓的机会。

论文“基于双阶段注意力转换门控长短期记忆网络的多变量时间序列预测”的作者试图结合这两种方法的优点,并提出了一种新的框架双阶段注意力转换门控 LSTMDA-CG-LSTM)。该模型采用了双重注意力机制。在第一阶段,它评估特征和时间间隔的重要性;然后,在通过专门的 CG-LSTM 块处理数据后,它会重新检查时间依赖性,放大重要信号,同时抑制相关性较低的信号。

该模型的另一个优点在于其精心设计的激活函数,该函数既能提高长期信息保留能力,又能提高对短期市场尖峰波动的响应能力。


DA-CG-LSTM 算法

DA-CG-LSTM 框架旨在从多元时间序列中提取有意义的模式。其架构将两层注意力机制与一个名为转换门控 LSTM 的改进循环块相结合。这使得该模型在预测动态多变量过程方面特别有效。

该过程首先将多变量时间序列输入到模型中。该序列由矩阵X = [ x 1, x 2, ..., x T ] ∈ R T*n 表示,其中每一行x t t 时刻的 n 个特征的向量。然而,该模型并不会盲目地处理这些信息。它首先学习识别哪些特征和哪些时间步真正重要。第一阶段就这样开始了:输入注意力。

最初,该模型会分析每个单独时间步内的特征。它根据以下公式计算时间步 t 时每个特征 x kt 的重要性得分:

这里 Webe 分别表示应用于每个输入特征向量 xt 的线性层的可训练权重和偏置。这一层将原始数据转换为信息量更大的潜在表示,以适用于相关性估计。此外,可训练向量 ve 可用作可解释的注意力掩码,用于计算标量重要性得分。这种设计使模型能够根据特征的贡献灵活地对它们进行排序,这在处理噪声大、高维的时间序列时尤其有价值。

使用 SoftMax 函数对得到的重要性系数进行归一化,将分数集转换为特征上的概率分布:

然后使用这些系数来调整原始输入向量 x̃t,其中每个特征根据其估计的重要性进行缩放。

但我们并不止步于此。下一步,模型会评估每个时间位置的重要性,以确定在处理过程中应强调哪些时间点。采用类似的结构:

由此得到的序列是时间加权表示:

因此,在循环处理开始之前,该模型就已经将注意力集中在了它认为最相关的信息上。

然后将该序列传递给修改后的循环模块 CG-LSTM 。这里是该过程的第二阶段。在标准的 LSTM 架构中,作者重新设计了与输入门和遗忘门相关的激活函数。这样做是为了提高模型对伴随短期市场变动的短期波动的敏感性,同时增强其记忆长期信息的能力。

在传统的 LSTM 模块中,输入门依靠 sigmoid 激活函数来确定应该保留哪些信息。然而,sigmoid 函数容易饱和:当输入值变得非常小或非常大时,它们的导数趋近于零,从而降低学习效率。DA-CG-LSTM 框架的作者提出将 sigmoid 函数与双曲正切函数相结合,其表达式如下:

这种设计有助于防止训练初期出现饱和现象,同时保持对细微但有意义的波动的敏感性。在金融时间序列分析中,这种能力尤为重要。例如,交易量的突然增加或波动性的激增可能表明市场状态发生了转变,而及时检测到这些模式能提供显著的预测优势。

CG-LSTM 中的遗忘门也进行了修改。与标准 LSTM 架构不同,它采用了一种将 sigmoid 函数与变换后的双曲正切表达式相结合的函数:

这种公式具有一种独特的性质:其导数范围在 0 到约 2.89 之间,从而产生更强的数值分散效应。在实践中,这使得模型能够更积极地丢弃不相关或过时的信息,从而专注于最近的发展情况。这一特性在金融市场中尤为宝贵,因为在金融市场中,历史事件很快就会失去相关性,而成功往往取决于对当前信号的反应。

CG-LSTM 模块的输出是序列 ( h1, h2, ..., hT ),其中每个 ht 都包含短期和长期信息。然而,仅仅保留信息是不够的 — 还必须能够正确回忆起这些信息。这就是第二层注意力机制的目的:时间注意力。

在这一阶段,模型实际上是在重新审视自己的记忆。它计算每个隐藏状态 hj 相对于当前时间步的重要性。使用 SoftMax 对结果值进行归一化,并构建上下文向量(整个时间历史信息的凝练表示),随后由第二个 CG-LSTM 块进行处理。

模型利用当前隐藏状态 ht 和增强后的上下文 ct 生成最终预测结果:

其中 f 通常是架构的全连接层或其他输出组件。

DA-CG-LSTM 架构不仅智能,而且具备良好的判断性。该模型不会试图记住所有信息。相反,它会做出明智的选择。它并不会对每一处噪声都作出反应,而是学会区分瞬态波动和有意义的信息模式。这种区分在金融预测中尤为重要。该系统能够识别出与过去观察到的信号类似的信号,评估其重要性,并调整其预测结果。

从这个意义上讲,DA-CG-LSTM 作为一个动态的、自适应的系统,通过结构化信息分析来学习、进化和得出结论。它的优势不仅在于其数学公式,还在于其概念的清晰性:

注意力 + 记忆 + 解读 = 有意义的预测

下面展示了作者的 DA-CG-LSTM 框架示意图。

DA-CG-LSTM 框架的原始示意图



MQL5 中的实现

在研究了 DA-CG-LSTM 框架的理论方面后,我们现在转向这项工作的实践部分,我们将展示使用 MQL5 对该方法进行自行实现。

我们将首先构建一个修改后的 CG-LSTM 块,如下图作者所绘示意图所示。

值得注意的是,与传统的 LSTM 块类似,输入张量与前一个时间步生成的隐藏状态连接起来。然后利用得到的张量构建四个实体:三个门控单元和一个新的上下文表示。这些实体中的每一个都是由一个单独的线性层生成的。然而,通过对这些层的输出应用不同的激活函数,可以引入非线性行为。

为各个实体使用四种不同的激活函数,自然需要顺序执行四个全连接层。显然,这不是最佳解决方案。在我们的工作中,我们致力于最大化操作并行性,因为这能加速模型训练和真实条件下的决策过程。

为了实现这一点,在实现经典的 LSTM 模块时,我们将整个前馈过程组织在一个单独的内核中。每个实体的值在并行工作组线程中同时计算,中间值通过本地内存进行交换。事实证明,这种方法非常有效。然而,当使用 DA-CG-LSTM 框架的作者提出的更复杂的激活函数序列时,我们需要额外的数据缓冲区来存储中间值,这大大增加了算法的复杂性。

因此,在本次实现中,我们决定采用另一种方法。前馈过程分为两个阶段。在第一阶段,所有四个实体都是在在激活前生成的。这可以通过使用全连接层或卷积层来实现,其中卷积层的输出张量大小与生成的实体数量成正比。在处理多维输入数据时,卷积层更为理想,因为它能够对单个一元序列进行独立操作。

第二阶段通过应用所需的激活函数并执行模块的内部过程来执行实际的 CG-LSTM 计算。

当然,我们将首先在 OpenCL 环境下实现第二阶段。

修改 OpenCL 程序


首先,我们在 CSLSTM_FeedForward 内核中实现了 CG-LSTM 块版本的前馈传递。该内核仅接收指向三个数据缓冲区的指针。

其中一个缓冲区包含输入数据(concatenated),其中存储了应用激活函数之前四个组件的值。为了最大限度地减少与全局内存访问相关的延迟并提高读取性能,这些值使用 float4 向量类型存储,从而允许通过一次内存访问获取四个连续元素。该组织能更高效地利用内存带宽并加速计算,尤其是在处理大量输入数据时。

剩余的两个缓冲区用于存储上下文和输出值。

__kernel void CSLSTM_FeedForward(__global const float4* __attribute__((aligned(16))) concatenated,
                                 __global float *memory,
                                 __global float *output)
  {
   uint id = (uint)get_global_id(0);
   uint total = (uint)get_global_size(0);       // hidden size
   uint idv = (uint)get_global_id(1);
   uint total_v = (uint)get_global_size(1);     // variables

该内核在二维执行空间内运行,不创建工作组。第一维度对应于单元隐藏状态的大小,而第二维度表示输入数据中单变量序列的数量。在内核体中,我们识别任务空间所有维度中的线程。

利用这些信息,我们确定数据缓冲区中的适当偏移量,并立即将相应的输入数据块加载到局部变量中。

uint shift = id + total * idv;
float4 concat = concatenated[shift];

接下来,我们将所需的激活函数应用于所有实体。

float fg = 1 - Activation(1 - 1 / pow(Activation(concat.s0, ActFunc_SIGMOID), 2), ActFunc_TANH);
float ig = Activation(Activation(concat.s1, ActFunc_SIGMOID), ActFunc_TANH);
float nc = Activation(concat.s2, ActFunc_TANH);
float og = Activation(concat.s3, ActFunc_SIGMOID);

之后,我们更新上下文值和隐藏状态。

float mem = IsNaNOrInf(memory[shift] * fg + ig * nc, 0);
float out = IsNaNOrInf(og * Activation(mem, ActFunc_TANH), 0);

计算结果保存到全局数据缓冲区的相应元素中,之后内核执行完成。

 memory[shift] = mem;
 output[shift] = out;
} 

生成的内核代码简洁易读,这主要归功于使用了一个负责选择激活函数的辅助方法。这种方法不仅简化了内核本身的逻辑,还使代码更加模块化和可扩展。此外,加入一个用于验证计算值的机制可以提高整个计算流程的可靠性。

完成前馈内核后,我们继续实现反向传播操作。显而易见,在前馈传递过程中没有使用可训练参数。所有可训练参数都放置在第一阶段使用的神经层中。因此,实现反向传播只需要我们在参与计算的组件之间正确分配误差梯度。这些操作在 CSLSTM_CalcHiddenGradient 内核中执行。

在保留原始执行空间的同时,为梯度分布核的参数补充了适当的数据缓冲区。

__kernel void CSLSTM_CalcHiddenGradient(__global const float4* __attribute__((aligned(16))) concatenated,
                                        __global float4* __attribute__((aligned(16))) grad_concat,
                                        __global const float* memory,
                                        __global const float* grad_output
                                       )
  {
   uint id = get_global_id(0);
   uint total = get_global_size(0);
   uint idv = get_global_id(1);
   uint shift = id + total * idv;

在内核体中,我们跨所有维度识别当前线程,并立即计算数据缓冲区中相应的偏移量。

需要注意的是,计算 DA-CG-LSTM 框架提出的复杂激活函数的导数需要几个中间值,这些中间值在前馈过程中有意没有保存。显然,保存这些数值需要大量的内存资源。此外,访问全局数据缓冲区是一项开销较大的操作。然而,由于大部分矩阵计算被委托给第一阶段的内部神经层,我们可以从预激活实体值中快速计算出所需的值。

为了实现这一点,我们将预激活数据读取到局部变量中,并重新计算函数,同时将中间结果保存在本地。 

   float4 concat = concatenated[shift];                    // Pre-activation values for all 4 gates
// --- Forward reconstruction of gates ---
   float fg_s = Activation(concat.s0, ActFunc_SIGMOID);
   float fg = 1.0f - Activation(1.0f - 1.0f / pow(fg_s, 2), ActFunc_TANH);  // Forget gate (ft)
   float ig_s = Activation(concat.s1, ActFunc_SIGMOID);
   float ig = Activation(ig_s, ActFunc_TANH);              // Input gate (it)
   float nc = Activation(concat.s2, ActFunc_TANH);         // New content (ct~)
   float og = Activation(concat.s3, ActFunc_SIGMOID);      // Output gate (ot)
   float mem = memory[shift];                              // New memory state (ct)
   float mem_t = Activation(mem, ActFunc_TANH);            // tanh(ct)

在这一阶段,我们还会从上一个时间步重建记忆状态。

// --- Reconstruct previous memory state (t-1) ---
   float prev_mem = IsNaNOrInf((mem - ig * nc) / fg, 0);

完成准备计算后,我们直接进行梯度分布运算。首先,我们将全局缓冲区中的输出梯度加载到局部变量中。

// --- Gradients computation ---
   float out_g = grad_output[shift];

接下来,我们使用相应激活函数的导数,将这个梯度分配到输出门和上下文记忆中。

float og_g = Deactivation(out_g * mem_t, og, ActFunc_SIGMOID);
float mem_g = Deactivation(out_g * og, mem_t, ActFunc_TANH);

然后,我们在新的上下文投影和输入门之间传播上下文记忆梯度。

float nc_g = Deactivation(mem_g * ig, nc, ActFunc_TANH);
float ig_g = Deactivation(Deactivation(mem_g * nc, ig, ActFunc_TANH), ig_s, ActFunc_SIGMOID);

在更新输入门的梯度时,应特别关注涉及相应激活函数导数的阶段,这些导数是在更新过程中依次应用的。

最后,我们在激活之前将误差梯度向下传播到遗忘门值。如前所述, DA-CG-LSTM 框架的作者为该组件提出了一种相对复杂的激活函数。因此,梯度传播必须分多个阶段进行。

首先,我们根据上下文记忆梯度及其先前值来确定遗忘门误差。

// ∂L/∂fg = ∂L/∂ct * mem_(t-1)
   float fg_g = mem_g * prev_mem;

然后利用双曲正切函数的导数和复数内部表达式的导数来调整所得值。

// Derivative of the complex forget gate:
// f(z) = 1 - tanh(1 - 1 / σ(z)^2)
   float fg_s_g = 2 / pow(fg_s, 3) * Deactivation(-fg_g, fg, ActFunc_TANH);
   fg_g = Deactivation(fg_s_g, fg_s, ActFunc_SIGMOID);

最后,我们应用 sigmoid 导数。

结果值被存储在全局数据缓冲区的相应位置。

// --- Write back gradients ---
   grad_concat[shift] = (float4)(fg_g, ig_g, nc_g, og_g);
  }

至此,我们在 OpenCL 端实现方面的工作就完成了。完整的源代码见附件。

创建 CG-LSTM 对象


接下来,我们进入主程序。在这里,我们创建了一个新的对象 CNeuronCGLSTMOCL ,负责管理我们的 CG-LSTM 模块的操作。新对象的结构如下所示。

class CNeuronCGLSTMOCL : public CNeuronBaseOCL
  {
protected:
   CNeuronBaseOCL    cConcatenateInputs;
   CNeuronConvOCL    cProjection;
   //---
   virtual bool      CSLSTM_feedForward(void);
   virtual bool      CSLSTM_CalcHiddenGradient(void);
   //---
   virtual bool      feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL) override;

public:
                     CNeuronCGLSTMOCL(void) {};
                    ~CNeuronCGLSTMOCL(void) {};
   //---
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint numNeurons,
                          ENUM_OPTIMIZATION optimization_type, uint batch) override;
   virtual bool      Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                          uint count, uint window, uint variables, 
                          ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual bool      Save(int const file_handle) override;
   virtual bool      Load(int const file_handle) override;
   //---
   virtual int       Type(void) override  const    {  return defNeuronCGLSTMOCL; }
   virtual bool      Clear(void) override;
   virtual CBufferFloat *getLSTMWeights(void) { return cProjection.GetWeightsConv(); }
   virtual void      SetOpenCL(COpenCLMy *obj) override;
   virtual bool      WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
  };

在新的类结构中,我们看到了一组熟悉的重载方法以及两个内部对象,随着我们实现类方法,这些对象的功能将变得更加清晰。所有内部对象都直接声明为类成员,这样我们就可以将类的构造函数和析构函数留空。已声明和继承的对象的初始化是在 Init 方法中执行的。

初始化方法接收一些常量,这些常量唯一地定义了所创建对象的架构。

bool CNeuronCGLSTMOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl,
                            uint count, uint window, uint variables,
                            ENUM_OPTIMIZATION optimization_type, uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, count * variables, optimization_type, batch))
      return false;
   SetActivationFunction(None);

这些值中的一部分会立即传递给父类中同名的方法,该方法已经处理了参数验证和继承对象的初始化。

请注意,我们明确指定了新创建的对象没有激活函数。

在父类初始化成功完成后,我们继续为已声明的对象做操作准备。在这种情况下,只有两个。第一个是全连接层,用于存储由输入数据和在前馈传递过程中生成的隐藏状态组成的拼接张量。

if(!cConcatenateInputs.Init(0, 0, OpenCL, (count + window) * variables, optimization, iBatch))
   return false;
cConcatenateInputs.SetActivationFunction(None);

再次明确指出,这里没有使用激活函数。

第二个对象是一个卷积层,负责将连接后的张量投影成四个实体。

if(!cProjection.Init(0, 1, OpenCL, count + window, count + window, count * 4, 1, variables, optimization, iBatch))
   return false;
cProjection.SetActivationFunction(None);

如前所述,该层同样在没有激活函数的情况下运行,这是我们明确指定的。

应特别关注卷积层的初始化。在创建过程中,我们明确地将序列长度设置为 1。乍一看,这似乎是一种限制;然而,这种设计选择反映了一种深思熟虑的架构设计决策。同时,我们指定了将并行处理的单变量(独立)序列的数量。

这种方法允许每个单变量序列维护其自身独立的一组可训练参数 — 即其自身的权重矩阵。因此,每个序列都可以独立进行分析和训练。每个序列都在其自身上下文中学习,对特定模式和规律做出响应,而不与其他序列共享参数。因此,我们获得了对输入数据更具表现力、适应性和结构灵活性的表示,尤其是在不同时间子序列服务于不同语义功能或代表个别市场因素的任务中。

这种参数隔离在训练过程中也发挥着重要作用。首先,它减少了通道之间的交叉干扰,从而减轻了对主导模式的过拟合。其次,每个单变量序列都可以专注于自身的数据特征。这种差异化学习不仅使模型在特定任务上更加准确,而且使其对不断变化的市场条件具有显著更强的鲁棒性。

此外,独立训练的单变量滤波器有助于提高泛化能力。该模型变得不那么容易记忆数据,而更有可能提取出一般模式。这对于金融时间序列尤为重要,因为其中的历史数据可能包含独特且不具代表性的事件。通过将学习过程分解为多个独立的分支,该模型即使在以前未见过的情境中,也能够识别出典型的市场信号。

最后,循环模型的一个决定性特征是它们依赖于前一次前馈传递的数据。因此,在完成初始化过程之前,我们会清除所有数据缓冲区。

   if(!Clear())
      return false;
//---
   return true;
  }

我们工作的下一阶段是实现前向传递操作,我们在 feedForward 方法中实现了该操作。这部分比较简单。该方法接收一个指向输入数据对象的指针,并立即对其进行验证。

bool CNeuronCGLSTMOCL::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

接下来,我们从投影层参数中提取输入张量和隐藏状态的维度。

int hidden = (int)cProjection.GetFilters() / 4;
int inputs = (int)cProjection.GetWindow() - hidden;
int variables = (int)cProjection.GetVariables();

然后,我们将输入数据与每个单变量序列的前一次前馈传递的结果进行拼接。

if(!Concat(NeuronOCL.getOutput(), getOutput(), cConcatenateInputs.getOutput(), inputs, hidden, variables))
   return false;

将所得值投影到四个实体中。

if(!cProjection.FeedForward(cConcatenateInputs.AsObject()))
   return false;

至此,剩下的就是调用包装器方法,将先前创建的前馈内核 CSLSTM_feedForward 排入执行队列。

 return CSLSTM_feedForward();
}

将内核放入执行队列的方法遵循了前几篇文章中讨论的相同架构,因此我们在此不会详细研究其内部算法。

完成了前馈实现后,我们接下来进行反向传播过程。如你所知,这一过程包括两个阶段:误差梯度的传播和可训练参数的优化。

在这种情况下,可训练参数仅存在于拼接数据的投影层内。因此,参数优化被简化为调用投影层对应的优化方法。

bool CNeuronCGLSTMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
   return cProjection.UpdateInputWeights(cConcatenateInputs.AsObject());
  }

calcInputGradients 中实现的用于在参与组件之间分配误差梯度的算法稍微复杂一些。该方法接收一个指向输入数据对象的指针。这是前馈过程中使用的同一个对象。然而,这一次,我们必须根据输入数据对模型最终输出的贡献,将误差梯度传播到输入数据中。

bool CNeuronCGLSTMOCL::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!NeuronOCL)
      return false;

在方法体中,我们会立即验证接收到的指针的有效性。在之前的文章中,已多次讨论过这一控制点的必要性。

接下来,与前馈方法类似,我们确定输入数据和隐藏状态的维度。

int hidden = (int)cProjection.GetFilters() / 4;
int inputs = (int)cProjection.GetWindow() - hidden;
int variables = (int)cProjection.GetVariables();

我们首先通过调用包装器方法,将相应的核函数放入执行队列,从而在实体之间分配误差梯度。

if(!CSLSTM_CalcHiddenGradient())
   return false;

然后,我们将梯度向下传播到拼接后的输入张量。

if(!cConcatenateInputs.calcHiddenGradients(cProjection.AsObject()))
   return false;

通过反向拼接,我们提取出与原始输入数据相对应的误差梯度。

if(!DeConcat(NeuronOCL.getGradient(), getPrevOutput(), cConcatenateInputs.getGradient(),
                                                             inputs, hidden, variables))
   return false;

值得注意的是,在初始化内部对象时,我们特意禁用了激活函数。然而,这并不排除在输入数据本身中使用激活函数。因此,我们会检查激活函数是否与输入数据相关联,并在必要时使用相应激活函数的导数来调整生成的梯度。

   if(NeuronOCL.Activation() != None)
      if(!DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), NeuronOCL.getGradient(),
                                                                       NeuronOCL.Activation()))
         return false;
//---
   return true;
  }

然后,我们将操作的逻辑结果返回给调用者,从而完成该方法。

至此,我们完成了对用于构建新 CNeuronCGLSTMOCL 类方法的算法的考察。附件中提供了此类及其所有方法的完整源代码。

不知不觉中,我们已经逐渐触及到了当前文章的实用极限。然而,这项研究的逻辑完整性需要后续研究来完善。我们暂时就先到这里吧。未完待续 — 下一部分内容同样精彩。 



结论

在本文中,我们探讨了 DA-CG-LSTM 框架的理论基础。与传统模型不同,它的架构融合了多种创新机制,包括 CG-LSTM 和双重注意力机制,这两种机制都能从数据中更深入、更准确地提取依赖关系。这些组件使模型能够有效地处理复杂的时间关系,同时捕捉长期和短期模式。

在实践部分,我们展示了我们自己使用 MQL5 实现的 CG-LSTM 模块。然而,这项工作尚未完成,我们将在下一篇文章中继续推进,使该项目得出合乎逻辑的结论。



参考


本文中使用的程序

# 名称 类型 描述
1 Research.mq5 EA 样本采集 EA
2 ResearchRealORL.mq5
EA
使用 Real-ORL 方法采集样本的 EA
3 Study.mq5 EA 离线模型训练的 EA
4 StudyOnline.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/17901

附加的文件 |
MQL5.zip (2687.97 KB)
最近评论 | 前往讨论 (2)
Vladimir Sanin
Vladimir Sanin | 26 4月 2025 在 08:26
您好。请问在哪里可以获取NeuroNet.mqh、NeuroNet.cl和Trajectory.mqh这些库?
此外,该模型的具体参数(输入数据尺寸、神经元数量、优化器)是什么?
Dmitriy Gizlyk
Dmitriy Gizlyk | 28 4月 2025 在 12:02
Владимир #:
您好。请问在哪里可以获取NeuroNet.mqh、NeuroNet.cl和Trajectory.mqh这些库?
此外,该模型的具体参数是什么(输入数据尺寸、神经元数量、优化器)?

您好,弗拉基米尔。

所有 NeuroNet.* 库都位于附件“MQL5\Experts\NeuroNet_DNG\NeuroNet.*”中,而 Trajectory.mqh 则位于“MQL5\Experts\DACGLSTM\Trajectory.mqh”中。

关于可训练模型的详细说明将在下一篇文章中介绍。

交易策略 交易策略
各种交易策略的分类都是任意的,下面这种分类强调从交易的基本概念上分类。
您应该了解的MQL5向导技巧(第七十三部分):使用一目均衡表形态与ADX-Wilder形态 您应该了解的MQL5向导技巧(第七十三部分):使用一目均衡表形态与ADX-Wilder形态
一目均衡表(Ichimoku-Kinko-Hyo)指标与韦尔德平均趋向指数(ADX-Wilder)震荡指标,可在MQL5智能交易系统(EA)中形成互补搭配。一目均衡表本身功能多元,但在本文中,我们主要利用其识别支撑与阻力位的特性。同时,我们也使用ADX来判断趋势强弱。我们依旧通过MQL5向导来构建并测试这一组合的潜在效果。
新手在交易中的10个基本错误 新手在交易中的10个基本错误
新手在交易中会犯的10个基本错误: 在市场刚开始时交易, 获利时不适当地仓促, 在损失的时候追加投资, 从最好的仓位开始平仓, 翻本心理, 最优越的仓位, 用永远买进的规则进行交易, 在第一天就平掉获利的仓位,当发出建一个相反的仓位警示时平仓, 犹豫。
神经网络在交易中的应用:演员—导演—评论家框架(终篇) 神经网络在交易中的应用:演员—导演—评论家框架(终篇)
演员-导演-评论家框架是经典智能体学习架构的演进。本文介绍了该方法在金融市场条件下的实现和调整方面的实践经验。