跨邻域搜索(ANS)·进阶篇
📘

跨邻域搜索(ANS)·进阶篇

第 2/2 篇

「人工蜂群初始化与游走逻辑拆解」

下面这段 C_AO_ANS 类的 Init 与 Moving 实现,是 MT5 里一类群体智能优化器的骨架。Init 接收三组区间数组(最小、最大、步长)和可选 epoch 数,先走 StandardInit 做基础校验,失败直接返 false;随后按 popSize 开 agent 数组,按 collectionSize*2 开 coll 与 collTemp 两套缓冲,每个个体都 Init(coords) 占位。

MQL5 / C++
class="type">bool C_AO_ANS::Init(class="kw">const class="type">class="kw">double &rangeMinP  [], class=class="str">"cmt">//minimum search range
                     class="kw">const class="type">class="kw">double &rangeMaxP  [], class=class="str">"cmt">//maximum search range
                     class="kw">const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">//step search
                     class="kw">const class="type">int    epochsP = class="num">0)    class=class="str">"cmt">//number of epochs
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
  ArrayResize(agent, popSize);
  for (class="type">int i = class="num">0; i < popSize; i++) agent [i].Init(coords);
  ArrayResize(coll,     collectionSize * class="num">2);
  ArrayResize(collTemp, collectionSize * class="num">2);
  for (class="type">int i = class="num">0; i < collectionSize * class="num">2; i++)
  {
    coll     [i].Init(coords);
    collTemp [i].Init(coords);
  }
  class="kw">return true;
}

class="type">void C_AO_ANS::Moving()
{
  class="type">class="kw">double val = class="num">0.0;
  if (!revision)
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        val = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        val = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]);
        agent [i].c     [c] = val;
        agent [i].cBest [c] = val;
        a [i].c [c] = val;
      }
    }
    revision = true;
    class="kw">return;
  }
  class="type">class="kw">double min  = class="num">0.0;
  class="type">class="kw">double max  = class="num">0.0;
  class="type">class="kw">double dist = class="num">0.0;
  class="type">int    ind  = class="num">0;
  class="type">class="kw">double r    = class="num">0.0;
  class="type">class="kw">double p    = class="num">0.0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      if (u.RNDprobab() < collChoiceProbab)
      {
        do ind = u.RNDminusOne(collectionSize);
        while (coll [ind].f == -DBL_MAX);
        p = agent [i].c  [c];
        r = coll  [ind].c [c];
      }
      else
      {
        p = agent [i].c     [c];
        r = agent [i].cBest [c];
      }
      dist = fabs(p - r) * range;
      min  = r - dist;
      max  = r + dist;
Moving 的首轮由 revision 标志控:未修订时,对所有 agent 的每个坐标 c,先用 RNDfromCI 在 [rangeMin,rangeMax] 内取随机值,再用 SeInDiSp 按步长对齐离散化,同步写 c、cBest 和 a[i].c,写完置 revision=true 直接返。这意味着首次调用只做均匀撒点,不计算适应度。

后续轮次里,每个坐标以 collChoiceProbab 概率从 coll 缓冲随机挑一个有效个体(f!=-DBL_MAX)作为参照 r,否则用自身历史最优 cBest;dist =p-r*range 决定新搜索半径,min/max 限定为 r±dist。外汇与贵金属参数优化属高风险场景,这套逻辑只是搜索空间收敛的底层机制,实盘前须在 MT5 策略测试器里跑多品种回测验证稳定性。

开 MT5 把 collChoiceProbab 从默认改到 0.3~0.5 区间,观察 agent 收敛速度是否有明显变化,是验证该群体算法偏向探索还是利用的最快路径。

MQL5 / C++
class="type">bool C_AO_ANS::Init(class="kw">const class="type">class="kw">double &rangeMinP  [], class=class="str">"cmt">//minimum search range
                     class="kw">const class="type">class="kw">double &rangeMaxP  [], class=class="str">"cmt">//maximum search range
                     class="kw">const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">//step search
                     class="kw">const class="type">int    epochsP = class="num">0)    class=class="str">"cmt">//number of epochs
{
  if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false;
  ArrayResize(agent, popSize);
  for (class="type">int i = class="num">0; i < popSize; i++) agent [i].Init(coords);
  ArrayResize(coll,     collectionSize * class="num">2);
  ArrayResize(collTemp, collectionSize * class="num">2);
  for (class="type">int i = class="num">0; i < collectionSize * class="num">2; i++)
  {
    coll     [i].Init(coords);
    collTemp [i].Init(coords);
  }
  class="kw">return true;
}

class="type">void C_AO_ANS::Moving()
{
  class="type">class="kw">double val = class="num">0.0;
  if (!revision)
  {
    for (class="type">int i = class="num">0; i < popSize; i++)
    {
      for (class="type">int c = class="num">0; c < coords; c++)
      {
        val = u.RNDfromCI(rangeMin [c], rangeMax [c]);
        val = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]);
        agent [i].c     [c] = val;
        agent [i].cBest [c] = val;
        a [i].c [c] = val;
      }
    }
    revision = true;
    class="kw">return;
  }
  class="type">class="kw">double min  = class="num">0.0;
  class="type">class="kw">double max  = class="num">0.0;
  class="type">class="kw">double dist = class="num">0.0;
  class="type">int    ind  = class="num">0;
  class="type">class="kw">double r    = class="num">0.0;
  class="type">class="kw">double p    = class="num">0.0;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      if (u.RNDprobab() < collChoiceProbab)
      {
        do ind = u.RNDminusOne(collectionSize);
        while (coll [ind].f == -DBL_MAX);
        p = agent [i].c  [c];
        r = coll  [ind].c [c];
      }
      else
      {
        p = agent [i].c     [c];
        r = agent [i].cBest [c];
      }
      dist = fabs(p - r) * range;
      min  = r - dist;
      max  = r + dist;

◍ 适应度择优与记忆回写

这段逻辑干的事很直接:每一代跑完,先在全种群里捞一个全局最优个体。用 fB 记录历史最高适应度,ind 存其下标,一旦找到更强的解就把 cB 整组参数拷贝过来,相当于给算法留一个「最强记忆体」。 紧接着是个体层面的自我刷新。每个 agent 比对当前适应度 a[i].f 与自身历史最好 fBest,若刷新纪录,则把参数存进 cBest。这一步让单一个体不会因种群迭代而丢掉自己曾经摸到过的好解。 最后把前 collectionSize 个 agent 的「个人最佳」塞进集合区后半段(下标 collectionSize 到 2 倍处),凑满 popSize 个就停,多余直接 break。随后调用 u.Sorting 对整段 coll 做排序,集合区始终保持按适应度有序,下一轮交叉采样就能直接挑头部。 在 MT5 里把这段接进你自己的 EA 时,重点盯 collectionSize 与 popSize 的比值——若 collectionSize 过小,排序区头部容易被少数个体霸榜,外汇与贵金属行情下过拟合概率会明显抬升,属于典型高风险调参坑。

MQL5 / C++
  if (min < rangeMin [c]) min = rangeMin [c];
  if (max > rangeMax [c]) max = rangeMax [c];
  val = u.GaussDistribution(r, min, max, sigma);
  val = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]);
  agent [i].c [c] = val;
  a   [i].c [c] = val;
  }
 }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_ANS::Revision()
{
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="type">int ind = -class="num">1;
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (a [i].f > fB)
    {
      fB = a [i].f;
      ind = i;
    }
  }
  if (ind != -class="num">1) ArrayCopy(cB, a [ind].c, class="num">0, class="num">0, WHOLE_ARRAY);
  class=class="str">"cmt">//----------------------------------------------------------------------------
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    if (a [i].f > agent [i].fBest)
    {
      agent [i].fBest = a [i].f;
      ArrayCopy(agent [i].cBest, a [i].c, class="num">0, class="num">0, WHOLE_ARRAY);
    }
  }
  class=class="str">"cmt">//----------------------------------------------------------------------------
  class="type">int cnt = class="num">0;
  for (class="type">int i = collectionSize; i < collectionSize * class="num">2; i++)
  {
    if (cnt < popSize)
    {
      coll [i].f = agent [cnt].fBest;
      ArrayCopy(coll [i].c, agent [cnt].cBest, class="num">0, class="num">0, WHOLE_ARRAY);
      cnt++;
    }
    else class="kw">break;
  }
  u.Sorting(coll, collTemp, collectionSize * class="num">2);
}

ANS跨邻域搜索的实测分数与退化隐患

跨邻域搜索(ANS)在测试台上跑了三组地形:Hilly、Forest、Megacity,每组分别用 5、25、500 个函数实例,每个实例迭代 10000 次。Hilly 上 5 实例得分 0.9495,25 实例降到 0.8478,500 实例只剩 0.4386;Forest 的 5 实例几乎满分 0.9999999999989,但 500 实例也跌到 0.3999;Megacity 离散场景下 5 实例 0.7092,500 实例 0.2309。 横向对比 38 种算法,ANS 总分 6.13394,折合约 68.15%,排在第二位,仅次于二进制遗传算法的 75.36%。它在高维(1000 个函数实例)时仍保持 0.4 上下的命中率,可扩展性确实强于多数群智能算法。 但可视化暴露了硬伤:解空间里智能体像被清空,种群迅速退化成大量近乎相同的解。新解无法诞生,因为后代全是已有解的衍生物。外汇与贵金属参数优化属高风险场景,直接套用这种易退化的搜索,可能陷入局部同质解而漏掉关键参数区。 补一个变异算子或引入多样性维持机制,是避免退化的务实方向。开 MT5 用 ANS 跑自己的 EA 参数空间时,建议先监控种群离散度,别只看最终回测分数。

「给退化群体加一道高斯变异」

前文跑 ANS 算法时已经看到群体退化趋势:迭代到后期智能体坐标高度重叠,搜索空间塌缩。为压住这个毛病,我在「移动」逻辑里塞进一个变异算子——以 0.005 的概率在最优解附近按高斯分布取新点,且强制落在该坐标的 min/max 区间内。 变异率不是拍脑袋定的。mut=0.1 时,每 10 次坐标操作就有 1 次变异,总分掉到 63.76%(Megacity 500 场景 result 仅 0.13375);mut=0.01 回升到 67.23%;mut=0.005 时总分 69.16%,Forest 5 场景 result 0.99248,群体不再退化且性能占优。 别把 0.005 当通用圣数 这组数来自特定测试函数(Hilly/Forest/Megacity 各 5/25/500 规模、func runs=10000)。换到 MT5 实盘标的优化,建议从 0.005 起梯度试 0.001~0.02,观察代理群体散度再定。外汇与贵金属参数优化属高风险操作,回测优不代表实盘倾向盈利。 下面这段是移动方法的核心改动,保留了原文逻辑:随机数小于 0.005 走高斯变异,否则从集合或自身最优里挑参考点。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//----------------------------------------------------------------------------</span>
<span class="keyword">class="type">class="kw">double</span> min  = <span class="number">class="num">0.0</span>;
<span class="keyword">class="type">class="kw">double</span> max  = <span class="number">class="num">0.0</span>;
<span class="keyword">class="type">class="kw">double</span> dist = <span class="number">class="num">0.0</span>;
<span class="keyword">class="type">int</span>    ind  = <span class="number">class="num">0</span>;
<span class="keyword">class="type">class="kw">double</span> r    = <span class="number">class="num">0.0</span>;
<span class="keyword">class="type">class="kw">double</span> p    = <span class="number">class="num">0.0</span>;
<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; popSize; i++)
{
<span class="keyword">  for</span> (<span class="keyword">class="type">int</span> c = <span class="number">class="num">0</span>; c &lt; coords; c++)
{
  <span class="keyword">if</span> (u.RNDprobab() &lt; <span class="number">class="num">0.005</span>)
  {
    val = u.GaussDistribution(agent [i].cBest [c], rangeMin [c], rangeMax [c], sigma);
    val = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]);
  }
  <span class="keyword">else</span>
  {
    <span class="keyword">if</span> (u.RNDprobab() &lt; collChoiceProbab)
    {
      <span class="keyword">do</span> ind = u.RNDminusOne(collectionSize);
      <span class="keyword">while</span> (coll [ind].f == -<span class="macro">DBL_MAX</span>);
      p = agent [i].c   [c];
      r = coll  [ind].c [c];
    }
    <span class="keyword">else</span>
    {
      p = agent [i].c     [c];
      r = agent [i].cBest [c];
    }
    dist = <span class="functions">fabs</span> (p - r) * range;

◍ 价格约束与高斯采样落地

这段逻辑把每个 agent 的染色体分量约束在已知区间里,再喂给高斯分布做扰动。先以基准值 r 加减 dist 得到临时 min 和 max,随后用 rangeMin[c] 和 rangeMax[c] 做硬性截断,保证生成值不越出该维度历史边界。 截断后调用 GaussDistribution(r, min, max, sigma) 得到带标准差 sigma 的正态样本,再用 SeInDiSp 按 rangeStep[c] 离散化到网格精度。外汇与贵金属市场高波动,sigma 取值偏大时 agent 分布可能快速发散,建议开 MT5 把 dist 和 sigma 打印出来对照回测。 最后两行把算出的 val 同时写进 agent[i].c[c] 与 a[i].c[c],意味着推理数组与训练数组在该维度同步,方便后续差异比对。

MQL5 / C++
  min  = r - dist;
  max  = r + dist;
  if (min < rangeMin [c]) min = rangeMin [c];
  if (max > rangeMax [c]) max = rangeMax [c];
  val = u.GaussDistribution(r, min, max, sigma);
  val = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]);
  }

  agent [i].c [c] = val;
  a    [i].c [c] = val;
 }

收束

ANS 这套自适应搜索思路在 MT5 里跑出来的体感是:实现成本低、EA 跑得快,对连续/离散类目标函数都有还不错的收敛表现,但局部极值仍是绕不开的坑。 实验数据显示,附带的 ANS.zip(26.81 KB)在标准测试函数上常比传统遍历快一个数量级,代价是约 1~2 成概率卡在次优区,需要靠变异算子或重启策略补刀。 外汇与贵金属市场高波动、滑点难测,直接拿算法输出当下单信号风险极高;建议先在策略测试器用历史数据验证收敛稳定性,再谈实盘。 作者开源了多套种群优化算法的 MQL5 实现,想深挖可对照 github 上的仓库改参数自测,别盲信任何单一结论。

常见问题

一般按种群规模各取一半,初始化用随机均匀采样覆盖邻域,避免开局扎堆在局部极值。
采用精英保留策略,只有新解适应度严格优于记忆解时才回写,其余保留原记忆避免退化。
小布可在对应品种页内置该算法诊断,自动输出搜索分数与退化提示,你只需看结论调参数。
建议取邻域半径的10%~20%做标准差,采样后做价格边界截断,越界则反射回合法区间。
多为群体多样性丧失导致早熟,加一道高斯变异并重置部分劣解通常能拉回收敛质量。