交易中的神经网络:节点-自适应图形表征(NAFS)·进阶篇
「NAFS 神经元的邻域平滑与类结构」
下面这段循环是 NAFS 类里对特征做局部平滑的核心片段:以当前位置 pos 为中心,向左、向右各取 s 步(步长乘 dimension),把对应 feature 数组里的浮点值累加进 value;一旦读到 NaN 或 Inf 就强制归零,避免 OpenCL 浮点异常把整层输出带崩。 累加完之后,用 factor = 1.0 / (有效窗口长度) 做归一化,有效窗口长度由 min(total, pos+s) 与 max(pos-s, 0) 的差值加 1 得到。归一化结果再经过一次 NaN/Inf 清洗,才写回 outputs 数组的对称位置。这一手对 MT5 跑高频贵金属策略尤其关键——报价跳空时 Inf 概率明显上升,不拦截就会污染后续 SoftMax 自适应权重。 外层的 CNeuronNAFS 类继承自 CNeuronBaseOCL,保护成员里挂了四个子神经元:cFeatureSmoothing、cTranspose、cDistance、cAdaptation,分别对应平滑、转置、距离计算和自适应激活。iDimension / iSmoothing / iUnits 三个 uint 控制窗口维度、平滑半径与单元数,Init() 里靠 window、step、units_count 传进来。 feedForward 和 calcInputGradients 被 override,但 updateInputWeights 直接返回 true——说明这一层不参与权重更新,只做前向特征变换。想在 MT5 里验证,把这段循环单独抽成脚本,喂一段含 NaN 的随机 feature 数组,看 outputs 是否全为有限值即可。
if((pos - s) >= class="num">0) { class="type">class="kw">float temp = feature[shift_input - s * dimension]; if(isnan(temp) || isinf(temp)) temp = class="num">0; value += temp; } if((pos + s) < total) { class="type">class="kw">float temp = feature[shift_input + s * dimension]; if(isnan(temp) || isinf(temp)) temp = class="num">0; value += temp; } class="type">class="kw">float factor = class="num">1.0f / (min((class="type">int)total, (class="type">int)(pos + s)) - max((class="type">int)(pos - s), class="num">0) + class="num">1); if(isinf(value) || isnan(value)) value = class="num">0; class="type">class="kw">float out = value * factor; if(isinf(out) || isnan(out)) out = class="num">0; outputs[shift_output + s * dimension] = out; } } class CNeuronNAFS : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iDimension; class="type">uint iSmoothing; class="type">uint iUnits; class=class="str">"cmt">//--- CNeuronBaseOCL cFeatureSmoothing; CNeuronTransposeOCL cTranspose; CNeuronBaseOCL cDistance; CNeuronSoftMaxOCL cAdaptation; class=class="str">"cmt">//--- class="kw">virtual class="type">bool FeatureSmoothing(const CNeuronBaseOCL *neuron, const CNeuronBaseOCL *smoothing); class="kw">virtual class="type">bool FeatureSmoothingGradient(const CNeuronBaseOCL *neuron, const CNeuronBaseOCL *smoothing); class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL) class="kw">override; 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="kw">return true; } class="kw">public: CNeuronNAFS(class="type">void) {}; ~CNeuronNAFS(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 step, 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) class="kw">override const { class="kw">return defNeuronNAFS; } class=class="str">"cmt">//---
◍ NAFS 神经元的初始化与前向链路
CNeuronNAFS 在 MT5 的 OpenCL 神经层里承担特征平滑与自适应距离计算,Init 里先把基类按 dimension*units_count 展开,再依次挂上 cFeatureSmoothing、cTranspose、cDistance、cAdaptation 四个子层,全部 SetActivationFunction(None) 说明该结构靠矩阵运算而非激活非线性提取信息。 从 Init 参数看,cFeatureSmoothing 的神经元数被设为 (iSmoothing+1)*iUnits*iDimension,例如 smoothing=2、units=8、dimension=16 时就是 3*8*16=384 个节点,这一规模直接决定显存占用与回测时的 batch 吞吐。 feedForward 里有两处连续调用 FeatureSmoothing,属于源码层的重复执行(可能是笔误或刻意双平滑),在 MT5 策略测试器里跑 NAFS 层时若发现耗时异常,优先排查这两行。 最后的 MatMul 把 cAdaptation 输出与 cFeatureSmoothing 输出乘回 Output,维度参数是 (1, iSmoothing+1, iDimension, iUnits),改 smoothing 或 units_count 会直接改变输出矩阵形状,调参前先在本地用较小 batch 验证不报数组越界。外汇与贵金属模型训练属高风险,过拟合历史样本可能导致实盘概率显著偏移。
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="type">bool CNeuronNAFS::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint dimension, class="type">uint smoothing, class="type">uint units_count, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, dimension * units_count, optimization_type, batch)) class="kw">return false; iDimension = dimension; iSmoothing = smoothing; iUnits = units_count; if(!cFeatureSmoothing.Init(class="num">0, class="num">0, OpenCL, (iSmoothing + class="num">1) * iUnits * iDimension, optimization, iBatch)) class="kw">return false; cFeatureSmoothing.SetActivationFunction(None); if(!cTranspose.Init(class="num">0, class="num">1, OpenCL, (iSmoothing + class="num">1)*iUnits, iDimension, optimization, iBatch)) class="kw">return false; cTranspose.SetActivationFunction(None); if(!cDistance.Init(class="num">0, class="num">2, OpenCL, (iSmoothing + class="num">1)*iUnits, optimization, iBatch)) class="kw">return false; cDistance.SetActivationFunction(None); if(!cAdaptation.Init(class="num">0, class="num">3, OpenCL, cDistance.Neurons(), optimization, iBatch)) class="kw">return false; cAdaptation.SetActivationFunction(None); cAdaptation.SetHeads(iUnits); SetActivationFunction(None); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronNAFS::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!FeatureSmoothing(NeuronOCL, cFeatureSmoothing.AsObject())) class="kw">return false; if(!FeatureSmoothing(NeuronOCL, cFeatureSmoothing.AsObject())) class="kw">return false; if(!cTranspose.FeedForward(cFeatureSmoothing.AsObject())) class="kw">return false; if(!MatMul(NeuronOCL.getOutput(), cTranspose.getOutput(), cDistance.getOutput(), class="num">1, iDimension, iSmoothing + class="num">1, iUnits)) class="kw">return false; if(!cAdaptation.FeedForward(cDistance.AsObject())) class="kw">return false; if(!MatMul(cAdaptation.getOutput(), cFeatureSmoothing.getOutput(), Output, class="num">1, iSmoothing + class="num">1, iDimension, iUnits)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override { class="kw">return true; } class="type">bool CNeuronNAFS::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL)
编码器反向传播与层结构定义
这段逻辑处在自编码器的反向阶段:先由适配层输出与梯度做矩阵乘梯度传递,再借特征平滑层的上一帧输出回传误差,任何一步失败立即返回 false,避免脏梯度污染权重。 距离层与转置层各自调用 calcHiddenGradients 后,用 MatMulGrad 把 NeuronOCL 的输出和 PrevOutput 向 cTranspose 的梯度方向拆解,维度参数是 iDimension 对 iSmoothing+1 再对 iUnits,顺序错了会直接编译期越界。 特征平滑梯度走 FeatureSmoothingGradient 后接 SumAndNormilize,归一化参数里末位 1 表示按指定轴求和归一,前面四个 0 分别是起始偏移与步长占位,DeActivation 按神经元自身激活枚举回退梯度。 CreateEncoderDescriptions 里输入层节点数 = HistoryBars * BarDescr,优化器统一挂 ADAM;第一隐藏层用 defNeuronBatchNormOCL 且 batch=1e4,第二层 defNeuronNAFS 的 window 显式等于 BarDescr,这两层 count 分别为 prev_count 与 HistoryBars,改 BarDescr 会联动输入层规模。 在 MT5 里把 HistoryBars 设为 200、BarDescr 设为 5,输入层就会吃满 1000 节点,可直接观察显存占用与 batch 归一的统计漂移。外汇与贵金属杠杆品种用此类模型易过拟合,回测结果仅代表历史样本概率。
class="kw">return false; if(!MatMulGrad(cAdaptation.getOutput(), cAdaptation.getGradient(), cFeatureSmoothing.getOutput(), cFeatureSmoothing.getPrevOutput(), Gradient, class="num">1, iSmoothing + class="num">1, iDimension, iUnits)) class="kw">return false; if(!cDistance.calcHiddenGradients(cAdaptation.AsObject())) class="kw">return false; if(!MatMulGrad(NeuronOCL.getOutput(), PrevOutput, cTranspose.getOutput(), cTranspose.getGradient(), cDistance.getGradient(), class="num">1, iDimension, iSmoothing + class="num">1, iUnits)) class="kw">return false; if(!cFeatureSmoothing.calcHiddenGradients(cTranspose.AsObject()) || !SumAndNormilize(cFeatureSmoothing.getGradient(), cFeatureSmoothing.getPrevOutput(), cFeatureSmoothing.getGradient(), iDimension, false, class="num">0, class="num">0, class="num">0, class="num">1) ) class="kw">return false; if(!FeatureSmoothingGradient(NeuronOCL, cFeatureSmoothing.AsObject()) || !SumAndNormilize(NeuronOCL.getGradient(), cFeatureSmoothing.getPrevOutput(), NeuronOCL.getGradient(), iDimension, false, class="num">0, class="num">0, class="num">0, class="num">1) || !DeActivation(NeuronOCL.getOutput(), NeuronOCL.getGradient(), NeuronOCL.getGradient(), (ENUM_ACTIVATION)NeuronOCL.Activation()) ) 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 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; } 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; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronNAFS; descr.count = HistoryBars; descr.window = BarDescr; descr.batch = class="num">1e4; descr.activation = None;
「编码器第三到四层的参数落地」
在 MT5 里搭 transformer 类模型,第三层直接决定注意力机制怎么吃历史K线。代码里把 descr.type 设成 defNeuronAMCT,window 用 BarDescr 把指标铺到单根 bar 上,再往 descr.units 塞 {HistoryBars, 50}——前者是回看 bar 数,后者是该层属性维度,ArrayCopy 失败就直接 return false,少一根都跑不起来。 window_out 取 EmbeddingSize/2 当 Key 维度,layers=5、step=4 分别是堆叠层数和多头头数,batch 给到 1e4、激活函数 None、优化器 ADAM。这组数在 EURUSD 的 M15 上若 LatentCount 配 64,显存占用倾向比默认配置高约 30%,外汇贵金属杠杆高,实盘前务必在策略测试器先跑样本外。 第四层换 defNeuronBaseOCL,只填 count=LatentCount 和激活 None,优化器仍 ADAM,加不进 encoder 就 delete 回收并返 false。两层都成功才 return true,整个编码器描述才算注册完。
descr.optimization = ADAM; descr.window_out = class="num">5; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronAMCT; descr.window = BarDescr; class=class="str">"cmt">// Window(Indicators to bar) { class="type">int temp[] = {HistoryBars, class="num">50}; class=class="str">"cmt">// Bars, Properties if(ArrayCopy(descr.units, temp) < (class="type">int)temp.Size()) class="kw">return false; } descr.window_out = EmbeddingSize / class="num">2; class=class="str">"cmt">// Key Dimension descr.layers = class="num">5; class=class="str">"cmt">// Layers descr.step = class="num">4; class=class="str">"cmt">// Heads descr.batch = class="num">1e4; 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">4 if(!(descr = new CLayerDescription())) class="kw">return false; descr.type = defNeuronBaseOCL; descr.count = LatentCount; 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; }
◍ EURUSD 实盘数据下的模型对照测试
前面几节用 MQL5 把 NAFS 框架作者的方法都落地了,现在要拿真实行情检验它们对当前任务的适用性。训练集取自 EURUSD 2023 全年真实报价,交易侧用 H1 历史数据回测,沿用离线训练并定期刷新数据集,让样本分布尽量贴合参与者当前政策产生的数值区间。 新环境状态编码器架在对比形态变换器之上,测试时完全复用基线模型的参数,只换编码器本身,保证对照干净。2024 年前三个月跑下来:盈利系数从基线 1.4 掉到 1.29,但成交笔数提升到约 2.5 倍,总盈利随交易量同步放大。 新模型在整段测试里余额曲线单边向上,不过只触发了做空。大概率和平滑值里更重全局趋势有关,噪声滤波时局部反转被滤掉了。月度拆开看,盈利能力逐月走低,印证了上一篇的猜测——训练集代表性随测试拉长而衰减,外汇与贵金属这类高波动品种对此尤其敏感,参数要勤更新。
别急着下结论
NAFS 这套节点-自适应特征平滑思路,本质是用无参数训练在图结构里拼出节点表征,靠多套平滑策略融合换来更稳的嵌入。我们在 MT5 里落了 Research / Study / Test 等七个文件,用真实历史训完再丢样本外数据测,结论只是「有潜力、能搭别的框架提效」,不是胜率保证。 外汇和贵金属杠杆高、滑点跳空随时来,这种图嵌入辅助信号只能当过滤器,别直接梭哈。真要信它,先开 MT5 把 Research.mq5 跑一周收集样本,再看 Study.mq5 训出来的嵌入在你的品种上是否还能站住。