
交易中的神经网络:广义 3D 引用表达分段
概述
3D 引用表达分段(3D-RES)是多模态领域的一个新兴领域,引发研究人员的极大兴趣。该任务侧重于基于给定的自然语言表达式对目标实例进行分段。然而,传统的 3D-RES 方式仅限涉及单个目标的场景,这极大制约了它们的实际适用性。在真实世界设定中,指令往往会在找不到目标,或需要同时识别多个目标时出状况。该现实体现出存在 3D-RES 模型无法处理的问题。为了解决这一豁口,《3D-GRES:广义 3D 引用表达分段》的作者提出了一种新颖方法,称为广义 3D 引用表达分段(3D-GRES),旨在解释引用任意目标数量的指令。
3D-GRES 的主要意图是准确识别一组相似对象内的多个目标。解决该类任务的关键是分解问题的方式,即允许多个查询来同时处理多对象语言指令的本地化。每个查询负责多对象场景中的单个实例。3D-GRES 的作者介绍了多查询解耦交互网络(MDIN),这是一个旨在简化查询、超点和文本之间交互的模块。为了有效地管理任意数量的目标,引入了一种机制,即允许多个查询独立运行,同时联合生成多对象输出。此处的每个查询都只负责多实例上下文内的单个目标。
为了通过可学习查询统一覆盖点云中的关键目标,作者提出了一个新的文本引导稀疏查询(TSQ)模块,其中利用文本式引用表达。此外,为了同时实现查询之间的独特性,并保持整体语义一致性,作者开发了一种称为多对象解耦优化(MDO)的优化策略。该策略将多对象掩码分解为独立的单对象监督,从而保留每个查询的鉴别能力。查询函数、与点云中包含文本式语义的超点特征之间对齐,确保多个目标之间的语义一致性。
1. 3D-GRES 算法
经典的 3D-RES 任务侧重于在引用表达的引导下,为点云场景内的单个目标对象生成 3D 掩码。这种传统形式有很大的局限性。首先,它不适用于点云中对象与给定表达式无匹配的状况。其二,它未考虑到多个对象满足所述准则的情况。模型能力和实际适用性之间的巨大豁口限制了 3D-RES 技术的实际应用。
为了克服这些限制,提出了广义 3D 引用表达分段(3D-GRES)方法,旨在从文本型描述中识别任意数量的对象。3D-GRES 分析 3D 点云场景 P,和引用表达 E。 这将生成相应的 3D 掩码 M,其或许为空、或包含一个或多个对象。该方法允许使用多目标表达式识别多个对象,并支持 “nothing” 表达式来验证目标对象的存在,从而在对象提取和互动方面提供强化的灵活性和健壮性。
3D-GRES 首先使用预训练的 RoBERTa 模型将其编码为文本令牌 𝒯 来处理输入引用表达。为了促进多模态对齐,编码的令牌被投影到维度-D 的多模态空间之中。位置编码被应用到生成的表示形式。
对于位置为 P、且特征为 F 的输入点云,使用稀疏 3D U-Net 提取超点,并将其投影到相同的 D-维多模态空间之中。
多查询解耦互动网络(MDIN)利用多查询来处理多物体场景中的独立实例,并将它们聚合到最终结果当中。在没有目标物体的场景中,预测依赖于每个查询的置信度分数 — 如果所有查询的置信度都较低,则预测输出 null。
MDIN 由若干雷同的模块组成,每个模块都包含一个查询-超点聚合(QSA)模块、及一个查询-语言聚合(QLA) 模块,该模块促进了查询、超点、和文本之间的交互。与之前所用随机初始化 查询 的模型不同,MDIN 使用文本型引导稀疏查询(TSQ)模块来生成文本型驱动的稀疏查询,确保高效的场景覆盖。此外,多对象解耦优化(MDO)策略支持多查询。
查询 可争取在点云空间内成为一个锚点。经由与 超点 交互,查询捕获点云的全局上下文。值得注意的是,选定的超点在交互过程中充当查询,强化局部聚合。这种本地化关注支持查询的有效分离。
最初,计算超点特征 S 和查询嵌入 Qf之间相似的分布。然后,查询会基于这些相似性分数聚合相关的超点。更新后的场景表示,现由 Qs 通知,会被传递至 QLA 模块,以便为查询-查询、和查询-语言交互建模。QLA 包括一个查询特征 Qs 的自注意力模块,和一个多模态交叉注意力模块,来捕获每个单词-查询之间的依赖关系。
依据相关上下文 Qr、语言感知特征 Ql 查询特征,然后使用一个 MLP 汇总并融合场景通知特征 Qs。
为了确保初在点云场景中始化查询的稀疏分布,同时保留基本的几何和语义信息,3D-GRES 的作者直接在超点上应用最远点采样。
为了进一步强化查询分离,及对不同对象的分配,该方法利用了由 TSQ 生成的查询的内在属性。每个查询都源自点云中的特定超点,内在与相应的对象链接。与目标实例关联的查询,会处理这些实例的分段,而不相关的对象则分配给最近的查询。该方式使用初步的直观约束来理顺查询,并将其分配至不同目标。
作者阐述的 3D-GRES 方法的可视化表示如下所示。
2. 利用 MQL5 实现
在研究了 3D-GRES 方法的理论层面之后,我们转到本文的实践部分,其中我们利用 MQL5 实现我们对所提议方式的愿景。首先,我们研究一下 3D-GRES 算法与我们之前验证的方法有何区别,以及它们有什么共同点。
首先,它是 3D-GRES 方法的多模态。这是我们第一次遇到引用表达,旨在令分析更具针对性的。我们肯定会取该思路的优点。不过,取代使用语言模型,我们将对账户状态和持仓进行编码,作为模型的输入。因此,根据账户状态的嵌入,将引导模型搜索入场或离场点。
另一个高亮的重点是可训练查询的处理方式。如同我们之前审视的模型,3D-GRES 使用一组可训练查询。它们的形成原理是不同的。SPFormer 和 MAFT 采用在训练期间优化,并在推理期间修复的静态查询。因此,模型已学会了一些形态,然后根据 “备案”行事。3D-GRES 的作者提议基于输入数据生成查询,令其更加本地化和动态。为了确保所分析场景空间的最优覆盖率,应用了各种启发式方法。我们还会在实现当中应用该思路。
甚至,在 3D-GRE 中也用到令牌的位置编码。这类似于 MAFT 方法,并作为我们在实现中选择父类的基础。按照这个基础,我们从扩展我们的 OpenCL 程序开始。
2.1综合查询
为了确保可训练查询能最大覆盖场景的空间,我们引入了综合损失,设计用于“排斥”来自近邻的查询:
此处 Sq 表示查询 q 的距离。显然,当 S=0 时,损失等于 1。随着查询之间的平均距离的增加,损失趋向于 0。相较之,在训练期间,模型将更均匀地散布查询。
不过,我们专注的不是损失本身的数值,而是梯度的方向,其调整查询参数,从而最大限度地分离它们彼此。在我们的实现中,我们即刻计算误差梯度,并将其添加到主要的反向传播流当中,启动相应地参数优化。该算法在 DiversityLoss 内核中实现。
该内核采用两个全局数据缓冲区、和两个标量变量作为参数。第一个缓冲区包含当前查询特征,第二个缓冲区存储计算出的综合损失梯度。
__kernel void DiversityLoss(__global const float *data, __global float *grad, const int activation, const int add ) { const size_t main = get_global_id(0); const size_t slave = get_local_id(1); const size_t dim = get_local_id(2); const size_t total = get_local_size(1); const size_t dimension = get_local_size(2);
我们的内核将在三维工作空间内运行。前两维对应于正在分析的查询数量,而第三维表示每个查询的特征向量的大小。为了最大限度地减少对较慢全局内存的访问,我们将沿任务空间的最后两个维度把线程分组至工作群之中。
如常,在内核主体内部,我们首先识别跨全局任务空间所有三个维度的当前线程。接下来,我们声明一个局部内存数组,以便促进工作组内线程之间的数据共享。
__local float Temp[LOCAL_ARRAY_SIZE];
我们还判定所分析数值在全局数据缓冲区中的偏移量。
const int shift_main = main * dimension + dim; const int shift_slave = slave * dimension + dim;
之后,我们从全局数据缓冲区加载值、并判定它们之间的偏差。
const int value_main = data[shift_main]; const int value_slave = data[shift_slave]; float delt = value_main - value_slave;
注意,任务空间和工作群被组织起来,如此每个线程从全局内存中仅读取 2 个数值。接下来,我们需要收集距所有流的距离之和。为此,我们首先组织一个循环来收集局部数组元素中各数值的总和。
for(int d = 0; d < dimension; d++) { for(int i = 0; i < total; i += LOCAL_ARRAY_SIZE) { if(d == dim) { if(i <= slave && (i + LOCAL_ARRAY_SIZE) > slave) { int k = i % LOCAL_ARRAY_SIZE; float val = pow(delt, 2.0f) / total; if(isinf(val) || isnan(val)) val = 0; Temp[k] = ((d == 0 && i == 0) ? 0 : Temp[k]) + val; } } barrier(CLK_LOCAL_MEM_FENCE); } }
值得注意的是,我们最初简单地将两个数值之间的差值存储在一个名为 delt 的变量之中。仅在把距离添加到局部数组之前,我们才会计算数值的平方。选择这种设计是有意为之:我们的损失函数的导数涉及原生差异本身。故此,我们将其保留为原始形式,从而避免以后的冗余重新计算。
在下一步中,我们累积局部数组中所有值的总和。
const int ls = min((int)total, (int)LOCAL_ARRAY_SIZE); int count = ls; do { count = (count + 1) / 2; if(slave < count) { Temp[slave] += ((slave + count) < ls ? Temp[slave + count] : 0); if(slave + count < ls) Temp[slave + count] = 0; } barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
只有这样,我们才能计算所分析查询的综合误差值,以及相应元素的误差梯度。
float loss = exp(-Temp[0]); float gr = 2 * pow(loss, 2.0f) * delt / total; if(isnan(gr) || isinf(gr)) gr = 0;
之后,我们面前有一条令人兴奋的道路:收集所分析查询各个特征项的误差梯度。误差梯度求和的算法类似于上述的距离求和算法。
for(int d = 0; d < dimension; d++) { for(int i = 0; i < total; i += LOCAL_ARRAY_SIZE) { if(d == dim) { if(i <= slave && (i + LOCAL_ARRAY_SIZE) > slave) { int k = i % LOCAL_ARRAY_SIZE; Temp[k] = ((d == 0 && i == 0) ? 0 : Temp[k]) + gr; } } barrier(CLK_LOCAL_MEM_FENCE); } //--- int count = ls; do { count = (count + 1) / 2; if(slave < count && d == dim) { Temp[slave] += ((slave + count) < ls ? Temp[slave + count] : 0); if(slave + count < ls) Temp[slave + count] = 0; } barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1); if(slave == 0 && d == dim) { if(isnan(Temp[0]) || isinf(Temp[0])) Temp[0] = 0; if(add > 0) grad[shift_main] += Deactivation(Temp[0],value_main,activation); else grad[shift_main] = Deactivation(Temp[0],value_main,activation); } barrier(CLK_LOCAL_MEM_FENCE); } }
重点要注意,上述算法结合了前馈和反向传播通验迭代两者。如此集成,令我们能够在模型训练期间使用专门算法,从而在推理过程中消除这些操作。如是结果,该优化对生产场景中的决策时间产生了积极影响。
据此,我们完成了 OpenCL 程序的工作,并转到构造实现 3D-GRES 方法核心思想的类。
2.23D-GRES 方法类
为了实现 3D-GRES 方法中提议的方式,我们将在主程序中创建一个新对象:CNeuronGRES。如前所述,它的核心功能将继承自 CNeuronMAFT 类。新类结构如下所示。
class CNeuronGRES : public CNeuronMAFT { protected: CLayer cReference; CLayer cRefKey; CLayer cRefValue; CLayer cMHRefAttentionOut; CLayer cRefAttentionOut; //--- virtual bool CreateBuffers(void); virtual bool DiversityLoss(CNeuronBaseOCL *neuron, const int units, const int dimension, const bool add = false); //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; //--- virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return false; } virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) override; public: CNeuronGRES(void) {}; ~CNeuronGRES(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint window_sp, uint units_sp, uint heads_sp, uint ref_size, uint layers, uint layers_to_sp, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) override const { return defNeuronGRES; } //--- 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 CNeuronGRES::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint units_count, uint heads, uint window_sp, uint units_sp, uint heads_sp, uint ref_size, uint layers, uint layers_to_sp, ENUM_OPTIMIZATION optimization_type, uint batch ) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) return false;
不幸的是,我们的新类结构与父类有很大不同,其避免全部重用所有继承方法。这也反映在初始化方法的逻辑中。于此,我们不仅必须初始化添加的组件,还必须手工初始化继承的组件。
在 Init 方法主体中,我们首先调用基类的同名初始化方法,其执行输入参数的初始验证,并激活神经层之间的数据交换接口,进行模型操作。
之后,我们将接收到的参数存储在我们的类内部变量之中。
iWindow = window; iUnits = units_count; iHeads = heads; iSPUnits = units_sp; iSPWindow = window_sp; iSPHeads = heads_sp; iWindowKey = window_key; iLayers = MathMax(layers, 1); iLayersSP = MathMax(layers_to_sp, 1);
在此,我们还将声明几个变量,临时存储指向各神经层对象的指针,我们将在方法中初始化这些变量。
CNeuronBaseOCL *base = NULL; CNeuronTransposeOCL *transp = NULL; CNeuronConvOCL *conv = NULL; CNeuronLearnabledPE *pe = NULL;
接下来,我们转到构造可训练查询生成模块。值得一提的是,3D-GRES 的作者提议基于输入点云生成动态查询。不过,所分析点云在元素数量和每个元素的特征向量维数方面可能与可训练查询集不同。我们分两个阶段应对这一挑战。首先,我们转置原始数据张量,并使用卷积层来改变序列中的元素数量。使用卷积层允许我们在独立的单变量序列中执行该操作。
//--- Init Querys cQuery.Clear(); transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(0, 0, OpenCL, iSPUnits, iSPWindow, optimization, iBatch) || !cQuery.Add(transp)) return false; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, 1, OpenCL, iSPUnits, iSPUnits, iUnits, 1, iSPWindow, optimization, iBatch) || !cQuery.Add(conv)) return false; conv.SetActivationFunction(SIGMOID);
在第二阶段,我们执行张量的逆转置,并将其投影到多模态空间之中。
transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(0, 2, OpenCL, iSPWindow, iUnits, optimization, iBatch) || !cQuery.Add(transp)) return false; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, 3, OpenCL, iSPWindow, iSPWindow, iWindow, iUnits, 1, optimization, iBatch) || !cQuery.Add(conv)) return false; conv.SetActivationFunction(SIGMOID);
现在我们只需添加完全可训练的位置编码。
pe = new CNeuronLearnabledPE(); if(!pe || !pe.Init(0, 4, OpenCL, iWindow * iUnits, optimization, iBatch) || !cQuery.Add(pe)) return false;
与父类的算法类似,我们将请求的位置编码数据置于单独的信息流当中。
base = new CNeuronBaseOCL(); if(!base || !base.Init(0, 5, OpenCL, pe.Neurons(), optimization, iBatch) || !base.SetOutput(pe.GetPE()) || !cQPosition.Add(base)) return false;
生成 超点 模型架构的算法是完全从父类复制,未进行任何更改。
//--- Init SuperPoints int layer_id = 6; cSuperPoints.Clear(); for(int r = 0; r < 4; r++) { if(iSPUnits % 2 == 0) { iSPUnits /= 2; CResidualConv *residual = new CResidualConv(); if(!residual || !residual.Init(0, layer_id, OpenCL, 2 * iSPWindow, iSPWindow, iSPUnits, optimization, iBatch) || !cSuperPoints.Add(residual)) return false; } else { iSPUnits--; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, layer_id, OpenCL, 2 * iSPWindow, iSPWindow, iSPWindow, iSPUnits, 1, optimization, iBatch) || !cSuperPoints.Add(conv)) return false; conv.SetActivationFunction(SIGMOID); } layer_id++; } conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, layer_id, OpenCL, iSPWindow, iSPWindow, iWindow, iSPUnits, 1, optimization, iBatch) || !cSuperPoints.Add(conv)) return false; conv.SetActivationFunction(SIGMOID); layer_id++; pe = new CNeuronLearnabledPE(); if(!pe || !pe.Init(0, layer_id, OpenCL, conv.Neurons(), optimization, iBatch) || !cSuperPoints.Add(pe)) return false; layer_id++;
为了生成引用表达的嵌入,我们使用了一个完全连接的 MLP,并添加了一个位置编码层。
//--- Reference cReference.Clear(); base = new CNeuronBaseOCL(); if(!base || !base.Init(iWindow * iUnits, layer_id, OpenCL, ref_size, optimization, iBatch) || !cReference.Add(base)) return false; layer_id++; base = new CNeuronBaseOCL(); if(!base || !base.Init(0, layer_id, OpenCL, iWindow * iUnits, optimization, iBatch) || !cReference.Add(base)) return false; base.SetActivationFunction(SIGMOID); layer_id++; pe = new CNeuronLearnabledPE(); if(!pe || !pe.Init(0, layer_id, OpenCL, base.Neurons(), optimization, iBatch) || !cReference.Add(pe)) return false; layer_id++;
重点要注意,MLP 的输出会产生一个与可训练查询张量维度对齐的张量。这种设计令我们能够将引用表达分解为多个语义分量,从而能够更全面地分析当前的市场状况。
此刻,我们已完成对象的初始化,可用来负责输入数据的主要处理。接下来,我们继续初始化内部神经层对象的循环。不过,在此之前,我们清除内部对象集合数组,从而确保洁净的设置。
//--- Inside layers cQKey.Clear(); cQValue.Clear(); cSPKey.Clear(); cSPValue.Clear(); cSelfAttentionOut.Clear(); cCrossAttentionOut.Clear(); cMHCrossAttentionOut.Clear(); cMHSelfAttentionOut.Clear(); cMHRefAttentionOut.Clear(); cRefAttentionOut.Clear(); cRefKey.Clear(); cRefValue.Clear(); cResidual.Clear(); for(uint l = 0; l < iLayers; l++) { //--- Cross-Attention //--- Query conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0,layer_id, OpenCL, iWindow, iWindow, iWindowKey*iHeads, iUnits, 1,optimization, iBatch) || !cQuery.Add(conv)) return false; layer_id++;
在循环主体中,我们首先初始化交叉注意力 查询超点 对象。在此,我们为注意力模块创建一个 查询 实体生成对象。然后,如有必要,我们添加生成 键 和 值 实体的对象。
if(l % iLayersSP == 0) { //--- Key conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iSPHeads, iSPUnits, 1, optimization, iBatch) || !cSPKey.Add(conv)) return false; layer_id++; //--- Value conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindowKey * iSPHeads, iSPUnits, 1, optimization, iBatch) || !cSPValue.Add(conv)) return false; layer_id++; }
我们加了一个记录多头注意力结果的层。
//--- Multy-Heads Attention Out base = new CNeuronBaseOCL(); if(!base || !base.Init(0, layer_id, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) || !cMHCrossAttentionOut.Add(base)) return false; layer_id++;
以及一个结果伸缩层。
//--- Cross-Attention Out conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, layer_id, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, 1, optimization, iBatch) || !cCrossAttentionOut.Add(conv)) return false; layer_id++;
交叉注意力模块末尾是一个残差连接层。
//--- Residual base = new CNeuronBaseOCL(); if(!base || !base.Init(0, layer_id, OpenCL, iWindow * iUnits, optimization, iBatch) || !cResidual.Add(base)) return false; layer_id++;
在下一步中,我们初始化 自注意力 模块,以便分析 查询-查询依赖关系。在此,我们根据上一个交叉注意力模块的结果生成所有实体。
//--- Self-Attention //--- Query conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0,layer_id, OpenCL, iWindow, iWindow, iWindowKey*iHeads, iUnits, 1, optimization, iBatch) || !cQuery.Add(conv)) return false; layer_id++; //--- Key conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0,layer_id, OpenCL, iWindow, iWindow, iWindowKey*iHeads, iUnits, 1, optimization, iBatch) || !cQKey.Add(conv)) return false; layer_id++; //--- Value conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0,layer_id, OpenCL, iWindow, iWindow, iWindowKey*iHeads, iUnits, 1, optimization, iBatch) || !cQValue.Add(conv)) return false; layer_id++;
在这种情况下,对于每个内层,我们生成含有相同数量注意力头的所有实体。
添加一个层,记录多头注意力的结果。
//--- Multy-Heads Attention Out base = new CNeuronBaseOCL(); if(!base || !base.Init(0, layer_id, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) || !cMHSelfAttentionOut.Add(base)) return false; layer_id++;
以及一个结果伸缩层。
//--- Self-Attention Out conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, layer_id, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, 1, optimization, iBatch) || !cSelfAttentionOut.Add(conv)) return false; layer_id++;
与 自注意力 模块并行的是 查询 交叉注意力模块,用于语义引用表达。此处的 查询 实体是基于上一个交叉注意力模块的结果而生成的。
//--- Reference Cross-Attention //--- Query conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0,layer_id, OpenCL, iWindow, iWindow, iWindowKey*iHeads, iUnits, 1, optimization, iBatch) || !cQuery.Add(conv)) return false; layer_id++;
键-值 张量由先前准备的语义嵌入形成。
//--- Key conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0,layer_id, OpenCL, iWindow, iWindow, iWindowKey*iHeads, iUnits, 1, optimization, iBatch) || !cRefKey.Add(conv)) return false; layer_id++; //--- Value conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0,layer_id, OpenCL, iWindow, iWindow, iWindowKey*iHeads, iUnits, 1, optimization, iBatch) || !cRefValue.Add(conv)) return false; layer_id++;
与 自注意力 模块类似,我们在每个新层上生成含有相同数量的注意力头的所有实体。
接下来,我们添加多头注意力结果、及结果伸缩层。
//--- Multy-Heads Attention Out base = new CNeuronBaseOCL(); if(!base || !base.Init(0, layer_id, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) || !cMHRefAttentionOut.Add(base)) return false; layer_id++; //--- Cross-Attention Out conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0,layer_id, OpenCL, iWindowKey*iHeads, iWindowKey*iHeads, iWindow, iUnits, 1, optimization, iBatch) || !cRefAttentionOut.Add(conv)) return false; layer_id++; if(!conv.SetGradient(((CNeuronBaseOCL*)cSelfAttentionOut[cSelfAttentionOut.Total() - 1]).getGradient(), true)) return false;
这个模块由一个残差连接层完成,它结合了所有三个注意力模块的结果。
//--- Residual base = new CNeuronBaseOCL(); if(!base || !base.Init(0, layer_id, OpenCL, iWindow * iUnits, optimization, iBatch)) return false; if(!cResidual.Add(base)) return false; layer_id++;
充实查询的最终处理是在具有剩余连接的 FeedForward 模块中实现。它的结构类似于原版变换器。
//--- Feed Forward conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, 4*iWindow, iUnits, 1, optimization, iBatch)) return false; conv.SetActivationFunction(LReLU); if(!cFeedForward.Add(conv)) return false; layer_id++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, layer_id, OpenCL, 4 * iWindow, 4 * iWindow, iWindow, iUnits, 1, optimization, iBatch)) return false; if(!cFeedForward.Add(conv)) return false; layer_id++; //--- Residual base = new CNeuronBaseOCL(); if(!base || !base.Init(0, layer_id, OpenCL, iWindow * iUnits, optimization, iBatch)) return false; if(!base.SetGradient(conv.getGradient())) return false; if(!cResidual.Add(base)) return false; layer_id++;
此外,我们将从父类转移用于校正对象中心的算法。注意,该对象由 3D-GRES 方法的作者提供。
//--- Delta position conv = new CNeuronConvOCL(); if(!conv || !conv.Init(0, layer_id, OpenCL, iWindow, iWindow, iWindow, iUnits, 1, optimization, iBatch)) return false; conv.SetActivationFunction(SIGMOID); if(!cQPosition.Add(conv)) return false; layer_id++; base = new CNeuronBaseOCL(); if(!base || !base.Init(0, layer_id, OpenCL, conv.Neurons(), optimization, iBatch)) return false; if(!base.SetGradient(conv.getGradient())) return false; if(!cQPosition.Add(base)) return false; layer_id++; }
现在,我们转到循环的下一次迭代,创建内层对象。所有循环迭代成功完成之后,我们替换指向数据缓冲区的指针,这令我们能够减少数据复制操作的数量,并加快学习过程。
base = cResidual[iLayers * 3 - 1]; if(!SetGradient(base.getGradient())) return false; //--- SetOpenCL(OpenCL); //--- return true; }
在方法操作结束时,我们返回布尔结果至调用程序,指示执行步骤的成功或失败。
值得注意的是,就像我们之前的文章一样,我们已将辅助数据缓冲区的创建移至一个名为 CreateBuffers 的单独方法当中。我鼓励您独立审查该方法。附件中提供了其完整的源代码。
初始化新类对象之后,我们继续构造前馈通验算法,其在 feedForward 方法中实现。这一次,该方法接受两个指向输入数据对象的指针。其中一个包含所分析点云,另一个表示引用表达。
bool CNeuronGRES::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput) { //--- Superpoints CNeuronBaseOCL *superpoints = NeuronOCL; int total_sp = cSuperPoints.Total(); for(int i = 0; i < total_sp; i++) { if(!cSuperPoints[i] || !((CNeuronBaseOCL*)cSuperPoints[i]).FeedForward(superpoints)) return false; superpoints = cSuperPoints[i]; }
在方法主体中,我们即刻组织了一个小型 超点 生成模型的前馈循环。类似地,我们生成查询。
//--- Query CNeuronBaseOCL *query = NeuronOCL; for(int i = 0; i < 5; i++) { if(!cQuery[i] || !((CNeuronBaseOCL*)cQuery[i]).FeedForward(query)) return false; query = cQuery[i]; }
为引用表达生成语义嵌入的张量需要一些更多的工作。接收引用表达作为原始数据缓冲区。但是前馈通验中的内部模块需要一个神经层对象作为输入。因此,我们使用内部语义嵌入生成器模型的第一层作为占位符来接收输入数据,类似于我们在主模型中处理输入的方式。不过,在这种情况下,我们不会完全复制缓冲区内容;代之,我们将底层数据指针替换为缓冲区中的数据指针。
//--- Reference CNeuronBaseOCL *reference = cReference[0]; if(!SecondInput) return false; if(reference.getOutput() != SecondInput) if(!reference.SetOutput(SecondInput, true)) return false;
接下来,我们为内部模型运行一个前馈循环。
for(int i = 1; i < cReference.Total(); i++) { if(!cReference[i] || !((CNeuronBaseOCL*)cReference[i]).FeedForward(reference)) return false; reference = cReference[i]; }
这样就完成了源数据的初步处理,我们可转到主要的数据解码算法。为此,我们组织了一个遍历解码器内层的循环。
CNeuronBaseOCL *inputs = query, *key = NULL, *value = NULL, *base = NULL, *cross = NULL, *self = NULL; //--- Inside layers for(uint l = 0; l < iLayers; l++) { //--- Calc Position bias cross = cQPosition[l * 2]; if(!cross || !CalcPositionBias(cross.getOutput(), ((CNeuronLearnabledPE*)superpoints).GetPE(), cPositionBias[l], iUnits, iSPUnits, iWindow)) return false;
我们定义位置偏移系数开始,遵循 MAFT 方法中所用的方式。这与最初的 3D-GRES 算法不同,在其中作者用到一个 MLP 来生成注意力掩码。
接下来,我们继续讨论交叉注意力模块 QSA,其负责针对 查询-超点 依赖关系建模。在该模块中,我们首先为 查询、键 和 值 实体生成张量。后两者仅在必要时生成。
//--- Cross-Attention query = cQuery[l * 3 + 5]; if(!query || !query.FeedForward(inputs)) return false; key = cSPKey[l / iLayersSP]; value = cSPValue[l / iLayersSP]; if(l % iLayersSP == 0) { if(!key || !key.FeedForward(superpoints)) return false; if(!value || !value.FeedForward(cSuperPoints[total_sp - 2])) return false; }
然后,我们分析依赖关系,同时参考位置偏差系数。
if(!AttentionOut(query, key, value, cScores[l * 3], cMHCrossAttentionOut[l], cPositionBias[l], iUnits, iHeads, iSPUnits, iSPHeads, iWindowKey, true)) return false;
我们伸缩多头注意力的结果,并添加残差连接值,随后归一化数据。
base = cCrossAttentionOut[l]; if(!base || !base.FeedForward(cMHCrossAttentionOut[l])) return false; value = cResidual[l * 3]; if(!value || !SumAndNormilize(inputs.getOutput(), base.getOutput(), value.getOutput(), iWindow, false, 0, 0, 0, 1)|| !SumAndNormilize(cross.getOutput(), value.getOutput(), value.getOutput(), iWindow, true, 0, 0, 0, 1)) return false; inputs = value;
在下一步中,我们规划 QLA 模块的操作。此处我们需要组织一个前馈通验两个注意力模块:
- 自我注意力 → 查询;
- 交叉注意力 → 查询引用。
首先,我们实现 自注意力 模块的操作。在此,我们依据从上一个解码器模块接收到的数据,完整生成 查询、键 和 值 实体张量。
//--- Self-Atention query = cQuery[l * 3 + 6]; if(!query || !query.FeedForward(inputs)) return false; key = cQKey[l]; if(!key || !key.FeedForward(inputs)) return false; value = cQValue[l]; if(!value || !value.FeedForward(inputs)) return false;
然后我们分析原版多头注意力模块中的依赖关系。
if(!AttentionOut(query, key, value, cScores[l * 3 + 1], cMHSelfAttentionOut[l], -1, iUnits, iHeads, iUnits, iHeads, iWindowKey, false)) return false; self = cSelfAttentionOut[l]; if(!self || !self.FeedForward(cMHSelfAttentionOut[l])) return false;
之后,我们伸缩得到的结果。
交叉注意力模块的构造方式类似。唯一的区别是 键 和 值 实体是据引用表达的语义嵌入中生成的。
//--- Reference Cross-Attention query = cQuery[l * 3 + 7]; if(!query || !query.FeedForward(inputs)) return false; key = cRefKey[l]; if(!key || !key.FeedForward(reference)) return false; value = cRefValue[l]; if(!value || !value.FeedForward(reference)) return false; if(!AttentionOut(query, key, value, cScores[l * 3 + 2], cMHRefAttentionOut[l], -1, iUnits, iHeads, iUnits, iHeads, iWindowKey, false)) return false; cross = cRefAttentionOut[l]; if(!cross || !cross.FeedForward(cMHRefAttentionOut[l])) return false;
接下来,我们汇总所有三个注意力模块的结果,并归一化获得的数据。
value = cResidual[l * 3 + 1]; if(!value || !SumAndNormilize(cross.getOutput(), self.getOutput(), value.getOutput(), iWindow, false, 0, 0, 0, 1) || !SumAndNormilize(inputs.getOutput(), value.getOutput(), value.getOutput(), iWindow, true, 0, 0, 0, 1)) return false; inputs = value;
接下来是原版变换器的 FeedForward 模块,含有残差连接和数据归一化。
//--- Feed Forward base = cFeedForward[l * 2]; if(!base || !base.FeedForward(inputs)) return false; base = cFeedForward[l * 2 + 1]; if(!base || !base.FeedForward(cFeedForward[l * 2])) return false; value = cResidual[l * 3 + 2]; if(!value || !SumAndNormilize(inputs.getOutput(), base.getOutput(), value.getOutput(), iWindow, true, 0, 0, 0, 1)) return false; inputs = value;
您或许已注意到,构造的前馈通验算法是 3D-GRES 和 MAFT 的一种共生体。故此,我们只需为 MAFT 方法添加最后一笔 — 调整查询位置。
//--- Delta Query position base = cQPosition[l * 2 + 1]; if(!base || !base.FeedForward(inputs)) return false; value = cQPosition[(l + 1) * 2]; query = cQPosition[l * 2]; if(!value || !SumAndNormilize(query.getOutput(), base.getOutput(), value.getOutput(), iWindow, false, 0, 0, 0,0.5f)) return false; }
之后,我们转入下一个解码器层。遍历解码器所有内层的迭代完成后,我们将丰富的查询值与它们的位置编码相加。我们通过基本接口将结果传递到模型的下一层。
value = cQPosition[iLayers * 2]; if(!value || !SumAndNormilize(inputs.getOutput(), value.getOutput(), Output, iWindow, true, 0, 0, 0, 1)) return false; //--- return true; }
此刻,我们简单地将布尔结果返回给调用程序,示意操作是否成功完成。
至此,我们总结了前馈通验方法的实现,并继续反向传播算法。如常,该过程分为两个阶段:
- 梯度分布(calcInputGradients);
- 模型参数的优化(updateInputWeights)。
在第一阶段,我们遵循前向通验操作的相反顺序,反向传播误差梯度。在第二阶段,我们调用包含可训练参数的内层相应更新方法。初看,这看起来相当标准。不过,有一个与综合查询相关的特定细节。因此,我们来更详细地研究 calcInputGradients 方法的实现,因其负责分派误差梯度。
该方法接收指向三个数据对象的指针作为参数,以及一个指定第二个输入源激活函数的常量。
bool CNeuronGRES::calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput, CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = -1) { if(!NeuronOCL || !SecondGradient) return false;
在方法主体中,我们只验证其中两个指针。在前馈通验期间,我们存储指向第二个输入源的指针。故在该阶段,验证参数中的指针有效性对我们来说并不重要。不过,对于存储误差梯度的缓冲区,情况并非如此。这就是为什么我们在继续之前要明确检查其有效性的原因。
此刻,我们还声明了一些变量来临时存储指向相关对象的指针。我们的实现准备阶段到此完毕。
CNeuronBaseOCL *residual = GetPointer(this), *query = NULL, *key = NULL, *value = NULL, *key_sp = NULL, *value_sp = NULL, *base = NULL;
接下来,我们规划一个遍历解码器内层的逆向循环。
//--- Inside layers for(int l = (int)iLayers - 1; l >= 0; l--) { //--- Feed Forward base = cFeedForward[l * 2]; if(!base || !base.calcHiddenGradients(cFeedForward[l * 2 + 1])) return false; base = cResidual[l * 3 + 1]; if(!base || !base.calcHiddenGradients(cFeedForward[l * 2])) return false;
归因精心规划在内部对象中替换了缓冲区指针,我们避免了不必要的数据复制操作,并从遍历 FeedForward 模块传播误差梯度开始。
在 FeedForward 模块的输入层级获得的误差梯度,与我们类的输出层级的相应值相加,其与该模块中的残差数据流一致。然后,这些操作的结果被传至 自注意力 模块的结果缓冲区。
//--- Residual value = cSelfAttentionOut[l]; if(!value || !SumAndNormilize(base.getGradient(), residual.getGradient(), value.getGradient(), iWindow, false, 0, 0, 0, 1)) return false; residual = value;
FeedForward 模块的输入由三个注意力模块的输出之和组成。相应地,生成的误差梯度必须传播回所有源。在汇总数据时,我们把完整梯度传播到每个分量。QSA 模块的输出也当作解码器中其它模块的输入。故此,其误差梯度将在以后累积,遵循与残差数据流相同的逻辑。为了避免不必要地将误差梯度复制到 查询-引用 交叉注意力模块之中,我们在对象初始化期间提前规划了指针替换。如是结果,当把数据传递到 自注意力 模块时,我们同时把相同的数据传递到 查询-引用 交叉注意力模块当中。这种小型优化剔除了冗余操作,有助于降低内存占用和训练时间。
现在,我们继续遍历 查询-引用 交叉注意力模块传播误差梯度。
//--- Reference Cross-Attention base = cMHRefAttentionOut[l]; if(!base || !base.calcHiddenGradients(cRefAttentionOut[l], NULL)) return false; query = cQuery[l * 3 + 7]; key = cRefKey[l]; value = cRefValue[l]; if(!AttentionInsideGradients(query, key, value, cScores[l * 3 + 2], base, iUnits, iHeads, iUnits, iHeads, iWindowKey)) return false;
我们将 查询 实体的误差梯度传递到 QSA 模块当中,之前已加入了从 FeedForward 模块(残差连接流)获得的误差梯度。
base = cResidual[l * 3]; if(!base || !base.calcHiddenGradients(query, NULL)) return false; value = cCrossAttentionOut[l]; if(!SumAndNormilize(base.getGradient(), residual.getGradient(),value.getGradient(), iWindow, false, 0, 0, 0, 1)) return false; residual = value;
类似地,我们遍历 自注意力 模块传递误差梯度。
//--- Self-Attention base = cMHSelfAttentionOut[l]; if(!base || !base.calcHiddenGradients(cSelfAttentionOut[l], NULL)) return false; query = cQuery[l * 3 + 6]; key = cQKey[l]; value = cQValue[l]; if(!AttentionInsideGradients(query, key, value, cScores[l * 2 + 1], base, iUnits, iHeads, iUnits, iHeads, iWindowKey)) return false;
但现在我们需要将所有三个实体的误差梯度添加到 QSA 模块当中。为此,我们将误差梯度按顺序传播到残差连接层级,并将获得的数值与先前累积的 QSA 模块梯度之和累加。
base = cResidual[l * 3 + 1]; if(!base.calcHiddenGradients(query, NULL)) return false; if(!SumAndNormilize(base.getGradient(), residual.getGradient(), residual.getGradient(), iWindow, false, 0, 0, 0, 1)) return false; if(!base.calcHiddenGradients(key, NULL)) return false; if(!SumAndNormilize(base.getGradient(), residual.getGradient(), residual.getGradient(), iWindow, false, 0, 0, 0, 1)) return false; if(!base.calcHiddenGradients(value, NULL)) return false; if(!SumAndNormilize(base.getGradient(), residual.getGradient(), residual.getGradient(), iWindow, false, 0, 0, 0, 1)) return false;
我们还会将累积梯度值总和传递到来自查询位置编码的并行信息流,将其添加到来自另一个信息流的梯度当中。
//--- Qeury position base = cQPosition[l * 2]; value = cQPosition[(l + 1) * 2]; if(!base || !SumAndNormilize(value.getGradient(), residual.getGradient(), base.getGradient(), iWindow, false, 0, 0, 0, 1)) return false;
现在我们只需遍历 QSA 模块传播误差梯度。此处,我们运用相同的算法遍历注意力模块传播误差梯度,但我们对来自多个解码器层的 “键” 和 “值” 实体的误差梯度进行了调整。我们首先将误差梯度收集到临时数据缓冲区之中,然后将结果值保存在相应对象的缓冲区当中。
//--- Cross-Attention base = cMHCrossAttentionOut[l]; if(!base || !base.calcHiddenGradients(residual, NULL)) return false; query = cQuery[l * 3 + 5]; if(((l + 1) % iLayersSP) == 0 || (l + 1) == iLayers) { key_sp = cSPKey[l / iLayersSP]; value_sp = cSPValue[l / iLayersSP]; if(!key_sp || !value_sp || !cTempCrossK.Fill(0) || !cTempCrossV.Fill(0)) return false; } if(!AttentionInsideGradients(query, key_sp, value_sp, cScores[l * 2], base, iUnits, iHeads, iSPUnits, iSPHeads, iWindowKey)) return false; if(iLayersSP > 1) { if((l % iLayersSP) == 0) { if(!SumAndNormilize(key_sp.getGradient(), GetPointer(cTempCrossK), key_sp.getGradient(), iWindowKey, false, 0, 0, 0, 1)) return false; if(!SumAndNormilize(value_sp.getGradient(), GetPointer(cTempCrossV), value_sp.getGradient(), iWindowKey, false, 0, 0, 0, 1)) return false; } else { if(!SumAndNormilize(key_sp.getGradient(), GetPointer(cTempCrossK), GetPointer(cTempCrossK), iWindowKey, false, 0, 0, 0, 1)) return false; if(!SumAndNormilize(value_sp.getGradient(), GetPointer(cTempCrossV), GetPointer(cTempCrossV), iWindowKey, false, 0, 0, 0, 1)) return false; } }
来自 查询 实体的误差梯度将被传播到初始数据的层级。此处我们还加入了残差连接的信息流数据。之后,我们转到遍历解码器层逆向循环的下一次迭代。
if(l == 0) base = cQuery[4]; else base = cResidual[l * 3 - 1]; if(!base || !base.calcHiddenGradients(query, NULL)) return false; //--- Residual if(!SumAndNormilize(base.getGradient(), residual.getGradient(), base.getGradient(), iWindow, false, 0, 0, 0, 1)) return false; residual = base; }
遍历解码器所有层传播误差梯度成功之后,我们需要通过数据预处理模块的操作将其传播到源数据层级。首先,我们传播来自可训练查询的误差梯度。为此,我们遍历一个位置编码层传递误差梯度。
//--- Qeury query = cQuery[3]; if(!query || !query.calcHiddenGradients(cQuery[4])) return false;
在该阶段,我们注入来自相应信息流的位置编码误差梯度。
base = cQPosition[0]; if(!DeActivation(base.getOutput(), base.getGradient(), base.getGradient(), SIGMOID) || !(((CNeuronLearnabledPE*)cQuery[4]).AddPEGradient(base.getGradient()))) return false;
然后我们添加查询 综合误差的梯度,但此处我们的工作已无位置编码信息。该步骤是有意为之,如此这般综合误差不会影响位置编码。
if(!DiversityLoss(query, iUnits, iWindow, true)) return false;
然后,在查询生成模型的各层上执行一个简单的逆向迭代循环,将误差梯度传播到输入数据层级。
for(int i = 2; i >= 0; i--) { query = cQuery[i]; if(!query || !query.calcHiddenGradients(cQuery[i + 1])) return false; } if(!NeuronOCL.calcHiddenGradients(query, NULL)) return false;
应当注意的是,误差梯度也必须传播到内部超点生成模型的输入层级。为了防止数据丢失,我们在局部变量中存储了一个指向输入数据对象梯度缓冲区的指针。然后,我们在输入数据对象中将其替换为查询生成模型转置层中的梯度缓冲区。
转置层不包含任何可训练参数,故其误差梯度的丢失不会带来任何风险。
CBufferFloat *inputs_gr = NeuronOCL.getGradient(); if(!NeuronOCL.SetGradient(query.getGradient(), false)) return false;
下一步是遍历超点生成模型传播误差梯度。不过,重点要注意,在遍历解码器层反向传播期间,我们没有将任何梯度传播到该模型。因此,我们必须首先从相应的 键 和 值 实体中收集误差梯度。我们知道,每个实体至少有一个张量。但还有另一个重要细节:“键” 实体是自 超点 模型最后一层的输出中生成,配以位置编码,而 值 实体是从倒数第二层获取的,无位置编码。故此,误差梯度必须沿着这些特定的数据路径传播。
首先,我们计算 键 实体第一层的误差梯度,并将其传递到内部模型的最后一层。
//--- Superpoints //--- From Key int total_sp = cSuperPoints.Total(); CNeuronBaseOCL *superpoints = cSuperPoints[total_sp - 1]; if(!superpoints || !superpoints.calcHiddenGradients(cSPKey[0])) return false;
然后我们检查 键 实体的层数,如有必要,为了预防之前获得的误差梯度丢失,我们将替换数据缓冲区。
if(cSPKey.Total() > 1) { CBufferFloat *grad = superpoints.getGradient(); if(!superpoints.SetGradient(GetPointer(cTempSP), false)) return false;
然后,我们遍历该实体的其余层,计算误差梯度,然后将结果与先前累积的值相加。
for(int i = 1; i < cSPKey.Total(); i++) { if(!superpoints.calcHiddenGradients(cSPKey[i]) || !SumAndNormilize(superpoints.getGradient(), grad, grad, iWindow, false, 0, 0, 0, 1)) return false; }
所有循环迭代成功完成之后,我们返回一个指向缓冲区的指针,其中包含累积的误差梯度合计。
if(!superpoints.SetGradient(grad, false)) return false; }
因此,在 超点 模型的最后一层,我们自 键 实体的所有层收集误差梯度,现在我们可将其传播到指定模型的下一层级。
superpoints = cSuperPoints[total_sp - 2]; if(!superpoints || !superpoints.calcHiddenGradients(cSuperPoints[total_sp - 1])) return false;
现在,在同一层级,我们需要从 值 实体收集误差梯度。此处我们运用相同算法。但在这种情况下,在误差梯度缓冲区中,我们已得到自后续层接收到的数据。因此,我们即刻替换数据缓冲区,然后循环从并行数据流中收集信息。
//--- From Value CBufferFloat *grad = superpoints.getGradient(); if(!superpoints.SetGradient(GetPointer(cTempSP), false)) return false; for(int i = 0; i < cSPValue.Total(); i++) { if(!superpoints.calcHiddenGradients(cSPValue[i]) || !SumAndNormilize(superpoints.getGradient(), grad, grad, iWindow, false, 0, 0, 0, 1)) return false; } if(!superpoints.SetGradient(grad, false)) return false;
然后我们还添加了综合误差,这就允许我们尽可能地综合 超点。
if(!DiversityLoss(superpoints, iSPUnits, iSPWindow, true)) return false;
接下来,在遍历 超点 模型层的逆向循环中,我们将误差梯度传播到输入数据的层级。
for(int i = total_sp - 3; i >= 0; i--) { superpoints = cSuperPoints[i]; if(!superpoints || !superpoints.calcHiddenGradients(cSuperPoints[i + 1])) return false; } //--- Inputs if(!NeuronOCL.calcHiddenGradients(cSuperPoints[0])) return false;
应该记住,即处理查询信息流之后,我们在输入层级保留了部分误差梯度。在此期间,我们替换了数据缓冲区。现在我们将两个信息流的误差梯度相加。然后,我们返回指向数据缓冲区的指针。
if(!SumAndNormilize(NeuronOCL.getGradient(), inputs_gr, inputs_gr, 1, false, 0, 0, 0, 1)) return false; if(!NeuronOCL.SetGradient(inputs_gr, false)) return false;
以这种方式,我们从第一个输入数据源的两个信息流中收集误差梯度。但我们仍需将误差梯度传播到第二个源数据对象。为此,我们首先同步指向第二个源数据对象的指针,以及 Reference 模型第一层的误差梯度缓冲区。
base = cReference[0]; if(base.getGradient() != SecondGradient) { if(!base.SetGradient(SecondGradient)) return false; base.SetActivationFunction(SecondActivation); }
然后,在指定模型的最后一层,我们从相应的 键 和 值 实体的所有张量中收集误差梯度。该算法类似于上面讨论的算法。
base = cReference[2]; if(!base || !base.calcHiddenGradients(cRefKey[0])) return false; inputs_gr = base.getGradient(); if(!base.SetGradient(GetPointer(cTempQ), false)) return false; if(!base.calcHiddenGradients(cRefValue[0])) return false; if(!SumAndNormilize(base.getGradient(), inputs_gr, inputs_gr, 1, false, 0, 0, 0, 1)) return false; for(uint i = 1; i < iLayers; i++) { if(!base.calcHiddenGradients(cRefKey[i])) return false; if(!SumAndNormilize(base.getGradient(), inputs_gr, inputs_gr, 1, false, 0, 0, 0, 1)) return false; if(!base.calcHiddenGradients(cRefValue[i])) return false; if(!SumAndNormilize(base.getGradient(), inputs_gr, inputs_gr, 1, false, 0, 0, 0, 1)) return false; } if(!base.SetGradient(inputs_gr, false)) return false;
我们遍历位置编码层传播误差梯度。
base = cReference[1]; if(!base.calcHiddenGradients(cReference[2])) return false;
并且我们添加了一个向量综合误差,以便确保语义成分的最大多样性。
if(!DiversityLoss(base, iUnits, iWindow, true)) return false;
之后,我们将误差梯度传播到输入数据层级。
base = cReference[0]; if(!base.calcHiddenGradients(cReference[1])) return false; //--- return true; }
在方法执行结束时,我们简单地将操作的逻辑结果返回给调用程序即可。
这标志着我们在新类中实现的算法方法检查结束。该类及其所有方法的完整源代码,都可在附件中找到。在那里,您还可找到模型架构的详细说明,以及准备本文时用到的所有程序。
可训练模型的架构几乎完全复制自以前的工作。唯一所做的修改是针对编码器中负责描述环境状态的单层。
此外,还对模型训练程序和与环境的交互逻辑进行了小幅更新。之所以进行这些修改,是因为我们需要将第二个数据源传递到环境状态的编码器之中。不过,这些修改i是有针对性的,并且最小。如前提醒,我们使用账户状态向量作为引用表达。这个向量的准备已实现,故其被我们的参与者模型所用。
3. 测试
我们已完成了大量工作,并利用 MQL5 构建了一个混合系统,其结合了 3D-GRES 和 MAFT 方法中提议的方法。现在是时候评估成果了。我们的任务是运用拟议的技术在真实历史数据上训练模型,并评估经训练的参与者政策的性能。
如常,为了训练模型,我们采用 EURUSD 金融产品整个 2023 年的真实历史数据,以及 H1 时间帧。所有指标参数均按其默认值设置。
在训练过程中,我们应用了之前在早期研究中验证过的算法。
训练后的参与者政策在 MetaTrader 5 策略测试器中据 2024 年 1 月的历史数据进行了测试。所有其它参数保持不变。测试结果呈现如下。
在测试期间,该模型执行了 22 笔交易,其中正好一半以盈利结束。值得注意的是,每笔盈利交易的平均利润是每笔亏损交易平均亏损的两倍多。最大的盈利交易比最大的亏损高出四倍。如是结果,该模型实现了 2.63 的盈利因子。然而,交易数量少、且测试周期短,不允许我们对该方法的长期有效性得出任何明确的结论。在实时环境中使用模型之前,应在更长的历史数据集上对其进行训练,并进行全面测试。
结束语
广义 3D 引用表达分段(3D-GRES)方法中提议的方式,通过对市场数据进行更深入的分析,在交易领域展现出有前景的适用性。该方法可适用于分段和分析多个市场信号,从而更精确地解释复杂的市场条件,并最终提升预测准确性、及决策能力。
在本文的实践部分,我们利用 MQL5 实现了我们所提议方式的愿景。我们的实验证明了所提议方案在实际交易场景中的潜力。
参考
文章中所用程序# | 名称 | 类型 | 说明 |
---|---|---|---|
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/15997



