神经网络变得简单(第 84 部分):可逆归一化(RevIN)·进阶篇
📘

神经网络变得简单(第 84 部分):可逆归一化(RevIN)·进阶篇

第 2/3 篇

◍ 反向归一化核里的梯度回传细节

在 MT5 的 OpenCL 神经网络推理中,RevInHiddenGraddient 这个核负责把输出梯度反算回输入梯度,相当于训练时的反向归一化步骤。 代码里先通过 get_global_id(0) 拿到当前神经元索引 n,再用 (n * optimization == 0 ? 7 : 9) % options_size 算出偏移 shift——这个三元表达式说明优化开关会让参数读取位置在 7 和 9 之间切换,实际部署时若 optimization 参数配错,梯度可能读到错误的方差项。 variance 取自 options[shift+1],k 取自 options[shift+3],当 k 不为零时才做 sqrt(variance) * output_gr[n] / k 的缩放;若结果为 NaN 则强制归零,避免 GPU 上浮点异常扩散到整层。 激活函数反向分三种:case 0 是 Tanh 类(钳制到 ±1 后乘 1−x²),case 1 是 Sigmoid 类(钳制到 0~1 后乘 x(1−x)),case 2 是带 0.01 负半轴的 Leaky 类。改激活类型时,直接调 activation 入参即可,不用动主体逻辑。 外层调度里 defNeuronRevInDenormOCL 会检查 layers 总数和图层类型,若目标层不是 defNeuronBatchNormOCL 就 delete temp 并返 false——这意味着归一化层与反归一化层必须配对,否则网络构建直接失败。外汇与贵金属模型用这套逻辑时,注意 GPU 驱动版本差异可能导致 CLayer 类型校验行为偏移,属高风险调试项。

MQL5 / C++
printf("Error of execution kernel %s: %d", __FUNCTION__, GetLastError());
class="kw">return false;
}
class=class="str">"cmt">//---
  class="kw">return true;
}
__kernel class="type">void RevInHiddenGraddient(__global class="type">float *inputs, __global class="type">float *inputs_gr,
                                  __global class="type">float *options, __global class="type">float *output_gr,
                                  class="type">int options_size,
                                  class="type">int optimization,
                                  class="type">int activation)
  {
  class="type">int n = get_global_id(class="num">0);
  class="type">int shift = (n * optimization == class="num">0 ? class="num">7 : class="num">9) % options_size;
  class="type">float variance = options[shift + class="num">1];
  class="type">float inp = inputs[n];
  class="type">float k = options[shift + class="num">3];
  class="type">float res = class="num">0;
  if(k != class="num">0)
    res = sqrt(variance) * output_gr[n] / k;
  if(isnan(res))
    res = class="num">0;
  class="kw">switch(activation)
    {
    case class="num">0:
      res = clamp(res + inp, -class="num">1.0f, class="num">1.0f) - inp;
      res = res * (class="num">1 - pow(inp == class="num">1 || inp == -class="num">1 ? class="num">0.99999999f : inp, class="num">2));
      break;
    case class="num">1:
      res= clamp(res + inp, class="num">0.0f, class="num">1.0f) - inp;
      res = res * (inp == class="num">0 || inp == class="num">1 ? class="num">0.00000001f : (inp * (class="num">1 - inp)));
      break;
    case class="num">2:
      if(inp < class="num">0)
        res *= class="num">0.01f;
      break;
    class="kw">default:
      break;
    }
class=class="str">"cmt">//---
  inputs_gr[n] = res;
  }
                      case defNeuronRevInDenormOCL:
                        if(desc.layers>=layers.Total())
                          {
                          class="kw">delete temp;
                          class="kw">return false;
                          }
                        if(((CLayer *)layers.At(desc.layers)).At(class="num">0).Type()!=defNeuronBatchNormOCL)
                          {
                          class="kw">delete temp;

反向归一化层的实例化与网络读取

在神经网络描述符的 switch 分支里,遇到 defNeuronRevInDenormOCL 类型时会先校验 OpenCL 指针有效性,无效则直接 return false。随后用 new 创建 CNeuronRevINDenormOCL 对象,若指针无效将 result 置 false;Init 传入 iOutputs、index、OpenCL、层数 1、索引 -1 与 NULL 父层,成功就把对象挂到 m_data[index] 并返回 true,失败则 delete 回收。 前一个分支对 revin 的初始化更重:需传入 outputs、0、opencl、desc.count、desc.layers 以及取到的具体层指针,任何一步 Add 或 Init 失败都会顺序 delete temp 与 revin 并回 false,避免半初始化对象泄漏。 CNet::Load 从文件句柄读结构时,先用 FileReadInteger(file_handle, INT_VALUE) 取数组长度 num,非 0 才循环 new CLayer(0, file_handle, opencl) 并调 Layer.Load;任一层 Load 失败就 break,意味着权重文件损坏时网络只加载到断点层。 开 MT5 把这段贴进 EA 的神经网络模块,故意传一个 POINTER_INVALID 的 OpenCL 上下文,能复现第二分支直接返回 false 且不产生 revin 对象的行为。

MQL5 / C++
         class="kw">return false;
            }
            revin = new CNeuronRevINDenormOCL();
            if(!revin)
            {
               class="kw">delete temp;
               class="kw">return false;
            }
            if(!revin.Init(outputs, class="num">0, opencl, desc.count, desc.layers,
((CLayer *)layers.At(desc.layers)).At(class="num">0)))
            {
               class="kw">delete temp;
               class="kw">delete revin;
               class="kw">return false;
            }
            if(!temp.Add(revin))
            {
               class="kw">delete temp;
               class="kw">delete revin;
               class="kw">return false;
            }
            break;
         case   defNeuronRevInDenormOCL:
            if(CheckPointer(OpenCL) == POINTER_INVALID)
               class="kw">return false;
            revin = new CNeuronRevINDenormOCL();
            if(CheckPointer(revin) == POINTER_INVALID)
               result = false;
            if(revin.Init(iOutputs, index, OpenCL, class="num">1, -class="num">1, NULL))
            {
             m_data[index] = revin;
             class="kw">return true;
            }
            class="kw">delete revin;
            break;
class="type">bool CNet::Load(const class="type">int file_handle)
  {
........
........
class=class="str">"cmt">//--- read array length
   num = FileReadInteger(file_handle, INT_VALUE);
class=class="str">"cmt">//--- read array
   if(num != class="num">0)
    {
       for(i = class="num">0; i < num; i++)
        {
         class=class="str">"cmt">//--- create new element
         CLayer *Layer = new CLayer(class="num">0, file_handle, opencl);
         if(!Layer.Load(file_handle))
            break;

「反归一化层与编码器描述体的衔接细节」

在加载网络结构时,若第 0 层被识别为反归一化类型(defNeuronRevInDenormOCL),需要回链到指定的归一化层做参数初始化。代码先取该层索引 l = revin.GetNormLayer(),一旦 layers.At(l) 为空或对应神经元不是 defNeuronBatchNormOCL,就直接 delete Layer 并 break,避免挂上不兼容的结构。 初始化调用 revin.Init(...) 的最后一个参数是归一化层神经元指针 neuron,若返回失败同样释放 Layer 退出。这意味着反向结构强依赖前向 BatchNorm 层存在,否则整个网络描述作废,返回 layers.Total() == num 判定成败。 编码器构建从清空数组开始,输入层用 defNeuronBaseOCL,节点数 = HistoryBars * BarDescr,优化器选 ADAM 且无激活。第一层紧跟 BatchNorm,batch 尺寸取 MathMax(1000, GPTBars),即至少 1000 条;第二层起用 Embedding 类型承接。 实盘里调 GPTBars 会直接改 BatchNorm 的批大小,过小可能让归一化统计抖动,外汇与贵金属波动下高风险,建议开 MT5 把 GPTBars 从默认改到 2000 以上观察层输出方差变化。

MQL5 / C++
if(Layer.At(class="num">0).Type() == defNeuronRevInDenormOCL)
  {
   CNeuronRevINDenormOCL *revin = Layer.At(class="num">0);
   class="type">int l = revin.GetNormLayer();
   if(!layers.At(l))
     {
      class="kw">delete Layer;
      break;
     }
   CNeuronBaseOCL *neuron = ((CLayer *)layers.At(l)).At(class="num">0);
   if(neuron.Type() != defNeuronBatchNormOCL)
     {
      class="kw">delete Layer;
      break;
     }
   if(!revin.Init(revin.getConnections(), class="num">0, opencl, revin.Neurons(), l, neuron))
     {
      class="kw">delete Layer;
      break;
     }
  }
 if(!layers.Add(Layer))
   break;
  }
  }
 FileClose(file_handle);
class=class="str">"cmt">//--- result
  class="kw">return (layers.Total() == num);
  }
class="macro">#define       NForecast               class="num">6        class=class="str">"cmt">//Number of forecast
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 = MathMax(class="num">1000, GPTBars);
  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 = defNeuronEmbeddingOCL;
    {

◍ 编码器堆叠里的层定义套路

这段贴的是行情特征编码器后半段的层描述装配逻辑,从第三层一路叠到第十二层,每层都先 new 一个 CLayerDescription,填完参数再丢进 encoder.Add;任意一步失败就 delete 并 return false,保证对象不泄漏。 第三层用 defNeuronConvOCL,节点数直接拉到 prev_count*5,滑动步长和窗口都设为 prev_wout,输出窗口重写回 EmbeddingSize;第四层转 defNeuronPEOCL,窗口扩成 prev_wout*5 但不再改输出宽。 第五到第十层是 5 次循环,统一挂 defNeuronConformerOCL,step 固定为 4、内部 layers=5、window_out=EmbeddingSize,相当于叠了 5 个轻量 transformer 块做时序聚合。 第十一层收口到 defNeuronBaseOCL,count 变成 NForecast*BarDescr,激活用 TANH、优化用 ADAM;第十二层接 defNeuronRevInDenormOCL 做反归一化,activation 设 None、layers=1。外汇与贵金属市场杠杆高、滑点跳空频繁,这类结构只是特征抽取骨架,实盘信号质量仍要在 MT5 用历史Tick重跑验证。 CreateDescriptions 函数开头对 actor / critic 做了空指针兜底,为空就 new 一个 CArrayObj 再继续,避免上层调用方漏传容器导致后续 Add 崩掉。

MQL5 / C++
class="type">int temp[] = {class="num">4, class="num">1, class="num">1, class="num">1, class="num">2};
   ArrayCopy(descr.windows, temp);
   }
   prev_count = descr.count = GPTBars;
   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">3
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronConvOCL;
   descr.count = prev_count * class="num">5;
   descr.step = descr.window = 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">4
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronPEOCL;
   descr.count = prev_count;
   descr.window = prev_wout * class="num">5;
   if(!encoder.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">5-class="num">10
   for(class="type">int i = class="num">0; i < class="num">5; i++)
     {
      if(!(descr = new CLayerDescription()))
         class="kw">return false;
      descr.type = defNeuronConformerOCL;
      descr.count = prev_count;
      descr.window = prev_wout;
      descr.step = class="num">4;
      descr.window_out = EmbeddingSize;
      descr.layers = class="num">5;
      if(!encoder.Add(descr))
        {
         class="kw">delete descr;
         class="kw">return false;
        }
     }
class=class="str">"cmt">//--- layer class="num">11
   if(!(descr = new CLayerDescription()))
      class="kw">return false;
   descr.type = defNeuronBaseOCL;
   prev_count = descr.count = NForecast*BarDescr;
   descr.activation = TANH;
   descr.optimization = ADAM;
   if(!encoder.Add(descr))
     {
      class="kw">delete descr;
      class="kw">return false;
     }
class=class="str">"cmt">//--- layer class="num">12
   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="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)
     {

Actor 网络的层定义与张量维度

在 MT5 里搭强化学习策略网络时,Actor 的层级结构直接决定信号生成的复杂度。下面这段构建逻辑把输入层到输出层逐层铺开,每一层都用 CLayerDescription 描述类型、神经元数和激活函数。 输入层承接账户状态特征,节点数等于 AccountDescr,激活选 None,优化器统一用 ADAM;紧接着的嵌入层把维度压到 EmbeddingSize,改用 SIGMOID 做非线性映射。 中间第 2 到 4 层是交叉注意力结构,循环 3 次生成。每层 units 设为 {1, NForecast}、windows 设为 {EmbeddingSize, BarDescr},输出窗口 window_out=16、滑动步长 step=4,无激活函数。这类设置让网络在 16 根 K 线的局部视窗内做跨序列关注,对外汇 1H 级别噪声有一定过滤倾向。 后续第 5 层回到基础全连接,LatentCount 个 SIGMOID 节点;第 6 层输出 2*NActions 个无激活节点,供均值方差拆分;第 7 层用 VAE 类型收口到 NActions 维动作空间。外汇与贵金属杠杆高,这套结构过拟合概率不低,建议先在 EURUSD 上用小样本回测验证梯度是否发散。

MQL5 / C++
  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;
  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-class="num">4
  for(class="type">int i = class="num">0; i < class="num">3; i++)
   {
    if(!(descr = new CLayerDescription()))
      class="kw">return false;
    descr.type = defNeuronCrossAttenOCL;
      {
       class="type">int temp[] = {class="num">1, NForecast};
       ArrayCopy(descr.units, temp);
      }
      {
       class="type">int temp[] = {EmbeddingSize, BarDescr};
       ArrayCopy(descr.windows, temp);
      }
    descr.window_out = class="num">16;
    descr.step = class="num">4;
    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">5
  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">6
  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">7
  if(!(descr = new CLayerDescription()))
   class="kw">return false;
  descr.type = defNeuronVAEOCL;
  descr.count = NActions;
  descr.optimization = ADAM;
  if(!actor.Add(descr))
   {
    class="kw">delete descr;
    class="kw">return false;
   }
class=class="str">"cmt">//--- Critic
  critic.Clear();

「评论家网络的层堆叠与采样温度」

这段构建逻辑把 critic 网络从输入到输出逐层挂进容器,输入层直接吃 NActions 个动作维度,激活留 None、优化器统一走 ADAM,没有任何特征变换。第二层把维度压到 EmbeddingSize 并套 SIGMOID,算是给后续交叉注意力做嵌入准备。 中间 layer 2-4 是三段循环,类型换成 defNeuronCrossAttenOCL,units 设 {1, NForecast}、windows 设 {EmbeddingSize, BarDescr},window_out=16、step=4。这意味着每步滑 4 根 bar、吐 16 维输出,跨注意力在预测窗和 bar 描述上同时扫。 尾段 layer 5-6 是两个 LatentCount 维的 SIGMOID 隐层,layer 7 用 None 激活映射到 NRewards 个奖励维度,整网无池化、纯前馈加交叉注意力。 Train 里只露了一行:probability = GetProbTrajectories(Buffer, 0.9),0.9 是轨迹采样的温度系数。调低它会更贪心、调高则更偏探索,外汇与贵金属市场高波动下建议先拿 0.9 跑通再微调。

MQL5 / C++
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-class="num">4
  for(class="type">int i = class="num">0; i < class="num">3; i++)
    {
      if(!(descr = new CLayerDescription()))
        class="kw">return false;
      descr.type = defNeuronCrossAttenOCL;
        {
         class="type">int temp[] = {class="num">1, NForecast};
         ArrayCopy(descr.units, temp);
        }
        {
         class="type">int temp[] = {EmbeddingSize, BarDescr};
         ArrayCopy(descr.windows, temp);
        }
      descr.window_out = class="num">16;
      descr.step = class="num">4;
      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">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 = 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">7
  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">//---
  class="kw">return true;
  }
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
  vector<class="type">float> probability = GetProbTrajectories(Buffer, class="num">0.9);
class=class="str">"cmt">//---

常见问题

在反向归一化核里,均值和标准差的梯度要分别按样本维度累计,再乘原缩放系数回传;写代码时别漏掉 affine 参数的梯度分支,否则训练会发散。
需保证描述体输出的张量形状与反归一化层期望的 feature 轴一致,并在实例化时把训练/推理标志位透传下去,避免线上用训练统计量。
可以。把网络定义贴给小布,它能对照层衔接约束标出维度不匹配或标志位漏传的位置,省去手动翻代码。
多数卡在反归一化层输出到全连接头的 reshape 步;确认编码器堆叠末尾的 flatten 方式,再核对 Actor 头输入维数即可。
进阶实践里常从 0.8 起做网格搜,配合验证集回报方差调;温度只是探索缩放,不直接决定收益,外汇贵金属场景仍属高风险。