交易中的神经网络:广义 3D 引用表达分段(基础篇)
◍ 把行情拆成三维引用再做特征
传统指标大多只在一维时间序列上滚动,Dmitriy Gizlyk 在 2025 年 6 月 27 日发布的这套思路,把单根 K 线周边上下文扩展成广义 3D 引用:价格、时间偏移与样本窗口三者共同定义一个特征单元。 这种表达的核心价值,是让神经网络不再只看到「当前收盘价」,而是同时拿到前 N 根、跨周期、邻域统计三类信息。MT5 里用标准 timeseries 接口就能拼出这类张量,无需额外插件。 实盘前建议先用 EURUSD 的 M15 历史数据跑一遍引用构建,确认每根 K 线的 3D 索引不越界;外汇与贵金属杠杆高、滑点大,任何特征工程都只是提高概率,不保证方向。
从单目标到任意数量的 3D 引用分段
传统 3D 引用表达分段(3D-RES)只能处理指令中指向单个目标的情况,一旦真实场景里出现“找不到”或“同时要多个”的状况,模型就直接失效。这种局限在机器人拣选、场景理解里很致命,因为自然语言指令本来就不会乖乖只说一个物体。 广义 3D 引用表达分段(3D-GRES)把任务扩到任意目标数量:用多个查询并行,每个查询只盯多对象场景里的某一个实例,再联合吐出掩码。关键点在于“解耦”——查询之间独立跑,但不丢失整句指令的语义约束。 具体实现上,多查询解耦交互网络(MDIN)负责让查询、超点、文本三者交互更顺;文本引导稀疏查询(TSQ)用引用表达覆盖点云关键目标;多对象解耦优化(MDO)把多对象掩码拆成单对象监督,保住每个查询的判别力。三类模块合起来,才让“说几个找几个”在概率上可行,而非凭空保证。
「从单目标掩码到任意数量识别的 3D-GRES」
传统 3D-RES 只能在点云里按文本描述抠出「一个」目标掩码,碰上描述无匹配、或多目标都满足的情况就直接失效,实际落地很受限。3D-GRES 把任务改成从文本 E 和场景 P 里产出掩码 M,M 可以是空、可以含一个、也可以含多个对象,还支持「nothing」类表达来显式验证目标不存在,灵活性和鲁棒性明显更贴近真实交互。 具体管线分四步:引用表达先过 RoBERTa 编码成令牌 𝒯,再投影到 D 维多模态空间并加位置编码;点云用稀疏 3D U-Net 提超点并也投到同维度 D 空间;多查询解耦互动网络(MDIN)用文本引导稀疏查询(TSQ)生成查询,而非随机初始化,靠最远点采样保证查询在点云里稀疏铺开;最后用查询-超点聚合(QSA)和查询-语言聚合(QLA)做跨模态交互,每个查询绑定特定超点,顺带把无关对象分给最近查询。 MDIN 里每个模块都跑一遍 QSA→QLA:QSA 算超点特征 S 与查询嵌入 Qf 的相似分布,查询按分数聚合超点得到 Qs;QLA 再对 Qs 做自注意力加词-查询交叉注意力,产出语言感知特征 Ql 和上下文 Qr,用 MLP 融成最终查询表示。若所有查询置信度都低,就输出 null——这意味着模型真能处理「场景里没有要说那个东西」的情况,而不是硬凑一个掩码。 想验证这套机制,可直接在 MT5 外接 Python 环境复现超点+TSQ 的初始化逻辑,重点看最远点采样后的查询覆盖率;若你做点云标的的另类数据接入,把 D 维投影维度从 256 调到 512 往往能缓解小目标漏检,但显存占用会近乎翻倍,贵金属/外汇高频场景接这类信号须警惕过拟合与滑点高风险。
◍ 在 MT5 里把 3D-GRES 跑成可训练查询
3D-GRES 和之前验证过的 SPFormer、MAFT 最大的不同,是把可训练查询从“训练完就固定”改成按输入数据动态生成。SPFormer 和 MAFT 用的是推理阶段冻结的静态查询,模型只会照备案形态行动;3D-GRES 则基于输入点云现场造查询,覆盖更本地化。我们在 MQL5 实现里沿用这个思路,并把账户状态、持仓编码进模型输入,让嵌入引导入场或离场搜索。 为了让动态查询尽量铺满场景空间,原文引入了综合损失(diversity loss):当查询间平均距离 S_q 趋近 0 时损失为 1,距离拉大则损失趋近 0。我们不在乎损失绝对值,只看梯度方向把查询彼此推开。该计算在 OpenCL 的 DiversityLoss 内核实现,三维任务空间前两维是查询数、第三维是特征向量长度,线程按后两维分组以减少全局内存访问;每个线程只从全局内存读 2 个值,先存原生差值 delt,平方后才进距离和,保留导数所需的原始差形。 类层面新建 CNeuronGRES,核心继承 CNeuronMAFT。Init 方法里先调基类初始化,再把点云转置+卷积对齐查询集规模,逆转置投多模态空间,加位置编码;引用表达用全连接 MLP 生成同维度嵌入,拆成多个语义分量。feedForward 接收点云与引用表达两个指针,解码循环里 QSA 交叉注意力用位置偏置(MAFT 式),QLA 并行自注意力与查询-引用交叉注意力,最后 FeedForward 加残差。 反向传播分 calcInputGradients 与 updateInputWeights。关键优化:初始化时提前替换指针,让自注意力与查询-引用交叉注意力共享同一误差梯度数据,省掉冗余复制,内存占用和训练时间都降。综合损失梯度在查询生成模型各层逆向传播时注入,但特意不进位置编码流,避免污染编码。 整套实现下来,前馈是 3D-GRES 与 MAFT 的共生体,推理阶段可剥掉训练专用操作,对生产决策延迟有正向影响。外汇/贵金属市场高风险,动态查询只是提升覆盖概率,不保证信号胜率,建议直接在 MT5 用附件代码开 OpenCL 跑一遍 DiversityLoss 内核看查询散布效果。
超点反向传播的梯度回收路径
在超点生成模型里做反向传播,第一道坎是解码器层遍历时根本不往模型内部回传梯度。必须手动从键(Key)和值(Value)实体把误差梯度捞回来——每个实体至少挂一个张量,但两条数据路径并不对称:键来自超点模型最后一层输出且带位置编码,值取自倒数第二层且不加位置编码,梯度得沿各自原路退回去。 具体落地时,先算键实体第一层梯度送进内部模型末层,再按层数检查并视情况替换数据缓冲区防丢,随后循环累加到已有梯度上,循环结束返回累计缓冲指针。值实体复用同一套,但缓冲里已有后续层数据,所以直接换缓冲再从并行流收梯度,最后补一个综合误差把超点尽量压全。 查询流处理完会在输入层留一部分梯度,这时替换缓冲并把两路求和,才完成第一个数据源的双流回收。第二个源则先同步指针与 Reference 模型首层梯度缓冲,在末层从键值所有张量收梯度,过位置编码层补向量综合误差保语义多样性,再退到输入层。整套逻辑跑完只给调用方返一个执行结果。 可训练架构基本沿用前作,唯一改动是编码器里描述环境状态的单层,以及训练程序和环境交互逻辑的小修,目的只是把账户状态向量作为引用表达塞进编码器。下面这段 OpenCL 核函数就是多样性损失梯度的一块实现,注意 LOCAL_ARRAY_SIZE 需在宿主端定义,否则编译失败。
__kernel class="type">void DiversityLoss(__global class="kw">const class="type">float *data, __global class="type">float *grad, class="kw">const class="type">int activation, class="kw">const class="type">int add ) { class="kw">const class="type">size_t main = get_global_id(class="num">0); class="kw">const class="type">size_t slave = get_local_id(class="num">1); class="kw">const class="type">size_t dim = get_local_id(class="num">2); class="kw">const class="type">size_t total = get_local_size(class="num">1); class="kw">const class="type">size_t dimension = get_local_size(class="num">2); __local class="type">float Temp[LOCAL_ARRAY_SIZE]; class="kw">const class="type">int shift_main = main * dimension + dim; class="kw">const class="type">int shift_slave = slave * dimension + dim; class="kw">const class="type">int value_main = data[shift_main]; class="kw">const class="type">int value_slave = data[shift_slave]; class="type">float delt = value_main - value_slave; for(class="type">int d = class="num">0; d < dimension; d++) { for(class="type">int i = class="num">0; i < total; i += LOCAL_ARRAY_SIZE) { if(d == dim) { if(i <= slave && (i + LOCAL_ARRAY_SIZE) > slave) { class="type">int k = i % LOCAL_ARRAY_SIZE; class="type">float val = pow(delt, class="num">2.0f) / total; if(isinf(val) || isnan(val)) val = class="num">0; Temp[k] = ((d == class="num">0 && i == class="num">0) ? class="num">0 : Temp[k]) + val; } } barrier(CLK_LOCAL_MEM_FENCE); } } class="kw">const class="type">int ls = min((class="type">int)total, (class="type">int)LOCAL_ARRAY_SIZE); class="type">int count = ls; do { count = (count + class="num">1) / class="num">2; if(slave < count) {
「梯度归约与 GRES 类骨架」
这段 OpenCL 内核收尾在做两件事:先把单线程算出的梯度 gr 用树形归约累加到 Temp[0],再按维度 d 是否等于当前 dim 把结果写回 grad[shift_main]。gr 的计算式为 2 * pow(loss, 2.0f) * delt / total,其中 loss = exp(-Temp[0]),若 gr 出现 NaN 或 Inf 直接置 0,避免训练发散。 归约循环里 count 从 ls 起每次 (count+1)/2 折半,slave 线程只处理自己负责的那一半;当 slave==0 且 d==dim 时,Temp[0] 若非法也清零,随后通过 Deactivation 函数把激活导数叠进主梯度。注意 add>0 走累加分支,否则直接覆盖,这决定了多层反向传播时梯度是否保留历史。 内核之后定义的 CNeuronGRES 类继承自 CNeuronMAFT,保护成员一口气挂了 cReference、cRefKey、cRefValue、cMHRefAttentionOut、cRefAttentionOut 五个 CLayer 对象,明显是为多头参考注意力预留的缓冲结构。CreateBuffers 被声明为虚函数,说明子类可能重写以分配这些层的本地内存。 在 MT5 里打开对应的 .mqh 把这段内核和类声明贴进自定义神经元工程,改一下 LOCAL_ARRAY_SIZE 观察归约步数变化,能直接验证显存栅栏 CLK_LOCAL_MEM_FENCE 是否成为瓶颈。外汇与贵金属模型训练波动剧烈,此类 GPU 核的数值稳定性只代表回测环境表现,实盘仍属高风险。
Temp[slave] += ((slave + count) < ls ? Temp[slave + count] : class="num">0); if(slave + count < ls) Temp[slave + count] = class="num">0; } barrier(CLK_LOCAL_MEM_FENCE); } class="kw">while(count > class="num">1); class="type">float loss = exp(-Temp[class="num">0]); class="type">float gr = class="num">2 * pow(loss, class="num">2.0f) * delt / total; if(isnan(gr) || isinf(gr)) gr = class="num">0; for(class="type">int d = class="num">0; d < dimension; d++) { for(class="type">int i = class="num">0; i < total; i += LOCAL_ARRAY_SIZE) { if(d == dim) { if(i <= slave && (i + LOCAL_ARRAY_SIZE) > slave) { class="type">int k = i % LOCAL_ARRAY_SIZE; Temp[k] = ((d == class="num">0 && i == class="num">0) ? class="num">0 : Temp[k]) + gr; } } barrier(CLK_LOCAL_MEM_FENCE); } class=class="str">"cmt">//--- class="type">int count = ls; do { count = (count + class="num">1) / class="num">2; if(slave < count && d == dim) { Temp[slave] += ((slave + count) < ls ? Temp[slave + count] : class="num">0); if(slave + count < ls) Temp[slave + count] = class="num">0; } barrier(CLK_LOCAL_MEM_FENCE); } class="kw">while(count > class="num">1); if(slave == class="num">0 && d == dim) { if(isnan(Temp[class="num">0]) || isinf(Temp[class="num">0])) Temp[class="num">0] = class="num">0; if(add > class="num">0) grad[shift_main] += Deactivation(Temp[class="num">0],value_main,activation); else grad[shift_main] = Deactivation(Temp[class="num">0],value_main,activation); } barrier(CLK_LOCAL_MEM_FENCE); } } class CNeuronGRES : class="kw">public CNeuronMAFT { class="kw">protected: CLayer cReference; CLayer cRefKey; CLayer cRefValue; CLayer cMHRefAttentionOut; CLayer cRefAttentionOut; class=class="str">"cmt">//--- class="kw">virtual class="type">bool CreateBuffers(class="type">void);