群体优化算法:抵抗陷入局部极值(第二部分)·进阶篇
📘

群体优化算法:抵抗陷入局部极值(第二部分)·进阶篇

第 2/2 篇

「五类优化器在测试地形上的得分对照」

把 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。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">class="kw">struct</span> S_Agent
{
&nbsp;&nbsp;<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> &amp;min [], <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;max [], <span class="keyword">class="type">int</span> doubleDigitsInChromo)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span>(c, coords);
&nbsp;&nbsp;&nbsp;&nbsp;f = -<span class="macro">DBL_MAX</span>;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span>(genes, coords);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayResize</span>(chromosome, <span class="number">class="num">0</span>, <span class="number">class="num">1000</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; coords; i++)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;genes [i].Init(min [i], max [i], doubleDigitsInChromo);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>);
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}
&nbsp;&nbsp;<span class="keyword">class="type">void</span> ExtractGenes()
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span> pos = <span class="number">class="num">0</span>;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; <span class="functions">ArraySize</span> (genes); i++)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;c [i] = genes [i].ToDouble(chromosome, pos);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;pos&nbsp;&nbsp;+= genes [i].length;
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;}
&nbsp;&nbsp;<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)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> value = val;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--------------------------------------------------------------------------</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (value &lt; genes [genePos].rangeMin)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayInitialize</span>(genes [genePos].gene, <span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--------------------------------------------------------------------------</span>
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (value &gt; genes [genePos].rangeMax)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<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>);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">return</span>;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;<span class="comment">class=class="str">"cmt">//--------------------------------------------------------------------------</span>
&nbsp;&nbsp;&nbsp;&nbsp;value = Scale(value, genes [genePos].rangeMin, genes [genePos].rangeMax, <span class="number">class="num">0.0</span>, genes [genePos].maxCodedDistance);
&nbsp;&nbsp;&nbsp;&nbsp;DecimalToGray((<span class="keyword">class="type">ulong</span>)value, genes [genePos].integPart);
&nbsp;&nbsp;&nbsp;&nbsp;value = value - (<span class="keyword">class="type">int</span>)value;
&nbsp;&nbsp;&nbsp;&nbsp;value *= genes [genePos].digitsPowered;
&nbsp;&nbsp;&nbsp;&nbsp;DecimalToGray((<span class="keyword">class="type">ulong</span>)value, genes [genePos].fractPart);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="functions">ArrayInitialize</span>(genes [genePos].gene, <span class="number">class="num">0</span>);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span>&nbsp;&nbsp; integGrayDigits = genes [genePos].integGrayDigits;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span>&nbsp;&nbsp; fractGrayDigits = genes [genePos].fractGrayDigits;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="type">uint</span>&nbsp;&nbsp; digits = <span class="functions">ArraySize</span> (genes [genePos].integPart);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (digits &gt; <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>);
&nbsp;&nbsp;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 被拆成整数和小数两段分别落位;外汇与贵金属杠杆高,这类编码仅用于策略搜索,实盘前务必做样本外验证。

MQL5 / C++
  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 策略测试器里先跑一遍自己的品种。

常见问题

看五类优化器在固定测试地形上的得分对照,若某优化器多次停在非全局最低点且分数明显偏高,就是陷入局部极值,可换算子或调种群规模验证。
用染色体缩放把实数区间映射到二进制位长,再做基因注入把当前参数写回代理,具体按文中缩放公式实现即可直接套用。
小布可接入你的品种页,自动调度五类优化器在测试地形出分并标红局部极值陷阱,你只需看结果调参。
多是位长与实数精度不匹配或注入前未归一化,检查缩放比例和边界钳制就能避开大部分报错。
因外汇贵金属高风险,优化器脱离样本地形易失效,仅作概率倾向参考,实盘须小额验证再放大。