📘

SIA 退火算法的种群优化思路

◍ SIA 退火算法的种群优化思路

模拟各向同性退火(SIA)属于种群优化算法的一类,核心是用一组候选解在解空间里同步做各向同性扰动,再按适应度做选择,避免单点模拟退火容易陷进局部极值的毛病。 在 MT5 里跑这类算法,通常把参数寻优目标写成适应度函数,让种群在每代保持一定多样性。作者 Andrey Dik 在 2024 年 7 月 24 日发布的示例帖,截至统计有 1 049 次查看、8 条讨论,说明这类非传统优化器在实盘圈有稳定关注度。 原文将内容拆成概述、算法、测试结果三块,本小节只交代前两块定位:概述讲动机,算法讲各向同性扰动与选择算子的具体形式,后续测试结果节再用历史行情验证。外汇与贵金属市场高杠杆、高波动,任何优化器给出的参数都只是历史拟合,实盘前必须用 MT5 策略测试器重跑。

模拟退火的三块基石与调参暗坑

模拟退火(SA)的底层逻辑只有三件事:往搜索里掺随机性、允许接受更差的决策、再让接受坏解的概率随“温度”下降。随机性负责铺开探索面,坏解接受度让算法能暂时跳出局部洼地,温度冷却则把搜索从广撒网收束到精修解。 它的命名来自金属退火——烧热的金属慢慢降温,内部结构重组;算法也一样,从高温(容易接纳劣解)起步,逐渐降温(劣解接纳概率走低),倾向收敛到更优解。初始解可随机也可继承上轮,状态变更可以是纯随机或受控扰动,是否被接受由冷却函数给出的概率决定。 真正卡交易者脖子的是两个参数:初始温度与冷却比。它们互相抵消——升温几乎能被调低冷却比替代,且对最终解质量影响极大却无显式规律,只能靠实验试出来。我们实测中把优化函数步长设为 0,条件异常苛刻;能在这种设定下跑赢的算法,在较简单实盘问题上表现更好的概率显著提升。 另一个硬限制是组合爆炸:10 个变量各取 10 个值,理论组合 10^10(100 亿),而我们适应度函数只跑约 10000 次。变量一多,搜索空间复杂度指数级膨胀,SA 在大变量规模下的效率是实打实的弱点。外汇与贵金属优化属高风险实验,回测优异不承诺实盘收益。

「正态分布与各向同性对退火搜索的拉扯」

把 SA 里生成新状态的均匀分布随机数直接换成正态变量,直觉上像是让搜索更聚焦,实测却拖了后腿。原参考 C_AO_SA 在 5/25/500 组 Rastrigin 上得分 0.81510 / 0.64746 / 0.50060,总评 2.83750;换成正态扩散(Diffusion 方法限定 ±2 标准差)后同条件总评掉到 2.53305,Rastrigin 各项得分同步下滑到 0.75581 / 0.59467 / 0.49569。 正态峰值压在均值附近,远离坐标的步长概率骤降,算法变成局部游走的倾向明显。在金属退火隐喻里,这等于分子只在原位置附近抖动,丧失了均匀撒网探索晶格远区的机会,统计上显见恶化,加大 sigma 只会更糟。 反向操作反而开窍:在 Moving 里引入各向同性,让种群个体按适应度择优复制对方坐标,再叠一个 [-0.1,0.1] 的均匀随机增量。C_AO_SIA:50:1000.0:0.1:0.1 的总评飙到 5.45188,Rastrigin 5 组得分 0.99774,几乎吃满。能量在个体间被摊平,局部极小被绕开的概率提高。 先加各向同性、再叠正态增量(参数 0.05)总评回撤到 4.43177。均匀底噪已被证明够用,正态的‘锐化’二次限制探索半径,没带来预期增益。外汇与贵金属模型若借用该思路做参数寻优,须清醒:历史回测的得分差不代表实盘胜率,杠杆品种的高风险不会因算法改良消失。

◍ 均匀分布平方反而拖垮评分

把均匀分布的随机变量直接平方,对模拟退火寻优没有正向作用,性能衰减比用正态分布还狠。上面三组基准里,Forest 函数在 500 维设定下得分仅 0.03226,Megacity 在同样维度掉到 0.02480,整体加权总分 3.29667,说明这种变换把搜索空间搅得更难收敛。 为了绕开模拟退火老痛点——温度与降温比互相耦合、参数难调,可以把温度下降函数和接受劣解概率并成一条公式:用 (1 - delta) * (acosh (-(x^3 - 3))) / 1.765,其中 delta 是末两次迭代适应度差归一化到 [0,1],x 是常规化世代步长。这样两个旋钮合成一个依赖关系,调参自由度降一截。 下面这段是扩散算子的实际写法,核心是用 Box-Muller 抽正态再截断到 ±sigma(这里硬写 2.0),再按当前坐标向上下区间缩放。外层两段循环分别做个体扰动和基于历史适应度的交叉回填,最后叠一个 ±0.1 比例的随机微扰。 开 MT5 把 sigma 从 2.0 改成 8.58 注释里的值跑一遍,Rastrigin 5 维得分可能从 0.87027 往上走,但维度一高依旧容易塌。外汇与贵金属参数优化属高风险,回测得分不等于实盘鲁棒。

MQL5 / C++
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">class="type">class="kw">double</span> C_AO_SIA::Diffusion(<span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> <span class="keyword"></span>value, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> rMin, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> rMax, <span class="keyword">const</span> <span class="keyword">class="type">class="kw">double</span> step)
{
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> logN = <span class="number">class="num">0.0</span>;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> u1&nbsp;&nbsp; = RNDfromCI(<span class="number">class="num">0.0</span>, <span class="number">class="num">1.0</span>);
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> u2&nbsp;&nbsp; = RNDfromCI(<span class="number">class="num">0.0</span>, <span class="number">class="num">1.0</span>);
&nbsp;&nbsp;logN = u1 &lt;= <span class="number">class="num">0.0</span> ? <span class="number">class="num">0.000000000000001</span> : u1;
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> z0 = sqrt(-<span class="number">class="num">2</span> * log(logN))* cos(<span class="number">class="num">2</span> * M_PI * u2);
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> sigma = <span class="number">class="num">2.0</span>;<span class="comment">class=class="str">"cmt">//Sigma &gt; class="num">8.583864105157389 ? class="num">8.583864105157389 : Sigma;</span>
&nbsp;&nbsp;<span class="keyword">if</span> (z0 &gt;=&nbsp;&nbsp;sigma) z0 = RNDfromCI(<span class="number">class="num">0.0</span>,&nbsp;&nbsp;&nbsp;&nbsp;sigma);
&nbsp;&nbsp;<span class="keyword">if</span> (z0 &lt;= -sigma) z0 = RNDfromCI(-sigma, <span class="number">class="num">0.0</span>);
&nbsp;&nbsp;<span class="keyword">class="type">class="kw">double</span> dist = d * (rMax - rMin);
&nbsp;&nbsp;<span class="keyword">if</span> (z0 &gt;= <span class="number">class="num">0.0</span>) <span class="keyword">class="kw">return</span> Scale(z0, <span class="number">class="num">0.0</span>,&nbsp;&nbsp;&nbsp;&nbsp;sigma, <span class="keyword">value</span>,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">value</span> + dist, <span class="keyword">class="kw">false</span>);
&nbsp;&nbsp;<span class="keyword">else</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; <span class="keyword">class="kw">return</span> Scale(z0, -sigma, <span class="number">class="num">0.0</span>,&nbsp;&nbsp; <span class="keyword">value</span> - dist, <span class="keyword">value</span>,&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">class="kw">false</span>);
}
<span class="comment">class=class="str">"cmt">//——————————————————————————————————————————————————————————————————————————————</span>
<span class="keyword">for</span> (<span class="keyword">class="type">int</span> i = <span class="number">class="num">0</span>; i &lt; popSize; i++)
{
&nbsp;&nbsp; <span class="keyword">for</span> (<span class="keyword">class="type">int</span> c = <span class="number">class="num">0</span>; c &lt; coords; c++)
&nbsp;&nbsp; {
&nbsp;&nbsp;&nbsp;&nbsp; a [i].c [c] = Diffusion(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
&nbsp;&nbsp;&nbsp;&nbsp; a [i].c [c] = SeInDiSp&nbsp;&nbsp;(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
&nbsp;&nbsp; }
}
<span class="keyword">class="type">int</span>&nbsp;&nbsp;&nbsp;&nbsp;r&nbsp;&nbsp; = <span class="number">class="num">0</span>;
<span class="keyword">class="type">class="kw">double</span> rnd = <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 &lt; popSize; i++)
{
&nbsp;&nbsp;<span class="keyword">for</span> (<span class="keyword">class="type">int</span> c = <span class="number">class="num">0</span>; c &lt; coords; c++)
&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;r = (<span class="keyword">class="type">int</span>)RNDfromCI(<span class="number">class="num">0</span>, popSize);
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (r &gt;= popSize) r = popSize - <span class="number">class="num">1</span>;
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">if</span> (a [r].fPrev &gt; a [i].fPrev)
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a [i].c [c] = a [r].cPrev [c];
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;<span class="keyword">else</span>
&nbsp;&nbsp;&nbsp;&nbsp;{
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;a [i].c [c] = a [i].cPrev [c];
&nbsp;&nbsp;&nbsp;&nbsp;}
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;rnd = RNDfromCI(-<span class="number">class="num">0.1</span>, <span class="number">class="num">0.1</span>);
&nbsp;&nbsp;&nbsp;&nbsp;a [i].c [c] = a [i].c [c] + rnd * (rangeMax [c] - rangeMin [c]) * d;
&nbsp;&nbsp;&nbsp;&nbsp;a [i].c [c] = SeInDiSp(a [i].c [c], rangeMin [c], rangeMax [c], rangeStep [c]);
&nbsp;&nbsp;}
}

SIA 在基准函数上的实测表现

用 SIA 试验台跑了三组标准测试函数,每组各跑 10000 次,维度分别取 5、25、500。Rastrigin 函数在 5 维下得分 0.99741、500 维仍保住 0.69720;Forest 函数 5 维得分 0.93282,但到 500 维掉到 0.17866;Megacity 离散函数 5 维 0.71667、500 维仅 0.08787。综合总分 5.86552,且相比对照算法少了一个可调参数。 横向拉表看,SIA 在 26 个算法里排第 2,最终结果 89.524,仅次于 SDSm 的 100.000。它在 Forest 的 1000 变量项和 Megacity 离散的 1000 变量项上拿到满分 1.00000,说明对尖锐多峰和大规模离散空间的覆盖能力突出;弱项是 Rastrigin 的 50 维(0.93642)和 Megacity 的 50 维(0.74342),没到头部水平。 收敛图像显示个体聚成清晰簇团,并盖住了主要局部极值,形态接近金属凝固结晶。做参数优化时,若你的外汇 EA 要搜的参数面是多峰尖锐型,SIA 可能比 PSO(总分 8.423)或 SA(13.295)更省迭代;但高维离散问题里它得分衰减快,实盘前建议先用历史 tick 跑 10000 代确认稳定性。外汇与贵金属杠杆高,回测优不代表实盘盈利,任何算法都有失效概率。

「别急着下结论」

SIA 这套模拟各向同性退火,实测在带锐化、离散特征的高维函数上表现突出,根源在于它能均匀铺开解空间搜索,复杂不规则区也不容易漏掉最优点。相比经典 SA 要调三个外参,它只留温度和扩散比两个,温度还被简化成抽象温度的一部分,默认 0.01,上手成本明显更低。 作者基于自己的实验跑分,把 SIA 推荐到神经网络训练、多参数问题以及组合优化里用;附带的 26_The_world_of_AO_SIA.zip(约 437.92 KB)里是历次文章算法的当前更新版代码,想自己验证的直接下回来跑。但文末也挑明了:结论来自实验、不对规范算法绝对准确负责,代码做过多处改动。 对外汇、贵金属这类高波动品种做参数寻优时,拿 SIA 替你扫参可能比网格快,但任何优化结果都只是历史样本上的概率倾向,实盘前务必在 MT5 策略测试器里重跑并控风险。

常见问题

容易早熟收敛,个体挤在一块儿失去探索能力。建议先跑短测试看散布度,再缓降温度。
各向同性在狭长方向浪费步长,可改各向异性或缩放步长,让搜索贴着谷底走。
小布可读取你的种群散布与收敛曲线,提示温度节奏和采样偏向是否失衡,省去手盯。
平方压缩了尾部,大步探索被砍,基准评分普遍掉一截。直接用正态或截断正态更稳。
噪声高时单一退火链易陷局部,加周期性重热或子群重启,实测恢复率更高。