English Русский Español 日本語 Português
preview
神经网络在交易中的应用:基于自适应模态分解的时间序列预测(终篇)

神经网络在交易中的应用:基于自适应模态分解的时间序列预测(终篇)

MetaTrader 5交易系统 |
82 1
Dmitriy Gizlyk
Dmitriy Gizlyk

概述

上一篇文章中,我们介绍了 ACEFormer 框架,这是一个专门针对金融市场特征进行优化的时间序列预测模型。我们研究了概率注意力机制的基本原理、在 OpenCL 内核级别实现该机制的算法,以及在保持预测准确性的同时提高计算效率的技术。然而,还有一个同样重要的问题尚未展开讨论:即这些机制如何融入主程序,以及如何在计算模块与交易算法之间建立可靠的通信。

在本文中,我们将继续实现 ACEFormer 框架作者提出的方法。我们的主要重点将放在程序端构建相应的算法上。然而,在继续讨论技术实现之前,让我们简要回顾一下 ACEFormer 的核心概念和优势。该框架的核心是 ACEEMDAlias Complete Ensemble Empirical Mode Decomposition with Adaptive Noise,带自适应噪声的别名完全总体经验模态分解)算法,该算法旨在从金融时间序列中去除噪声。ACEEMD 在保留图表上关键转折点的同时,有效解决了边界效应问题,并且在平滑过程中没有丢失关键信息。特别关注的是第一个固有模态函数(IMF),明确去除该函数有助于消除高频噪声,同时不过度抑制有用信号。

这一阶段的输出是时间序列的自适应模态表示,该表示将作为基于带有概率注意力机制的 Transformer 架构构建的蒸馏模块的输入。这种方法不仅能过滤掉市场噪声,还能使模型专注于历史数据中真正有信息的区域,从而在高市场波动性和随机性的条件下提高预测准确性。

处理后的表示随后会通过一个传统的自注意力模块,使模型能够捕捉原始序列的全局上下文,并提高不同时间范围之间的一致性。通过将局部概率注意力与全局自注意力相结合,该模型能够同时关注剧烈的市场逆转和持续的趋势。

该架构的最终阶段是一个最终预测头,它将高级特征表示转换为特定的数值预测。

ACEFormer 结合了两种方法的优势:经验模态分解的稳健性和深度注意力机制的灵活性。同时,该模型足够紧凑,可在用户终端中实时部署,且无需过多的计算资源。得益于其模块化架构和对并行处理的支持,ACEFormer 可以轻松适应从短期动量模型到中期仓位交易系统等多种交易策略。

下面展示了作者提供的 ACEFormer 框架示意图。



构建概率注意力模块

在完成了 OpenCL 程序端概率注意力机制的实现后,我们现在进入下一阶段:在主程序端开发相应的高级模块。为了实现这一目标,我们创建了一个专门的 CNeuronMHProbAttention 对象,该对象封装了完整的概率注意力算法。

新类继承自 CResidualConv,该类已经实现了一种由两个通过残差连接相连的连续卷积层组成的架构。这使我们能够专注于实现概率注意力逻辑,而无需修改父类提供的前馈机制。因此,该设计实现了高度的模块化,同时便于与模型的其他组件实现无缝集成。

新类的结构如下所示。

class CNeuronMHProbAttention :  public CResidualConv
  {
protected:
   uint                       iWindow;
   uint                       iWindowKey;
   uint                       iHeads;
   uint                       iUnits;
   uint                       iTopKQuerys;
   uint                       iRandomKeys;
   int                        ibScore;
   //---
   CNeuronConvOCL             cQKV;
   CNeuronBaseOCL             cQ;
   CNeuronBaseOCL             cKV;
   CNeuronBaseOCL             cRandomK;
   CNeuronBaseOCL             cMHAttentionOut;
   CNeuronConvOCL             cPooling;
   CNeuronTransposeOCL        cTranspose[2];
   CNeuronConvOCL             cScaling;
   //---
   virtual bool               RandomKeys(CBufferFloat* indexes, int random, int units, int heads);
   virtual bool               QueryImportance(void);
   virtual bool               TopKIndexes(void);
   //---
   virtual bool               AttentionOut(void);
   virtual bool               AttentionInsideGradients(void);
   //---
   virtual bool               feedForward(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               updateInputWeights(CNeuronBaseOCL *NeuronOCL) override;
   virtual bool               calcInputGradients(CNeuronBaseOCL *prevLayer) override;

public:
                              CNeuronMHProbAttention(void) {};
                             ~CNeuronMHProbAttention(void) {};
   //---
   virtual bool               Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, 
                                   uint window, uint window_key, uint heads, uint units_count,
                                   ENUM_OPTIMIZATION optimization_type, uint batch);
   //---
   virtual int                Type(void)   const   {  return defNeuronMHProbAttention;   }
   //---
   virtual bool               Save(int const file_handle) override;
   virtual bool               Load(int const file_handle) override;
   //---
   virtual bool               WeightsUpdate(CNeuronBaseOCL *source, float tau) override;
   virtual void               SetOpenCL(COpenCLMy *obj) override;
  };

CNeuronMHProbAttention 的结构反映了概率注意力机制的逐步实现,从随机密钥生成和查询重要性估计开始,到梯度反向传播结束。每个功能组件都作为单独的模块实现,实现了职责的清晰分离,并支持精细的定制。

从架构角度来看, CNeuronMHProbAttention 的一个显著特点是它的内部对象均以静态方式定义,而非在实例化时动态分配。这种设计不仅简化了内存管理,而且在创建该类的实例时,也无需进行动态资源分配。因此,构造函数和析构函数都保持为空,从而提高了对象创建和销毁的可靠性和可预测性。

所有内部对象和变量的必要初始化都集中在 Init 方法中,该方法实际上充当了类构造函数。它负责组装所有组件,配置它们的参数,并分配所需的资源。

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

由于新类派生自 CResidualConv(实现了残差卷积架构),因此初始化过程首先调用父类的相关方法。这立即提供了底层基础设施,包括卷积层、残差连接和缓冲区初始化。因此,我们不会在重新实现上浪费资源,而是可以专注于独特的注意力逻辑。

下一步是初始化定义注意力机制行为的关键参数。

iWindow = window;
iWindowKey = MathMax(5, window_key);
iHeads = MathMax(1, heads);
iUnits = units_count;
iTopKQuerys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits));
iRandomKeys = int(MathMin(5 * MathMax(MathLog(iUnits),1), iUnits));

除了常见的参数外,还引入了两个特定于概率注意力机制的新变量:

  • iTopKQuerys — 被选中进行进一步处理的最具信息量的查询数量;
  • iRandomKeys查询选择过程中使用的随机抽样的数量。

为确保可扩展性,这两个参数的值均根据输入序列总长度的对数函数进行计算。

对象参数存储完毕后,框架将按顺序构建所有负责实现注意力机制的内部组件。每个对象都按照严格定义的顺序单独进行初始化。要初始化的第一个组件是 cQKV,这是一个卷积层,它能同时生成三个实体:Q查询)、 K)和V)。该层为每个注意力头生成密集的特征表示,同时使用 TANH 激活函数对其进行编码。

int index = 0;
if(!cQKV.Init(0, index, OpenCL, iWindow, iWindow, 3 * iWindowKey * iHeads, iUnits, optimization, iBatch))
   return false;
cQKV.SetActivationFunction(TANH);

值得回顾的是,之前开发的实现概率注意力机制的核函数为查询张量使用了专用缓冲区。因此,下一步是创建两个额外的对象来分隔这些实体。

index++;
if(!cQ.Init(0, index, OpenCL, cQKV.Neurons() / 3, optimization, iBatch))
   return false;
index++;
if(!cKV.Init(0, index, OpenCL, 2 * cQ.Neurons(), optimization, iBatch))
   return false;

以下对象存储随机选择的索引。

index++;
if(!cRandomK.Init(0, index, OpenCL, iHeads * MathMax(iRandomKeys, iTopKQuerys), optimization, iBatch))
   return false;

请注意,此层的大小由两个值中的较大值决定:采样的数量和最重要的查询的数量。这个想法是为了确保在计算过程中可能需要的所有索引都有足够的内存容量。由于我们无法预先确定哪个数量会更大 — 随机iRandomKeys 还是查询iTopKQuerys — 我们根据两者中的最大值来分配内存。然后将结果值乘以注意力头的数量(iHeads),因为每个头都是独立运作的,因此需要各自的一组索引。

因此,使用单个对象 cRandomK 来存储随机采样的索引和最具信息量的查询。这使内部结构更紧凑,减少了对象的总数,并简化了内存管理。

下一个组件用于存储最具信息量的查询所对应的多头注意力输出。

index++;
if(!cMHAttentionOut.Init(0, index, OpenCL, iTopKQuerys * iHeads * iWindowKey, optimization, iBatch))
   return false;

紧接着是自适应聚合层,它将所有注意力头的输出组合成一个统一的表示。

index++;
if(!cPooling.Init(0, index, OpenCL, iHeads * iWindowKey, iHeads * iWindowKey, iWindow, iTopKQuerys,
                                                                              optimization, iBatch))
   return false;
cPooling.SetActivationFunction(TANH);

在这个阶段,算法已经产生了概率注意力机制的输出。但是,需要强调一个重要的架构考虑因素:这些输出仅对应于选定的前 K 个查询。因此,所得张量的时间维度远小于原始输入序列。

这会带来架构层面的维度不一致问题。概率注意力块产生压缩表示,而传统的自注意力架构则保留原始序列长度。保持这种维度一致性对于正确应用残差连接至关重要,因为残差连接能确保训练的稳定性和梯度的有效传播。

为了解决这一矛盾,我们采取了以下策略:

  • 首先,将注意力输出矩阵进行转置,使其布局适合沿特征维度进行卷积。

index++;
if(!cTranspose[0].Init(0, index, OpenCL, iTopKQuerys, iWindow, optimization, iBatch))
   return false;

  • 接下来,应用卷积层 cScaling 将输出恢复为原始序列长度。因此,张量重新获得了与原始输入相同的维度。值得注意的是,这种上采样是针对每个特征独立执行的,从而可以在考虑全局注意力结构的同时重建每个时间步。
index++;
if(!cScaling.Init(0, index, OpenCL, iTopKQuerys, iTopKQuerys, iUnits, iWindow, optimization, iBatch))
   return false;
cScaling.SetActivationFunction(None);
  • 最后,通过执行第二次转置操作,数据被恢复为其原始布局。
if(!cTranspose[1].Init(0, index, OpenCL, iWindow, iUnits, optimization, iBatch))
   return false;

该操作序列实现了随机概率注意力机制与传统自注意力架构要求之间的最大兼容性。该模型在保持结构完整性的同时,还维持了稳定训练所需的计算效率和灵活性。

特别关注 ibScore 缓冲区,该缓冲区存储反向传播过程中所需的中间注意力分数。此缓冲区仅在 OpenCL 上下文中分配。

   ibScore = OpenCL.AddBuffer(sizeof(float) * iTopKQuerys * iUnits * iHeads, CL_MEM_READ_WRITE);
   if(ibScore < 0)
      return false;
//---
   return true;
  }

在所有内部对象初始化完毕后,该方法通过返回逻辑成功状态来结束。

下一个主要阶段是前馈算法的实现。然而,在处理主要数据流之前,需要进行少量的准备工作。

与本系列中开发的大多数模块一样,有几种方法只是将 OpenCL 内核执行任务加入队列。由于之前的文章已经讨论过它们的基础算法,因此在此重复这些内容意义不大。相反,我们将重点关注 RandomKeys 方法中实现的采样算法。

概率注意力机制的目标是减少在识别最具信息量的查询时必须检查的的数量。这降低了计算成本,同时引入了一个随机成分,有助于缓解过拟合问题,并提高模型的泛化能力。

RandomKeys 方法接收指向索引缓冲区的指针,该缓冲区将填充采样的索引。参数randomunitsheads 分别指定随机样本的数量、可用的总数和注意力头的数量。

bool CNeuronMHProbAttention::RandomKeys(CBufferFloat *indexes, int random, int units, int heads)
  {
   if(!indexes || random > units ||
      indexes.Total() < (random * heads)
     )
      return false;

该方法首先验证输入参数。如果缓冲区指针为空,或者请求的随机样本数量超出了可用的数量,或者缓冲区太小,则该方法会立即返回 false

一旦验证阶段成功完成,就会创建一个 random × heads 矩阵,其中每一列对应一个单独的注意力头,每一行存储一个采样的位置。

matrix<float> ind = matrix<float>::Zeros(random, heads);

接下来有两种可能的情况。如果不需要采样(random == units),则矩阵仅用连续索引填充,这意味着所有可用的都被使用。

if(random == units)
  {
   for(int r = 0; r < random; r++)
     {
      for(int c = 0; c < heads; c++)
         ind[r, c] = (float)r;
     }
  }

当所选的数量(random)小于可用值的总数(units)时,简单的随机抽样策略可能会产生一个对可用键所覆盖的数据范围不具代表性的样本。仅仅从整个范围内随机抽取索引,可能会导致出现重复值、局部聚类,或者相反,某些区域完全没有被表示,从而产生空白。因此,模型可能无法观察到输入序列的重要部分,从而对训练过程产生负面影响。

为了避免这个问题,我们的实现采用了均匀分层抽样。首先,将整个索引范围划分为大小相等的区间。

else
  {
   double step = double(units) / random;

结果可能不是整数,这完全可以接受,因为样本大小很少是序列长度的精确因子。

对于每个分层区间(范围段),从其边界内随机选择一个值。

 for(int r = 0; r < random; r++)
   {
    for(int c = 0; c < heads; c++)
       ind[r, c] = float(int((r + MathRand() / 32767.0) * step));
   }
}

因此,每个注意力头都会接收到各自独立生成的一组索引,而这些索引集合共同覆盖整个序列,不会偏向或过度集中在特定区域。

采样过程完成后,样本将被写入索引缓冲区,随后在 OpenCL 内核中使用。

   if(!indexes.AssignArray(ind) ||
      !indexes.BufferWrite())
      return false;
//---
   return true;
  }

这种方法具有几个重要优势。首先,它保证了整个特征空间的均匀覆盖。其次,它能在最小化重复样本可能性的同时,防止索引超出有效范围。因此,采样子集的代表性显著增强,为模型提供了对原始数据更全面、更均衡的视角。在处理金融时间序列时,这一点尤为重要,因为低波动性时期与市场快速波动时期同样具有信息价值。

准备阶段完成后,我们可以继续进行实施的核心部分 — feedForward 方法,该方法执行完整的概率注意力算法。

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

第一个操作调用 cQKV 卷积层的 FeedForward 方法。顾名思义,这一层生成一个包含查询表示的拼接张量。输出是一个单个张量,其中三个分量按顺序存储。

然而,在算法的后续阶段,这些表示必须分成两个张量:一个包含查询Q ),另一个包含KV )。这是通过使用 DeConcat 方法实现的,该方法提取组合QKV 张量的相应部分,并将它们转移到 cQcKV

if(!DeConcat(cQ.getOutput(), cKV.getOutput(), cQKV.getOutput(), iWindowKey * iHeads,
                                                    2 * iWindowKey * iHeads, iUnits))
   return false;

然后通过调用 RandomKeys 方法执行前面描述的采样算法。

if(!RandomKeys(cRandomK.getOutput(), iRandomKeys, iUnits, iHeads))
   return false;

下一阶段调用 QueryImportanceTopKIndexes 方法。这些方法将负责对查询重要性进行排名并选择最具信息量的查询的内核加入队列。

if(!QueryImportance() || !TopKIndexes())
   return false;

算法的核心计算在 AttentionOut 方法中完成。它将负责计算注意力的注意力内核排入队列,该内核仅针对选定的查询

if(!AttentionOut())
   return false;

然后,所有注意力头的输出结果通过 cPooling 卷积层进行聚合。该层还应用了 TANH 激活函数,增强了输出信号的表现力。

if(!cPooling.FeedForward(cMHAttentionOut.AsObject()))
   return false;

随后,对聚合输出进行上采样,以将张量恢复到原始输入序列的维度,从而正确应用残差连接。

if(!cTranspose[0].FeedForward(cPooling.AsObject()))
   return false;
if(!cScaling.FeedForward(cTranspose[0].AsObject()))
   return false;
if(!cTranspose[1].FeedForward(cScaling.AsObject()))
   return false;

注意力模块中的最后一个操作调用了 SumAndNormalize 方法,该方法通过添加残差连接和执行归一化,将放大后的表示与原始输入结合起来。这确保了梯度传播的稳定性,并加速了训练过程中的收敛速度。

if(!SumAndNormilize(cTranspose[1].getOutput(), NeuronOCL.getOutput(), cTranspose[1].getOutput(),
                    iWindow, true, 0, 0, 0, 1))
   return false;

最后,将得到的张量传递给父类的相应方法,该方法完成前向传播阶段并返回输出张量,以供神经网络的下一个模块进行处理。

 return CResidualConv::feedForward(cTranspose[1].AsObject());
}

因此,整个概率注意力机制被封装在 CNeuronMHProbAttention 对象中。这种设计提供了高度模块化、可复用且易于扩展的实现方式。

一旦前向传播完成,训练过程就进入了最关键的阶段:反向传播优化误差梯度以更新模型参数。在此阶段,计算每个组件对总体损失的贡献,然后相应地调整网络权重。对于 CNeuronMHProbAttention 类实现的概率注意力机制,反向传播变得更加复杂,因为梯度仅通过选定的查询子集进行传播。这需要对流程进行细致的组织,并严格遵守计算逻辑。

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

calcInputGradients 方法首先对指向前一层的指针进行基本验证,该指针作为方法的参数之一接收。如果没有对前一层的正确引用,误差梯度就无法在网络中进一步传播。

下一步是一个重要的准备操作:显式清除查询梯度缓冲区。

if(!cQ.getGradient().Fill(0))
   return false;

此操作是必需的,因为注意力算法仅处理查询的一个子集 — 即在前馈过程中选择的最相关的查询。梯度矩阵的其余元素可能仍然包含不再有效的过时或未定义的值,这些值可能会破坏后续的参数更新。因此,在正确的梯度值开始累积之前,整个矩阵会被清空。

接下来,我们调用父类的相应方法。该操作会将误差梯度继续传回 FeedForward 模块。因此,这部分反向传播过程被委托给已经验证过的逻辑。

if(!CResidualConv::calcInputGradients(cTranspose[1].AsObject()))
   return false;

进一步的步骤是依次将误差梯度传播通过概率注意力机制的每个内部块,这与前馈传递的结构完全相同,但顺序相反。梯度首先经过缩放块。

if(!cScaling.calcHiddenGradients(cTranspose[1].AsObject()))
   return false;
if(!cTranspose[0].calcHiddenGradients(cScaling.AsObject()))
   return false;

然后,它们通过多头注意力聚合层进行传播。

if(!cPooling.calcHiddenGradients(cTranspose[0].AsObject()))
   return false;
if(!cMHAttentionOut.calcHiddenGradients(cPooling.AsObject()))
   return false;

这些阶段对于精确重建梯度至关重要,因为前馈过程涉及数据形状和结构的多次变换。现在,必须以正确的顺序将这些操作颠倒过来。

特别关注的是 AttentionInsideGradients 方法,该方法在概率注意力机制内部执行梯度传播。在这里,错误会小心翼翼地通过选定的查询子集进行传播。它是整个反向传播过程中最敏感的部分:即使只有一小部分原始信息参与该过程,它也能确保权重更新的准确性和数学一致性。

if(!AttentionInsideGradients())
   return false;

下一阶段通过将查询的梯度合并为一个 QKV 张量来合并梯度,从而恢复前馈过程中产生的原始表示。如果在 cQKV 块中应用了激活函数,则通过计算该函数的导数来校正梯度。这样即可将其对传播信号的影响纳入计算,并保持了整个过程的数学正确性。

if(!Concat(cQ.getGradient(), cKV.getGradient(), cQKV.getGradient(), iWindowKey * iHeads,
                                                        2 * iWindowKey * iHeads, iUnits))
   return false;
if(cQKV.Activation() != None)
   if(!DeActivation(cQKV.getOutput(), cQKV.getGradient(), cQKV.getGradient(), cQKV.Activation()))
      return false;

然后将此阶段获得的梯度传递给前一层。

if(!prevLayer.calcHiddenGradients(cQKV.AsObject()))
   return false;

唯一剩下的部分是通过残差连接流动的误差梯度。首先,使用应用于原始输入层的激活函数的导数来校正该梯度。然后,将两个分支的梯度相加。

   if(prevLayer.Activation() != None)
      if(!DeActivation(prevLayer.getOutput(), cTranspose[1].getGradient(), cTranspose[1].getGradient(),
                                                                               prevLayer.Activation()))
         return false;
   if(!SumAndNormilize(cTranspose[1].getGradient(), prevLayer.getGradient(), prevLayer.getGradient(),
                       iWindow, false, 0, 0, 0, 1))
      return false;
//---
   return true;
  }

因此, calcInputGradients 方法实现了概率注意力模块的完整反向传播算法。它保留了每次梯度变换的准确性和一致性,使得概率注意力机制能够被集成到复杂的神经网络架构中,而不会存在信息丢失或训练信号失真的风险。

训练过程的最后阶段涉及更新模型参数,并通过 updateInputWeights 方法实现。由于所有可训练参数都位于内部对象中,因此实现过程只需依次调用这些组件的相应更新方法即可。实现过程十分直接,因此本文不会对该方法进行详细讨论。概率注意力模块的完整源代码(包括 updateInputWeights 方法)已在附件中提供。


模型架构

在对基本实现组件进行回顾之后,有必要考察一下训练系统的整体架构。与我们之前的几项工作一样,我们采用了基于演员-导演-评论家范式的分层学习框架。这种方法实现了环境处理、决策制定和行动评估之间的明确分离,这在复杂且高度动态的金融市场环境中尤为重要。

在此框架下,训练了四个模型。第一个组件 — 环境状态编码器 — 起着核心作用。它负责深入分析市场形势,并构建出简洁而信息量丰富的潜在表征。该模型采用 ACEFormer 框架提出的技术,用于预测指定规划期内的未来环境状态。因此,它产生了一个稳定且动态有意义的市场表征,该表征随后成为代理行动的基础。

第二个模型 — 演员 — 同时处理两种信息源。第一部分由当前账户状态和未平仓位组成,而第二部分是由编码器生成的潜在环境表示。演员经过训练,可以选择最符合当前市场状况的行动,其决策随后由其余两个模型进行评估。

导演评论家模型评价演员选择的行为。导演引入了一种严格的二元过滤机制,有助于消除策略上不利的决策,而评论家则通过估算所选动作的预期效用,提供了一种更为柔和的定量评估。这两个组成部分共同使智能体能够开发出稳健且具有适应性的交易策略。

本文重点介绍环境状态编码器的架构,因为它是包含 ACEFormer 框架关键元素的组件。其余模型的架构通常遵循我们之前发表的文章中所述的原则。

与往常一样,输入层由一个足够大的全连接层组成。

//--- Encoder
   encoder.Clear();
//--- Input layer
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBaseOCL;
   int prev_count = descr.count = (HistoryBars * BarDescr);
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

值得注意的是,与原始 ACEFormer 实现不同,我们不会为预测元素附加零值。

数据处理的第一阶段由专用的特征预处理模块执行。该模块将归一化、随机噪声注入和特征维度的卷积变换相结合。

该过程首先对输入数据进行归一化处理。这消除了各个特征之间的尺度差异,并稳定了训练过程。为了进一步提高模型的泛化能力和对局部数据波动的鲁棒性,在归一化过程中引入了可控量的随机噪声。这通过模拟真实市场条件的变化性,起到了一种正则化机制的作用,使模型能够更好地适应不稳定或未曾见过的情形。

经过归一化和噪声增强的数据随后通过卷积层,该层将特征维度转换为后续网络组件所需的格式。此操作可同时减少冗余信息,并在指定的时间窗口内提取特征之间最相关的空间关系。

//--- layer 1
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormWithNoise;
   descr.count = prev_count;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 2
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_count = descr.count = HistoryBars;
   descr.window = BarDescr;
   descr.step = BarDescr;
   int prev_out = descr.window_out = NSkills;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

预处理后,对数据进行转置,以便从时间分析切换到独立特征分析。这使得注意力机制能够识别每个特征内部的有意义的依赖关系,从而改进表示和训练过程。

//--- layer 3
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count = descr.window = prev_out;
   prev_out = descr.count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

接下来进入蒸馏模块。在编码器架构中,它实现了 ACEFormer 框架的核心思想:提取最具信息量的特征并聚合有意义的信息。其核心是概率注意力模块,该模块首先通过关注最重要的特征来处理输入。这使得模型能够专注于原始信号中最具信息量的部分。

//--- layer 4
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMHProbAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

紧接在注意力模块之后的是一个卷积层,该层用于减少特征的时间维度。其目的是在压缩信息的同时,保留其基本特征。

//--- layer 5
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_out = descr.count = (prev_out + 1) / 2;
   descr.window = 2;
   descr.step = 2;
   int filt=descr.window_out = 5;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

随后是一个聚合模块,其工作方式类似于最大池化层,即在每个窗口内选择最强的激活值,并保留主要特征。

//--- layer 6
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count*prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

最后,归一化层可以稳定特征分布并加速训练过程。

//--- layer 7
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

我们的实现采用了三个连续的蒸馏模块,每个模块都独立地降低单个特征时间序列的维度。

//--- layer 8
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMHProbAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 9
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_out = descr.count = (prev_out + 1) / 2;
   descr.window = 2;
   descr.step = 2 ;
   filt=descr.window_out = 3;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 10
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count*prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 11
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 12
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronMHProbAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 13
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   prev_out = descr.count = (prev_out + 1) / 2;
   descr.window = 2;
   descr.step = 2 ;
   filt=descr.window_out = 3;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 14
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronProofOCL;
   descr.count = prev_count*prev_out;
   descr.window = filt;
   descr.step = filt;
   descr.layers = prev_count;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }
//--- layer 15
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronBatchNormOCL;
   descr.count = prev_count * prev_out;
   descr.batch = 1e4;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

因此,蒸馏模块不仅降低了维度,还提取并浓缩了原始数据的精华,生成了一个对噪声具有鲁棒性的丰富潜在表示。

然后,编码器架构包含一个两层的自注意力模块,其目的是捕获压缩潜在表示中特征之间的依赖关系。这有助于发现隐藏的关系,并跨不同的特征维度同步信息。

//--- layer 16
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronDMHAttention;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = 4;
   descr.layers = 2;
   descr.window_out = 32;
   descr.batch = 1e4;
   descr.optimization = ADAM;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

因此,该模型学会了将数据视为一个相互作用的组件构成的连贯系统,而不仅仅是一系列独立的观测结果。

为了独立预测每个单独的特征序列,编码器使用了两个连续的卷积层。这种设计能高效地将经抽象后的潜在表示转化为每个目标特征的数值预测。

第一卷积层使用数量增多的滤波器(预测参数数量的 4 倍),并应用SoftPlus激活函数。这会产生一个平滑的正值转换,在提高训练稳定性的同时抑制噪声。

//--- layer 17
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 1;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = 4 * NForecast;
   descr.layers = prev_count;
   descr.activation = SoftPlus;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

第二层通过将输出数量减少到预测参数的数量( NForecast )来完成解码过程,并应用 TANH 激活函数。这有助于将预测值保持在可控范围内,这在处理归一化数据时尤为重要。

//--- layer 18
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = 1;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = NForecast;
   descr.layers = prev_count;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

这种架构简单却有效。它为生成准确预测提供了足够的灵活性,同时保持了单个特征预测的独立性 — 在金融时间序列建模中,这是一个关键特性,因为每个变量都可能携带独特的信息。

在编码器的最后阶段,预测值被转换回原始数据空间,从而使模型输出能够被解释。

第一个操作是张量转置。它将时间维度恢复到其原始方向,并将预测值与其对应的时间步长对齐。

//--- layer 19
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronTransposeOCL;
   descr.count = prev_count;
   prev_count=descr.window = prev_out;
   prev_out=descr.count;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

接下来,卷积层将特征维度映射回原始输入数据的格式。本质上,这是一种投影:模型压缩或扩展潜在表示,使其与原始数据形成的空间相兼容。

//--- layer 20
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count;
   descr.window = prev_out;
   descr.step = prev_out;
   prev_out = descr.window_out = BarDescr;
   descr.layers = 1;
   descr.activation = TANH;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

最后,逆归一化将模型内部归一化表示的预测结果转换回真实世界的值,这些值可供用户解读,并可直接用于交易决策。

//--- layer 21
   if(!(descr = new CLayerDescription()))
      return false;
   descr.type = defNeuronRevInDenormOCL;
   descr.count = prev_count * prev_out;
   descr.layers = 1;
   descr.activation = None;
   if(!encoder.Add(descr))
     {
      delete descr;
      return false;
     }

这些操作共同弥合了模型内部表示与真实市场之间的差距,将抽象的潜在预测转化为可操作的预测结果。

所有可训练模型的完整架构已在附件中提供。



测试

MQL5 环境中,已有大量工作致力于 ACEFormer 框架的适配与实现。该框架的核心组件现已集成到可训练模型的架构中。下一步是在真实的历史数据上评估生成的模型。

训练数据集是通过运行 MetaTrader 5 策略测试器,并使用随机代理策略对 2024 年全年的 1 分钟 EURUSD 价格数据进行测试而生成的。这种方法使模型能够应对广泛的市场场景,并提高了学习行为的通用性。

训练分两个阶段进行。第一阶段为离线训练,在优化误差稳定之前不更新数据集。为此,我们在图表上运行了 Study.mq5 EA。第二阶段采用在线训练,在策略测试器中使用 StudyOnline.mq5 EA,使模型能够在与真实交易非常相似的条件下进行微调。

为了客观地评价结果,我们使用涵盖 2025 年 1 月至 3 月的样本外历史数据对训练好的模型进行了测试。这消除了过拟合现象,并凸显了所得结果的实用价值。

在训练和测试过程中,所有其他环境参数和技术指标均保持不变,以确保观察到的结果能够反映所学策略的质量。

测试结果如下所示。

总体而言,该模型在测试期间实现了盈利,共执行了 13 笔交易。盈利交易占比略高于 50%。然而,值得注意的是,在三个月的评估期内仅有 13 笔交易,这表明交易活动相对较少。

一种可能的解释是概率注意力机制本身的性质。通过仅选择最具信息量的特征和查询,该模型实现了更好的泛化能力,但可能对较弱的交易信号变得不那么敏感。

另一个促成因素可能是训练数据集的代表性有限。该模型在训练过程中可能并未遇到足够多样化的市场条件,因此在整个评估期间,在类似情况下无法自信地采取行动。扩大训练数据的规模和多样性可以提高智能体的行为灵活性。


结论

在本文中,我们探讨了 ACEFormer 框架,该框架提供了一种从时间序列中提取信息特征并构建紧凑潜在表示的有效方法。其架构融合了概率注意力机制、特征蒸馏和深度序列转换,使其特别适合分析嘈杂且高度波动的金融数据。

从实用角度出发,我们使用 MQL5 实现了 ACEFormer 的关键组件,并将其集成到可训练的演员–导演–评论家架构中。特别关注了环境状态编码器的设计,该编码器实现了框架所提出的特征处理技术。

实验结果令人鼓舞:该模型在样本外测试期间产生了正收益,证实了所提出架构的整体可行性。同时,实验结果显示交易活动相对较少,这可能是由于概率注意力的特性以及训练数据集的多样性有限所导致的。这些观察结果为未来的研究提供了明确的方向。


参考


本文中用到的程序

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

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

附加的文件 |
MQL5.zip (2720.51 KB)
最近评论 | 前往讨论 (1)
Aleksander
Aleksander | 8 5月 2025 在 11:26
13笔交易是指1个货币对吗?如果将10个货币对纳入分析,会有多少笔交易?
神经网络在交易中的应用:不依赖特定数据的时间序列泛化(Mamba4Cast) 神经网络在交易中的应用:不依赖特定数据的时间序列泛化(Mamba4Cast)
在本文中,我们介绍了 Mamba4Cast 框架,并深入探讨了其关键组件之一:基于时间戳的位置编码。本文展示了如何结合数据的日历结构来构建时间嵌入。
灰色模型在金融时间序列技术分析中的应用研究 灰色模型在金融时间序列技术分析中的应用研究
本文研究灰色模型,这是一种具有潜力的工具,能够提升交易者在分析与策略构建方面的能力。我们探讨了将该模型应用于技术分析并构建交易策略的若干实现方案。
MQL5 向导技巧(第七十七部分):使用 Gator 振荡器与积累/派发振荡器(AD Oscillator) MQL5 向导技巧(第七十七部分):使用 Gator 振荡器与积累/派发振荡器(AD Oscillator)
Gator 振荡器(Gator Oscillator)由比尔·威廉姆斯(Bill Williams)开发,与累积/派发振荡器(Accumulation/Distribution Oscillator)是另一对可在 MQL5 EA 中协同使用的指标组合。我们使用 Gator 振荡器来确认趋势,同时使用 AD 振荡器通过检查成交量来确认趋势。在探索这种指标组合时,我们一如既往地使用 MQL5 向导来构建和测试它们的潜力。
数据科学与机器学习(第四十四部分):利用向量自回归(VAR)预测外汇 OHLC 时间序列 数据科学与机器学习(第四十四部分):利用向量自回归(VAR)预测外汇 OHLC 时间序列
本文探讨如何使用向量自回归(VAR)模型预测外汇 OHLC(开盘价、最高价、最低价、收盘价)时间序列数据。文章涵盖 VAR 模型的实现、模型训练以及在 MetaTrader 5 中的实时预测,帮助交易者分析相互关联的货币走势,改进交易策略。