动物迁徙优化(AMO)算法·进阶篇
(2/3)· 当驯鹿群避开碰撞的规则被写成坐标更新公式,优化问题的探索与利用如何被重新定义
「AMO 种群初始化与邻域扰动的内部逻辑」
这段 C_AO_AMO 类的实现,把自适应元启发式优化(AMO)在 MT5 下的种群运转拆成了三步:初始化、移动、修订。看懂这三段,你就能在 EA 里直接改 popSize 与 rangeStep 观察参数空间搜索行为。 构造函数里先调 StandardInit 做范围校验,随后用 ArrayResize 把 population 与 pTemp 都拉到 popSize 长度,再让每个个体 Init(coords) 占位。注意 epochsP 默认传 0,意味着若外部不显式给训练轮数,类内部不会自行限制迭代。 Moving() 第一次跑时 revision 为 false,会先用均匀随机(RNDfromCI)在 [rangeMin, rangeMax] 内撒点,再用 SeInDiSp 按 rangeStep 对齐到离散网格,之后置 revision=true 直接返回。后续调用才进入邻域高斯扰动:对每个个体 i 取邻居索引 ind1,算坐标差绝对值 dist,以自身坐标 x 为中心、[x-dist, x+dist] 为界(夹到全局范围),用 GaussDistribution 采样新值再离散化。 Revision() 负责记录历史最优:遍历种群,若临时个体 a[i].f 大于当前最优 fB 就更新 fB 与索引 ind,最后 ArrayCopy 把最优坐标 cB 拷出。外汇与贵金属市场高波动,这类优化器用于参数寻优时过拟合概率偏高,实盘前务必多周期验证。
class="kw">const class="type">int epochsP = class="num">0) { if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false; class=class="str">"cmt">//---------------------------------------------------------------------------- ArrayResize(population, popSize); ArrayResize(pTemp, popSize); for (class="type">int i = class="num">0; i < popSize; i++) population [i].Init(coords); class="kw">return true; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_AMO::Moving() { class=class="str">"cmt">//---------------------------------------------------------------------------- if (!revision) { for (class="type">int i = class="num">0; i < popSize; i++) { 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]); } } revision = true; class="kw">return; } class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">int ind1 = class="num">0; class="type">class="kw">double dist = class="num">0.0; class="type">class="kw">double x = class="num">0.0; class="type">class="kw">double min = class="num">0.0; class="type">class="kw">double max = 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++) { class=class="str">"cmt">//------------------------------------------------------------------------ ind1 = GetNeighborsIndex(i); dist = fabs(population [ind1].c [c] - a [i].c [c]); x = a [i].c [c]; min = x - dist; max = x + dist; if (min < rangeMin [c]) min = rangeMin [c]; if (max > rangeMax [c]) max = rangeMax [c]; a [i].c [c] = u.GaussDistribution(x, min, max, deviation); a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_AMO::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">//---------------------------------------------------------------------------- class="type">int ind1 = class="num">0; class="type">int ind2 = class="num">0; class="type">class="kw">double dist = class="num">0.0; class="type">class="kw">double x = class="num">0.0; class="type">class="kw">double min = class="num">0.0;
交叉算子与邻域采样的实现细节
这段实现把种群规模悄悄扩了一倍:Init 里 ArrayResize(population, popSize * 2) 和 pTemp 同样翻倍,并给 2*popSize 个个体做 Init(coords)。多出来的半数容量,是给下面的交叉阶段当临时缓冲 a[] 用的,避免原地覆盖父代。
交叉循环里概率按排名走:第 i 个个体的 prob = 1 - 1/(i+1),也就是说排名越靠后(i 越大)被重组的概率越接近 1。对每个坐标 c,若随机数小于 prob,就随机抽两个不同父代取中点,再用 SeInDiSp 把值钳回 [rangeMin, rangeMax] 并按 rangeStep 离散化。
邻域函数 GetNeighborsIndex 用 MathRand() 做边界感知采样:头尾各 Ncount 个元素只在局部 N=2*Ncount+1 窗口内取随机下标,中间元素则在 i-Ncount 起跳的窗口里取。这套写法在 MT5 里直接贴进 EA 的类的 cpp 就能跑,建议把 neighborsNumberOnSide 先设 2,看排序收敛是否变慢。
外汇与贵金属市场波动剧烈、杠杆风险高,这类群体算法参数敏感,实盘前务必在策略测试器用历史数据验证,结果仅代表历史样本表现,未来可能失效。
class="type">class="kw">double max = class="num">0.0; class="type">class="kw">double prob = class="num">0.0; for (class="type">int i = class="num">0; i < popSize; i++) { prob = class="num">1.0 - (class="num">1.0 / (i + class="num">1)); for (class="type">int c = class="num">0; c < coords; c++) { if (u.RNDprobab() < prob) { ind1 = u.RNDminusOne(popSize); ind2 = u.RNDminusOne(popSize); if (ind1 == ind2) { ind2++; if (ind2 > popSize - class="num">1) ind2 = class="num">0; } a [i].c [c] = (population [ind1].c [c] + population [ind2].c [c]) * class="num">0.5; a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } } class=class="str">"cmt">//---------------------------------------------------------------------------- for (class="type">int i = class="num">0; i < popSize; i++) population [i] = a [i]; u.Sorting(population, pTemp, popSize); } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">int C_AO_AMO::GetNeighborsIndex(class="type">int i) { class="type">int Ncount = neighborsNumberOnSide; class="type">int N = Ncount * class="num">2 + class="num">1; class="type">int neighborIndex; class=class="str">"cmt">// Select a random neighbor given the array boundaries if (i < Ncount) { class=class="str">"cmt">// For the first Ncount elements neighborIndex = MathRand() % N; } else { if (i >= popSize - Ncount) { class=class="str">"cmt">// For the last Ncount elements neighborIndex = (popSize - N) + MathRand() % N; } else { class=class="str">"cmt">// For all other elements neighborIndex = i - Ncount + MathRand() % N; } } class="kw">return neighborIndex; } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">bool C_AO_AMOm::Init(class="kw">const class="type">class="kw">double &rangeMinP [], class="kw">const class="type">class="kw">double &rangeMaxP [], class="kw">const class="type">class="kw">double &rangeStepP [], class="kw">const class="type">int epochsP = class="num">0) { if (!StandardInit(rangeMinP, rangeMaxP, rangeStepP)) class="kw">return false; class=class="str">"cmt">//---------------------------------------------------------------------------- ArrayResize(population, popSize * class="num">2); ArrayResize(pTemp, popSize * class="num">2); for (class="type">int i = class="num">0; i < popSize * class="num">2; i++) population [i].Init(coords); class="kw">return true; } class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————
◍ 变异阶段怎么用邻域距离约束搜索范围
在差分进化类 EA 里,子代变异不是纯随机撒点,而是先拿当前个体 i 在种群里的邻居 ind1,算两者在第 c 维上的绝对距离 dist,再把搜索区间锁在 [x-dist, x+dist] 之内,超出预设的 rangeMin/rangeMax 就截断。 代码里 prob = 1.0 - (1.0/(i+1)) 意味着排序越靠后的个体 i 越大(i 从 0 起,i=0 时 prob=0,i=9 时 prob≈0.9),交叉算子的触发概率随排名陡升,这是典型的排名偏好设计。 如果随机数小于 prob,就从种群里抽两个不同下标 ind1、ind2 做平均赋值:a[i].c[c] = (population[ind1].c[c] + population[ind2].c[c]) * 0.5,否则走高斯扰动 u.GaussDistribution(x, min, max, deviation)。最后统一用 SeInDiSp 把值吸附到离散网格步长上,避免浮点越界。 开 MT5 把 popSize 设成 30 跑一遍,观察 i 接近 popSize-1 时交叉平均占比是否明显拉高,能直接验证这套排名概率是否按预期工作;外汇与贵金属杠杆高,参数扰动可能引发净值剧烈回撤,需先在策略测试器用历史数据确认。
for (class="type">int i = class="num">0; i < popSize; i++) population [i + popSize] = a [i]; u.Sorting(population, pTemp, popSize * class="num">2); class="type">int ind1 = class="num">0; class="type">int ind2 = class="num">0; class="type">class="kw">double dist = class="num">0.0; class="type">class="kw">double x = class="num">0.0; 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 prob = class="num">0.0; for (class="type">int i = class="num">0; i < popSize; i++) { prob = class="num">1.0 - (class="num">1.0 / (i + class="num">1)); for (class="type">int c = class="num">0; c < coords; c++) { class=class="str">"cmt">//------------------------------------------------------------------------ ind1 = GetNeighborsIndex(i); dist = fabs(population [ind1].c [c] - a [i].c [c]); x = a [i].c [c]; min = x - dist; max = x + dist; if (min < rangeMin [c]) min = rangeMin [c]; if (max > rangeMax [c]) max = rangeMax [c]; a [i].c [c] = u.GaussDistribution(x, min, max, deviation); class=class="str">"cmt">//---------------------------------------------------------------------------- if (u.RNDprobab() < prob) { ind1 = u.RNDminusOne(popSize); ind2 = u.RNDminusOne(popSize); if (ind1 == ind2) { ind2++; if (ind2 > popSize - class="num">1) ind2 = class="num">0; } a [i].c [c] = (population [ind1].c [c] + population [ind2].c [c]) * class="num">0.5; } a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } class="type">int ind1 = class="num">0; class="type">int ind2 = class="num">0; class="type">class="kw">double dist = class="num">0.0; class="type">class="kw">double x = class="num">0.0; 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 prob = class="num">0.0; for (class="type">int i = class="num">0; i < popSize; i++) { prob = class="num">1.0 - (class="num">1.0 / (i + class="num">1)); for (class="type">int c = class="num">0; c < coords; c++) { class=class="str">"cmt">//------------------------------------------------------------------------ ind1 = GetNeighborsIndex(i); dist = fabs(population [ind1].c [c] - a [i].c [c]); x = population [ind1].c [c];
「变异与精英留存的实现细节」
这段逻辑处在个体生成循环内部,先以当前解 x 为中心、dist 为半径划定搜索边界,再用 rangeMin/rangeMax 截断越界部分,保证新基因落在参数定义域内。 min = x - dist; max = x + dist; if (min < rangeMin[c]) min = rangeMin[c]; if (max > rangeMax[c]) max = rangeMax[c]; a[i].c[c] = u.GaussDistribution(x, min, max, deviation); a[i].c[c] = u.SeInDiSp(a[i].c[c], rangeMin[c], rangeMax[c], rangeStep[c]); 上方先用高斯分布做局部扰动,再用离散化函数把连续值吸附到合法步长上。 概率 prob 控制是否触发交叉:当随机数小于 prob 且再掷一次小于 0.01 时,取两个随机旧个体做平均交叉,这层 1% 的小概率让种群偶尔大幅跳变,可能逃离局部极值。 if (u.RNDprobab() < prob) { if (u.RNDprobab() <= 0.01) { ind1 = u.RNDminusOne(popSize); ind2 = u.RNDminusOne(popSize); a[i].c[c] = (population[ind1].c[c] + population[ind2].c[c]) * 0.5; a[i].c[c] = u.SeInDiSp(a[i].c[c], rangeMin[c], rangeMax[c], rangeStep[c]); } } 注释掉的 ind1==ind2 修正分支说明作者曾考虑去重,但最终留了简单版。 Revision 函数在每代末尾跑:先扫一遍找最优适应度 fB 并记录索引,若找到就用 ArrayCopy 把该染色体存进全局最优 cB。 int ind = -1; for (int i = 0; i < popSize; i++) { if (a[i].f > fB) { fB = a[i].f; ind = i; } } if (ind != -1) ArrayCopy(cB, a[ind].c, 0, 0, WHOLE_ARRAY); 随后把当代种群追加进 population 后半段,调用 Sorting 对两倍规模做排序,隐式完成精英保留——前 popSize 个会进入下一代。 for (int i = 0; i < popSize; i++) population[popSize + i] = a[i]; u.Sorting(population, pTemp, popSize * 2); 在 MT5 里把 popSize 设到 50、prob 设 0.8 跑黄金小时图,能直接观察 fB 是否单调改善来判断截断逻辑有无写错。
min = x - dist; max = x + dist; if (min < rangeMin [c]) min = rangeMin [c]; if (max > rangeMax [c]) max = rangeMax [c]; a [i].c [c] = u.GaussDistribution(x, min, max, deviation); a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); class=class="str">"cmt">//------------------------------------------------------------------------ if (u.RNDprobab() < prob) { if (u.RNDprobab() <= class="num">0.01) { ind1 = u.RNDminusOne(popSize); ind2 = u.RNDminusOne(popSize); class=class="str">"cmt">//if (ind1 == ind2) { class=class="str">"cmt">//ind2++; class=class="str">"cmt">//if (ind2 > popSize - class="num">1) ind2 = class="num">0; a [i].c [c] = (population [ind1].c [c] + population [ind2].c [c]) * class="num">0.5; a [i].c [c] = u.SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]); } } class=class="str">"cmt">//ind1 = u.RNDminusOne(popSize); class=class="str">"cmt">//a [i].c [c] = population [ind1].c [c]; } } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_AMO::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++) population [popSize + i] = a [i]; u.Sorting(population, pTemp, popSize * class="num">2); }