头脑风暴优化算法(第二部分): 多模态·进阶篇
🧠

头脑风暴优化算法(第二部分): 多模态·进阶篇

(2/3)·从第一部分的理论铺垫走到真实代码与多模态测试,看 BSO 如何绕过搜索空间障碍

案例拆解新手友好 第 2/3 篇

接上篇,我们继续深挖头脑风暴优化算法。第一部分里聚类与变异的逻辑还停在纸面,很多人卡在不知道智能体结构该怎么写、种群类如何继承。本篇把代码和测试结果摊开,省去你反复试错的时间。

◍ 正文

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">const</span> <span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp; epochsP = <span class="number">0</span>);&nbsp;&nbsp;<span class="comment">//number of epochs</span> &nbsp;&nbsp;<span class="keyword">void</span> Moving&nbsp;&nbsp;&nbsp;&nbsp;(); &nbsp;&nbsp;<span class="keyword">void</span> Revision&nbsp;&nbsp;(); &nbsp;&nbsp;<span class="keyword">void</span> Injection (<span class="keyword">const</span> <span class="keyword">int</span> popPos, <span class="keyword">const</span> <span class="keyword">int</span> coordPos, <span class="keyword">const</span> <span class="keyword">double</span> <span class="keyword">value</span>); &nbsp;&nbsp;<span class="comment">//----------------------------------------------------------------------------</span> &nbsp;&nbsp;<span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp;parentPopSize; <span class="comment">//parent population size;</span> &nbsp;&nbsp;<span class="keyword">int</span>&nbsp;&nbsp;&nbsp;&nbsp;clustersNumb;&nbsp;&nbsp;<span class="comment">//number of clusters</span> &nbsp;&nbsp;<span class="keyword">double</span> p_Replace;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">//replace probability</span> &nbsp;&nbsp;<span class="keyword">double</span> p_One;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="comment">//probability of choosing one</span> &nbsp;&nbsp;<span class="keyword">double</span> p_One_center;&nbsp;&

聚类中心偏移与新想法的生成逻辑

这段逻辑跑在种群迭代里,核心是用概率门控决定要不要动聚类结构。当随机数小于 p_Replace 时,算法挑一个非空聚类,把它质心沿各维度用高斯扰动重采样,扰动半径取到该维区间跨度的 0.8 倍。 代码里先建 clustList 把 count>0 的聚类收拢,clListSize 就是非空聚类数;RNDminusOne 返回 [0, clListSize) 的索引。质心重算时 min/max 做了边界夹紧,避免越出 rangeMin/rangeMax,再用 GaussDistribution(...,3) 做 3 倍标准差内的截断正态采样。 另一路 p_One 分支则是从单个聚类抽想法,cIndx_1 同样来自非空表。外汇与贵金属市场高波动,这类基于聚类的搜索若直接接实盘信号,参数失配可能导致频繁重聚类,回测与实盘偏差倾向放大。 把下面这段贴进 MT5 的 EA 源码对照,能看清质心偏移的边界处理:dist 写死 0.8 倍跨度,若你的特征区间差异大,可改成按维自适应。

MQL5 / C++
for (class="type">int c = class="num">0; c < coords; c++)
{
  a [i].c [c] = u.RNDfromCI(rangeMin [c], rangeMax [c]);
  a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
  agent [i].c [c] = a [i].c [c];
}
}
class="kw">return;
}
class=class="str">"cmt">//----------------------------------------------------------------------------
class=class="str">"cmt">//----------------------------------------------------------------------------
class="type">int    cIndx_1    = class="num">0;  class=class="str">"cmt">//index in the list of non-empty clusters
class="type">int    iIndx_1    = class="num">0;  class=class="str">"cmt">//index in the list of ideas in the cluster
class="type">int    cIndx_2    = class="num">0;  class=class="str">"cmt">//index in the list of non-empty clusters
class="type">int    iIndx_2    = class="num">0;  class=class="str">"cmt">//index in the list of ideas in the cluster
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">class="kw">double val        = class="num">0.0;
class="type">class="kw">double X1         = class="num">0.0;
class="type">class="kw">double X2         = class="num">0.0;
class="type">int    clListSize = class="num">0;
class="type">int    clustList [];
ArrayResize(clustList, class="num">0, clustersNumb);
class=class="str">"cmt">//----------------------------------------------------------------------------
class=class="str">"cmt">//let&class="macro">#x27;s make a list of non-empty clusters
for (class="type">int cl = class="num">0; cl < clustersNumb; cl++)
{
  if (clusters [cl].count > class="num">0)
  {
    clListSize++;
    ArrayResize(clustList, clListSize);
    clustList [clListSize - class="num">1] = cl;
  }
}
for (class="type">int i = class="num">0; i < popSize; i++)
{
  class=class="str">"cmt">//==========================================================================
  class=class="str">"cmt">//generating a new idea that replaces the selected cluster center(cluster center offset)
  if (u.RNDprobab() < p_Replace)
  {
    cIndx_1 = u.RNDminusOne(clListSize);
    for (class="type">int c = class="num">0; c < coords; c++)
    {
      val = clusters [clustList [cIndx_1]].centroid [c];
      dist = (rangeMax [c] - rangeMin [c]) * class="num">0.8;
      min = val - dist; if (min < rangeMin [c]) min = rangeMin [c];
      max = val + dist; if (max > rangeMax [c]) max = rangeMax [c];
      val = u.GaussDistribution(val, min, max, class="num">3);
      val = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]);
      clusters [clustList [cIndx_1]].centroid [c] = val;
    }
  }
  class=class="str">"cmt">//==========================================================================
  class=class="str">"cmt">//an idea from one cluster is selected
  if (u.RNDprobab() < p_One)
  {
    cIndx_1 = u.RNDminusOne(clListSize);
    class=class="str">"cmt">//------------------------------------------------------------------------

「聚类交叉育种的两种抽取路径」

这段逻辑处理遗传算法里「从聚类空间取父代」的两种模式:单聚类内取样,以及双聚类之间交叉取样。外汇与贵金属市场高波动,这类种群初始化方式只影响搜索多样性,不预示任何收益。 单聚类分支里,先以 p_One_center 概率直接搬用该聚类质心坐标;否则从聚类已登记 idea 列表中随机抽一条作为父代向量。双聚类分支则先决出两个不同聚类索引 cIndx_1 与 cIndx_2,当聚类列表长度为 1 时两者同指 0,长度为 2 时固定取 0 和 1,更长则随机不重复抽取。 双聚类取样的核心在 p_Two_center:命中则对两质心做区间随机(RNDfromCI),未命中则各自从两聚类随机抽 idea 再做区间随机。你可以把 p_One_center、p_Two_center 两个阈值从 0.5 起调,观察 MT5 策略测试里种群分散度的变化。

MQL5 / C++
if (u.RNDprobab() < p_One_center) class=class="str">"cmt">//select cluster center
      {
        for (class="type">int c = class="num">0; c < coords; c++)
        {
          a [i].c [c] = clusters [clustList [cIndx_1]].centroid [c];
        }
      }
      class=class="str">"cmt">//------------------------------------------------------------------------
      else                                        class=class="str">"cmt">//random idea from the cluster
      {
        iIndx_1 = u.RNDminusOne(clusters [clustList [cIndx_1]].count);
        for (class="type">int c = class="num">0; c < coords; c++)
        {
          a [i].c [c] = parents [clusters [clustList [cIndx_1]].ideasList [iIndx_1]].c [c];
        }
      }
    }
    class=class="str">"cmt">//==========================================================================
    class=class="str">"cmt">//select ideas from two clusters
    else
    {
      if (clListSize == class="num">1)
      {
        cIndx_1 = class="num">0;
        cIndx_2 = class="num">0;
      }
      else
      {
        if (clListSize == class="num">2)
        {
          cIndx_1 = class="num">0;
          cIndx_2 = class="num">1;
        }
        else
        {
          cIndx_1 = u.RNDminusOne(clListSize);
          do
          {
            cIndx_2 = u.RNDminusOne(clListSize);
          }
          while (cIndx_1 == cIndx_2);
        }
      }
      class=class="str">"cmt">//------------------------------------------------------------------------
      if (u.RNDprobab() < p_Two_center) class=class="str">"cmt">//two cluster centers selected
      {
        for (class="type">int c = class="num">0; c < coords; c++)
        {
          X1 = clusters [clustList [cIndx_1]].centroid [c];
          X2 = clusters [clustList [cIndx_2]].centroid [c];
          a [i].c [c] = u.RNDfromCI(X1, X2);
        }
      }
      class=class="str">"cmt">//------------------------------------------------------------------------
      else class=class="str">"cmt">//two ideas from two selected clusters
      {
        iIndx_1 = u.RNDminusOne(clusters [clustList [cIndx_1]].count);
        iIndx_2 = u.RNDminusOne(clusters [clustList [cIndx_2]].count);
        for (class="type">int c = class="num">0; c < coords; c++)
        {
          X1 = parents [clusters [clustList [cIndx_1]].ideasList [iIndx_1]].c [c];
          X2 = parents [clusters [clustList [cIndx_2]].ideasList [iIndx_2]].c [c];
          a [i].c [c] = u.RNDfromCI(X1, X2);
        }
      }
    }

◍ 变异与种群修订的实现细节

变异阶段用 epochsNow 映射到 1~200 的进度 x,再以逻辑斯蒂函数 ξ = 1/(1+exp(-(100-x)/k_Mutation)) 控制扰动幅度,dist 取参数区间跨度的 distribCoeff 倍乘 ξ。若 x 接近 100,ξ 趋近 0.5,扰动可能达到区间跨度的一半,低进度时扰动显著收窄。 逐行看变异循环:先按当前进度算 x 与 ξ,dist 受 rangeMax/rangeMin 约束;min/max 夹紧在参数边界内,最后用 GaussDistribution 以 8 为形状参数重采样该维参数。保存代理时调用 SeInDiSp 把连续值吸附到离散步长,再写回 agent 与 a 数组。 Revision 函数负责把子代适应度回灌父代池:先拷贝 agent.f 到父代,再把新想法并入 parents,总量 parentPopSize+popSize。排序后若首个体优于历史最优 fB,则更新 fB 与最优坐标 cB。 聚类仅在首次 revision 初始化 KMeans,之后每轮重跑。遍历 clustersNumb 个簇,取簇内父代中适应度最高者作为质心——若某簇 count 为 0 则保持 -DBL_MAX 不参与。外汇与贵金属参数优化属高风险,回测优解在实盘可能失效。

MQL5 / C++
  class=class="str">"cmt">//Mutation
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    class="type">int x = (class="type">int)u.Scale(epochsNow, class="num">1, epochs, class="num">1, class="num">200);
    class="type">class="kw">double ξ = (class="num">1.0 / (class="num">1.0 + exp(-((class="num">100 - x) / k_Mutation))));class=class="str">"cmt">// * u.RNDprobab();
    class="type">class="kw">double dist = (rangeMax [c] - rangeMin [c]) * distribCoeff * ξ;
    class="type">class="kw">double min = a [i].c [c] - dist; if (min < rangeMin [c]) min = rangeMin [c];
    class="type">class="kw">double max = a [i].c [c] + dist; if (max > rangeMax [c]) max = rangeMax [c];
    val = a [i].c [c];
    a [i].c [c] = u.GaussDistribution(val, min, max, class="num">8);
  }
  class=class="str">"cmt">//Save the agent-----------------------------------------------------------
  for (class="type">int c = class="num">0; c < coords; c++)
  {
    val = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
    a    [i].c [c] = val;
    agent [i].c [c] = val;
  }
}
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="type">void C_AO_BSO::Revision()
{
  class=class="str">"cmt">//get fitness--------------------------------------------------
  for (class="type">int i = class="num">0; i < popSize; i++)
  {
    agent [i].f = a [i].f;
  }
  class=class="str">"cmt">//pass new ideas to the population--------------------------------------------
  for (class="type">int i = parentPopSize; i < parentPopSize + popSize; i++)
  {
    parents [i] = agent [i - parentPopSize];
  }
  class=class="str">"cmt">//sort out the parent population----------------------------------------
  u.Sorting(parents, parentsTemp, parentPopSize + popSize);
  if (parents [class="num">0].f > fB)
  {
    fB = parents [class="num">0].f;
    ArrayCopy(cB, parents [class="num">0].c, class="num">0, class="num">0, WHOLE_ARRAY);
  }
  class=class="str">"cmt">//perform clustering-----------------------------------------------------
  if (!revision)
  {
    km.KMeansInit(parents, parentPopSize, clusters);
    revision = true;
  }
  km.KMeansInit(parents, parentPopSize, clusters);
  km.KMeans(parents, parentPopSize, clusters);
  class=class="str">"cmt">//Assign the best cluster solution as the cluster center--------------------------
  for (class="type">int cl = class="num">0; cl < clustersNumb; cl++)
  {
    clusters [cl].f = -DBL_MAX;
    if (clusters [cl].count > class="num">0)
    {
      for (class="type">int p = class="num">0; p < parentPopSize; p++)
      {
        if (parents [p].label == cl)
        {
          if (parents [p].f > clusters [cl].f)
          {
            clusters [cl].f = parents [p].f;
            ArrayCopy(clusters [cl].centroid, parents [p].c, class="num">0, class="num">0, WHOLE_ARRAY);
          }
        }
      }
    }
  }
}
把重复劳动交给小布
这些诊断与参数扫描的重复劳动小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只需专注决策本身。

常见问题

label 标记集群归属,-1 表示尚未分入任何聚类,聚类阶段再据此分配,避免初始误判。
K-Means++ 用距离感知选初始质心,降低空簇概率,让 BSO 在多峰空间更易覆盖不同最优区。
popSize 是总种群规模,parentPopSize 控制进入下一轮父代的数量,影响探索与利用的平衡。
小布内置的 AIGC 诊断可辅助你看盘口与参数分布,但算法回测仍需在 MT5 环境自行部署代码。
过高会削弱精英保留,种群易丢失已找到的优解,多模态下可能错过次要峰值。