
神经网络变得简单(第 76 部分):配合多未来变换器探索不同的交互形态
概述
预测即将到来的价格走势是构建成功交易策略的关键先决条件之一。因此,我们需要探索创建准确和多模态预测未来走势的可能性。在之前的文章中,我们领略了一些预测价格走势的方法。其中包括多模态,提供了若干种事件开发变体。
然而,它们都很少专注所分析个体之间未来互动的可能性,而这可能导致信息丢失和次优预测。此外,如果我们有多名个体,则很难伸缩前面讨论过的方法,因为独立预测可能会导致组合数量呈指数级增长。由于预测彼此冲突,结果组合的大多数都是不可行的。因此,重要的是要专注于预测整体场景,同时估测多名个体的未来状态。
论文《多未来变换器:自动驾驶行为预测中学习多种互动模式》的作者建议使用多未来变换器(MFT)方法来解决这些问题。其主要思路是把未来的多模态分布分解为若干个单模态分布,这样就可以有效地模拟场景中个体之间互动的各种模态。
在 MFT 中,预测由拥有固定参数的神经网络在单次前馈验算中生成,无需对潜在变量进行随机采样、预先检测锚点、或运行后处理迭代算法。这就允许模型按可判定、可重复的方式运行。
1. 多未来变换器算法
多未来变换器方法的主要目标是一致性预测场景中所有个体的未来走势 Y。为此,它分析个体的动态状态 X,和上下文信息 M。因此,要捕获的总体概率分布为 P(Y|X,M),其为多模态,作为场景清晰演变的结果。
联合多模态分布直接建模通常非常困难。该方法的作者引入了一个假设,即目标分布可以分解为若干个单模态分布的混合,然后分别为这些单模态分布建模,其公式化为
其中 Ik 表示第 k 个模态分量;
p(Ik|X,M) 是不同模态的概率分布;
p(Y|X,M,Ik) 表示经因式分解的单模态分布。
在这个公式下,MFT 的关键点是每种目标分布模式之间的主要区别在于个体间、以及个体与上下文之间的互动模型不同。通过研究模式对应互动的形态,可以实现每个单模态分布的建模。直观地讲,未来场景的不确定性主要能分解为两部分:意图不确定性,和互动不确定性。
通过针对个体与上下文之间的互动进行建模,可以很好地捕捉到不可观察的意图,因为代表个体意图的未来轨迹的端点与场景的上下文密切相关。为了解释互动的不确定性,个体之间的不同互动模式均已建模。因此,通过联合捕获到的个体到个体的互动,以及个体与上下文的互动,可以最大限度地减少未来的不确定性,并判定场景的演变。
为了实现上述多模态联合分布分解,设计出所提议的 MFT 方法的一般架构。它由三个部分组成:
- 编码器
- 并联互动模块
- 预测头部
该模型包括两种类型的编码器:
- 动态个体编码器用来从观察到的动态状态中提取特征,
- 上下文片段编码器用作地图查看器,来学习条纹点的逐点函数。
MFT 模型的核心是一个并行互动模块,它由并行结构中的多个互动模块组成,研究每种模式下个体走势的未来特征。这三个预测头部包括:
- 运动解码器,
- 个体分数解码器,
- 场景分数解码器。
它们负责解码每名个体的未来轨迹,并估算每个预测轨迹和每个场景模式的置信度分数。在该架构中,每个模式的前馈和反向传播信号沿途经过的路径彼此独立,并且每个路径都包含一个唯一的互动模块,即在相同模式的信号之间提供信息互动。因此,互动单元可以同时捕获模式不同形态的相应互动。不过,编码器和预测头部对于每种模式都是通用的,而互动模块则因对象不同而参数相异。因此,理论上具有不同参数的每个单模态分布都能按效率更高的参数进行建模。该方法的原始可视化如下所示。
在历史视界上观察到的个体轨迹可以表示为 X={x1,...,xatt},其中在每个时间步 xt=(x,y,vx,vy,w) 的动态状态包含当时的位置 (x,y)、速度 (vx,vy),和偏航角度 w。
个体的动态状态被视为多维空间中的一组点,其中每个点由坐标 (x,y) 表示,其它维度表示额外的局部特征。对于这些点,互动属性和给定的局部结构被保留,因为个体的观察状态彼此互动,并共同形成个体的观察轨迹此外,这些轨迹点的另一个重要属性是时态顺序,与静态轨道点相比,这是一个隐含的特征。自然地,具有相反时间顺序的相同轨迹点会导致完全不同的观测轨迹。上述属性要求模型不仅通过组合点特征来表示轨迹点,而且还通过引入时态顺序信息来表示轨迹点。为此目的,采用绝对位置编码来编码顺序信息。该模型按如下方式计算每名个体的动态特征:
多个编码器层堆叠在一起,从而提升模型表示动态信息的能力。对覆盖个体轨迹的所有点均化,来汇总与特定个体相关的历史动态信息。
还提取了与当前时刻对应的点特征,来获得动态信息。这样提供的性能类似于均化池。在与部分可观察历史状态打交道时,基于自关的框架可以简单地掩盖不可观察的位置,而无需额外的工作量,从而有效地避免了由无效填充引起的特征混淆。
行为预测任务的一个独有特征是由未来场景不确定性引起的多模态。一种常见的方式是使用多个结果头部,基于共同特征向量独立解码每名个体的未来轨迹。然而,这种方法有两个主要缺点:
- 各种可能的未来轨迹的运动信息包含在一个具有固定维度的特征向量之中,导致信息有限,严重限制了模型的表达能力;
- 来自不同模式的正向和反向信号经由特征互动和梯度传播进行混合,导致模式混淆问题,从而降低了进行多模态预测的能力。
代之,在 MFT 中,互动建模被划分为不同的模式,通过学习与每种模式对应的独特互动形态,来达成多模态成果。为此目的,设计了一个并行互动模块。该模块包含多个并行互动模块,每个模块代表事件未来发展的某种模式。在该结构中,内部模式正向和反向信号穿插相应模式的互动模块。同时,其它模式互不干扰,避免了模式混淆的问题。
此外,每个互动模块都按参数生成一个单独的对象,令模型具有足够的表现力,来应对不同模式之间的巨大变化。每个互动模块都使用自关注机制针对个体之间的互动进行建模,并使用交叉关注机制来捕获个体与场景之间的互动。通过残差连接把这两种类型的互动特征加到动态特征之中,从而获得每名个体和模式即将到来的最终运动函数。上述过程可以描述如下:
并行互动模块的最终结构可以看作是将变换器解码器与串行结构的并行化。然而,与标准预测相比,多模态预测和增加的表达能力是以相似的计算成本达成。由该方法作者进行的实验确认,结合在场景层面运用的赢家通吃的损失策略,所提议的方法可以针对多名个体的行为提供一致的预测。还进行了一项研究,来演示两种类型的建模互动不可或缺,及模块结构设计的合理性。
作者的模块化结构的可视化呈现如下。
除了预测每名个体的未来轨迹外,MFT 还对场景的每种可能发展、以及个体的每条预测轨迹进行可能性评估。对于运动规划任务,理想的输入是多模态场景预测,并为每种场景模式提供相应的置信度分数。规划算法可以通过同时考虑所有相邻个体来直接计算可能的未来运动轨迹。为此目的,该方法的作者开发出预测各种场景及其概率的头部。
为了估算场景级别的概率,之后场景中所有个体的特征向量进行均化。这允许我们获得场景级视图。
此外,要特别关注个体,应从个体本身的角度进行似然估算。因此,创建了第三个模型头部,来解码每名个体的预测轨迹的置信度估算。多未来变换器解码过程表述如下:
注意,为了解码多模态场景,所有可能的场景都实用相同参数的解码器。
2. 利用 MQL5 实现
在研究了多未来变换器方法的理论层面之后,我们转入本文的实践部分。我们来研究如何利用 MQL5 实现该算法。
从以上多未来变换器算法的讲述中,我们可以说,主要的实现难点是并行互动模块。我们的函数库之前已经实现了多目自关注度(CNeuronMLMHAttentionOCL),和交叉关注度(CNeuronMH2AttentionOCL)层。然而,即便使用多目关注度,其中的数据流也会在向前和向后验算期间混合。这不满足多未来变换器方法的条件。
因此,我们开始实现该方法的举措是创建一个新的神经层类。
2.1. 并联互动模块
我们在 CNeuronMFTOCL 类中实现并行互动模块算法,该类继承自 CNeuronMLMHAttentionOCL。
这个类是有意选择的。CNeuronMLMHAttentionOCL 已经包含多层自关注度算法的已实现功能。我们使用已有代码来实现新类。但我们将预测各种场景,替代按顺序计算关注度模块层。此外,我们还将添加多未来变换器算法提供的交叉关注度功能。
新类的结构如下所示。如您所见,除了重新定义主要方法之外,我们还添加了另一个数据转置的数据缓冲区集合,我们将需要它来实现交叉关注度机制。
class CNeuronMFTOCL : public CNeuronMLMHAttentionOCL { protected: //--- CCollection cTranspose; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL); virtual bool Transpose(CBufferFloat *in, CBufferFloat *out, int rows, int cols); virtual bool MHCA(CBufferFloat *q, CBufferFloat *kv, CBufferFloat *score, CBufferFloat *out); //--- virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); virtual bool MHCAInsideGradients(CBufferFloat *q, CBufferFloat *qg, CBufferFloat *kv, CBufferFloat *kvg, CBufferFloat *score, CBufferFloat *aog); public: CNeuronMFTOCL(void) {}; ~CNeuronMFTOCL(void) {}; virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint units_count, uint features, ENUM_OPTIMIZATION optimization_type, uint batch); virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer); //--- virtual int Type(void) const { return defNeuronMFTOCL; } //--- methods for working with files virtual bool Save(int const file_handle); virtual bool Load(int const file_handle); virtual CLayerDescription* GetLayerInfo(void); virtual bool WeightsUpdate(CNeuronBaseOCL *source, float tau); virtual void SetOpenCL(COpenCLMy *obj); };
新的数据缓冲区集合声明为静态,允许我们将类的构造函数和析构函数留空。我们在 Init 方法中初始化类和内部对象。
bool CNeuronMFTOCL::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint units_count, uint features, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * features, optimization_type, batch)) return false;
在参数中,该方法接收重新创建所需架构所需的所有信息。在方法的主体中,我们调用神经层基类 CNeuronBaseOCL 的相关方法。
请注意,我们不会调用直接父类 CNeuronMLMHAttentionOCL 的初始化方法。取而代之,我们转向更高层次 — 神经层的基类 CNeuronBaseOCL。这是由于 CNeuronMFTOCL 和 CNeuronMLMHAttentionOCL 的实现存在一些差异。
在 CNeuronMLMHAttentionOCL 父类中,我们在关注度模块的若干个层中按顺序处理源数据,在输出时,我们收到的结果其维度与源数据相似。如今,在新的 CNeuronMFTOCL 层中,我们将始终如一地为可能的发展生成各种形态。相应地,层的结果将是原始数据大小的倍数(按预测选项的数量)。因此,我们需要增加结果缓冲区。
此外,为了避免不必要的数据复制,在父类中,我们替换了层本身,和最后一个内部层的结果,以及梯度缓冲区。在实现新类时,这种方式是不可接受的,因为我们必须将若干个并行模块的结果串联到单一缓冲区之中。
我们回到我们的初始化方法。在成功执行神经层基类初始化方法后,我们将架构的关键参数保存到内部类变量当中。
iWindow = fmax(window, 1); iWindowKey = fmax(window_key, 1); iUnits = fmax(units_count, 1); iHeads = fmax(heads, 1); iLayers = fmax(features, 1);
请注意,我们将所有参数保存在父类的变量当中。此处,变量的名称与其功能之间略有不同:iLayers 变量将存储所计划选项的数字。为了更有效地使用资源,我决定不创建额外的变量,并“忽略”变量的名称与功能之间的差异。
接下来,我们将计算模块的主要参数。
//--- MHSA uint num = 3 * iWindowKey * iHeads * iUnits; //Size of QKV tensor uint qkv_weights = 3 * (iWindow + 1) * iWindowKey * iHeads; //Size of weights' matrix of QKV tensor uint scores = iUnits * iUnits * iHeads; //Size of Score tensor uint mh_out = iWindowKey * iHeads * iUnits; //Size of multi-heads self-attention uint out = iWindow * iUnits; //Size of output tensor uint w0 = (iWindowKey + 1) * iHeads * iWindow; //Size W0 tensor
//--- MHCA uint num_q = iWindowKey * iHeads * iUnits; //Size of Q tensor uint num_kv = 2 * iWindowKey * iHeads * iUnits; //Size of KV tensor uint q_weights = (iWindow + 1) * iWindowKey * iHeads; //Size of weights' matrix of Q tensor uint kv_weights = 2 * (iUnits + 1) * iWindowKey * iHeads; //Size of weights' matrix of KV tensor uint scores_ca = iUnits * iWindow * iHeads; //Size of Score tensor
//--- FF uint ff_1 = 4 * (iWindow + 1) * iWindow; //Size of weights' matrix 1-st feed forward layer uint ff_2 = (4 * iWindow + 1) * iWindow; //Size of weights' matrix 2-nd feed forward layer
准备工作完成后,我们将组织一个循环,其中我们将依次为并行互动模块中的每个场景创建数据缓冲区。
for(uint i = 0; i < iLayers; i++) { CBufferFloat *temp = NULL; for(int d = 0; d < 2; d++) { //--- MHSA //--- Initilize QKV tensor temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(num, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Tensors.Add(temp)) return false;
此处,我们将创建一个嵌套循环来创建前向和后向验算缓冲区。在嵌套循环的主体中,我们首先创建一个 MHSA 模块的 Query、Key、和 Value 嵌入的关联缓冲区。我们还为 'Score' 矩阵添加了缓冲区。
//--- Initialize scores temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(scores, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!S_Tensors.Add(temp)) return false;
添加多目关注度结果的缓冲区。
//--- Initialize multi-heads attention out temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(mh_out, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!AO_Tensors.Add(temp)) return false;
在 MHSA 模块的输出端,我们创建了一个缓冲区,用于组合不同关注度的结果。
//--- Initialize attention out temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(out, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Tensors.Add(temp)) return false;
于此需要注意的是,为了满足 MFT 算法的需求,关注度的结果仅在一种互动模式内予以组合。
接下来,我们为交叉关注度模块(MHCA) 创建类似的缓冲区。不过,现在我们将 Query 嵌入缓冲区与 Key 和 Value 分开,因为将用到不同的源数据。
//--- MHCA //--- Initilize QKV tensor temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(num_q, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Tensors.Add(temp)) return false;
temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(out, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!cTranspose.Add(temp)) return false;
temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(num_kv, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Tensors.Add(temp)) return false;
//--- Initialize scores temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(scores_ca, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!S_Tensors.Add(temp)) return false;
//--- Initialize multi-heads cross attention out temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(mh_out, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!AO_Tensors.Add(temp)) return false;
//--- Initialize attention out temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(out, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Tensors.Add(temp)) return false;
在为关注度模块创建缓冲区之后,我们创建 FeedForward 模块缓冲区。
//--- Initialize Feed Forward 1 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(4 * out, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Tensors.Add(temp)) return false;
//--- Initialize Feed Forward 2 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(out, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Tensors.Add(temp)) return false; }
我们已创建了独立模块的结果及误差梯度缓冲区。接下来,我们需要为这些模块创建可训练权重的矩阵。我们以相同的顺序创建它们。首先,我们为 MHSA 模块的 Query、Key 和 Value 嵌入生成权重。
//--- MHSA //--- Initilize QKV weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(qkv_weights)) return false; float k = (float)(1 / sqrt(iWindow + 1)); for(uint w = 0; w < qkv_weights; w++) { if(!temp.Add((GenerateWeight() - 0.5f)* k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Weights.Add(temp)) return false;
还有一个层,用于组合关注度。
//--- Initilize Weights0 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(w0)) return false; for(uint w = 0; w < w0; w++) { if(!temp.Add((GenerateWeight() - 0.5f)* k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false;
针对 MHCA 模块重复操作。
//--- MHCA //--- Initilize Q weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(q_weights)) return false; for(uint w = 0; w < q_weights; w++) { if(!temp.Add((GenerateWeight() - 0.5f)* k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Weights.Add(temp)) return false;
//--- Initilize KV weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(kv_weights)) return false; float kv = (float)(1 / sqrt(iUnits + 1)); for(uint w = 0; w < kv_weights; w++) { if(!temp.Add((GenerateWeight() - 0.5f)* kv)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Weights.Add(temp)) return false;
//--- Initilize Weights0 temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(w0)) return false; for(uint w = 0; w < w0; w++) { if(!temp.Add((GenerateWeight() - 0.5f)* k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false;
为 FeedForward 模块创建矩阵。
//--- Initilize FF Weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(ff_1)) return false; for(uint w = 0; w < ff_1; w++) { if(!temp.Add((GenerateWeight() - 0.5f)* k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false;
//--- temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(ff_2)) return false; k = (float)(1 / sqrt(4 * iWindow + 1)); for(uint w = 0; w < ff_2; w++) { if(!temp.Add((GenerateWeight() - 0.5f)* k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false;
之后,我们实现另一个嵌套循环,在该循环中,我们创建更新权重矩阵的缓冲区。注意,矩缓冲区的数量取决于所选的优化算法。
for(int d = 0; d < (optimization == SGD ? 1 : 2); d++) { //--- MHSA temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(qkv_weights, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Weights.Add(temp)) return false;
temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(w0, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false;
//--- MHCA temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(q_weights, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Weights.Add(temp)) return false;
temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(kv_weights, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Weights.Add(temp)) return false;
temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(w0, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false;
//--- FF Weights momentus temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(ff_1, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false;
temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(ff_2, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false; } } //--- return true; }
所有必要的数据缓冲区成功初始化之后,我们以 true 结果退出该方法。
我们创建了很多缓冲区。为了不混淆,我们来创建一个数据缓冲区导航表。
id | QKV_Tensors | S_Tensors, AO_Tensors | FF_Tensors | QKV_Weights | FF_Weights |
---|---|---|---|---|---|
0 | Query, Key, Value MHSA | MHSA | MHSA Out | Query, Key, Value MHSA | MHSA W0 |
1 | Query MHCA | MHCA | MHCA Out | Query MHCA | MHCA W0 |
2 | Key, Value MHCA | Gradient MHSA | FF1 | Key, Value MHCA | FF1 |
3 | Gradient Query, Key, Value MHSA | Gradient MHCA | FF2 | Momentum1 Query, Key, Value MHSA | FF2 |
4 | Gradient Query MHCA | Gradient MHSA Out | Momentum1 Query MHCA | Momentum1 MHSA W0 | |
5 | Gradient Key, Value MHCA | Gradient MHCA Out | Momentum1 Key, Value MHCA | Momentum1 MHCA W0 | |
6 | Gradient FF1 | Momentum2 Query, Key, Value MHSA | Momentum1 FF1 | ||
7 | Gradient FF2 | Momentum2 Query MHCA | Momentum1 FF2 | ||
8 | Momentum2 Key, Value MHCA | Momentum2 MHSA W0 | |||
9 | Momentum2 MHCA W0 | ||||
10 | Momentum2 FF1 | ||||
11 | Momentum2 FF2 |
在 CNeuronMFTOCL 类初始化之后,我们转入构造前馈算法。在实现 MFT 算法时,我们并未在 OpenCL 程序端创建新的内核。不过,为了调用之前创建的内核,我们必须在主程序的一侧创建一些方法。特别是,对于我们的 CNeuronMFTOCL 类,我们创建了一个矩阵转置方法。该方法算法类似于转置层的前馈验算。不过,在细节上存在差异。在参数中,新方法接收指向 2 个数据缓冲区(源数据和结果)的指针,以及原始矩阵的大小。
bool CNeuronMFTOCL::Transpose(CBufferFloat *in, CBufferFloat *out, int rows, int cols) { if(!in || !out) return false; //--- uint global_work_offset[2] = {0, 0}; uint global_work_size[2] = {rows, cols}; if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_in, in.GetIndex())) return false; if(!OpenCL.SetArgumentBuffer(def_k_Transpose, def_k_tr_matrix_out, out.GetIndex())) return false; if(!OpenCL.Execute(def_k_Transpose, 2, global_work_offset, global_work_size)) { string error; CLGetInfoString(OpenCL.GetContext(), CL_ERROR_DESCRIPTION, error); printf("%s %d Error of execution kernel Transpose: %d -> %s", __FUNCTION__, __LINE__, GetLastError(), error); return false; } //--- return true; }
因此,在方法的主体中,我们检查接收到的指向数据缓冲区的指针,并按来自任务空间维度中的参数指示数值。内核调用算法本身保持不变。
MHCA 前馈方法的状况类似。在方法参数中,我们接收指向数据缓冲区的指针。CNeuronMH2AttentionOCL::attentionOut 方法没有参数,并且在其功能中使用了内部对象。
bool CNeuronMFTOCL::MHCA(CBufferFloat *q, CBufferFloat *kv, CBufferFloat *score, CBufferFloat *out) { if(!q || !kv || !score || !out) return false;
在方法的主体中,我们创建任务空间的维度。
uint global_work_offset[3] = {0}; uint global_work_size[3] = {iUnits, iWindow, iHeads}; uint local_work_size[3] = {1, iWindow, 1};
我们将参数传递给内核。
ResetLastError(); if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_q, q.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_kv, kv.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_score, score.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgumentBuffer(def_k_MH2AttentionOut, def_k_mh2ao_out, out.GetIndex())) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; } if(!OpenCL.SetArgument(def_k_MH2AttentionOut, def_k_mh2ao_dimension, (int)iWindowKey)) { printf("Error of set parameter kernel %s: %d; line %d", __FUNCTION__, GetLastError(), __LINE__); return false; }
将其添加到队列之中。
if(!OpenCL.Execute(def_k_MH2AttentionOut, 3, global_work_offset, global_work_size, local_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); return false; } //--- return true; }
MFT 前馈算法在 CNeuronMFTOCL::feedForward 方法中实现。与其它神经层的类似方法一样,该方法在其参数中接收指向前一个神经层的指针。在方法主体中,我们立即检查接所接收指针的相关性。
bool CNeuronMFTOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(NeuronOCL) == POINTER_INVALID) return false;
接下来,我们组织了一个顺序循环,计算个体之间互动的各种模式 .
for(uint i = 0; (i < iLayers && !IsStopped()); i++) { //--- MHSA //--- Calculate Queries, Keys, Values CBufferFloat *inputs = NeuronOCL.getOutput(); CBufferFloat *qkv = QKV_Tensors.At(i * 6); if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? 6 : 9)), inputs, qkv, iWindow, 3 * iWindowKey * iHeads, None)) return false;
此处,我们首先获取 MHSA 模块的 Query、Key、和 Value 实体。然后我们定义关注度系数矩阵。
//--- Score calculation CBufferFloat *temp = S_Tensors.At(i * 4); if(IsStopped() || !AttentionScore(qkv, temp, false)) return false;
生成多目自关注度的结果。
//--- Multi-heads attention calculation CBufferFloat *out = AO_Tensors.At(i * 4); if(IsStopped() || !AttentionOut(qkv, temp, out)) return false;
我们将多目关注度的结果减少到原始数据的大小。我要提醒你,关注度是在个体互动一个变体的框架之内组合。
//--- Attention out calculation temp = FF_Tensors.At(i * 8); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 8 : 12)), out, temp, iWindowKey * iHeads, iWindow, None)) return false;
我们将结果张量与原始数据累加,并对操作结果进行常规化。
//--- Sum and normalize attention if(IsStopped() || !SumAndNormilize(temp, inputs, temp, iWindow)) return false;
接下来是交叉关注度模块。此处,我们首先定义 Query 实体。
//--- MHCA inputs = temp; CBufferFloat *q = QKV_Tensors.At(i * 6 + 1); if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? 6 : 9) + 1), inputs, q, iWindow, iWindowKey * iHeads, None)) return false;
然后,我们转置原始数据,并计算 Key 和 Value 实体。
CBufferFloat *tr = cTranspose.At(i * 2); if(IsStopped() || !Transpose(inputs, tr, iUnits, iWindow)) return false;
CBufferFloat *kv = QKV_Tensors.At(i * 6 + 2); if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? 6 : 9) + 2), tr, kv, iUnits, 2 * iWindowKey * iHeads, None)) return false;
判定多目交叉关注度的结果。
//--- Multi-heads cross attention calculation temp = S_Tensors.At(i * 4 + 1); out = AO_Tensors.At(i * 4 + 1); if(IsStopped() || !MHCA(q, kv, temp, out)) return false;
获得的结果被压缩到原始数据的大小。
//--- Cross Attention out calculation temp = FF_Tensors.At(i * 8 + 1); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 1), out, temp, iWindowKey * iHeads, iWindow, None)) return false;
将其添加到 MHSA 模块的结果当中,并对数据进行常规化。
//--- Sum and normalize attention if(IsStopped() || !SumAndNormilize(temp, inputs, temp, iWindow)) return false;
然后我们经由 FeedForward 模块组织数据前馈。
//--- Feed Forward inputs = temp; temp = FF_Tensors.At(i * 8 + 2); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 2), inputs, temp, iWindow, 4 * iWindow, LReLU)) return false; out = FF_Tensors.At(i * 8 + 3); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 3), temp, out, 4 * iWindow, iWindow, activation)) return false;
FeedForward 模块操作结果被添加到 MHCA 结果当中。接收到的数据被常规化,并写入结果缓冲区,其偏移量与所分析互动模式相对应。
//--- Sum and normalize out if(IsStopped() || !SumAndNormilize(out, inputs, Output, iWindow, true, 0, 0, i * inputs.Total())) return false; } //--- return true; }
接下来,我们转到下一个迭代循环,分析场景中个体之间的下一个互动模式。
在成功分析了个体之间互动的所有模式后,我们以结果 true 完成该方法。
在前馈验算期间,我们执行基本功能,分析场景中个体的互动模式,并预测进一步事件的可能变体。故此,如果缺了反向验算方法,就不可能进行模型训练。在反向验算过程中,模型参数被优化,从而获得更好的结果。因此,在实现前馈方法之后,我们转到构造反向验算算法。
与前馈验算一样,为了实现反向传播验算,我们需要创建一个额外的方法 MHCAInsideGradients。它的算法几乎与 CNeuronMH2AttentionOCL::AttentionInsideGradients 的算法完全相似。唯一的区别是 MHCAInsideGradients 不与内部类对象一起工作,而是与方法参数中接收的缓冲区一起工作。我们已经在本文前面看到了该类的修改示例。故此,我们现在不会详细研究该方法。您可在附件中找到其完整代码。
反向传播验算算法在 calcInputGradients 方法中实现。在参数中,该方法接收指向前一个神经层对象的指针。
bool CNeuronMFTOCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer) == POINTER_INVALID) return false;
在方法的主体中,我们立即检查接收到的指针的有效性。之后,我们将指向缓冲区的指针保存在局部变量当中,所有并行互动模块都将使用该缓冲区。
CBufferFloat *out_grad = Gradient; CBufferFloat *inp = prevLayer.getOutput(); CBufferFloat *grad = prevLayer.getGradient();
完成准备工作后,我们组织了一个循环,通过并行互动的单独模块传播误差梯度。
for(int i = 0; (i < (int)iLayers && !IsStopped()); i++) { //--- Passing gradient through feed forward layers if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 3), Gradient, FF_Tensors.At(i * 8 + 2), FF_Tensors.At(i * 8 + 6), 4 * iWindow, iWindow, None, i * inp.Total())) return false;
此处,我们首先通过 FeedForward 模块传播误差梯度。
CBufferFloat *temp = FF_Tensors.At(i * 8 + 5); if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 2), FF_Tensors.At(i * 8 + 6), FF_Tensors.At(i * 8 + 1), temp, iWindow, 4 * iWindow, LReLU)) return false;
在前馈验算中,FeedForward 模块操作的结果被添加到 MHCA 模块的输出中。与此类似,我们在反方向传播误差梯度。不过,这次我们不执行常规化。
//--- Sum gradient if(IsStopped() || !SumAndNormilize(Gradient, temp, temp, iWindow, false, i * inp.Total(), 0, 0)) return false; out_grad = temp;
接下来,我们需要通过 cross-attention 模块传播误差梯度。此处,我们首先跨越关注度传播误差梯度。
//--- MHCA //--- Split gradient to multi-heads if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12) + 1), out_grad, AO_Tensors.At(i * 4 + 1), AO_Tensors.At(i * 4 + 3), iWindowKey * iHeads, iWindow, None)) return false;
然后我们将误差梯度传播到对应的实体。
if(IsStopped() || !MHCAInsideGradients(QKV_Tensors.At(i * 6 + 1), QKV_Tensors.At(i * 6 + 4), QKV_Tensors.At(i * 6 + 2), QKV_Tensors.At(i * 6 + 5), S_Tensors.At(i * 4 + 1), AO_Tensors.At(i * 4 + 3))) return false;
现在我们需要转置 MHCA 模块的 Key 和 Value 的误差梯度。
CBufferFloat *tr = cTranspose.At(i * 2 + 1); if(IsStopped() || !Transpose(QKV_Tensors.At(i * 6 + 5), tr, iWindow, iUnits)) return false;
我们首先汇总获得的结果。
//--- Sum temp = FF_Tensors.At(i * 8 + 4); if(IsStopped() || !SumAndNormilize(QKV_Tensors.At(i * 6 + 4), tr, temp, iWindow, false)) return false;
然后,我们将 MHCA 模块输出端的误差梯度添加到它们当中。
if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false)) return false;
之后,我们需要通过 MHSA 模块传播误差梯度。如同交叉关注度,我们首先在关注度之间分配误差梯度。
//--- MHSA //--- Split gradient to multi-heads out_grad = temp; if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? 8 : 12)), out_grad, AO_Tensors.At(i * 4), AO_Tensors.At(i * 4 + 2), iWindowKey * iHeads, iWindow, None)) return false;
将误差梯度传播到对应的实体。
//--- Passing gradient to query, key and value if(IsStopped() || !AttentionInsideGradients(QKV_Tensors.At(i * 6), QKV_Tensors.At(i * 6 + 3), S_Tensors.At(i * 4), S_Tensors.At(i * 4 + 1), AO_Tensors.At(i * 4 + 1))) return false;
直至传播到源数据级别。
if(IsStopped() || !ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? 6 : 9)), QKV_Tensors.At(i * 6 + 3), inp, tr, iWindow, 3 * iWindowKey * iHeads, None)) return false;
现在我们需要在 MHSA 模块的输入和输出处添加误差梯度,并将结果写入前一层的梯度缓冲区。但有一点。我们需要将所有并行互动模块的误差梯度合计写入前一层的梯度缓冲区。因此,我们首先检查并行互动模块的标识符。对于第一个模块,我们只需将 2 个线程的梯度总和写入前一层误差的梯度缓冲区当中。对于其余的并行互动模块,我们将生成的误差梯度添加到上一层缓冲区中的数据当中。
//--- Sum gradients if(i > 0) { if(IsStopped() || !SumAndNormilize(grad, tr, grad, iWindow, false)) return false; if(IsStopped() || !SumAndNormilize(out_grad, grad, grad, iWindow, false)) return false; } else if(IsStopped() || !SumAndNormilize(out_grad, tr, grad, iWindow, false)) return false; } //--- return true; }
然后我们转到循环的下一次迭代,将误差梯度传递给下一个并行互动模块。
成功完成循环的所有迭代后,我们使用 'true' 终止该方法。
我们在 CNeuronMFTOCL 类中组织了前馈烟笋和反向传播误差梯度。为了完成该类主要功能的实现,我们需要添加一个更新权重参数 updateInputWeights 的方法。该方法算法非常简单。在一个循环中,我们调用父类方法 ConvolutionUpdateWeights,其中更新单独缓冲区的参数。操作所需的数据缓冲区在方法的参数中传递。
bool CNeuronMFTOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(NeuronOCL) == POINTER_INVALID) return false; CBufferFloat *inputs = NeuronOCL.getOutput(); for(uint l = 0; l < iLayers; l++) { if(IsStopped() || !ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? 6 : 9)), QKV_Tensors.At(l * 6 + 3), inputs, (optimization == SGD ? QKV_Weights.At(l * 6 + 3) : QKV_Weights.At(l * 9 + 3)), (optimization == SGD ? NULL : QKV_Weights.At(l * 9 + 6)), iWindow, 3 * iWindowKey * iHeads)) return false; if(IsStopped() || !ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? 6 : 9) + 1), QKV_Tensors.At(l * 6 + 4), inputs, (optimization == SGD ? QKV_Weights.At(l * 6 + 4) : QKV_Weights.At(l * 9 + 4)), (optimization == SGD ? NULL : QKV_Weights.At(l * 9 + 7)), iWindow, iWindowKey * iHeads)) return false; if(IsStopped() || !ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? 6 : 9) + 2), QKV_Tensors.At(l * 6 + 5), cTranspose.At(l * 2), (optimization == SGD ? QKV_Weights.At(l * 6 + 5) : QKV_Weights.At(l * 9 + 5)), (optimization == SGD ? NULL : QKV_Weights.At(l * 9 + 8)), iUnits, 2 * iWindowKey * iHeads)) return false; //--- if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 8 : 12)), FF_Tensors.At(l * 8 + 4), AO_Tensors.At(l * 4), (optimization == SGD ? FF_Weights.At(l * 8 + 4) : FF_Weights.At(l * 12 + 4)), (optimization == SGD ? NULL : FF_Weights.At(l * 12 + 8)), iWindowKey * iHeads, iWindow)) return false; if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 8 : 12) + 1), FF_Tensors.At(l * 8 + 5), AO_Tensors.At(l * 4 + 1), (optimization == SGD ? FF_Weights.At(l * 8 + 5) : FF_Weights.At(l * 12 + 5)), (optimization == SGD ? NULL : FF_Weights.At(l * 12 + 9)), iWindowKey * iHeads, iWindow)) return false; //--- if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 8 : 12) + 2), FF_Tensors.At(l * 8 + 6), FF_Tensors.At(l * 8 + 1), (optimization == SGD ? FF_Weights.At(l * 8 + 6) : FF_Weights.At(l * 12 + 6)), (optimization == SGD ? NULL : FF_Weights.At(l * 12 + 10)), iWindow, 4 * iWindow)) return false; //--- if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? 8 : 12) + 3), FF_Tensors.At(l * 8 + 7), FF_Tensors.At(l * 8 + 2), (optimization == SGD ? FF_Weights.At(l * 8 + 7) : FF_Weights.At(l * 12 + 7)), (optimization == SGD ? NULL : FF_Weights.At(l * 12 + 11)), 4 * iWindow, iWindow)) return false; } //--- return true; }
实现 CNeuronMFTOCL 类中多未来变换器算法的主要功能到此结束。您可以调取附件中的代码领略辅助方法的实现。在那里,您还可以找到撰写本文时用到的所有程序的完整代码。我们现在转入实现构建和训练模型的智能系统。
2.2模型架构
我们为并行互动模块算法构建了一个类,该算法由多未来变换器方法的作者提出。不过,这只是我们模型的一个层面。现在我们需要描述所创建模型的完整架构。模型将被饲喂原始源数据。作为模型工作的结果,我们需要获得个体的最优动作向量,当在金融市场中运作时,其实现可以带来盈利。
我必须说,在构建我的模型时,我略微偏离了该方法作者提议的架构。这有若干个原因。
首先,所提议多未来变换器方法是为了预测车辆自主驾驶与环境互动。而我们计划是将我们的模型用于金融市场。这两项任务都有各自的特点,这在模型的构造上留下了印记。
其次,在上一篇文章中,我们付出大量精力关注优化模型性能。我很喜欢从所获得的经验里受益。因此,我用到了来自上一篇文章中的模型架构作为供体。针对轨迹规划模型进行了修改。
预测未来轨迹的模型架构在文件 “...\Experts\MFT\Trajectory.mqh” 中的 CreateTrajNetDescriptions 方法中描述。在参数中,该方法接收指向动态数组的指针,以便创建 3 个模型:
- 状态编码器
- 端点解码器
- 场景概率
在之前的文章中,我们决定不预测价格走势的完整轨迹,而是专注于预测主要的价格走势极值:
- 收盘价
- 最高价
- 最低价
因此,在我们的工作中,我们将价格走势动态的状态视为场景的变体。我们不分析单一个体的最终轨迹概率。
bool CreateTrajNetDescriptions(CArrayObj *encoder, CArrayObj *endpoints, CArrayObj *probability) { //--- CLayerDescription *descr; //--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) return false; } if(!endpoints) { endpoints = new CArrayObj(); if(!endpoints) return false; } if(!probability) { probability = new CArrayObj(); if(!probability) return false; }
在方法的主体中,我们检查参数中接收到的对象指针的相关性,并在必要时创建一个新的。
首先,我们描述了编码器的架构,我们将其饲喂描述环境当前状态的原始初始数据。
//--- 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; }
如常,接收到的数据在批量常规化层中进行预处理。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = MathMax(1000, GPTBars); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
之后,我们创建当前状态的嵌入,并将其保存在模型的内部堆栈之中。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronEmbeddingOCL; { int temp[] = {prev_count}; ArrayCopy(descr.windows, temp); } prev_count = descr.count = GPTBars; int prev_wout = descr.window_out = EmbeddingSize; if(!encoder.Add(descr)) { delete descr; return false; }
每个嵌入元素都与环境场景中的单一个体相关联。根据 MFT 方法,我们将实现源数据的位置编码。
//--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronPEOCL; descr.count = prev_count; descr.window = prev_wout; if(!encoder.Add(descr)) { delete descr; return false; }
深入到原始 MFT 方法架构,还有一个 MHSA 模块堆栈。此处是我对拟议架构的第一个偏差。我决定采用上一篇文章的工作,并留下 2 个 Crystal-GCN 层,由批量常规化层分隔。
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronCGConvOCL; descr.count = prev_count * prev_wout; descr.window = descr.count; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 5 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count * prev_wout; descr.batch = MathMax(1000, GPTBars); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; } //--- layer 6 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronCGConvOCL; descr.count = prev_count * prev_wout; descr.window = descr.count; if(!encoder.Add(descr)) { delete descr; return false; }
接下来是 MHSA 层。
//--- layer 7 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMLMHAttentionOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = 4; descr.window_out = 16; descr.layers = 1; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
接下来,该方法的作者提对每名个体的动态均化。此处,在 parallel interaction 模块中,它们针对数据与上下文映射数据进行交叉分析。在我们的例子中,没有即将到来的可能价格走势的地图。取而代之,我们将会用到所分析特征的轨迹上下文分析。
接下来,在我们的架构中,是上面创建的并行计算模块。在这个模块中,我们使用带有 4 个关注度的自关注和交叉关注模块。该方法的作者在他们的工作中仅在并行互动模块中用了 1 个关注度。个体互动模式的数量由 NForecast 常量确定。
//--- layer 8 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronMFTOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = 4; descr.window_out = 16; descr.layers = NForecast; descr.optimization = ADAM; if(!encoder.Add(descr)) { delete descr; return false; }
请注意以下架构要点。在我们的实现中,我们构建了 CNeuronMFTOCL 类,以这种方式,其结果可表示为 3-维张量 {序列元素数、互动模式、个体数}。这种数据格式对于后续处理不是很方便,因为我们需要操控每个单独的互动模式。互动模式在张量维度内的位置令这项工作复杂化。因此,我们进一步转置数据,以便将个体互动模式移动到张量的第一维。
//--- layer 9 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; descr.window = prev_wout * NForecast; if(!encoder.Add(descr)) { delete descr; return false; }
因此,在编码器的输出处,我们收到了张量 {个体互动模式、个体数量、描述个体动态的向量长度}。
在编码器之后,我们创建终点解码器模型。我们将编码器结果的张量馈送到模型之中。
//--- Endpoints endpoints.Clear(); //--- Input layer if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = (prev_count * prev_wout) * NForecast; descr.activation = None; descr.optimization = ADAM; if(!endpoints.Add(descr)) { delete descr; return false; }
我要提醒您,MTF 方法要求对所有个体互动模式采用具有相同参数的解码器。我们可以使用卷积层来实现类似的方法,其窗口大小和步长等于每名个体互动模式的张量。我们将这个阶段分为 2 个阶段。我们首先折叠每个单独个体的动态。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = NForecast * prev_wout; descr.window = prev_count; descr.step = descr.window; descr.window_out = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!endpoints.Add(descr)) { delete descr; return false; }
然后我们获得每次所分析互动模式的场景选项。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConvOCL; descr.count = NForecast; descr.window = LatentCount * prev_wout; descr.step = descr.window; descr.window_out = 3; descr.activation = None; descr.optimization = ADAM; if(!endpoints.Add(descr)) { delete descr; return false; }
估算单个预测场景概率的模型几乎没有变化。与解码器的情况一样,编码器的结果被馈送到模型之中。
//--- Probability probability.Clear(); //--- Input layer if(!probability.Add(endpoints.At(0))) return false;
它们与预测场景选项相结合。
//--- layer 1 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count * prev_wout * NForecast; descr.step = 3 * NForecast; descr.optimization = ADAM; descr.activation = SIGMOID; if(!probability.Add(descr)) { delete descr; return false; }
数据由 2 个全连接层进行分析。
//--- layer 2 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!probability.Add(descr)) { delete descr; return false; } //--- layer 3 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronBaseOCL; descr.count = NForecast; descr.activation = None; descr.optimization = ADAM; if(!probability.Add(descr)) { delete descr; return false; }
并由 SoftMax 函数常规化。
//--- layer 4 if(!(descr = new CLayerDescription())) return false; descr.type = defNeuronSoftMaxOCL; descr.count = NForecast; descr.step = 1; descr.activation = None; descr.optimization = ADAM; if(!probability.Add(descr)) { delete descr; return false; } //--- return true; }
扮演者模型的架构几乎相同。我只需在处理当前状态编码器的结果方面进行一些小的修改。
还应当注意的是,针对模型架构所做的更改,几乎没影响到环境互动和模型训练算法。因此,所有 EA 都是从前一篇文章中复制而来的,并且进行了最少的编辑。因此,我不会在本文的框架内赘述它们的算法。无论如何,您可以在附件中找到文章中用到的所有程序的完整代码。
3. 测试
我们利用 MQL5 工具实现了多未来变换器算法,并讲述了模型的架构,这就允许我们利用所提议方式来训练模型,预测即将到来的价格走势的几个选项,并评估其实现的可能性。现在是时候在 MetaTrader 5 策略测试器中依据真实数据检查我们的工作成果了。
如常,该模型依据 EURUSD H1 历史数据进行训练和测试。2023 年前 7 个月的数据用于训练模型。为了测试已训练模型,我们采用 2023 年 8 月的历史数据。
为了训练模型,我用到了来自上一篇文章中的训练数据集和训练 EA。因此,可以注意到,所获结果的变化主要是由于模型架构的变化。当然,我们不能排除所用随机参数对模型进行初始化时引入的随机性因素。甚而,在训练过程中,从经验回放缓冲区采样数据也是随机的。但随着训练局次的增加,这个因素的影响会最小化。
在上一篇文章中,该模型展示了相当稳定的结果,但交易数量非常少。在新模型中,我们看到交易数量增加,同时保持积极的结果。
在 2023 年 8 月期间,该模型执行了 13 笔交易,其中 6 笔以盈利了结。该模型在测试期间显示盈利因子为 1.63。
结束语
在本文中,我们领略了另一种预测即将到来的价格走势的方法 — 多未来变换器。这种方法的主要特点之一是为个体运动构建多模态预报,强调它们彼此之间、以及与环境场景的互动。这令我们能够对即将到来的走势做出更准确的预测。
在实践部分,我们利用 MQL5 实现了所提议的方式。我们在 MetaTrader 5 策略测试器中依据真实数据训练和测试了模型。获得的结果确认了所提议方式的有效性。我们可以注意到所获得预测的多样性,这是由于隔离了单个模态的预测,同时维护了对个体互动的分析。
参考
文中所用程序
# | 已发行 | 类型 | 说明 |
---|---|---|---|
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/14226
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



