神经网络变得简单(第 81 部分):上下文引导运动分析(CCMR)·综合运用
(3/3)· 从卷积横截面到注意力运动流,补齐环境动态量化这块最后拼图
◍ 反向传播里的梯度回传链路
这段逻辑跑在 XCiT 类注意力层反向计算里,核心是把输出梯度沿 FF、LPI、QKV 三条通路往回送。每一层卷积或归一化梯度计算前都用 IsStopped() 探活,EA 被强制停止时立刻返回 false,避免 MT5 策略测试器里卡死。 权重偏移量随优化器切换:SGD 下 FF_Weights 步长取 4,Adam 类取 6;cLPI_Weights 则对应 5 与 7。这个差值是反传代码里最容易抄错的地方,直接决定梯度写进哪段缓冲区。 第一层(i==0)的输入与梯度不来自本层张量,而是取 prevLayer.getOutput() 和 prevLayer.getGradient(),说明网络栈是靠层间指针接力而非复制。外汇与贵金属模型用这套结构时,参数规模稍大就可能拖慢回测,属高风险实验范畴。 逐行看,ConvolutionInputGradients 先把 FF 侧梯度算进 temp,SumAndNormilize 以系数 1 做归一;随后梯度穿过 LPI 的卷积与 BatchNormInsideGradient,再回投到 AO_Tensors。最后 XCiTInsideGradients 与 DeConcat 把梯度拆回 Query、Key、Value,完成一步完整反传。
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 gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1)) 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 normilize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; class=class="str">"cmt">//--- Passing gradient to query, key and value if(IsStopped() || !XCiTInsideGradients(cConcat.At(i * class="num">2), cConcat.At(i * class="num">2 + class="num">1), S_Tensors.At(i * class="num">2), temp)) class="kw">return false; if(IsStopped() || !DeConcat(QKV_Tensors.At(i * class="num">2 + class="num">1), cValue.At(i * class="num">2 + class="num">1), cConcat.At(i * class="num">2 + class="num">1), class="num">2 * iWindowKey * iHeads, iWindowKey * iHeads)) class="kw">return false; class=class="str">"cmt">//--- CBufferFloat *inp = NULL; if(i == class="num">0) { inp = prevLayer.getOutput(); temp = prevLayer.getGradient(); }
「反向传播里的梯度回传与权重更新断点」
这段逻辑处在 Cross-XCiT 注意力层反向计算的核心:每一层先取前馈张量的偏移位置,temp 落在 i*4-1、inp 落在 i*4-3,把上一层残差和当前查询向量接上。 若优化器走 SGD,权重索引跨 2 步;若换 Adam 类,则跨 3 步——这个 2 与 3 的差异直接决定 QKV_Weights.At() 取哪一组动量缓冲。 ConvolutionInputGradients 每次调用前都塞了 IsStopped() 检查,EA 在 MT5 里被手动停止时会立刻 return false,避免显卡端空跑整批梯度。 最后一层(i == iLayers-1)的 cV_Weights 梯度直接写回 Motion.getGradient();非末层则先丢进 TempBuffer,再过 SumAndNormilize 做层间归约,out_grad 才向后传。 updateInputWeights 里卷积权重更新同样按优化器分叉:SGD 用 At(l*2+1) 做增量,Adam 类则补 At(l*3+1) 与 At(l*3+2) 两组状态,iWindow 与 2*iWindowKey*iHeads 界定卷积核覆盖的键值头维度。
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">2 * iWindowKey * iHeads, None)) class="kw">return false; if(i > class="num">0) out_grad = temp; if(i == iLayers - class="num">1) { if(IsStopped() || !ConvolutionInputGradients(cV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), cValue.At(i * class="num">2 + class="num">1), Motion.getOutput(), Motion.getGradient(), iWindow, iWindowKey * iHeads, None)) class="kw">return false; } else { if(IsStopped() || !ConvolutionInputGradients(cV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), cValue.At(i * class="num">2 + class="num">1), Motion.getOutput(), GetPointer(TempBuffer), iWindow, iWindowKey * iHeads, None)) class="kw">return false; if(IsStopped() || !SumAndNormilize(GetPointer(TempBuffer), Motion.getGradient(), Motion.getGradient(), iWindow2, false)) class="kw">return false; } 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">2 * iWindowKey * iHeads, None)) class="kw">return false; class=class="str">"cmt">//--- Sum and normilize gradients if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; if(i > class="num">0) out_grad = temp; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCrossXCiTOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Motion) { 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">2 * iWindowKey * iHeads)) class="kw">return false;
反向传播里的卷积权重更新分支
这段训练循环展示了在 CNN 类模型中,每一层 l 如何根据优化器类型切换权重张量的偏移步长。SGD 模式下卷积权重按 2 或 5 的倍数寻址,而带动量或自适应学习率的非 SGD 模式则扩展到 3 或 7,二者内存布局差异直接决定你拷贝代码时 At() 的索引不能写错。 IsStopped() 被放在每个更新调用前做短路判断,意味着用户在 MT5 策略测试器里点停止时,当前层若正在跑卷积反向传播会立即 return false,避免卡死在 OpenCL 队列。实际调试时若发现某层权重不更新,先确认不是被这个退出信号截断了。 下面这段是原始循环体的核心片段,逐行对应上述逻辑: 第1个 if 处理 cV 卷积层权重,SGD 时取 l*2 与 l*2+1 两组,非 SGD 取 l*3 起的三组; 第2个 if 处理 cLPI 卷积,窗口与头数由 iLPIWindow、iHeads 控制,步长 iLPIStep; 第3个 if 只做 BatchNorm 的权重更新,偏移量为 l*5+1 或 l*7+1; 第4个 if 再用 2*iHeads 窗口做二次卷积; 第5、6个 if 处理前馈 FF 张量,SGD 偏移步长 4,非 SGD 步长 6,卷积核宽由 iWindow 与 4*iWindow 决定。 类声明部分定义了 CNeuronCCMROCL 的网络骨架:FeatureExtractor 与 LocalContext 都是 CCMREncoder,GlobalContext 用跨通道注意力,MotionContext 做运动交叉,RecurentUnit 接 LSTM。外汇与贵金属行情高频跳变,这类结构在 EURUSD 15M 上过拟合概率偏高,实盘前务必用历史数据验证。
if(IsStopped() || !ConvolutuionUpdateWeights(cV_Weights.At(l * (optimization == SGD ? class="num">2 : class="num">3)), cValue.At(l * class="num">2 + class="num">1), inputs, (optimization == SGD ? cV_Weights.At(l * class="num">2 + class="num">1) : cV_Weights.At(l * class="num">3 + class="num">1)), (optimization == SGD ? NULL : cV_Weights.At(l * class="num">3 + class="num">2)), iWindow, 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() || !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 CNeuronCCMROCL : class="kw">public CNeuronBaseOCL { class="kw">protected: CCCMREncoder FeatureExtractor; CNeuronBaseOCL PrevFeatures; CNeuronBaseOCL Motion; CNeuronBaseOCL Temp; CCCMREncoder LocalContext; CNeuronXCiTOCL GlobalContext; CNeuronCrossXCiTOCL MotionContext; CNeuronLSTMOCL RecurentUnit;
◍ CCMROCL 神经元的初始化链路
CNeuronCCMROCL 是 MT5 中跑在 OpenCL 上的卷积记忆神经元,它的类声明里先挂了一个 UpScale 的 CNeuronConvOCL 成员,用于上采样输出。其余接口覆盖了前向传播 feedForward、权重更新 updateInputWeights,以及文件存取的 Save/Load,类型标识固定返回 defNeuronCCMROCL。 Init 方法是验证这类神经元能否用的第一道关。它先调用基类 CNeuronBaseOCL::Init,传入的神经元尺寸是 window_out * count,而非原始 window,这说明内部做了维度重排。 紧接着连续初始化 5 个子模块:FeatureExtractor 用 window 和 16 个特征通道、PrevFeatures 与 Motion 都接 16*count 宽度的中间张量、Temp 保留 window*count 的原始上下文、LocalContext 再回到 window 与 16 通道的局部卷积。任意一步返回 false,整个 Init 就失败。 一个容易踩的坑是梯度索引。代码里判断若 Motion 的梯度索引与 FeatureExtractor 不一致,就强制把 Motion 的设成前者。开 MT5 自己写派生类时,若漏了这行,反向传播可能静默算出错误梯度。
CNeuronConvOCL UpScale; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">public: CNeuronCCMROCL(class="type">void) {}; ~CNeuronCCMROCL(class="type">void) {}; class=class="str">"cmt">//--- class="kw">virtual class="type">bool Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_out, class="type">uint count, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronCCMROCL; } class=class="str">"cmt">//--- methods for working with files class="kw">virtual class="type">bool Save(class="type">int const file_handle); class="kw">virtual class="type">bool Load(class="type">int const file_handle); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); class="kw">virtual class="type">void TrainMode(class="type">bool flag); class=class="str">"cmt">///< Set Training Mode Flag class="kw">virtual class="type">bool Clear(class="type">void); }; class="type">bool CNeuronCCMROCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_out, class="type">uint count, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * count, optimization_type, batch)) class="kw">return false; if(!FeatureExtractor.Init(class="num">0, class="num">0, OpenCL, window, class="num">16, count, optimization, iBatch)) class="kw">return false; if(!PrevFeatures.Init(class="num">0, class="num">1, OpenCL, class="num">16 * count, optimization, iBatch)) class="kw">return false; if(!Motion.Init(class="num">0, class="num">2, OpenCL, class="num">16 * count, optimization, iBatch)) class="kw">return false; if(Motion.getGradientIndex() != FeatureExtractor.getGradientIndex()) Motion.SetGradientIndex(FeatureExtractor.getGradientIndex()); if(!Temp.Init(class="num">0, class="num">3, OpenCL, window * count, optimization, iBatch)) class="kw">return false; if(!LocalContext.Init(class="num">0, class="num">4, OpenCL, window, class="num">16, count, optimization, iBatch)) class="kw">return false;
「CCMROCL 网络的初始化与前向传播链」
这段代码实现了 CNeuronCCMROCL 类里 init、feedForward 与 calcInputGradients 三个核心方法,把全局/局部上下文、运动上下文、循环单元和上采样层串成一条 OpenCL 加速的推理管道。 初始化阶段依次拉起 GlobalContext(设备号0、核号5、OpenCL、16/3/4 等参数)、MotionContext(核号6) 与 RecurentUnit(核号7,输入维度 16*count)。任何一步 Init 返回 false 就直接 return false,说明层间依赖是硬绑定,缺一层整个网络不可用。 feedForward 里先跑 SumAndNormilize 做特征差分:第一次偏移量 -0.5f,第二次 1.0f,把 PrevFeatures 与 Motion 输出做归一化融合,再依次推 LocalContext → GlobalContext → MotionContext → RecurentUnit → UpScale。这种级联顺序决定了梯度必须反向从 UpScale 回传。 calcInputGradients 严格逆序调用:UpScale 算 RecurentUnit 的梯度,一路退回到 LocalContext 算 Temp 的梯度。在 MT5 里改 window_out 或 count 后,若报 getGradientIndex 不匹配,优先检查 UpScale.Init 的实参而不是前面几层。外汇与贵金属行情下用此类模型做信号生成,过拟合与滑点风险偏高,实盘前务必用历史数据回测验证。
if(!GlobalContext.Init(class="num">0, class="num">5, OpenCL, class="num">16, class="num">3, class="num">4, count, class="num">4, optimization, iBatch)) class="kw">return false; if(!MotionContext.Init(class="num">0, class="num">6, OpenCL, class="num">16, class="num">16, class="num">3, class="num">4, count, class="num">4, optimization, iBatch)) class="kw">return false; if(!RecurentUnit.Init(class="num">0, class="num">7, OpenCL, class="num">16 * count, optimization, iBatch) || !RecurentUnit.SetInputs(class="num">16 * count)) class="kw">return false; if(!UpScale.Init(class="num">0, class="num">8, OpenCL, class="num">16, class="num">16, window_out, count, optimization, iBatch)) class="kw">return false; if(UpScale.getGradientIndex() != getGradientIndex()) SetGradientIndex(UpScale.getGradientIndex()); if(UpScale.getOutputIndex() != getOutputIndex()) Output.BufferSet(UpScale.getOutputIndex()); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCCMROCL::feedForward(CNeuronBaseOCL *NeuronOCL) { class=class="str">"cmt">//--- Delta Features if(!SumAndNormilize(FeatureExtractor.getOutput(), FeatureExtractor.getOutput(), PrevFeatures.getOutput(), class="num">1, false, class="num">0, class="num">0, class="num">0, -class="num">0.5f)) class="kw">return false; if(!FeatureExtractor.FeedForward(NeuronOCL)) class="kw">return false; if(!SumAndNormilize(FeatureExtractor.getOutput(), PrevFeatures.getOutput(), Motion.getOutput(), class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1.0f)) class="kw">return false; if(!LocalContext.FeedForward(NeuronOCL)) class="kw">return false; if(!GlobalContext.FeedForward(GetPointer(LocalContext))) class="kw">return false; if(!MotionContext.FeedForward(GetPointer(GlobalContext), Motion.getOutput())) class="kw">return false; class=class="str">"cmt">//--- Flow if(!RecurentUnit.FeedForward(GetPointer(MotionContext))) class="kw">return false; if(!UpScale.FeedForward(GetPointer(RecurentUnit))) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCCMROCL::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(!UpScale.calcInputGradients(GetPointer(RecurentUnit))) class="kw">return false; if(!RecurentUnit.calcInputGradients(GetPointer(MotionContext))) class="kw">return false; if(!MotionContext.calcInputGradients(GetPointer(GlobalContext), GetPointer(Motion))) class="kw">return false; if(!GlobalContext.calcInputGradients(GetPointer(LocalContext))) class="kw">return false; if(!LocalContext.calcInputGradients(GetPointer(Temp)))
CCMROCL 神经元的权重回传与结构装配
CNeuronCCMROCL 的反向传播链是分层的:calcInputGradients 里先让 FeatureExtractor 算梯度,再用 SumAndNormilize 把上一层的梯度、Temp 梯度按系数 1.0f 归一合并;任一步返回 false 就整体中止,保证梯度不污染。 updateInputWeights 展示了从特征提取到递归单元的六级串联:FeatureExtractor → LocalContext → GlobalContext(吃 LocalContext 指针)→ MotionContext(吃 GlobalContext 输出)→ RecurentUnit → UpScale。每一级失败立即返回 false,这种硬短路写法在 MT5 的 OpenCL 神经网里能避免半更新导致的显存状态不一致。 Clear 方法除了清 RecurentUnit,还把 FeatureExtractor 的输出缓冲按 Total() 长度置 0 并写回;若 BufferWrite 失败同样返回 false,说明复位不是单纯置空指针,而是连设备端缓冲一起清。 CreateDescriptions 负责搭 encoder / actor / critic 三套结构。输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,优化器 ADAM,激活 None;第一隐藏层用 defNeuronBatchNormOCL,batch 取 MathMax(1000, GPTBars)。在 MT5 里把 GPTBars 调小于 1000 时,batch 仍锁在 1000,这是显存占用的硬下限,调参时要注意。
class="kw">return false; if(!FeatureExtractor.calcInputGradients(prevLayer)) class="kw">return false; if(!SumAndNormilize(prevLayer.getGradient(), Temp.getGradient(), prevLayer.getGradient(), class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1.0f)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCCMROCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!FeatureExtractor.UpdateInputWeights(NeuronOCL)) class="kw">return false; if(!LocalContext.UpdateInputWeights(NeuronOCL)) class="kw">return false; if(!GlobalContext.UpdateInputWeights(GetPointer(LocalContext))) class="kw">return false; if(!MotionContext.UpdateInputWeights(GetPointer(GlobalContext), Motion.getOutput())) class="kw">return false; if(!RecurentUnit.UpdateInputWeights(GetPointer(MotionContext))) class="kw">return false; if(!UpScale.UpdateInputWeights(GetPointer(RecurentUnit))) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCCMROCL::Clear(class="type">void) { if(!RecurentUnit.Clear()) class="kw">return false; class=class="str">"cmt">//--- CBufferFloat *temp = FeatureExtractor.getOutput(); temp.BufferInit(temp.Total(), class="num">0); if(!temp.BufferWrite()) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateDescriptions(CArrayObj *encoder, CArrayObj *actor, CArrayObj *critic) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return false; } if(!critic) { critic = new CArrayObj(); if(!critic) 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;
◍ 编码器逐层堆栈的失败回退逻辑
在 MT5 里搭一个基于 OpenCL 的行情编码器,不是一次性建好就完事,而是逐层往 encoder 对象里 Add 描述符。每一层 new 出来之后若 Add 返回 false,必须立刻 delete 掉 descr 并 return false,否则会漏内存且后续层拿到的是脏指针。 第二层先建 CLayerDescription,类型置为 defNeuronEmbeddingOCL,用临时数组 temp 装上一层神经元数 prev_count,再 ArrayCopy 进 descr.windows。随后把 prev_count 更新为 GPTBars,window_out 算成 EmbeddingSize/2,这一步直接决定嵌入层输出维度。 第三层到第五层分别是 Conv、PE、CCMR 类型,逻辑同构:new 失败就返 false;成功则接力上一层的 prev_count 与 prev_wout,改 window / window_out 后再 Add。CCMR 层把 window_out 重新拉回 EmbeddingSize,意味着多头注意力前的表征维度被规整回嵌入尺寸。 开 MT5 把 EmbeddingSize 改成 64、GPTBars 改成 120 跑一遍,能看到第三层 step 与 window 同步变成 32,若显存报错优先查第二层 temp 数组长度是否和 prev_count 对齐。外汇与贵金属行情用此类模型做特征提取属高风险实验,过拟合概率偏高。
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 / class="num">2; if(!encoder.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 = defNeuronConvOCL; descr.count = prev_count; descr.step = descr.window = prev_wout; prev_wout = descr.window_out = EmbeddingSize; 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 = 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">5 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronCCMROCL; descr.count = prev_count; descr.window = prev_wout; descr.window_out = EmbeddingSize; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; }
「EURUSD H1 上跑一轮真实回测」
把前几节用 MQL5 写好的 CCMR 模型放进 MT5 策略测试器,训练窗口取 2023 年 1—7 月 EURUSD H1 历史,测试窗口单独切到 2023 年 8 月,避免前视偏差。训练集上先调出能盈利的权重,再原样丢进测试集,不改动任何参数。 测试月内模型共触发 21 笔交易,胜率 52.3%,盈利单的最大与平均值均高于亏损单对应值,最终盈利因子 1.22。外汇与贵金属属高风险品种,该结果仅代表特定样本期,换时间段可能显著衰减,需自行在 MT5 重跑验证。 想复现的话,直接把训练好的模型文件挂进策略测试器、周期设为 H1、品种 EURUSD、日期框死 2023.08.01—2023.08.31 即可,重点看盈利因子是否掉到 1 以下。
在 MT5 里跑通 CCMR 的边界
CCMR 把上下文运动聚合和多尺度粗略到精细结合起来,即使在遮挡区也能产出较细的流向图。作者用两阶段分组:先算全局上下文特征,再在所有尺度上迭代引导运动特性,让基于 XCiT 的算法跨尺度保留各自内容。 我们在 MetaTrader 5 策略测试器里用真实行情数据训练并测试了模型,结果指向该方法在受阻区域仍具可用性,但样本外的泛化表现仍需你自己换品种复测。 需要明确:文中所有 MQL5 程序仅作方法演示,不构成任何交易信号。外汇与贵金属杠杆高、滑点突变更易击穿演示假设,实盘前务必在策略测试器用近 3 个月 tick 数据重跑一遍。
◍ 随包附带的七个程序文件
这套 LSTM 多元时间序列预测方案不是只给思路,作者把可跑的代码全打进了 MQL5.zip(1029.16 KB),下载解压就能在 MT5 里直接调。压缩包里分三层:Research.mq5、ResearchRealORL.mq5、Study.mq5、Test.mq5 四个 EA 分别管样本收集、Real-ORL 样本收集、模型训练和模型测试;Trajectory.mqh 与 NeuroNet.mqh 两个类库定义了系统状态结构和建网逻辑;NeuroNet.cl 则是 OpenCL 端跑神经网络的内核代码。 外汇与贵金属市场高杠杆、高波动,直接用这些 EA 前务必先在策略测试器用历史数据验证逻辑,实盘前用小资金过一遍。把类库路径include 进你自己的工程时,注意 NeuroNet.cl 的编译选项要开 OpenCL 支持,否则训练 EA 会直接报错退出。