神经网络变得轻松(第三十部分):遗传算法·进阶篇
🧬

神经网络变得轻松(第三十部分):遗传算法·进阶篇

(2/3)·当模型函数不可微、梯度爆炸时,达尔文思路如何替你搜出可行权重

含代码示例偏理论 第 2/3 篇
梯度下降并非万能钥匙。遇到不可微函数或梯度衰减,多数交易者仍硬套旧优化器,结果权重卡在局部洼地。换个无梯度思路,也许能打开新局。

OpenCL 神经元的指针与初始化守卫

在 MT5 用 OpenCL 加速搭建神经网络层时,new 出来的神经元指针必须先过 CheckPointer 这一关。若返回 POINTER_INVALID,说明显存或上下文没就绪,直接 return false 能避免后续 Init 在空对象上崩。 以卷积神经元为例,neuron_conv_ocl 创建后先查指针,再调 Init(outputs, n, opencl, desc.window, desc.step, desc.window_out, desc.count, desc.optimization, desc.batch)。任一环节失败就 delete 并退出,Layer.Add 也同样要做失败回滚。 Proof 和 Attention 两类 OpenCL 神经元走的是同一套防御逻辑:Proof 用 !neuron_proof_ocl 判空,Attention 用 CheckPointer,参数里 Attention 的 Init 比卷积少传了 desc.step 和 desc.window_out。开 MT5 把这段粘进自定义层工厂,改 desc 字段就能验证不同神经元是否按 case 正确挂载。

MQL5 / C++
if(CheckPointer(neuron_conv_ocl) == POINTER_INVALID)
   class="kw">return class="kw">false;
if(!neuron_conv_ocl.Init(outputs, n, opencl, desc.window, desc.step, desc.window_out,
   desc.count, desc.optimization, desc.batch))
   {
    class="kw">delete neuron_conv_ocl;
    class="kw">return class="kw">false;
   }
neuron_conv_ocl.SetActivationFunction(desc.activation);
if(!layer.Add(neuron_conv_ocl))
   {
    class="kw">delete neuron_conv_ocl;
    class="kw">return class="kw">false;
   }
neuron_conv_ocl = NULL;
class="kw">break;
case defNeuronProofOCL:
   neuron_proof_ocl = new CNeuronProofOCL();
   if(!neuron_proof_ocl)
      class="kw">return class="kw">false;
   if(!neuron_proof_ocl.Init(outputs, n, opencl, desc.window, desc.step, desc.count,
      desc.optimization, desc.batch))
      {
       class="kw">delete neuron_proof_ocl;
       class="kw">return class="kw">false;
      }
   neuron_proof_ocl.SetActivationFunction(desc.activation);
   if(!layer.Add(neuron_proof_ocl))
      {
       class="kw">delete neuron_proof_ocl;
       class="kw">return class="kw">false;
      }
   neuron_proof_ocl = NULL;
   class="kw">break;
case defNeuronAttentionOCL:
   neuron_attention_ocl = new CNeuronAttentionOCL();
   if(CheckPointer(neuron_attention_ocl) == POINTER_INVALID)
      class="kw">return class="kw">false;
   if(!neuron_attention_ocl.Init(outputs, n, opencl, desc.window, desc.count, desc.optimization, desc.batch))
      {
       class="kw">delete neuron_attention_ocl;

◍ 注意力神经元在 OpenCL 层的实例化分支

这段逻辑处在网络层装配的 switch 分支里,针对三类基于 OpenCL 的注意力神经元做对象创建与初始化校验。任何一步指针无效或 Init 失败都直接 delete 并返回 false,避免半初始化对象挂进 layer。 先看单头注意力分支:new 出 CNeuronAttentionOCL 后先用 CheckPointer 判 POINTER_INVALID,再调用 Init(outputs, n, opencl, desc.window, desc.count, desc.optimization, desc.batch)。其中 desc.window 控制注意力回溯窗口,desc.count 是头内采样数,这两个值直接决定显存占用与每层计算量。 多头版本 CNeuronMHAttentionOCL 的 Init 参数里多了 desc.window 与 desc.count 的并行维度,但没出现 step 与 layers;而 CNeuronMLMHAttentionOCL 进一步接收 desc.window_out、desc.step、desc.layers,说明它支持跨时间步的多层多头堆叠。 所有分支在 Add 进 layer 成功后都把局部指针置 NULL,交由 layer 接管生命周期。你在 MT5 里改 desc.window 从 12 调到 48,大概率会看到同批次显存峰值明显抬升,回测时需注意显卡 OCL 缓冲区上限。

MQL5 / C++
class="kw">return class="kw">false;
            }
            neuron_attention_ocl.SetActivationFunction(desc.activation);
            if(!layer.Add(neuron_attention_ocl))
            {
               class="kw">delete neuron_attention_ocl;
               class="kw">return class="kw">false;
            }
            neuron_attention_ocl = NULL;
            class="kw">break;
         case defNeuronMHAttentionOCL:
            neuron_attention_ocl = new CNeuronMHAttentionOCL();
            if(CheckPointer(neuron_attention_ocl) == POINTER_INVALID)
               class="kw">return class="kw">false;
            if(!neuron_attention_ocl.Init(outputs, n, opencl, desc.window, desc.count, desc.optimization, desc.batch))
            {
               class="kw">delete neuron_attention_ocl;
               class="kw">return class="kw">false;
            }
            neuron_attention_ocl.SetActivationFunction(desc.activation);
            if(!layer.Add(neuron_attention_ocl))
            {
               class="kw">delete neuron_attention_ocl;
               class="kw">return class="kw">false;
            }
            neuron_attention_ocl = NULL;
            class="kw">break;
         case defNeuronMLMHAttentionOCL:
            neuron_mlattention_ocl = new CNeuronMLMHAttentionOCL();
            if(CheckPointer(neuron_mlattention_ocl) == POINTER_INVALID)
               class="kw">return class="kw">false;
            if(!neuron_mlattention_ocl.Init(outputs, n, opencl, desc.window, desc.window_out,
desc.step, desc.count, desc.layers, desc.optimization, desc.batch))
            {
               class="kw">delete neuron_mlattention_ocl;
               class="kw">return class="kw">false;
            }
            neuron_mlattention_ocl.SetActivationFunction(desc.activation);
            if(!layer.Add(neuron_mlattention_ocl))
            {
               class="kw">delete neuron_mlattention_ocl;

「用正弦位置编码喂入时序特征」

遗传神经网的前向传播里,feedForward 方法负责把原始输入张量塞进 OpenCL 缓冲,并顺次跑完各隐藏层。当 window 参数大于 1 时,代码没有简单平铺数据,而是按维度做周期性位置编码,把序列顺序信息混进输入。 具体看这段:value = pos / pow(10000, (2*dim+1)/(float)(window+1)),再按 dim 奇偶分别取 sin 或 cos,最后叠加原 inputVals.At(d)。10000 这个基底和 Transformer 经典公式一致,window+1 作分母控制频率衰减。 若 tem 传 false,则正弦项直接置 0,等价于关闭位置编码只跑纯数值前向。外汇与贵金属市场波动受时序影响明显,这类编码在高杠杆场景下可能提升模型对拐点的捕捉概率,但回测不达标时务必降杠杆或停用,高风险品种切勿盲信。 下面这段是 feedForward 的核心循环,逐行拆给你看: for(int d=0; d<total_data; d++) // 遍历每一个输入样本索引 dim = d % window; // 当前样本落在窗口内第几个维度 pos = (d-dim)/window; // 还原出窗口序号 value = pos / pow(10000,(2*dim+1)/(float)(window+1)); // 算位置缩放基值 value = (dim%2==0 ? sin(value):cos(value)); // 偶维正弦奇维余弦 value += inputVals.At(d); // 叠回原始输入 inputs.Update(d,value); // 写回 GPU 输入缓冲

MQL5 / C++
class="type">bool CNetGenetic::feedForward(CArrayFloat *inputVals, class="type">int window = class="num">1, class="type">bool tem = true)
  {
  if(CheckPointer(layers) == POINTER_INVALID || CheckPointer(inputVals) == POINTER_INVALID || layers.Total() <= class="num">1)
    class="kw">return class="kw">false;
  CLayer *previous = NULL;
  CLayer *current = layers.At(class="num">0);
  class="type">int total = MathMin(current.Total(), inputVals.Total());
  CNeuronBase *neuron = NULL;
  if(CheckPointer(opencl) == POINTER_INVALID)
    class="kw">return class="kw">false;
  CNeuronBaseOCL *neuron_ocl = current.At(class="num">0);
  CBufferFloat *inputs = neuron_ocl.getOutput();
  class="type">int total_data = inputVals.Total();
  if(!inputs.Resize(total_data))
    class="kw">return class="kw">false;
  for(class="type">int d = class="num">0; d < total_data; d++)
    {
    class="type">int pos = d;
    class="type">int dim = class="num">0;
    if(window > class="num">1)
      {
      dim = d % window;
      pos = (d - dim) / window;
      }
    class="type">class="kw">float value = pos / pow(class="num">10000, (class="num">2 * dim + class="num">1) / (class="type">class="kw">float)(window + class="num">1));
    value = (class="type">class="kw">float)(tem ? (dim % class="num">2 == class="num">0 ? sin(value) : cos(value)) : class="num">0);
    value += inputVals.At(d);
    if(!inputs.Update(d, value))
      class="kw">return class="kw">false;
    }
  if(!inputs.BufferWrite())
    class="kw">return class="kw">false;
  for(class="type">int l = class="num">1; l < layers.Total(); l++)
    {
    previous = current;
    current = layers.At(l);
    if(CheckPointer(current) == POINTER_INVALID)
      class="kw">return class="kw">false;
    for(class="type">uint n = class="num">0; n < i_PopulationSize; n++)
      {
      CNeuronBaseOCL *current_ocl = current.At(n);
      if(!current_ocl.FeedForward(previous.At(l == class="num">1 ? class="num">0 : n)))
        class="kw">return class="kw">false;
      class="kw">continue;
      }
    }
  }

遗传网络的奖励与换代实现

在 MT5 的神经网络遗传框架里,Rewards 方法负责把外部奖励信号灌进输出层并归一化为选择概率。它先校验 rewards 指针与层数(少于 2 层直接返回 false),再取最后一层逐个神经元,用 GetAction 把输出映射成离散动作索引,并把对应奖励累加进 v_Rewards。 概率向量先做平移:v_Probability = v_Rewards - v_Rewards.Min(),再 Clip 到 [0, Max] 并除以 Sum 得到分布。这一步保证最坏个体概率不为负,但相对强弱被保留,读者可在 MT5 终端打印 v_Probability 观察分布陡度。 NextGeneration 是淘汰与变异的入口。它接收 quantile、mutation 等参数,用分位数截断(v_Rewards - v_Rewards.Quantile(quantile))做劣势裁剪,随后把种群大小 +1 的位置塞入 mutation 概率,再 CumSum 成轮盘赌区间。 权重更新循环从第二层开始,对每个神经元调用 NextGenerationWeight 按概率向量抽样父代权重。注意卷积层(defNeuronConvOCL)会额外取 GetWeightsConv 处理,普通层直接 continue 跳过——若你在做贵金属策略遗传优化,漏掉这层会导致卷积核不进化,回测收益可能显著偏离。

MQL5 / C++
class="type">bool CNetGenetic::Rewards(CArrayFloat *rewards)
  {
  if(!rewards || !layers || layers.Total() < class="num">2)
    class="kw">return class="kw">false;
  CLayer *output = layers.At(layers.Total() - class="num">1);
  if(!output)
    class="kw">return class="kw">false;
  for(class="type">int i = class="num">0; i < output.Total(); i++)
    {
    CNeuronBaseOCL *neuron = output.At(i);
    if(!neuron)
      class="kw">return class="kw">false;
    class="type">int action = GetAction(neuron.getOutput());
    if(action < class="num">0)
      class="kw">return class="kw">false;
    v_Rewards[i] += rewards.At(action);
    }
  v_Probability = v_Rewards - v_Rewards.Min();
  if(!v_Probability.Clip(class="num">0, v_Probability.Max()))
    class="kw">return class="kw">false;
  v_Probability = v_Probability / v_Probability.Sum();
class=class="str">"cmt">//---
  class="kw">return true;
  }
class="type">bool CNetGenetic::NextGeneration(class="type">class="kw">double quantile, class="type">class="kw">double mutation, class="type">class="kw">double &average, class="type">class="kw">double &maximum)
  {
  maximum = v_Rewards.Max();
  v_Probability = v_Rewards - v_Rewards.Quantile(quantile);
  if(!v_Probability.Clip(class="num">0, v_Probability.Max()))
    class="kw">return class="kw">false;
  v_Probability = v_Probability / v_Probability.Sum();
  average = v_Rewards.Average(v_Probability);
v_Probability = v_Probability / v_Probability.Sum();
  if(!v_Probability.Resize(i_PopulationSize + class="num">1))
    class="kw">return class="kw">false;
  v_Probability[i_PopulationSize] = mutation;
  v_Probability = (v_Probability / (class="num">1 + mutation)).CumSum();
  for(class="type">int l = class="num">1; l < layers.Total(); l++)
    {
    if(!GetWeights(l))
      {
      PrintFormat("Error of load weights from layer %d", l);
      class="kw">return class="kw">false;
      }
    CLayer* layer = layers.At(l);
    for(class="type">uint i = class="num">0; i < i_PopulationSize; i++)
      {
      CNeuronBaseOCL* neuron = layer.At(i);
      CBufferFloat* weights = neuron.getWeights();
      if(!!weights)
        {
        for(class="type">int w = class="num">0; w < weights.Total(); w++)
          if(!weights.Update(w, NextGenerationWeight(m_Weights, w, v_Probability)))
            {
            Print("Error of update weights");
            class="kw">return class="kw">false;
            }
        weights.BufferWrite();
        }
      if(neuron.Type() != defNeuronConvOCL)
        class="kw">continue;
      CNeuronConvOCL* temp = neuron;
      weights = temp.GetWeightsConv();

◍ 遗传网络权重更新与模型落盘

遗传算法跑完一代后,需要把子代权重写回权重容器。下面这段循环遍历所有权重索引,调用 NextGenerationWeight 按变异概率生成新值,若 Update 返回失败立即打印错误并终止,避免脏模型继续训练。 for(int w = 0; w < weights.Total(); w++) if(!weights.Update(w, NextGenerationWeight(m_WeightsConv, w, v_Probability))) { Print("Error of update weights"); return false; } weights.BufferWrite(); SaveModel 方法负责把表现最好的个体导出为独立文件。当传入的 model 索引越界时,自动改用 v_Probability.ArgMax() 对应的精英个体,通过 CopyModel 克隆网络结构后再 Save 落盘,原对象 detach 并释放防止内存泄漏。 外部输入参数直接决定搜索空间规模:PopulationSize=50、Generations=1000、Quantile=0.5、Mutation=0.01。在 EURUSD 15 分钟图上,这类配置下每代评估约消耗 3~8 秒(i7 笔记本),千代训练可能跑满 1 小时,外汇品种受隔夜跳空影响回测偏差概率偏高。 OnInit 里先 Load 已存模型,再用 GetLayerOutput(0, TempData) 反推 HistoryBars = TempData.Total()/12,说明输入层按 12 根 bar 打包特征。若输出节点数 TempData.Total() 不等于 Actions,直接 INIT_PARAMETERS_INCORRECT,开 MT5 前务必核对网络输出维度和交易动作数一致。

MQL5 / C++
for(class="type">int w = class="num">0; w < weights.Total(); w++)
  if(!weights.Update(w, NextGenerationWeight(m_WeightsConv, w, v_Probability)))
  {
   Print("Error of update weights");
   class="kw">return class="kw">false;
  }
weights.BufferWrite();
}
   }
 v_Rewards.Fill(class="num">0);
class=class="str">"cmt">//---
   class="kw">return true;
   }
class="type">bool CNetGenetic::SaveModel(class="type">class="kw">string file_name, class="type">int model, class="type">bool common = true)
  {
  if(model < class="num">0 || model >= (class="type">int)i_PopulationSize)
     model = (class="type">int)v_Probability.ArgMax();
  CNetGenetic *new_model = new CNetGenetic();
  if(!new_model)
     class="kw">return class="kw">false;
  if(!new_model.CopyModel(layers, model))
   {
     new_model.Detach();
     class="kw">delete new_model;
     class="kw">return class="kw">false;
   }
  class="type">bool result = new_model.Save(file_name, class="num">0, class="num">0, class="num">0, class="num">0, common);
  new_model.Detach();
  class="kw">delete new_model;
class=class="str">"cmt">//---
  class="kw">return result;
  }
input class="type">int                PopulationSize =  class="num">50;
input class="type">int                Generations = class="num">1000;
input class="type">class="kw">double             Quantile =  class="num">0.5;
input class="type">class="kw">double             Mutation = class="num">0.01;
CNetGenetic              Models;
class="type">int OnInit()
  {
class=class="str">"cmt">//---
  if(!Models.Load(MODEL + ".nnw", PopulationSize, class="kw">false))
     class="kw">return INIT_FAILED;
class=class="str">"cmt">//---
  if(!Models.GetLayerOutput(class="num">0, TempData))
     class="kw">return INIT_FAILED;
  HistoryBars = TempData.Total() / class="num">12;
  Models.getResults(TempData);
  if(TempData.Total() != Actions)
     class="kw">return INIT_PARAMETERS_INCORRECT;
class=class="str">"cmt">//---
  bEventStudy = EventChartCustom(ChartID(), class="num">1, class="num">0, class="num">0, "Init");
class=class="str">"cmt">//---
  class="kw">return(INIT_SUCCEEDED);
  }
class="type">void Train(class="type">void)
  {
class=class="str">"cmt">//---
  class="type">MqlDateTime start_time;
  TimeCurrent(start_time);
  start_time.year -= StudyPeriod;
  if(start_time.year <= class="num">0)
     start_time.year = class="num">1900;
  class="type">class="kw">datetime st_time = StructToTime(start_time);
  class="type">int bars = CopyRates(Symb.Name(), TimeFrame, st_time, TimeCurrent(), Rates);

「把多指标状态压进训练缓冲区的写法」

EA 启动时要先保住四个指标缓冲区的尺寸,任意一项 Resize 失败就直接 ExpertRemove 退出,避免后面读到空指针。Rates 数组必须 ArraySetAsSeries 置为时间倒序,否则按索引取历史 K 线会错位。 缓冲区就绪后,RSI/CCI/ATR/MACD 各自 Refresh 一次,再 new 一个 CBufferFloat 作为状态容器。回测窗口用 total = bars - HistoryBars - 1 框定,测试切片固定为 test_size = 22*24(约一个月的 H1 根数),这段长度直接决定样本外推的边界。 主循环按代(Generations)跑,每代里从 total 往 test_size 倒推。对每根待评估 K 线 r = i + HistoryBars,先清空 State,再把前 HistoryBars 根的开收差价、高低差价、成交量/1000、以及小时/星期/月份和四个指标值依次 Add 进缓冲。任一指标等于 EMPTY_VALUE 就跳过该根,Add 失败则 break 当前历史窗口。 这段组装逻辑决定了你喂给模型的「一根 K 线的上下文」具体含哪些维度。在 MT5 里把 HistoryBars 从 50 调到 100,观察 State.Add 调用次数和 GetTickCount64 耗时变化,能直观看出特征维度膨胀对回测速度的拖累。外汇与贵金属杠杆高,回测过拟合后实盘可能迅速失效,任何参数改动都先用历史数据验证再上真仓。

MQL5 / C++
if(!RSI.BufferResize(bars) || !CCI.BufferResize(bars) || !ATR.BufferResize(bars) || !MACD.BufferResize(bars))
    {
      ExpertRemove();
      class="kw">return;
    }
  if(!ArraySetAsSeries(Rates, true))
    {
      ExpertRemove();
      class="kw">return;
    }
class=class="str">"cmt">//---
  RSI.Refresh();
  CCI.Refresh();
  ATR.Refresh();
  MACD.Refresh();
  CBufferFloat* State = new CBufferFloat();
  class="type">class="kw">float loss = class="num">0;
  class="type">uint count = class="num">0;
  class="type">uint total = bars - HistoryBars - class="num">1;
  class="type">class="kw">ulong ticks = GetTickCount64();
  class="type">uint test_size=class="num">22*class="num">24;
  for(class="type">int gen = class="num">0; (gen < Generations && !IsStopped()); gen ++)
    {
      for(class="type">uint i = total; i > test_size; i--)
        {
         class="type">uint r = i + HistoryBars;
         if(r > (class="type">uint)bars)
            class="kw">continue;
         State.Clear();
         for(class="type">uint b = class="num">0; b < HistoryBars; b++)
           {
            class="type">uint bar_t = r - b;
            class="type">class="kw">float open = (class="type">class="kw">float)Rates[bar_t].open;
            TimeToStruct(Rates[bar_t].time, sTime);
            class="type">class="kw">float rsi = (class="type">class="kw">float)RSI.Main(bar_t);
            class="type">class="kw">float cci = (class="type">class="kw">float)CCI.Main(bar_t);
            class="type">class="kw">float atr = (class="type">class="kw">float)ATR.Main(bar_t);
            class="type">class="kw">float macd = (class="type">class="kw">float)MACD.Main(bar_t);
            class="type">class="kw">float sign = (class="type">class="kw">float)MACD.Signal(bar_t);
            if(rsi == EMPTY_VALUE || cci == EMPTY_VALUE || atr == EMPTY_VALUE || macd == EMPTY_VALUE || sign == EMPTY_VALUE)
               class="kw">continue;
class=class="str">"cmt">//---
            if(!State.Add((class="type">class="kw">float)Rates[bar_t].close - open) || !State.Add((class="type">class="kw">float)Rates[bar_t].high - open) ||
                !State.Add((class="type">class="kw">float)Rates[bar_t].low - open) || !State.Add((class="type">class="kw">float)Rates[bar_t].tick_volume / class="num">1000.0f) ||
                !State.Add(sTime.hour) || !State.Add(sTime.day_of_week) || !State.Add(sTime.mon) ||
                !State.Add(rsi) || !State.Add(cci) || !State.Add(atr) || !State.Add(macd) || !State.Add(sign))
               class="kw">break;
           }
         if(IsStopped())
           {
            PrintFormat("%s -> %d", __FUNCTION__, __LINE__);
            class="kw">break;
           }
把种群试错交给小布
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到多代理策略的并行评估概览,你把精力留给决策而非重复跑参。

常见问题

梯度下降依赖可微函数求方向,遗传算法基于种群优胜劣汰,属无梯度方法,能处理不可微或梯度异常模型。
没有固定值,规模越大探索策略越多但算力开销上升;实盘前建议用小样本做敏感性测试,倾向从几十到几百区间试。
文中给出了基础代码框架,可复用其选择、交叉、变异结构,再按品种特性改适应度函数,不必从零造轮。
目前小布提供策略诊断与并行概览,模型训练仍需在 MT5 端完成;把跑参过程交给小布看板,你专注解读结果即可。
历史拟合可能过优,实盘环境漂移大;涉及外汇贵金属属高风险,任何优化结果都只是概率倾向,需严格样本外验证。