神经网络变得轻松(第二十一部分):变分自动编码器(VAE)·进阶篇
「变分自编码层的梯度回流实现」
在 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 跑同一段历史,看验证集误差曲线是否更平。
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 导致整个网络不更新。外汇与贵金属行情高频噪声大,用这类随机层做特征压缩须清楚:过拟合概率不低,属高风险实验。
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。
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 设备已启用再实跑。
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; }