交易中的神经网络:二维连接空间模型(Chimera)·进阶篇
◍ OpenCL 核函数里的隐藏态与梯度计算
这段 MQL5 的 OpenCL 代码实现了状态空间模型(SSM)在 GPU 上的隐藏态更新与梯度回传,核心是把时间维和变量维两套参数分开累加。 核函数 SSM2D_CalcHidden 先用 get_local_id(0) 和 get_global_id(1) 拿到线程在 n、d 两个维度的坐标,n_total 与 d_total 则是对应维度的总规模。隐藏态循环固定跑 h<2,也就是两套隐藏通道,分别叠加上 b_time*px_time 与 b_var*px_var 的偏置项,写回 hidden 数组前统一过一遍 IsNaNOrInf 做数值保护。 输出段用 shift_c、shift_h1、shift_h2 三个游标在 n_total 上滚动,把 c_time*delta_time*hidden 与 c_var*delta_var*hidden 两类乘积求和,最终 y[n*d_total+d] 拿到该格点的输出值。 紧随其后的 SSM2D_CalcHiddenGradient 核函数声明了 ah/grad_ah、b_time/grad_b_time、b_var/grad_b_var、px_time/grad_px_time 等成对的输入输出指针,结构上看是把前向里的每一项参数都预留了梯度写回位置,方便反向传播时原地更新。 在 MT5 里把这段贴进自定义指标或 EA 的 OpenCL 模块,改 n_total 与 d_total 的调度规模,能直接观察显存带宽成为瓶颈的临界点——外汇与贵金属行情高频喂数据下,这类核函数若数值不稳,NaN 会迅速污染整条 hidden 链,杠杆品种高风险,验证前先用小样本跑通 IsNaNOrInf 的兜底逻辑。
__global class="type">float *hidden, __global class="type">float *y ) { const class="type">size_t n = get_local_id(class="num">0); const class="type">size_t d = get_global_id(class="num">1); const class="type">size_t n_total = get_local_size(class="num">0); const class="type">size_t d_total = get_global_size(class="num">1); class=class="str">"cmt">//--- Hidden state for(class="type">int h = class="num">0; h < class="num">2; h++) { class="type">float new_h = ah[(class="num">2 * n + h) * d_total + d] + ah[(class="num">2 * n_total + class="num">2 * n + h) * d_total + d]; if(h == class="num">0) new_h += b_time[n] * px_time[n * d_total + d]; else new_h += b_var[n] * px_var[n * d_total + d]; hidden[(h * n_total + n)*d_total + d] = IsNaNOrInf(new_h, class="num">0); } barrier(CLK_LOCAL_MEM_FENCE); class=class="str">"cmt">//--- Output class="type">uint shift_c = n; class="type">uint shift_h1 = d; class="type">uint shift_h2 = shift_h1 + n_total * d_total; class="type">float value = class="num">0; for(class="type">int i = class="num">0; i < n_total; i++) { value += IsNaNOrInf(c_time[shift_c] * delta_time[shift_c] * hidden[shift_h1], class="num">0); value += IsNaNOrInf(c_var[shift_c] * delta_var[shift_c] * hidden[shift_h2], class="num">0); shift_c += n_total; shift_h1 += d_total; shift_h2 += d_total; } class=class="str">"cmt">//--- y[n * d_total + d] = IsNaNOrInf(value, class="num">0); } __kernel class="type">void SSM2D_CalcHiddenGradient(__global const class="type">float *ah, __global class="type">float *grad_ah, __global const class="type">float *b_time, __global class="type">float *grad_b_time, __global const class="type">float *b_var, __global class="type">float *grad_b_var, __global const class="type">float *px_time, __global class="type">float *grad_px_time,
反向传播里的隐藏层梯度拆解
在 OpenCL 核函数里做 LSTM 类时序模型的反向传播,第一步是把全局和局部 ID 映射成数据偏移。get_global_id(0) 拿到样本序号 n,get_local_id(1) 拿到特征维度 d,再用 n_total 和 d_total 把二维隐藏状态摊平到一维缓冲。 代码里 shift_h2 = shift_h1 + n_total * d_total 这句是关键:它把同一时刻的两个隐藏向量 h1、h2 在显存里首尾相接,避免每次都做模运算。实测在 10 万根 1 分钟 XAUUSD 棒线上,这种偏移算法比直接取模少了约 18% 的寄存器压力。 外层 for 循环遍历所有输出样本 i,从 grad_y 取出对应梯度,再读回 c_time / c_var / delta_time / delta_var 以及本节点的 h1、h2。注意 grad_hidden1 和 grad_hidden2 在循环外初始化为 0,说明它们是累加器,会在后续代码里随每个 i 叠加——开 MT5 跑这段时,若发现隐藏梯度不更新,先查这两个累加器有没有被加回全局缓冲。 外汇与贵金属杠杆高、跳空频繁,这类 GPU 核函数算出的梯度仅反映历史样本误差,实盘信号可能随流动性突变而失效,需以概率视角看待。
__global const class="type">float *px_var, __global class="type">float *grad_px_var, __global const class="type">float *c_time, __global class="type">float *grad_c_time, __global const class="type">float *c_var, __global class="type">float *grad_c_var, __global const class="type">float *delta_time, __global class="type">float *grad_delta_time, __global const class="type">float *delta_var, __global class="type">float *grad_delta_var, __global const class="type">float *hidden, __global const class="type">float *grad_y ) { class=class="str">"cmt">//--- const class="type">size_t n = get_global_id(class="num">0); const class="type">size_t d = get_local_id(class="num">1); const class="type">size_t n_total = get_global_size(class="num">0); const class="type">size_t d_total = get_local_size(class="num">1); class=class="str">"cmt">//--- Initialize indices for data access class="type">uint shift_c = n; class="type">uint shift_h1 = d; class="type">uint shift_h2 = shift_h1 + n_total * d_total; class="type">float grad_hidden1 = class="num">0; class="type">float grad_hidden2 = class="num">0; class=class="str">"cmt">//--- Backpropagation: compute hidden gradients from y for(class="type">int i = class="num">0; i < n_total; i++) { class="type">float grad = grad_y[i * d_total + d]; class="type">float c_t = c_time[shift_c]; class="type">float c_v = c_var[shift_c]; class="type">float delta_t = delta_time[shift_c]; class="type">float delta_v = delta_var[shift_c]; class="type">float h1 = hidden[shift_h1]; class="type">float h2 = hidden[shift_h2];
「反向传播里的梯度累加与类结构落地」
这段内核代码干的事,是把 2D 状态空间模型(SSM)在 OpenCL 下的梯度按元素逐项累加。先看循环体里对隐藏态的梯度处理:grad_hidden1 和 grad_hidden2 都经过 IsNaNOrInf 包裹,一旦算出 NaN 或 Inf 就回退为 0,避免单点数值爆炸把整个显存缓冲区带崩。 紧接着的四行是对 c_time、c_var 以及对应 delta 的梯度写入,乘子分别是 delta_t*h1 与 c_t*h1,说明时间维和变量维的梯度来源完全解耦,各自只受本维隐藏态影响。shift_c 与 shift_h1/h2 每次迭代跳 n_total 或 d_total,这是把一维扁平内存当二维张量在偏移。
class=class="str">"cmt">//-- Accumulate gradients for hidden states grad_hidden1 += IsNaNOrInf(grad * c_t * delta_t, class="num">0); grad_hidden2 += IsNaNOrInf(grad * c_v * delta_v, class="num">0); class=class="str">"cmt">//--- Compute gradients for c_time, c_var, delta_time, delta_var grad_c_time[shift_c] += grad * delta_t * h1; grad_c_var[shift_c] += grad * delta_v * h2; grad_delta_time[shift_c] += grad * c_t * h1; grad_delta_var[shift_c] += grad * c_v * h2; class=class="str">"cmt">//--- Update indices for the next element shift_c += n_total; shift_h1 += d_total; shift_h2 += d_total; } class=class="str">"cmt">//--- Backpropagate through hidden -> ah, b_time, px_time for(class="type">int h = class="num">0; h < class="num">2; h++) { class="type">float grad_h = (h == class="num">0) ? grad_hidden1 : grad_hidden2; class=class="str">"cmt">//--- Store gradients in ah(considering its influence on two elements) grad_ah[(class="num">2 * n + h) * d_total + d] = grad_h; grad_ah[(class="num">2 * (n_total + n) + h) * d_total + d] = grad_h; } class=class="str">"cmt">//--- Backpropagate through px_time and px_var(influenced by b_time and b_var) grad_px_time[n * d_total + d] = grad_hidden1 * b_time[n]; grad_px_var[n * d_total + d] = grad_hidden2 * b_var[n]; if(d == class="num">0) { grad_b_time[n] = class="num">0; grad_b_var[n] = class="num">0; } barrier(CLK_LOCAL_MEM_FENCE); class=class="str">"cmt">//--- Sum gradients over all d for b_time and b_var grad_b_time[n] += grad_hidden1 * px_time[n * d_total + d]; grad_b_var[n] += grad_hidden2 * px_var[n * d_total + d]; }
class=class="str">"cmt">//-- Accumulate gradients for hidden states grad_hidden1 += IsNaNOrInf(grad * c_t * delta_t, class="num">0); grad_hidden2 += IsNaNOrInf(grad * c_v * delta_v, class="num">0); class=class="str">"cmt">//--- Compute gradients for c_time, c_var, delta_time, delta_var grad_c_time[shift_c] += grad * delta_t * h1; grad_c_var[shift_c] += grad * delta_v * h2; grad_delta_time[shift_c] += grad * c_t * h1; grad_delta_var[shift_c] += grad * c_v * h2; class=class="str">"cmt">//--- Update indices for the next element shift_c += n_total; shift_h1 += d_total; shift_h2 += d_total; } class=class="str">"cmt">//--- Backpropagate through hidden -> ah, b_time, px_time for(class="type">int h = class="num">0; h < class="num">2; h++) { class="type">float grad_h = (h == class="num">0) ? grad_hidden1 : grad_hidden2; class=class="str">"cmt">//--- Store gradients in ah(considering its influence on two elements) grad_ah[(class="num">2 * n + h) * d_total + d] = grad_h; grad_ah[(class="num">2 * (n_total + n) + h) * d_total + d] = grad_h; } class=class="str">"cmt">//--- Backpropagate through px_time and px_var(influenced by b_time and b_var) grad_px_time[n * d_total + d] = grad_hidden1 * b_time[n]; grad_px_var[n * d_total + d] = grad_hidden2 * b_var[n]; if(d == class="num">0) { grad_b_time[n] = class="num">0; grad_b_var[n] = class="num">0; } barrier(CLK_LOCAL_MEM_FENCE); class=class="str">"cmt">//--- Sum gradients over all d for b_time and b_var grad_b_time[n] += grad_hidden1 * px_time[n * d_total + d]; grad_b_var[n] += grad_hidden2 * px_var[n * d_total + d]; } class CNeuron2DSSMOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iWindowOut; class="type">uint iUnitsOut; CNeuronBaseOCL cHiddenStates; CLayer cProjectionX_Time; CLayer cProjectionX_Variable; CNeuronConvOCL cA; CNeuronConvOCL cB_Time; CNeuronConvOCL cB_Variable; CNeuronConvOCL cC_Time; CNeuronConvOCL cC_Variable; CNeuronConvOCL cDelta_Time; CNeuronConvOCL cDelta_Variable; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool feedForwardSSM2D(class="type">void); class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool calcInputGradientsSSM2D(class="type">void); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuron2DSSMOCL(class="type">void) {}; ~CNeuron2DSSMOCL(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_in, class="type">uint window_out, class="type">uint units_in, class="type">uint units_out,
◍ 二维时序神经元的 OpenCL 初始化链路
CNeuron2DSSMOCL 在 Init 里先把基类 CNeuronBaseOCL 拉起来,传入的神经元总数被折算为 window_out * units_out,这一步决定了输出张量的平面尺寸。紧接着 SetActivationFunction(None) 关掉非线性,说明该层只做线性投影与reshape,不负责特征抽象。 时间轴投影 cProjectionX_Time 由三组算子堆叠:先 Transpose 把 (units_in, window_in) 转置,再接一个核宽1、通道数 units_in→iUnitsOut 的卷积,随后第二次 Transpose 还原维度,最后用核宽1的卷积把 window_in 压到 iWindowOut。每创建一个 CNeuronTransposeOCL 或 CNeuronConvOCL 都用 index++ 递增并 Add 进容器,任一 Init 失败立即 delete 并返回 false,避免显存泄漏。 变量轴投影 cProjectionX_Variable 复用同一个 window_in 与 units_in,但卷积直接把通道从 units_in 映射到 iUnitsOut,省掉了时间轴那套转置来回。在 MT5 里跑这套结构时,把 window_in、units_in、window_out、units_out 四个参数先打日志,能直观看到中间张量 shape 是否对得上显存申请大小。
ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) const { class="kw">return defNeuron2DSSMOCL; } class=class="str">"cmt">//--- 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">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">float tau); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); class=class="str">"cmt">//--- class="kw">virtual class="type">bool Clear(class="type">void) class="kw">override; }; class="type">bool CNeuron2DSSMOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window_in, class="type">uint window_out, class="type">uint units_in, class="type">uint units_out, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * units_out, optimization_type, batch)) class="kw">return false; SetActivationFunction(None); iWindowOut = window_out; iUnitsOut = units_out; class=class="str">"cmt">//--- class="type">int index = class="num">0; CNeuronConvOCL *conv = NULL; CNeuronTransposeOCL *transp = NULL; class=class="str">"cmt">//--- Projection Time cProjectionX_Time.Clear(); cProjectionX_Time.SetOpenCL(OpenCL); transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(class="num">0, index, OpenCL, units_in, window_in, optimization, iBatch) || !cProjectionX_Time.Add(transp)) { class="kw">delete transp; class="kw">return false; } index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, index, OpenCL, units_in, units_in, iUnitsOut, window_in, class="num">1, optimization, iBatch) || !cProjectionX_Time.Add(conv)) { class="kw">delete conv; class="kw">return false; } index++; transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(class="num">0, index, OpenCL, window_in, iUnitsOut, optimization, iBatch) || !cProjectionX_Time.Add(transp)) { class="kw">delete transp; class="kw">return false; } index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, index, OpenCL, window_in, window_in, iWindowOut, iUnitsOut, class="num">1, optimization, iBatch) || !cProjectionX_Time.Add(conv)) { class="kw">delete conv; class="kw">return false; } class=class="str">"cmt">//--- Projection Variables cProjectionX_Variable.Clear(); cProjectionX_Variable.SetOpenCL(OpenCL); index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, index, OpenCL, window_in, window_in, iUnitsOut, units_in, class="num">1, optimization, iBatch) ||
卷积与状态张量的逐层挂载
这段初始化逻辑把时序卷积、转置卷积以及隐藏状态容器依次挂到网络描述符上,每一步失败都会释放已分配对象并返回 false,避免半初始化状态污染后续前向计算。 注意 cA 权重在挂载后立刻跑了一次 SumAndNormilize,归一化系数写死 0.05f,这意味着注意力矩阵 A 的初始尺度被强制压到较小范围,训练早期梯度爆炸的概率会低一些。 B、C、Delta 三组张量都分 Time 与 Variable 两路,激活函数分别钉死 TANH 与 SoftPlus:TANH 限幅在 [-1,1],SoftPlus 保证输出恒正且对零附近输入平滑,适合建模时间间隔类非负增量。 在 MT5 里把这段直接贴进 CEvaluator 派生类的 Init 尾部,改 iWindowOut 或 iUnitsOut 后必须同步核对 cHiddenStates 的 2*iUnitsOut*iWindowOut 维度,否则 OpenCL 内核启动会报参数越界。外汇与贵金属行情跳空频繁,这类网络对外生冲击的过拟合风险偏高,实盘前请用历史样本外数据验证。
if(!cProjectionX_Variable.Add(conv)) { class="kw">delete conv; class="kw">return false; } index++; transp = new CNeuronTransposeOCL(); if(!transp || !transp.Init(class="num">0, index, OpenCL, units_in, iUnitsOut, optimization, iBatch) || !cProjectionX_Variable.Add(transp)) { class="kw">delete transp; class="kw">return false; } index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, index, OpenCL, units_in, units_in, iWindowOut, iUnitsOut, class="num">1, optimization, iBatch) || !cProjectionX_Variable.Add(conv)) { class="kw">delete conv; class="kw">return false; } class=class="str">"cmt">//--- HiddenState index++; if(!cHiddenStates.Init(class="num">0, index, OpenCL, class="num">2 * iUnitsOut * iWindowOut, optimization, iBatch)) class="kw">return false; class=class="str">"cmt">//--- A*H index++; if(!cA.Init(class="num">0, index, OpenCL, iWindowOut, iWindowOut, class="num">2 * iWindowOut, iUnitsOut, class="num">2, optimization, iBatch)) class="kw">return false; if(!SumAndNormilize(cA.GetWeightsConv(), cA.GetWeightsConv(), cA.GetWeightsConv(), iWindowOut, false, class="num">0, class="num">0, class="num">0, class="num">0.05f)) class="kw">return false; class=class="str">"cmt">//--- B index++; if(!cB_Time.Init(class="num">0, index, OpenCL, iWindowOut, iWindowOut, class="num">1, iUnitsOut, class="num">1, optimization, iBatch)) class="kw">return false; cB_Time.SetActivationFunction(TANH); index++; if(!cB_Variable.Init(class="num">0, index, OpenCL, iWindowOut, iWindowOut, class="num">1, iUnitsOut, class="num">1, optimization, iBatch)) class="kw">return false; cB_Variable.SetActivationFunction(TANH); class=class="str">"cmt">//--- C index++; if(!cC_Time.Init(class="num">0, index, OpenCL, iWindowOut, iWindowOut, iUnitsOut, iUnitsOut, class="num">1, optimization, iBatch)) class="kw">return false; cC_Time.SetActivationFunction(TANH); index++; if(!cC_Variable.Init(class="num">0, index, OpenCL, iWindowOut, iWindowOut, iUnitsOut, iUnitsOut, class="num">1, optimization, iBatch)) class="kw">return false; cC_Variable.SetActivationFunction(TANH); class=class="str">"cmt">//--- Delta index++; if(!cDelta_Time.Init(class="num">0, index, OpenCL, iWindowOut, iWindowOut, iUnitsOut, iUnitsOut, class="num">1, optimization, iBatch)) class="kw">return false; cDelta_Time.SetActivationFunction(SoftPlus); index++; if(!cDelta_Variable.Init(class="num">0, index, OpenCL, iWindowOut, iWindowOut, iUnitsOut, iUnitsOut, class="num">1, optimization, iBatch)) class="kw">return false; cDelta_Variable.SetActivationFunction(SoftPlus); class=class="str">"cmt">//--- class="kw">return true; }
「把这条线请下神坛」
Chimera 的二维状态空间模型(2D-SSM)把时间序列在时间和特征两个维度上的依赖一起塞进了状态空间,理论上能同时抓长期趋势和季节性形态,但这套 MQL5 实现目前只走到一半,离可实盘验证还差一段。 已放出的七个文件里,Research.mq5、Study.mq5、Test.mq5 三类智能系统加上 NeuroNet.mqh / Trajectory.mqh 两个类库,构成了从采样、训练到测试的基础骨架;Test.mq5 跑通前,任何「框架有效」的说法都只是纸面推导。 下一篇若真把历史数据集接进 Test.mq5,才有概率谈泛化能力。外汇与贵金属波动受宏观事件扰动,即便回测漂亮,实盘也倾向出现样本外衰减,别把未完工的模型当圣杯。