神经网络变得轻松(第三十部分):遗传算法(基础篇)
📘

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

第 1/3 篇

◍ 用遗传算法替你扫参数

在 MT5 里做 EA 优化,网格搜索常被维度爆炸拖垮:10 个参数各取 20 档,就是 20^10 ≈ 1.024e13 种组合,穷举不现实。遗传算法把参数编码成染色体,靠选择、交叉、变异迭代逼近较优解,适合高维非线性空间。 MQL5 的 OnTester 里可自定义适应度函数,把训练集回测的净值曲线稳定性、最大回撤、夏普近似写进去,让优化器按你定义的“生存压力”筛选后代。下面这段是种群个体结构的基础定义,先跑通它再谈复杂算子。 外汇与贵金属杠杆高、滑点跳空频繁,遗传算法给出的只是历史样本上的概率较优参数,实盘仍需小仓位验证。

MQL5 / C++
class="kw">struct Chromosome
{
   class="type">class="kw">double genes[class="num">10];      class=class="str">"cmt">// 10个待优化参数
   class="type">class="kw">double fitness;        class=class="str">"cmt">// 适应度值
};

「可微分为什么卡住模型训练」

在 MT5 里跑过 EA 参数寻优的人都知道,梯度下降类算法依赖一个硬前提:模型函数在整个数值域内可微。只有满足这点,才能算出每个权重对误差的贡献方向,进而按步长反向校正。 但现实里常遇到原函数不可微、或梯度在深层网络里爆炸 / 衰减的情况。此时传统分析法效率骤降,甚至完全失效。 这类场景下更务实的做法是切到进化优化:用种群变异和选择替代梯度计算,绕开可微要求。外汇与贵金属杠杆高、价格跳跃频繁,不可微结构并不少见,直接上梯度法容易踩坑。

用达尔文思路替 EA 找参数

进化优化属于无梯度方法,核心是从达尔文自然选择借来的逻辑:每一代种群里最适应环境的个体留下,其余淘汰。放到 MT5 里,我们不是训练单个模型,而是同时跑一整批策略实例,让它们在相同行情条件下各自按自己的规则下单。 种群规模是个要手调的超参数,直接决定探索宽度。规模越大,同代里并存的策略差异越多,覆盖的行情反应越全。这和强化学习里单代理重复随机试错不同,这里是多代理并行,每个实例占一条微处理器线程,由节点机收结果、造下一代。 一代跑完(场次时间有限,和前面强化学习约束一致),按奖励最大或损失最小挑出最优代表,比例由超参数定。重点:没用误差梯度,所以不可微的自定义目标函数也能当筛选条件。 选完亲本后,每个模型参数当成一条染色体,单个权重就是基因。继承时基因位置不动、每个基因随机选父或母;新一代填满后旧亲本直接丢弃,不进下一轮。 纯随机容易陷进老套路,所以再加一个突变概率超参数:每个新成员有一定概率不继承、而是直接塞进一个随机基因。这样每一代除了重组还有新料,和真实进化的扰动最接近。外汇/贵金属波动剧烈,这套并行随机搜索只是提高找到鲁棒参数的概率,不保证实盘盈利。

◍ 在 MT5 里用 CNetGenetic 把多个代理者塞进一个模型

理论看完,直接落到 MT5 实现。这次要并行跑多个线性模型,而不是像之前 CNet 那样只管单个。两条路:一是动态建对象数组、逐个跑,资源开销大;二是改类结构。我选了后者,从公开类 CNet 继承出新类 CNetGenetic,避免动原有工作流。 新类关键变量就四个:i_PopulationSize 种群规模、v_Probability 选亲本的概率向量、v_Rewards 各模型累积奖励向量、m_Weights 和 m_WeightsConv 存全连接与卷积层参数的矩阵。构造函数只初始化这些,设默认种群规模。析构函数为空,因为没建额外对象实例。 原 CNet 结构是 CNet -> CArrayLayer -> CLayer -> CNeuronBaseOCL。迁到 GPU 后每个 CLayer 实际只包一个神经元对象。我利用这点:不复制源数据层(索引0),从索引1开始,按种群规模往 CLayer 动态数组里加神经元对象。这样一个模型实例就装下了整个种群,靠代理者索引区分操作。 前馈时外循环走层、内循环走代理者。指定前一层神经元索引要判层号:源数据层永远用0,其它层用代理者序号。循环虽不是完全并行,但所有代理者复用同一份源数据,省了重复准备成本。 奖励回收包进 Rewards 方法:环境一次性传所有可能动作的奖励向量,方法内按代理者数采样动作、把奖励累加进 v_Rewards。外部程序不必每个代理者重复这套。 下一代在 NextGeneration 里生成。用 vector::Max() 一行取最大奖励,vector::Quantile() 定淘汰分位,vector::Clip(0, vector::Max()) 把弱势者概率置零,再归一化。突变比例加进分布后,逐层覆盖权重——从 m_Weights 里按概率分布随机取父项参数写回,卷积层同理。跑完把 v_Rewards 清零。 种群规模建议先设较小值验证:例如 i_PopulationSize=16 时,CreatePopulation 会在每层加15个神经元对象(除源数据层)。您打开 MT5 附加的 CNetGenetic 代码,改 SetPopulationSize 传参,就能看 OpenCL 显存占用随层深线性上涨。外汇/贵金属 EA 优化属高风险,回测过拟合可能令实盘失效。

「把遗传优化塞进三个方法调用里」

沿用原始奖励机制不做改动,是为了单独剥离出优化流程本身对结果的影响。每跑完一轮系统状态迭代,就把中间信息画出来肉眼盯流程,然后接着下一轮迭代。 单场评估结束,落盘保存当前最优代理的参数;随后调用 CNetGenetic::NextGeneration 拉起新一代,这里必须自己盯紧执行状态别漏了控制。日志里打印已得结果,循环进入新一代种群的评估。 优化收尾做数据清理,EA 退出。类的封装让主程序极简:整套优化实际只顺序调了类的 3 个方法,类比梯度下降训练,但单个代理内的业务量被砍掉一大截。 默认种群规模在构造函数写死为 100(i_PopulationSize(100)),SetPopulationSize 里同步把概率向量和奖励向量初始化成等长的零向量。想改规模,要么改构造入参,要么外部调 SetPopulationSize 覆盖。

MQL5 / C++
class CNetGenetic : class="kw">public CNet
	{
class="kw">protected:
	 class="type">uint					i_PopulationSize;
	 vector					v_Probability;
	 vector					v_Rewards;
	 matrixf				m_Weights;
	 matrixf				m_WeightsConv;
	 class=class="str">"cmt">//---
	 class="type">bool					CreatePopulation(class="type">void);
	 class="type">int					GetAction(CBufferFloat * probability);
	 class="type">bool					GetWeights(class="type">uint layer);
	 class="type">class="kw">float					NextGenerationWeight(matrixf &array, class="type">uint shift, vector &probability);
	 class="type">class="kw">float					GenerateWeight(class="type">uint total);
class="kw">public:
							 CNetGenetic();
							~CNetGenetic();
	 class=class="str">"cmt">//---
	 class="type">bool					Create(CArrayObj *Description, class="type">uint population_size);
	 class="type">bool					SetPopulationSize(class="type">uint size);
	 class="type">bool					feedForward(CArrayFloat *inputVals, class="type">int window = class="num">1, class="type">bool tem = true);
	 class="type">bool					Rewards(CArrayFloat *targetVals);
	 class="type">bool					NextGeneration(class="type">class="kw">double quantile, class="type">class="kw">double mutation, class="type">class="kw">double &average, class="type">class="kw">double &mamximum);
	 class="type">bool					Load(class="type">class="kw">string file_name, class="type">uint population_size, class="type">bool common = true);
	 class="type">bool					SaveModel(class="type">class="kw">string file_name, class="type">int model, class="type">bool common = true);
	 class=class="str">"cmt">//---
	 class="type">bool					CopyModel(CArrayLayer *source, class="type">uint model);
	 class="type">bool					Detach(class="type">void);
	};
CNetGenetic::CNetGenetic() :	i_PopulationSize(class="num">100)
	{
	 SetPopulationSize(i_PopulationSize);
	}
class="type">bool CNetGenetic::SetPopulationSize(class="type">uint size)
	{
	 i_PopulationSize = size;
	 v_Probability = vector::Zeros(i_PopulationSize);
	 v_Rewards = vector::Zeros(i_PopulationSize);
class=class="str">"cmt">//---
	 class="kw">return true;
	}
class="type">bool CNetGenetic::Create(CArrayObj *Description, class="type">uint population_size)
	{
	 if(CheckPointer(Description) == POINTER_INVALID)
		class="kw">return class="kw">false;
class=class="str">"cmt">//---
	 if(!SetPopulationSize(population_size))
		class="kw">return class="kw">false;
	 CNet::Create(Description);

种群初始化时的层克隆逻辑

遗传算法里种群不是凭空生成的,而是基于已定义的第一层结构向后逐层克隆神经元。代码先卡死前置条件:layers 指针有效且层数不少于 2,否则直接返回 false,这说明单隐层网络不在该类的支持范围内。 从第二层开始,循环读取该层的描述符 CLayerDescription,拿到 outputs(上一层连接数)作为新神经元的输入维度,再按 i_PopulationSize 补齐当前层神经元数量。这里 layer.Total() 是已有个体数,n 从它起步到种群上限,避免重复构造。 根据 layer.At(0).Type() 分支 new 出对应类型神经元,例如 defNeuron 分支走 CNeuronBaseOCL,调 Init(outputs, n, opencl, desc.count, desc.optimization, desc.batch) 做显存与权重分配,失败即 delete 回滚。外汇与贵金属模型用这套跑群体进化时,显存占用会随 i_PopulationSize 线性放大,实盘前务必在 MT5 终端用较小的种群数(如 20)验证不爆卡。 顺带一提,卷积、注意力、LSTM 等类型在 switch 里各有独立构造分支,但都共用「先 new 再 Init 再 Add」的节奏;改网络结构时,只动类型枚举和对应类即可,不用碰外层种群循环。

MQL5 / C++
  class="kw">return CreatePopulation();
  }
class="type">bool CNetGenetic::CreatePopulation(class="type">void)
  {
  if(!layers || layers.Total() < class="num">2)
      class="kw">return class="kw">false;
  CLayer *layer = layers.At(class="num">0);
  if(!layer || !layer.At(class="num">0))
      class="kw">return class="kw">false;
class=class="str">"cmt">//---
  CNeuronBaseOCL *neuron_ocl = layer.At(class="num">0);
  class="type">int prev_count = neuron_ocl.Neurons();
  for(class="type">int i = class="num">1; i < layers.Total(); i++)
    {
      layer = layers.At(i);
      if(!layer || !layer.At(class="num">0))
        class="kw">return class="kw">false;
      class=class="str">"cmt">//---
      neuron_ocl = layer.At(class="num">0);
      CLayerDescription *desc = neuron_ocl.GetLayerInfo();
      class="type">int outputs = neuron_ocl.getConnections();
      for(class="type">uint n = layer.Total(); n < i_PopulationSize; n++)
        {
          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;
          CNeuronLSTMOCL *lstm = NULL;
          class="kw">switch(layer.At(class="num">0).Type())
            {
              case defNeuron:
              case defNeuronBaseOCL:
                neuron_ocl = new CNeuronBaseOCL();
                if(CheckPointer(neuron_ocl) == POINTER_INVALID)
                  class="kw">return class="kw">false;
                if(!neuron_ocl.Init(outputs, n, opencl, desc.count, desc.optimization, desc.batch))
                  {
                    class="kw">delete neuron_ocl;
                    class="kw">return class="kw">false;
                  }
                neuron_ocl.SetActivationFunction(desc.activation);
                if(!layer.Add(neuron_ocl))
                  {
                    class="kw">delete neuron_ocl;
                    class="kw">return class="kw">false;
                  }
                neuron_ocl = NULL;
                class="kw">break;
              case defNeuronConvOCL:
                neuron_conv_ocl = new CNeuronConvOCL();

常见问题

可以用遗传算法让程序自动交叉变异筛选,把多组参数当种群跑进化,比人工一个个试快得多。
可能是可微分路径陷入局部极值或梯度消失,传统梯度法卡住时,换达尔文式随机搜索往往能跳出。
小布可把遗传优化流程封装成对话指令,你描述品种和约束,它直接给出可跑的种群配置建议。
初始化会对基准网络做层克隆,每个代理者拿到相同结构不同权重的副本,再各自进化。
核心就三个方法调用:初始化种群、执行代际进化、取最优代理者,其余都是内部逻辑。