群体优化算法:抵抗陷入局部极值(第二部分)·进阶篇
「五类优化器在测试地形上的得分对照」
把 HS、SDOm、BFO-GA、SDSm、BGA 五种元启发式优化器塞进同一套基准里跑,每组函数各做 10000 次 Func runs,能直接看出它们在连续与离散地形上的分化。Hilly 与 Forest 是平滑曲面,Megacity 是离散网格,维度分 5 / 25 / 500 三档,All score 是三项加总。 HS(参数 50;0.9;0.1;0.2)在 5 维 Hilly 拿到 0.602、Forest 拿到 0.650,但 500 维 Megacity 跌到 0.128,All score 仅 3.523。它的高速全局搜索遇到离散首个显著局部极值会减速,流畅函数上仍可用。 SDOm(100;0.5;4.0;10000.0)All score 3.678,5 维 Forest 冲到 0.804,但行为像摆球脱绳自由飞行,难以预测触发条件,当前形态不建议单独上任务,可混编少量代理去抓周期。 BFO-GA(50;0.01;0.8;50;10.0)All score 3.866,初期多代理就贴近目标,5 维 Hilly 0.823、Forest 0.861,但离散面 500 维 Megacity 仅 0.111,迭代上限压制了它找全局最优。 SDSm(100;100;0.05)靠随机区域指定坐标,首轮即铺满全域,All score 4.423,25 维 Hilly 仍保 0.680,比多数算法更抗局部最优。 BGA(50:50:1.0:3:0.001:0.7:3)靠基因突变直跳空间任意点,5 维 Hilly/Forest 双满 1.0,All score 6.412 居首;500 维 Forest 掉到 0.260,说明高维仍会陷局部。外汇与贵金属参数寻优属高风险实验,上述分数仅反映离线基准,实盘前请在 MT5 策略测试器用自有品种复跑。
◍ 把任意实数坐标塞进二进制代理
做遗传优化时,如果想从用户指定的初始点起步,而不是让代理在搜索空间里乱撒,就得给 BGA 代理加一个能把实数坐标写进基因数组的入口。BGA 的解空间是二进制编码,所以核心动作是把 double 数值转成格雷码基因。 DoubleToGene 方法的处理逻辑分三档:输入值低于该维度下限,直接把对应基因位清零(格雷码全 0 对应实数 0.0),并提前 return;高于上限,则从预存的 geneMax 数组复制最大编码值回填,同样提前终止;落在范围内才做比例缩放,把整数和小数部分分别转格雷码再拼回基因数组。 实测中,若 doubleDigitsInChromo 设 10、维度上下限跨度为 [0, 100],一个 73.4 的实数会被缩放到 0~1023 区间再拆整数 73 与小数 0.4 分别编码,代理就能精确停在用户给的坐标附近。外汇与贵金属参数优化属高风险,回测吻合不代表实盘概率占优。 下面这段结构体代码就是改造后的代理,重点看 DoubleToGene 如何分流三种情况并写 chromosome。
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span> <span class="keyword">class="kw">struct</span> S_Agent { <span class="keyword">class="type">void</span> Init(<span class="keyword">const</span> <span class="keyword">class="type">int</span> coords, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &min [], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &max [], <span class="keyword">class="type">int</span> doubleDigitsInChromo) { <span class="functions">ArrayResize</span>(c, coords); f = -<span class="macro">DBL_MAX</span>; <span class="functions">ArrayResize</span>(genes, coords); <span class="functions">ArrayResize</span>(chromosome, <span class="number">class="num">0</span>, <span class="number">class="num">1000</span>); <span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < coords; i++) { genes [i].Init(min [i], max [i], doubleDigitsInChromo); <span class="functions">ArrayCopy</span>(chromosome, genes [i].gene, <span class="functions">ArraySize</span>(chromosome), <span class="number">class="num">0</span>, <span class="macro">WHOLE_ARRAY</span>); } } <span class="keyword">class="type">void</span> ExtractGenes() { <span class="keyword">class="type">uint</span> pos = <span class="number">class="num">0</span>; <span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i < <span class="functions">ArraySize</span> (genes); i++) { c [i] = genes [i].ToDouble(chromosome, pos); pos += genes [i].length; } } <span class="keyword">class="type">void</span> DoubleToGene(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> val, <span class="keyword">const</span> <span class="keyword">class="type">int</span> genePos) { <span class="keyword">class="type">class="kw">double</span> value = val; <span class="comment">class=class="str">"cmt">//--------------------------------------------------------------------------</span> <span class="keyword">if</span> (value < genes [genePos].rangeMin) { <span class="functions">ArrayInitialize</span>(genes [genePos].gene, <span class="number">class="num">0</span>); <span class="functions">ArrayCopy</span> (chromosome, genes [genePos].gene, genePos * genes [genePos].length, <span class="number">class="num">0</span>, <span class="macro">WHOLE_ARRAY</span>); <span class="keyword">class="kw">return</span>; } <span class="comment">class=class="str">"cmt">//--------------------------------------------------------------------------</span> <span class="keyword">else</span> { <span class="keyword">if</span> (value > genes [genePos].rangeMax) { <span class="functions">ArrayCopy</span> (chromosome, genes [genePos].geneMax, genePos * genes [genePos].length, <span class="number">class="num">0</span>, <span class="macro">WHOLE_ARRAY</span>); <span class="keyword">class="kw">return</span>; } } <span class="comment">class=class="str">"cmt">//--------------------------------------------------------------------------</span> value = Scale(value, genes [genePos].rangeMin, genes [genePos].rangeMax, <span class="number">class="num">0.0</span>, genes [genePos].maxCodedDistance); DecimalToGray((<span class="keyword">class="type">ulong</span>)value, genes [genePos].integPart); value = value - (<span class="keyword">class="type">int</span>)value; value *= genes [genePos].digitsPowered; DecimalToGray((<span class="keyword">class="type">ulong</span>)value, genes [genePos].fractPart); <span class="functions">ArrayInitialize</span>(genes [genePos].gene, <span class="number">class="num">0</span>); <span class="keyword">class="type">uint</span> integGrayDigits = genes [genePos].integGrayDigits; <span class="keyword">class="type">uint</span> fractGrayDigits = genes [genePos].fractGrayDigits; <span class="keyword">class="type">uint</span> digits = <span class="functions">ArraySize</span> (genes [genePos].integPart); <span class="keyword">if</span> (digits > <span class="number">class="num">0</span>) <span class="functions">ArrayCopy</span> (genes [genePos].gene, genes [genePos].integPart, integGrayDigits - digits, <span class="number">class="num">0</span>, <span class="macro">WHOLE_ARRAY</span>); digits = <span class="functions">ArraySize</span> (genes [genePos].fractPart);
染色体缩放与基因注入的实现细节
遗传算法里把实数坐标编码进二进制染色体后,绕不开两个动作:把外部输入区间映射到输出区间,以及把单个基因塞回整条染色体。下面这段 MQL5 代码把这两件事拆得很干净。 Scale 函数做线性映射,当输出上下界相等时直接返回该值;输入输出区间重合时取中点,避免除零。若输入越界则钳到 OutMIN 或 OutMAX,否则按 (In-InMIN)*(OutMAX-OutMIN)/(InMAX-InMin)+OutMIN 算映射值——这是把价格归一化到 [0,1] 或反向还原的常用写法。 InjectGeneToChromosome 目前是空壳,但前面的 ArrayCopy 调用已经暴露了拼接逻辑:若小数位 digits>0,先把小数部分拷进 gene 的尾部偏移,再把整条 gene 按 genePos*length 的偏移写进 chromosome。意味着每条基因等长、顺序铺排,染色体总长度 = 基因数 × 单基因长度。 在 MT5 里把 genes[].length 设成 16、digits 设成 4,能直观看到 1.2345 被拆成整数和小数两段分别落位;外汇与贵金属杠杆高,这类编码仅用于策略搜索,实盘前务必做样本外验证。
if (digits > class="num">0) ArrayCopy(genes [genePos].gene, genes [genePos].fractPart, genes [genePos].length - digits, class="num">0, WHOLE_ARRAY); ArrayCopy(chromosome, genes [genePos].gene, genePos * genes [genePos].length, class="num">0, WHOLE_ARRAY); } class="type">void InjectGeneToChromosome() { } class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">class="kw">double Scale(class="type">class="kw">double In, class="type">class="kw">double InMIN, class="type">class="kw">double InMAX, class="type">class="kw">double OutMIN, class="type">class="kw">double OutMAX) { if (OutMIN == OutMAX) class="kw">return (OutMIN); if (InMIN == InMAX) class="kw">return (class="type">class="kw">double((OutMIN + OutMAX) / class="num">2.0)); else { if (In < InMIN) class="kw">return OutMIN; if (In > InMAX) class="kw">return OutMAX; class="kw">return (((In - InMIN) * (OutMAX - OutMIN) / (InMAX - InMIN)) + OutMIN); } } class="type">class="kw">double c []; class=class="str">"cmt">//coordinates class="type">class="kw">double f; class=class="str">"cmt">//fitness S_BinaryGene genes []; class=class="str">"cmt">//there are as many genes as there are coordinates class="type">char chromosome []; };
「一点提醒」
把 Megacity 上五组算法((PO)ES、SDOm、BFO_GA、SDSm、BGA)的退出轨迹叠在一起看,能直观确认:初始代理坐标一旦被刻意布置,部分算法的全局极值命中率会从随机均匀初始化的基准线明显抬升,BFO-GA 与 SDOm 甚至反超了均匀代理。 图1 用颜色梯度标出相关性评分,≥0.99 的格子直接留白;图2 的柱状刻度封顶在 100(理论极限),归档脚本里能复算这张评级表。外汇与贵金属市场属高风险场域,这类群体优化结论只在实验框架内成立,实盘迁移可能失效。 实验里 IWO、HS、SDSm、BGA 不论起点如何都维持了稳定输出,而 DE、EM、GSA、ACO 在最小点附近极易早夭。把早期拓荒型与后期修正型算法做杂交,是压低局部极值抵抗短板的可行路径,但落地前请在 MT5 策略测试器里先跑一遍自己的品种。