交易中的神经网络:对比形态变换器(终章)·进阶篇
📘

交易中的神经网络:对比形态变换器(终章)·进阶篇

第 2/3 篇

◍ 注意力网络的逐层前向与前向梯度回传

这段实现把多层感知结构里的信息流向拆成两段:先正向把信号从第一隐藏层喂到末层,再反向把梯度从后向前算回输入侧。 正向循环从索引 2 开始,因为第 0、1 层已在外部初始化;每一层调用 FeedForward 时都拿上一层输出和当前注意力缓存做输入,任何一层返回 false 就直接中断整网推理。 反向的 calcInputGradients 从倒数第二层向前扫到索引 1,对隐藏层调 calcHiddenGradients 并把临时缓冲 cTemp 的指针传进去。当某层类型是 defNeuronRelativeCrossAttention 时,还会跑一次 SumAndNormilize 把梯度在特征维做归一,参数里最后一个 1 表示沿该轴求和。 在 MT5 里把这段贴进自定义网络类的 .mqh,把 defNeuronRelativeCrossAttention 的枚举值打印出来,能确认你的交叉注意力层是否真的走了归一分支,而不是静默跳过。外汇与贵金属模型训练波动剧烈,这类归一缺失往往让梯度在几十步内发散,属高风险调试项。

MQL5 / C++
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 回测梯度稳定性。

MQL5 / C++
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 能直接看到显存占用量阶跃。

MQL5 / C++
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 前建议在历史数据跑回测验证稳定性,杠杆品种高风险。

MQL5 / C++
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(),

常见问题

优先查每层输出是否带了不可导的硬截断,以及学习率是否高于稳定区间;逐层打印梯度范数可定位第几层消失。
在拼接前统一用线性映射压到同一隐维度,并在类构造里断言两边序列长度一致,否则直接抛错。
小布可加载你的品种页,自动按初始化链路生成网络结构并跑前向拼接诊断,把梯度异常标红,你只调超参。
形态管道偏置建议零初始化防早期偏移,原子管道用小的正随机更易跳出对称,具体看样本尺度。
对两条管道分别缓存中间激活,回传时复用;并把非关键层设为冻结,只训顶层注意力头。