神经网络在交易中的应用:多元时间序列的双重聚类(DUET)·进阶篇
◍ Top-K 门控的 OpenCL 核函数实现
在 MT5 的 OpenCL 神经层里,Top-K 门控靠两个核函数配合:前向 TopKgates 算每个样本在窗口内的相对 logit,反向 TopKgatesGrad 只把被选中的位置回传梯度。下面这段是前向里最硬的一段——用局部数组做分块 Top-K 比较。
const class="type">int shift_gate = var * window + idx; class="type">class="kw">float logit = IsNaNOrInf(inputs[shift_logit], MIN_VALUE); class="type">class="kw">float noise = IsNaNOrInf(noises[shift_gate], class="num">0); if(noise != class="num">0) { noise *= Activation(inputs[shift_std], class="num">3); logit += IsNaNOrInf(noise, class="num">0); } __local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE]; class=class="str">"cmt">//--- const class="type">uint ls = min((class="type">uint)window, (class="type">uint)LOCAL_ARRAY_SIZE); class="type">uint bigger = class="num">0; class="type">class="kw">float max_logit = logit; class=class="str">"cmt">//--- Top K class="macro">#pragma unroll for(class="type">int i = class="num">0; i < window; i += ls) { if(idx >= i && idx < (i + ls)) temp[idx % ls] = logit; barrier(CLK_LOCAL_MEM_FENCE); for(class="type">int i1 = class="num">0; (i1 < min((class="type">int)ls,(class="type">int)(window-i)) && bigger <= k); i1++) { if(temp[i1] > logit) bigger++; if(temp[i1] > max_logit) max_logit = temp[i1]; } barrier(CLK_LOCAL_MEM_FENCE); } if(bigger <= k) gates[shift_gate] = logit - max_logit; else gates[shift_gate] = MIN_VALUE;
shift_gate 把二维 (var, idx) 摊平成一维门控下标;logit 先取主输入,遇 NaN/Inf 落回 MIN_VALUE 避免核崩溃。noise 非零时乘上 std 通道经激活函数(参数 3)后的结果,相当于给 logit 加噪,这是稀疏门控随机性的来源。
局部数组 temp 大小受 LOCAL_ARRAY_SIZE 限制,循环按块长 ls 搬运,每个 workgroup 内用 barrier(CLK_LOCAL_MEM_FENCE) 保证读写不打架。内层统计 bigger(比自己大的个数)和 max_logit,最后若 bigger<=k 保留 logit-max_logit(SoftMax 前的偏移),否则写 MIN_VALUE 直接屏蔽——这就是 Top-K 稀疏化的落点。
反向核里 gate<=MIN_VALUE 的位置梯度直接置零并返回,说明被屏蔽的样本不参与权重更新;有噪声的项还要过 Deactivation 反传 std 通道梯度。开 MT5 把 k 和 LOCAL_ARRAY_SIZE 调小(如 k=2、LOCAL_ARRAY_SIZE=64),在 EURUSD 的 M15 上跑一遍能直观看到门控稀疏率随 window 上升而下降。外汇与贵金属杠杆高,这类实验请先用模拟盘验证。
const class="type">int shift_gate = var * window + idx; class="type">class="kw">float logit = IsNaNOrInf(inputs[shift_logit], MIN_VALUE); class="type">class="kw">float noise = IsNaNOrInf(noises[shift_gate], class="num">0); if(noise != class="num">0) { noise *= Activation(inputs[shift_std], class="num">3); logit += IsNaNOrInf(noise, class="num">0); } __local class="type">class="kw">float temp[LOCAL_ARRAY_SIZE]; class=class="str">"cmt">//--- const class="type">uint ls = min((class="type">uint)window, (class="type">uint)LOCAL_ARRAY_SIZE); class="type">uint bigger = class="num">0; class="type">class="kw">float max_logit = logit; class=class="str">"cmt">//--- Top K class="macro">#pragma unroll for(class="type">int i = class="num">0; i < window; i += ls) { if(idx >= i && idx < (i + ls)) temp[idx % ls] = logit; barrier(CLK_LOCAL_MEM_FENCE); for(class="type">int i1 = class="num">0; (i1 < min((class="type">int)ls,(class="type">int)(window-i)) && bigger <= k); i1++) { if(temp[i1] > logit) bigger++; if(temp[i1] > max_logit) max_logit = temp[i1]; } barrier(CLK_LOCAL_MEM_FENCE); } if(bigger <= k) gates[shift_gate] = logit - max_logit; else gates[shift_gate] = MIN_VALUE;
「Top-K 门控神经元的类骨架与初始化落点」
在 MT5 的 OpenCL 神经网络扩展里,CNeuronTopKGates 这个类把多头稀疏门控塞进了一层神经元。protected 段先挂了几个核心成员:iK 记录 top-k 数值,cbNoise 是浮点缓冲用来做噪声扰动,cProjection 是卷积投影层,cGates 是基元门控层,后面三个虚函数 TopKgates / TopKgatesGradient / feedForward 等构成前向和梯度更新的重载链。 从 GetGates 能直接读出关系:门控数量等于投影层滤波器数除以 2,即 cProjection.GetFilters()/2;GetUnits 则直接返回投影层的单元数。这意味着你在调参时,gates 和 units_count 会经 2*gates 的滤波器倍数被卷积层吸收,改一个会影响另一头的宽度。 Init 函数是实打实的装配现场。它先以 gates*units_count 的总宽度调父类 CNeuronSoftMaxOCL 的初始化,再 SetHeads(units_count);随后 cProjection 以 window×window 卷积、2*gates 滤波器、units_count 单元、激活函数置为 None 起手。若 cbNoise 缓冲初始化或 OpenCL 创建失败,整层直接返回 false,模型编译会断在这一行。 开 MT5 验证时,可以把 defNeuronTopKGates 类型的层丢进自己的 EA 神经网络结构,故意把 gates 设成奇数看 GetGates 向下取整的副作用,再断点跟 Init 里的 cProjection.Init 返回码,能少走很多弯路。外汇与贵金属行情下用这类 GPU 网络做推理,仍属高风险,过拟合和实时滑点都可能吃掉回测优势。
class="kw">protected: class="type">int iK; CBufferFloat cbNoise; CNeuronConvOCL cProjection; CNeuronBaseOCL cGates; class=class="str">"cmt">//--- class="kw">virtual class="type">bool TopKgates(class="type">void); class="kw">virtual class="type">bool TopKgatesGradient(class="type">void); class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronTopKGates(class="type">void) {}; ~CNeuronTopKGates(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 units_count, class="type">uint gates, class="type">uint top_k, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">override const { class="kw">return defNeuronTopKGates; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int const file_handle) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override; class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">uint GetGates(class="type">void) const { class="kw">return cProjection.GetFilters() / class="num">2; } class="kw">virtual class="type">uint GetUnits(class="type">void) const { class="kw">return cProjection.GetUnits(); } }; class="type">bool CNeuronTopKGates::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint units_count, class="type">uint gates, class="type">uint top_k, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronSoftMaxOCL::Init(numOutputs, myIndex, open_cl, gates * units_count, optimization_type, batch)) class="kw">return class="kw">false; SetHeads(units_count); if(!cProjection.Init(class="num">0, class="num">0, OpenCL, window, window, class="num">2 * gates, units_count, class="num">1, optimization, iBatch)) class="kw">return class="kw">false; cProjection.SetActivationFunction(None); if(!cbNoise.BufferInit(Neurons(), class="num">0) || !cbNoise.BufferCreate(OpenCL))
MoE 门控与训练噪声的实现细节
这段 CNeuronTopKGates 的 feedForward 展示了混合专家(MoE)在 MT5 神经网络层里的前向逻辑:训练态下用标准正态分布给门控加噪,推理态则直接填零。 具体看,bTrain 为真时调用 MathRandomNormal(0,1,Neurons(),random) 生成均值为 0、方差为 1 的随机噪声数组,长度等于神经元数;写入 cbNoise 缓冲后参与 TopKgates 筛选。若不在训练,则 cbNoise.Fill(0) 关闭噪声,保证线上推断稳定。 CNeuronMoE 类把 cGates 与 cExperts 封装在一起,Init 参数里 experts 和 top_k 直接决定专家总数与每次激活的专家数。想在 MT5 验证,可把 top_k 设为 2、experts 设为 8,观察前向输出是否只走两个子网络。外汇与贵金属行情高波动,此类模型仅作概率参考,实盘须控仓。
class="kw">return class="kw">false; if(!cGates.Init(class="num">0, class="num">1, OpenCL, Neurons(), optimization, iBatch)) class="kw">return class="kw">false; cGates.SetActivationFunction(None); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronTopKGates::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cProjection.FeedForward(NeuronOCL)) class="kw">return class="kw">false; if(bTrain) { class="type">class="kw">double random[]; if(!Math::MathRandomNormal(class="num">0, class="num">1, Neurons(), random)) class="kw">return class="kw">false; if(!cbNoise.AssignArray(random)) class="kw">return class="kw">false; if(!cbNoise.BufferWrite()) class="kw">return class="kw">false; } else if(!cbNoise.Fill(class="num">0)) class="kw">return class="kw">false; if(!TopKgates()) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return CNeuronSoftMaxOCL::feedForward(cGates.AsObject()); } class CNeuronMoE : class="kw">public CNeuronBaseOCL { class="kw">protected: CNeuronTopKGates cGates; CLayer cExperts; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronMoE(class="type">void) {}; ~CNeuronMoE(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 units_count, class="type">uint experts, class="type">uint top_k, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">override const { class="kw">return defNeuronMoE; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int const file_handle) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *source, class="type">class="kw">float tau) class="kw">override;
◍ MoE 神经元的初始化与前向传播链
CNeuronMoE::Init 里先调基类 CNeuronBaseOCL::Init,输入维度被强行拉成 window_out * units_count,这一步若返回 false 整个构造直接流产。 随后 cGates 用 window、units_count、experts、top_k 初始化门控,index 从 0 递增;cExperts 容器清场并绑定 OpenCL 上下文,后面挂的四层子网络都靠这个 index 错开避免张量错位。 专家分支是「卷积 → 转置 → 卷积 → 转置」的固定组合:第一层卷积核宽 window、输出 window_out*experts 通道、激活 SoftPlus;转置层把 (units_count, experts) 翻成 (experts, units_count) 供下一卷积吃;末层卷积激活设 None,再经转置输出。任何一层 new 失败或 Init 失败都 delete 并返回 false,MT5 里若报 MoE 构建中断,优先查 experts 与 top_k 是否超出显存允许的整数倍。 feedForward 先跑 cGates.FeedForward 算路由权重,再对 cExperts 里 total 个子网络串行前推:prev 指针每轮指向刚算完的 neuron,形成严格的数据依赖链。若某专家返回 false,整函数立刻退出——这意味着单个专家数值溢出也会拖挂整条推理,回测时建议把 window_out 控制在 8~32 之间降低爆炸概率。 外汇与贵金属行情受杠杆与跳空影响大,此类 GPU 推理模块仅作信号参考,实盘前务必在 MT5 策略测试器用历史 tick 跑通 Init 与 feedForward 全流程。
class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">void TrainMode(class="type">bool flag) { bTrain = flag; cGates.TrainMode(bTrain); } }; class="type">bool CNeuronMoE::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_out, class="type">uint units_count, class="type">uint experts, class="type">uint top_k, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window_out * units_count, optimization_type, batch)) class="kw">return class="kw">false; class="type">int index = class="num">0; if(!cGates.Init(class="num">0, index, OpenCL, window, units_count, experts, top_k, optimization, iBatch)) class="kw">return class="kw">false; cExperts.Clear(); cExperts.SetOpenCL(OpenCL); CNeuronConvOCL *conv = NULL; CNeuronTransposeRCDOCL *transp = NULL; index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, index, OpenCL, window, window, window_out * experts, units_count, class="num">1, optimization, iBatch) || !cExperts.Add(conv)) { class="kw">delete conv; class="kw">return class="kw">false; } conv.SetActivationFunction(SoftPlus); transp = new CNeuronTransposeRCDOCL(); index++; if(!transp || !transp.Init(class="num">0, index, OpenCL, units_count, experts, window_out, optimization, iBatch) || !cExperts.Add(transp)) { class="kw">delete transp; class="kw">return class="kw">false; } transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); index++; conv = new CNeuronConvOCL(); if(!conv || !conv.Init(class="num">0, index, OpenCL, window_out, window_out, window_out, units_count, experts, optimization, iBatch) || !cExperts.Add(conv)) { class="kw">delete conv; class="kw">return class="kw">false; } conv.SetActivationFunction(None); transp = new CNeuronTransposeRCDOCL(); index++; if(!transp || !transp.Init(class="num">0, index, OpenCL, experts, units_count, window_out, optimization, iBatch) || !cExperts.Add(transp)) { class="kw">delete transp; class="kw">return class="kw">false; } transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronMoE::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cGates.FeedForward(NeuronOCL)) class="kw">return class="kw">false; CNeuronBaseOCL *prev = NeuronOCL; class="type">int total = cExperts.Total(); for(class="type">int i = class="num">0; i < total; i++) { CNeuronBaseOCL *neuron = cExperts[i]; if(!neuron || !neuron.FeedForward(prev)) class="kw">return class="kw">false; prev = neuron; }
「门控矩阵乘法的返回校验」
这段片段处在某一层前向计算收尾处,先调用 MatMul 把门控输出 cGates.getOutput() 与上一层 prev.getOutput() 做乘加,结果写进本层 getOutput()。 参数里 Neurons() / cGates.GetUnits() 作为列分块数传入,说明网络把神经元按门控单元数做了切分,MatMul 并非整矩阵一口气算,而是按门控单元分块卷积式累加。 若 MatMul 返回 false,函数立即 return false 中断该次前向;只有矩阵运算成功才 return true。开 MT5 把 cGates.GetUnits() 调小,能直接观察到分块数上升、单次矩阵宽度下降,可用于验证显存与耗时变化。外汇与贵金属模型训练波动剧烈,此类底层调用失败率随品种跳空而升高,属高风险环节。
if(!MatMul(cGates.getOutput(), prev.getOutput(), getOutput(), class="num">1, cGates.GetGates(), Neurons() / cGates.GetUnits(), cGates.GetUnits())) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; }
画得少,看得清
DUET 框架把时间聚类(TCM)和通道聚类(CCM)叠在一起处理多元时间序列,TCM 跟住时间维度的漂移调模型,CCM 挑出关键变量、压住噪声,思路不是堆特征而是先做减法。 本篇只落地了 TCM 的实现,配套给出现成的 Research.mq5、Study.mq5、Test.mq5 等 7 个文件(ZIP 约 2538.92 KB),开 MT5 把 Research 跑一遍就能采到样本。 下一篇会用真实历史数据测模型,给这套双聚类方法一个自己的交代;外汇与贵金属属高风险品种,任何模型输出都只是概率倾向,别当确定性信号用。