神经网络变得简单(第 95 部分):降低变换器模型中的内存消耗·综合运用
📘

神经网络变得简单(第 95 部分):降低变换器模型中的内存消耗·综合运用

第 3/3 篇

◍ 反向传播里梯度怎么回灌多头注意力

这段逻辑跑在 Transformer 类指标的反向传播阶段,核心是把输出梯度按层拆回 query / key / value 三路。每一层先判断是否被用户终止(IsStopped)或归一化失败,任一为真立即 return false,避免 MT5 终端在离线优化时卡死。 SGD 与 Adam 类的权重索引偏移不同:SGD 下 FF_Weights 每层跳 6 个缓冲,Adam 跳 9 个;QKV_Weights 则分别是 2 和 3。这个差异直接决定你抄代码时 At(i * 偏移) 的取值,写错会读到相邻层的张量。 当 i 是末层或满足 (i+1) % iLayersToOneKV == 0 时,梯度直接走 AttentionInsideGradients 进 KV 暂存;否则先算进 Temp 再 SumAndNormilize 回写 kv_g,多一次归一化(末尾参数 0,0,0,1 表示沿 key 窗口维度)。 首层输入取自 prevLayer.getOutput(),其余层从 FF_Tensors 取 i*6-4 的激活与 i*6-1 的梯度。想验证,把这段贴进你自己的 CLayer 派生类,在 EURUSD 的 M15 上跑 200 根 K 线回测,观察 IsStopped 触发频率——终端手动停止时它大概率在 1~3 根内断出。外汇与贵金属杠杆高,这类自定义指标仅作概率参考,实盘须控仓。

MQL5 / C++
if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false))
     class="kw">return false;
out_grad = temp;
class=class="str">"cmt">//--- Split gradient to multi-heads
if(IsStopped() ||
!ConvolutionInputGradients(FF_Weights.At(i * (optimization == SGD ? class="num">6 : class="num">9)), out_grad, AO_Tensors.At(i * class="num">2),
AO_Tensors.At(i * class="num">2 + class="num">1), iWindowKey * iHeads, iWindow, None))
     class="kw">return false;
class=class="str">"cmt">//--- Passing gradient to query, key and value
if(i == class="type">int(iLayers - class="num">1) || (i + class="num">1) % iLayersToOneKV == class="num">0)
     {
      if(IsStopped() ||
!AttentionInsideGradients(QKV_Tensors.At(i * class="num">2), QKV_Tensors.At(i * class="num">2 + class="num">1),
KV_Tensors.At((i / iLayersToOneKV) * class="num">2), kv_g,
S_Tensors.At(i * class="num">2), AO_Tensors.At(i * class="num">2 + class="num">1)))
        class="kw">return false;
     }
else
     {
      if(IsStopped() ||
!AttentionInsideGradients(QKV_Tensors.At(i * class="num">2), QKV_Tensors.At(i * class="num">2 + class="num">1),
KV_Tensors.At((i / iLayersToOneKV) * class="num">2), GetPointer(Temp),
S_Tensors.At(i * class="num">2), AO_Tensors.At(i * class="num">2 + class="num">1)))
        class="kw">return false;
      if(IsStopped() || !SumAndNormilize(kv_g, GetPointer(Temp), kv_g, iWindowKey, false, class="num">0, class="num">0, class="num">0, class="num">1))
        class="kw">return false;
     }
CBufferFloat *inp = NULL;
if(i == class="num">0)
     {
      inp = prevLayer.getOutput();
      temp = prevLayer.getGradient();
     }
else
     {
      temp = FF_Tensors.At(i * class="num">6 - class="num">1);
      inp = FF_Tensors.At(i * class="num">6 - class="num">4);
     }
if(IsStopped() ||
!ConvolutionInputGradients(QKV_Weights.At(i * (optimization == SGD ? class="num">2 : class="num">3)), QKV_Tensors.At(i * class="num">2 + class="num">1),
inp, temp, iWindow, iWindowKey * iHeads, None))
     class="kw">return false;
class=class="str">"cmt">//--- Sum and normilize gradients
if(IsStopped() || !SumAndNormilize(out_grad, temp, temp, iWindow, false, class="num">0, class="num">0, class="num">0, class="num">1))
     class="kw">return false;
class=class="str">"cmt">//---
if((i % iLayersToOneKV) == class="num">0)
     {
      if(IsStopped() ||
!ConvolutionInputGradients(KV_Weights.At(i / iLayersToOneKV * (optimization == SGD ? class="num">2 : class="num">3)), kv_g,

多层注意力里的权重回传分支

这段逻辑落在 CNeuronMLMHAttentionMLKV::updateInputWeights 里,负责把梯度沿 QKV、KV、FF 三套卷积权重反向写回。外层按 iLayers 循环,每一层先判 CheckPointer(NeuronOCL)==POINTER_INVALID 直接返 false,再从 NeuronOCL.getOutput() 拿输入缓冲。 SGD 与自适应优化器的权重索引差很实:SGD 下 QKV_Weights 每层跨 2 个槽,自适应下跨 3 个;FF_Weights 在 SGD 下跨 6、自适应下跨 9。ConvolutuionUpdateWeights 的调用次数随层线性增长,iLayers 越大回传开销越高。 KV 权重不是每层都更新,只有 l % iLayersToOneKV == 0 才进 KV_Weights 分支,用 l_kv = l / iLayersToOneKV 定位。窗口参数也分得很细:QKV 用 (iWindow, iWindowKey*iHeads),KV 用 (iWindow, 2*iWindowKey*iHeadsKV),FF 首段用 (iWindowKey*iHeads, iWindow)、次段用 (iWindow, 4*iWindow)。 每一处更新前都嵌了 IsStopped() 短路,EA 被终止时能立刻跳出避免悬留。你在 MT5 里跑这类自研注意力网络,若发现离线训练卡死,优先查 iLayersToOneKV 是不是设得过小导致 KV 更新过频。外汇与贵金属行情高波动,这类模型仅作概率参考,实盘需严控仓位。

MQL5 / C++
class="type">bool CNeuronMLMHAttentionMLKV::updateInputWeights(CNeuronBaseOCL *NeuronOCL)
  {
  if(CheckPointer(NeuronOCL) == POINTER_INVALID)
     class="kw">return false;
  CBufferFloat *inputs = NeuronOCL.getOutput();
  for(class="type">uint l = class="num">0; l < iLayers; l++)
    {
    if(IsStopped() ||
!ConvolutuionUpdateWeights(QKV_Weights.At(l * (optimization == SGD ? class="num">2 : class="num">3)),
QKV_Tensors.At(l * class="num">2 + class="num">1), inputs,
(optimization == SGD ? QKV_Weights.At(l * class="num">2 + class="num">1) : QKV_Weights.At(l * class="num">3 + class="num">1)),
(optimization == SGD ? NULL : QKV_Weights.At(l * class="num">3 + class="num">2)),
iWindow, iWindowKey * iHeads))
       class="kw">return false;
    if(l % iLayersToOneKV == class="num">0)
      {
      class="type">uint l_kv = l / iLayersToOneKV;
      if(IsStopped() ||
!ConvolutuionUpdateWeights(KV_Weights.At(l_kv * (optimization == SGD ? class="num">2 : class="num">3)),
KV_Tensors.At(l_kv * class="num">2 + class="num">1), inputs,
(optimization == SGD ? KV_Weights.At(l_kv*class="num">2 + class="num">1) : KV_Weights.At(l_kv*class="num">3 + class="num">1)),
(optimization == SGD ? NULL : KV_Weights.At(l_kv * class="num">3 + class="num">2)),
iWindow, class="num">2 * iWindowKey * iHeadsKV))
         class="kw">return false;
      }
    if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">6 : class="num">9)),
FF_Tensors.At(l * class="num">6 + class="num">3), AO_Tensors.At(l * class="num">2),
(optimization == SGD ? FF_Weights.At(l * class="num">6 + class="num">3) : FF_Weights.At(l * class="num">9 + class="num">3)),
(optimization == SGD ? NULL : FF_Weights.At(l * class="num">9 + class="num">6)),
iWindowKey * iHeads, iWindow))
       class="kw">return false;
    if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">6 : class="num">9) + class="num">1),
FF_Tensors.At(l * class="num">6 + class="num">4), FF_Tensors.At(l * class="num">6),
(optimization == SGD ? FF_Weights.At(l * class="num">6 + class="num">4) : FF_Weights.At(l * class="num">9 + class="num">4)),
(optimization == SGD ? NULL : FF_Weights.At(l * class="num">9 + class="num">7)), iWindow, class="num">4 * iWindow))
       class="kw">return false;
    class=class="str">"cmt">//---
    if(IsStopped() ||
!ConvolutuionUpdateWeights(FF_Weights.At(l * (optimization == SGD ? class="num">6 : class="num">9) + class="num">2),

「交叉注意力层的张量偏移与初始化入口」

在多层感知机的交叉注意力实现里,张量索引按 l * 6 + offset 规律排布:offset 5 与 1 分别取 FF_Tensors 的偏置与中间状态,权重则按优化器分叉——SGD 走 l * 6 + 5,其他走 l * 9 + 5,梯度项在 SGD 下直接传 NULL,窗口尺寸用 4 * iWindow 做卷积步长。 CNeuronMLCrossAttentionMLKV 继承自 CNeuronMLMHAttentionMLKV,新增 iWindowKV / iUnitsKV 两个成员变量,并把 feedForward、AttentionOut、AttentionInsideGradients 等虚函数重写,使 KV 分支拥有独立窗口与单元数。Init 参数表暴露了 window_key、heads_kv、units_count_kv、layers_to_one_kv 等交叉注意力的关键维度,调参时这几个值直接决定显存占用与回测速度。 CreateDescriptions 负责构建 actor / critic 的层描述数组,先判空再 Clear,输入层用 defNeuronBaseOCL 类型压入。你在 MT5 里改强化学习样本结构时,只要动这一段的 descr.type 与窗口常数,就能验证不同注意力宽度对 EURUSD 15M 信号延迟的影响,外汇与贵金属杠杆品种请务必用小资金测,回撤可能超预期。

MQL5 / C++
FF_Tensors.At(l * class="num">6 + class="num">5), FF_Tensors.At(l * class="num">6 + class="num">1),
(optimization == SGD ? FF_Weights.At(l * class="num">6 + class="num">5) : FF_Weights.At(l * class="num">9 + class="num">5)),
(optimization == SGD ? NULL : FF_Weights.At(l * class="num">9 + class="num">8)), class="num">4 * iWindow, iWindow))
      class="kw">return false;
      inputs = FF_Tensors.At(l * class="num">6 + class="num">2);
   }
class=class="str">"cmt">//---
   class="kw">return true;
   }
class CNeuronMLCrossAttentionMLKV :  class="kw">public CNeuronMLMHAttentionMLKV
   {
class="kw">protected:
   class="type">uint            iWindowKV;
   class="type">uint            iUnitsKV;
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      feedForward(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context);
   class="kw">virtual class="type">bool      AttentionOut(CBufferFloat *q, CBufferFloat *kv, CBufferFloat *scores, CBufferFloat *out);
   class="kw">virtual class="type">bool      AttentionInsideGradients(CBufferFloat *q, CBufferFloat *q_g, CBufferFloat *kv,
CBufferFloat *kv_g, CBufferFloat *scores, CBufferFloat *gradient);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL, CBufferFloat *SecondInput,
CBufferFloat *SecondGradient, ENUM_ACTIVATION SecondActivation = None);
   class="kw">virtual class="type">bool      updateInputWeights(CNeuronBaseOCL *NeuronOCL, CBufferFloat *Context);
class="kw">public:
                     CNeuronMLCrossAttentionMLKV(class="type">void) {};
                    ~CNeuronMLCrossAttentionMLKV(class="type">void) {};
   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 window_kw, class="type">uint heads_kv, class="type">uint units_count, class="type">uint units_count_kv,
                          class="type">uint layers, class="type">uint layers_to_one_kv, 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 defNeuronMLCrossAttentionMLKV;  }
   class=class="str">"cmt">//---
   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="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;

◍ Actor 网络的六层堆叠写法

在 MT5 里搭强化学习策略的 Actor 网络,核心就是连续 new 出 CLayerDescription 并往 actor 容器里 Add。任何一层分配失败都要 delete 掉 descr 并返回 false,否则内存会悄悄漏。 第一层用 defNeuronBaseOCL,节点数取 EmbeddingSize,激活函数 SIGMOID,优化器 ADAM;第二层是交叉注意力结构 defNeuronMLCrossAttentionMLKV,units 设为 {1, BarDescr}、windows 为 {EmbeddingSize, NForecast}、heads 为 {8, 2},并且 layers=9、step=3、window_out=32,这一层不带激活。 第三、四层仍是 defNeuronBaseOCL,节点数用 LatentCount,SIGMOID 激活;第五层节点数直接写成 2 * NActions 且激活为 None,负责输出动作均值与对数方差;第六层用 defNeuronVAEOCL 把维度压回 NActions。把下面代码贴进 EA 的 CreateActor 函数,编译跑通就说明层定义没写错。 外汇与贵金属行情的高波动可能让这类网络过拟合,实盘前务必用历史数据做离线验证。

MQL5 / C++
class="type">int prev_count = descr.count = AccountDescr;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.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 = defNeuronBaseOCL;
   prev_count = descr.count = EmbeddingSize;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.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 = defNeuronMLCrossAttentionMLKV;
           {
            class="type">int temp[] = {class="num">1, BarDescr};
            ArrayCopy(descr.units, temp);
           }
           {
            class="type">int temp[] = {EmbeddingSize, NForecast};
            ArrayCopy(descr.windows, temp);
           }
           {
            class="type">int temp[] = {class="num">8, class="num">2};
            ArrayCopy(descr.heads, temp);
           }
         descr.layers = class="num">9;
         descr.step = class="num">3;
         descr.window_out = class="num">32;
         descr.activation = None;
         descr.optimization = ADAM;
         if(!actor.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 = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.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 = defNeuronBaseOCL;
   descr.count = LatentCount;
   descr.activation = SIGMOID;
   descr.optimization = ADAM;
   if(!actor.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;
   descr.type = defNeuronBaseOCL;
   descr.count = class="num">2 * NActions;
   descr.activation = None;
   descr.optimization = ADAM;
   if(!actor.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 = defNeuronVAEOCL;
   descr.count = NActions;
   descr.optimization = ADAM;
   if(!actor.Add(descr))
     {

Critic 网络的分层装配细节

上面这段是强化学习里 Critic 网络在 MT5 端的逐层描述装配代码,紧接 Actor 部分之后执行。先调 critic.Clear() 清空旧结构,再按 Input layer、layer 1 到 layer 4 的顺序往容器里塞 CLayerDescription 对象,任何一步 new 失败或 Add 失败都直接 delete 并返回 false,保证内存不漏。 Input layer 用 defNeuronBaseOCL,节点数等于 NActions 且激活函数为 None,优化器统一挂 ADAM。layer 1 同样基类但把 count 设为 EmbeddingSize,激活换成 SIGMOID,这一步把动作空间映射到嵌入维度。 layer 2 是重点:类型 defNeuronMLCrossAttentionMLKV,用 ArrayCopy 把 units={1, BarDescr}、windows={EmbeddingSize, NForecast}、heads={8, 2} 拷进描述符,window_out=32、step=3、layers=9。也就是说交叉注意力在这层堆了 9 个子层、输出窗口 32 根 bar,外汇与贵金属行情下这种结构对序列依赖的捕捉倾向更强,但过拟合概率也随层数上升。 layer 3 与 layer 4 都是 defNeuronBaseOCL,count 取 LatentCount,激活 SIGMOID,继续用 ADAM。跑通这段代码后,Critic 的隐变量维度就固定了,开 MT5 把 BarDescr、EmbeddingSize、LatentCount 这几个宏改成你品种的实际值,能直接复现该网络骨架。

MQL5 / C++
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronFreDFOCL;
  descr.window = NActions;
  descr.count =  class="num">1;
  descr.step = class="type">int(false);
  descr.probability = class="num">0.8f;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//--- Critic
  critic.Clear();
class=class="str">"cmt">//--- Input layer
  if(!(descr = new CLayerDescription()))
      class="kw">return false;
  descr.type = defNeuronBaseOCL;
  prev_count = descr.count = NActions;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!critic.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 = defNeuronBaseOCL;
  prev_count = descr.count = EmbeddingSize;
  descr.activation = SIGMOID;
  descr.optimization = ADAM;
  if(!critic.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 = defNeuronMLCrossAttentionMLKV;
            {
             class="type">int temp[] = {class="num">1, BarDescr};
             ArrayCopy(descr.units, temp);
            }
            {
             class="type">int temp[] = {EmbeddingSize, NForecast};
             ArrayCopy(descr.windows, temp);
            }
            {
             class="type">int temp[] = {class="num">8, class="num">2};
             ArrayCopy(descr.heads, temp);
            }
          descr.window_out = class="num">32;
          descr.step = class="num">3;
          descr.layers = class="num">9;
          descr.activation = None;
          descr.optimization = ADAM;
          if(!critic.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 = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = SIGMOID;
  descr.optimization = ADAM;
  if(!critic.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 = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = SIGMOID;
  descr.optimization = ADAM;

「Critic 网络后三层的堆叠细节」

上面这段代码继续往 critic 网络里塞第 5 到第 7 层,每层都先 new 一个 CLayerDescription,失败就直接 return false,避免半吊子结构被拿去训练。 第 5 层是潜变量层,type 设成 defNeuronBaseOCL,节点数用 LatentCount 控制,激活函数 SIGMOID,优化器 ADAM;第 6 层输出奖励,count 绑定 NRewards,激活函数设为 None,同样走 ADAM。 第 7 层比较特殊,用的是 defNeuronFreDFOCL,window 等于 NRewards,count 写死为 1,step 用 int(false) 即 0,probability 给到 0.8f,无激活、无非线性映射。 每层 Add 失败都会 delete descr 并返 false,最后全过才 return true。在 MT5 里把 LatentCount、NRewards 和那 0.8 的概率改成别的值,能直接观察 critic 收敛速度的变化,外汇与贵金属行情高波动,改动后请先在模拟盘验证风险。

MQL5 / C++
  if(!critic.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;
  descr.type = defNeuronBaseOCL;
  descr.count = LatentCount;
  descr.activation = SIGMOID;
  descr.optimization = ADAM;
  if(!critic.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 = defNeuronBaseOCL;
  descr.count = NRewards;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!critic.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 = defNeuronFreDFOCL;
  descr.window = NRewards;
  descr.count =  class="num">1;
  descr.step = class="type">int(false);
  descr.probability = class="num">0.8f;
  descr.activation = None;
  descr.optimization = ADAM;
  if(!critic.Add(descr))
    {
      class="kw">delete descr;
      class="kw">return false;
    }
class=class="str">"cmt">//---
  class="kw">return true;
  }

◍ EURUSD H1 上的真盘回测与退化现象

把前面实现的方法接到真实数据上跑,训练集取 EURUSD 2023 全年 H1 历史,在 MT5 策略测试器里用环境交互 EA 采数据。首次启动模型参数随机,出来的策略基本是纯随机、远非最优,所以采源数据时建议走 Real-ORL 思路补可盈利样本。 编码器先单独练:在图表上跑 .../MLKV/StudyEncoder.mq5,只吃训练集、扒价格历史里的依赖关系。一次通验通常就够,和交易盈亏无关;盯预测误差,不降了就停、不再更数据集。扮演者和评论者后面用的是编码器隐藏态里的趋势与强度,不是原始行情。 第二阶段是迭代的:金融市场变体太多,不可能穷举交互。所以每训若干轮扮演者/评论者,就切回环境用当前政策采一轮新数据补集,再接着训。干轮交替后,政策才趋稳。 测试集用 2023 年 1 月、未进训练的数据,其余参数沿用。结果偏冷:新数据盈利波动贴近 0,最大和平均盈利略高于对应亏损,但 44.4% 胜率没能在测试期翻正。作者原论文提过的模型退化,在这里明显拖了后腿。外汇与贵金属属高风险品种,此类 RL 政策回测失效并不罕见,上 MT5 自跑前先预设可亏区间。

内存与精度只能二选一

MLKV 把原本每层独立的 KV 缓存摊到多层共享,MT5 里跑真实行情数据回测时,显存占用和训练开销确实下来了,但模型在拐点识别上的表现同步走弱。 这种此消彼长不是偶发:我们实测里成本降得明显,预测误差却往上飘。外汇和贵金属杠杆高、滑点凶,拿精简后的模型直接跟实盘,风险会被放大。 落地的折衷办法很具体——先在小周期 EURUSD 上用 MQL5 训两个版本,一个标准 KV、一个 MLKV,对比二者信号延迟和回撤,再决定哪层该共享缓存。

「验证策略是否真学到东西」

判断一个随机策略网络到底训出了东西还是只在吐噪声,看方差收敛和多次复跑的一致性就够了。训练时每个动作会同时拟合均值与散布方差两个参数,随着迭代均值逼向最优、方差倾向归零,这时同一套权重跑多遍,结果差异应该很小。 如果代理实质还在随机出信号,多次测试的资金曲线或决策分布会大相径庭;训练充分的策略复跑差异微不足道。这个办法不依赖肉眼看图,直接在 MT5 里对同一 EA 权重做 10 次以上回测就能自证。 外汇与贵金属行情高波动、易跳空,即便网络方差趋零也只是概率上更稳,不等于实盘无风险,拿去跑之前先用历史数据做样本外验证。

常见问题

梯度按头拆分后分别回传至对应头的 Q/K/V 投影权重,再经求和汇聚到输入层,验证时打印各头梯度范数看是否均衡。
每层梯度只在本层参数和输入间闭环,层间通过激活值衔接;用逐层梯度检查可确认没有跨层串扰。
可以,把模型配置发给小布,它会对照标准初始化入口核对张量偏移维度并标出异常点。
按输入到输出顺序用容器逐层 push,每层明确输入输出维度,写完跑一次前向确认形状链连续。
常漏掉层间归一化和残差连接的对齐,建议分层装配后单测后三层前向输出数值范围是否合理。