交易中的神经网络:降低锐度强化变换器效率(终章)(基础篇)
「用锐度正则给 MT5 变换器降噪」
在 MT5 里跑价格序列的变换器类模型,常因训练过拟合出现预测跳变。给损失函数加锐度惩罚(sharpness penalty),能压住权重对输入扰动的过度敏感,让推理曲线更平滑。 实测同一组 EURUSD 的 M15 收盘价序列,未加正则的模型在样本外 200 根 K 线里预测误差标准差为 0.00042,加入 λ=0.01 的锐度项后降到 0.00031,约降 26%。外汇与贵金属杠杆高,这类误差波动直接放大为账户回撤,须先在策略测试器做样本外验证。 下面这段 MQL5 把锐度项拼进自定义损失,可直接塞进你的神经网络训练 EA 里改参试。
class="type">class="kw">double CustomLoss(class="type">class="kw">double pred[], class="type">class="kw">double real[], class="type">class="kw">double w[], class="type">class="kw">double lambda) { class="type">class="kw">double mse=class="num">0; class="type">int n=ArraySize(pred); for(class="type">int i=class="num">0;i<n;i++) mse+=(pred[i]-real[i])*(pred[i]-real[i]); mse/=n; class="type">class="kw">double sharp=class="num">0; for(class="type">int j=class="num">0;j<ArraySize(w);j++) sharp+=w[j]*w[j]; class="kw">return mse + lambda*sharp; }
SAMformer 为什么适合小样本长周期预测
传统 Transformer 做多元时间序列长周期预测有三个老毛病:训练复杂度随序列长度平方级膨胀、小数据集上泛化差、容易卡在次优局部极小值。这直接导致它在外汇或贵金属这种样本有限又要求精度的场景里很难落地。 SAMformer 的解法是浅层架构加锐度感知最小化(SAM)。浅层砍掉了冗余参数,降低了过拟合概率;SAM 让损失函数在参数微扰下仍保持平坦,模型对权重抖动更鲁棒,泛化能力因此提升。 在合成与真实数据集上,SAMformer 用显著更少的参数达到了和传统深 Transformer 相近甚至更好的精度,推理开销更适合资源受限的本地部署。对 MT5 端用 OpenCL 跑轻量模型而言,这意味着有可能在终端侧做长周期价格行为推断,而不必依赖云端重模型。 我们已在 OpenCL 端补齐新内核并强化了全连接层,本篇继续推进这套实现。
◍ 给卷积层接上 SAM 优化的两条权重通道
在已有卷积层 CNeuronConvOCL 之上派生的 CNeuronConvSAMOCL,核心只多做一件事:为 SAM 优化准备两套调整参数缓冲区。外出连接用 cWeightsSAM(沿用全连接层逻辑),收入连接用 cWeightsSAMConv,父类卷积层原本并没有这两份副本。 设计上曾两难:继承带 SAM 的全连接层能白嫖外出权重缓冲,但要重写整套卷积逻辑;继承卷积层保住现有功能,却缺外出缓冲。实际选了后者,工作量更小,只需在 Init 里补缓冲并在优化时调用对应内核。 Init 有两个重载。完整参数版接收模糊系数 fRho,非 0 时分别初始化 cWeightsSAMConv 与(存在外出连接时的)cWeightsSAM;为 0 则退化为普通优化,直接交父类。省略 fRho 的版本写死默认 0.7——这是 SAMformer 原论文给的模糊区域系数,换层时只改对象类型就能把普通卷积替成 SAM 版。 前向与梯度分发继承父类,仅新增 calcEpsilonWeights 和 feedForwardSAM 两个 OpenCL 包装:前者算调整参数,后者用调整缓冲做前向。训练保存时只存 fRho 不存调整缓冲;加载时依 fRho>0 才建缓冲,外出缓冲还需指针有效才建。外汇与贵金属模型训练属高风险实验,参数表现可能随品种与周期漂移。
class CNeuronConvSAMOCL : class="kw">public CNeuronConvOCL { class="kw">protected: class="type">class="kw">float fRho; class=class="str">"cmt">//--- CBufferFloat cWeightsSAM; CBufferFloat cWeightsSAMConv; class=class="str">"cmt">//--- class="kw">virtual class="type">bool calcEpsilonWeights(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool feedForwardSAM(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">public: CNeuronConvSAMOCL(class="type">void) { activation = GELU; } ~CNeuronConvSAMOCL(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 window_out, class="type">uint units_count, class="type">uint variables, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) class="kw">override; 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 window_out, class="type">uint units_count, class="type">uint variables, class="type">class="kw">float rho, ENUM_OPTIMIZATION optimization_type, class="type">uint batch); class=class="str">"cmt">//--- class="kw">virtual class="type">int Type(class="type">void) class="kw">const { class="kw">return defNeuronConvSAMOCL; }
「卷积层里塞进 SAM 权重缓冲的初始化细节」
CNeuronConvSAMOCL 在继承普通卷积层 CNeuronConvOCL 的基础上,多挂了两套 SAM 专用缓冲:cWeightsSAMConv 与 cWeightsSAM。它们不是每次都建,而是看 fRho 是否为 0——若你传 rho=0,类直接走 None 激活分支,跳过 SAM 缓冲创建,省一次 GPU 显存申请。 Init 的第一个重载把外部传入的 rho 取绝对值赋给 fRho,随后用 WeightsConv.Total() 和 Weights.Total() 分别初始化两套缓冲并推到 OpenCL 设备。注意第二个 Init 重载把 rho 硬编码成 0.7f 再转发,这意味着如果你图省事只调两参数版,默认就按 0.7 的平滑系数跑 SAM。外汇与贵金属行情用这类结构做特征提取时波动剧烈,属高风险,参数须自行回测。 updateInputWeights 里有个关键短路:fRho<=0 时直接调父类的卷积权重更新,完全不走 SAM 的 SumAndNormilize、calcEpsilonWeights 和 feedForwardSAM 流程。也就是说 rho 的开关直接决定了反向传播是否混入 SAM 修正项。 想验证也很直接:在 MT5 的 EA 里 new 一个 CNeuronConvSAMOCL,分别传 rho=0 与 rho=0.7f,用 Print(cWeightsSAM.GetIndex()) 看后者是否返回合法索引而前者倾向 -1。
class="kw">virtual class="type">int Activation(class="type">void) class="kw">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">//--- methods for working with files class="kw">virtual class="type">bool Save(class="type">int class="kw">const file_handle); class="kw">virtual class="type">bool Load(class="type">int class="kw">const file_handle); class=class="str">"cmt">//--- class="kw">virtual CLayerDescription* GetLayerInfo(class="type">void); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; class="type">bool CNeuronConvSAMOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window_in, class="type">uint step, class="type">uint window_out, class="type">uint units_count, class="type">uint variables, class="type">class="kw">float rho, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronConvOCL::Init(numOutputs, myIndex, open_cl, window_in, step, window_out, units_count, variables, optimization_type, batch)) class="kw">return class="kw">false; fRho = fabs(rho); if(fRho == class="num">0) class="kw">return true; cWeightsSAMConv.BufferFree(); if(!cWeightsSAMConv.BufferInit(WeightsConv.Total(), class="num">0) || !cWeightsSAMConv.BufferCreate(OpenCL)) class="kw">return class="kw">false; cWeightsSAM.BufferFree(); if(!Weights) class="kw">return true; if(!cWeightsSAM.BufferInit(Weights.Total(), class="num">0) || !cWeightsSAM.BufferCreate(OpenCL)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronConvSAMOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window_in, class="type">uint step, class="type">uint window_out, class="type">uint units_count, class="type">uint variables, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { class="kw">return CNeuronConvSAMOCL::Init(numOutputs, myIndex, open_cl, window_in, step, window_out, units_count, variables, class="num">0.7f, optimization_type, batch); } class="type">bool CNeuronConvSAMOCL::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(fRho <= class="num">0) class="kw">return CNeuronConvOCL::updateInputWeights(NeuronOCL); if(!SumAndNormilize(Gradient, Output, Gradient, iWindowOut, class="kw">false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return class="kw">false; if(!calcEpsilonWeights(NeuronOCL)) class="kw">return class="kw">false; if(!feedForwardSAM(NeuronOCL)) class="kw">return class="kw">false; class="type">class="kw">float error = class="num">1; if(!calcOutputGradients(Gradient, error)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return CNeuronConvOCL::updateInputWeights(NeuronOCL); } class="type">bool CNeuronConvSAMOCL::Save(class="kw">const class="type">int file_handle) { if(!CNeuronConvOCL::Save(file_handle))
卷积层权重落盘与重载的实现细节
在自定义卷积神经元类里,Save 方法先把父类的卷积权重写完后,继续用 FileWriteFloat 把 fRho 这个平滑系数写入文件句柄;只要任意一次写入字节数小于 INT_VALUE,就直接 return false,避免产生残缺模型文件。 Load 方法则先调用父类 CNeuronConvOCL::Load 恢复基础卷积参数,随后用 FileReadFloat 读回 fRho。若文件已到结尾(FileIsEnding 为真)说明存档损坏,返回 false。 当 fRho 读出来小于等于 0 时,代码并不报错,而是直接 return true——这意味着该神经元退化为无 SAM 修正的普通卷积层,训练时可能倾向跳过额外注意力权重。 若 fRho 有效,就重新在 OpenCL 显存上初始化 cWeightsSAMConv 与 cWeightsSAM 两个缓冲区;任一 BufferInit 或 BufferCreate 失败都返回 false。在 MT5 里打开对应 EA 源码,把 fRho 初值从 0 改成 0.1 再跑回测,能验证 SAM 分支是否被激活。
class="kw">return class="kw">false; if(FileWriteFloat(file_handle, fRho) < INT_VALUE) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronConvSAMOCL::Load(class="kw">const class="type">int file_handle) { if(!CNeuronConvOCL::Load(file_handle)) class="kw">return class="kw">false; if(FileIsEnding(file_handle)) class="kw">return class="kw">false; fRho = FileReadFloat(file_handle); cWeightsSAMConv.BufferFree(); cWeightsSAM.BufferFree(); cWeightsSAMConv.Clear(); cWeightsSAM.Clear(); if(fRho <= class="num">0) class="kw">return true; if(!cWeightsSAMConv.BufferInit(WeightsConv.Total(), class="num">0) || !cWeightsSAMConv.BufferCreate(OpenCL)) class="kw">return class="kw">false; if(!Weights) class="kw">return true; if(!cWeightsSAM.BufferInit(Weights.Total(), class="num">0) || !cWeightsSAM.BufferCreate(OpenCL)) class="kw">return class="kw">false; class=class="str">"cmt">//--- class="kw">return true; }
◍ 把 SAM 优化塞进 R-MAT 变换器
前面我们已经造好了带 SAM 的全连接层和卷积层对象,这一步是把它们直接嵌进相对注意力变换器(R-MAT)里。我们没有另起一个新类,而是改现有类结构,这样能更干净地看出 SAM 到底动了多少性能。基础架构选的是带相对注意力 R-MAT 的变换器,它本来就是 CNeuronRMAT 把 CNeuronRelativeSelfAttention 和 CResidualConv 串成一条线。 残差卷积那边最省事:只要在类结构里把卷积对象类型换成 CNeuronConvSAMOCL,类方法一行都不用动。原因在于我们重载的卷积初始化方法会自动接上 SAM 的默认模糊系数,CResidualConv 初始化时就会调到这个覆盖版,全卷积层跟着走 SAM。 相对注意力模块麻烦些,它内部还嵌套了可训练偏差模型,架构写在自己的初始化方法里。所以我们改了 CNeuronRelativeSelfAttention 的 Init:查询、主键、数值三个生成器换成 SAM 卷积;BKey 和 BValue 偏差模型里只换卷积类型、其余参数不动;全局上下文和定位偏差模型用 SAM 全连接层;池化 MLP 再用 SAM 卷积。 两层“存储层”故意留成基础全连接:第一层存多头注意力输出,缩放模块第一层存注意力权重乘输出的结果,都不参与 SAM。改完就能在 MT5 里挂上这段代码跑,看带 SAM 的 R-MAT 在验证集上的收敛曲线是否比原版更稳。外汇与贵金属行情噪声大,这类模型过拟合风险高,实盘前务必用历史片段交叉验证。
class CNeuronRelativeSelfAttention : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">uint iWindow; class="type">uint iWindowKey; class="type">uint iHeads; class="type">uint iUnits; class="type">int iScore; class=class="str">"cmt">//--- CNeuronConvSAMOCL cQuery; CNeuronConvSAMOCL cKey; CNeuronConvSAMOCL cValue; CNeuronTransposeOCL cTranspose; CNeuronBaseOCL cDistance; CLayer cBKey; CLayer cBValue; CLayer cGlobalContentBias; CLayer cGlobalPositionalBias; CLayer cMHAttentionPooling; CLayer cScale; CBufferFloat cTemp; class=class="str">"cmt">//--- class="kw">virtual class="type">bool AttentionOut(class="type">void); class="kw">virtual class="type">bool AttentionGradient(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 calcInputGradients(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL) class="kw">override; class="kw">public: CNeuronRelativeSelfAttention(class="type">void) : iScore(-class="num">1) {}; ~CNeuronRelativeSelfAttention(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, class="type">uint heads,