神经网络变得轻松(第三十部分):遗传算法(基础篇)
◍ 用遗传算法替你扫参数
在 MT5 里做 EA 优化,网格搜索常被维度爆炸拖垮:10 个参数各取 20 档,就是 20^10 ≈ 1.024e13 种组合,穷举不现实。遗传算法把参数编码成染色体,靠选择、交叉、变异迭代逼近较优解,适合高维非线性空间。 MQL5 的 OnTester 里可自定义适应度函数,把训练集回测的净值曲线稳定性、最大回撤、夏普近似写进去,让优化器按你定义的“生存压力”筛选后代。下面这段是种群个体结构的基础定义,先跑通它再谈复杂算子。 外汇与贵金属杠杆高、滑点跳空频繁,遗传算法给出的只是历史样本上的概率较优参数,实盘仍需小仓位验证。
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 覆盖。
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」的节奏;改网络结构时,只动类型枚举和对应类即可,不用碰外层种群循环。
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();