交易中的神经网络:对比形态变换器(终章)·进阶篇
◍ 注意力网络的逐层前向与前向梯度回传
这段实现把多层感知结构里的信息流向拆成两段:先正向把信号从第一隐藏层喂到末层,再反向把梯度从后向前算回输入侧。 正向循环从索引 2 开始,因为第 0、1 层已在外部初始化;每一层调用 FeedForward 时都拿上一层输出和当前注意力缓存做输入,任何一层返回 false 就直接中断整网推理。 反向的 calcInputGradients 从倒数第二层向前扫到索引 1,对隐藏层调 calcHiddenGradients 并把临时缓冲 cTemp 的指针传进去。当某层类型是 defNeuronRelativeCrossAttention 时,还会跑一次 SumAndNormilize 把梯度在特征维做归一,参数里最后一个 1 表示沿该轴求和。 在 MT5 里把这段贴进自定义网络类的 .mqh,把 defNeuronRelativeCrossAttention 的枚举值打印出来,能确认你的交叉注意力层是否真的走了归一分支,而不是静默跳过。外汇与贵金属模型训练波动剧烈,这类归一缺失往往让梯度在几十步内发散,属高风险调试项。
if (!neuron || !neuron.FeedForward(cLayers[class="num">0])) class="kw">return false; } for (class="type">int i = class="num">2; i < cLayers.Total(); i++) { neuron = cLayers[i]; if (!neuron.FeedForward(cLayers[i - class="num">1], NeuronOCL.getOutput())) class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronPropertyAwareAttention::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return false; if (cTemp.GetIndex() < class="num">0 || cTemp.Total() < NeuronOCL.Neurons()) { cTemp.BufferFree(); if (!cTemp.BufferInit(NeuronOCL.Neurons(), class="num">0) || !cTemp.BufferCreate(OpenCL)) class="kw">return false; } if (!NeuronOCL.getGradient() || !NeuronOCL.getGradient().Fill(class="num">0)) class="kw">return false; CNeuronBaseOCL *neuron = NULL; for (class="type">int i = cLayers.Total() - class="num">2; i > class="num">0; i--) { neuron = cLayers[i]; if (!neuron.calcHiddenGradients(cLayers[i + class="num">1], NeuronOCL.getOutput(), GetPointer(cTemp), (ENUM_ACTIVATION)NeuronOCL.Activation())) class="kw">return false; if (neuron.Type() == defNeuronRelativeCrossAttention) { if (!SumAndNormilize(NeuronOCL.getGradient(), GetPointer(cTemp), NeuronOCL.getGradient(), class="num">1, false, class="num">0, class="num">0, class="num">0, class="num">1)) class="kw">return false; } class=class="str">"cmt">//--- class="kw">return true; }
把原子与形态两条管道焊进一个类
AMCT 框架的核心不是再写新算子,而是把已有的烛条编码器、形态编码器、属性交叉注意力等模块塞进一个 CNeuronAMCT 对象里协同工作。类里只声明了组件和 cMotifProjection、cPropertyProjection 两个动态数组,所有成员都设成静态,构造析构留空,初始化全丢给 Init 方法按传入的主常量去建内部对象。 两条管道(烛条级、形态级)吃同一份输入数据,但输出张量维度不同,直接统调会打架。原文的做法是用轻量卷积模型把形态管道的输出先转置、再按「输入窗口=形态序列长度、过滤器数=烛条序列长度」做单变量序列缩放,每个序列元素配独立可学习权重矩阵,灵活度更高。 三种表征(烛条、形态、属性)最终被拉到统一维度级联成单一张量,再丢进基于依赖关系的池化层出最终值。这里有个实打实可验证的细节:形态输出是两级级联嵌入,转置后卷积步幅和窗口都等于形态序列长度,过滤器数对齐烛条序列长度,开 MT5 把这两个数值填进 CLayer 参数就能复现缩放逻辑。 反向传播里的 calcInputGradients 最易写错。因为 Init 时把类接口缓冲区指针直接替换成了池化对象缓冲,免了数据拷贝,梯度分派直接走内部对象。形态编码器输出同时喂给形态缩放模型,所以要用局部变量存原指针、换成双精度缓冲来合并二级信息流梯度,完了必须恢复指针。 两个坑得记牢:换指针时显式传 false 防删原缓冲,传 true 会删对象导致后续访问崩;原文没按作者原版做形态对比表征学习,而是把对比误差注入点挪到了相对交叉注意力对象里,相当于重构了误差回传路径。外汇与贵金属模型训练波动剧烈,这类自定义架构过拟合概率偏高,上实盘前先用历史 Tick 回测梯度稳定性。
class CNeuronAMCT : class="kw">public CNeuronBaseOCL { class="kw">protected: CNeuronRMAT cAtomEncoder; CNeuronMotifEncoder cMotifEncoder; CLayer cMotifProjection; CNeuronPropertyAwareAttention cPropertyDecoder; CLayer cPropertyProjection; CNeuronBaseOCL cConcatenate; CNeuronMHAttentionPooling cPooling; 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: CNeuronAMCT(class="type">void) {}; ~CNeuronAMCT(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 properties,
「AMCT 神经元的初始化链路」
CNeuronAMCT 在 MT5 的 OpenCL 环境里把原子编码、动机编码和属性解码三段拼成一条前向图,Init 方法是整张图的入口。先调基类 CNeuronBaseOCL::Init,传入的窗口尺寸会被乘以 units_count,这一步直接决定显存里特征张量的首维长度。 编码器部分依次挂 cAtomEncoder 与 cMotifEncoder,两者共用 window、window_key、units_count、heads、layers 五参数,idx 从 0 自增标识层序。cMotifEncoder 的神经元总数除以 window 得到 motifs,这个数字后续喂给转置与卷积层,是图结构宽度的核心变量。 动机投影分支里先 new 一个 CNeuronTransposeOCL 做 motifs×window 转置,再接 CNeuronConvOCL:卷积核参数写死为 motifs 输入、motifs 输出、units_count 通道、核宽 1、步长 window。卷积激活函数直接复用 cAtomEncoder 的激活枚举,转置层激活则复用卷积层,这种跨层借用省掉了重复配置。 属性解码侧同理,先建 cPropertyDecoder(properties 维度由外部传参),再补一个转置层把 properties×window 翻过来。整段代码没有硬编码具体数值收益,只在结构上传达:改 heads 或 layers 会同时撼动编码、投影、解码三端的张量形状,开 MT5 把 heads 从 4 调到 8 能直接看到显存占用量阶跃。
class="type">uint units_count, class="type">uint heads, class="type">uint layers, 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 defNeuronAMCT; } class=class="str">"cmt">//--- 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">float tau) class="kw">override; class="kw">virtual class="type">void SetOpenCL(COpenCLMy *obj) class="kw">override; }; class="type">bool CNeuronAMCT::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window, class="type">uint window_key, class="type">uint properties, class="type">uint units_count, class="type">uint heads, class="type">uint layers, 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; class="type">int idx = class="num">0; if (!cAtomEncoder.Init(class="num">0, idx, OpenCL, window, window_key, units_count, heads, layers, optimization, iBatch)) class="kw">return false; idx++; if (!cMotifEncoder.Init(class="num">0, idx, OpenCL, window, window_key, units_count, heads, layers, optimization, iBatch)) class="kw">return false; cMotifProjection.Clear(); cMotifProjection.SetOpenCL(OpenCL); class="type">int motifs = class="type">int(cMotifEncoder.Neurons() / window); idx++; CNeuronTransposeOCL *transp = new CNeuronTransposeOCL(); if (!transp || !transp.Init(class="num">0, idx, OpenCL, motifs, window, optimization, iBatch) || !cMotifProjection.Add(transp)) class="kw">return false; idx++; CNeuronConvOCL *conv = new CNeuronConvOCL(); if (!conv || !conv.Init(class="num">0, idx, OpenCL, motifs, motifs, units_count, class="num">1, window, optimization, iBatch) || !cMotifProjection.Add(conv)) class="kw">return false; conv.SetActivationFunction((ENUM_ACTIVATION)cAtomEncoder.Activation()); idx++; transp = new CNeuronTransposeOCL(); if (!transp || !transp.Init(class="num">0, idx, OpenCL, window, units_count, optimization, iBatch) || !cMotifProjection.Add(transp)) class="kw">return false; transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); idx++; if (!cPropertyDecoder.Init(class="num">0, idx, OpenCL, window, window_key, properties, motifs, heads, layers, optimization, iBatch)) class="kw">return false; cPropertyProjection.Clear(); cPropertyProjection.SetOpenCL(OpenCL); idx++; transp = new CNeuronTransposeOCL(); if (!transp || !transp.Init(class="num">0, idx, OpenCL, properties, window, optimization, iBatch) || !cPropertyProjection.Add(transp)) class="kw">return false; idx++; conv = new CNeuronConvOCL();
◍ AMCT 网络的前向拼接与梯度回传
这段 CNeuronAMCT 的初始化尾部,先把卷积层挂到属性投影上,再接一层转置卷积做上采样,最后用拼接+池化收口。卷积初始化传入 window 与 units_count,转置卷积同样吃 window 和 units_count,拼接层输入维度硬编码为 3 * window * units_count,池化层核长取 window、步长取 3,这些数字直接决定显存占用与每层张量形状。
| [CODE]if (!conv |
|---|
| !conv.Init(0, idx, OpenCL, properties, properties, units_count, 1, window, optimization, iBatch) |
!cPropertyProjection.Add(conv)) return false; conv.SetActivationFunction((ENUM_ACTIVATION)cAtomEncoder.Activation()); idx++; transp = new CNeuronTransposeOCL();
| if (!transp |
|---|
| !transp.Init(0, idx, OpenCL, window, units_count, optimization, iBatch) |
!cPropertyProjection.Add(transp)) return false; transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); idx++; if (!cConcatenate.Init(0, idx, OpenCL, 3 * window * units_count, optimization, iBatch)) return false; idx++; if(!cPooling.Init(0, idx, OpenCL, window, units_count, 3, optimization, iBatch)) return false;
| if (!SetOutput(cPooling.getOutput(), true) |
|---|
!SetGradient(cPooling.getGradient(), true)) return false; //--- return true; } [/CODE] 逐行看:conv 层 Init 第 7 个参数填 1,代表输出通道数为 1;transp 复用 conv 的激活函数枚举,保证编码-解码对称。cConcatenate 的 3 * window * units_count 来自原子、 motif、属性三路输出等长拼接,若你改了 window 不同步改这里会越界崩核。 feedForward 里先跑 Atom 与 Motif 双编码器,再走属性解码器,随后两个 for 循环分别把 motif 投影和属性投影串成链。拼接调用 Concat(...) 把三路输出按 window、window、window、units 排进 cConcatenate,最后 cPooling.FeedForward 出结果。 calcInputGradients 反向时先让 cConcatenate 从池化层拿隐藏梯度,再用 DeConcat 把梯度拆回原子、motif、属性三路。外汇与贵金属行情噪声大,这类深网在 MT5 实盘易过拟合,调 window 或 units 前建议在历史数据跑回测验证稳定性,杠杆品种高风险。
if (!conv || !conv.Init(class="num">0, idx, OpenCL, properties, properties, units_count, class="num">1, window, optimization, iBatch) || !cPropertyProjection.Add(conv)) class="kw">return false; conv.SetActivationFunction((ENUM_ACTIVATION)cAtomEncoder.Activation()); idx++; transp = new CNeuronTransposeOCL(); if (!transp || !transp.Init(class="num">0, idx, OpenCL, window, units_count, optimization, iBatch) || !cPropertyProjection.Add(transp)) class="kw">return false; transp.SetActivationFunction((ENUM_ACTIVATION)conv.Activation()); idx++; if (!cConcatenate.Init(class="num">0, idx, OpenCL, class="num">3 * window * units_count, optimization, iBatch)) class="kw">return false; idx++; if(!cPooling.Init(class="num">0, idx, OpenCL, window, units_count, class="num">3, optimization, iBatch)) class="kw">return false; if (!SetOutput(cPooling.getOutput(), true) || !SetGradient(cPooling.getGradient(), true)) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronAMCT::feedForward(CNeuronBaseOCL *NeuronOCL) { if(!cAtomEncoder.FeedForward(NeuronOCL)) class="kw">return false; if(!cMotifEncoder.FeedForward(NeuronOCL)) class="kw">return false; if(!cPropertyDecoder.FeedForward(cMotifEncoder.AsObject())) class="kw">return false; class=class="str">"cmt">//--- Motifs projection CNeuronBaseOCL *prev = cMotifEncoder.AsObject(); CNeuronBaseOCL *current = NULL; for(class="type">int i = class="num">0; i < cMotifProjection.Total(); i++) { current = cMotifProjection[i]; if(!current || !current.FeedForward(prev, NULL)) class="kw">return false; prev = current; } class=class="str">"cmt">//--- Property projection prev = cPropertyDecoder.AsObject(); for(class="type">int i = class="num">0; i < cPropertyProjection.Total(); i++) { current = cPropertyProjection[i]; if(!current || !current.FeedForward(prev, NULL)) class="kw">return false; prev = current; } class=class="str">"cmt">//--- Concatenate class="type">uint window = cAtomEncoder.GetWindow(); class="type">uint units = cAtomEncoder.GetUnits(); prev = cMotifProjection[cMotifProjection.Total() - class="num">1]; if(!Concat(cAtomEncoder.getOutput(), prev.getOutput(), current.getOutput(), cConcatenate.getOutput(), window, window, window, units)) class="kw">return false; class=class="str">"cmt">//--- Out if(!cPooling.FeedForward(cConcatenate.AsObject())) class="kw">return false; class=class="str">"cmt">//--- class="kw">return true; } class="type">bool CNeuronAMCT::calcInputGradients(CNeuronBaseOCL *NeuronOCL) { if(!NeuronOCL) class="kw">return false; if(!cConcatenate.calcHiddenGradients(cPooling.AsObject())) class="kw">return false; class="type">uint window = cAtomEncoder.GetWindow(); class="type">uint units = cAtomEncoder.GetUnits(); CNeuronBaseOCL *motifs = cMotifProjection[cMotifProjection.Total() - class="num">1]; CNeuronBaseOCL *prop = cPropertyProjection[cPropertyProjection.Total() - class="num">1]; if (!motifs || !prop || !DeConcat(cAtomEncoder.getGradient(), motifs.getGradient(), prop.getGradient(), cConcatenate.getGradient(),