交易中的神经网络:降低锐度强化变换器效率(终章)·进阶篇
残差卷积与自注意力神经元的类结构
在 MT5 的 OpenCL 神经网实现里,相对自注意力神经元通过 defNeuronRelativeSelfAttention 类型标识,对外暴露窗口与单元数两个只读参数:GetWindow 返回 iWindow,GetUnits 返回 iUnits。这两个值在卷积类里常被当作时序长度与特征宽度传入。 CResidualConv 类内部用长度为 3 的数组 cConvs[3] 与 cNorm[3] 存放三层卷积与批归一化对象,说明残差块固定堆叠 3 次卷积-归一化。外部只需调用一次 Init,传入 window 与 window_out 即可让底层展开这三层。 如果你要在自己的 EA 里复用这类结构,直接照 Init 的参数表填:numOutputs 是下一层节点数,window 是输入窗口(如 64),count 是卷积核数,batch 是训练批大小。填错 batch 会导致显存分配失败,MT5 终端会报 4014 错误码。
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 class="kw">const { class="kw">return defNeuronRelativeSelfAttention; } class=class="str">"cmt">//--- class="kw">virtual class="type">bool Save(class="type">int class="kw">const file_handle) class="kw">override; class="kw">virtual class="type">bool Load(class="type">int class="kw">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 GetWindow(class="type">void) class="kw">const { class="kw">return iWindow; } class="kw">virtual class="type">uint GetUnits(class="type">void) class="kw">const { class="kw">return iUnits; } }; class CResidualConv : class="kw">public CNeuronBaseOCL { class="kw">protected: class="type">int iWindowOut; class=class="str">"cmt">//--- CNeuronConvSAMOCL cConvs[class="num">3]; CNeuronBatchNormOCL cNorm[class="num">3]; CNeuronBaseOCL cTemp; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer); class="kw">public: CResidualConv(class="type">void) {}; ~CResidualConv(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 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">const { class="kw">return defResidualConv; } 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="kw">virtual CLayerDescription* GetLayerInfo(class="type">void); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); class="kw">virtual class="type">void TrainMode(class="type">bool flag); };
◍ 相对自注意力层的初始化链路
在 MT5 的 OpenCL 神经网络框架里,CNeuronRelativeSelfAttention 的 Init 方法是把多头相对注意力拆成若干子层并逐一挂载的地方。它先调用基类 CNeuronBaseOCL::Init,输入维度按 window * units_count 算,这一步失败直接返回 false,整个层不生效。 随后把窗口长度、键值窗口、单元数与头数存进成员变量:iWindow、iWindowKey、iUnits、iHeads。这四个量决定了后面所有卷积与矩阵层的形状,改一个就可能让显存布局对不上。 接着用 idx 从 0 开始逐个初始化 cQuery、cKey、cValue 三个分支,激活函数都设成 GELU;cTranspose 与 cDistance 分别按 iUnits 维度和 iUnits*iUnits 维度建层。 真正吃显存的是后面四段 CNeuronConvSAMOCL:两组进 cBKey、两组进 cBValue,其中带 TANH 激活的负责单元间映射,不带的那组按 iWindowKey*iHeads 做时序卷积。 最后挂一个 CNeuronBaseSAMOCL 到 cGlobalContentBias,并把输出缓冲区写成 1×1 的常量偏置。外汇与贵金属行情序列用这类结构做特征提取时波动剧烈,过拟合概率偏高,建议先用小 iHeads(如 2~4)在 MT5 策略测试器里跑通再放大。
class="type">bool CNeuronRelativeSelfAttention::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, ENUM_OPTIMIZATION optimization_type, class="type">uint batch) { if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count, optimization_type, batch)) class="kw">return class="kw">false; class=class="str">"cmt">//--- iWindow = window; iWindowKey = window_key; iUnits = units_count; iHeads = heads; class=class="str">"cmt">//--- class="type">int idx = class="num">0; if(!cQuery.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return class="kw">false; cQuery.SetActivationFunction(GELU); idx++; if(!cKey.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return class="kw">false; cKey.SetActivationFunction(GELU); idx++; if(!cValue.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch)) class="kw">return class="kw">false; cKey.SetActivationFunction(GELU); idx++; if(!cTranspose.Init(class="num">0, idx, OpenCL, iUnits, iWindow, optimization, iBatch)) class="kw">return class="kw">false; idx++; if(!cDistance.Init(class="num">0, idx, OpenCL, iUnits * iUnits, optimization, iBatch)) class="kw">return class="kw">false; idx++; CNeuronConvSAMOCL *conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iUnits, iUnits, iWindow, iUnits, class="num">1, optimization, iBatch) || !cBKey.Add(conv)) class="kw">return class="kw">false; idx++; conv.SetActivationFunction(TANH); conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch) || !cBKey.Add(conv)) class="kw">return class="kw">false; idx++; conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iUnits, iUnits, iWindow, iUnits, class="num">1, optimization, iBatch) || !cBValue.Add(conv)) class="kw">return class="kw">false; idx++; conv.SetActivationFunction(TANH); conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iWindowKey * iHeads, iUnits, class="num">1, optimization, iBatch) || !cBValue.Add(conv)) class="kw">return class="kw">false; idx++; CNeuronBaseOCL *neuron = new CNeuronBaseSAMOCL(); if(!neuron || !neuron.Init(iWindowKey * iHeads * iUnits, idx, OpenCL, class="num">1, optimization, iBatch) || !cGlobalContentBias.Add(neuron)) class="kw">return class="kw">false; idx++; CBufferFloat *buffer = neuron.getOutput(); buffer.BufferInit(class="num">1, class="num">1); if(!buffer.BufferWrite()) class="kw">return class="kw">false; neuron = new CNeuronBaseSAMOCL(); if(!neuron ||
「多头注意力池化与缩放层的 OpenCL 装配细节」
这段初始化逻辑在 MT5 的 OpenCL 环境里把多头自注意力池化(cMHAttentionPooling)和缩放(cScale)两组子网络逐个挂到索引上。每加一个神经元或卷积层,idx 都会自增,一旦 Init 或 Add 返回 false,整个构建直接 return false,所以层参数写错一个就可能让模型加载失败。 先看注意力侧:全局内容偏置(cGlobalContentBias)用 iWindowKey * iHeads * iUnits 作输出维度,而位置偏置(cGlobalPositionalBias)分两路,一路输出维度为 1、一路为 iWindowKey * iHeads * iUnits。随后卷积层 CNeuronConvSAMOCL 第一层核宽 iWindow、输出 iWindowKey*iHeads,激活 TANH;第二层核宽 iWindow、输出 iHeads,激活 None;最后接 CNeuronSoftMaxOCL, heads 设为 iUnits。 缩放侧 cScale 先接一个 BaseOCL(输出 iWindowKey*iUnits),再接两个 ConvSAMOCL:第一层窗口 2*iWindow、核 iWindowKey、输出 iUnits、激活 LReLU;第二层窗口 iWindow、核 2*iWindow、激活 None。末尾把 conv 的梯度设为可训练(SetGradient(..., true))。 在 MT5 里跑这套,重点核对 iWindow、iHeads、iUnits 三个量:比如 iWindow=10、iHeads=4、iUnits=8 时,注意力第一卷积输出维度就是 40。数值对不上,OpenCL 显存分配会直接报错。
if(!neuron || !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) || !cGlobalContentBias.Add(neuron)) class="kw">return class="kw">false; idx++; neuron = new CNeuronBaseSAMOCL(); if(!neuron || !neuron.Init(iWindowKey * iHeads * iUnits, idx, OpenCL, class="num">1, optimization, iBatch) || !cGlobalPositionalBias.Add(neuron)) class="kw">return class="kw">false; idx++; buffer = neuron.getOutput(); buffer.BufferInit(class="num">1, class="num">1); if(!buffer.BufferWrite()) class="kw">return class="kw">false; neuron = new CNeuronBaseSAMOCL(); if(!neuron || !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) || !cGlobalPositionalBias.Add(neuron)) class="kw">return class="kw">false; idx++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads * iUnits, optimization, iBatch) || !cMHAttentionPooling.Add(neuron) ) class="kw">return class="kw">false; idx++; conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindowKey * iHeads, iWindowKey * iHeads, iWindow, iUnits, class="num">1, optimization, iBatch) || !cMHAttentionPooling.Add(conv) ) class="kw">return class="kw">false; idx++; conv.SetActivationFunction(TANH); conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindow, iWindow, iHeads, iUnits, class="num">1, optimization, iBatch) || !cMHAttentionPooling.Add(conv) ) class="kw">return class="kw">false; idx++; conv.SetActivationFunction(None); CNeuronSoftMaxOCL *softmax = new CNeuronSoftMaxOCL(); if(!softmax || !softmax.Init(class="num">0, idx, OpenCL, iHeads * iUnits, optimization, iBatch) || !cMHAttentionPooling.Add(softmax) ) class="kw">return class="kw">false; softmax.SetHeads(iUnits); idx++; neuron = new CNeuronBaseOCL(); if(!neuron || !neuron.Init(class="num">0, idx, OpenCL, iWindowKey * iUnits, optimization, iBatch) || !cScale.Add(neuron) ) class="kw">return class="kw">false; idx++; conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, iWindowKey, iWindowKey, class="num">2 * iWindow, iUnits, class="num">1, optimization, iBatch) || !cScale.Add(conv) ) class="kw">return class="kw">false; conv.SetActivationFunction(LReLU); idx++; conv = new CNeuronConvSAMOCL(); if(!conv || !conv.Init(class="num">0, idx, OpenCL, class="num">2 * iWindow, class="num">2 * iWindow, iWindow, iUnits, class="num">1, optimization, iBatch) || !cScale.Add(conv) ) class="kw">return class="kw">false; conv.SetActivationFunction(None); class=class="str">"cmt">//--- if(!SetGradient(conv.getGradient(), true)) class="kw">return class="kw">false; class=class="str">"cmt">//---
把 OpenCL 上下文塞进智能指针
在 MQL5 里用智能指针管理 OpenCL 资源时,最后一步往往是把外部创建的上下文对象交给指针托管。上面这段没有独立函数名的收尾代码,作用就是调用 SetOpenCL(OpenCL) 把上下文绑进智能指针,紧接着 return true 表示初始化链路走通。 注意这里 OpenCL 变量应是前面已经成功创建的上下文句柄;若前面 clCreateContext 返回空,这步 SetOpenCL 等于接管了一个无效资源,后续内核编译会直接失败。开 MT5 把这段接在你自己的初始化函数末尾,跑一次看 return 是否为 true 即可验证上下文托管是否生效。
SetOpenCL(OpenCL); class=class="str">"cmt">//--- class="kw">return true; }
◍ 环境编码器与策略网络的层堆叠细节
这套模型把环境编码器放在最前面,前两层保持不动:源数据层尺寸等于 HistoryBars * BarDescr,紧接一个批大小 1e4 的批量归一化层。这两层只负责把原始张量原样收住,不掺注意力。 从第三层开始走 SAMformer 的通道注意力路线。先过数据转置层把历史深度和通道数对调,再接一个相对注意力模块(defNeuronRMAT),分析窗口等于历史深度,元素数等于独立通道数。原框架只用 1 个注意力头且删了 FeedForward,这里改成 2 个头并保留前馈,模糊系数统一给 0.7。 降维分两步走:先用带 SAM 的卷积层压通道维度,再用带 SAM 的全连接层吐出固定大小的环境嵌入。所有 descr.probability 都填 0.7,意味着每层权重更新都留 30% 模糊区,训练时梯度可能更稳但收敛速度倾向变慢。 参与者网络首层是 SAM 全连接,尺寸对齐账户状态向量;之后两个 SAM 全连接做出账户嵌入,与环境嵌入做串联。决策模块三层 SAM 全连接输出两倍动作向量,前半是均值后半是方差,借自动编码器潜层把随机性塞进政策里。多头/空头参数各过一层 SAM 卷积再 sigmoid,保证仓位、止盈、止损一组参数内部自洽。 评论者架构几乎镜像,只是把账户状态换成参与者产出的交易参数,决策模块不去随机化,顶层同样挂频率增益前馈层。下面这段是编码器前几层的实际声明,逐行对应上面说的结构。
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 class="kw">false; } class=class="str">"cmt">//--- Encoder encoder.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseOCL; class=class="str">"cmt">// 源数据层:基础神经元 class="type">int prev_count = descr.count = (HistoryBars * BarDescr); class=class="str">"cmt">// 输入尺寸=历史柱*每柱描述 descr.activation = None; class=class="str">"cmt">// 无激活 descr.optimization = ADAM; class=class="str">"cmt">// ADAM优化 if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBatchNormOCL; class=class="str">"cmt">// 批量归一化层 descr.count = prev_count; class=class="str">"cmt">// 尺寸同输入 descr.batch = class="num">1e4; class=class="str">"cmt">// 批大小1万 descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronTransposeOCL; class=class="str">"cmt">// 转置层:换轴 descr.count = HistoryBars; descr.window= BarDescr; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">3 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronRMAT; class=class="str">"cmt">// 相对注意力模块 descr.window=HistoryBars; class=class="str">"cmt">// 窗口=历史深度 descr.count=BarDescr; class=class="str">"cmt">// 计数=通道数 descr.window_out = EmbeddingSize/class="num">2; class=class="str">"cmt">// Key维度=嵌入一半 descr.layers = class="num">1; class=class="str">"cmt">// 层数=class="num">1
「编码器尾部与 actor 网络的层定义」
编码器最后三层把卷积采样和基线层收口:第 3 层用 1e4 的 batch、ADAM 优化、无激活;第 4 层是 ConvSAMOCL,window 与 step 都等于 HistoryBars,dropout 概率锁在 0.7,输出维度由 LatentCount/BarDescr 决定;第 5 层退回 BaseSAMOCL,count 等于 LatentCount,同样 0.7 丢弃率。任何一层 Add 失败就 delete 描述符并返 false,这种写法能在 MT5 里避免内存泄漏但会中断整个模型构建。 CreateDescriptions 先确保 actor 与 critic 两个 CArrayObj 指针有效,缺则 new 出来,失败直接返 false;随后 actor.Clear() 清空旧描述,从输入层开始堆:输入层 type 为 BaseSAMOCL,count 取自 AccountDescr,激活 None、概率 0.7。 actor 的第 1 层把维度压到 EmbeddingSize,激活换 SIGMOID;第 2 层用 Concatenate 把 LatentCount 与 EmbeddingSize 做窗口拼接,step 设为 LatentCount,激活改 LReLU。外汇与贵金属行情下用这类 RL 结构做信号生成属高风险,过拟合概率偏高,建议先用历史 Bars 跑离线校验。
descr.step = class="num">2; 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 class="kw">false; } class=class="str">"cmt">//--- layer class="num">4 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConvSAMOCL; descr.count = BarDescr; descr.window = HistoryBars; descr.step = HistoryBars; descr.window_out = LatentCount/BarDescr; descr.probability = class="num">0.7f; descr.activation = GELU; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">5 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseSAMOCL; descr.count = LatentCount; descr.probability = class="num">0.7f; descr.activation = None; descr.optimization = ADAM; if(!encoder.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CreateDescriptions(CArrayObj *&actor, CArrayObj *&critic) { class=class="str">"cmt">//--- CLayerDescription *descr; class=class="str">"cmt">//--- if(!actor) { actor = new CArrayObj(); if(!actor) class="kw">return class="kw">false; } if(!critic) { critic = new CArrayObj(); if(!critic) class="kw">return class="kw">false; } class=class="str">"cmt">//--- Actor actor.Clear(); class=class="str">"cmt">//--- Input layer if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseSAMOCL; class="type">int prev_count = descr.count = AccountDescr; descr.activation = None; descr.probability=class="num">0.7f; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">1 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronBaseSAMOCL; prev_count = descr.count = EmbeddingSize; descr.activation = SIGMOID; descr.optimization = ADAM; descr.probability=class="num">0.7f; if(!actor.Add(descr)) { class="kw">delete descr; class="kw">return class="kw">false; } class=class="str">"cmt">//--- layer class="num">2 if(!(descr = new CLayerDescription())) class="kw">return class="kw">false; descr.type = defNeuronConcatenate; descr.count = LatentCount; descr.window = EmbeddingSize; descr.step = LatentCount; descr.activation = LReLU; descr.optimization = ADAM; if(!actor.Add(descr)) { class="kw">delete descr;