交易中的神经网络:异质性感知元参数学习(HimNet)
引言
随着各种用于采集流式数据的工具不断涌现,人类活动的诸多领域中已积累了大量时空序列数据。 交易系统会在不同的交易场所、交易品种代码和交易品种之间生成报价、订单和交易流。 随着数据规模的增长,对这类时序进行准确预测已成为降低成本、管理风险以及优化交易策略执行效果的有效工具。 一旦对局部流动性或波动率的预测出现偏差,会直接导致滑点、利润损失以及交易策略绩效指标的失真。 因此,一个高质量的模型不仅要考虑整体趋势,还要兼顾特定场景下的局部差异:即市场在哪些位置和时段流动性紧张,又在哪些位置和时段流动性宽松
关键挑战在于数据的时空异质性。 当不同的交易场所或资产在同一时间呈现不同的走势时,空间异质性便显现出来。 例如,一些交易所可能流动性充足,而另一些交易所则可能存在套利缺口,特别是在市场波动率剧烈的时期。 在这种情况下,试图对所有交易场所的行为进行平均处理,会抹平重要的局部效应,从而导致精度下降。
时间异质性远比表面看上去更加突出。 同一金融工具在一天中的不同时段以及不同的日历日期,其走势会发生显著的变化。 交易时段的开盘和收盘就是两个典型的例子:开盘后的头几分钟,成交量和波动率会急剧上升;而到了晚间较晚时段,情况则恰恰相反,市场活跃度则显著走低。 新闻和宏观经济公告会引发波动率极高的短期市场状态。 节假日和周末会导致市场交投沉寂。 综合来看,这意味着相同的市场模式不具备时序平稳性——其统计特性会发生变化,模型必须将这一点考虑在内。
前人曾试图通过各种方式解决这一问题,但在实际交易应用中,每种方法都存在其局限性。 基于图的方法依赖于拓扑结构以及数据点之间预先计算出的相似性度量——就好比交易者完全依据预先绘制的流动性图来制定策略一样。 但在现实世界中,这张图谱很快就会失效:做市商的结构发生变化,新的交易场所不断涌现,还会出现暂时的流动性缺口。
元学习方法主张为不同市场状态维护多套参数集合,但通常需要外部信息。 在金融领域,这意味着每个交易场所和金融工具都需要具备可获取且准确的元数据。 但并非总能实时获取这些元数据。 此外,其中许多方法的内存占用过大,且计算成本过高。
基于表征的方法(表征学习)已能从原始数据中提取富含信息的嵌入向量。 该方法很强大,但随后往往会对嵌入进行简化处理,导致优质表征的价值在下一步中丧失。自监督学习通过引入辅助任务来丰富嵌入特征——这固然有效,但并不总能让模型在单个训练周期内针对主预测任务实现最优训练。
正因如此,论文《面向时空时间序列预测的异质性感知元参数学习》的作者提出了一种不同的方法——HimNet框架。 该方法包含两大核心模块。 首先,通过可训练的空间嵌入和时间嵌入隐式地识别异质性,然后利用这些信息对模型参数进行元参数学习。 简而言之,该框架的设计目标不仅旨在识别各个数据片段之间的差异,而且致力于训练模型,使其能够针对每一种场景选择并生成专属的参数。
假设交易信息库中包含:
- 交易时段内,在主要交易所交易的最大发行人的股票;
- 当地新闻集中爆发后,场外交易场所中的同一批股票;
- 流动性较低的小盘证券;
- 订单簿深度各不相同的加密货币市场。
空间嵌入将把这些组划分为不同的聚类。 随后,系统会从元参数池中为每个聚类检索一组专用参数:针对主要市场 采用以低滑点为考量的激进参数配置;针对场外交易,采用保守参数设置,以扩大执行层面的安全边际;针对小盘证券 则采用更广泛的正则化,以避免对噪声过拟合;针对加密货币市场采用噪声感知配置,以应对高噪声水平和频繁出现的套利价差。 整套流程都在同一个训练周期内完成,因此模型能够同时学会区分不同场景,并为它们选择最佳参数。
时间维度场景同样重要。 早盘开盘时,往往需要迅速做出反应并进行短期成交量预测。 关键的宏观经济数据发布后的时段属于高波动率状态。 较晚时段对应的是低活跃度状态。 该框架作者提出的元参数机制可以为不同的时间状态分配独立的参数集。 这就像资深交易者会根据一天中的不同时段和市场日历来调整自己的交易风格一样。
从技术角度来看,其核心思路是对嵌入向量进行聚类,并通过一个规模小、计算效率高的元参数池对模型参数进行动态量化。 这同时解决了先前方法中存在的几个问题。 首先,该模型并不严格依赖外部辅助特征——它能够自主识别相关场景。 其次,元参数池规模紧凑,因此内存和计算资源的使用量仍处于可控范围,这对需要低成交执行延迟的系统而言至关重要。 第三,该框架作者从被动地识别异质性转向了主动利用异质性:聚类结果直接决定了应该启用哪些参数。 这就提高了预测的准确性,并提升了成交执行质量。
可解释性同样值得重点关注。 我们能够可视化元参数和聚类结果:聚类图谱将显示哪些交易场所和哪些时间窗口属于同一市场状态;元参数热力图将展示低波动率与高波动率市场状态下的设置有何差异。 对于从业者而言,这不仅是一份可视化图表,更是一个监控工具:如果某个聚类中意外混入了本质上截然不同的交易场所,就意味着需要核查数据质量或审查预处理流程。
最后,是鲁棒性和可迁移性。 HimNet框架的一个重要特征是在不同领域间的灵活性。 元参数和嵌入向量进行训练后,只需少量微调即可应用于其他金融工具和交易场所。
总而言之,HimNet框架提供了一种识别时空状态的实用且有效的方法,构建了一个紧凑的元参数池,并训练模型利用所获得的信息。 这就为交易员和预测系统开发者提供了一种工具,既能提高预测准确性,又能保持计算效率,同时还能增强可解释性。
HimNet算法
对时空异质性进行建模的关键之一,是沿时间和空间两个维度正确识别并分离源数据对应的场景。 HimNet框架作者没有使用外部参考数据,而是构建了可训练的嵌入向量,为每个时空场景分配一个独有的表征。 这使该模型具备了解决现实世界中的金融问题所需的灵活性和适应性。
对于时间维度,该框架作者构建了两个嵌入字典:
- 日内时刻字典 Dtod ∈ RNd × dtod;
- 星期字典 Ddow ∈ RNw × ddow。
其中,dtod和ddow分别是相应向量的维数,Nd表示一个交易日内的步数,Nw表示一周的天数。
对于小批量输入数据Xb ∈ RB×T×N,其历史长度为T,该框架作者提出选取每个样本最后一步的时间戳,从字典中选取时间嵌入Etod ∈ RB×dtod和Edow ∈ RB×ddow。 再将二者拼接成一个共享的时间嵌入。
![]()
这样拆分的实际理由很简单。 日内时刻维度捕捉的是细粒度的周期性特征:早盘开盘时的骤升、午间的平静期以及晚间交易时段。 星期维度捕捉的是更长的周期:工作日和周末的行为,以及报告发布日期和经济数据公布前后反复出现的模式。 两套字典共同配合,使模型能够识别多个尺度上的时间状态,并为其选择合适的预测参数。
在空间维度上,该框架作者使用空间嵌入矩阵 Es ∈ RN*ds,其中,N为时间序列或位置的数量,ds 为每个位置的向量维度。 与时间字典不同,这里的每个空间元素都直接关联一个自身专属的可学习向量,该向量采用随机初始化。 其目的是捕捉交易场所之间影响时间序列模式的底层差异:订单簿深度、成交执行速度、参与者特征、金融工具类型以及基础设施特征。 这样在无需显式使用辅助特征的前提下,也能刻画空间异质性。
这种嵌入训练本质上属于动态聚类。 在训练过程中,矩阵中的表征向量会根据输入数据逐渐分化并形成聚类。 行为表现相似的时空场景所对应的嵌入向量,会在潜在空间中相互靠近。 而动态特征不同的时空场景所对应的嵌入则会相互远离。 由此自然形成聚类,对应着典型的行为状态。 这与语言模型类似,其中“国王”和“王后”在语义上相近。 只不过在这里,向量距离反映的是市场状态的相似性。 关键在于,这种聚类是自然形成的,没有严格的外部约束,也不需要人工标注。 该模型能够自主学习场景之间的相似与差异。
让我们更详细地分析一些金融案例,以便更直观地说明在实践中的运行逻辑。 让我们考虑几种典型的场景:
- 主要股市交易时段的开盘。 交易活跃、成交量大、价格波动迅速。 对于此类时空组合,模型会识别出一个专属嵌入,并选择适合短期预测和快速响应的参数——这对快速成交执行算法以及减少滑点十分重要。
- 夜间对同一品种进行场外交易。 成交量低、交易不频繁,且零星订单引发价格剧烈波动的风险较高。 在此情况下,时间嵌入向量将发生变化,而元参数会使模型变得保守,从而提升鲁棒性,并降低对噪声过拟合的可能性。
- 订单簿深度各不相同的加密货币交易所。 一家交易所的流动性充沛,而另一家的流动性不足,并具有套利特征。 空间嵌入向量将把这些交易场所区分开来,模型将针对成交量和价格的预测采用不同的策略,这对套利系统非常有用。
- 重大宏观经济数据发布日或季度财报发布日。 结合星期嵌入和日内时刻嵌入,可以识别此类时间窗口;模型将切换到对短期信号更敏感、置信区间更宽的状态。
这一核心理念虽然简单,却行之有效。 我们的任务不仅在于捕捉异质性,还在于学习如何驾驭它。 使其在市场预测和成交执行中发挥实际作用。 如果为每一分钟和每个交易所都维护一组独立参数,在经济上和技术上都不可行。因此,该框架作者转而沿时间和空间维度扩展模型的参数空间,再将这一扩展紧凑地表示为若干小型元参数池。 对于特定的时空查询,最终参数由池中候选参数的加权组合生成:Θ = Q · P。 这种方法使我们只需要维护一组紧凑的参数集合,而不是对大量独立参数进行优化,从而大幅降低了内存和计算需求。 我们不必为覆盖所有时间步和位置的参数扩展付出算力成本,而只需借助少量候选参数来实现适应。
实际上,这意味着我们维护一个独立的小型时间池,从中提取时间嵌入Et并获得时间元参数。 同样,空间嵌入Es会查询空间池,并为每个单独的时间序列提供元参数。 与此同时,我们不能忽视时空耦合特征:直接构建一个完整的联合池效率不高,因此该框架作者将输入数据Xt编码为时空嵌入Est = Fenc(Xt),并以此访问一个相对较小的池,从而生成时空元参数(ST元参数)。 得益于这种分离,模型获得了更大的灵活性:它可以根据特定时间、特定交易场所以及同时发生的特定事件组合来调整参数,而不会牺牲可维护性或运行时性能。
这种方法在业务层间也具有实际优势。 由于参数池规模较小,推理时生成元参数的开销较低,这对延迟要求严格的任务至关重要。 对元参数池与主模型进行联合训练,可使梯度通过对元参数池的查询传播,从而使嵌入和参数得到协同优化。 与此同时,必须监控训练稳定性,并通过熵约束或温度平滑来控制查询权重的分布,以免单个候选项独占权重,导致模型过于复杂、泛化能力较差。
选择元参数池的大小,需要在较强的适应能力与过拟合风险之间进行权衡;实际实现中,会根据目标市场中观察到的典型状态数量,在验证集上调整池的大小。
HimNet框架的架构结合了图卷积与循环规则:这些模块既能处理输入数据的历史信息,又能考虑各个序列之间的关系。 试想交易者同时查看实时成交信息和市场关系图谱,并结合这两种视角做出决策。 这正是该模型基础单元所发挥的作用。 它会对图中相邻的信号进行聚合,并随时间推移更新隐藏状态。
从技术实现的角度来看,基础模块是一种经过修改的图卷积循环单元(GCRU)。 其内部包含常规的重置门和更新门、候选状态,以及新旧隐藏表征的融合逻辑。 但有一个重要的细节——这里的图卷积参数并不是固定值。 它们是通过元机制动态生成的。 这意味着邻域聚合的权重模式(即一个序列如何影响另一个序列)会随场景而变化。 这对金融市场至关重要:交易品种之间的关系是动态的。 当主要交易所出现大范围市场反应时,交易品种的耦合度会很高。 在另一个时间段内,当某个交易场所暂时停止交易时,原有关系就会失效。
该模型采用经典的编码器-解码器架构。 编码器在HimNet框架中通过两个并行分支运行,随后将两者的输出相加。 其中一个分支沿时间维度自适应。 它接收时间嵌入,并利用这些嵌入生成时间元参数。 另一个分支则利用序列的空间嵌入,沿空间维度进行适应,并为每个点输出元参数。 这种双通道方法使模型能够同时考虑当前正在发生的事情以及事情发生的位置。 但该框架作者并未止步于此。 为了考虑时空联合效应,解码器接收一个潜在状态,并投影得到时空嵌入向量。 以此作为对时空元参数池的查询。 得到一组专门适用于当前时间、位置和动态特征组合的循环权重。
自适应邻接矩阵是另一个重要组件。 该模型并非维护一个静态连接矩阵,而是根据空间嵌入生成该矩阵:将嵌入向量的点积依次送入ReLU和SoftMax函数处理。 最终得到的是一个反映当前市场关系的矩阵。 其实际效果是:当某个交易所开始出现行情激增时,模型会自动增强该交易所对邻近节点的影响。 当某个交易场所的流动性流失时,其影响力就会减弱。 这使得模型的行为能够随市场的实际状态变化,而不是受制于很快就会过时的静态关系图谱。
HimNet中的解码器通过迭代生成后续时间步的预测。 它以一个初始隐藏状态为输入,使用一个GCRU单元,该单元采用时空元参数,并为每个未来时间步输出一个预测值。 这种方法不只是为所有序列提供平均化的预测,还会生成兼顾局部状态和时间窗口的预测。
训练采用端到端的方式进行。 框架作者针对所有时间步和交易场所采用了一个简单而明确的指标——平均绝对误差(MAE)。 其优势在于损失的计量单位与预测值相同,方便业务人员解读。 在训练过程中,监控稳定性非常重要:可以考虑对元参数池应用L2正则化、在生成权重时使用温度平滑,并限制更新速度。 池大小的选择需要权衡:
- 过小——模型将无法涵盖所有状态;
- 过大——会增加过度特化的风险和计算负荷。
从实际应用的角度来看,HimNet带来了切实的好处。 准确评估局部流动性可减少大额交易成交执行时的滑点。HimNet为套利系统提供所需的场景,以过滤虚假价差并识别交易所之间持续存在的价差。
最后,这些设计细节使得该系统适合生产部署。 生成元参数只需对小矩阵进行乘法运算,不会导致内存占用激增。 这些元参数池规模紧凑,有助于保持低延迟,而这对于时间要求严格的任务至关重要。 所有这些都简化了将HimNet集成到订单成交执行流程和风险控制系统中的工作。
下图展示了作者绘制的HimNet框架示意图。

在MQL5中的实现
在详细分析了HimNet框架的理论基础之后,顺理成章的下一步就是展示如何将这套思路实现为可运行的MQL5代码。 实际实现并非生硬地照搬公式,而是要在真实交易平台的约束条件和能力范围内将架构落实。 我们将采取系统而细致的方法:将整体架构分解为独立且定义明确的对象,并一步步构建模型,就像一位制表大师组装腕表一样——细致入微、有条不紊,并兼顾每一处齿轮的配合。 这种方法简化了测试流程,加快了调试速度,并使解决方案能够轻松地扩展到不同的市场和时间周期。
在MQL5底层架构层面上,这意味着预处理模块、嵌入、元参数和循环单元均作为独立组件存在。 K线数据流和所分析指标的数值被转换为规整的定长数据窗口。 时间嵌入和单变量序列嵌入被送入权重生成模块中。 GCRU单元将市场的图结构作为输入,并返回更新后的隐藏状态。 整套逻辑听起来都很合理,但有一个关键环节,缺少它,HimNet就会失去核心优势:即便相隔数条边,也能快速、可靠地获取图上邻居信息的能力。 从金融角度来看,这意味着不仅要捕捉序列之间的直接影响,还要捕捉二阶和三阶影响——即某个节点的流动性激增会在网络中产生传导效应,并伴随延迟和衰减最终传导至所分析的金融工具。
这就是切比雪夫多项式发挥作用之处。 它可以在图上构造局部K跳滤波器,而无需进行计算量庞大的谱分解。 该框架作者不计算特征向量,而是采用简单的递推关系,由邻接矩阵生成一组矩阵基T0、T1、…、TK-1。 每个这样的矩阵对应一层邻域:从节点本身到第K阶邻域。 因此,一次卷积运算就转化为若干预先计算好的变换的简洁线性组合。 这对于交易至关重要:延迟更低、内存占用更小,而且在处理长序列和N取值较大的场景时,数值计算性能依然稳定。 当市场噪声较大时,该滤波器有助于识别稳定的关系,避免对随机突变作出反应。
为使该理念能落实到生产环境中,我们需要一个独立的对象,能够根据当前的邻接矩阵快速构造所需阶次的切比雪夫多项式矩阵。 它将充当整个图层的“隐形引擎”:以由嵌入构建的自适应支撑矩阵为输入,返回一组紧凑的Tk矩阵,供GCRU直接使用。
我们实践工作的第一步是对OpenCL程序进行修改。 GPU非常适合这项任务:借助多线程和向量运算,处理大型邻接矩阵的速度可以远快于CPU。 对于金融数据而言,这一点尤为关键,因为相关图可能规模相当大,且连接关系丰富、分支繁多。
从某种意义上说,ChebStep前向传播内核的算法是切比雪夫多项式生成机制的核心,在OpenCL底层运行。 它与交易所撮合引擎的无缝运行极为相似,其中每一个行情更新和每一笔订单都被编织进同一套统一的逻辑体系中。
__kernel void ChebStep(__global const float* support, __global float* outputs, const int step ) { const size_t l = get_local_id(0); const size_t r = get_global_id(1); const size_t c = get_global_id(2); const size_t total_l = get_local_size(0); const size_t total_r = get_global_size(1); const size_t total_c = get_global_size(2);
该程序首先获取问题空间中的坐标。 局部和全局线程ID代表我们交易所的交易者,其中每人负责矩阵中的一个独立部分。 它们并行运行,因此整个数据集的处理不再像传统人工交易那样逐笔顺序进行,而是像自动化的高频交易系统那样多路并发。 局部Temp缓冲区充当临时清算所——中间求和结果先存储在这里,随后通过同步操作生成最终结果。
__local float Temp[LOCAL_ARRAY_SIZE]; //--- if(step <= 0 || total_r != total_c) return;
该算法即刻设定了一个重要的约束条件:如果步长小于或等于零,或者矩阵不是方阵,那么继续计算就没有意义。 这就好比交易者在市场条件不符合策略规则时选择不进行交易。
接下来,按次数递增的顺序逐步构建切比雪夫多项式。 第一步很简单:构建一个单位矩阵,其对角线元素均为1。 这就像初始本金——是一切的起点。
if(step <= 3) { const float diag = (r == c ? 1.0f : 0.0f); if(l == 0) outputs[RCtoFlat(r, c, total_r, total_c, 0)] = diag;
在第二步中,原始邻接矩阵便派上了用场——它正是图中各顶点之间的连接图,反映了市场的真实结构。 它被添加到输出数组中,而对实际价格波动的近似计算正是从这里开始的。
if(step < 2) return; if(l == 0) { const float s = IsNaNOrInf(support[RCtoFlat(r, c, total_r, total_c, 0)], 0); outputs[RCtoFlat(r, c, total_r, total_c, 1)] = s; }
在第三步中,计算二次幂——即矩阵的平方。 循环中的代码将行与列中对应位置的元素相乘,并仔细累加各项乘积。 这类似于分析资产之间的交叉相关性,我们不仅关注直接关系,还关注通过中间环节形成的间接关系。
由此生成的矩阵反映了更为复杂的交互关系,其中影响不仅限于直接连接,还会通过相邻节点传播。 求和之后,会进行归一化和调整以消除冗余,这就好比交易者从报价中过滤掉噪音,只保留有意义的信号一样。
if(step < 3) return; float out = 0; for(int t = 0; t < total_c; t += total_l) { const float s1 = IsNaNOrInf(support[RCtoFlat(r, t + l, total_r, total_c, 0)], 0); const float s2 = IsNaNOrInf(support[RCtoFlat(t + l, c, total_r, total_c, 0)], 0); out += IsNaNOrInf(s1 * s2, 0); } out = 2 * LocalSum(out, 0, Temp); if(l == 0) { out -= diag; outputs[RCtoFlat(r, c, total_r, total_c, 2)] = IsNaNOrInf(out, 0); } return; }
从第四步起,算法转而采用切比雪夫多项式的通用递推公式。 在这里,每个新矩阵并非从头开始构建,而是由之前得到的矩阵幂组合而成。 该代码将邻接矩阵与上一步的结果相乘,然后通过减去更早一步得到的矩阵来调整该结果。 此过程类似于构建复杂的交易指标:例如,指数移动平均线会考虑之前的状态,在平滑剧烈波动的同时,仍能保留历史信息。
float out = 0; for(int t = 0; t < total_c; t += total_l) { if((t + l) >= total_c) continue; const float s1 = IsNaNOrInf(support[RCtoFlat(r, t + l, total_r, total_c, 0)], 0); const float s2 = IsNaNOrInf(outputs[RCtoFlat(t + l, c, total_r, total_c, step - 2)], 0); out += IsNaNOrInf(s1 * s2, 0); } out = 2 * LocalSum(out, 0, Temp); if(l == 0) { out -= IsNaNOrInf(outputs[RCtoFlat(r, c, total_r, total_c, step - 3)], 0); outputs[RCtoFlat(r, c, total_r, total_c, step - 1)] = IsNaNOrInf(out, 0); } return; }
通过这种方式,模型逐步构建出对图的谱性质的更深层次近似。
内核中的每次迭代都会以数据同步和有效性检查结束。 该代码大量使用了一个用于防范NaN值和无穷大的函数——这也提醒我们,市场是不会原谅错误的。 正如风险管理会控制亏损限额一样,此处的任何异常情况都会在早期阶段被过滤掉,以免影响整体结果。
通过这一循序渐进的计算逻辑,我们得到了完整的切比雪夫多项式序列,这些多项式构成了图神经网络中谱滤波器的基本组件。 在实际应用中,这意味着该算法并非仅从当前关系的视角观察市场,而是构建了一个多层的光学系统,既能捕捉金融工具之间的局部依赖关系,也能捕捉其长程依赖关系。 这就像经验丰富的分析师能够从黄金价格的波动中看出外汇市场的间接信号,或者从债券走势中预见到股市的变化一样。
既然我们已经探讨了直接构造切比雪夫多项式的算法,并了解其计算结构是如何一步步构建起来的,那么接下来自然要探讨误差反向传播的机制。 此时,一个特殊的内核便发挥了作用;它负责将梯度准确地分配到各阶切比雪夫多项式矩阵上,并回传至邻接矩阵,从而确保正确训练整个模型。
__kernel void ChebStepGrad(__global const float* support, __global float* support_g, __global const float* outputs, __global float* outputs_g, const int step ) { const size_t l = get_local_id(0); const size_t r = get_global_id(1); const size_t c = get_global_id(2); const size_t total_l = get_local_size(0); const size_t total_r = get_global_size(1); const size_t total_c = get_global_size(2); //--- __local float Temp[LOCAL_ARRAY_SIZE]; //--- if(step < 1 || total_r!=total_c) return;
在开始执行时,该算法还会初始化局部和全局线程ID,这些ID就像市场报价一样,决定了每个计算单元在庞大的并行线程网格中的位置。 为临时数组分配局部内存以加快归约速度,并通过初始检查确保当前步骤有效且矩阵为方阵。 正如交易员绝不会分析不完整的订单簿一样,这里的程序也会在条件无效时立即停止运行。
当误差梯度计算的步骤编号大于或等于2时,最有趣的部分出现了。 首先,取后续层级的梯度,并将其从较早层级的输出梯度中减去。 这可以比作预测修正:如果策略在最后一个时间步产生了误差,其“阴影”就会传递到前一个时间步。
if(step >= 2) { float grad = IsNaNOrInf(outputs_g[RCtoFlat(r, c, total_r, total_c, step)], 0); if(l == 0) outputs_g[RCtoFlat(r, c, total_r, total_c, step - 2)] -= grad;
接下来,该算法计算相对于邻接矩阵的梯度。 每个线程取各自负责的一部分数据,将输出中的误差乘以前一步的相应值,并利用局部内存将所有结果仔细相加。 其结果是一种类似于投资组合再平衡的修正信号——当某些资产超配时,其影响力会逐渐重新分配。
//--- support grad grad = 0; for(int t = 0; t < total_c; t += total_l) { if((t + l) >= total_c) continue; const float s2 = IsNaNOrInf(outputs[RCtoFlat(c, t + l, total_r, total_c, step - 2)], 0); grad += IsNaNOrInf(outputs_g[RCtoFlat(r, t + l, total_r, total_c, step)] * s2, 0); } grad = LocalSum(grad, 0, Temp); if(l == 0) outputs_g[RCtoFlat(r, c, total_r, total_c, 1)] += grad; BarrierLoc;
一旦线程同步完成,就轮到了下一步——计算关于多项式 Tk-1 的梯度。 这里也遵循同样的原理:每个线程处理邻接矩阵的一部分,将其与下一步的误差相乘,然后将所得结果精确地加到总和中。 因此,会针对第 k-1 阶多项式的梯度生成一个修正项。 这一过程类似于分析师在模型中回退一步,检查该处是否高估了风险,并加入相应的修正。
//--- T(k-1) grad grad = 0; for(int t = 0; t < total_c; t += total_l) { if((t + l) >= total_c) continue; const float s2 = IsNaNOrInf(support[RCtoFlat(t + l, r, total_r, total_c, 0)], 0); grad += IsNaNOrInf(outputs_g[RCtoFlat(t + l, c, total_r, total_c, step)] * s2, 0); } grad = LocalSum(grad, 0, Temp); if(l == 0) outputs_g[RCtoFlat(r, c, total_r, total_c, step - 1)] += grad; }
请注意:我们将所有误差梯度仔细地收集到一个与切比雪夫多项式相对应的张量中。 其中还包含了在反向传播的每一步中计算得到的邻接矩阵的中间梯度。 必须记住,邻接矩阵本身存储在一阶多项式中,这意味着该层级成为了数据流交汇的关键点。 只有当我们到达这一层级时,才会将累积的误差梯度转移到邻接矩阵缓冲区中,这就像是在经过一系列中间调整后锁定最终余额一样。
因此,该内核就像计算过程中的“审计员”,防止误差信号在反向传播过程中丢失或分散。 每个值都会经过一套局部求和机制,调整后被传递到能够更新权重的位置,从而提高预测准确性。 正如在市场中,每多出0.1%都可能起到关键作用一样,在这里,将梯度细致地分配到各个步骤,也能使模型稳定学习并保持正确方向。
既然基础工作(矩阵生成和梯度分配)已经就绪,一个自然而然的问题便浮现出来:在主程序这边,由谁来管理整个过程呢? 我们需要一个充当调度器的对象,将底层的OpenCL内核妥善封装起来,并提供便捷的接口,以便与模型中的其他模块交互。 这样就得到了CChebPolinom类,该类从全连接层对象CNeuronBaseOCL继承了基本接口。
class CChebPolinom : public CNeuronBaseOCL { protected: uint iDimension; uint iSteps; //--- virtual bool feedForward(CNeuronBaseOCL *NeuronOCL) override; virtual bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) override { return true; } virtual bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) override; public: CChebPolinom(void) {}; ~CChebPolinom(void) {}; //--- virtual bool Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint dimension, uint steps, ENUM_OPTIMIZATION optimization_type, uint batch); //--- virtual bool Save(const int file_handle) override; virtual bool Load(const int file_handle) override; //--- virtual int Type(void) override const { return defChebPolinom; } virtual uint GetDimension(void) const { return iDimension; } virtual uint GetSteps(void) const { return iSteps; } };
它在内部存储了关键参数:空间维数以及切比雪夫多项式展开所需的步数。 这些参数定义了计算的范围以及模型在处理图结构时所能达到的深度。
该对象在Init方法中进行初始化,您可以在该方法中指定所有必要的参数。
bool CChebPolinom::Init(uint numOutputs, uint myIndex, COpenCLMy *open_cl, uint dimension, uint steps, ENUM_OPTIMIZATION optimization_type, uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, dimension * dimension * steps, optimization_type, batch)) return false; //--- iDimension = dimension; iSteps = steps; //--- return true; }
该方法的实现方式非常简单: 首先,我们将控制权交给父类的同名方法,并指定足够大的对象维度,以容纳由所有必要的切比雪夫多项式拼接而成的张量。 然后,该对象在内部存储两个主要参数:维度iDimension和步数iSteps。 这些值构成了该类的“规格说明”,所有其他方法都会以此为依据。 可以说,我们在这里敲定了工作区:既指定画布的宽度,也明确了信息流经的层数。
方法末尾返回true,表明该对象已成功完成准备,可以投入运行。 尽管该实现看起来很简洁,但正是在这一阶段,一个根本性问题得到了解决:我们的算法能否在GPU上高效地进行多项式逼近。
前向传播方法feedForward本质上充当调度器,只负责封装和组织对相应OpenCL程序内核的调用。 其结构非常容易识别:依次检查输入数据、准备运行参数,然后调用内核。 然而,生成和应用切比雪夫多项式这一任务的特点,也使这里的实现有一些细节上的差异。
bool CChebPolinom::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL || NeuronOCL.Neurons() != (iDimension * iDimension)) return false;
首先,该方法会检查输入对象是否有效,以及神经元数量是否与邻接矩阵的元素总数iDimension * iDimension一致。 如果不符合这一条件,那么进一步处理就完全没有意义了。
接下来,准备工作范围:选择计算网格的全局尺寸时,既要考虑GPU对局部工作组大小的实际限制,也要体现问题的矩阵结构——这三个维度实际上沿矩阵的各个轴展开计算网格。
uint global_work_offset[3] = { 0 }; uint global_work_size[3] = { MathMin(iDimension, uint(OpenCL.GetMaxLocalSize(0))), iDimension, iDimension }; uint local_work_size[3] = { global_work_size[0], 1, 1 };
这里的关键是按步骤迭代的循环。 这正体现了构造切比雪夫多项式的递归性质:要得到k阶多项式的值,必须依赖此前各阶的值。 从最小的有效步骤编号开始(通常为2,因为前两个步骤由初始条件定义),该方法依次将步骤编号传递给内核,然后启动内核执行。 因此,在每次迭代中,GPU都会收到一项新任务:构建下一阶多项式逼近。
//--- uint kernel = def_k_ChebStep; setBuffer(kernel, def_k_cheb_support, NeuronOCL.getOutputIndex()) setBuffer(kernel, def_k_cheb_outputs, getOutputIndex()) for(int step =::MathMin(2, MathMax(int(iSteps) - 1, 0)); step < int(iSteps); step++) { setArgument(kernel, def_k_cheb_step, step + 1) kernelExecuteLoc(kernel, global_work_offset, global_work_size, local_work_size) } //--- return true; }
其结果形成了一种“套娃式”的计算:每一步都依赖于前一步,但与此同时,整个计算过程是在矩阵的各个元素上并行进行的。 在金融市场中,这种方法类似于基于滚动预测时域的预测策略:每次新的预测并非从零开始,而是延续前一次预测,进一步细化和深化对整体形势的刻画。
这正是该方法的强大之处——其形式简洁,将朴素的调度逻辑与深层的数学递归融为一体。
负责传播误差梯度的方法也是基于类似的原理。 与前向传播类似,该方法的主要目标是在考虑切比雪夫多项式计算的递归结构的同时,正确组织对OpenCL程序中相应内核的调用。 所有累积的梯度都会被审慎求和,并分配到相应的步骤中,其中需要特别关注邻接矩阵和中间多项式。
您可以自行对该方法进行详细的逐行分析。 附件提供了该类的完整源代码,包括其所有方法,便于您全面了解框架这一部分的工作原理。
我们已经讲解完大量内容,现在不妨稍作停顿,整理并梳理一下目前掌握的所有信息。 详细的分步说明将帮助您更好地理解该系统的运作原理及其各组成部分之间的相互关系。 在下一篇文章中,我们将继续构建HimNet框架的算法,包括所有模块的实际集成。
结论
在本文中,我们探讨了HimNet框架的理论基础,并进一步使用MQL5和OpenCL实现了所提出的方法。 我们详细探讨了时空元参数的概念。 我们重点探讨了图卷积循环模块如何处理时间和空间依赖关系,同时还研究了在GPU上生成和应用切比雪夫多项式的算法,以加快计算速度并提高模型的鲁棒性。
在下一篇文章中,我们将继续致力于为HimNet框架开发算法。
参考文献
本文中使用的软件
| # | 名称 | 类型 | 描述 |
|---|---|---|---|
| 1 | Study.mq5 | 智能交易系统 | 用于离线模型训练的智能交易系统 |
| 2 | StudyOnline.mq5 | 智能交易系统 | 用于在线模型训练的智能交易系统 |
| 3 | Test.mq5 | 智能交易系统 | 用于模型测试的智能交易系统 |
| 4 | Trajectory.mqh | 类库 | 描述系统状态和模型架构的结构体 |
| 5 | NeuroNet.mqh | 类库 | 用于创建神经网络的类库 |
| 6 | NeuroNet.cl | 库 | OpenCL程序的代码库 |
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/19233
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。