神经网络变得简单(第 86 部分):U-形变换器·进阶篇
「U型注意力层的类结构与初始化链路」
在 MT5 的 OpenCL 神经网络扩展里,CNeuronUShapeAttention 继承自 CNeuronBaseOCL,内部用两个 CNeuronMLMHAttentionOCL 做上下分支注意力,再用两个 CNeuronConvOCL 做合并与拆分卷积,cNeck 指针则承接瓶颈层。 析构时只释放 cNeck,说明注意力与卷积子层走的是栈内数组生命周期,不需要手动 delete。这种结构在 EURUSD 的 1H 回测中,若 units_count 设 64、heads 设 4,可能比单层注意力更利于捕捉跨窗口依赖。 Init 里第一道校验是基类用 window * units_count 作为输出维度初始化;随后 cAttention[0] 和 cMergeSplit[0] 依次初始化,后者卷积核参数写死为 2*window 输入、4*window 输出、半宽 (units_count+1)/2。 当 inside_bloks>0 时会 new 一个同级 CNeuronUShapeAttention 做递归下采样,units_count 翻倍、inside_bloks 减 1,直到为 0 退出。复制下面代码到 MT5 的 Include/NeuralNetwork 目录,改 units_count 即可观察显存占用的非线性增长。
class CNeuronUShapeAttention : class="kw">public CNeuronBaseOCL { class="kw">protected: CNeuronMLMHAttentionOCL cAttention[class="num">2]; CNeuronConvOCL cMergeSplit[class="num">2]; CNeuronBaseOCL *cNeck; class=class="str">"cmt">//--- class="kw">virtual class="type">bool feedForward(CNeuronBaseOCL *NeuronOCL); class="kw">virtual class="type">bool calcInputGradients(CNeuronBaseOCL *prevLayer); class="kw">virtual class="type">bool updateInputWeights(CNeuronBaseOCL *NeuronOCL); class=class="str">"cmt">//--- class="kw">public: CNeuronUShapeAttention(class="type">void) {}; ~CNeuronUShapeAttention(class="type">void) { class="kw">delete cNeck; } 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 heads, class="type">uint units_count, class="type">uint layers, class="type">uint inside_bloks, 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 defNeuronUShapeAttention; } class=class="str">"cmt">//--- methods for working with files 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="kw">virtual CLayerDescription* GetLayerInfo(class="type">void); class="kw">virtual class="type">bool WeightsUpdate(CNeuronBaseOCL *net, class="type">class="kw">float tau); class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj); }; class="type">bool CNeuronUShapeAttention::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint heads, class="type">uint units_count, class="type">uint layers, class="type">uint inside_bloks, 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(!cAttention[class="num">0].Init(class="num">0, class="num">0, OpenCL, window, window_key, heads, units_count, layers, optimization, iBatch)) class="kw">return false; if(!cMergeSplit[class="num">0].Init(class="num">0, class="num">1, OpenCL, class="num">2 * window, class="num">2 * window, class="num">4 * window, (units_count + class="num">1) / class="num">2, optimization, iBatch)) class="kw">return false; if(inside_bloks > class="num">0) { CNeuronUShapeAttention *temp = new CNeuronUShapeAttention(); if(!temp) class="kw">return false; if(!temp.Init(class="num">0, class="num">2, OpenCL, window, window_key, heads, class="num">2 * units_count, layers, inside_bloks - class="num">1,
U型注意力网络的初始化与前向链路
这段实现展示了一个名为 CNeuronUShapeAttention 的自定义神经网络层,结构呈 U 型:两次注意力模块(cAttention[0]、cAttention[1])中间夹一个卷积瓶颈 cNeck,前后各接一个合并/拆分层。Init 里第二层注意力用 window_key 作键维度、heads 个头,输出通道固定为 2*units_count,这种对称设计在时序特征抽取里常见于把局部与全局依赖都吃进来。 初始化时若 temp->Init(0,2,OpenCL,window,window,window,2*units_count,optimization,iBatch) 返回失败会立刻 delete 并回 false,说明每层张量描述符(窗口大小、批大小 iBatch)必须和 OpenCL 上下文严格匹配,否则整层构建直接流产。 feedForward 的调用顺序就是 U 型数据流:注意力0 → 合并拆分0 → 瓶颈 → 注意力1 → 合并拆分1 → 残差求和归一(SumAndNormilize 带参数 1,false)。最后一步把输入 NeuronOCL 的输出与末层输出做加和归一,意味着网络默认带 skip connection,梯度不会在深层轻易消失。 calcInputGradients 反向走的是严格逆序,从 cAttention[1] 一路回传至 prevLayer,且同样用 SumAndNormilize 把梯度累加。在 MT5 里跑这类层时,把 iBatch 设成 32 或 64 通常比 1 更能压住 OpenCL 内核启动开销,但显存占用会随 window*window*2*units_count 线性涨,黄金分钟线小窗口先试 16 窗口较稳。外汇与贵金属模型训练属高风险实验,过拟合会让实盘信号失效。
if(!cAttention[class="num">1].Init(class="num">0, class="num">3, OpenCL, window, window_key, heads, class="num">2 * units_count, layers, optimization, iBatch)) class="kw">return false; if(!cMergeSplit[class="num">1].Init(class="num">0, class="num">4, OpenCL, class="num">2 * window, class="num">2 * window, window, units_count, optimization, iBatch)) class="kw">return false; if(Gradient != cMergeSplit[class="num">1].getGradient()) SetGradient(cMergeSplit[class="num">1].getGradient()); class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronUShapeAttention::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cAttention[class="num">0].FeedForward(NeuronOCL)) class="kw">return false; if(!cMergeSplit[class="num">0].FeedForward(cAttention[class="num">0].AsObject())) class="kw">return false; if(!cNeck.FeedForward(cMergeSplit[class="num">0].AsObject())) class="kw">return false; if(!cAttention[class="num">1].FeedForward(cNeck)) class="kw">return false; if(!cMergeSplit[class="num">1].FeedForward(cAttention[class="num">1].AsObject())) class="kw">return false; if(!SumAndNormilize(NeuronOCL.getOutput(), cMergeSplit[class="num">1].getOutput(), Output, class="num">1, false)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronUShapeAttention::calcInputGradients(CNeuronBaseOCL *prevLayer) { if(!prevLayer) class="kw">return false; if(!cAttention[class="num">1].calcHiddenGradients(cMergeSplit[class="num">1].AsObject())) class="kw">return false; if(!cNeck.calcHiddenGradients(cAttention[class="num">1].AsObject())) class="kw">return false; if(!cMergeSplit[class="num">0].calcHiddenGradients(cNeck.AsObject())) class="kw">return false; if(!cAttention[class="num">0].calcHiddenGradients(cMergeSplit[class="num">0].AsObject())) class="kw">return false; if(!prevLayer.calcHiddenGradients(cAttention[class="num">0].AsObject())) class="kw">return false; if(!SumAndNormilize(prevLayer.getGradient(), Gradient, prevLayer.getGradient(), class="num">1, false))
◍ U型注意力层的权重回传与存取实现
在 MT5 的 OpenCL 神经网络封装里,CNeuronUShapeAttention 的 updateInputWeights 方法按顺序串起 5 个内部层:两次注意力、两次合并拆分、以及中间的 neck 层。任何一层返回 false 就立刻中断,说明这套 U 型结构对权重更新顺序强依赖,调参时若某一子层学习率异常,整个网络回传会静默失败。 Save 与 Load 方法暴露了持久化结构:循环 2 次写/读 cAttention 与 cMergeSplit,再单独处理 cNeck。Load 里先用 FileReadInteger 读类型标签,若已有 cNeck 且类型不匹配就 delete 重建——这意味着你换模型拓扑后直接读旧文件,可能悄悄丢掉不兼容的 neck 层而不报错。 验证方式很直接:在 MT5 终端新建 EA,include 相关神经网络头文件,给 CNeuronUShapeAttention 实例分别调用 Save/Load 并打印 file_handle 返回值;若 Load 后 cNeck.Type() 与你 new 的类型不一致,基本能锁定是旧缓存文件在作怪。外汇与贵金属品种上跑这类模型,杠杆风险高,回测与实盘偏差可能放大,任何信号都只是概率倾向。
class="kw">return false; if(!DeActivation(prevLayer.getOutput(), prevLayer.getGradient(), prevLayer.getGradient(), prevLayer.Activation())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronUShapeAttention::updateInputWeights(CNeuronBaseOCL *NeuronOCL) { if(!cAttention[class="num">0].UpdateInputWeights(NeuronOCL)) class="kw">return false; if(!cMergeSplit[class="num">0].UpdateInputWeights(cAttention[class="num">0].AsObject())) class="kw">return false; if(!cNeck.UpdateInputWeights(cMergeSplit[class="num">0].AsObject())) class="kw">return false; if(!cAttention[class="num">1].UpdateInputWeights(cNeck)) class="kw">return false; if(!cMergeSplit[class="num">1].UpdateInputWeights(cAttention[class="num">1].AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronUShapeAttention::Save(const class="type">int file_handle) { if(!CNeuronBaseOCL::Save(file_handle)) class="kw">return false; for(class="type">int i = class="num">0; i < class="num">2; i++) { if(!cAttention[i].Save(file_handle)) class="kw">return false; if(!cMergeSplit[i].Save(file_handle)) class="kw">return false; } if(!cNeck.Save(file_handle)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronUShapeAttention::Load(const class="type">int file_handle) { if(!CNeuronBaseOCL::Load(file_handle)) class="kw">return false; for(class="type">int i = class="num">0; i < class="num">2; i++) { if(!LoadInsideLayer(file_handle, cAttention[i].AsObject())) class="kw">return false; if(!LoadInsideLayer(file_handle, cMergeSplit[i].AsObject())) class="kw">return false; } class="type">int type = FileReadInteger(file_handle); if(!!cNeck) { if(cNeck.Type() != type) class="kw">delete cNeck; } if(!cNeck) { class="kw">switch(type) { case defNeuronUShapeAttention: cNeck = new CNeuronUShapeAttention(); if(!cNeck) class="kw">return false; break; case defNeuronConvOCL: cNeck = new CNeuronConvOCL(); if(!cNeck)
「编码器网络层的逐层装配」
在 MT5 的 OpenCL 神经网络框架里,CreateEncoderDescriptions 负责把编码器各层描述压进 CArrayObj 容器。函数开头先判空,若传入指针为空就 new 一个,分配失败直接返回 false,避免后续空指针崩在 GPU 内核里。 输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,激活函数设 None、优化器走 ADAM。紧接着三层分别是 BatchNorm(batch=1000)、Dropout(probability=0.4f)和两次一维卷积 defNeuronConvOCL,卷积窗宽与步长都等于 BarDescr,把 (HistoryBars*BarDescr) 压到 HistoryBars 个通道。 第二层卷积的 window_out 被设为 EmbeddingSize,而第一层卷积的 window_out = EmbeddingSize/2,这两处常量直接决定嵌入向量的维度上限。你在调参时若改了 EmbeddingSize,必须同步核对 layer3/layer4 的 window_out,否则张量形状对不上,Load 模型时可能返回 false。 每层 Add 失败都走 delete descr + return false 的清理路径,这说明该构建过程不支持部分成功——任一层描述没进数组,整个编码器就废了,调用方应检查返回值再决定要不要跑训练。
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">1000; 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 = defNeuronDropoutOCL; descr.count = prev_count; descr.probability = class="num">0.4f; descr.activation = None; 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 = defNeuronConvOCL; prev_count = descr.count = HistoryBars; descr.window = BarDescr; descr.step = descr.window; class="type">int prev_wout = descr.window_out = EmbeddingSize / class="num">2; 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 = defNeuronConvOCL; descr.count = prev_count; descr.window = prev_wout; descr.step = prev_wout; prev_wout = descr.window_out = EmbeddingSize; 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;
编码器尾段与双网络描述装配
编码器第 6 层用了 U 型注意力结构,窗口滑动步长写死为 4,内部叠了 3 个子层、批大小 2;这种配置在贵金属 1 小时序列上可能让注意力更偏中期依赖,但显存占用会随 EmbeddingSize 放大。 第 7 到 9 层是三层普通 OCL 全连接,激活函数分别取 SIGMOID、LReLU、TANH,最后一层输出节点数由 BarDescr*(HistoryBars+NForecast) 算出,直接决定了解码端的还原维度。 第 10 层挂了 RevInDenormOCL 做反归一化,优化器指定 ADAM、层数 1,意味着逆标准化权重也进反向传播。外汇与贵金属杠杆高,这类端到端结构过拟合概率不低,上 MT5 前建议先拿历史样本跑一遍梯度检查。 CreateDescriptions 里 Actor / Critic 两个容器若为空会现场 new,但清空动作只调了 actor.Clear(),critic 旧描述不会自动释放,复制这段代码时得补一行 critic.Clear() 避免重复叠层。
descr.type = defNeuronLearnabledPE; descr.count = prev_count*prev_wout; 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 = defNeuronUShapeAttention; descr.count = prev_count; descr.window = prev_wout; descr.step = class="num">4; descr.window_out = EmbeddingSize; descr.layers = class="num">3; descr.batch = class="num">2; 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 = defNeuronBaseOCL; descr.count = LatentCount; descr.activation=SIGMOID; 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 = defNeuronBaseOCL; descr.count = LatentCount; descr.activation=LReLU; 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; descr.type = defNeuronBaseOCL; prev_count=descr.count = BarDescr*(HistoryBars+NForecast); descr.activation=TANH; 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 = defNeuronRevInDenormOCL; prev_count = descr.count = prev_count; 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">//--- class="kw">return true; } class="macro">#define LatentLayer class="num">9 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 false; } if(!critic) { critic = new CArrayObj(); if(!critic) class="kw">return false; } class=class="str">"cmt">//--- Actor actor.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 = AccountDescr; descr.activation = None; descr.optimization = ADAM;