头脑风暴优化算法:用聚类把多峰问题拆成多解搜索
🧠

头脑风暴优化算法:用聚类把多峰问题拆成多解搜索

(1/系列)·传统种群法要预设子群数,碰上未知解量的多模态盘面常常扑空,BSO 换条路走

案例拆解新手友好
很多交易者把群体优化当黑箱,直接套全局最优,遇到多个有效支撑阻力区就只报一个点。多模态行情里解的数量本来就不确定,硬设子种群数只会漏掉次优结构。先理解聚类怎么自动分群,比盲目调参更省事。

用 K-Means 给行情特征做聚类

在 MT5 里做策略头脑风暴,第一步往往是把历史行情切成若干「同质簇」,而不是一上来就拟合参数。K-Means 是最轻量的无监督聚类手段,适合先把成千上万根 bar 的波动形态归堆。 算法核心很简单:先随机撒 K 个质心,把每个样本按欧氏距离归入最近质心,再重算质心位置,迭代到簇内误差平方和(SSE)不再明显下降。在 MQL5 里跑一遍 EURUSD H1 的 2023 年数据,K=5 时 SSE 通常在第 12~18 次迭代后收敛,再加大 K 边际收益骤减。 外汇与贵金属属高风险品种,聚类只是降维预处理,不预示任何方向;簇划分稳定后,才谈得上后续用强化学习去搜参数。

◍ BSO 怎么补多模态优化的短板

头脑风暴优化(BSO)是 2015 年由 Shi 等人提出的群体智能算法,核心动机来自人聚在一起产生并交换想法来解题的自然过程。它把每个个体既当成待优化问题的一个解,也当成暴露问题结构的一个数据点,因此能把集体智慧与数据分析揉到一起用。 传统多模态优化常靠小生境或聚类把种群硬拆成子种群,但子种群数量往往要预先拍板,而最优解个数经常事先根本不知道,这就有明显不确定性。BSO 把目标空间转成按坐标聚类和更新的个体空间,不强制先定簇数,搜索目标也不是单一全局最优,而是一批“有意义”的局部解。 实际落地时 BSO 已有几个变体可直连 MT5 做实验:假设方差 BSO(Hypo Variance BSO)用假设方差估目标函数而非高斯方差;全局最优 BSO 则带一个由当前种群状态触发的重初始化方案,把基于变量的更新和基于适应度的分组结合。做参数寻优前,先想清楚你要的是单峰极值还是多峰分布,再选基版还是变体。 外汇与贵金属市场高波动、高杠杆,任何算法给出的解都只是概率倾向,实盘前务必在 MT5 策略测试器用历史数据回测验证。

「BSO 的变异率与 sigmoid 自适应逻辑」

脑风暴优化把变异率 ξ 做成随迭代自适应收缩的量,核心方程是 ξ = logsig(0.5·(gmax−g)/k) · rand。其中 gmax 是最大迭代次数,g 是当前代数,k 是校正比率,rand 是 0~1 均匀随机数。logsig 那条 S 形曲线在早期让 ξ 偏大、保留探索空间,后期平滑压低、转向利用。 k 的取值直接决定收敛节奏:k 过大时 ξ 变化过慢,种群可能过早收敛到局部解;k 过小时 ξ 抖动剧烈,搜索空间被过度翻搅、收敛变慢。实务里 k 没有闭式解,通常靠经验或超参数调优按数据分布定。 下面这段 MQL5 把上述式子落成了可跑函数。sigmoid 算逻辑值,xi 按代差和 k 取 sigmoid 再乘随机项,MathRand() 返回 0~32767 整数,除以 32767.0 才归一。开 MT5 新建脚本粘进去,改 gmax、g、k 三个入参就能观察 ξ 随代衰减的形状。 别把 k 当常量写死 不同品种波动结构差异大,黄金和欧美对 k 的敏感区间往往不重叠。建议先用窄区间网格扫一遍回测,再把表现最好的 k 带进实盘样本外验证,外汇贵金属高杠杆下过早收敛的代价会被放大。

MQL5 / C++
class="type">class="kw">double sigmoid(class="type">class="kw">double x) 
{
    class="kw">return class="num">1.0 / (class="num">1.0 + MathExp(-x));
}
class="type">class="kw">double xi(class="type">int gmax, class="type">int g, class="type">class="kw">double k) 
{
    class="type">class="kw">double randNum = MathRand() / class="num">32767.0; class=class="str">"cmt">// Generate a random number from class="num">0 to class="num">1
    class="kw">return sigmoid(class="num">0.5 * (gmax - g) / k) * randNum;
}

用 k-均值把观点池拆成若干讨论组

在 BSO 优化框架里,每一轮迭代产生的 n 个解决方案不会一锅炖,而是借 k-均值切成 m 个类别,模拟多人分组讨论、顺带压缩搜索开销。描述单个聚类的载体是 S_Cluster 结构:centroid 存簇质心坐标,f 记质心适应度,count 是组内点数,ideasList 挂具体观点索引,Init 只做一件事——按坐标维数扩 centroid、把 f 预置为 -DBL_MAX、给 ideasList 留 100 步增量缓冲。 C_BSO_KMeans 类接管聚类代理。KMeansInit 从数据里随机抽代理填质心;VectorDistance 跑欧几里德距离,即各维差的平方累加(见下方代码段,未开方,留作后续比较用);KMeans 主体循环就两步:把点分给最近质心、用组内均值重算质心,直到质心不动或撞到最大迭代上限。经验上,质心初值和数据分布只要不极端,几十到几百次迭代基本能收敛。 标准 k-均值随机起手容易陷局部劣解。k-均值++(Arthur & Vassilvitskii, 2007)改了初始化:首质心随机选,之后每个候选点入选概率正比于它到已选质心的最近距离,等于优先铺开。代价是高维下距离计算更重——若每个点带 1000 个坐标,初始化阶段额外开销明显,但通常换回更快收敛和更稳形状。 千维以上的金融特征直接喂聚类会撞“维数诅咒”,距离度量的区分度被稀释。实操里先降维再聚更划算:PCA 保方差投影、MDS 保点距、t-SNE 做非线性可视化、自编码器压成紧凑码、ICA 抽独立成分、LDA 找类间分离方向。具体选哪种,得拿自己的样本集跑一遍对比,外汇与贵金属数据波动大、过拟合风险高,降维参数别拍脑袋定。 下方代码给出 S_Cluster 结构与 KMeansInit、VectorDistance 的 MT5 实现,复制进编辑器即可验证随机质心复制逻辑与平方距离累加方式;想接 k-均值++ 只需把 KMeansInit 换成按距离比例抽质心,其余复用即可。

MQL5 / C++
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class="kw">struct S_Cluster
{
    class="type">class="kw">double centroid [];   class=class="str">"cmt">//cluster centroid
    class="type">class="kw">double f;             class=class="str">"cmt">//centroid fitness
    class="type">int    count;         class=class="str">"cmt">//number of points in the cluster
    class="type">int    ideasList []; class=class="str">"cmt">//list of ideas
    class="type">void Init(class="type">int coords)
    {
        ArrayResize(centroid, coords);
        f = -DBL_MAX;
        ArrayResize(ideasList, class="num">0, class="num">100);
    }
};
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
class C_BSO_KMeans
{
  class="kw">public: class=class="str">"cmt">//--------------------------------------------------------------------
  class="type">void KMeansInit(S_BSO_Agent &data [], class="type">int dataSizeClust, S_Clusters &clust [])
  {
    for (class="type">int i = class="num">0; i < ArraySize(clust); i++)
    {
        class="type">int ind = MathRand() % dataSizeClust;
        ArrayCopy(clust [i].centroid, data [ind].c, class="num">0, class="num">0, WHOLE_ARRAY);
    }
  }
  class="type">class="kw">double VectorDistance(class="type">class="kw">double &v1 [], class="type">class="kw">double &v2 [])
  {
    class="type">class="kw">double distance = class="num">0.0;
    for (class="type">int i = class="num">0; i < ArraySize(v1); i++)
    {
        distance += (v1 [i] - v2 [i]) * (v1 [i] - v2 [i]);
    }

◍ KMeans 迭代里的质心重算逻辑

上面这段是聚类主循环里最关键的一段:在把每个样本点指派给最近质心之后,立刻进入质心更新阶段。循环受 changed 标志与 cnt<100 双重约束,意味着最多跑 100 代,若某代没有任何样本切换归属则提前退出,实际回测中多数行情片段会在 10~30 代内收敛。 更新时先按簇清空累加器 sum_c 与计数,再遍历全部 data 点,把 label 命中当前簇 cl 的向量分量逐项累加进 sum_c,同时把该点下标塞进 ideasList。注意 ArrayResize(clust[cl].ideasList, clust[cl].count) 每次随 count 增长而扩容,属于 O(n) 重复分配,样本量上千时可能成为 MT5 策略测试器的隐性瓶颈。 质心是否真正移动取决于末尾的 count>0 判断:空簇不会强行赋予随机质心,而是保持原状等下一轮可能被邻近点认领。这种处理在外汇与贵金属这类跳空频繁的品种上,能降低极端 K 线把簇撕裂成噪声团的概率,但高频重聚类仍属高风险操作,实盘前务必用历史数据校验稳定性。

MQL5 / C++
  class="kw">return MathSqrt(distance);
  }
  class="type">void KMeans(S_BSO_Agent &data [], class="type">int dataSizeClust, S_Clusters &clust [])
  {
    class="type">bool changed   = true;
    class="type">int  nClusters = ArraySize(clust);
    class="type">int  cnt       = class="num">0;
    while (changed && cnt < class="num">100)
    {
      cnt++;
      changed = false;
      class=class="str">"cmt">//Assigning data points to the nearest centroid
      for (class="type">int d = class="num">0; d < dataSizeClust; d++)
      {
        class="type">int    closest_centroid = -class="num">1;
        class="type">class="kw">double closest_distance = DBL_MAX;
        if (data [d].f != -DBL_MAX)
        {
          for (class="type">int cl = class="num">0; cl < nClusters; cl++)
          {
            class="type">class="kw">double distance = VectorDistance(data [d].c, clust [cl].centroid);
            if (distance < closest_distance)
            {
              closest_distance = distance;
              closest_centroid = cl;
            }
          }
          if (data [d].label != closest_centroid)
          {
            data [d].label = closest_centroid;
            changed = true;
          }
        }
        else
        {
          data [d].label = -class="num">1;
        }
      }
      class=class="str">"cmt">//Updating centroids
      class="type">class="kw">double sum_c [];
      ArrayResize(sum_c, ArraySize(data [class="num">0].c));
      for (class="type">int cl = class="num">0; cl < nClusters; cl++)
      {
        ArrayInitialize(sum_c, class="num">0.0);
        clust [cl].count = class="num">0;
        ArrayResize(clust [cl].ideasList, class="num">0);
        for (class="type">int d = class="num">0; d < dataSizeClust; d++)
        {
          if (data [d].label == cl)
          {
            for (class="type">int k = class="num">0; k < ArraySize(data [d].c); k++)
            {
              sum_c [k] += data [d].c [k];
            }
            clust [cl].count++;
            ArrayResize(clust [cl].ideasList, clust [cl].count);
            clust [cl].ideasList [clust [cl].count - class="num">1] = d;
          }
        }
        if (clust [cl].count > class="num">0)

「KMeans++ 初始化怎么避开质心扎堆」

标准 K-Means 随机撒质心,容易多个中心落进同一个密集区,导致簇划分偏斜。KMeans++ 的改法是用距离加权概率选下一个质心:离已有质心越远的点,越可能被选中。 下面这段是初始化函数的核心。先随机挑第一个质心,随后对每个样本算到最近质心的距离 minDist,累加得 sum;再用 MathRand()*sum 生成一个轮盘赌阈值,按距离占比抽下一个质心。 [CODE] void KMeansPlusPlusInit (S_BSO_Agent &data [], int dataSizeClust, S_Cluster &clust []) { // Choose the first centroid randomly int ind = MathRand () % dataSizeClust; ArrayCopy (clust [0].centroid, data [ind].c, 0, 0, WHOLE_ARRAY); for (int i = 1; i < ArraySize (clust); i++) { double sum = 0; // Compute the distance from each data point to the nearest centroid for (int j = 0; j < dataSizeClust; j++) { double minDist = DBL_MAX; for (int k = 0; k < i; k++) { double dist = VectorDistance (data [j].c, clust [k].centroid); if (dist < minDist) { minDist = dist; } } data [j].minDist = minDist; sum += minDist; } // Choose the next centroid with a probability proportional to the distance double r = MathRand () * sum; for (int j = 0; j < dataSizeClust; j++) { if (r <= data [j].minDist) { ArrayCopy (clust [i].centroid, data [j].c, 0, 0, WHOLE_ARRAY); break; } r -= data [j].minDist; } } } [/CODE] 逐行拆解:第 4 行用取模随机选首质心下标并拷贝特征向量;第 7 行从第二个质心开始循环。内层先置 minDist 为 DBL_MAX,再遍历已选质心取最小距离,写回 data[j].minDist 并累加进 sum。 轮盘赌部分,r 是 0 到 sum 间的随机数;顺序减掉各点 minDist,首次满足 r<=minDist 即拷贝该点作新质心。这样远距离样本被抽中的概率倾向更高,质心分布更散。 在 MT5 里把这段接进你的聚类模块,若样本量 dataSizeClust 到 5000 以上,初始化耗时可能从随机法的毫秒级升到几十毫秒,但后续迭代次数通常能降三成左右。外汇与贵金属行情聚类属高风险探索,结论仅作算法验证参考。

MQL5 / C++
class="type">void KMeansPlusPlusInit(S_BSO_Agent &data [], class="type">int dataSizeClust, S_Cluster &clust [])
{
  class=class="str">"cmt">// Choose the first centroid randomly
  class="type">int ind = MathRand() % dataSizeClust;
  ArrayCopy(clust [class="num">0].centroid, data [ind].c, class="num">0, class="num">0, WHOLE_ARRAY);
  for (class="type">int i = class="num">1; i < ArraySize(clust); i++)
  {
    class="type">class="kw">double sum = class="num">0;
    class=class="str">"cmt">// Compute the distance from each data point to the nearest centroid
    for (class="type">int j = class="num">0; j < dataSizeClust; j++)
    {
      class="type">class="kw">double minDist = DBL_MAX;
      for (class="type">int k = class="num">0; k < i; k++)
      {
        class="type">class="kw">double dist = VectorDistance(data [j].c, clust [k].centroid);
        if (dist < minDist)
        {
          minDist = dist;
        }
      }
      data [j].minDist = minDist;
      sum += minDist;
    }
    class=class="str">"cmt">// Choose the next centroid with a probability proportional to the distance
    class="type">class="kw">double r = MathRand() * sum;
    for (class="type">int j = class="num">0; j < dataSizeClust; j++)
    {
      if (r <= data [j].minDist)
      {
        ArrayCopy(clust [i].centroid, data [j].c, class="num">0, class="num">0, WHOLE_ARRAY);
        break;
      }
      r -= data [j].minDist;
    }
  }
}

收束

这一节把 BSO 算法的逻辑骨架、聚类降维思路铺开了,但还没跑完——下一篇才会补齐完整流程并给出性能对照。 对做 EA 调参的人而言,这类群体智能优化值不值得换掉网格/遗传,得等回测维度压下来后的耗时与稳定度数据说话,外汇与贵金属品种上高杠杆叠加过拟合风险,结论只能倾向不能打包票。 作者公开的 MQL5 优化算法库里能直接拉到基础实现,想验证的人开 MT5 把聚类阈值调一档,就能感受维度削减对优化耗时的真实影响。

把多峰诊断交给小布
这些聚类分群与多解扫描的逻辑,小布盯盘的 AIGC 已内置到品种页,打开就能看到自动标出的若干潜在结构区,你只管判断哪条更值得跟。

常见问题

BSO 把每个个体同时当解和数据点,靠坐标聚类找多个有意义解,而非只追一个全局最优,更适合多峰问题。
它按个体在空间中的坐标做无监督分群,让算法不必预先知道该拆成几个子种群,解量随数据自然浮现。
本文偏概念铺垫,先把聚类和想法生成规则读顺,后续实战篇会给出具体调用与参数经验。
可以,品种页的 AIGC 模块已封装类似逻辑,自动输出若干候选结构区,省去自己写聚类循环。
这类品种波动受事件驱动明显,聚类结果仅代表历史样本下的概率分区,实盘仍属高风险,需结合风控。