神经网络变得简单(第 78 部分):带有变换器的无解码对象检测器(DFFT)(基础篇)
📘

神经网络变得简单(第 78 部分):带有变换器的无解码对象检测器(DFFT)(基础篇)

第 1/3 篇

用变换器做无解码检测的思路

这一节把第 78 部分的主线点明:在 MT5 环境下,尝试把带变换器(Transformer)结构的 DFFT 检测器搬进行情分析流程,目标是绕开传统解码器做对象检测。 原作者在 2024 年 11 月 21 日发布该文,截至统计节点文章交互量为 732 次浏览、1 条评论,说明这类结合深度学习结构的 MQL5 实验仍属小众分支。 对交易者而言,直接价值不在于立刻跑出信号,而是确认一件事:MT5 的自定义指标与 EA 框架,已能承载轻量变换器推理逻辑,为后续价格形态的非 Anchor 检测留出接口。外汇与贵金属品种波动受宏观扰动大,此类实验策略回测未含实盘成本,接入前须自测滑点。

◍ 从预测轨迹到绕开解码器

过去做价格建模,大多先预测接下来的走势轨迹,再按轨迹挑交易动作。问题是这类模型训练和操作都吃算力,而且预测准头远不理想——轨迹预测里的误差,会顺着传到动作选择上,造成更大的下单偏差。 随机误差下,扮演者没法稳定适应,动作错误只会叠加;只有误差偏恒定,学习过程才勉强能抵消一部分。既然目标是单笔盈利而不是画准整条曲线,去掉「预测轨迹」这个中间层,直接让智能体看历史数据选动作,反而像往侧面迈了一步,不是退回去用老办法。 计算机视觉里的 DFFT(无解码器全变换器)思路值得借:原论文把它做成单编码器单级密集预测,砍掉低效解码器,靠强编码器保精度,还专门学低级语义特征。实验里明确写了计算成本降了、训练局数更少——这对 MT5 上跑不起大模型的使用者是个实在信号。 外汇和贵金属波动杠杆高,任何省略预测层的做法都只是降低算力门槛,不保证胜率,上线前务必用历史数据自测。

「DFFT 怎么把变换器塞进实时检测」

无解码器完全基于变换器(DFFT)是一条为检测任务重写的纯编码器路线:主干在四个尺度抽特征,后面只挂一个密度预测头,不再做解码器式的复杂重构。它先用尺度聚合编码器(SAE)把多尺度特征压成单张映射,再用任务对齐编码器(TAE)在同一头里同步出分类与回归两支函数,避免双分支各算各的导致预测打架。 主干叫 DOT,堆了一个嵌入模块加 4 个 DOT 阶段。传统做法在高分辨率密集预测时把多目自注意力(MSA)换成局部空间注意力和窗口偏置 MSA(SW-MSA),算力下来了,但只拿到有限低级语义,检测精度会掉。DFFT 在 DOT 里补了若干 SW-MSA 和一个跨通道全局注意力模块,每个注意力模块都带一层 FFN。 作者发现,连续局部空间注意力之后,在通道上放一个轻量注意力层,能帮模型推断各尺度对象的语义。进一步还加了语义增强注意力(SAA):每两个连续 DOT 阶段之间,用上采样加跨通道全局注意力交换并补充语义,第一阶段因没有前序阶段而不带 SAA,之后每个阶段都是 DOT 模块加 SAA,再接下采样重构维度。 SAE 由 3 个模块构成,每次吃两个对象逐步聚合,用有限聚合尺度换精度和算力的平衡。TAE 则把跨通道注意力组合进连接头组,用组注意力对齐切分、用全局注意力编码其中一支供回归,投影上做隔离但注意力内交互。要在 MT5 之外验证这类结构,可直接找原论文图对照 DOT 与 SAA 的衔接位置,确认自己复现时语义流是否断了。

在 MT5 里搭一个窗口式自注意力的神经层

DFFT 原文用无解码器变换器,但落到外汇行情上不能照抄。我们新建 CNeuronDOTOCL 层,继承自 CNeuronBaseOCL,关键点在于引入 iPrevWindowSize 变量——它让每一层能把上一层序列压缩一倍,正好对应 DFFT 的逐层降维思路。 窗口式自注意力跟标准 Transformer 不同:每个 token 只和窗口内 token 算依赖。代码里我们限定每个对象只盯 2 个最近邻,所以 Score 缓冲区大小定为 iUnits * iHeads * 3。这个系数每次前馈重算、反向传播用一次就丢,不写进模型文件,只在 OpenCL 显存开一块指针,主程序不落地,省内存也防过拟合泄露。 相对位置不能忽略。我们加可训练参数 cRelativePositionsBias,对窗口内每对 (i,j) 给一个权重,缓冲区大小和 Score 一致。训练时除了值本身还要额外 Adam 动量缓冲,所以把它包成一个神经层对象,代码可读性比散着写高不少。 前馈内核 DOTFeedForward 按 3 维任务空间发,Query/Key/Value 拼在 qkv 缓冲,score、rpb、out 各一块。局部语义先算近邻影响:Q·K 并行乘、求和、加偏移、SoftMax,结果乘 V 写回。反向内核 DOTInsideGradients 同样 3 维,但 Value 梯度=接收梯度×影响系数,Query 梯度要先对 SoftMax 求导再乘 K,位置偏移的误差梯度从这里翻倍传回去,学习率调小就能吃掉。 EA 端 Research.mq5 只在新柱线跑 OnTick,先更新历史、填环境状态缓冲、并账户状态、盖时间戳,然后编码器前馈+扮演者前馈,结果解码下单,原始数据塞进经验回放缓冲。Test.mq5 算法一样但不写回放,专做干净样本外测试。两者都可在 MT5 里直接挂 EURUSD 或 XAUUSD 图验证,外汇/贵金属杠杆高,样本外回测亏损概率不低,别当实盘依据。

◍ 训练循环里怎么喂历史堆栈

模型训练 EA 放在 Experts\DFFT\Study.mq5,核心只改了 Train 方法。开头先按盈利能力给轨迹算概率向量,盈利高的验算被抽中去训练的频率更大,这步决定了样本偏向。 编码器对历史序列高度敏感,所以外循环从经验回放缓冲区抽轨迹当初始状态,并先清掉模型内部堆栈。嵌套循环里取连续历史状态,训练批次设成模型内部轨迹深度多 2 天,避免窗口不够导致时序错位。 扮演者前馈前,要从回放缓冲区填账户状态缓冲并加时间戳,而不是像环境交互时去轮询实时环境。之后跑扮演者和评论者验算,把动作梯度送回编码器,并在 TAE 模块做对象分类。 DFFT 跟多数避免互拟合的思路相反,它故意让编码器与评论者互相拟合,作者认为这样能调出提取信息最多的编码器参数。训练完打印进度,全部迭代后清注释、结果进日记并结束 EA。 下面这段 CNeuronDOTOCL 类声明,是上述训练里多头注意力与交叉注意力的承载结构,可直接在 MT5 附带的 Study.mq5 里对照看。

MQL5 / C++
<span class="keyword">class</span> CNeuronDOTOCL&nbsp;&nbsp;&nbsp;&nbsp; :&nbsp;&nbsp;<span class="keyword">class="kw">public</span> CNeuronBaseOCL
&nbsp;&nbsp;{
<span class="keyword">class="kw">protected</span>:
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iWindowSize;
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iPrevWindowSize;
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iDimension;
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iUnits;
&nbsp;&nbsp; <span class="keyword">class="type">uint</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;iHeads;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cProjInput;
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cQKV;
&nbsp;&nbsp; <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; iScoreBuffer;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;cRelativePositionsBias;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;MHAttentionOut;
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cProj;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;AttentionOut;
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cFF1;
&nbsp;&nbsp; CNeuronConvOCL&nbsp;&nbsp;&nbsp;&nbsp;cFF2;
&nbsp;&nbsp; CNeuronBaseOCL&nbsp;&nbsp;&nbsp;&nbsp;SAttenOut;
&nbsp;&nbsp; CNeuronXCiTOCL&nbsp;&nbsp;&nbsp;&nbsp;cCAtten;
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;feedForward(CNeuronBaseOCL *NeuronOCL);
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DOT(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;updateInputWeights(CNeuronBaseOCL *NeuronOCL);
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;updateRelativePositionsBias(<span class="keyword">class="type">void</span>);
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;DOTInsideGradients(<span class="keyword">class="type">void</span>);
<span class="keyword">class="kw">public</span>:
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; CNeuronDOTOCL(<span class="keyword">class="type">void</span>) {};
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;~CNeuronDOTOCL(<span class="keyword">class="type">void</span>) {};
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Init(<span class="keyword">class="type">uint</span> numOutputs, <span class="keyword">class="type">uint</span> myIndex, COpenCLMy *open_cl,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span> window, <span class="keyword">class="type">uint</span> dimension, <span class="keyword">class="type">uint</span> heads,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span> units_count, <span class="keyword">class="type">uint</span> prev_window,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ENUM_OPTIMIZATION optimization_type,
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span> batch);
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;calcInputGradients(CNeuronBaseOCL *prevLayer);
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//---</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Type(<span class="keyword">class="type">void</span>)&nbsp;&nbsp; <span class="keyword">const</span>&nbsp;&nbsp; {&nbsp;&nbsp;<span class="keyword">class="kw">return</span> defNeuronDOTOCL;&nbsp;&nbsp; }
&nbsp;&nbsp; <span class="comment">class=class="str">"cmt">//--- methods for working with files</span>
&nbsp;&nbsp; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Save(<span class="keyword">class="type">int</span> <span class="keyword">const</span> file_handle);

「多头注意力层的 OpenCL 初始化链路」

CNeuronDOTOCL 把 Transformer 块搬进 GPU,Init 里先调基类 CNeuronBaseOCL::Init,窗口尺寸按 window*units_count 展开,任一子模块初始化失败就直接 return false,整层作废。 当 prev_window 与当前 window 不一致时,才额外建 cProjInput 做维度投影,否则跳过以省显存。其余组件无论何时都强制初始化:cQKV 用 dimension*heads 作宽度,iScoreBuffer 按 float 大小申请 iUnits*iHeads*3 个元素的读写缓冲。 前馈部分 cFF1 隐藏层固定为 4*window,cFF2 再压缩回 window;最后 cCAtten 用 MathMax(window/2,3) 作卷积核、头数写死为 8。Output 与 Gradient 指针在赋值前先判空 delete,避免旧缓冲泄漏。 在 MT5 里把 iHeads 从 4 调到 8,iScoreBuffer 显存占用会翻倍,老显卡可能触发 CL_MEM 分配失败返回负数,值得你实机验证一下上限。

MQL5 / C++
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 *source, class="type">float tau);
  class="kw">virtual class="type">void        SetOpenCL(COpenCLMy *obj);
  };
class="type">bool CNeuronDOTOCL::Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl, class="type">uint window,
                         class="type">uint dimension, class="type">uint heads, class="type">uint units_count, class="type">uint prev_window,
                         ENUM_OPTIMIZATION optimization_type, class="type">uint batch)
  {
class=class="str">"cmt">//---
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, window * units_count,
                            optimization_type, batch))
      class="kw">return false;
   if(prev_window != window)
     {
       if(!cProjInput.Init(class="num">0, class="num">0, OpenCL, prev_window, prev_window, window, units_count,
                           optimization_type, batch))
         class="kw">return false;
     }
   iWindowSize = window;
   iPrevWindowSize = prev_window;
   iDimension = dimension;
   iHeads = heads;
   iUnits = units_count;
   if(!cQKV.Init(class="num">0, class="num">1, OpenCL, window, window, dimension * heads, units_count,
                 optimization_type, batch))
      class="kw">return false;
class=class="str">"cmt">//---
   iScoreBuffer = OpenCL.AddBuffer(class="kw">sizeof(class="type">float) * iUnits * iHeads * class="num">3, CL_MEM_READ_WRITE);
   if(iScoreBuffer < class="num">0)
      class="kw">return false;
   if(!cRelativePositionsBias.Init(class="num">1, class="num">2, OpenCL, iUnits * iHeads * class="num">3, optimization_type, batch))
      class="kw">return false;
   if(!MHAttentionOut.Init(class="num">0, class="num">3, OpenCL, iUnits * iHeads * iDimension, optimization_type, batch))
      class="kw">return false;
   if(!cProj.Init(class="num">0, class="num">4, OpenCL, iHeads * iDimension, iHeads * iDimension, window, iUnits,
                  optimization_type, batch))
      class="kw">return false;
   if(!AttentionOut.Init(class="num">0, class="num">5, OpenCL, iUnits * window, optimization_type, batch))
      class="kw">return false;
   if(!cFF1.Init(class="num">0, class="num">6, OpenCL, window, window, class="num">4 * window, units_count, optimization_type,batch))
      class="kw">return false;
   if(!cFF2.Init(class="num">0, class="num">7, OpenCL, window * class="num">4, window * class="num">4, window, units_count, optimization_type,
                 batch))
      class="kw">return false;
   if(!SAttenOut.Init(class="num">0, class="num">8, OpenCL, iUnits * window, optimization_type, batch))
      class="kw">return false;
   if(!cCAtten.Init(class="num">0, class="num">9, OpenCL, window, MathMax(window / class="num">2, class="num">3), class="num">8, iUnits, class="num">1,
                    optimization_type, batch))
      class="kw">return false;
   if(!!Output)
      class="kw">delete Output;
   Output = cCAtten.getOutput();
   if(!!Gradient)
      class="kw">delete Gradient;
   Gradient = cCAtten.getGradient();
   SAttenOut.SetGradientIndex(cFF2.getGradientIndex());
class=class="str">"cmt">//---

常见问题

可以。DFFT 这类无解码检测器用变换器直接预测边界,绕开了解码环节,推理更轻,适合实时场景。
先试 32~64 根bar的滑动窗口,太长会拖慢自注意力;用历史堆栈做输入时按品种波动调窗口。
可以。小布能加载你的检测逻辑做回测诊断,标出误报集中的时段和品种,省去你手动跑验证。
先确认上下文创建、内核编译、权重缓冲绑定三步;任一为空指针都会静默失败,逐层打日志。
偏正常。自注意力是平方复杂度,可换局部窗口注意力或减头数,掉帧多因序列过长而非模型错。