龟壳演化算法(TSEA)·进阶篇
(2/3)·把龟壳分层生长搬进 EA,内层保留劣解的思路为何能躲过局部最优
「KNN 投票落点与 TSEA 默认参数」
上面那段闭括号收尾的是 K 近邻分类的归票逻辑:先对距离索引做冒泡排序,再开一个长度为 n_clusters 的 votes 数组并清零,随后只遍历前 k_neighbors 个近邻,把带有效标签(非 -1 且在簇数范围内)的样本对应簇票数加一。 最后两轮线性扫描找最大票簇——max_votes 初始 0、max_votes_cluster 初始 -1,只要某簇票数比当前最大值高就刷新,函数最终返回得票最高的簇编号;若所有近邻都无标签,返回值是 -1,调用方需自行处理这种「无监督邻域」情形。 紧跟着的 C_AO_TSEA 类把这套聚类骨架包装成可调参的分析对象。构造函数里写死了 5 个关键数:种群规模 popSize=100、纵向簇 vClusters=3、横向簇 hClusters=10、近邻数 neighbNumb=5、单格最大代理 maxAgentsInCell=3。 这些字面量同时被写进 params 数组,所以你在 MT5 里改面板参数后,必须走 SetParams() 把 double 型的 val 强转回 int 覆盖成员变量,否则实际跑的还是构造时的旧值。外汇与贵金属波动剧烈,这类聚类参数对样本窗口敏感,实盘前建议在历史数据上多次重算验证。
BubbleSort(distances_indices, class="num">0, n - class="num">1); class=class="str">"cmt">// Define the cluster for the point class="type">int votes []; ArrayResize(votes, n_clusters); ArrayInitialize(votes, class="num">0); for (class="type">int j = class="num">0; j < k_neighbors; j++) { class="type">int label = data [distances_indices [j].index].label; if (label != -class="num">1 && label < n_clusters) { votes [label]++; } } class="type">int max_votes = class="num">0; class="type">int max_votes_cluster = -class="num">1; for (class="type">int j = class="num">0; j < n_clusters; j++) { if (votes [j] > max_votes) { max_votes = votes [j]; max_votes_cluster = j; } } class="kw">return max_votes_cluster; };
TSEA 优化器的初始化与种群落格
这套树状空间探索算法(TSEA)把搜索空间切成纵向 vClusters 与横向 hClusters 两套簇,再用 agent 数组承载 popSize 个搜索个体。Init 函数先调 StandardInit 校验传入的 rangeMinP / rangeMaxP / rangeStepP 三套边界数组,任一不合法就直接返回 false,MT5 上你会看到EA初始化失败。 下面这段是 Init 的核心内存布局逻辑,建议直接拷进 MT5 看数组怎么铺开: bool C_AO_TSEA::Init (const double &rangeMinP [], //minimum search range const double &rangeMaxP [], //maximum search range const double &rangeStepP [], //step search const int epochsP = 0) //number of epochs { if (!StandardInit (rangeMinP, rangeMaxP, rangeStepP)) return false; ArrayResize (agent, popSize); for (int i = 0; i < popSize; i++) agent [i].Init (coords); ArrayResize (clusters, hClusters); for (int i = 0; i < hClusters; i++) clusters [i].Init (coords); ArrayResize (cell, vClusters); for (int i = 0; i < vClusters; i++) { ArrayResize (cell [i].cell, hClusters); for (int c = 0; c < hClusters; c++) ArrayResize (cell [i].cell [c].agent, 0, maxAgentsInCell); } minFval = DBL_MAX; stepF = 0.0; epochs = epochsP; epochsNow = 0; return true; } 逐行拆一下:ArrayResize(agent, popSize) 给种群分配槽位,随后每个 agent[i] 用 coords 维度初始化;clusters 按 hClusters 横向簇数铺开。cell 是二维网格,纵轴 vClusters、横轴 hClusters,每个格子的 agent 子数组预留 maxAgentsInCell 容量——这一步决定了网格内最多塞多少个体,外汇参数寻优时若 maxAgentsInCell 太小,局部簇容易溢出丢解。 minFval 初始化成 DBL_MAX 意味着首轮适应度必被刷新,epochsNow 从 0 计起。默认 epochsP=0 表示不限制迭代轮数,实盘跑黄金 XAUUSD 这类高波动品种时,可能因为永不收敛而卡死回测,建议显式传一个整数上限。
class="type">bool C_AO_TSEA::Init(const class="type">class="kw">double &rangeMinP [], class=class="str">"cmt">//minimum search range const class="type">class="kw">double &rangeMaxP [], class=class="str">"cmt">//maximum search range const class="type">class="kw">double &rangeStepP [], class=class="str">"cmt">//step search 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(clusters, hClusters); for (class="type">int i = class="num">0; i < hClusters; i++) clusters [i].Init(coords); ArrayResize(cell, vClusters); for (class="type">int i = class="num">0; i < vClusters; i++) { ArrayResize(cell [i].cell, hClusters); for (class="type">int c = class="num">0; c < hClusters; c++) ArrayResize(cell [i].cell [c].agent, class="num">0, maxAgentsInCell); } minFval = DBL_MAX; stepF = class="num">0.0; epochs = epochsP; epochsNow = class="num">0; class="kw">return true; }
◍ 网格里挑种子与 0.8 概率的邻域借用
种群初始化只在非修订模式下跑一次:给 popSize 个个体在每个坐标维度上先丢入均匀随机数,再用 SeInDiSp 按 rangeStep 吸附到离散格点,写进 agent 数组后直接 return,不在每代重复采样。 修订模式下,代码先扫 vClusters × hClusters 的网格,对每个非空 cell 找适应度 f 最大的 agent,把下标存进 indBest。实测中若某 cell 有 12 个 agent、最大 f 落在第 3 个,indBest 就记 3,供后续交叉直接调用。 主干循环对每个个体以 0.8 概率进入邻域借用:用 (-rnd²+1)·vClusters 做偏向 0 行的垂直抽样,水平向随机挑列,直到命中非空 cell。抽中后 50% 概率直接取该 cell 的 indBest 坐标,否则随机选一个 agent。 坐标赋值阶段有 0.6 概率沿用邻居值、否则回退全局最优 cB;扰动幅度固定为 (rangeMax-rangeMin)·0.1。外汇与贵金属参数空间里这种 10% 步长可能让 EA 在欧美盘切换时跳过关键阈值,建议开 MT5 把 0.1 改成 0.05 观察收敛节奏。
class=class="str">"cmt">//---------------------------------------------------------------------------- class=class="str">"cmt">//class="num">1. Generate random individuals for the population 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]); agent [i].c [c] = a [i].c [c]; } } class="kw">return; } class=class="str">"cmt">//---------------------------------------------------------------------------- class="type">int vPos = class="num">0; class="type">int hPos = class="num">0; class="type">int pos = class="num">0; class="type">int size = class="num">0; class="type">class="kw">double val = class="num">0.0; class="type">class="kw">double rnd = 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 v = class="num">0; v < vClusters; v++) { for (class="type">int h = class="num">0; h < hClusters; h++) { size = ArraySize(cell [v].cell [h].agent); if (size > class="num">0) { max = -DBL_MAX; pos = -class="num">1; for (class="type">int c = class="num">0; c < size; c++) { if (cell [v].cell [h].agent [c].f > max) { max = cell [v].cell [h].agent [c].f; pos = c; cell [v].cell [h].indBest = c; } } } } } for (class="type">int i = class="num">0; i < popSize; i++) { if (u.RNDprobab() < class="num">0.8) { while (true) { rnd = u.RNDprobab(); rnd = (-rnd * rnd + class="num">1.0) * vClusters; vPos = (class="type">int)rnd; if (vPos > vClusters - class="num">1) vPos = vClusters - class="num">1; hPos = u.RNDminusOne(hClusters); size = ArraySize(cell [vPos].cell [hPos].agent); if (size > class="num">0) break; } pos = u.RNDminusOne(size); if (u.RNDprobab() < class="num">0.5) pos = cell [vPos].cell [hPos].indBest; for (class="type">int c = class="num">0; c < coords; c++) { if (u.RNDprobab() < class="num">0.6) val = cell [vPos].cell [hPos].agent [pos].c [c]; else val = cB [c]; class="type">class="kw">double dist = (rangeMax [c] - rangeMin [c]) * class="num">0.1;
「适应度分层与聚类修订的实现细节」
这段逻辑紧接种群生成之后,负责把每一代 agent 的适应度映射到二维网格,并为后续交叉挑选父代。Revision() 先遍历 popSize 个个体,把 a[i].f 同步给 agent[i].f,同时记录全局最优 fB 与最差 minFval,若发现更优个体就用 ArrayCopy 把它的坐标 c 写进 cB。 步长 stepF 直接由 (fB - minFval) / vClusters 算得,这意味着纵向被切成 vClusters 条等高带。每个 agent 的 labelClustV 按 (fB 除外) (agent[i].f - minFval) / stepF 取整得到,越界统一钳到 vClusters-1;最优个体强制落最顶层。 非首轮时 revision 已为 true,跳过 KMeans 初始化,直接复用上一轮聚类中心;首轮则 KMeansPlusPlusInit 加 KMeans 跑一遍 clusters 类。外汇与贵金属市场高波动,这类自适应群体算法仅用于历史回测与参数搜索,实盘信号概率仍受滑点与点差侵蚀。 父代抽取在 else 分支:用 (-rnd*rnd+1.0)*vClusters 做偏向低索引的随机,锁定 vPos 后随机选两个横向簇 hPos 与 hPos2,要求两边都有 agent 才 break。取各自随机成员做坐标均值,再经 SeInDiSp 离散化写回 a[i].c。
min = val - dist; if (min < rangeMin [c]) min = rangeMin [c]; max = val + dist; if (max > rangeMax [c]) max = rangeMax [c]; val = u.PowerDistribution(val, min, max, class="num">30); a [i].c [c] = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]); agent [i].c [c] = a [i].c [c]; } } else { class="type">int size2 = class="num">0; class="type">int hPos2 = class="num">0; class="type">int pos2 = class="num">0; while (true) { rnd = u.RNDprobab(); rnd = (-rnd * rnd + class="num">1.0) * vClusters; vPos = (class="type">int)rnd; if (vPos > vClusters - class="num">1) vPos = vClusters - class="num">1; hPos = u.RNDminusOne(hClusters); size = ArraySize(cell [vPos].cell [hPos].agent); hPos2 = u.RNDminusOne(hClusters); size2 = ArraySize(cell [vPos].cell [hPos2].agent); if (size > class="num">0 && size2 > class="num">0) break; } pos = u.RNDminusOne(size); pos2 = u.RNDminusOne(size2); for (class="type">int c = class="num">0; c < coords; c++) { val = (cell [vPos].cell [hPos ].agent [pos ].c [c] + cell [vPos].cell [hPos2].agent [pos2].c [c]) * class="num">0.5; a [i].c [c] = u.SeInDiSp(val, rangeMin [c], rangeMax [c], rangeStep [c]); agent [i].c [c] = a [i].c [c]; } } } class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class=class="str">"cmt">//—————————————————————————————————————————————————————————————————————————————— class="type">void C_AO_TSEA::Revision() { class=class="str">"cmt">//get fitness-------------------------------------------------- class="type">int pos = -class="num">1; for (class="type">int i = class="num">0; i < popSize; i++) { agent [i].f = a [i].f; if (a [i].f > fB) { fB = a [i].f; pos = i; } if (a [i].f < minFval) minFval = a [i].f; } if (pos != -class="num">1) ArrayCopy(cB, a [pos].c, class="num">0, class="num">0, WHOLE_ARRAY); stepF = (fB - minFval) / vClusters; class=class="str">"cmt">//Vertical layout of the child population----------------------------------- for (class="type">int i = class="num">0; i < popSize; i++) { if (agent [i].f == fB) agent [i].labelClustV = vClusters - class="num">1; else { agent [i].labelClustV = class="type">int((agent [i].f - minFval) / stepF); if (agent [i].labelClustV > vClusters - class="num">1) agent [i].labelClustV = vClusters - class="num">1; } } class=class="str">"cmt">//---------------------------------------------------------------------------- if (!revision) { km.KMeansPlusPlusInit(agent, popSize, clusters); km.KMeans(agent, popSize, clusters); revision = true; } class=class="str">"cmt">//----------------------------------------------------------------------------