神经网络变得轻松(第二十一部分):变分自动编码器(VAE)·进阶篇
📘

神经网络变得轻松(第二十一部分):变分自动编码器(VAE)·进阶篇

第 2/3 篇

「变分自编码层的梯度回流实现」

在 MT5 的 OpenCL 神经层里,VAE 的隐藏梯度计算靠一个 __kernel 函数完成。它先用 get_global_id(0) 取当前线程索引 i,用 get_global_size(0) 拿到总规模 total,再按 KL 散度乘子 kld_mult 算出每项 kld,公式里 0.5f 权重和 exp / pow 调用直接决定均值与对数方差两路回传的尺度。 inp_grad[i] 只承接梯度叠加 kld*inputs[i],而 inp_grad[i+total] 那一路要乘 random[i]*exp(0.5f*inputs[i+total]),说明重参数化的随机噪声在反向时也要原样参与。若 kld_mult 设错,方差通道的梯度会整体偏移,训练可能过早塌缩。 CVAE 类把 m_fKLD_Mult 默认置为 0.01f,对外留了 SetKLDMult 接口。做贵金属或外汇序列建模时,这个 0.01 只是起步值,调大到 0.1 倾向让后验更靠近先验,但回测中曾出现收敛变慢的现象,需自己在 MT5 里跑几轮对比。 别把 0.01 当默认值就不动 KL 散度乘子直接压着隐变量正则强度。外汇小时线噪声大,乘子过小可能让编码器学会「忽略随机性」,在样本外给出偏乐观的重构误差。开 MT5 把 SetKLDMult 从 0.01 拉到 0.05 跑同一段历史,看验证集误差曲线是否更平。

MQL5 / C++
 outputs[i] = inputs[i] + exp(class="num">0.5f * inputs[i + total]) * random[i];
}
__kernel class="type">void VAE_CalcHiddenGradient(__global class="type">float* inputs,
                                      __global class="type">float* inp_grad,
                                      __global class="type">float* random,
                                      __global class="type">float* gradient,
                                      class="kw">const class="type">float kld_mult
                                      )
  {
   class="type">uint i = (class="type">uint)get_global_id(class="num">0);
   class="type">uint total = (class="type">uint)get_global_size(class="num">0);
   class="type">float kld = kld_mult * class="num">0.5f * (inputs[i + total] - exp(inputs[i + total]) - pow(inputs[i], class="num">2.0f) + class="num">1);
   inp_grad[i] = gradient[i] + kld * inputs[i];
   inp_grad[i + total] = class="num">0.5f * (gradient[i] * random[i] * exp(class="num">0.5f * inputs[i + total]) -
                                 kld * (class="num">1 - exp(inputs[i + total]))) ;
  }
class CVAE : class="kw">public CNeuronBaseOCL
  {
class="kw">protected:
   class="type">float               m_fKLD_Mult;
   CBufferDouble*      m_cRandom;
   class="kw">virtual class="type">bool        feedForward(CNeuronBaseOCL *NeuronOCL);
   class="kw">virtual class="type">bool        updateInputWeights(CNeuronBaseOCL *NeuronOCL) { class="kw">return true; }
class="kw">public:
                     CVAE();
                    ~CVAE();
   class="kw">virtual class="type">bool      Init(class="type">uint numOutputs, class="type">uint myIndex, COpenCLMy *open_cl,
                          class="type">uint numNeurons, ENUM_OPTIMIZATION optimization_type, class="type">uint batch);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">void      SetKLDMult(class="type">float value) { m_fKLD_Mult = value;}
   class="kw">virtual class="type">bool      calcInputGradients(CNeuronBaseOCL *NeuronOCL);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">bool      Save(class="type">int class="kw">const file_handle);
   class="kw">virtual class="type">bool      Load(class="type">int class="kw">const file_handle);
   class=class="str">"cmt">//---
   class="kw">virtual class="type">int       Type(class="type">void)            class="kw">const             {  class="kw">return defNeuronVAEOCL; }
   };
CVAE::CVAE()   : m_fKLD_Mult(class="num">0.01f)
  {
   m_cRandom = new CBufferDouble();
  }
CVAE::~CVAE()
  {
   if(!!m_cRandom)

VAE 层在 OpenCL 上的前向与前向梯度实现

变分自编码(VAE)层在 MT5 的 OpenCL 封装里,靠 CVAE 类把随机噪声和隐藏层输出耦合进前向传播。Init 方法先调基类 CNeuronBaseOCL::Init 拿到神经元结构,再为 m_cRandom 建一个长度等于 numNeurons、初值 0.0 的双精度缓冲,并推到显存——这一步若返回 false,整层直接不可用。 feedForward 里有个硬约束:上游 NeuronOCL 的神经元数必须正好是当前层的两倍(NeuronOCL.Neurons()/2 == Neurons()),否则直接退出。随后用 MathRandomNormal(0,1,...) 拉一批标准正态随机数填进 m_cRandom,经 BufferWrite 同步到设备,再绑定三个参数缓冲(输入、随机、输出)后启动 def_k_VAEFeedForward 内核,NDrange 维度就是 Neurons()。 反向侧的 calcInputGradients 把上游输出、上游梯度、本层权重和本层梯度全部绑到 def_k_VAECalcHiddenGradient 内核,并额外传一个标量 m_fKLD_Mult 作为 KL 散度项乘子。这个乘子直接调控正则强度:调大倾向压制隐变量分布偏移,调小则让重构误差主导。 在 MT5 里改 VAE 结构时,先确认上游维度是偶数且对半切,否则 feedForward 会静默返回 false 导致整个网络不更新。外汇与贵金属行情高频噪声大,用这类随机层做特征压缩须清楚:过拟合概率不低,属高风险实验。

MQL5 / C++
class="kw">delete m_cRandom;
}
class="type">bool CVAE::Init(class="type">uint numOutputs,
class="type">uint myIndex,
COpenCLMy *open_cl,
class="type">uint numNeurons,
ENUM_OPTIMIZATION optimization_type,
class="type">uint batch)
  {
   if(!CNeuronBaseOCL::Init(numOutputs, myIndex, open_cl, numNeurons, optimization_type, batch))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(!m_cRandom)
     {
       m_cRandom = new CBufferDouble();
       if(!m_cRandom)
          class="kw">return class="kw">false;
     }
   if(!m_cRandom.BufferInit(numNeurons, class="num">0.0))
      class="kw">return class="kw">false;
   if(!m_cRandom.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CVAE::feedForward(CNeuronBaseOCL *NeuronOCL)
  {
   if(!OpenCL || !NeuronOCL || !m_cRandom)
      class="kw">return class="kw">false;
   if(NeuronOCL.Neurons() % class="num">2 != class="num">0 ||
      NeuronOCL.Neurons() / class="num">2 != Neurons())
      class="kw">return class="kw">false;
   class="type">class="kw">double random[];
   if(!MathRandomNormal(class="num">0, class="num">1, m_cRandom.Total(), random))
      class="kw">return class="kw">false;
   if(!m_cRandom.AssignArray(random))
      class="kw">return class="kw">false;
   if(!m_cRandom.BufferWrite())
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_VAEFeedForward, def_k_vaeff_inputs, NeuronOCL.getOutput().GetIndex()))
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_VAEFeedForward, def_k_vaeff_random, m_cRandom.GetIndex()))
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_VAEFeedForward, def_k_vaeff_outputd, Output.GetIndex()))
      class="kw">return class="kw">false;
   class="type">uint off_set[] = {class="num">0};
   class="type">uint NDrange[] = {Neurons()};
   if(!OpenCL.Execute(def_k_VAEFeedForward, class="num">1, off_set, NDrange))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CVAE::calcInputGradients(CNeuronBaseOCL *NeuronOCL)
  {
   if(!OpenCL || !NeuronOCL)
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_input,
NeuronOCL.getOutput().GetIndex()))
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_inp_grad,
NeuronOCL.getGradient().GetIndex()))
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_random, Weights.GetIndex()))
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgumentBuffer(def_k_VAECalcHiddenGradient, def_k_vaehg_gradient, Gradient.GetIndex()))
      class="kw">return class="kw">false;
   if(!OpenCL.SetArgument(def_k_VAECalcHiddenGradient, def_k_vaehg_kld_mult, m_fKLD_Mult))
      class="kw">return class="kw">false;
   class="type">int off_set[] = {class="num">0};
   class="type">int NDrange[] = {Neurons()};

◍ VAE 模块的存盘与 OpenCL 重建路径

在 MT5 的神经网络封装里,CVAE 类把变分自编码器的参数落盘和加载拆成了两个明确分支。Save 方法先调用基类 CNeuronBaseOCL::Save 写底层权重,再用 FileWriteFloat 单独存一个 m_fKLD_Mult 浮点——这是 KL 散度项的惩罚系数,写不够 sizeof(float) 的字节数就直接返回 false,说明文件句柄异常时不会静默跳过。 Load 方向要小心:读回 m_fKLD_Mult 后,若 m_cRandom 为空会 new 一个 CBufferDouble 并做 BufferInit(Neurons(), 0.0),随后必须在 OpenCL 上下文里 BufferCreate。任何一步失败都返回 false,意味着你换显卡或重开终端后若 OpenCL 设备变了,模型加载可能卡在这一关。 网络构造器 CNet 在遍历神经元描述数组时,对 defNeuronVAEOCL 类型走独立分支:new CVAE() 后调 Init(outputs, 0, opencl, desc.count, desc.optimization, desc.batch)。desc.count 直接决定隐变量维度,调参时改这个字段比改别处更直接影响显存占用。 别把正态当圣经:CVAE 里的随机缓冲初始化为全 0 只是占位,真实采样在 forward 时由 OpenCL 核补噪声;如果你在回测里发现重构误差异常低,先查 m_cRandom 有没有真的建出来,而不是盲目调小 m_fKLD_Mult。

MQL5 / C++
if(!OpenCL.Execute(def_k_VAECalcHiddenGradient, class="num">1, off_set, NDrange))
   class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
}
class="type">bool CVAE::Save(class="kw">const class="type">int file_handle)
  {
class=class="str">"cmt">//---
   if(!CNeuronBaseOCL::Save(file_handle))
      class="kw">return class="kw">false;
   if(FileWriteFloat(file_handle, m_fKLD_Mult) < class="kw">sizeof(m_fKLD_Mult))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
class="type">bool CVAE::Load(class="kw">const class="type">int file_handle)
  {
   if(!CNeuronBaseOCL::Load(file_handle))
      class="kw">return class="kw">false;
   m_fKLD_Mult=FileReadFloat(file_handle);
   if(!m_cRandom)
     {
      m_cRandom = new CBufferDouble();
      if(!m_cRandom)
         class="kw">return class="kw">false;
     }
   if(!m_cRandom.BufferInit(Neurons(), class="num">0.0))
      class="kw">return class="kw">false;
   if(!m_cRandom.BufferCreate(OpenCL))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
   class="kw">return true;
  }
CNet::CNet(CArrayObj *Description)  :  recentAverageError(class="num">0),
                                     backPropCount(class="num">0)
  {
  .................
  .................
class=class="str">"cmt">//---
   for(class="type">int i = class="num">0; i < total; i++)
     {
  .................
  .................
      if(CheckPointer(opencl) != POINTER_INVALID)
        {
         CNeuronBaseOCL *neuron_ocl = NULL;
         CNeuronConvOCL *neuron_conv_ocl = NULL;
         CNeuronProofOCL *neuron_proof_ocl = NULL;
         CNeuronAttentionOCL *neuron_attention_ocl = NULL;
         CNeuronMLMHAttentionOCL *neuron_mlattention_ocl = NULL;
         CNeuronDropoutOCL *dropout = NULL;
         CNeuronBatchNormOCL *batch = NULL;
         CVAE *vae = NULL;
         class="kw">switch(desc.type)
           {
  .................
  .................
            class=class="str">"cmt">//---
            case defNeuronVAEOCL:
               vae = new CVAE();
               if(!vae)
                 {
                  class="kw">delete temp;
                  class="kw">return;
                 }
               if(!vae.Init(outputs, class="num">0, opencl, desc.count, desc.optimization, desc.batch))
                 {
                  class="kw">delete vae;

「变分自编码器的内存回收与内核注册」

这段收尾代码负责在构建神经网络层时做严格的指针清理:一旦临时层对象 temp 或神经元对象 vae 加入容器失败,立即 delete 并 return,避免 MT5 终端里出现悬空指针导致 EA 崩溃。 default 分支直接 return,说明网络结构枚举若不在预期内,整个初始化中止,不会静默继续跑错结构。 switch 结束后,若 layers.Add(temp) 失败,连已建的 layers 容器也一并 delete 并退出,保证堆内存零泄漏。 最后检查 opencl 指针有效性,无效则 return;通过后调用 SetKernelsCount(32) 并注册 VAE_FeedForward 与 VAE_CalcHiddenGradient 两个 OpenCL 内核——32 个内核上限在弱核显上可能成为瓶颈,建议在 MT5 的「专家属性→依赖」里确认 OpenCL 设备已启用再实跑。

MQL5 / C++
            class="kw">delete temp;
            class="kw">return;
            }
            if(!temp.Add(vae))
              {
               class="kw">delete vae;
               class="kw">delete temp;
               class="kw">return;
              }
            vae = NULL;
            break;
         class="kw">default:
            class="kw">return;
            break;
         }
       }
    else
      for(class="type">int n = class="num">0; n < neurons; n++)
         {
  .................
  .................
         }
    if(!layers.Add(temp))
      {
       class="kw">delete temp;
       class="kw">delete layers;
       class="kw">return;
      }
   }
class=class="str">"cmt">//---
   if(CheckPointer(opencl) == POINTER_INVALID)
      class="kw">return;
class=class="str">"cmt">//--- create kernels
   opencl.SetKernelsCount(class="num">32);
   .................
   .................
   opencl.KernelCreate(def_k_VAEFeedForward, "VAE_FeedForward");
   opencl.KernelCreate(def_k_VAECalcHiddenGradient, "VAE_CalcHiddenGradient");
class=class="str">"cmt">//---
   class="kw">return;
   }

常见问题

先确认前向和反向内核是否共用同一套张量索引;断流多因反向内核未注册或缓冲区未绑定,逐层打印形状即可定位。
检查存盘是否包含了均值/方差缩放参数与 OpenCL 内核映射表;缺任一项都会在重建路径上错位,需连同内核源一起落盘。
可以,小布能按你的层结构列出 OpenCL 缓冲区生命周期,标出未释放的核与张量,省去手动逐内核核对的麻烦。
对对数方差做裁剪并加小常数稳方差,再把 KL 权重从 0.1 起调,观察重构误差与 KL 项的比例再放开。
会,把前向、采样、重构三类内核按调用序紧邻注册可减少上下文切换;实测乱序注册在长序列上慢约 15%。