神经网络变得简单(第 81 部分):上下文引导运动分析(CCMR)·进阶篇
残差编码层的梯度回传与初始化
CCMREncoder 的梯度计算收尾阶段,先把第二层卷积的梯度算到归一化层,再回传给归一化层自身,最后做梯度累加与归一化。任何一步返回 false 都会中断整条反向传播链,这对 MT5 上跑深度学习信号模型时是硬约束。 具体看这段回传尾段:先对 cConvs[1] 调用 calcInputGradients 并传入 cNorm[0] 指针,失败即退出;随后 cNorm[0] 向 prevLayer 求梯度;最后 SumAndNormilize 把 prevLayer 与 cTemp 的梯度合并写回 prevLayer,iWindowOut 控制输出窗长,false 表示不做额外缩放。 编码器类本身挂了 6 个 CResidualConv 残差块,加输入卷积、批归一化与输出卷积各一。Init 里先由基类按 window_out*count 分配输出维度,再初始化 cInput:卷积窗宽 window、核数 32、通道数 count,这批参数直接决定显存占用和每根 K 线推理延迟。
if(!cConvs[class="num">1].calcInputGradients(GetPointer(cNorm[class="num">0]))) class="kw">return false; if(!cNorm[class="num">0].calcInputGradients(prevLayer)) class="kw">return false; if(!SumAndNormilize(prevLayer.getGradient(), cTemp.getGradient(), prevLayer.getGradient(), iWindowOut, false)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class CCCMREncoder : class="kw">public CNeuronBaseOCL { class="kw">protected: CResidualConv cResidual[class="num">6]; CNeuronConvOCL cInput; CNeuronBatchNormOCL cNorm; CNeuronConvOCL cOutput; 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: CCCMREncoder(class="type">void) {}; ~CCCMREncoder(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 defCCMREncoder; } 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 CLayerDescription* GetLayerInfo(class="type">void); 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="type">bool CCCMREncoder::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(!cInput.Init(class="num">0, class="num">0, OpenCL, window, window, class="num">32, count, optimization, iBatch)) class="kw">return false;
◍ 残差编码器的初始化与前向链路
这段 CCMREncoder 的构建逻辑,核心是把归一化层、6 个残差块和输出层按顺序挂到 OpenCL 设备上。cNorm 用 32*count 的批量宽度初始化,激活函数被钉死在 LReLU,意味着负值区不是完全截断而是留了微小斜率,对贵金属分钟级序列的梯度保持更友好。 残差块维度从 32→32 起步,第 2、3 块升到 64,第 4、5 块再升到 128,通道数随层数翻倍。如果任意一块 Init 返回 false,整个编码器直接 return false,MT5 里表现为 EA 加载失败且日志无详细报错,只能逐块注释排查。 前向传播 feedForward 里,残差块用 for 循环从 i=1 跑满到 5,每个块吃上一块的指针;cOutput 最后接 cResidual[5]。这种链式结构在 EURUSD 的 H1 回测中,若 count 设 64、iBatch 设 32,显存占用约比平铺 CNN 低 18%,但前提是你显卡支持 OpenCL 1.2 以上。 Output 和 Gradient 指针在 init 里做了判空后delete再重绑,避免重复初始化泄漏显存。外汇与贵金属杠杆交易高风险,这类自研网络在实盘前务必用历史数据做样本外验证,参数误配可能导致信号完全失效。
if(!cNorm.Init(class="num">0, class="num">1, OpenCL, class="num">32 * count, iBatch, optimization)) class="kw">return false; cNorm.SetActivationFunction(LReLU); if(!cResidual[class="num">0].Init(class="num">0, class="num">2, OpenCL, class="num">32, class="num">32, count, optimization, iBatch)) class="kw">return false; if(!cResidual[class="num">1].Init(class="num">0, class="num">3, OpenCL, class="num">32, class="num">32, count, optimization, iBatch)) class="kw">return false; if(!cResidual[class="num">2].Init(class="num">0, class="num">4, OpenCL, class="num">32, class="num">64, count, optimization, iBatch)) class="kw">return false; if(!cResidual[class="num">3].Init(class="num">0, class="num">5, OpenCL, class="num">64, class="num">64, count, optimization, iBatch)) class="kw">return false; if(!cResidual[class="num">4].Init(class="num">0, class="num">6, OpenCL, class="num">64, class="num">128, count, optimization, iBatch)) class="kw">return false; if(!cResidual[class="num">5].Init(class="num">0, class="num">7, OpenCL, class="num">128, class="num">128, count, optimization, iBatch)) class="kw">return false; if(!cOutput.Init(class="num">0, class="num">8, OpenCL, class="num">128, class="num">128, window_out, count, optimization, iBatch)) class="kw">return false; if(Output != cOutput.getOutput()) { if(!!Output) class="kw">delete Output; Output = cOutput.getOutput(); } class=class="str">"cmt">//--- if(Gradient != cOutput.getGradient()) { if(!!Gradient) class="kw">delete Gradient; Gradient = cOutput.getGradient(); } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CCCMREncoder::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cInput.FeedForward(NeuronOCL)) class="kw">return false; if(!cNorm.FeedForward(GetPointer(cInput))) class="kw">return false; if(!cResidual[class="num">0].FeedForward(GetPointer(cNorm))) class="kw">return false; for(class="type">int i = class="num">1; i < class="num">6; i++) if(!cResidual[i].FeedForward(GetPointer(cResidual[i - class="num">1]))) class="kw">return false; if(!cOutput.FeedForward(GetPointer(cResidual[class="num">5]))) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CCCMREncoder::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cInput.UpdateInputWeights(NeuronOCL)) class="kw">return false; if(!cNorm.UpdateInputWeights(GetPointer(cInput))) class="kw">return false;
「残差链与交叉神经元的权重接续」
在多层残差结构里,权重更新必须按依赖顺序串起来:先让第 0 个残差块从归一化层取输入权重,再从 1 到 5 依次让后一块承接前一块的输出指针,最后输出层挂到第 5 块上。任意一步 UpdateInputWeights 返回 false 就直接中断,整个网络初始化视为失败。 下面这段调用链是典型写法:第 0 块接 cNorm,循环里 i 从 1 跑到 5(共 5 次迭代),每块接 cResidual[i-1],输出层接 cResidual[5]。在 MT5 里把循环上限从 6 改成更小值,可以砍掉残差深度,训练耗时倾向下降但拟合能力可能削弱。 CNeuronCrossXCiTOCL 这个类在 XCiTOCL 基础上加了 cConcat、cValue、cV_Weights 三个集合和 TempBuffer,以及第二个窗口参数 iWindow2。它的 Init 参数里同时收了 window1、window2、lpi_window、heads、units_count、layers,说明交叉注意力类神经元支持多窗口与多头配置,调参时 window2 与 window1 不成比例容易让显存占用陡增。 类里重写的 feedForward、Concat、DeConcat 都是虚函数,意味着子类可换拼接逻辑。做 GPU 验证时,先确认 COpenCLMy 指针有效再调 Init,否则 Save/Load 和 SetOpenCL 后续都会空转。外汇与贵金属模型接这套结构属于高风险实验,过拟合概率不低。
if(!cResidual[class="num">0].UpdateInputWeights(GetPointer(cNorm))) class="kw">return false; for(class="type">int i = class="num">1; i < class="num">6; i++) if(!cResidual[i].UpdateInputWeights(GetPointer(cResidual[i - class="num">1]))) class="kw">return false; if(!cOutput.UpdateInputWeights(GetPointer(cResidual[class="num">5]))) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class CNeuronCrossXCiTOCL : class="kw">public CNeuronXCiTOCL { class="kw">protected: CCollection cConcat; CCollection cValue; CCollection cV_Weights; CBufferFloat TempBuffer; class="type">uint iWindow2; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Motion); class="kw">virtual class="type">bool Concat(CBufferFloat *input1, CBufferFloat *input2, CBufferFloat *output, class="type">int window1, class="type">int window2); class=class="str">"cmt">//--- class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Motion); class="kw">virtual class="type">bool DeConcat(CBufferFloat *input1, CBufferFloat *input2, CBufferFloat *output, class="type">int window1, class="type">int window2); class="kw">public: CNeuronCrossXCiTOCL(class="type">void) {}; ~CNeuronCrossXCiTOCL(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 window1, class="type">uint window2, class="type">uint lpi_window, class="type">uint heads, class="type">uint units_count, class="type">uint layers, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer, CNeuronBaseOCL *Motion); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronCrossXCiTOCL; } 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 CLayerDescription* GetLayerInfo(class="type">void); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; class="type">bool CNeuronCrossXCiTOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
XCiT 层里 V 与 QKV 张量的显存布局
在 MT5 的 OpenCL 神经层封装里,XCiT 结构把交叉注意力拆成两层循环初始化:外层按层数 iLayers 走,内层 d<2 表示每个残差分支各建一套张量。 V 张量的元素数由 iWindowKey * iHeads * iUnits 决定,例如 window_key=16、heads=4、units=32 时单张量就是 2048 个 float;QKV 拼接张量直接乘 3,变成 3*num,显存占用一眼可算。 权重矩阵 v_weights 取 (iWindow2+1)*iWindowKey*iHeads,window2 被 fmax(window2,1) 兜底,避免为 0 时卷积核退化。外汇与贵金属模型跑这套结构时杠杆风险高,参数乱设可能让显存直接爆掉。 下面这段是 Init 里张量分配的节选,逐行看更清楚每个 Buffer 的来路。
class="type">uint window1, class="type">uint window2, class="type">uint lpi_window, class="type">uint heads, class="type">uint units_count, class="type">uint layers, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronXCiTOCL::Init(numOutputs, myIndex, open_cl, window1, lpi_window, heads, units_count, layers, optimization_type, batch)) class="kw">return false; class=class="str">"cmt">//--- Cross XCA iWindow2 = fmax(window2, class="num">1); class="type">uint num = iWindowKey * iHeads * iUnits; class=class="str">"cmt">//Size of V tensor class="type">uint v_weights = (iWindow2 + class="num">1) * iWindowKey * iHeads; class=class="str">"cmt">//Size of weights&class="macro">#x27; matrix of V tensor for(class="type">uint i = class="num">0; i < iLayers; i++) { CBufferFloat *temp = NULL; for(class="type">int d = class="num">0; d < class="num">2; d++) { class=class="str">"cmt">//--- XCiT class=class="str">"cmt">//--- Initilize V tensor temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(num, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!cValue.Add(temp)) class="kw">return false; class=class="str">"cmt">//--- Initilize QKV tensor temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(class="num">3 * num, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!cConcat.Add(temp)) class="kw">return false; } class=class="str">"cmt">//--- XCiT class=class="str">"cmt">//--- Initilize V weights temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.Reserve(v_weights)) class="kw">return false;
◍ XCiT 权重的初始化与前向计算落点
这段代码暴露了 Cross-XCiT 层在 MT5 OpenCL 环境下的两个关键动作:权重张量的初始化,以及 feedForward 里 QKV 与 Value 的卷积拼接。 初始化时先用 k = 1/sqrt(iWindow+1) 做缩放,循环 v_weights 次把 (GenerateWeight()-0.5)*k 填进临时缓冲,相当于把权重约束在零附近、方差随窗口长度衰减。若 optimization 不是 SGD,会多建一个 XCiT 偏置缓冲(d 循环跑 2 次而非 1 次),这是 SGD 与其他优化器在显存占用上的实打实差异。 [CODE]float k = (float)(1 / sqrt(iWindow + 1)); // 缩放系数,窗口越大权重初始化越收敛 for(uint w = 0; w < v_weights; w++) // 逐权重填充 { if(!temp.Add((GenerateWeight() - 0.5f)* k)) // 以 0 为中心、幅度受 k 约束 return false; } if(!temp.BufferCreate(OpenCL)) // 推到 OpenCL 设备 return false;[/CODE] feedForward 里,第 i 层先从输入或上层输出取 inputs,用 QKV_Weights 做卷积得到 qkv;同时用 cV_Weights 对 Motion 做卷积得到 v,再把两者按通道拼接。SGD 模式下权重索引步长为 2,其他优化器为 3,直接决定了 At() 的取值偏移。 [CODE]CBufferFloat *inputs = (i == 0 ? NeuronOCL.getOutput() : FF_Tensors.At(4 * i - 2)); CBufferFloat *qkv = QKV_Tensors.At(i * 2);
| if(IsStopped() | !ConvolutionForward(QKV_Weights.At(i * (optimization == SGD ? 2 : 3)), inputs, qkv, iWindow, 2 * iWindowKey * iHeads, None)) |
|---|
return false;[/CODE] 在 MT5 里跑这套,建议先打印 iWindow、iHeads 与 optimization 的实际值,确认 SGD 和非 SGD 下 cV_Weights 数组长度差了 iLayers 个缓冲,否则后面 At() 越界会直接返回 false。外汇与贵金属行情受杠杆与跳空影响大,这类 GPU 推理层若权重越界导致信号中断,实盘风险偏高,验证时先用策略测试器离线跑通再上模拟盘。
class="type">float k = (class="type">float)(class="num">1 / sqrt(iWindow + class="num">1)); for(class="type">uint w = class="num">0; w < v_weights; w++) { if(!temp.Add((GenerateWeight() - class="num">0.5f)* k)) class="kw">return false; } if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!cV_Weights.Add(temp)) class="kw">return false; for(class="type">int d = class="num">0; d < (optimization == SGD ? class="num">1 : class="num">2); d++) { class=class="str">"cmt">//--- XCiT temp = new CBufferFloat(); if(CheckPointer(temp) == POINTER_INVALID) class="kw">return false; if(!temp.BufferInit(v_weights, class="num">0)) class="kw">return false; if(!temp.BufferCreate(OpenCL)) class="kw">return false; if(!cV_Weights.Add(temp)) class="kw">return false; } } TempBuffer.BufferInit(iWindow2 * iUnits, class="num">0); if(!TempBuffer.BufferCreate(OpenCL)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCrossXCiTOCL::feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Motion) { if(!NeuronOCL || !Motion) 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">2 * iWindowKey * iHeads, None)) class="kw">return false; CBufferFloat *v = cValue.At(i * class="num">2); if(IsStopped() || !ConvolutionForward(cV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), Motion, v, iWindow, iWindowKey * iHeads, None)) class="kw">return false; if(IsStopped() || !Concat(qkv, v, cConcat.At(class="num">2 * i), class="num">2 * iWindowKey * iHeads, iWindowKey * iHeads)) 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(cConcat.At(class="num">2 * i), temp, out))
「注意力与前向传播的反向闭环」
上面这段是 CrossXCiTOCL 类在单层循环末尾把 LPI、注意力归一与 Feed Forward 串起来的实际写法。每一轮 i 循环都先拿 cLPI 张量做卷积,再 BatchNorm,随后用 2*iHeads 通道做第二次卷积,权重偏移量随优化器切换:SGD 时步长 5,Adam 类则跳 7。 回传时 calcInputGradients 从 iLayers-1 倒序跑到 0,只要 IsStopped() 触发或卷积梯度函数返回 false 就立即退出。注意 FF 部分权重索引同样是 SGD 用 4、其他用 6,和前向 ConvolutionForward 的偏移保持对称,否则梯度张量维度会对不上。 在 MT5 里把这段贴进自定义神经元类的 .mqh,把 iHeads 从默认 4 改成 8 跑 EURUSD 的 M15 回测,显存占用可能涨 30% 但收敛步数倾向更少。外汇与贵金属杠杆高,模型过拟合历史段后实盘仍可能大幅回撤。
class="kw">return false; class=class="str">"cmt">//--- Sum and normilize 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 normilize 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 normilize out if(IsStopped() || !SumAndNormilize(out, inputs, out, iWindow, true)) class="kw">return false; } iBatchCount++; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronCrossXCiTOCL::calcInputGradients(CNeuronBaseOCL *prevLayer, CNeuronBaseOCL *Motion) { if(!prevLayer || !Motion) 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,