交易中的神经网络:降低锐度强化变换器效率(SAMformer)·进阶篇
「卷积权重epsilon的本地归约写法」
在 MT5 的 OpenCL 内核里算卷积层权重 epsilon 时,核心是先按本地线程把梯度平方累进 __local float temp[LOCAL_ARRAY_SIZE],再用折半归约把各段平方和并成单值。代码里 LOCAL_ARRAY_SIZE 是编译期常量,本地工作组大小不能超过它,否则越界。
归约循环用 do { count = (count+1)/2; ... } while(count>1); 把 temp 数组从长度 ls 逐步压到 temp[0],最后 norm = sqrt(temp[0]) 得到梯度的二范数。分母里写死 1.2e-7 做数值保护,避免除零导致 NaN 扩散到 matrix_epsw。
实际在显卡上跑这套内核,若 inputs 超过 256 且 LOCAL_ARRAY_SIZE 设 256,本地归约能省掉约 inputs - 1 次全局内存回写,显存带宽压力倾向明显下降。外汇与贵金属模型训练属高风险场景,回测参数漂移可能让权重更新失效。
别把本地数组当无限用
工作组维度 get_local_size(0)-1 被存成 window_in,但 temp 长度绑定 LOCAL_ARRAY_SIZE 而非 window_in。若主机端派发的工作组大于该常量,内核会静默写坏相邻本地内存,MT5 策略测试器可能只报 CL_EXEC_STATUS_ERROR。LOCAL_ARRAY_SIZE 必须在 clBuildProgram 前用 -D 宏对齐。
__local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE]; const class="type">int ls = min((class="type">int)inputs, (class="type">int)LOCAL_ARRAY_SIZE); const class="type">int shift_w = out * (inputs + class="num">1) + inp; const class="type">class="kw">float w =IsNaNOrInf(matrix_w[shift_w],class="num">0); class="type">class="kw">float grad = fabs(w) * IsNaNOrInf(matrix_g[out],class="num">0) * (inputs == inp ? class="num">1.0f : IsNaNOrInf(matrix_i[inp],class="num">0)); const class="type">int local_shift = inp % ls; for(class="type">int i = class="num">0; i <= inputs; i += ls) { if(i <= inp && inp < (i + ls)) temp[local_shift] = (i == class="num">0 ? class="num">0 : temp[local_shift]) + IsNaNOrInf(grad * grad,class="num">0); barrier(CLK_LOCAL_MEM_FENCE); } class=class="str">"cmt">//--- class="type">int count = ls; do { count = (count + class="num">1) / class="num">2; if(inp < count) temp[inp] += ((inp + count) < inputs ? IsNaNOrInf(temp[inp + count],class="num">0) : class="num">0); if(inp + count < inputs) temp[inp + count] = class="num">0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > class="num">1); class="type">class="kw">float norm = sqrt(IsNaNOrInf(temp[class="num">0],class="num">0)); class="type">class="kw">float epsw = IsNaNOrInf(w * w * grad * rho / (norm + class="num">1.2e-7), w); class=class="str">"cmt">//--- matrix_epsw[shift_w] = epsw; } __kernel class="type">void CalcEpsilonWeightsConv(__global const class="type">class="kw">float *matrix_w, __global const class="type">class="kw">float *matrix_g, __global const class="type">class="kw">float *matrix_i, __global class="type">class="kw">float *matrix_epsw, const class="type">int inputs, const class="type">class="kw">float rho, const class="type">int step ) { class=class="str">"cmt">//--- const class="type">size_t inp = get_local_id(class="num">0); const class="type">size_t window_in = get_local_size(class="num">0) - class="num">1; const class="type">size_t out = get_global_id(class="num">1); const class="type">size_t window_out = get_global_size(class="num">1); const class="type">size_t v = get_global_id(class="num">2); const class="type">size_t variables = get_global_size(class="num">2); __local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE];
SAM 神经元里的梯度规约与权重更新内核
这段 OpenCL 内核在做一件具体的事:把卷积类神经元的局部梯度按窗口累加,再做一次并行规约求平方和,最后算出带平滑项的权重epsilon。外汇与贵金属行情具有高杠杆高风险,这类模型只适合在 MT5 策略测试器里做离线验证,切勿直接用于实盘盲跟。 先看局部内存的边界处理:ls 取窗口长度与本地数组大小的最小值,local_shift 用 inp % ls 把全局索引映射到本地暂存。循环里每跨 ls 步就做一次 temp[local_shift] 的梯度平方累加,并用 barrier(CLK_LOCAL_MEM_FENCE) 保证工作组内同步,避免竞态。 do-while 那段是经典的双调规约:count 从 ls 起每次减半,inp 小于 count 的线程把后半段值加进来并清零,反复横跳直到 count 只剩 1。最终 norm 等于 temp[0] 开根号,也就是这批梯度的 L2 范数。 epsw 的计算式为 w*w*grad*rho/(norm+1.2e-7),分母加 1.2e-7 是为防零除的平滑项,rho 来自上层 SAM 神经元的 fRho 成员。概率上,这种带范数归一的项在骤变行情里会压住梯度爆炸,但也可能拖慢对突破的响应。 类声明里 CNeuronBaseSAMOCL 继承自 CNeuronBaseOCL,多了 cWeightsSAM 缓冲和 fRho 字段,三个虚函数 calcEpsilonWeights / feedForwardSAM / updateInputWeights 把上面内核嵌回了 MQL5 的面向对象训练框架,Init 里必须传 rho 和优化类型才能跑通。
const class="type">int ls = min((class="type">int)(window_in + class="num">1), (class="type">int)LOCAL_ARRAY_SIZE); const class="type">int shift_w = (out + v * window_out) * (window_in + class="num">1) + inp; const class="type">int total = (inputs - window_in + step - class="num">1) / step; const class="type">int shift_out = v * total * window_out + out; const class="type">int shift_in = v * inputs + inp; const class="type">class="kw">float w = IsNaNOrInf(matrix_w[shift_w], class="num">0); class="type">class="kw">float grad = class="num">0; for(class="type">int t = class="num">0; t < total; t++) { if(inp != window_in && (inp + t * step) >= inputs) break; class="type">class="kw">float g = IsNaNOrInf(matrix_g[t * window_out + shift_out],class="num">0); class="type">class="kw">float i = IsNaNOrInf(inp == window_in ? class="num">1.0f : matrix_i[t * step + shift_in],class="num">0); grad += IsNaNOrInf(g * i,class="num">0); } grad *= fabs(w); const class="type">int local_shift = inp % ls; for(class="type">int i = class="num">0; i <= inputs; i += ls) { if(i <= inp && inp < (i + ls)) temp[local_shift] = (i == class="num">0 ? class="num">0 : temp[local_shift]) + IsNaNOrInf(grad * grad,class="num">0); barrier(CLK_LOCAL_MEM_FENCE); } class=class="str">"cmt">//--- class="type">int count = ls; do { count = (count + class="num">1) / class="num">2; if(inp < count) temp[inp] += ((inp + count) < inputs ? IsNaNOrInf(temp[inp + count],class="num">0) : class="num">0); if(inp + count < inputs) temp[inp + count] = class="num">0; barrier(CLK_LOCAL_MEM_FENCE); } while(count > class="num">1); class="type">class="kw">float norm = sqrt(IsNaNOrInf(temp[class="num">0],class="num">0)); class="type">class="kw">float epsw = IsNaNOrInf(w * w * grad * rho / (norm + class="num">1.2e-7),w); class=class="str">"cmt">//--- matrix_epsw[shift_w] = epsw; } class CNeuronBaseSAMOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">class="kw">float fRho; CBufferFloat cWeightsSAM; class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcEpsilonWeights(CNeuronBaseSAMOCL *NeuronOCL); class="kw">virtual class="type">bool feedForwardSAM(CNeuronBaseSAMOCL *NeuronOCL); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">public: CNeuronBaseSAMOCL(class="type">void) {}; ~CNeuronBaseSAMOCL(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 numNeurons, class="type">class="kw">float rho, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//---
◍ SAM 神经元类的权重与持久化接口
CNeuronBaseSAMOCL 在基类之上扩展了自归一化相关的权重缓冲 cWeightsSAM,并通过虚函数把 Save / Load / Type / Activation 等暴露给上层网络调用。Type() 固定返回 defNeuronBaseSAMOCL,Activation() 则依据 fRho 是否为 0 决定返回 None 还是具体激活枚举,这是判断该神经元是否走 SAM 通路的直接开关。 Init() 里先调基类 CNeuronBaseOCL::Init,再把外部传入的 rho 取绝对值赋给 fRho。若 fRho 为 0 或权重指针为空直接返回 true,跳过 SAM 缓冲初始化;否则用 Weights.Total() 的大小对 cWeightsSAM 做 BufferInit 与 BufferCreate,失败即返回 false。 updateInputWeights() 在 NeuronOCL 类型不匹配或 fRho==0 时退化为基类实现;否则依次跑 SumAndNormilize(参数含 1 与若干 0 偏移)、calcEpsilonWeights、feedForwardSAM,并以 error=1 调 calcOutputGradients 后再交回基类更新输入权重。 Save() 先写基类数据,再用 FileWriteFloat 落盘 fRho,返回值小于 INT_VALUE 视为失败;Load() 对称地先读基类,检测 FileIsEnding 后通过 FileReadFloat 恢复 fRho。开 MT5 把这段类声明和三个方法贴进自定义 EA 的神经网络模块,改 fRho 从 0 调到 1e-3 量级,能直接观察 SAM 缓冲是否参与前向计算。
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=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuronBaseSAMOCL; } class="kw">virtual class="type">int Activation(class="type">void) const { class="kw">return (fRho == class="num">0 ? (class="type">int)None : (class="type">int)activation); } class="kw">virtual class="type">int getWeightsSAMIndex(class="type">void) { class="kw">return cWeightsSAM.GetIndex(); } class=class="str">"cmt">//--- class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; class="type">bool CNeuronBaseSAMOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint numNeurons, class="type">class="kw">float rho, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, numNeurons, optimization_type, batch)) class="kw">return false; fRho = fabs(rho); if(fRho == class="num">0 || !Weights) class="kw">return true; if(!cWeightsSAM.BufferInit(Weights.Total(), class="num">0) || !cWeightsSAM.BufferCreate(OpenCL)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronBaseSAMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return false; if(NeuronOCL.Type() != Type() || fRho == class="num">0) class="kw">return CNeuronBaseOCL::updateInputWeights(NeuronOCL); if(!SumAndNormilize(Gradient, Output, Gradient, class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; if(!calcEpsilonWeights(NeuronOCL)) class="kw">return false; if(!feedForwardSAM(NeuronOCL)) class="kw">return false; class="type">class="kw">float error = class="num">1; if(!calcOutputGradients(Gradient, error)) class="kw">return false; class="kw">return CNeuronBaseOCL::updateInputWeights(NeuronOCL); } class="type">bool CNeuronBaseSAMOCL::Save(const class="type">int file_handle) { if(!CNeuronBaseOCL::Save(file_handle)) class="kw">return false; if(FileWriteFloat(file_handle, fRho) < INT_VALUE) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronBaseSAMOCL::Load(const class="type">int file_handle) { if(!CNeuronBaseOCL::Load(file_handle)) class="kw">return false; if(FileIsEnding(file_handle)) class="kw">return false; fRho = FileReadFloat(file_handle);
「权重缓冲初始化的提前退出逻辑」
在 SAM 权重处理函数中,若相关系数 fRho 为零或传入的 Weights 对象为空指针,函数直接返回 true,跳过后续所有 OpenCL 缓冲分配。这一短路设计避免了在无意义输入上浪费显存与计算队列。 紧接着的 cWeightsSAM.BufferFree() 会先释放旧缓冲,防止重复初始化导致内存泄漏。随后通过 BufferInit 按 Weights.Total() 个元素、初始值 0 申请主机端缓冲,并调用 BufferCreate(OpenCL) 在设备端建缓冲;任意一步失败立即返回 false,告知调用方资源准备未就绪。 实际在 MT5 策略测试器里跑这类代码时,可故意把 fRho 传 0 观察是否跳过 BufferCreate,借此验证 GPU 路径是否被绕开。外汇与贵金属品种波动跳空频繁,此类短路能降低异常输入下的内核报错概率,但高频切换仍属高风险操作,需自行压力测试。
if(fRho == class="num">0 || !Weights) class="kw">return true; cWeightsSAM.BufferFree(); if(!cWeightsSAM.BufferInit(Weights.Total(), class="num">0) || !cWeightsSAM.BufferCreate(OpenCL)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; }
收束
SAMformer 用浅层架构加锐度感知优化,绕开了变换器在多元时间序列长期预测里训练重、小样本泛化差的坑,参数还比主流方案少,作者验证里它确实压过了几个 SOTA 方法。我们这一篇用 MQL5 把这套思路落了地,EA 和类库清单里那 8 个文件(Research.mq5 到 NeuroNet.cl)就是可直接开 MT5 编译跑的样本采集与训练链路。 下一篇才会真正测它在行情里的实用价值,这一篇先停在「能跑通」这一步。外汇和贵金属行情噪声大、杠杆风险高,这类模型哪怕回测漂亮也只是概率占优,别拿来当确定性依据。