
交易中的神经网络:层次化向量变换器(HiVT)
概述
智驾面临的挑战与交易者面临的挑战明显重叠。对于汽车智驾,在动态环境中导航的安全机动是一项关键任务。为达成这一目标,这些车辆必须洞察周围环境,并预测道路上的未来事件。然而,准确预测附近道路使用者,诸如车辆、自行车、和行人的机动是一个复杂问题,尤其是在他们的目标或意图未明时。在多个体出行场景中,个体行为受到与其他个体复杂交互的影响,而依赖于地图的交通规则更加复杂。因此,理解场景中多名个体的综合行为极具挑战性。
最近的研究用到一种矢量化方式,通过从轨迹和地图元素中提取向量或点集来提供更紧凑的场景表示。然而,现有的矢量化方法难以在快速变化的出行条件下进行实时运动预测。因为这样的方法通常都对坐标系偏移敏感。为了缓解该问题,以目标个体为中心归一化场景,并与其移动方向对齐。在预测大量个体运动时,这种方式会变得有问题,瓶颈在于针对每名目标个体的重复场景归一化、及特征刷新的高昂计算成本。此外,现有方法会针对跨空间和时间维度的所有元素之间的关系建模,以便捕获矢量化元素之间的互动细节。随着元素数量的增加,这不可避免地导致泛滥的计算开销。由于准确的实时预测对于智驾安全至关重要,故许多研究人员正努力通过开发一个新框架来实现更快、更精确的多个体运动预测,从而将这一过程提升到新的水平。
论文《HiVT: 多个体运动预测的层次化矢量变换》中提出了一种这样的方式。该方法利用对称性和层次化结构进行多个体运动预测。HiVT 的作者将运动预测任务分解为多个阶段,并基于变换器架构对元素之间的互动进行层次化建模。
在第一阶段,该模型提取局部上下文特征,以避免针对所有元素之间的互动进行成本高昂的建模。整个场景被划分为一组局部区域,每个区域都以一名已建模个体为中心。上下文特征是从每位个体中心区域的局部矢量化元素中提取,其中包含与中心个体相关的丰富信息。
在第二阶段,为了补偿局部视野的局限性,并捕获远景依赖关系,引入了以个体为中心的区域之间的全局信息传输机制。作者任用一个在局部坐标系之间搭配几何连接的变换器来达成这一点。
组合的局部和全局表示,令解码器能够在模型的单次前向通验中预测所有个体的未来轨迹。为了进一步利用该任务的对称性,作者引入了一种对全局坐标系偏移不变的场景表示,按照相对位置来描述所有矢量化元素。基于这种场景表示,他们实现了针对空间学习的旋转不变交叉注意力模块,令模型能够独立于场景方向学习局部和全局表示。
1. HiVT 算法
HiVT 方法首先将道路场景以矢量化元素的集合表述。基于该场景表述,模型按层次聚合时空信息。道路场景由个体和地图信息组成。为了结构化场景表示,首先提取矢量化元素,包括个体的道路轨迹区段,及地图数据中的车道区段。
矢量化元素与语义和几何属性相关联。与之前的矢量化方法不同,那时个体或车道的几何属性包括绝对点位置,作者避免使用绝对位置,替换为以相对位置来描述几何属性。这令场景整体是一组向量。具体来说,个体 i 的轨迹表示为 "pt,i - pt-1,i",其中 pt,i 是个体 i 在时间步 t 的位置。
对于车道段 xi,几何属性定义为 p1,xi - p0,xi,其中 p0,xi 和 p1,xi 是 xi 的起点和终点坐标。将点集转换为向量集,可天然确保平移不变性。不过,元素之间的相对位置信息会丢失。为了保持空间关系,为个体-个体,以及个体-车道对引入了相对位置向量。例如,在时间步 t 处,个体 j 相对于个体 i 的位置向量是 ptj - pti,在保持平移不变性的同时,充分描述了它们的空间关系。这种场景表述确保任何应用的学习函数都遵守转移不变性,不会丢失信息。
为了准确预测高动态环境中的未来个体轨迹,该模型必须有效地学习众多矢量化元素之间的时空依赖关系。变换器在捕获跨越各种任务元素之间的长期依赖关系方面展现出潜力。然而,将变换器直接应用于时空元素会导致计算复杂度为 O((NT + L)^2),其中 N、T 和 L 分别是个体数量、历史时间步、和车道段。为了有效地从大量元素中提取信息,HiVT 通过在每个时间步,针对局部空间关系进行建模,以此分解空间和时间维度。具体来说,该空间被划分为 N 个局部区域,每个区域都有一位个体为中心。在每个局部区域中,中央个体的局部环境由相邻个体的轨迹和局部车道段表示。局部信息聚合到每个区域的特征向量中,针对每个时间步的个体-个体互动、每位个体的时态依赖关系,以及当前时间步的个体-车道互动进行建模。聚合后,特征向量包含与中心个体相关的丰富信息。由于空间和时态维度的因式分解,计算复杂度从 O((NT + L)^2) 降低至 O(NT^2 + TN^2 + NL),然后通过限制 k < N 和 l < L 的局部区域的半径,进一步降低到 O(NT^2 + TNk + Nl)。
尽管局部编码器提取了丰富的表述,其信息量仍受局部区域的限制。为了防止预测品质下降,作者引入了一个全局互动模块,其可补偿受限的局部收视域,并凭借局部区域之间的消息传递来捕获场景级动态。这个全局互动模块的计算成本为 O(N^2),显著提升了模型的表达能力,与局部编码器相比,它相对较轻量。
多个体运动预测问题表现出平移对称性和旋转对称性。现有方法相对于每位个体重新归一化所有矢量化元素,并为每位个体进行多次预测,从而达成不变性。该范例随个体数量线性伸缩。对比之下,HiVT 可在单次前向通验中预测所有个体的运动,同时凭借不变场景表述,和旋转稳健空间学习模块来维护不变性。
个体-个体互动模块在每个时间步捕获每个局部区域中的中心个体和相邻个体之间的关系。为了利用问题的对称性,作者提出了一个旋转不变的交叉注意力模块,它聚合了空间信息。具体来说,他们使用中心个体 pT,i — pT-1,i 的最终轨迹段作为局部区域的参考向量,并根据参考定向 ʘi 旋转所有局部向量。旋转后的向量,及其相关语义属性由多层感知器(MLP)处理,从而获得中心个体 zti 和任意相邻个体 ztij 在任意时间步 t 的嵌入。
由于所有几何属性在投喂到 MLP 之前都相对于中心个体进行了归一化,故这些嵌入是旋转不变的。除了轨迹段之外,输入函数 фnbr(•) 还包括相邻个体关于中心个体的相对位置向量,从而令相邻嵌入具备空间感知能力。然后,中心个体的嵌入向量被转换为 查询 向量,而相邻嵌入向量则用于计算 键 和 数值 实体。生成的实体将在注意力模块用到。
与经典的变换器不同,HiVT 作者提出了一种特征融合函数,它将环境特征与中心个体的特征 zti 集成到一起。这令注意力模块能够更好地控制特征更新。类似于原始的变换器架构,所提议注意力模块能够扩展到多个注意力头。多头注意力模块的输出通过 MLP 模块通验,得到个体 i 在时间步 t 处的空间嵌入 sti。
此外,该方法作者在每个模块之前按层进行数据归一化,并在每个模块之后使用残差级联。在实践中,能够用跨越所有局部区域和时间步长的高效并行学习运算来实现该模块。
已实现利用时态变换器编码器进一步捕获每个局部区域的时态信息,该编码器遵循个体-个体互动模块。对于任意中心个体 i,该模块的初始序列由嵌入 sti 组成,接受来自个体-个体互动模块不同时间步的信息。该方法的作者将额外的可训练令牌 sT+1 添加到原始序列的末尾。然后,他们将可学习的定位编码添加到所有令牌中,并将令牌放置于矩阵 Si 当中,其会被投喂到时态注意力模块。
时态学习模块还由多头注意力和 MLP 模块交替组成。
地图的局部结构可以指示中心个体的未来意向。因此,局部地图信息将添加到中心个体的嵌入之中。为此而动,该方法首先旋转局部路段,和个体当前时间步 T 所在道路的相对位置向量。然后经 MLP 对旋转向量进行编码。使用中心个体的时空特征作为查询,使用 MLP 编码的路段特征作为键-值向量,交叉注意力个体-道路的实现方式与上述方式类似。
该方法作者还应用了一个 MLP 模块来获得中心个体 i 的最终局部嵌入hi。在按顺序对个体-个体互动、时态依赖关系、和个体-道路互动建模后,嵌入封装了与局部区域的中心个体相关的丰富信息。
在 HiVT 算法的下一阶段,在全局互动模块中处理局部嵌入,从而捕获场景中的大范围依赖关系。由于局部特征是从以个体为中心的坐标系中提取的,故在跨局部区域交换信息时,全局互动模块必须考虑不同框图之间的几何关系。为达成这一点,作者扩展了变换器编码器,以便合并局部坐标系之间的差异。当信息从个体 j 传送到个体 i 时,作者使用 MLP 获得成对嵌入,然后将其包含在向量变换之中。
为了针对全局互动进行配对建模,应用了与局部编码器相同的空间注意力机制,然后是一个 MLP 模块,该模块为每位个体输出全局表示。
预测出行个体的运动本质上是多模态的。因此,作者提议参数化未来轨迹的分布,作为混合模型,其中每个分量都服从拉普拉斯(Laplace)分布。在单次前向通验中为所有个体生成预测。对于每个组件 f 的每个代理 i,MLP 将本地和全局表示作为输入。然后,它会输出个体的位置,及其在局部坐标系中每个未来时间步的相关不确定性。回归头的输出张量具有维度 [F, N, H, 4],其中 F 是混合分量的数量,N 是场景中的个体数量,H 是未来时间步长的预测横向范围。此处还用到了一个 MLP。它遵循一个 SoftMax 函数,其判定每位个体的混合模型系数。
作者的 HiVT 方法可视化如下所示。
2. 利用 MQL5 实现
我们已经审阅了 HiVT 作者提出的综合算法。我们现在转向实践层面,利用 MQL5 实现我们对这些方法的解释。
重点要注意,HiVT 作者所提议方式与我们之前采用的机制有很大不同。如是结果,我们即将承担大量的工作。
2.1初始状态的矢量化
我们首先规划状态矢量化过程。当然,我们之前已经探索了各种状态矢量化算法,包括分片线性时间序列表示、数据分段、和不同的嵌入技术。然而,在这种情况下,作者提出了一种完全不同的方式。我们将在 OpenCL 端的 HiVTPrepare 内核里实现它。
__kernel void HiVTPrepare(__global const float *data, __global float2 *output ) { const size_t t = get_global_id(0); const size_t v = get_global_id(1); const size_t total_v = get_global_size(1);
在内核参数中,我们仅用两个指向全局数据缓冲区的指针:一个对应输入值,另一个对应运算结果。
重点要注意,不同于输入数据,我们为结果缓冲区采用向量类型 float2。以前,我们针对复数值用过这种类型。不过,在本例中,我们未用到复数数学。代之,选择该数据类型是由处理二维空间中的场景旋转需求所驱动。使用两元素向量允许便利地存储平面内的坐标和位移。
您或许已注意到,内核参数并未显式包含定义输入和输出张量维度的常量。我们计划从二维任务空间获取该信息。第一个维度指示所分析历史的深度,而第二个维度指定正在处理的多模态序列中单变量时间序列的数量。
这种方式基于以下假设:我们的多模态序列由一维单变量时间序列的集合组成。
在内核主体中,我们标识跨越所有任务空间维度的当前线程。然后,我们判定全局数据缓冲区内的对应偏移常数。
const int shift_data = t * total_v; const int shift_out = shift_data * total_v;
为了阐明结果缓存区中的偏移量,值得说一点我们计划在该内核中实现的算法。
如理论部分所述,HiVT 方法的作者提议采用相对值取代绝对值,并围绕中心个体旋转场景。
按照这个逻辑,我们首先判定每位个体在给定时间步的偏差。
float value = data[shift_data + v + total_v] - data[shift_data + v];
接下来,我们计算所得位移的倾角度数。当然了,判定平面中的倾角需要两个位移坐标。不过,输入数据仅包含单一数值。鉴于我们正在与时间序列打交道,故我们能够假设沿时间轴的单位步长来推导出第二个位移坐标。也就是,我们取 “1” 作为单步沿时间轴的位移。
const float theta = atan(value);
现在我们可以检测角度的正弦和余弦,以此构造旋转矩阵。
const float cos_theta = cos(theta); const float sin_theta = sin(theta);
之后,我们就能旋转中心个体的移动矢量。
const float2 main = Rotate(value, cos_theta, sin_theta);
由于我们需要针对所有个体执行旋转,故我将该运算移到单独的函数之中。
注意,作为旋转的结果,我们得到了沿 2 个坐标轴的位移。为了存储数据,我们所用的向量类型是 float2。
接下来,我们运行一个循环,遍历给定时间步的所有个体。
for(int a = 0; a < total_v; a++) { float2 o = main; if(a != v) o -= Rotate(data[shift_data + a + total_v] - data[shift_data + a], cos_theta, sin_theta); output[shift_out + a] = o; } }
在遍历中心个体的循环主体中,我们保存其运动,对于其他个体,我们计算他们相对于中心个体的运动。为此,我们首先判定每位个体的顺位。我们根据中心个体的旋转矩阵对其进行旋转。我们从中心个体的运动矢量中减去得到的位移。
因此,对于每位个体,在每个时间步长处,我们获得 2 列(平面上的坐标)的场景描述张量,行数等于所分析单变量序列的数量。
此处值得一提的是,该方法作者将个体数量限制在局部区段的半径。我们并未这样做,因为指标值的背离通常会给出相当不错的交易信号。
2.2单一时间步内的注意力
在实现所提议方式的过程中,我们面临的下一个问题是在单一时间步内组织个体之间的注意力机制。
我们之前曾在单变量中实现了注意力机制。但这是一个“垂直”分析。在这种情况下,我们需要一个“横向”分析。当然,我们可以通过创建一个新的“横向注意力”类来解决这个问题,但这是一种相当费劲的方法。
有一个更快的解决方案。我们可转置原始数据,并用现有的 “垂直注意力” 解决方案。诚然,会存在细微差别。在这种情况下,现有的转置二维矩阵的算法并不合适。因此,我们将创建一个转置三维张量的算法。在这个转置过程中,我们把 第一和第二维度互换,而第三维度保持不变。
这正是我们使用现有的 “垂直注意力” 算法所需要的。
为了规划该过程,我们将创建一个 TransposeRCD 内核。
__kernel void TransposeRCD(__global const float *matrix_in, ///<[in] Input matrix __global float *matrix_out ///<[out] Output matrix ) { const int r = get_global_id(0); const int c = get_global_id(1); const int d = get_global_id(2); const int rows = get_global_size(0); const int cols = get_global_size(1); const int dimension = get_global_size(2); //--- matrix_out[(c * rows + r)*dimension + d] = matrix_in[(r * cols + c) * dimension + d]; }
我必须说,内核算法几乎完全重复了类似的转置二维矩阵内核。仅多加了一个任务空间维度。相应地,数据缓冲区中的偏移量会根据所加维度进行调整。
CNeuronTransposeRCDOCL 类结构也是如此。此处我们使用二维矩阵转置类 CNeuronTransposeOCL 作为父本。
class CNeuronTransposeRCDOCL : public CNeuronTransposeOCL { protected: virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronTransposeRCDOCL(void){}; ~CNeuronTransposeRCDOCL(void){}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint count, uint window, uint dimension, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) const { return defNeuronTransposeRCDOCL; } };
注意,我们不会在类主体中声明任何其它变量或对象。为了实现该过程,继承的这些对我们来说已经足够了。这允许我们仅覆盖内核调用方法,而所有其它功能都由父类的方法覆盖。因此,我们不会详研类方法的算法。我建议您自行验证它们。该类及其所有方法的完整代码都包含在附件当中。
2.3个体-个体注意力模块
接下来,我们转到实现个体-个体注意力模块。在这个模块的框架内,假设在一个时间步内构建个体局部嵌入之间的注意力。上面创建的三维张量转置类大大简化了我们的工作。不过,使用笔者提出的特征统一控制机制方法需要对算法进行调整。
为了规划指定注意力模块的进程,我们将创建一个新类 CNeuronHiVTAAEncoder。在这种情况下,我们将使用自变量注意力层 CNeuronMVMHAttentionMLKV 作为父类。
class CNeuronHiVTAAEncoder : public CNeuronMVMHAttentionMLKV { protected: virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; //--- virtual bool calcInputGradients(CNeuronBaseOCL *prevLayer) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override; public: CNeuronHiVTAAEncoder(void){}; ~CNeuronHiVTAAEncoder(void){}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint heads_kv, uint units_count, uint layers, uint layers_to_one_kv, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual int Type(void) const { return defNeuronMVMHAttentionMLKV; } };
如您所见,我们不会在该类的结构中声明额外的变量或对象。父类结构绰绰有余。CNeuronMVMHAttentionMLKV 类使用数据缓冲区的动态集合,反过来这些数据缓冲区又由类方法进行操作。我们能够向现有集合添加所需数量的数据缓冲区。
类对象的新实例在 Init 方法中实现初始化。
bool CNeuronHiVTAAEncoder::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint window, uint window_key, uint heads, uint heads_kv, uint units_count, uint layers, uint layers_to_one_kv, uint variables, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count * variables, optimization_type, batch)) return false;
在方法参数中,我们接收主要常量,这令我们能够准确判定用户指定对象的架构。在方法主体中,我们调用神经层基类的相同方法。
注意,我们调用的是基类方法,而非直接父类的方法。这是因为我们后面还需要重新定义一些数据缓存区。
父类方法成功执行之后,我们将从外部程序接收到对象架构定义的常量,并保存到内部变量之中。
iWindow = fmax(window, 1); iWindowKey = fmax(window_key, 1); iUnits = fmax(units_count, 1); iHeads = fmax(heads, 1); iLayers = fmax(layers, 1); iHeadsKV = fmax(heads_kv, 1); iLayersToOneKV = fmax(layers_to_one_kv, 1); iVariables = variables;
接下来,我们立即计算常数,判定内部对象大小。
uint num_q = iWindowKey * iHeads * iUnits * iVariables; //Size of Q tensor uint num_kv = iWindowKey * iHeadsKV * iUnits * iVariables; //Size of KV tensor uint q_weights = (iWindow * iHeads + 1) * iWindowKey; //Size of weights' matrix of Q tenzor uint kv_weights = (iWindow * iHeadsKV + 1) * iWindowKey; //Size of weights' matrix of KV tenzor uint scores = iUnits * iUnits * iHeads * iVariables; //Size of Score tensor uint mh_out = iWindowKey * iHeads * iUnits * iVariables; //Size of multi-heads self-attention uint out = iWindow * iUnits * iVariables; //Size of attention out tensore uint w0 = (iWindowKey * iHeads + 1) * iWindow; //Size W0 weights matrix uint gate = (2 * iWindow + 1) * iWindow; //Size of weights' matrix gate layer uint self = (iWindow + 1) * iWindow; //Size of weights' matrix self layer
该算法基本上是从父类继承而来,仅略微进行了一些编辑。
完成准备工作之后,我们创建一个循环,迭代次数等于指定嵌套层数量。在这个循环主体中,在每次迭代时,我们都会创建执行每个嵌套层功能所需的对象。
for(uint i = 0; i < iLayers; i++) { CBufferFloat *temp = NULL; for(int d = 0; d < 2; d++) { //--- Initilize Q 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;
在此,我们首先为中间数据和单独模块的结果创建缓冲区,并记录相应的误差梯度。
注意,数据缓冲区和相应的误差梯度具有相同的大小。因此,为了减少人力,我们将创建一个 2 层迭代的嵌套循环。在循环的第一层迭代中,我们创建数据缓冲区,在第二层迭代期间,我们创建相应误差梯度的缓冲区。
首先,我们创建一个缓冲区以将查询实体写入其中。然后是键和数值缓冲区。
//--- Initilize KV tensor if(i % iLayersToOneKV == 0) { 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(!K_Tensors.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(!V_Tensors.Add(temp)) return false; temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(2 * num_kv, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!KV_Tensors.Add(temp)) return false; }
创建和初始化数据缓冲区的算法完全雷同。仅有的区别是,我们的算法提供了多个嵌套层共用一个“键-值”张量的能力。因此,在创建缓冲区之前,我们检查当前层上该动作的必要性。
接下来,我们初始化对象之间的依赖系数缓冲区。
//--- 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;
根据多头自注意力算法,使用投影层将多头注意力的结果压缩到原始数据级别。我们创建一个缓冲区来保存生成的投影。
//--- 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;
到目前为止讲述的算法几乎完全重复了父类的方法。但接踵而来,为实现管理特征统一机制而引入的更改。在此,根据提议算法,我们首先必须将源数据与注意力模块的结果级联起来。
//--- Initialize Concatenate temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit(2 * out, 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Tensors.Add(temp)) return false;
然后,结果用于计算控制系数。
//--- Initialize Gate 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;
之后,我们对原始数据进行投影。
//--- Initialize Self 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;
在嵌套循环结束时,我们创建当前嵌套层的输出缓冲区。
//--- Initialize Out if(i == iLayers - 1) { if(!FF_Tensors.Add(d == 0 ? Output : Gradient)) return false; continue; } 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; }
于此应当注意的是,我们只为中间内部层创建输出和梯度缓存区。对于最后一个嵌套层,我们只需将指针复制到类的相应缓冲区即可。
在创建中间结果缓冲区和相应的误差梯度之后,我们初始化训练参数矩阵。我们将得到若干个。首先,它是查询实体生成矩阵。
//--- Initilize Q weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(q_weights)) return false; float k = (float)(1 / sqrt(iWindow + 1)); for(uint w = 0; w < q_weights; w++) { if(!temp.Add(GenerateWeight() * 2 * k - k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Weights.Add(temp)) return false;
此处,我们首先创建一个缓冲区,然后用随机参数填充它。这些参数将在模型训练过程中进行优化。
与此类似,我们创建“键”和“数值”实体生成参数。不过,我们不会为每个嵌套层生成矩阵。
//--- Initialize K weights if(i % iLayersToOneKV == 0) { temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(kv_weights)) return false; for(uint w = 0; w < kv_weights; w++) { if(!temp.Add(GenerateWeight() * 2 * k - k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!K_Weights.Add(temp)) return false; //--- temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(kv_weights)) return false; for(uint w = 0; w < kv_weights; w++) { if(!temp.Add(GenerateWeight() * 2 * k - k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!V_Weights.Add(temp)) return false; }
此外,我们还需要一个多头注意力结果的投影矩阵。
//--- Initialize 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() * 2 * k - k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false;
在此,我们还为特征合并控制模块添加了参数。
//--- Initialize Gate Weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(gate)) return false; k = (float)(1 / sqrt(2 * iWindow + 1)); for(uint w = 0; w < gate; w++) { if(!temp.Add(GenerateWeight() * 2 * k - k)) return false; } if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false;
以及源数据的投影。
//--- Self temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.Reserve(self)) return false; k = (float)(1 / sqrt(iWindow + 1)); for(uint w = 0; w < self; w++) { if(!temp.Add(GenerateWeight() * 2 * k - 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++) { temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit((d == 0 || optimization == ADAM ? q_weights : iWindowKey * iHeads), 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!QKV_Weights.Add(temp)) return false; if(i % iLayersToOneKV == 0) { temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit((d == 0 || optimization == ADAM ? kv_weights : iWindowKey * iHeadsKV), 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!K_Weights.Add(temp)) return false; //--- temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit((d == 0 || optimization == ADAM ? kv_weights : iWindowKey * iHeadsKV), 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!V_Weights.Add(temp)) return false; } temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit((d == 0 || optimization == ADAM ? w0 : iWindow), 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false; //--- Initilize Gate Momentum temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit((d == 0 || optimization == ADAM ? gate : 2 * iWindow), 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false; //--- Initilize Self Momentum temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) return false; if(!temp.BufferInit((d == 0 || optimization == ADAM ? self : iWindow), 0)) return false; if(!temp.BufferCreate(OpenCL)) return false; if(!FF_Weights.Add(temp)) return false; } }
初始化嵌套层对象成功之后,我们创建一个额外的缓冲区,记录临时中间结果。
if(!Temp.BufferInit(MathMax(2 * num_kv, out), 0)) return false; if(!Temp.BufferCreate(OpenCL)) return false; //--- return true; }
完成方法执行。之后,我们将方法作的布尔结果返回给调用程序。
初始化对象之后,下一步是构造前馈通验算法,该算法在 feedForward 方法中实现。
bool CNeuronHiVTAAEncoder::feedForward(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(NeuronOCL) == POINTER_INVALID) return false;
在该方法参数中,我们接收一个指向包含初始数据的对象指针,并立即检查接收到的指针的相关性。该控制成功完成之后,我们将运行一个循环,在其中实现每个嵌套层操作的顺序执行。
CBufferFloat *kv = NULL; for(uint i = 0; (i < iLayers && !IsStopped()); i++) { //--- Calculate Queries, Keys, Values CBufferFloat *inputs = (i == 0 ? NeuronOCL.getOutput() : FF_Tensors.At(10 * i - 6)); CBufferFloat *q = QKV_Tensors.At(i * 2); if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? 2 : 3)), inputs, q, iWindow, iWindowKey * iHeads, None)) return false;
首先,我们生成查询实体。然后,如有必要,我们形成一个“键-值”张量。
if((i % iLayersToOneKV) == 0) { uint i_kv = i / iLayersToOneKV; kv = KV_Tensors.At(i_kv * 2); CBufferFloat *k = K_Tensors.At(i_kv * 2); CBufferFloat *v = V_Tensors.At(i_kv * 2); if(IsStopped() || !ConvolutionForward(K_Weights.At(i_kv * (optimization == SGD ? 2 : 3)), inputs, k, iWindow, iWindowKey * iHeadsKV, None)) return false; if(IsStopped() || !ConvolutionForward(V_Weights.At(i_kv * (optimization == SGD ? 2 : 3)), inputs, v, iWindow, iWindowKey * iHeadsKV, None)) return false; if(IsStopped() || !Concat(k, v, kv, iWindowKey * iHeadsKV * iVariables, iWindowKey * iHeadsKV * iVariables, iUnits)) return false; }
在形成所需实体的张量后,我们能够计算多头注意力的结果。
//--- Score calculation and Multi-heads attention calculation CBufferFloat *temp = S_Tensors.At(i * 2); CBufferFloat *out = AO_Tensors.At(i * 2); if(IsStopped() || !AttentionOut(q, kv, temp, out)) return false;
然后我们将它们压缩至初始数据的维度。
//--- Attention out calculation temp = FF_Tensors.At(i * 10); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 6 : 9)), out, temp, iWindowKey * iHeads, iWindow, None)) return false;
为了计算控制系数,我们首先将注意力模块的结果和初始数据级联起来。
//--- Concat out = FF_Tensors.At(i * 10 + 1); if(IsStopped() || !Concat(temp, inputs, out, iWindow, iWindow, iUnits)) return false;
然后我们计算控制系数。
//--- Gate if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? 6 : 9) + 1), out, FF_Tensors.At(i * 10 + 2), 2 * iWindow, iWindow, SIGMOID)) return false;
然后我们只需要对原始输入进行投影。
//--- Self if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ?inputs FF_Tensors.At(i * 10 + 3), iWindow, iWindow, None)) return false;
之后,我们将获得的投影与注意力模块的结果相结合,同时参考控制系数。
//--- Out if(IsStopped() || !GateElementMult(FF_Tensors.At(i * 10 + 3), temp, FF_Tensors.At(i * 10 + 2), FF_Tensors.At(i * 10 + 4))) return false; } //--- return true; }
之后,我们转到在循环的新迭代中处置下一个嵌套层。
模块中所有嵌套层的操作成功完成之后,我们完结方法执行,并将逻辑结果返回给调用方,指示操作的完成状态。
我们实现前馈算法的工作至此就完结了。我建议您自行打造反向传播方法的算法。您可在附件中找到所有类及其方法的完整代码,以及准备本文时用到的所有程序。
结束语
在本文中,我们探讨了一种相当有趣、且有前途的层次化矢量变换器(HiVT)方法,提出该方法是要预测多位个体的运动。该方法将问题分解为局部上下文提取和全局互动的各个阶段建模,提供了一种解决预测问题的有效方式。
该方法作者采取了综合的方式来解决问题,并提出了多种方式来提高所提议模型的有效性。不幸的是,实现所提议方式的工作量超出了文章的格式。故此,这部分只涵盖了准备工作。启动工作将在下一篇文章中完成。据真实历史数据上测试所提议方式的结果也将在第二部分阐述。
参考
文章中所用程序
# | 发行 | 类型 | 说明 |
---|---|---|---|
1 | Research.mq5 | EA | 样本收集 EA |
2 | ResearchRealORL.mq5 | EA | 利用 Real ORL方法收集样本的 EA |
3 | Study.mq5 | EA | 模型训练 EA |
4 | StudyEncoder.mq5 | EA | 编码器训练 EA |
5 | Test.mq5 | EA | 模型测试 EA |
6 | Trajectory.mqh | 类库 | 系统状态描述结构 |
7 | NeuroNet.mqh | 类库 | 创建神经网络的类库 |
8 | NeuroNet.cl | 代码库 | OpenCL 程序代码库 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/15688



