交易中的神经网络:状态空间模型·进阶篇
(2/3)·自注意力遇上限长窗口与二次缩放,SSM 如何以线性成本留住长期依赖
◍ Mamba 神经元的权重更新与类结构
在 SSMOCL 的反馈阶段,若未处于训练模式(bTrain 为假),会直接调用 SumAndNormilize 对 cA、cB 与隐藏状态做归一化,失败时返回 false,否则返回 true。这一步跳过了梯度累积,适合推理或前向验证场景。 updateInputWeights 里依次更新 cA 指向隐藏状态的权重、做归一化、再更新 cB 与 cC 的入边权重,任何一环失败立即退出。这种串行的失败即返结构,能保证 OpenCL 显存里的中间张量状态一致,避免半更新导致的数值漂移。 CNeuronMambaOCL 类继承了 CNeuronBaseOCL,内部挂了 6 个组件:cXProject、cZProject、cInsideConv 三个卷积层,cSSM 状态空间模块,cZSSM 与 cOutProject。Temp 缓冲用于暂存前向中间结果,减少重复分配。 Init 函数中,基类用 window*units_count 作为展开维度初始化;随后 cXProject 以 window_key+2 作为卷积核宽度、units_count 为输出通道数拉起。把 window_key 加 2 通常是为了容纳左右 padding 或前后状态偏移,具体效果可在 MT5 策略测试器里改这个数看预测延迟变化。
if(!bTrain) if(!SumAndNormilize(cA.getOutput(), cB.getOutput(), cHiddenStates.getOutput(), iWindowHidden, true)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronSSMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cA.UpdateInputWeights(cHiddenStates.AsObject())) class="kw">return false; if(!SumAndNormilize(cA.getOutput(), cB.getOutput(), cHiddenStates.getOutput(), iWindowHidden, true)) class="kw">return false; if(!cB.UpdateInputWeights(NeuronOCL)) class="kw">return false; if(!cC.UpdateInputWeights(cAB.AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class CNeuronMambaOCL : class="kw">public CNeuronBaseOCL { class="kw">protected: CNeuronConvOCL cXProject; CNeuronConvOCL cZProject; CNeuronConvOCL cInsideConv; CNeuronSSMOCL cSSM; CNeuronBaseOCL cZSSM; CNeuronConvOCL cOutProject; CBufferFloat Temp; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class=class="str">"cmt">//--- class="kw">public: CNeuronMambaOCL(class="type">void) {}; ~CNeuronMambaOCL(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_key, class="type">uint units_count, 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 defNeuronMambaOCL; } 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="type">bool CNeuronMambaOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint units_count, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) class="kw">return false; if(!cXProject.Init(class="num">0, class="num">0, OpenCL, window, window, window_key + class="num">2, units_count, class="num">1, optimization, iBatch))
「Mamba 编码层的初始化与前向链路」
在 MT5 的 OpenCL 神经网络封装里,CNeuronMambaOCL 的 init 把投影、卷积与 SSM 子模块逐一定义:cXProject 与 cZProject 用线性投影,激活函数分别设为 None 与 SIGMOID;cInsideConv 以窗口参数 (3,1,1) 做一维卷积,同样接 SIGMOID。 cSSM 与 cZSSM 承担状态空间计算,后者输入维度被拉到 2*window_key*units_count,激活置为 None;cOutProject 再把拼接后的特征投回 window 维度,无激活直接出结果。任一子模块 Init 返回 false 都会让整层初始化中断并返回 false。 feedForward 的调用顺序很固定:先跑两个投影,再用 cXProject 的输出喂给卷积,卷积结果进 cSSM,随后把 cSSM 与 cZProject 的输出做 Concat 送入 cZSSM,最后由 cOutProject 出层输出。想验证的话,在 MT5 策略测试器里给 HistoryBars 设 50、BarDescr 设 4,输入层节点数会落到 200(50×4),优化器默认 ADAM。 CreateEncoderDescriptions 负责把描述对象塞进数组:若 encoder 为空就 new 一个 CArrayObj,清表后先加输入层 descr,type 写 defNeuronBaseOCL、激活 None、优化 ADAM,prev_count 记录上一层节点数供后续层引用。
class="kw">return false; cXProject.SetActivationFunction(None); if(!cZProject.Init(class="num">0, class="num">1, OpenCL, window, window, window_key, units_count, class="num">1, optimization, iBatch)) class="kw">return false; cZProject.SetActivationFunction(SIGMOID); if(!cInsideConv.Init(class="num">0, class="num">2, OpenCL, class="num">3, class="num">1, class="num">1, window_key, units_count, optimization, iBatch)) class="kw">return false; cInsideConv.SetActivationFunction(SIGMOID); if(!cSSM.Init(class="num">0, class="num">3, OpenCL, window_key, window_key, units_count, optimization, iBatch)) class="kw">return false; if(!cZSSM.Init(class="num">0, class="num">4, OpenCL, class="num">2 * window_key * units_count, optimization, iBatch)) class="kw">return false; cZSSM.SetActivationFunction(None); if(!cOutProject.Init(class="num">0, class="num">5, OpenCL, class="num">2*window_key, class="num">2*window_key, window, units_count, class="num">1, optimization, iBatch)) class="kw">return false; cOutProject.SetActivationFunction(None); if(!Temp.BufferInit(window * units_count, class="num">0)) class="kw">return false; if(!Temp.BufferCreate(OpenCL)) class="kw">return false; if(!SetOutput(cOutProject.getOutput())) class="kw">return false; if(!SetGradient(cOutProject.getGradient())) class="kw">return false; SetActivationFunction(None); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronMambaOCL::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cXProject.FeedForward(NeuronOCL)) class="kw">return false; if(!cZProject.FeedForward(NeuronOCL)) class="kw">return false; if(!cInsideConv.FeedForward(cXProject.AsObject())) class="kw">return false; if(!cSSM.FeedForward(cInsideConv.AsObject())) class="kw">return false; if(!Concat(cSSM.getOutput(), cZProject.getOutput(), cZSSM.getOutput(), class="num">1, class="num">1, cSSM.Neurons())) class="kw">return false; if(!cOutProject.FeedForward(cZSSM.AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateEncoderDescriptions(CArrayObj *&encoder) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!encoder) { encoder = new CArrayObj(); if(!encoder) class="kw">return false; } class=class="str">"cmt">//--- 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; }
编码器后半段的层堆叠与维度变换
这段配置从 layer 1 一路铺到 layer 9,核心是把历史 K 线张量逐步压缩再展开成预测窗口。layer 1 先放一个 BatchNorm 层,batch 设成 1e4、优化器走 ADAM,不做激活;紧接着塞进 MambaOCL 层,window 取单根 Bar 描述长度,window_out 拉到 4 倍 BarDescr,循环里连续 Add 三次(i=2 到 4),Units 数量直接绑定 HistoryBars。 layer 5 用 TransposeOCL 做轴交换,count 和 window 都回到 BarDescr;之后 layer 6、7 是两记 ConvOCL:第一层 window_out = 4*NForecast 且用 LReLU,第二层收到 NForecast 并切 TANH,把输出规整到预测步数内。 layer 8 再转置一次,window 设为 NForecast,无激活;到 layer 9 时只 new 了描述对象还没填字段就截断在原文里。外汇与贵金属行情下用这类结构做推理,过拟合和滑点风险都偏高,参数务必在 MT5 用历史数据先跑通内存与维度。 下面把前半段关键代码逐行拆一下: //--- layer 1 if(!(descr = new CLayerDescription())) // 尝试新建层描述对象,失败直接返回 false return false; descr.type = defNeuronBatchNormOCL; // 层类型:OpenCL 批归一化 descr.count = prev_count; // 神经元数沿用上一层输出 descr.batch = 1e4; // 批大小 10000 descr.activation = None; // 无激活函数 descr.optimization = ADAM; // 优化器 ADAM if(!encoder.Add(descr)) // 加入编码器,失败则 { delete descr; // 释放描述对象 return false; // 返回失败 } if(!(descr = new CLayerDescription())) // 再建一个新描述 return false; descr.type = defNeuronMambaOCL; // Mamba 序列层 descr.window = BarDescr; // 输入窗口=单Bar描述维度 descr.window_out = 4 * BarDescr; // 内部维度扩 4 倍 prev_count = descr.count = HistoryBars; // 单元数=历史Bar数 descr.batch = 1e4; // 同样万级批 descr.activation = None; descr.optimization = ADAM; for(int i = 2; i <= 4; i++) // 重复添加 3 次同配置 if(!encoder.Add(descr)) { delete descr; 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 = class="num">1e4; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronMambaOCL; descr.window = BarDescr; class=class="str">"cmt">//window descr.window_out = class="num">4 * BarDescr; class=class="str">"cmt">//Inside Dimension prev_count = descr.count = HistoryBars; class=class="str">"cmt">//Units descr.batch = class="num">1e4; descr.activation = None; descr.optimization = ADAM; for(class="type">int i = class="num">2; i <= class="num">4; i++) 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 = defNeuronTransposeOCL; descr.count = prev_count; descr.window = 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">6 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = BarDescr; descr.window = prev_count; descr.window_out = class="num">4 * NForecast; descr.activation = LReLU; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">7 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronConvOCL; descr.count = BarDescr; descr.window = class="num">4 * NForecast; descr.window_out = NForecast; descr.activation = TANH; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">8 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronTransposeOCL; descr.count = BarDescr; descr.window = NForecast; 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">9 if(!(descr = new CLayerDescription())) class="kw">return false;
◍ 编码层里的逆归一与丢弃概率
这段 CLayerDescription 配置直接决定了编码器尾部两层的行为。第 9 层把 type 设成 defNeuronRevInDenormOCL,作用是把前面归一化的输入反向还原,count 取 BarDescr 乘 NForecast,单层、无激活、用 ADAM 优化。 第 10 层换成 defNeuronFreDFOCL,window 等于 BarDescr,count 压缩到 NForecast,step 写成 int(true) 即 1,probability 给了 0.7f。这个 0.7 的丢弃概率意味着训练时约七成神经元可能被随机屏蔽,是防止过拟合的硬参数。 两层都走 encoder.Add(descr),失败就 delete 并 return false,最后成功才 return true。开 MT5 把 probability 从 0.7 调到 0.5 跑同一段样本,能直观看到收敛曲线变抖,说明正则强度在起作用。外汇与贵金属模型训练波动大,高概率丢弃不保证泛化更好,需结合验证集判断。
descr.type = defNeuronRevInDenormOCL; descr.count = BarDescr * NForecast; descr.activation = None; descr.optimization = ADAM; descr.layers = class="num">1; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">10 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronFreDFOCL; descr.window = BarDescr; descr.count = NForecast; descr.step = class="type">int(true); descr.probability = class="num">0.7f; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; }
「用 2023 EURUSD 数据跑通的两阶段验证」
模型先用 2023 年 EURUSD 的 H1 历史数据做训练,指标全取默认值。第一阶段只训状态编码器去预测未来横向区间的价格走向,不掺任何参与者动作,所以旧数据集能直接全量复用,除非训练窗口拉长或变动才需重采。 编码器本身很紧凑,训练稳且快;预测图上能看出:一张平滑示意趋势转折,另一张近乎线性跟随行进中的趋势。 第二阶段迭代训参与者策略,同时训评论者数值函数做效率引导。这阶段必须周期刷新训练集,把当前策略产生的新数据补进去,否则模型会训偏。 实盘回测落在 2024 年 1 月:52 笔交易,27 笔盈利,胜率近 52%;平均盈利 39.36 点,平均亏损 -29.82 点,但最大亏损比最大盈利多约 30%,净值回撤超 35%。模型能盈利却无持续增长趋势,明显还需调参。 按周期拆,周五整体赚、周三亏;日内某些时段盈亏集中堆叠。平均持仓刚过 1 小时、最长 2 小时,这种时段聚集现象值得单独拉出来深挖。外汇与贵金属属高风险品类,上述回测仅反映历史样本,实盘可能偏离。