
交易中的神经网络:双曲型潜在扩散模型(HypDiff)
概述
图形包含原料数据拓扑结构的多样性和显要性。这些拓扑特征往往反映出底层物理原理和发展形态。传统的基于经典图论的随机图形模型严重依赖人工启发来设计特定拓扑结构的算法,并缺乏有效建模多样化和复杂图形结构的灵活性。为了解决这些局限,已开发出许多生成图形的深度学习模型。具备去噪能力的概率扩散模型展现出强大的性能和潜力,特别是在可视化任务当中。
然而,由于图形结构的不规则性、以及非欧几里德(Euclidean)性质,在这种境况下应用扩散模型存在两个主要限制:
- 计算复杂度高。图形生成本质上涉及处理离散、稀疏和其它非欧几里德拓扑特征。普通扩散模型中所用的高斯噪声扰动不太适用离散数据。如是结果,由于结构稀疏性,离散图形扩散模型典型情况下展现出高度时空复杂性。甚至,该类模型依赖于连续的高斯噪声过程来生成完全连接的噪声图形,这往往会导致结构信息、及其背后的拓扑特性丢失。
- 非欧几里德结构的各向异性。不同于规则结构数据,非欧几里德空间中图形节点的嵌入,在连续潜在空间内是各向异性的。当节点嵌入映射到欧几里德空间时,它们沿特定方向表现出明显的各向异性。潜在空间中的各向同性扩散过程,倾向于将这种各向异性结构信息视为噪声,导致其在去噪阶段丢失。
双曲几何空间作为表征离散树状、或分层结构的理想连续流形,已被广泛接收,并在各种图形学习任务里运用。论文《图形生成的双曲几何潜在扩散模型》的作者声称,双曲几何在解决图形潜在扩散过程中的非欧几里德结构各向异性问题方面具有巨大潜力。在双曲空间中,节点嵌入的分布往往是全局各向同性的。同时,各向异性在局部也有所保留。甚至,双曲几何统一了极坐标中的角度和径向测量,提供了具有物理语义、和可解释性的几何维度。值得注意的是,双曲几何可为潜在空间提供反映图形内在结构的几何先验。
基于这些洞察,作者旨在设计一个基于双曲几何的合适潜在空间,以实现在非欧几里德结构上进行高效的扩散过程,以便生成图形,同时保留拓扑完整性。如此行事,他们试图解决两个核心问题:
- 连续高斯分布的可加性在双曲潜在空间中未定义。
- 开发针对非欧几里德结构的有效各向异性扩散过程。
为了克服这些问题,作者提出了一种双曲潜在扩散模型(HypDiff)。对于双曲空间中的高斯分布可加性问题,引入了一种基于径向量值的扩散过程。此外,应用角度约束来抑制各向异性噪声,从而保留结构先验,并引导扩散模型在图形中获得更精细的结构细节。
1. HypDiff 算法
双曲潜在扩散模型(HypDiff)解决了图形生成中的两个关键挑战。它利用双曲几何来抽象图形节点的隐式层次化结构,并引入两个几何约束来保留基本拓扑属性。作者采用两阶段训练策略。首先,他们训练一个双曲自动编码器,来获得预训练的节点嵌入,其二,他们训练一个双曲几何潜在扩散过程。
初始步骤是将图形数据 𝒢 = (𝐗, A) 嵌入到低维双曲空间之中,从而改善图形的潜在扩散过程。
所提议双曲自动编码器包括双曲几何编码器,和费米-狄拉克(Fermi-Dirac)解码器。编码器将图形 𝒢 = (𝐗, A) 映射到双曲几何空间中,以便获得相应的双曲表示,而费米-狄拉克解码器则将重造表征,返回到图形数据域。双曲流形 ℍᵈ Hd,及其切线空间 𝒯x 可通过指数和对数映射进行相互转换。多层感知器(MLP)、或图形神经网络(GNN),可用来针对这些指数/对数表征进行操作。在他们的实现中,作者使用双曲图卷积网络(HGCNs)作为双曲几何编码器。
由于高斯分布可加性在双曲空间中失败,传统的黎曼(Riemannian)正态分布、或包裹正态分布不能直接应用。作者提议使用多个流形的乘积空间,取代直接在双曲空间中扩散嵌入。 为了解决这个问题,HypDiff 的作者引入了一种在双曲空间中的新扩散过程。至于计算效率,双曲空间的高斯分布近似于切平面 𝒯μ 的高斯分布。
与支持线性加法的欧几里德空间不同,双曲空间使用莫比乌斯(Möbius)加法。这为基于流形的扩散带来了挑战。甚至,各向同性噪声会迅速降低信噪比,从而难以保存拓扑信息。
潜在空间中的图形各向异性本质上携带关于图形结构的归纳偏差。一个核心问题是判定这种各向异性的主导方向。为了解决这个问题,HypDiff 方法的作者提出了一个双曲各向异性扩散框架。此处的核心思想是基于相似度的节点聚类选择一个主要扩散方向(即角度)。这可有效地将双曲潜在空间分割为多个扇区。然后,每个聚类的节点被投影到其质心的切平面上,以便扩散。
在预处理期间,这些聚类可运用任何基于相似性的聚类算法形成。
双曲聚类参数 k ∈ [1, n] 定义了划分双曲空间的扇区数量。双曲各向异性扩散相当于克莱因(Klein)模型 𝕂c,n 内具有多个曲率 Ci ∈|k|,近似为对簇质心 Oi∈{|k|} 处切平面集 𝒯𝐨i∈{|k|} 的投影。
该属性在 HypDiff 作者的近似算法和多曲率克莱因模型之间建立了优雅联系。
所提议算法的行为基于 k 的数值而变化。这令双曲几何中的各向异性能够更灵活、更细粒度地表征,其强化了噪声注入和模型训练期间的准确性和效率。
双曲几何可以自然地、几何化描述图形成长过程中的节点连通性。节点的受欢迎程度可通过其径向坐标来抽象,而相似度能通过双曲空间中的角距离来表达。
主要意图是为拥有几何径向生长的扩散模拟,与双曲空间的内在特性保持一致。
标准扩散模型在图形上表现不佳的根本原因是信噪比的快速下降。在 HypDiff 中,以每个聚集中心到北极 O 的测地线方向作为目标扩散方向,在几何约束下引导前向扩散过程。
遵循标准的去噪、和反向扩散建模程序,HypDiff 的作者采用基于 UNet 的去噪扩散模型(DDM)来训练 X0 的预测。
此外,HypDiff 的作者验证,采样可在单一切线空间中联合执行,而非跨聚集中心的多个切线空间,从而提升效率。
作者展示的 HypDiff 框架的可视化如下。
2. 利用 MQL5 实现
在回顾了 HypDiff 方法的理论层面之后,我们现在转到本文的实践部分,我们将利用 MQL5 实现所提议方法的解释。值得注意的是,从开局起,实现会相当漫长、且具有挑战性。故此,请为后续的大量工作做好思想准备。
2.1扩展 OpenCL 程序
我们通过修改现有的 OpenCL 程序开始实际实现。第一步涉及将输入数据投影到双曲空间当中。在该变换过程中,考虑序列中元素的每个位置至关重要,在于双曲空间将欧几里德空间参数、与时态层面结合到一起。按照原始方法,我们应用洛伦兹(Lorentz)模型。该投影在 HyperProjection 内核中实现。
__kernel void HyperProjection(__global const float *inputs, __global float *outputs ) { const size_t pos = get_global_id(0); const size_t d = get_local_id(1); const size_t total = get_global_size(0); const size_t dimension = get_local_size(1);
内核将接收指向数据缓冲区的指针作为参数:分析下的序列和变换结果。这些数据缓冲区的特征将经由工作负载空间进行定义。第一个维度对应于序列的长度,而第二个维度指定描述序列中每个单独元素特征向量的大小。工作项将基于最终维度分组到工作组之中。
注意,每个序列元素的特征向量将包含一个附加分量。
接下来,我们声明一个局部数组,于工作组内线程之间数据交换时所用。
__local float temp[LOCAL_ARRAY_SIZE]; const int ls = min((int)dimension, (int)LOCAL_ARRAY_SIZE);
我们定义数据在缓冲区中的偏移常量。
const int shift_in = pos * dimension + d; const int shift_out = pos * (dimension + 1) + d + 1;
我们将全局缓冲区的输入数据加载到相应工作流的局部元素之中,并计算二次值。我们还应当确保检查操作的执行结果。
float v = inputs[shift_in]; if(isinf(v) || isnan(v)) v = 0; //--- float v2 = v * v; if(isinf(v2) || isnan(v2)) v2 = 0;
接下来,我们需要计算输入数据向量的范数。为此,我们用局部数组来累加其平方值。这是因为每个工作组线程包含一个元素。
//--- if(d < ls) temp[d] = v2; barrier(CLK_LOCAL_MEM_FENCE); for(int i = ls; i < (int)dimension; i += ls) { if(d >= i && d < (i + ls)) temp[d % ls] += v2; barrier(CLK_LOCAL_MEM_FENCE); } //--- int count = min(ls, (int)dimension); //--- do { count = (count + 1) / 2; if(d < count) temp[d] += ((d + count) < dimension ? temp[d + count] : 0); if(d + count < dimension) temp[d + count] = 0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
此处应当注意,我们需要向量范数只是计算向量中第一个元素的值,其描述被分析序列元素的双曲坐标。我们在不更改的情况下移动所有其它元素,但位置会偏移。
outputs[shift_out] = v;
为了避免额外操作,我们仅在每个工作组的第一个线程中判定双曲向量的第一个元素值。
此处我们首先计算原始序列中所分析元素的偏移比例。之后我们减去上面计算初始表征向量时获得的范数值平方。最后,我们计算所获数值的平方根。
if(d == 0) { v = ((float)pos) / ((float)total); if(isinf(v) || isnan(v)) v = 0; outputs[shift_out - 1] = sqrt(fmax(temp[0] - v * v, 1.2e-07f)); } }
注意,在提取平方根时,我们显式确保仅取用大于零的值。这样就消除了计算期间运行时错误和无效结果的风险。
为了实现反向传播算法,我们将立即创建 HyperProjectionGrad 内核,其经由先前定义的前馈操作实现误差梯度传播。请注意以下两点。首先,元素在序列中的位置是静态的、且非参数化。这意味着不可传播梯度给它。
其二,剩余元素的梯度经由两个单独的信息线程传播。一是直接梯度传播。同时,原始特征向量的所有分量都用于计算向量范数,进而判定双曲表征的第一个元素。因此,每个特征必须从双曲向量的第一个元素中获得误差梯度的比例份额。
现在我们来检查一下这些方法是如何在代码中实现的。HyperProjectionGrad 内核取 3 个数据缓冲区指针作为参数。引入了新的输入梯度缓冲区(inputs_gr)。包含原始序列的双曲表征的缓冲区被其相应的误差梯度缓冲区(outputs_gr)替代。
__kernel void HyperProjectionGrad(__global const float *inputs, __global float *inputs_gr, __global const float *outputs_gr ) { const size_t pos = get_global_id(0); const size_t d = get_global_id(1); const size_t total = get_global_size(0); const size_t dimension = get_global_size(1);
我们保留内核任务空间等于前馈通验,但我们不再将线程组合到工作组之中。在内核主体中,我们首先识别任务空间中的当前线程。基于获得的数值,我们判定数据缓冲区中的偏移量。
const int shift_in = pos * dimension + d; const int shift_start_out = pos * (dimension + 1); const int shift_out = shift_start_out + d + 1;
从全局缓冲区加载数据的模块中,我们计算来自原始表征的所分析元素值,及其在双曲表示级别的误差梯度。
float v = inputs[shift_in]; if(isinf(v) || isnan(v)) v = 0; float grad = outputs_gr[shift_out]; if(isinf(grad) || isnan(grad)) grad = 0;
然后,我们从双曲表征的第一个元素判定误差梯度的分数,其被定义为误差梯度与所分析元素的输入值的乘积。
v = v * outputs_gr[shift_start_out]; if(isinf(v) || isnan(v)) v = 0;
另外,不要忘记控制每个阶段的过程。
我们将总误差梯度保存在相应的全局数据缓冲区当中。
//---
inputs_gr[shift_in] = v + grad;
}
在该阶段,我们已实现了将输入数据投影到双曲空间之中。然而,HypDiff 方法的作者建议在双曲空间投影到切平面上运作扩散过程。
初看,将数据从平面空间投影到双曲空间,然后再投影回来只是为了引入噪声,这看似很奇怪。然而,关键点是原始平面表征可能与最终投影有很大差异。因为原始数据平面和用于投影双曲表征的切平面不是同一平面。
这个概念可比作依据照片起草技术图纸。首先,基于先验知识和经验,我们在脑海中重造了照片中所描绘物体的三维表征。然后,我们将该心理图像转化为具有侧视图、正面视图、和俯视图的二维技术图。类似地,HypDiff 将数据投影到多个切平面上,每个切平面都以双曲空间中的不同点为中心。
为了实现该功能,我们将创建 LogMap 内核。该内核接受七个数据缓冲区指针作为参数,诚然,这相当多。其中包括三个输入数据缓冲区:
- features 缓冲区包含表征输入数据的双曲嵌入的张量。
- “centroids” 缓冲区保存质心的坐标。它们当作执行投影的切平面基点。
- curvatures 缓冲区定义与每个质心关联的曲率参数。
outputs 缓冲区存储投影操作的结果。更多的三个缓冲区存储中间结果,其会在反向传播通验计算期间所用。
此处应当注意,我们在实现中略微偏离了原版的框架。在原版 HypDiff 方法中,作者在数据预处理阶段对序列元素进行了预聚类。他们仅把每个组的成员投影到切平面上。然而,在我们的方式中,我们选择不对序列元素进行预分组。取而代之,我们将每个元素投影到每个切平面上。自然,这将增加操作数量。但另一方面,它将丰富模型对于所分析序列的理解。
__kernel void LogMap(__global const float *features, __global const float *centroids, __global const float *curvatures, __global float *outputs, __global float *product, __global float *distance, __global float *norma ) { //--- identify const size_t f = get_global_id(0); const size_t cent = get_global_id(1); const size_t d = get_local_id(2); const size_t total_f = get_global_size(0); const size_t total_cent = get_global_size(1); const size_t dimension = get_local_size(2);
在方法主体中,我们辨别三维任务空间中的当前操作线程。第一个维度指向原始序列的元素。第二个指向质心。第三个指向所分析序列元素的描述向量中的位置。在这种情况下,我们根据最后一个维度将线程组合到工作组当中。
接下来,我们在工作组中声明一个局部数据交换数组。
//--- create local array __local float temp[LOCAL_ARRAY_SIZE]; const int ls = min((int)dimension, (int)LOCAL_ARRAY_SIZE);
我们定义数据在缓冲区中的偏移常量。
//--- calc shifts const int shift_f = f * dimension + d; const int shift_out = (f * total_cent + cent) * dimension + d; const int shift_cent = cent * dimension + d; const int shift_temporal = f * total_cent + cent;
之后,我们从全局缓冲区加载输入数据,并验证所获得数值的有效性。
//--- load inputs float feature = features[shift_f]; if(isinf(feature) || isnan(feature)) feature = 0; float centroid = centroids[shift_cent]; if(isinf(centroid) || isnan(centroid)) centroid = 0; float curv = curvatures[cent]; if(isinf(curv) || isnan(curv)) curv = 1.2e-7;
接下来,我们需要计算输入数据的张量与质心的乘积。但由于我们所用的是双曲表征,故我们将采用闵可夫斯基(Minkowski)乘积。为了计算它,我们首先执行相应标量数值的乘法。
//--- dot(features, centroids) float fc = feature * centroid; if(isnan(fc) || isinf(fc)) fc = 0;
然后我们将工作组内获得的数值相加。
//--- if(d < ls) temp[d] = (d > 0 ? fc : -fc); barrier(CLK_LOCAL_MEM_FENCE); for(int i = ls; i < (int)dimension; i += ls) { if(d >= i && d < (i + ls)) temp[d % ls] += fc; barrier(CLK_LOCAL_MEM_FENCE); } //--- int count = min(ls, (int)dimension); //--- do { count = (count + 1) / 2; if(d < count) temp[d] += ((d + count) < dimension ? temp[d + count] : 0); if(d + count < dimension) temp[d + count] = 0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1); float prod = temp[0]; if(isinf(prod) || isnan(prod)) prod = 0;
注意,与欧几里德空间中通用的向量乘法不同,我们取向量的第一个元素与逆反值的乘积。
我们检查操作结果的有效性,并将得到的数值保存在全局临时数据存储缓冲区的相应元素当中。我们在反向传播过程中将会需要该数值。
product[shift_temporal] = prod;
这令我们能够判定所分析元素从质心偏移的程度和方向。
//--- project float u = feature + prod * centroid * curv; if(isinf(u) || isnan(u)) u = 0;
我们判定所获移位向量的闵可夫斯基(Minkowski)范数。如前,我们取每个元素的平方。
//--- norm(u) float u2 = u * u; if(isinf(u2) || isnan(u2)) u2 = 0;
我们将工作组内获得的数值相加,取第一个元素的平方,符号相反。
if(d < ls) temp[d] = (d > 0 ? u2 : -u2); barrier(CLK_LOCAL_MEM_FENCE); for(int i = ls; i < (int)dimension; i += ls) { if(d >= i && d < (i + ls)) temp[d % ls] += u2; barrier(CLK_LOCAL_MEM_FENCE); } //--- count = min(ls, (int)dimension); //--- do { count = (count + 1) / 2; if(d < count) temp[d] += ((d + count) < dimension ? temp[d + count] : 0); if(d + count < dimension) temp[d + count] = 0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1); float normu = temp[0]; if(isinf(normu) || isnan(normu) || normu <= 0) normu = 1.0e-7f; normu = sqrt(normu);
再次,我们将用获得的数值作为反向传播通验的一部分。故此,我们将其保存在临时数据存储缓冲区之中。
norma[shift_temporal] = normu;
在下一步中,我们依照质心曲率参数来判定双曲空间中自所分析点到质心的距离。在这种情况下,我们不会重新计算向量的乘积,而是取之前获得的数值。
//--- distance features to centroid float theta = -prod * curv; if(isinf(theta) || isnan(theta)) theta = 0; theta = fmax(theta, 1.0f + 1.2e-07f); float dist = sqrt(clamp(pow(acosh(theta), 2.0f) / curv, 0.0f, 50.0f)); if(isinf(dist) || isnan(dist)) dist = 0;
验证所获数值的有效性,并将结果保存在全局临时数据存储缓冲区当中。
distance[shift_temporal] = dist;
我们调整偏移向量的值。
float proj_u = dist * u / normu;
然后我们只需要将获得的数值投影到切平面上。于此,与上面执行的洛伦兹投影类似,我们需要调整投影向量的第一个元素。为此,我们计算投影向量和质心向量的乘积,无需考虑第一个元素。
if(d < ls) temp[d] = (d > 0 ? proj_u * centroid : 0); barrier(CLK_LOCAL_MEM_FENCE); for(int i = ls; i < (int)dimension; i += ls) { if(d >= i && d < (i + ls)) temp[d % ls] += proj_u * centroid; barrier(CLK_LOCAL_MEM_FENCE); } //--- count = min(ls, (int)dimension); //--- do { count = (count + 1) / 2; if(d < count) temp[d] += ((d + count) < dimension ? temp[d + count] : 0); if(d + count < dimension) temp[d + count] = 0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
调整第一个投影元素的数值。
//--- if(d == 0) { proj_u = temp[0] / centroid; if(isinf(proj_u) || isnan(proj_u)) proj_u = 0; proj_u = fmax(u, 1.2e-7f); }
保存结果。
//---
outputs[shift_out] = proj_u;
}
如您所见,内核算法相当繁琐,有大量复杂的连接。这样很难理解误差梯度在反向传播过程中所采取的路径。无论如何,我们必须解开这个谜团。请更加注意细节。反向传播算法在 LogMapGrad 内核中实现。
__kernel void LogMapGrad(__global const float *features, __global float *features_gr, __global const float *centroids, __global float *centroids_gr, __global const float *curvatures, __global float *curvatures_gr, __global const float *outputs, __global const float *outputs_gr, __global const float *product, __global const float *distance, __global const float *norma ) { //--- identify const size_t f = get_local_id(0); const size_t cent = get_global_id(1); const size_t d = get_local_id(2); const size_t total_f = get_local_size(0); const size_t total_cent = get_global_size(1); const size_t dimension = get_local_size(2);
在内核参数中,我们在源和输出级别添加了误差梯度缓冲区。这有 4 个额外的数据缓冲区。
我们保留了与前馈通验类似的内核任务空间,不过,我们改变了工作组的分组原则。因为现在我们不仅要收集序列中各个元素向量中的数值,还要收集质心的梯度。每个质心都要与所分析序列的所有元素打交道。相应地,应从每一个接收误差梯度。
在内核主体中,我们辨别任务空间所有维度的操作线程。之后,我们创建一个局部数组,作为工作组元素之间交换数据所用。
//--- create local array __local float temp[LOCAL_ARRAY_SIZE]; const int ls = min((int)dimension, (int)LOCAL_ARRAY_SIZE);
我们定义在全局数据缓冲区中的偏移常量。
//--- calc shifts const int shift_f = f * dimension + d; const int shift_out = (f * total_cent + cent) * dimension + d; const int shift_cent = cent * dimension + d; const int shift_temporal = f * total_cent + cent;
之后,我们从全局缓冲区加载数据。首先,我们提取输入数据和中间值。
//--- load inputs float feature = features[shift_f]; if(isinf(feature) || isnan(feature)) feature = 0; float centroid = centroids[shift_cent]; if(isinf(centroid) || isnan(centroid)) centroid = 0; float centroid0 = (d > 0 ? centroids[shift_cent - d] : centroid); if(isinf(centroid0) || isnan(centroid0) || centroid0 == 0) centroid0 = 1.2e-7f; float curv = curvatures[cent]; if(isinf(curv) || isnan(curv)) curv = 1.2e-7; float prod = product[shift_temporal]; float dist = distance[shift_temporal]; float normu = norma[shift_temporal];
然后我们计算包含所分析序列元素与质心偏移量的向量值。与前馈操作不同,我们已拥有了所有必要的数据。
float u = feature + prod * centroid * curv; if(isinf(u) || isnan(u)) u = 0;
我们在结果级别加载现有的误差梯度。
float grad = outputs_gr[shift_out]; if(isinf(grad) || isnan(grad)) grad = 0; float grad0 = (d>0 ? outputs_gr[shift_out - d] : grad); if(isinf(grad0) || isnan(grad0)) grad0 = 0;
请注意,我们不仅加载了所分析元素的误差梯度,还加载了所分析序列元素描述向量中第一个元素的误差梯度。此处的原因与上面描述的 HyperProjectionGrad 内核类似。
接下来,我们初始化局部变量,以便累积误差梯度。
float feature_gr = 0; float centroid_gr = 0; float curv_gr = 0; float prod_gr = 0; float normu_gr = 0; float dist_gr = 0;
首先,我们把来自切平面上数据投影的误差梯度传播到偏移向量。
float proj_u_gr = (d > 0 ? grad + grad0 / centroid0 * centroid : 0);
注意,偏移向量的第一个元素对结果没有影响。因此,它的梯度为 “0”。其它元素同时获得直接误差梯度、及结果第一个元素的份额。
然后我们判定质心的误差梯度的第一个值。我们在循环中计算它们,从序列的所有元素中收集数值。
for(int id = 0; id < dimension; id += ls) { if(d >= id && d < (id + ls)) { int t = d % ls; for(int ifeat = 0; ifeat < total_f; ifeat++) { if(f == ifeat) { if(d == 0) temp[t] = (f > 0 ? temp[t] : 0) + outputs[shift_out] / centroid * grad; else temp[t] = (f > 0 ? temp[t] : 0) + grad0 / centroid0 * outputs[shift_out]; } barrier(CLK_LOCAL_MEM_FENCE); }
从局部数组内序列的所有元素收集误差梯度后,我们将用到一个线程,并将收集到的值传送至局部变量。
if(f == 0) { if(isnan(temp[t]) || isinf(temp[t])) temp[t] = 0; centroid_gr += temp[0]; } } barrier(CLK_LOCAL_MEM_FENCE); }
我们还需要确保所有操作线程的阻碍能无一例外地访问。
接下来,我们计算距离、范数、和偏移向量的误差梯度。
dist_gr = u / normu * proj_u_gr;
float u_gr = dist / normu * proj_u_gr;
normu_gr = dist * u / (normu * normu) * proj_u_gr;
请注意,偏移向量的元素在每条线程中都是单独的。但向量范数和距离是离散值。因此,我们需要汇总所分析序列每一个元素内相应的误差梯度。首先,我们收集距离的误差梯度。我们通过局部数组汇总数值。
for(int ifeat = 0; ifeat < total_f; ifeat++) { if(d < ls && f == ifeat) temp[d] = dist_gr; barrier(CLK_LOCAL_MEM_FENCE); for(int id = ls; id < (int)dimension; id += ls) { if(d >= id && d < (id + ls) && f == ifeat) temp[d % ls] += dist_gr; barrier(CLK_LOCAL_MEM_FENCE); } //--- int count = min(ls, (int)dimension); //--- do { count = (count + 1) / 2; if(f == ifeat) { if(d < count) temp[d] += ((d + count) < dimension ? temp[d + count] : 0); if(d + count < dimension) temp[d + count] = 0; } barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1); if(f == ifeat) { if(isinf(temp[0]) || isnan(temp[0])) temp[0] = 0; dist_gr = temp[0];
紧接着,我们判定相应质心的曲率参数、及向量乘积的误差梯度。
if(d == 0) { float theta = -prod * curv; float theta_gr = 1.0f / sqrt(curv * (theta * theta - 1)) * dist_gr; if(isinf(theta_gr) || isnan(theta_gr)) theta_gr = 0; curv_gr += -pow(acosh(theta), 2.0f) / (2 * sqrt(pow(curv, 3.0f))) * dist_gr; if(isinf(curv_gr) || isnan(curv_gr)) curv_gr = 0; temp[0] = -curv * theta_gr; if(isinf(temp[0]) || isnan(temp[0])) temp[0] = 0; curv_gr += -prod * theta_gr; if(isinf(curv_gr) || isnan(curv_gr)) curv_gr = 0; } } barrier(CLK_LOCAL_MEM_FENCE);
不过,请注意,累积曲率参数误差的梯度,只是为了存储在全局数据缓冲区之中。对比而言,矢量乘积误差梯度是影响元素之间后续分布的中间值。因此,在工作组内同步它对我们来说很重要。故在该阶段,我们将其保存在局部数组元素之中。稍后我们将把它移至局部变量。
if(f == ifeat) prod_gr += temp[0]; barrier(CLK_LOCAL_MEM_FENCE);
我想您注意到了大量的重复控制。这令代码复杂化,但对于组织工作组线程的同步阻碍的正确传递是必要的。
接下来,我们按类似方式汇总偏移向量范数误差梯度。
if(d < ls && f == ifeat) temp[d] = normu_gr; barrier(CLK_LOCAL_MEM_FENCE); for(int id = ls; id < (int)dimension; id += ls) { if(d >= id && d < (id + ls) && f == ifeat) temp[d % ls] += normu_gr; barrier(CLK_LOCAL_MEM_FENCE); } //--- count = min(ls, (int)dimension); //--- do { count = (count + 1) / 2; if(f == ifeat) { if(d < count) temp[d] += ((d + count) < dimension ? temp[d + count] : 0); if(d + count < dimension) temp[d + count] = 0; } barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1); if(f == ifeat) { normu_gr = temp[0]; if(isinf(normu_gr) || isnan(normu_gr)) normu_gr = 1.2e-7;
然后我们调整偏移矢量误差梯度。
u_gr += u / normu * normu_gr; if(isnan(u_gr) || isinf(u_gr)) u_gr = 0;
我们将其分派在输入数据和质心之间。
feature_gr += u_gr; centroid_gr += prod * curv * u_gr; } barrier(CLK_LOCAL_MEM_FENCE);
此处重点要注意,偏移矢量的误差梯度必须分派到矢量乘积级别、和曲率参数。不过,这些实体是标量值。这意味着我们需要聚合所分析序列的每个元素值。在该阶段,我们将位移矢量的相应误差梯度的乘积、与质心的元素求和。本质上,该操作相当于计算这些向量的点积。
//--- dot (u_gr * centroid) if(d < ls && f == ifeat) temp[d] = u_gr * centroid; barrier(CLK_LOCAL_MEM_FENCE); for(int id = ls; id < (int)dimension; id += ls) { if(d >= id && d < (id + ls) && f == ifeat) temp[d % ls] += u_gr * centroid; barrier(CLK_LOCAL_MEM_FENCE); } //--- count = min(ls, (int)dimension); //--- do { count = (count + 1) / 2; if(f == ifeat) { if(d < count) temp[d] += ((d + count) < dimension ? temp[d + count] : 0); if(d + count < dimension) temp[d + count] = 0; } barrier(CLK_LOCAL_MEM_FENCE); } while(count > 1);
我们使用获得的数值将误差梯度分派到相应的实体。
if(f == ifeat && d == 0) { if(isinf(temp[0]) || isnan(temp[0])) temp[0] = 0; prod_gr += temp[0] * curv; if(isinf(prod_gr) || isnan(prod_gr)) prod_gr = 0; curv_gr += temp[0] * prod; if(isinf(curv_gr) || isnan(curv_gr)) curv_gr = 0; temp[0] = prod_gr; } barrier(CLK_LOCAL_MEM_FENCE);
接下来,我们在工作组内的矢量乘积级别同步误差梯度值。
if(f == ifeat) { prod_gr = temp[0];
我们分派获得的数值贯穿输入数据。
feature_gr += prod_gr * centroid * (d > 0 ? 1 : -1); centroid_gr += prod_gr * feature * (d > 0 ? 1 : -1); } barrier(CLK_LOCAL_MEM_FENCE); }
所有操作成功完成,并将误差梯度完全收集到局部变量中之后,我们将获得的数值传播到全局数据缓冲区。
//--- result features_gr[shift_f] = feature_gr; centroids_gr[shift_cent] = centroid_gr; if(f == 0 && d == 0) curvatures_gr[cent] = curv; }
至此,我们结束内核实现。
您或许已注意到,该算法非常复杂,但很有趣。理解它需要密切关注细节。
如前所述,实现 HypDiff 框架涉及大量工作。在本文中,我们特意专关注 OpenCL 程序中算法的实现。附件中提供了其完整的源代码。然而,我们几乎达到文章长度的极限。因此,我提议在下一篇文章中继续探索主程序端的框架算法实现。这种方式将令我们能够在逻辑上将整个工作分为两部分。
结束语
双曲几何的使用有效地解决了离散图形数据、和连续扩散模型之间不匹配带来的挑战。HypDiff 框架引入了一种生成双曲高斯噪声的高级方法。它旨在解决双曲空间内高斯分布中的可加性失效问题。基于角度相似性的几何约束应用在各向异性扩散过程,以便保留局部图形结构。
在本文的实践部分,我们开始实现所拟议方法,即利用 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/16306
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。



