跨邻域搜索(ANS)·进阶篇
「人工蜂群初始化与游走逻辑拆解」
下面这段 C_AO_ANS 类的 Init 与 Moving 实现,是 MT5 里一类群体智能优化器的骨架。Init 接收三组区间数组(最小、最大、步长)和可选 epoch 数,先走 StandardInit 做基础校验,失败直接返 false;随后按 popSize 开 agent 数组,按 collectionSize*2 开 coll 与 collTemp 两套缓冲,每个个体都 Init(coords) 占位。
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;
| 后续轮次里,每个坐标以 collChoiceProbab 概率从 coll 缓冲随机挑一个有效个体(f!=-DBL_MAX)作为参照 r,否则用自身历史最优 cBest;dist = | p-r | *range 决定新搜索半径,min/max 限定为 r±dist。外汇与贵金属参数优化属高风险场景,这套逻辑只是搜索空间收敛的底层机制,实盘前须在 MT5 策略测试器里跑多品种回测验证稳定性。 |
|---|
开 MT5 把 collChoiceProbab 从默认改到 0.3~0.5 区间,观察 agent 收敛速度是否有明显变化,是验证该群体算法偏向探索还是利用的最快路径。
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 过小,排序区头部容易被少数个体霸榜,外汇与贵金属行情下过拟合概率会明显抬升,属于典型高风险调参坑。
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 走高斯变异,否则从集合或自身最优里挑参考点。
<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 < popSize; i++) { <span class="keyword"> for</span> (<span class="keyword">class="type">int</span> c = <span class="number">class="num">0</span>; c < coords; c++) { <span class="keyword">if</span> (u.RNDprobab() < <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() < 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],意味着推理数组与训练数组在该维度同步,方便后续差异比对。
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 上的仓库改参数自测,别盲信任何单一结论。