神经网络变得轻松(第三十一部分):进化算法(基础篇)
◍ 进化算法在 MT5 里的底层构造
进化算法本质是把交易策略的参数空间当成种群,用选择、交叉、变异去逼近更优解。和传统梯度下降不同,它不要求目标函数可导,适合处理 EA 里那些离散且非线性的参数组合。 在 MQL5 里实现时,核心是先定义个体结构——通常是一个 double 数组存参数,再用一个适应度函数回测该组参数在历史数据上的表现。种群规模建议从 50~200 起步,太少容易早熟收敛,太多则拖慢 MT5 的优化器单代耗时。 下面这段给出了最基础的个体定义与适应度接口骨架,实际跑起来要接 OnTester 拿到历史回测权益曲线。注意外汇和贵金属杠杆高,历史适应度高不代表实盘概率占优,参数过拟合风险始终在。
「非梯度优化为何绕开导数也能跑」
在 MT5 里做模型调参,经常会撞上目标函数不可导、或导数算起来代价过高的情况。这时候梯度下降类方法直接失效,而非梯度优化靠的是“评估—筛选—繁衍”的种群逻辑,不依赖任何解析梯度。 上一篇文章里我们拆过遗传算法:把模型的每个权重当成一条基因,先随机撒一批模型组成种群,跑完评估后只留表现靠前的个体进入下一代。 每一代结束,算法给新种群随机配对“亲本”,子代的基因从双亲处随机继承。这种机制让它在黑箱目标上也能搜解,但代价是评估次数远高于梯度法——实盘前务必在策略测试器里先跑小种群验证开销。外汇与贵金属杠杆高,参数过拟合会放大账户回撤风险。
用实验梯度把随机性收编
前面那套遗传优化里,每一代都要把整个种群跑完,大多数个体直接淘汰,等于做了大量无用功。模型往哪个方向演化,很大程度上靠运气,没有任何机制保证它朝目标走。 梯度下降法在每次迭代都朝抗梯度方向走,误差被刻意压小,方向是确定的。但它要求你能算出判定函数对参数的导数。 导数本质是函数值随参数变化的极限比率。解析导数算不出来时,可以实验近似:固定其它条件,把参数 x 微调一小步,看函数值变了多少,两者之比就是近似导数。 对非线性模型,建议每个参数做双向扰动——加一个 delta 再减同一个 delta,取两次结果的平均值,这样在给定点的导数近似更稳。进化策略正是靠这个属性来定优化方向。 麻烦在于实验梯度极耗算力。判定一个参数对结果的影响,需要对同一份源数据做 3 次前馈:原值、加扰、减扰,全部参数迭代成本直接翻 3 倍。 折中思路是把模型连同参数集当成一个黑盒整体。给原模型所有参数叠加随机噪音 ε,生成一群彼此不同但偏差很小的邻近模型,在有限训练集上测各自有效性。 更新时不再逐个分析参数,而是用种群里每个模型的噪音按总奖励加权求平均,再乘学习系数加回原参数。公式 w' = w + α·Σ(R_i·ε_i)/ΣR_i 与梯度下降权重更新长得几乎一样。 OpenAI 在 2017 年 3 月论文里提出这套进化策略,用一千多台并行机 10 分钟解决三维人形行走,靠堆算力换近线性加速。外汇与贵金属模型若照搬,需自担过拟合与高频波动的高风险,并行规模小则收敛慢得多。
◍ 从遗传父类里抠出一个进化网络类
把论文里的进化思路落到 MT5,做法不是重写一套神经网络,而是从已有的遗传算法类私下继承出 CNetEvolution。继承设成 protected,父类没用到的重载方法对外直接不可见,能少很多调用侧的误用混淆。类体内不声明任何新变量,权重矩阵和卷积矩阵全沿用父类对象,构造和析构都留空。 关键偏差在噪音注入前不备份原始权重,且选动作走概率算法而非原作者的贪婪法,还额外加了突变参数。种群初始化时先调父类 Create 生成同架构随机模型,再立刻跑 NextGeneration 把参数收拢到接近——突变上限必须钳住,否则每轮都近似随机重采样,种群永远训不出来。 NextGeneration 里写概率向量 v_Probability 的逻辑值得细看:用单模型总奖励减种群平均奖励,正差给正概率、负差给负概率,再按绝对值归一使和为 1。假设噪音服从正态分布,原始模型奖励约处于分布中部,这样正噪音乘正概率推权重同向、负噪音乘负概率把偏移扭回,训练倾向朝总奖励最大化走。外汇与贵金属杠杆品种下,这类概率偏移只提高收敛可能,不保证样本外稳健。 GetWeights 负责从种群参数矩阵减均值向量反推附加噪音矩阵,再套同样概率加权更新。加载预训练模型时只存奖励最优的那个,读盘后调父类填充随机种群,再跑一次 NextGeneration:因总奖励向量已重置为零,索引 0 的加载模型以 100% 概率成为新种群基准,随机模型参与度为 0。开 MT5 把下面类头文件贴进 EA 包含目录,能直接验证继承重定向是否编过。
<span class="keyword">class</span> CNetEvolution : <span class="keyword">class="kw">protected</span> CNetGenetic { <span class="keyword">class="kw">protected</span>: <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> GetWeights(<span class="keyword">class="type">uint</span> layer) <span class="keyword">class="kw">override</span>; <span class="keyword">class="kw">public</span>: CNetEvolution() {}; ~CNetEvolution() {}; <span class="comment">class=class="str">"cmt">//---</span> <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> Create(CArrayObj *Description, <span class="keyword">class="type">uint</span> population_size) <span class="keyword">class="kw">override</span>; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> SetPopulationSize(<span class="keyword">class="type">uint</span> size) <span class="keyword">class="kw">override</span>; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> feedForward(CArrayFloat *inputVals, <span class="keyword">class="type">int</span> window = <span class="number">class="num">1</span>, <span class="keyword">class="type">bool</span> tem = <span class="macro">true</span>) <span class="keyword">class="kw">override</span>; <span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">bool</span> Rewards(CArrayFloat *rewards) <span class="keyword">class="kw">override</span>;