
交易中的神经网络:运用形态变换器进行市场分析
概述
涵盖过去的十年,深度学习(DL)在各个领域都取得了重大进展,这些进步引起了金融市场研究人员的关注。受到深度学习成功的启发,许多人打算将其应用于市场趋势预测、和复杂数据相互关系分析。这种分析的一个关键层面是原生数据的表示格式,其保留了所分析金融产品的内在关系和结构。大多数现有模型都依据同构图,这限制了它们捕获与市场形态关联的丰富语义信息的能力。类似于在自然语言处理中所用的 N-元语法,频繁发生的市场形态可被用来更精确地识别互连、及预测趋势。
为解决这个问题,我们决定采用化学元素分析领域的某些方式。与市场形态极其相似,基序(有意义的子图)频繁出现在分子结构当中,并可用来揭示分子性质。我们来探索一下 Molformer 框架,其在论文《Molformer:基于基序变换器的 3D 异质分子图》 中阐述。
Molformer 方法的作者定义了一种新颖的异质分子图(HMG)作为模型的输入,由原子和基序水平两者构成节点。这种设计为集成不同级别的节点提供了一个干净的接口,并防止由原子语义分割不当而导致的误差传播。关于基序,作者针对不同的分子类型采用不同的策略。对于小分子,基序词汇由功能组判定,以化学领域知识分组。对于由连续氨基酸组成的蛋白质,引入了一种基于强化学习(RL)的智能基序挖掘方法,以便识别最重要的氨基酸亚序列。
为了有效地与 HMG 保持一致,Molformer 框架引入了一个基于变换器架构构建的等变几何模型。Molformer 在两个关键方面不同于之前研究过的基于变换器模型。首先,它采用异构自注意力(HSA)来捕获不同级别节点之间的互动。其二,引入一种注意力最远点采样(AFPS)算法来聚合节点特征,并获得整个分子的全面表示。
作者的论文发表了实验结果,展示了该方式在应对化工行业挑战方面的有效性。我们来评估这些方法解决金融市场趋势预测任务的潜力。
1. Molformer 算法
基序代表频繁出现的亚结构形态,并作为构建复杂分子结构的模块。它们封装了整个分子的丰富生化特性。在化学社区,已开发出一套标准原则,识别小分子中含有明显功能性能力的基序。在大蛋白分子中,基序对应于影响其功能的蛋白质共有的局部三维结构区域、或氨基酸序列。每个基序典型情况仅由少数元素组成,并可以描述次要结构元素之间的联系。基于这一特性,Molformer 框架的作者设计了一种启发式方式,使用强化学习(RL)发现蛋白质基序。在他们的工作中,他们建议关注由四个氨基酸组成的基序,其形成最小的多肽,并在蛋白质中支配不同功能特性。在该阶段,主要意向是从 K 四次氨基酸基质中辨别最有效的词典 𝓥。鉴于目标是为特定任务找到最优词典,故仅研究来自下游数据集的现有四元,而非所有可能的组合实际上是可行的。
学到的词典 𝓥 用作基序提取、及在下游任务中构造 HMG 的模板。然后基于这些 HMG 对 Molformer 进行训练。其有效性则被当作通过政策梯度更新参数 θ 的奖励 r。如是结果,代理人可为特定任务选择四次基序的最优词典。
值得注意的是,所提议基序挖掘过程表示为一个单步博弈,在于 πθ 政策网络每次迭代只生成一次词典 𝓥。因此,轨迹只由一个动作组成,而基于所选词典 𝓥 的 Molformer 成果构成了整体奖励的一部分。
该框架的作者将基序和原子分开,基序则被当作形成 HMG 的新节点。这令基序级和原子级表征脱钩,从而促进了模型在基序级准确提取语义的任务。
类似于自然语言中短语和单个词汇之间的关系,分子中的基序比原子携带更高等级的语义含义。由此,它们在定义其原子成分的功能效力方面扮演着至关重要的角色。Molformer 的作者将每个基序类别当作一种新的节点类型,并构建 HMG 作为模型的输入,譬如 HMG 同时包含基序级节点和原子级节点。每个基序的位置由其成分元素的 3D 坐标的加权合计表示。与单词分段类似,由多级节点组成的 HMG 利用原子信息来指导分子表示学习,防止由于语义分段不当而导致的误差传播。
Molformer 配以若干专为 3D HMG 设计的新分量修改了变换器架构。每个编码器模块由 HSA、一个前馈网络(FFN)、和两级归一化组成。随后是注意力最远点采样(AFPS),自适应创建分子表示,然后将其投喂到完全连接预测器之中,从而跨越广阔范围的下游任务正确预测。
分别在原子和基序级上依据 N+M 节点制订 HMG 之后,重点是为模型提供区分多阶节点之间相互作用的能力。为达成这一点,作者引入了一个函数 φ(i,j)→Z,其以三种类型定义任意两个节点之间的关系:原子-原子、原子-基序、和 基序-基序。然后引入一个可学习的标量 bφ(i,j),根据它们在 HMG 内的层次化关系自适应地处理所有节点。
进而,作者还研究了三维分子几何学的运用。鉴于 3D 平移和旋转等全局变化的健壮性是分子表示学习的基本原则,故他们旨在通过在成对距离矩阵 𝑫 上应用卷积操作来确保旋转平移不变性。
甚至,事实证明,在稀疏 3D 空间中运用局部境况已被证明重要性。然而,据观察,自注意力 有效地捕获全局数据形态,但又往往会忽略局部环境。基于这一观察,作者针对 自注意力 施加基于距离的约束,以便从局部和全局境况中提取多尺度形态。为此目的,开发了一种多尺度方法来可靠地捕获细节。具体来说,在每个尺度 s 上超出特定距离阈值 τs 的节点将被掩盖。然后,把不同尺度下提取的特征组合为一个多尺度表示,并投喂到 FFN 之中。
Molformer 框架的原始可视化提供如下。
2. 利用 MQL5 实现
在回顾了 Molformer 方法的理论层面之后,我们现在转入本文的实践部分,其中我们利用 MQL5 实现所提议方法的解释。如我们之前的工作,我们把实现框架的整个过程划分为执行重复操作的单独模块。
2.1注意力池化
起初,我们将 R-MAT 方法作者提出的基于依赖关系的池化算法隔离到一个独立的类之中。
不要感到惊讶,我们实现 Molformer 框架先从整合 R-MAT 方法开始。这两种方法都是为了解决化工行业中的类似挑战而提出的。在我们看来,我们会用到它们之间的一些交集。基于依赖关系的池化算法就是这些交集之一。
我们在 CNeuronMHAttentionPooling 类中规划该算法的进程,其结构如下所示。
class CNeuronMHAttentionPooling : public CNeuronBaseOCL { protected: uint iWindow; uint iHeads; uint iUnits; CLayer cNeurons; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronMHAttentionPooling(void) {}; ~CNeuronMHAttentionPooling(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint units_count, uint heads, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronMHAttentionPooling; } //--- 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; };
在该类中,我们声明了三个内部变量和一个动态数组,我们将按照它们被调用的顺序在其中存储指向内部对象的指针。数组本身声明为静态,允许我们将类的构造函数和析构函数留空。所有继承的、和新声明得对象,都在 Init 方法中执行初始化,取其参数常量来明确定义所创建对象的架构。
bool CNeuronMHAttentionPooling::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint units_count, uint heads, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) return false;
在对象初始化方法主体中,我们首先调用父类中的同名方法,其中已实现了部分必要的控件和继承对象的初始化算法。之后,我们把从外部程序接收的常量存储到内部变量之中。
iWindow = window; iUnits = units_count; iHeads = heads;
We prepare our dynamic array.
cNeurons.Clear(); cNeurons.SetOpenCL(OpenCL);
然后我们开始创建嵌套对象结构。在此,我们创建了一个两层 MLP,其中我们使用双曲正切在神经层之间创建非线性。
int idx = 0; CNeuronConvOCL *conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, idx, OpenCL, iWindow*iHeads, iWindow*iHeads, 4*iWindow, iUnits, 1, optimization, iBatch) || !cNeurons.Add(conv) ) return false; idx++; conv.SetActivationFunction(TANH); conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, idx, OpenCL, 4*iWindow, 4*iWindow, iHeads, iUnits, 1, optimization, iBatch) || !cNeurons.Add(conv) ) return false;
MLP 的输出由 Softmax 函数归一化序列的各个元素项。
idx++; conv.SetActivationFunction(None); CNeuronSoftMaxOCL *softmax = new CNeuronSoftMaxOCL(); if(!softmax || !softmax.Init(0, idx, OpenCL, iHeads * iUnits, optimization, iBatch) || !cNeurons.Add(softmax) ) return false; softmax.SetHeads(iUnits); //--- return true; }
我们在结束该方法时向调用程序返回一个布尔结果,指示操作成功。
重点要注意,在这种情况下,我们不会执行任何数据缓冲区指针替换。这是因为我们创建的对象只生成中间数据。所创建对象的真正结果是经所创建 MLP 的归一化输出,再乘以输入数据张量形成的。然后,该操作的结果将存储在从父类继承的相应缓冲区之中。类似的方式也适用于误差梯度缓冲区。
类的初始化方法完成后,我们继续在 feedForward 方法中构造前向通验算法。
bool CNeuronMHAttentionPooling::feedForward(CNeuronBaseOCL *NeuronOCL) { CNeuronBaseOCL *current = NULL; CObject *prev = NeuronOCL;
在方法参数中,我们接收指向源数据对象的指针。在方法主体中,我们声明了两个局部变量,临时存储指向对象的指针。我们传递指向源数据对象的指针之一。
接下来,我们组织一个循环遍历内部 MLP 对象,并按顺序调用内部模型的同名方法。
for(int i = 0; i < cNeurons.Total(); i++) { current = cNeurons[i]; if(!current || !current.FeedForward(prev) ) return false; prev = current;; }
循环的所有迭代完成后,我们获得序列中每个单独元素的注意力头的影响系数。现在,如前所述,我们需要按获得的系数乘以输入数据张量,来计算输入数据中注意力头的加权平均值。该张量乘法的结果被存储在对象的结果缓冲区之中。
if(!MatMul(current.getOutput(), NeuronOCL.getOutput(), Output, 1, iHeads, iWindow, iUnits)) return false; //--- return true; }
最后,我们把操作的布尔结果返回给调用程序,结束该方法。
我建议把该类的反向传播方法留给独立学习。该类及其所有方法的完整代码可在附件中找到。
2.2形态提取
在我们工作的下一阶段,我们将创建形态提取对象。如理论部分所述,形态嵌入在传递给模型之前被添加到输入数据张量当中。不过,我们的方式有所不同:我们将向模型投喂标准数据集作为输入,且将在模型本身内执行式形态提取、以及它们的嵌入与输入数据张量的级联。
重点要注意,添加到输入数据中的每个形态嵌入都必须拥有单个序列元素的维度,并且位于同一子空间内。第一个问题将经由架构决策来解决。第二个问题是,我们将在形态嵌入的训练中尝试解决。
为了完成这些任务,我们将创建一个新类 CNeuronMotifs。其结构如下所示。
class CNeuronMotifs : public CNeuronBaseOCL { protected: CNeuronConvOCL cMotifs; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronMotifs(void) {}; ~CNeuronMotifs(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint dimension, uint window, uint step, uint units_count, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronMotifs; } //--- 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; //--- virtual void SetActivationFunction(ENUM_ACTIVATION value) override; };
在该类中,我们只声明一个内部卷积层,其负责执行形态嵌入功能。不过,值得注意的是,我们覆盖了指定激活函数的方法。有趣的是,在我们之前的任何实现中都没有覆盖该方法。在这种情况下,这样做是为了将内层的激活函数与对象本身的激活函数同步。
void CNeuronMotifs::SetActivationFunction(ENUM_ACTIVATION value) { CNeuronBaseOCL::SetActivationFunction(value); cMotifs.SetActivationFunction(activation); }
我们在 Init 方法中初始化所声明卷积层,以及所有继承的对象。在该方法参数中,我们传递常量,这就允许我们唯一地判定正在创建的对象的架构。
bool CNeuronMotifs::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint dimension, uint window, uint step, uint units_count, ENUM_OPTIMIZATION optimization_type, uint batch ) { uint inputs = (units_count * step + (window - step)) * dimension; uint motifs = units_count * dimension;
然而,与我们之前研究的类似方法不同,在这种情况下,我们没有足够的数据直接从父类调用同名方法。这主要是由于结果缓冲区的大小。如上所述,我们期望的输出是输入数据和形态嵌入的级联张量。因此,我们将首先基于可用数据判定输入数据张量和形态嵌入张量的大小,然后再调用父类的初始化方法,传递判定的大小之和。
if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, inputs + motifs, optimization_type, batch)) return false;
下一步是根据从外部程序接收的参数初始化内部形态嵌入卷积层。
if(!cMotifs.Init(0, 0, OpenCL, dimension * window, dimension * step, dimension, units_count, 1, optimization, iBatch)) return false;
注意,返回的嵌入大小等于输入数据的维度。
我们使用上面覆盖的方法强制取消激活函数。
SetActivationFunction(None); //--- return true; }
之后,我们通过将作的布尔结果传递给调用程序,并完结该方法。
对象初始化之后是前馈通验流程的构造,我们在 feedForward 方法中实现这些流程。此处的一切都非常直截了当。
bool CNeuronMotifs::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false;
它取指向输入数据对象的指针作为参数,第一步是验证该指针的有效性。之后,我们同步输入数据层和当前对象的激活函数。
if(NeuronOCL.Activation() != activation)
SetActivationFunction((ENUM_ACTIVATION)NeuronOCL.Activation());
该操作允许我们将嵌入层的输出区域与输入数据同步。
仅有在准备工作运作之后,我们才会运作内层前馈通验。
if(!cMotifs.FeedForward(NeuronOCL)) return false;
然后我们把获得的嵌入张量与输入数据级联起来。
if(!Concat(NeuronOCL.getOutput(), cMotifs.getOutput(), Output, NeuronOCL.Neurons(), cMotifs.Neurons(), 1)) return false; //--- return true; }
我们将级联的张量写入从父类继承的结果缓冲区,并通过返回一个布尔结果来结束该方法,该结果指示调用程序的操作成功。
接下来,我们转到反向传播方法的工作。或许正如您猜的那样,它们的算法同样简单。例如,在误差梯度分布方法 calcInputGradients 中,我们仅只执行一次从父类继承的误差梯度缓冲区的脱级联操作,在输入数据对象和内部层之间分派数值。
bool CNeuronMotifs::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) return false; if(!DeConcat(NeuronOCL.getGradient(),cMotifs.getGradient(),Gradient,NeuronOCL.Neurons(),cMotifs.Neurons(),1)) return false; //--- return true; }
不过,这种表面简单需要一些澄清。首先,我们未经相应对象的激活函数导数来调整传递给输入数据和内部层的误差梯度。在这种情况下,这般操作是多余的。这是通过同步对象的激活函数指针、内部层、及我们在设计前馈通验方法时建立的输入数据来达成的。这个简单的操作令我们能够获得误差梯度,已在对象结果层面,经由正确激活函数的导数进行了调整。由此,我们针对已调整误差梯度执行脱级联。
第二点注意,我们不会将误差梯度从内部形态提取层传递至输入数据。有趣的是,这样做的原因是我们任务的性质:从输入数据中提取形态。我们的目标是识别显耀的形态,而不是把输入数据 “拟合” 到所需的形态。然而,很容易看出,输入数据仍通过直接数据流接收自己的误差梯度。
该类及其所有方法的完整代码可在附件中找到。
2.3多尺度注意力
我们需要创建的另一个 “构建模块” 是多尺度注意力对象。我必须要说,此处我们可能与原版 Molformer 算法产生了重大偏差。该框架作者实现了一种屏蔽机制,即排除了超过目标一定距离的对象。因此,他们只将注意力集中在一个确定的区域内。
然而,在我们的实现中,我们采取了不同的方式。首先,取代所提议注意力机制,我们采用了上一篇文章中讨论的相对自注意力方法,其不仅分析了位置偏移量,还分析了上下文信息。其二,为了调整注意力尺度,我们增加了所分析元素的单个大小,覆盖了原始序列的 2 、3 和 4 个元素。这就如同分析更高时间帧图表。我们解决方案的实现在 CNeuronMultiScaleAttention 类中提供。新类结构如下所示。
class CNeuronMultiScaleAttention : public CNeuronBaseOCL { protected: uint iWindow; uint iUnits; //--- CNeuronBaseOCL cWideInputs; CNeuronRelativeSelfAttention cAttentions[4]; CNeuronBaseOCL cConcatAttentions; CNeuronMHAttentionPooling cPooling; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronMultiScaleAttention(void) {}; ~CNeuronMultiScaleAttention(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronMultiScaleAttention; } //--- 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; };
在此,我们声明一个固定的相对注意力对象数组,显式定义尺度的数量。此外,类结构还声明了另外 3 个对象,我们将在类方法的实现过程中领略其目的。
我们将所有内部对象声明为静态,如此我们就可将类的构造函数和析构函数留空。这些声明和继承对象的初始化均在 Init 方法中执行。
bool CNeuronMultiScaleAttention::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) return false;
在方法参数中,如常,我们接收定义所创建对象架构的唯一常量。在方法主体中,我们立即调用父类的同名方法。我相信没有必要啰嗦,该方法已包含继承对象所需的检查、和初始化算法。
父类方法成功执行后,我们将一些常量存储在内部变量之中。
iWindow = window; iUnits = units_count;
在初始化新声明的对象之前,重点要注意,在该阶段我们不知道输入数据张量的大小。甚至,我们不知道它的维度是否与我们的分析尺度兼容。事实上,我们接收的输入张量或许都不是我们尺度的倍数。不过,输入到大尺度注意力对象的张量需要拥有正确的大小。为了满足这个需求,我们将创建一个内部对象,把输入数据复制到其中,添加零值来填充任何缺失的元素。但首先,我们将判定所需的缓冲区大小,作为比我们的尺度最接近更大倍数的最大值。
uint units1 = (iUnits + 1) / 2; uint units2 = (iUnits + 2) / 3; uint units3 = (iUnits + 3) / 4; uint wide = MathMax(MathMax(iUnits, units1 * 2), MathMax(units2 * 3, units3 * 4));
然后我们初始化对象,以便复制所需大小的输入数据。
int idx = 0; if(!cWideInputs.Init(0, idx, OpenCL, wide * iWindow, optimization, iBatch)) return false; CBufferFloat *temp = cWideInputs.getOutput(); if(!temp || !temp.Fill(0)) return false;
我们用零值填充该层的结果缓冲区。
在下一步中,我们在保持其它参数的同时,初始化不同尺度的内部注意力对象。
idx++; if(!cAttentions[0].Init(0, idx, OpenCL, iWindow, window_key, iUnits, heads, optimization, iBatch)) return false; idx++; if(!cAttentions[1].Init(0, idx, OpenCL, 2 * iWindow, window_key, units1, heads, optimization, iBatch)) return false; idx++; if(!cAttentions[2].Init(0, idx, OpenCL, 3 * iWindow, window_key, units2, heads, optimization, iBatch)) return false; idx++; if(!cAttentions[3].Init(0, idx, OpenCL, 4 * iWindow, window_key, units3, heads, optimization, iBatch)) return false;
此处应当注意,尽管注意力对象的尺度不同,但我们期望在输出中获得相当大小的张量。这是因为,本质上,它们都用到单一的初始数据源。因此,为了级联注意力结果,我们声明的对象比原始数据大 4 倍。
idx++; if(!cConcatAttentions.Init(0, idx, OpenCL, 4 * iWindow * iUnits, optimization, iBatch)) return false;
为了求注意力结果均值,我们将用到上面创建的基于依赖项的池化。
idx++; if(!cPooling.Init(0, idx, OpenCL, iWindow, iUnits, 4, optimization, iBatch)) return false;
然后,我们将所创建对象的结果缓冲区和误差梯度的指针替换为池化层相应缓冲区的指针。
SetActivationFunction(None); if(!SetOutput(cPooling.getOutput()) || !SetGradient(cPooling.getGradient())) return false; //--- return true; }
在方法最后,我们将操作结果传递给调用程序。
注意,在该类中,我们并未规划对象来实现残差连接,在前面讨论的注意力模块中我们已用过它了。这是因为我们所用的内部相对注意力模块已包含了残差连接。如是结果,注意力成果的均化已参考了这些残差联系。添加更多操作都是多余的。
对象初始化之后,我们转到构造前馈通验流程,我们将在 feedForward 方法中实现这些流程。
bool CNeuronMultiScaleAttention::feedForward(CNeuronBaseOCL *NeuronOCL) { //--- Attention if(!cAttentions[0].FeedForward(NeuronOCL)) return false;
在 feedForward 方法参数中,如常,我们收到一个指向输入数据对象的指针,我们立即将其传递给内部单尺度注意力层中的同名方法。在内部对象方法中,除了核心操作以外,我们还检查接收指针的有效性。由此,在内部类方法操作成功执行后,我们能安全地使用从外部程序获取的指针。在下一步中,我们将输入数据传输到相应内层的缓存区当中。此后,我们同步激活函数。
if(!Concat(NeuronOCL.getOutput(), NeuronOCL.getOutput(), cWideInputs.getOutput(), iWindow, 0, iUnits)) return false; if(cWideInputs.Activation() != NeuronOCL.Activation()) cWideInputs.SetActivationFunction((ENUM_ACTIVATION)NeuronOCL.Activation());
重点要注意,在这种情况下,我们采用级联方法来复制输入数据,其中我们指定两个指向输入数据对象结果缓冲区的指针。对于第一个缓冲区,我们指示输入数据的窗口大小,对于第二个缓冲区,它是 “0”。显然,依据这些参数设置,我们将在指定的结果缓冲区中获得输入数据的副本。同时,并未针对缺失数据添加显式清零操作,因其在我们讨论对象初始化期间已执行。
不过,添加零值是隐式发生的。在内部输入数据对象初始化期间,我们在它的结果缓冲区里填充了零值。在训练和操作期间,我们期望收到相同大小的输入数据张量。由此,每次复制输入数据时,我们都会覆盖相同的元素,而其余的元素仍将保持填充零值。
形成扩展的导入数据对象后,我们规划一个循环来执行多尺度的注意力操作。在该循环中,我们将按顺序调用更大规模的注意力对象的前馈通验方法,传递它们指向扩展输入数据对象的指针。
//--- Multi scale attentions for(int i = 1; i < 4; i++) if(!cAttentions[i].FeedForward(cWideInputs.AsObject())) return false;
我们将所有尺度的注意力结果级联到一个张量之中。尽管所分析数据的尺度不同,但输出会产生可比较的张量,并且原始序列的每个元素都保留在其位置。因此,我们在原始序列元素的上下文中执行张量的级联。
//--- Concatenate Multi-Scale Attentions if(!Concat(cAttentions[0].getOutput(), cAttentions[1].getOutput(), cAttentions[2].getOutput(), cAttentions[3].getOutput(), cConcatAttentions.getOutput(), iWindow, iWindow, iWindow, iWindow, iUnits)) return false;
然后,以同样的方式,就原始序列的元素而言,我们考虑到依赖关系,针对多尺度注意力的结果执行加权池化。
//--- Attention pooling if(!cPooling.FeedForward(cConcatAttentions.AsObject())) return false; //--- return true; }
在结束该方法之前,我们向调用方返回一个布尔值,指示初始化成功或失败。
如是提醒,在对象的初始化阶段,我们替换了指向结果缓冲区和误差梯度缓冲区的指针。因此,池化结果直接放入模型神经网络层之间通信的缓冲区当中。由此,我们省略了冗余数据复制操作。
我建议把该类的反向传播方法留给独立学习。该类及其所有方法的完整代码在附件中提供。
2.4构建 Molformer 框架
上面已完成了大量工作来构建 Molformer 框架的各个组件。现在,是时候将这些单独的组件汇编到框架的完整架构之中了。为此目的,我们将创建一个新的 CNeuronMolformer 类。在本例中,我们将以 CNeuronRMAT 作为父类,其实现了最简单的线性模型的机制。新类结构如下所示。
class CNeuronMolformer : public CNeuronRMAT { public: CNeuronMolformer(void) {}; ~CNeuronMolformer(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint layers, uint motif_window, uint motif_step, ENUM_OPTIMIZATION optimization_type, uint batch); //Molformer //--- virtual int Type(void) override const { return defNeuronMolformer; } };
注意,与之前实现的组件不同,此处我们只覆盖新类 Init 的初始化方法。这要归功于父类中规划的线性结构。现在,依据所需的对象序列填充从父类继承的动态数组就足够了。在父类方法中已构造了这些组件之间的整体交互算法。
在这个唯一覆盖方法的参数中,我们会收到一系列常量,允许我们按照用户的意图明确地解释所创建对象的架构。
bool CNeuronMolformer::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint layers, uint motif_window, uint motif_step, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) return false;
在方法主体中,我们立即调用全连接神经层的基类方法。
重点要注意,我们调用的是神经层基类的方法,而不是直接父对象的方法。在方法主体中,我们需要创建一个全新的架构。故此,我们不会重新创建父类的架构解决方案。
下一步是准备一个动态数组,我们将在其中存储指向正在创建的对象的指针。
cLayers.Clear(); cLayers.SetOpenCL(OpenCL);
现在我们转去讨论与创建和初始化所需对象相关的操作。于此,我们首先创建并初始化形态提取对象。对于动态数组,我们添加指向新对象的指针。
int idx = 0; CNeuronMotifs *motif = new CNeuronMotifs(); uint motif_units = units_count - MathMax(motif_window - motif_step, 0); motif_units = (motif_units + motif_step - 1) / motif_step; if(!motif || !motif.Init(0, idx, OpenCL, window, motif_window, motif_step, motif_units, optimization, iBatch) || !cLayers.Add(motif) ) return false;
然后我们创建局部变量来临时存储指向对象的指针,并运行一个循环,这将创建编码器的内层。内层数量由方法参数中的常数决定。
idx++; CNeuronMultiScaleAttention *msat = NULL; CResidualConv *ff = NULL; uint units_total = units_count + motif_units; for(uint i = 0; i < layers; i++) { //--- Attention msat = new CNeuronMultiScaleAttention(); if(!msat || !msat.Init(0, idx, OpenCL, window, window_key, units_total, heads, optimization, iBatch) || !cLayers.Add(msat) ) return false; idx++;
在循环主体中,我们首先创建并初始化多尺度注意力对象。然后我们添加一个具有残差连接的卷积模块。
//--- FeedForward ff = new CResidualConv(); if(!ff || !ff.Init(0, idx, OpenCL, window, window, units_total, optimization, iBatch) || !cLayers.Add(ff) ) return false; idx++; }
我们将指向已创建对象的指针添加到内部对象的动态数组之中。
接下来,请注意,在多尺度注意力模块的输出中,我们获得了输入数据和形态嵌入的级联张量,其中包含有关内部依赖关系的信息。不过,在类的输出中,我们需要返回一个包含丰富输入数据的张量。我们将对单个单元序列中的数据使用缩放函数,而不是简单地 “丢弃” 形态嵌入。为此,我们首先转置上一层的结果。
//--- Out CNeuronTransposeOCL *transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(0, idx, OpenCL, units_total, window, optimization, iBatch) || !cLayers.Add(transp) ) return false; idx++;
然后我们添加一个卷积层,它将执行缩放单个正交序列的功能。
CNeuronConvOCL *conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, idx, OpenCL, units_total, units_total, units_count, window, 1, optimization, iBatch) || !cLayers.Add(conv) ) return false; idx++;
将输出重置为原始数据表示形式。
idx++; transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(0, idx, OpenCL, window, units_count, optimization, iBatch) || !cLayers.Add(transp) ) return false;
之后,我们只需要替换指向数据缓冲区的指针,并将操作的逻辑结果返回给调用程序。
if(!SetOutput(transp.getOutput()) || !SetGradient(transp.getGradient())) return false; //--- return true; }
至此,我们针对 Molder 框架类的讨论至此完毕。您可在附件中找到所有提供的类、及其方法的完整源代码。附件还包含本文中用到的所有程序的完整代码。请注意,我们采用的是早期文章中的交互和训练计划。对环境状态编码器的架构进行了一些细微的更改,我鼓励您独立探索。您可在附件中找到可训练模型架构的完整描述。现在,我们继续工作的最后阶段 — 训练模型、并测试结果。
3. 测试
在本文中,我们利用 MQL5 实现了 Molformer 框架,现在正在转入最后阶段 — 训练模型、并评估经过训练的参与者行为政策。我们遵循之前工作中描述的训练算法,同时训练三个模型:状态编码器、参与者、和评论者。编码器分析市场状况,参与者基于学到的政策执行交易,评论者评估参与者的动作,并为完善行为政策提供指导。
训练是依据 EURUSD 的 2023 全年 H1 时间帧真实历史数据进行的,并配以所分析指标的标准参数。
训练过程是迭代的。它包括训练数据集的定期更新。
为了验证经训练政策的有效性,我们采用了 2024 年 1 月的历史数据。测试结果呈现如下。
经训练模型在测试期间执行了 25 笔交易,其中 17 笔以盈利了结。这占总数的 68%。甚至,平均和最大盈利交易是相应亏损交易的两倍。
净值曲线也证实了所提议模型的潜力,该曲线显示出明显的上升趋势。然而,较短的测试区间和有限的交易数量表明,这一结果仅表明潜力。
结束语
Molformer 方法代表了市场数据分析和预测领域的重大进步。利用异构市场图,包括单一资产、及其按市场形态的组合形式出现,该模型能够参考更复杂的关系和数据结构,从而显著提升测未来价格走势的准确性。
在本文的实践部分,我们利用 MQL5 实现了我们对 Molformer 方法的愿景。我们将提议的解决方案集成到模型之中,并依据真实历史数据对其进行训练。如是结果,我们创建了一个能够将所获得的知识推广至新市场形势、并产生盈利的模型。测试结果确认了这一点。我们相信,所提议方式能成为金融分析领域进一步研究和应用的基础,为交易者和分析师提供在不确定的情况下制定明智决策的新工具。
参考
文章中所用程序
# | 名称 | 类型 | 说明 |
---|---|---|---|
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/16130
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



您好,我无法使用 test.mq5 Expert Advisor 下单。
问题是数组元素 temp[0] 和 temp[3] 总是小于 min_lot,我的错误出在哪里?