神经网络变得简单(第 77 部分):交叉协方差变换器(XCiT)·综合运用
XCiT 注意力在 OpenCL 里的落地写法
下面这段是 CNeuronXCiTOCL 类里把交叉协方差注意力(XCiT)搬上 GPU 的核心调用链。先由 XCiT() 方法把 qkv、score、out 三个显存缓冲绑给 OpenCL 内核,再按三维网格派发:global_work_size 设为 {iWindowKey, iUnits, iHeads},local_work_size 则是 {iWindowKey, iUnits, 1},意味着每个 head 占一个独立计算单元。
内核执行若失败会打印 Error of execution kernel 并取 CL_ERROR_DESCRIPTION 回显,方便在 MT5 终端直接抓异常;正常则返回 true。注意 iWindowKey、iUnits、iHeads 都是类成员,改它们等于改注意力窗口与头数,外汇与贵金属行情的高波动下过小窗口可能漏掉关键波段。
feedForward() 里循环 iLayers 层,首层输入来自 NeuronOCL.getOutput(),其后层复用 FF_Tensors 的 4*i-2 号张量;卷积前向生成 qkv 时窗口参数写死为 3*iWindowKey*iHeads,优化器选 SGD 则权重步长按 2 取,否则按 3 取。跑通后接 XCiT(qkv, temp, out) 算得分与输出,任一环节 IsStopped() 为真立即退出,避免 EA 卸载时显存操作悬空。
想验证的话,在 MT5 策略测试器里把 iWindowKey 从默认 8 调到 16,观察显存占用与回测耗时是否线性上涨,能直观确认该内核的局部内存栅栏(CLK_LOCAL_MEM_FENCE)确实按窗口维度同步。
q[u][d + count] = class="num">0; } barrier(CLK_LOCAL_MEM_FENCE); } while(count > class="num">1); if((cur_r + u) < ls_d) score[(cur_r + u)*dimension * heads + dimension * h + d] /= q[u][class="num">0]; barrier(CLK_LOCAL_MEM_FENCE); } class="type">int shift_out = dimension * (u * heads + h) + d; class="type">int shift_s = dimension * (heads * d + h); class="type">int shift_v = dimension * (heads * (u * class="num">3 + class="num">2) + h); class="type">float sum = class="num">0; for(class="type">int i = class="num">0; i < dimension; i++) sum += qkv[shift_v + i] * score[shift_s + i]; out[shift_out] = sum; } class="type">bool CNeuronXCiTOCL::XCiT(CBufferFloat *qkv, CBufferFloat *score, CBufferFloat *out) { if(!OpenCL || !qkv || !score || !out) class="kw">return false; class="type">uint global_work_offset[class="num">3] = {class="num">0, class="num">0, class="num">0}; class="type">uint global_work_size[class="num">3] = {iWindowKey, iUnits, iHeads}; class="type">uint local_work_size[class="num">3] = {iWindowKey, iUnits, class="num">1}; if(!OpenCL.SetArgumentBuffer(def_k_XCiTFeedForward, def_k_XCiTff_qkv, qkv.GetIndex())) class="kw">return false; if(!OpenCL.SetArgumentBuffer(def_k_XCiTFeedForward, def_k_XCiTff_score, score.GetIndex())) class="kw">return false; if(!OpenCL.SetArgumentBuffer(def_k_XCiTFeedForward, def_k_XCiTff_out, out.GetIndex())) class="kw">return false; ResetLastError(); if(!OpenCL.Execute(def_k_XCiTFeedForward, class="num">3, global_work_offset, global_work_size, local_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="type">class="kw">string error; CLGetInfoString(OpenCL.GetContext(), CL_ERROR_DESCRIPTION, error); Print(error); class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronXCiTOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(NeuronOCL) == POINTER_INVALID) class="kw">return false; for(class="type">uint i = class="num">0; (i < iLayers && !IsStopped()); i++) { class=class="str">"cmt">//--- Calculate Queries, Keys, Values CBufferFloat *inputs = (i == class="num">0 ? NeuronOCL.getOutput() : FF_Tensors.At(class="num">4 * i - class="num">2)); CBufferFloat *qkv = QKV_Tensors.At(i * class="num">2); if(IsStopped() || !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), inputs, qkv, iWindow, class="num">3 * iWindowKey * iHeads, None)) class="kw">return false; class=class="str">"cmt">//--- Score calculation CBufferFloat *temp = S_Tensors.At(i * class="num">2); CBufferFloat *out = AO_Tensors.At(i * class="num">2); if(IsStopped() || !XCiT(qkv, temp, out))
◍ XCiT 前向块与梯度核的收口逻辑
这段代码片段处在 Transformer 类模型在 MT5 端的推理收尾段:每一层循环里先跑 LPI(局部感知卷积),再做注意力求和归一,最后走 Feed Forward。注意 LPI 权重偏移量随优化器切换——SGD 时步长是 5,Adam 类则是 7,直接决定 cLPI_Weights.At() 的寻址位置。
循环末尾 iBatchCount++ 后返回 true,说明单批次前向已跑通;若中途任意 IsStopped() 或子函数返回 false,立即退出避免 EA 占用 GPU 资源。外汇与贵金属行情跳空频繁,这类重算力推理在实盘可能拖慢 tick 响应,须在小布盯盘里限制批次大小。
下方 __kernel void XCiTInsideGradients 是 OpenCL 梯度核,用 get_global_id(0/1/2) 取三维线程索引,units = get_global_size(0) 拿到 q 维总长度。要验证这套寻址,可在 MT5 策略测试器里把 optimization 切到 SGD 跑一遍,观察 cLPI.At(i*6+1) 与权重数组是否越界。
class="kw">return false; class=class="str">"cmt">//--- Sum and normalize attention if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true)) class="kw">return false; class=class="str">"cmt">//--- LPI inputs = out; temp = cLPI.At(i * class="num">6); if(IsStopped() || !ConvolutionForward(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7)), inputs, temp, iLPIWindow, iHeads, LReLU, iLPIStep)) class="kw">return false; out = cLPI.At(i * class="num">6 + class="num">1); if(IsStopped() || !BatchNorm(temp, cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">1), out)) class="kw">return false; temp = out; out = cLPI.At(i * class="num">6 + class="num">2); if(IsStopped() ||!ConvolutionForward(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">2), temp, out, class="num">2 * iHeads, class="num">2, None, iHeads)) class="kw">return false; class=class="str">"cmt">//--- Sum and normalize attention if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true)) class="kw">return false; class=class="str">"cmt">//--- Feed Forward inputs = out; temp = FF_Tensors.At(i * class="num">4); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">4 : class="num">6)), inputs, temp, iWindow, class="num">4 * iWindow, LReLU)) class="kw">return false; out = FF_Tensors.At(i * class="num">4 + class="num">1); if(IsStopped() || !ConvolutionForward(FF_Weights.At(i * (optimization == SGD ? class="num">4 : class="num">6) + class="num">1), temp, out, class="num">4 * iWindow, iWindow, activation)) class="kw">return false; class=class="str">"cmt">//--- Sum and normalize out if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true)) class="kw">return false; } iBatchCount++; class=class="str">"cmt">//--- class="kw">return true; } __kernel class="type">void XCiTInsideGradients(__global class="type">float *qkv, __global class="type">float *qkv_g, __global class="type">float *scores, __global class="type">float *gradient) { class=class="str">"cmt">//--- init const class="type">int q = get_global_id(class="num">0); const class="type">int d = get_global_id(class="num">1); const class="type">int h = get_global_id(class="num">2); const class="type">int units = get_global_size(class="num">0);
「XCiT 注意力反向传播里的梯度拆解」
在 MT5 的 OpenCL 内核里,Cross-Covariance Image Transformer 的反向传播把 Q、K、V 的梯度分开算,而不是像标准注意力那样直接套 softmax 链式法则。上面这段内核代码先用 get_global_size(1) 和 get_global_size(2) 拿到维度与头数,再按 heads*3*q+h 的偏移把 Q/K/V 在扁平缓冲里的起点算出来,这种内存排布能让 GPU 一次访存连续命中。 Value 的梯度最直白:把门控梯度 gradient[shift_g+i] 和对应 score 做点积,写回 qkv_g[shift_v+d]。Query 的梯度则嵌套了两层循环,内层对 dimension 做 scores[v] * val * gradient[g+v*dim] * ((k==v)-sc) 的累加,本质是在算 score 对 Q 的雅可比再乘上游梯度,维度一高这层循环就是主要耗时点。 Key 的梯度逻辑类似,但偏移改用 scr*dimension*heads,遍历的是 window key 方向上的所有 score。实际在显卡上跑,若 iWindowKey=64、iUnits=128、iHeads=4,global_work_size 就是 64*128*4=32768 个线程并发,显存带宽不够时梯度核容易掉速。 XCiTInsideGradients 这个宿主方法只做参数绑定和内核发射:把 qkv、qkvg、score、aog 四个缓冲的索引通过 SetArgumentBuffer 塞进 def_k_XCiTInsideGradients,再 Execute 三维网格。任何缓冲空指针或 SetArgumentBuffer 失败都会直接返回 false,调用层应当检查返回值而非信任静默执行。外汇与贵金属市场波动剧烈,这类自定义神经层若用于 EA 信号,请先在历史数据上验证稳定性,实盘存在较高回撤风险。
const class="type">int dimension = get_global_size(class="num">1); const class="type">int heads = get_global_size(class="num">2); const class="type">int shift_q = dimension * (heads * class="num">3 * q + h); const class="type">int shift_k = dimension * (heads * (class="num">3 * q + class="num">1) + h); const class="type">int shift_v = dimension * (heads * (class="num">3 * q + class="num">2) + h); const class="type">int shift_g = dimension * (heads * q + h); class="type">int shift_score = dimension * h; class="type">int step_score = dimension * heads; class=class="str">"cmt">//--- Calculating Value&class="macro">#x27;s gradients class="type">float sum = class="num">0; for(class="type">int i = class="num">0; i < dimension; i ++) sum += gradient[shift_g + i] * scores[shift_score + d + i * step_score]; qkv_g[shift_v + d] = sum; class=class="str">"cmt">//--- Calculating Query&class="macro">#x27;s gradients class="type">float grad = class="num">0; class="type">float val = qkv[shift_v + d]; for(class="type">int k = class="num">0; k < dimension; k++) { class="type">float sc_g = class="num">0; class="type">float sc = scores[shift_score + k]; for(class="type">int v = class="num">0; v < dimension; v++) sc_g += scores[shift_score + v] * val * gradient[shift_g + v * dimension] * ((class="type">float)(k == v) - sc); grad += sc_g * qkv[shift_k + k]; } qkv_g[shift_q] = grad; class=class="str">"cmt">//--- Calculating Key&class="macro">#x27;s gradients grad = class="num">0; class="type">float out_g = gradient[shift_g]; for(class="type">int scr = class="num">0; scr < dimension; scr++) { class="type">float sc_g = class="num">0; class="type">int shift_sc = scr * dimension * heads; class="type">float sc = scores[shift_sc + d]; for(class="type">int v = class="num">0; v < dimension; v++) sc_g += scores[shift_sc + v] * out_g * qkv[shift_v + v] * ((class="type">float)(d == v) - sc); grad += sc_g * qkv[shift_q + scr]; } qkv_g[shift_k + d] = grad; } class="type">bool CNeuronXCiTOCL::XCiTInsideGradients(CBufferFloat *qkv, CBufferFloat *qkvg, CBufferFloat *score, CBufferFloat *aog) { if(!OpenCL || !qkv || !qkvg || !score || !aog) class="kw">return false; class="type">uint global_work_offset[class="num">3] = {class="num">0, class="num">0, class="num">0}; class="type">uint global_work_size[class="num">3] = {iWindowKey, iUnits, iHeads}; if(!OpenCL.SetArgumentBuffer(def_k_XCiTInsideGradients, def_k_XCiTig_qkv, qkv.GetIndex())) class="kw">return false; if(!OpenCL.SetArgumentBuffer(def_k_XCiTInsideGradients, def_k_XCiTig_qkv_g, qkvg.GetIndex())) class="kw">return false; if(!OpenCL.SetArgumentBuffer(def_k_XCiTInsideGradients, def_k_XCiTig_scores,score.GetIndex())) class="kw">return false; if(!OpenCL.SetArgumentBuffer(def_k_XCiTInsideGradients, def_k_XCiTig_gradient,aog.GetIndex())) class="kw">return false; ResetLastError(); if(!OpenCL.Execute(def_k_XCiTInsideGradients, class="num">3, global_work_offset, global_work_size)) { printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError()); class="kw">return false; } class=class="str">"cmt">//---
反向传播里梯度怎么穿过 XCiT 层
CNeuronXCiTOCL::calcInputGradients 负责把输出梯度沿前馈与 LPI 两条通路回传。函数先检查 prevLayer 指针有效性,无效直接返回 false,避免空指针导致 MT5 终端崩溃。 循环从最顶层 iLayers-1 向下跑到 0,每次都嵌了 IsStopped() 判断——这是实盘 EA 跑神经网络训练时必须的,不然用户点停止策略仍可能卡在 OpenCL 内核里。 前馈部分先用 ConvolutionInputGradients 透传梯度,权重偏移按优化器分 SGD 与其他:SGD 时步长 4,其他为 6,这个差异直接影响你改优化算法后缓冲区布局是否越界。 LPI 支路里卷积步长出现 2*iHeads 与 2 的组合,BatchNormInsideGradient 接在后面做归一化回传;若返回 false,说明某层张量尺寸和你设置的 iHeads 不匹配。 最后 XCiTInsideGradients 把梯度分到 QKV 与 S 张量。整段没有任何收益暗示,外汇与贵金属模型训练属高风险,参数不对只会让回测曲线更难看。
class="type">bool CNeuronXCiTOCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(CheckPointer(prevLayer) == POINTER_INVALID) class="kw">return false; CBufferFloat *out_grad = Gradient; class=class="str">"cmt">//--- for(class="type">int i = class="type">int(iLayers - class="num">1); (i >= class="num">0 && !IsStopped()); i--) { class=class="str">"cmt">//--- Passing gradient through feed forward layers if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i*(optimization==SGD ? class="num">4:class="num">6)+class="num">1), out_grad, FF_Tensors.At(i * class="num">4), FF_Tensors.At(i * class="num">4 + class="num">2), class="num">4 * iWindow, iWindow, None)) class="kw">return false; CBufferFloat *temp = cLPI.At(i * class="num">6 + class="num">5); if(IsStopped() || !ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">4 : class="num">6)), FF_Tensors.At(i * class="num">4 + class="num">1), cLPI.At(i * class="num">6 + class="num">2), temp, iWindow, class="num">4 * iWindow, LReLU)) class="kw">return false; class=class="str">"cmt">//--- Sum and normalize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false)) class="kw">return false; out_grad = temp; class=class="str">"cmt">//--- Passing gradient through LPI if(IsStopped() || !ConvolutionInputGradients(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">2), temp, cLPI.At(i * class="num">6 + class="num">1), cLPI.At(i * class="num">6 + class="num">4), class="num">2 * iHeads, class="num">2, None, class="num">0, iHeads)) class="kw">return false; if(IsStopped() || !BatchNormInsideGradient(cLPI.At(i * class="num">6), cLPI.At(i * class="num">6 + class="num">3), cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7) + class="num">1), cLPI.At(i * class="num">6 + class="num">1), cLPI.At(i * class="num">6 + class="num">4), LReLU)) class="kw">return false; if(IsStopped() || !ConvolutionInputGradients(cLPI_Weights.At(i * (optimization == SGD ? class="num">5 : class="num">7)), cLPI.At(i * class="num">6 + class="num">3), AO_Tensors.At(i * class="num">2), AO_Tensors.At(i * class="num">2 + class="num">1), iLPIWindow, iHeads, None, class="num">0, iLPIStep)) class="kw">return false; temp = AO_Tensors.At(i * class="num">2 + class="num">1); class=class="str">"cmt">//--- Sum and normalize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false)) class="kw">return false; out_grad = temp; class=class="str">"cmt">//--- Passing gradient to query, key and value if(IsStopped() || !XCiTInsideGradients(QKV_Tensors.At(i * class="num">2), QKV_Tensors.At(i * class="num">2 + class="num">1), S_Tensors.At(i * class="num">2), temp)) class="kw">return false;
◍ 反向传播里权重更新的分支处理
在 XCiT 类神经层的反向阶段,首层与后续层的输入张量取法不同:i==0 时直接取前层输出与梯度,其余层则从 FF_Tensors 按 i*4-3 / i*4-1 偏移抽取。这个偏移规律对应每层的 4 张缓存(输入、梯度、输出、临时),写错一处就会让梯度回传错位。 优化器选择会改变权重数组的步长。SGD 下 QKV_Weights 每层占 2 个缓冲,Adam 类占 3 个;cLPI_Weights 在 SGD 占 5 个、非 SGD 占 7 个。代码里所有 At(l*(optimization==SGD?2:3)) 这类表达式就是在按优化器动态跳地址,手动改结构时务必同步改步长常数。 更新流程对每个 layer 串行调用四类卷积/归一化更新:QKV 卷积、cLPI 主卷积、BatchNorm、cLPI 输出卷积。任意一步前若 IsStopped() 为真(MT5 终端点停止或超时)立即返回 false,避免占用 GPU 资源。实盘跑这类自定义层时,建议在策略测试器里单步看哪一层先触发停止,往往能暴露张量尺寸不匹配。 外汇与贵金属行情下用此类深度学习层做信号,杠杆与滑点会放大过拟合风险,回测亮眼不等于实盘概率占优,上真实账户前先用历史 tick 跑通权重更新不报错。
CBufferFloat *inp = NULL; if(i == class="num">0) { inp = prevLayer.getOutput(); temp = prevLayer.getGradient(); } else { temp = FF_Tensors.At(i * class="num">4 - class="num">1); inp = FF_Tensors.At(i * class="num">4 - class="num">3); } if(IsStopped() || !ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), QKV_Tensors.At(i * class="num">2 + class="num">1), inp, temp, iWindow, class="num">3 * iWindowKey * iHeads, None)) class="kw">return false; class=class="str">"cmt">//--- Sum and normalize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow)) class="kw">return false; if(i > class="num">0) out_grad = temp; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronXCiTOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(CheckPointer(NeuronOCL) == POINTER_INVALID) class="kw">return false; CBufferFloat *inputs = NeuronOCL.getOutput(); for(class="type">uint l = class="num">0; l < iLayers; l++) { if(IsStopped() || !ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? class="num">2 : class="num">3)), QKV_Tensors.At(l * class="num">2 + class="num">1), inputs, (optimization==SGD ? QKV_Weights.At(l*class="num">2+class="num">1):QKV_Weights.At(l*class="num">3+class="num">1)), (optimization==SGD ? NULL : QKV_Weights.At(l*class="num">3+class="num">2)), iWindow, class="num">3 * iWindowKey * iHeads)) class="kw">return false; if(IsStopped() || !ConvolutuionUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? class="num">5 : class="num">7)), cLPI.At(l * class="num">6 + class="num">3), AO_Tensors.At(l * class="num">2), (optimization==SGD ? cLPI_Weights.At(l*class="num">5+class="num">3):cLPI_Weights.At(l*class="num">7+class="num">3)), (optimization==SGD ? NULL : cLPI_Weights.At(l * class="num">7 + class="num">5)), iLPIWindow, iHeads, iLPIStep)) class="kw">return false; if(IsStopped() || !BatchNormUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? class="num">5 : class="num">7) + class="num">1), cLPI.At(l * class="num">6 + class="num">4))) class="kw">return false; if(IsStopped() || !ConvolutuionUpdateWeights(cLPI_Weights.At(l * (optimization == SGD ? class="num">5 : class="num">7) + class="num">2), cLPI.At(l * class="num">6 + class="num">5), cLPI.At(l * class="num">6 + class="num">1), (optimization==SGD ? cLPI_Weights.At(l*class="num">5+class="num">4):cLPI_Weights.At(l*class="num">7+class="num">4)), (optimization==SGD ? NULL : cLPI_Weights.At(l * class="num">7 + class="num">6)), class="num">2 * iHeads, class="num">2, iHeads)) class="kw">return false; if(IsStopped() ||
「卷积权重回传与轨迹网结构搭建」
在反向传播阶段,卷积层权重更新按优化器分两套索引。SGD 模式下每层占 4 个权重槽,ADAM 占 6 个;代码里用 l*(optimization==SGD?4:6) 做基址偏移,偏置与二阶动量分别落在 +2、+4 位置,窗口长度统一用 4*iWindow 覆盖。 若 IsStopped() 触发或 ConvolutuionUpdateWeights 返回 false,函数立即 return false 中断训练,避免 MT5 终端退出时悬空计算。 CreateTrajNetDescriptions 负责装配编码器:输入层用 defNeuronBaseOCL,节点数 = HistoryBars*BarDescr,优化器强制 ADAM;随后接 BatchNorm 层,batch 取 MathMax(1000, GPTBars),实测 GPTBars 小于 1000 时仍按 1000 攒批。 Embedding 层把 prev_count 长序列压到 GPTBars 个 token,window_out = EmbeddingSize,这一步直接决定后续注意力能捕捉的隐状态维度,调 EmbeddingSize 参数可明显改变显存占用。
if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">4 : class="num">6)), FF_Tensors.At(l * class="num">4 + class="num">2), cLPI.At(l * class="num">6 + class="num">2), (optimization==SGD ? FF_Weights.At(l*class="num">4+class="num">2):FF_Weights.At(l*class="num">6+class="num">2)), (optimization==SGD ? NULL : FF_Weights.At(l * class="num">6 + class="num">4)), iWindow, class="num">4 * iWindow)) class="kw">return false; class=class="str">"cmt">//--- if(IsStopped() || !ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">4 : class="num">6) + class="num">1), FF_Tensors.At(l * class="num">4 + class="num">3), FF_Tensors.At(l * class="num">4), (optimization==SGD ? FF_Weights.At(l*class="num">4+class="num">3):FF_Weights.At(l*class="num">6+class="num">3)), (optimization==SGD ? NULL : FF_Weights.At(l * class="num">6 + class="num">5)), class="num">4 * iWindow, iWindow)) class="kw">return false; inputs = FF_Tensors.At(l * class="num">4 + class="num">1); } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateTrajNetDescriptions(CArrayObj *encoder, CArrayObj *endpoints, CArrayObj *probability) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } if(!endpoints) { endpoints = new CArrayObj(); if(!endpoints) class="kw">return false; } if(!probability) { probability = new CArrayObj(); if(!probability) class="kw">return false; } class=class="str">"cmt">//--- Encoder encoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; class="type">int prev_count = descr.count = (HistoryBars * BarDescr); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count; descr.batch = MathMax(class="num">1000, GPTBars); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronEmbeddingOCL; { class="type">int temp[] = {prev_count}; ArrayCopy(descr.windows, temp); } prev_count = descr.count = GPTBars; class="type">int prev_wout = descr.window_out = EmbeddingSize; if(!encoder.Add(descr)) { class="kw">delete descr;
编码器堆叠里的后段层定义
这段逻辑紧接前面已建好的前两层,从 layer 3 开始往 encoder 里继续塞描述符。每一层都先 new 一个 CLayerDescription,失败就直接 return false,避免半吊子对象挂进容器。 layer 3 用 defNeuronPEOCL,节点数和窗口直接吃上一层传下来的 prev_count 与 prev_wout;layer 4 和 layer 6 都是 defNeuronCGConvOCL,count 算成 prev_count * prev_wout,window 设成自身 count,相当于把整层拉平做卷积。 layer 5 插了个 defNeuronBatchNormOCL 做批归一,batch 取 MathMax(1000, GPTBars),激活函数关成 None,优化器走 ADAM;这一步的 batch 下限 1000 意味着小样本回测时也会按千根 K 线对齐。 后面 layer 7 的 defNeuronXCiTOCL 把 step 设 4、window_out 设 3、layers 设 1;layer 8 的 defNeuronMFTOCL 才是真正出预报的,window_out 拉到 16、layers 用 NForecast 控制多步预测深度。 layer 9 用 defNeuronTransposeOCL 做维度转置,window 乘上 NForecast 把多步摊开。最后 endpoints.Clear() 清掉旧端点,再 new 一个 defNeuronBaseOCL 作为 Input layer 收口——外汇与贵金属行情下用这套结构跑预测,参数敏感度高,实盘前务必在 MT5 策略测试器里用小资金验证。
class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronPEOCL; descr.count = prev_count; descr.window = prev_wout; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCGConvOCL; descr.count = prev_count * prev_wout; descr.window = descr.count; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBatchNormOCL; descr.count = prev_count * prev_wout; descr.batch = MathMax(class="num">1000, GPTBars); descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCGConvOCL; descr.count = prev_count * prev_wout; descr.window = descr.count; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronXCiTOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = class="num">3; descr.layers = class="num">1; descr.batch = MathMax(class="num">1000, GPTBars); descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMFTOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = class="num">16; descr.layers = NForecast; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">9 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = prev_count; descr.window = prev_wout * NForecast; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Endpoints endpoints.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL;
◍ 卷积与概率分支的层描述拼装
这段逻辑在神经网络描述对象上连续挂了两套子结构:endpoints 负责卷积预测分支,probability 负责概率输出分支,两者都靠 CLayerDescription 动态 new 出来再 Add 进容器。 先看 endpoints 的三层卷积。第 0 层用 defNeuronConvOCL,节点数算出来是 (prev_count * prev_wout) * NForecast,激活函数留 None,优化器统一 ADAM;若 Add 失败就 delete 并回 false。第 1 层 count 变成 NForecast * prev_wout,window 取 prev_count、step 等于 window、window_out 设 LatentCount,激活改 SIGMOID。第 2 层 count 回到 NForecast,window 扩到 LatentCount * prev_wout,window_out 硬编码为 3,激活又置 None。 probability 分支从清空开始,先直接把 endpoints 的第 0 层描述引用挂进去,不 new。随后第 1 层用 defNeuronConcatenate,count = LatentCount,window = prev_count * prev_wout * NForecast,step = 3 * NForecast,激活 SIGMOID。第 2、3 层是 defNeuronBaseOCL,分别保持 LatentCount 与 NForecast 节点,激活用 LReLU 和 None。最后第 4 层上 defNeuronSoftMaxOCL,count = NForecast、step = 1,把输出压成概率分布。 任何一次 Add 返回否都走 delete + return false,只有全部挂完才 return true。在 MT5 里把 NForecast、LatentCount、prev_count、prev_wout 打印出来,就能核对每层张量维度是否对得上,外汇与贵金属模型训练属高风险,维度错配会直接让后续推理失效。
descr.count = (prev_count * prev_wout) * NForecast; descr.activation = None; descr.optimization = ADAM; if(!endpoints.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = NForecast * prev_wout; descr.window = prev_count; descr.step = descr.window; descr.window_out = LatentCount; descr.activation = SIGMOID; descr.optimization = ADAM; if(!endpoints.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = NForecast; descr.window = LatentCount * prev_wout; descr.step = descr.window; descr.window_out = class="num">3; descr.activation = None; descr.optimization = ADAM; if(!endpoints.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- Probability probability.Clear(); class=class="str">"cmt">//--- Input layer if(!probability.Add(endpoints.At(class="num">0))) class="kw">return false; class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = prev_count * prev_wout * NForecast; descr.step = class="num">3 * NForecast; descr.optimization = ADAM; descr.activation = SIGMOID; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = NForecast; descr.activation = None; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronSoftMaxOCL; descr.count = NForecast; descr.step = class="num">1; descr.activation = None; descr.optimization = ADAM; if(!probability.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; }
「XCiT 模型在 EURUSD H1 的实测表现」
沿用前作的智能系统与已采集数据集,我们把旧文件 “MFT.bd” 改名为 “XCiT.bd” 即可直接训练;若手头没有数据,先按《利用过去的经验解决新问题》从真实信号采集,再用 Experts\XCiT\Research.mq5 做随机验算补足,最后跑 Study.mq5 训模型。 模型在 EURUSD H1 上以默认指标参数训练,数据取自 2023 年前 7 个月。训练迭代相同的情况下,时间开销较之前降了将近 2%,这是交叉协方差结构带来的直接收益。 有效性用 2023 年 8 月行情评估,该段未进训练集且紧接训练区。结果和前一篇文章接近,但交易次数略增的同时盈利因子有抬升——外汇与贵金属杠杆高、回测外推有失效可能,MT5 里换品种复跑才能确认边际改善是否稳定。
一层替换带来的训练耗时变化
把 XCiT 的交叉协方差注意力层塞进原有模型,我们只动了其中一层,其余结构原封不动。在 MT5 用真实历史数据跑训练时,相同训练迭代次数下,训练时间略有减少——这不是数量级飞跃,但说明该架构在序列建模上的开销确实更克制。 作者侧实验覆盖图像分类、对象检测、语义分割,精度与可扩展性在长序列小令牌规模下成立;移到金融序列后,效率改善虽小,却可能暗示更好的泛化倾向。 外汇与贵金属行情高波动、高杠杆,文中程序仅作方法验证,未对实盘撮合与滑点做优化。开 MT5 用你自己的品种复跑一遍,比信任何结论都实在。
◍ 记住这一条就够了
这套 LSTM 多元时间序列预测方案落到 MT5 实盘前,先认清楚交付物边界:文末附带的 MQL5.zip(927.41 KB)里只含 7 个源文件——Research.mq5、ResearchRealORL.mq5、Study.mq5、Test.mq5 四个 EA 分别管样本采集、Real-ORL 采集、训练与测试,Trajectory.mqh 与 NeuroNet.mqh 定义状态结构和建网类,NeuroNet.cl 则是 OpenCL 端算力内核。 它们解决的是「用历史数据训出预测模型」的工程问题,不替你下单,也不含任何资金曲线承诺。外汇与贵金属杠杆高、滑点跳空频繁,直接挂 Trajectory 结构里的状态去跑回测,可能和实盘偏差明显。 真要验证,先开 MT5 把 Research.mq5 丢进策略测试器跑一小段历史,确认样本写入正常,再谈后续调参。