并行粒子群优化·进阶篇
🧠

并行粒子群优化·进阶篇

(2/3)· 遗传算法不够用?这篇把 PSO 类结构与并行骨架拆给你看

案例拆解 第 2/3 篇

接上篇铺垫的优化器动机,我们继续深挖:标准测试器只给枚举和遗传两条路,想换粒子群就得自己造轮子。多数人卡在不会把算法类嵌进 MT5 代理体系,结果本地多核白白闲置。

「用基准函数验证粒子群优化没跑偏」

拿 testpso.mq5 脚本跑一遍,是确认 PSO 优化真正生效的最低成本办法。它依赖 ParticleSwarmParallel.mqh,里面除了基础类,还塞了后面要拆的改进版逻辑。 测试框架走的是 OOP:基类 BaseFunctor 管目标函数,派生类对象在构造时通过 PSOTests::register 自动挂进测试清单,run 方法统一触发所有对象的 test。脚本里内置了 rosenbrock、griewank、sphere 等经典基准函数。 标准基准函数都是求最小化,但这套算法按最大化搜 EA 性能,所以 calculate 里统一取负。函数连续、无离散步长。sphere 示例里搜索范围 ±100,三维期望极值就是 (0,0,0)。 实跑日志能直接看穿精度:sphere 在 99 个 cycle 后目标值压到 -8.16e-07,输出坐标约 (-0.00059, -0.00048, 0.00048),贴近精确解。因为粒子随机初始化,每次数值会抖,精度由输入参数决定。 群规模与组数由经验规则自动选,日志行 PSO[3] created: 15/3 表示 3 维空间、15 粒子、3 组。你改 BaseFunctor 构造里的维数或循环次数,就能在 MT5 里复现不同收敛曲线。外汇与贵金属实盘接这套优化属高风险,回测优不等同实盘稳。

MQL5 / C++
class BaseFunctor: class="kw">public Functor
{
class="kw">protected:
  class="kw">const class="type">int params;
  class="type">class="kw">double max[], min[], steps[];
class="kw">public:
  BaseFunctor(class="kw">const class="type">int p): params(p) class=class="str">"cmt">// number of parameters
  {
    ArrayResize(max, params);
    ArrayResize(min, params);
    ArrayResize(steps, params);
    ArrayInitialize(steps, class="num">0);
    PSOTests::register(&this);
  }
  class="kw">virtual class="type">void test(class="kw">const class="type">int loop)   class=class="str">"cmt">// worker method
  {
    Swarm swarm(params, max, min, steps);
    swarm.optimize(this, loop);
    class="type">class="kw">double result[];
    swarm.getSolution(result);
    for(class="type">int i = class="num">0; i < params; i++)
    {
      Print(i, " ", result[i]);
    }
  }
};
class PSOTests
{
  class="kw">static BaseFunctor *testCases[];
class="kw">public:
  class="kw">static class="type">void register(BaseFunctor *f)
  {
    class="type">int n = ArraySize(testCases);
    ArrayResize(testCases, n + class="num">1);
    testCases[n] = f;
  }
  class="kw">static class="type">void run(class="kw">const class="type">int loop = class="num">100)
  {
    for(class="type">int i = class="num">0; i < ArraySize(testCases); i++)
    {
      testCases[i].test(loop);
    }
  }
};
class Sphere: class="kw">public BaseFunctor
{
class="kw">public:
  Sphere(): BaseFunctor(class="num">3) class=class="str">"cmt">// expected global minimum(class="num">0, class="num">0, class="num">0)
  {
    for(class="type">int i = class="num">0; i < params; i++)
    {
      max[i] = class="num">100;
      min[i] = -class="num">100;
    }
  }
  class="kw">virtual class="type">void test(class="kw">const class="type">int loop)
  {
    Print("Optimizing " + class="kw">typename(this));
    BaseFunctor::test(loop);
  }
  class="kw">virtual class="type">class="kw">double calculate(class="kw">const class="type">class="kw">double &vec[])
  {
    class="type">int dim = ArraySize(vec);
    class="type">class="kw">double sum = class="num">0;
    for(class="type">int i = class="num">0; i < dim; i++) sum += pow(vec[i], class="num">2);
    class="kw">return -sum; class=class="str">"cmt">// negative for maximization
  }
};
class="type">void OnStart()
{
  PSOTests::Sphere sphere;
  PSOTests::Griewank griewank;
  PSOTests::Rosenbrock rosenbrock;
  PSOTests::run();
}

粒子群跑完千次后的收敛残差

一段 PSO 寻优日志里,第 90 轮迭代完成后 skipped 0 of 10,累计残差 -0.0005473814163781119;第 99 轮 skipped 仍为 0,残差收敛到 -7.255520122486163e-06,量级已压到 1e-6。 PSO Finished 982 of 1000 planned calculations: true,说明计划千次计算里实际跑了 982 次,未达满额但返回成功状态,这种截断在 MT5 策略测试器里常见于步长约束或早停阈值触发。 末尾两行是寻得的最优参数映射:索引 0 对应 1.001858172119364,索引 1 对应 1.003524791491219,可直接抄进输入参数做复验。外汇与贵金属参数寻优属高风险行为,残差小不代表实盘稳健,请用历史数据重跑确认。

MQL5 / C++
Cycle class="num">90 done, skipped class="num">0 of class="num">10 / -class="num">0.0005473814163781119
Cycle class="num">99 done, skipped class="num">0 of class="num">10 / -class="num">7.255520122486163e-06
PSO Finished class="num">982 of class="num">1000 planned calculations: true
class="num">0 class="num">1.001858172119364
class="num">1 class="num">1.003524791491219

◍ 把粒子群拆进多核测试代理

单线程跑 PSO 只是验证思路,MT5 终端真正值钱的是能把计算摊到所有处理器核心上。原 Swarm 类在一个实例里管多个组,没法直接并行,得改成交由多个 EA 实例各带一个组、各自占一个本地测试代理,组间再交换已探过的参数点。 并行组数至少等于核心数,也可以更高但建议取核心数的整数倍;空间维度越大往往越需要更多组,不过核心数太少会让简单测试也卡住。每个实例跑完用 getSolution 把小群结果以帧发回终端,终端挑最优过程,这部分和标准优化通道一致。 组间不能靠终端回传数据——测试器架构只允许代理到终端的受控传输,反向是封闭分发。所以索引二叉树改用 FILE_COMMON 共享文件夹落地:每个代理写自己的索引文件,别的进程随时读并并回本地树。MQL 里文件不关闭别的进程读不到刷新内容,FILE_SHARE_READ/WRITE 和 FileFlush 都救不了,因此必须写完就关。 树导出用了 visitor 模式,Exporter 按遍历顺序把节点值写 CSV 一行一个。默认不排有序遍历(PSO_DEBUG_BINTREE 关着),而是用节点层级和值异或决定左右访问顺序,避免从文件重载时退化成长分支。等跑过的进程变多,共享历史里唯一点不断并回本地索引,后续组启动后 calculate 命中已查点概率上升,PSO 周期倾向加速,接近空间全覆盖时 _unique 会逼近可能参数组合数。 下面这段是核心改造的 MQL5 骨架,注意 Swarm 构造里 MQL_OPTIMIZATION 下把组数硬编码成 1,以及 Exporter 的无序遍历分支。

MQL5 / C++
Swarm(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> size, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> globals, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> params, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;max[], <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;min[], <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">double</span> &amp;step[])
{
<span class="keyword">if</span>(<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_OPTIMIZATION</span>))
{
init(size == class="num">0 ? params * AUTO_SIZE_FACTOR : size, class="num">1, params, max, min, step);
}
...
<span class="keyword">class="kw">template</span>&lt;<span class="keyword">class="kw">typename</span> T&gt;
<span class="keyword">class </span>Visitor
{
<span class="keyword">class="kw">public</span>:
<span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> visit(TreeNode&lt;T&gt; *node) = class="num">0;
};
<span class="keyword">class="kw">template</span>&lt;<span class="keyword">class="kw">typename</span> T&gt;
<span class="keyword">class </span>Exporter: <span class="keyword">class="kw">public</span> Visitor&lt;T&gt;
{
<span class="keyword">class="kw">private</span>:
<span class="keyword">class="type">int</span> file;
<span class="keyword">class="type">uint</span> level;
<span class="keyword">class="kw">public</span>:
Exporter(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> name): level(class="num">0)
{
file = <span class="functions">FileOpen</span>(name, <span class="macro">FILE_READ</span> | <span class="macro">FILE_WRITE</span> | <span class="macro">FILE_CSV</span> | <span class="macro">FILE_ANSI</span> | <span class="macro">FILE_SHARE_READ</span>| <span class="macro">FILE_SHARE_WRITE</span> | <span class="macro">FILE_COMMON</span>, &class="macro">#x27;,&class="macro">#x27;);
}
~Exporter()
{
<span class="functions">FileClose</span>(file);
}
<span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> visit(TreeNode&lt;T&gt; *node) <span class="keyword">class="kw">override</span>
{
<span class="keyword">class="macro">#ifdef</span> PSO_DEBUG_BINTREE
<span class="keyword">if</span>(node.getLeft()) visit(node.getLeft());
<span class="functions">FileWrite</span>(file, node.getValue());
<span class="keyword">if</span>(node.getRight()) visit(node.getRight());
<span class="keyword">class="macro">#else</span>
<span class="keyword">class="kw">const</span> T v = node.getValue();
<span class="functions">FileWrite</span>(file, v);
level++;
<span class="keyword">if</span>((level | (<span class="keyword">class="type">uint</span>)v) % class="num">2 == class="num">0)
{
<span class="keyword">if</span>(node.getLeft()) visit(node.getLeft());
<span class="keyword">if</span>(node.getRight()) visit(node.getRight());
}
<span class="keyword">else</span>
{
<span class="keyword">if</span>(node.getRight()) visit(node.getRight());
<span class="keyword">if</span>(node.getLeft()) visit(node.getLeft());
}
level--;
<span class="keyword">class="macro">#endif</span>
}
};
<span class="keyword">class="kw">template</span>&lt;<span class="keyword">class="kw">typename</span> T&gt;
<span class="keyword">class </span>BinaryTree
{
...
<span class="keyword">class="type">void</span> visit(Visitor&lt;T&gt; *visitor)
{
visitor.visit(root);
}
};
<span class="keyword">class="type">void</span> exportIndex(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> id)
{
<span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> name = sharedName(id);
Exporter&lt;<span class="keyword">class="type">class="kw">ulong</span>&gt; exporter(name);
index.visit(&amp;exporter);
}
<span class="keyword">class="macro">#define</span> PPSO_FILE_PREFIX <span class="class="type">class="kw">string">"PPSO-"</span>
<span class="keyword">class="type">class="kw">string</span> sharedName(<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> id, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> prefix = PPSO_FILE_PREFIX, <span class="keyword">class="kw">const</span> <span class="keyword">class="type">class="kw">string</span> ext = <span class="class="type">class="kw">string">".csv"</span>)
{
<span class="keyword">class="type">class="kw">ushort</span> array[];

「多核索引的合并与文件读取实现」

这段逻辑解决了一个实际问题:在 MT5 里用多核回测或并行计算时,每个核心会写出带自身编号的索引文件,启动时要把它们并回一张总表。 共享名生成用了终端路径做 crc64 散列,再拼上程序名与核心 id;当 id 为 -1 时退化为通配符形式,方便后面一次性扫全部核心的落盘文件。 restoreIndex 先调 FileFindFirst 用通配符在 FILE_COMMON 目录里捞文件名,再对每个命中文件开一个 FileReader 把内容喂给当前对象;do-while 配 FileFindNext 把目录扫干净后关句柄,返回恒为 true。

FileReader 构造即 FileOpen,标志位叠了 FILE_READFILE_CSVFILE_SHARE_READFILE_SHARE_WRITEFILE_ANSI,说明索引是 ANSI 编码的 CSV,且允许别的进程同时读写——这对实时盯盘下的热恢复很关键。

Swarm::feed 是合并的落点:每读一行先 _read++;若 index.add 返回 false 表示树里还没有该值,计 _restored;否则再试 merge.add,失败则说明是重复值,计 _unique。三个计数器直接暴露了「已读 / 新恢复 / 冗余」的体量,开 MT5 跑一遍就能在日志里看到具体分布。 外汇与贵金属市场高杠杆、滑点无常,任何本地索引恢复机制都只是辅助,不能替代对行情风险的独立判断。

MQL5 / C++
StringToShortArray(TerminalInfoString(TERMINAL_PATH), array);
class="kw">const class="type">class="kw">string program = MQLInfoString(MQL_PROGRAM_NAME) + "-";
if(id != -class="num">1)
{
class="kw">return prefix + program + StringFormat("%08I64X-%04d", crc64(array), id) + ext;
}
class="kw">return prefix + program + StringFormat("%08I64X-*", crc64(array)) + ext;
}
class="type">bool restoreIndex()
{
class="type">class="kw">string name;
class="kw">const class="type">class="kw">string filter = sharedName(-class="num">1); class=class="str">"cmt">// use wildcards to merge multiple indices for all cores
class="type">long h = FileFindFirst(filter, name, FILE_COMMON);
if(h != INVALID_HANDLE)
{
do
{
FileReader reader(name, FILE_COMMON);
reader.read(this);
}
while(FileFindNext(h, name));
FileFindClose(h);
}
class="kw">return true;
}
class Feed
{
class="kw">public:
class="kw">virtual class="type">bool feed(class="kw">const class="type">int dump) = class="num">0;
};
class FileReader
{
class="kw">protected:
class="type">int dump;
class="kw">public:
FileReader(class="kw">const class="type">class="kw">string name, class="kw">const class="type">int flags = class="num">0)
{
dump = FileOpen(name, FILE_READ | FILE_CSV | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_ANSI | flags, &class="macro">#x27;,&class="macro">#x27;);
}
class="kw">virtual class="type">bool isReady() class="kw">const
{
class="kw">return dump != INVALID_HANDLE;
}
class="kw">virtual class="type">bool read(Feed &pass)
{
if(!isReady()) class="kw">return class="kw">false;
while(!FileIsEnding(dump))
{
if(!pass.feed(dump))
{
class="kw">return class="kw">false;
}
}
class="kw">return true;
}
};
class Swarm: class="kw">public Feed
{
class="kw">private:
...
class="type">int _read;
class="type">int _unique;
class="type">int _restored;
BinaryTree<class="type">class="kw">ulong> merge;
class="kw">public:
...
class="kw">virtual class="type">bool feed(class="kw">const class="type">int dump) class="kw">override
{
class="kw">const class="type">class="kw">ulong value = (class="type">class="kw">ulong)FileReadString(dump);
_read++;
if(!index.add(value)) _restored++;    class=class="str">"cmt">// just added into the tree
else if(!merge.add(value)) _unique++; class=class="str">"cmt">// was already indexed, hitting _unique in total
class="kw">return true;
}
...

把粒子群丢进 MT5 测试器做并行压测

把旧脚本改成 PPSO.mq5 的 EA 形态,先不开交易,只在“Math calculations”模式下跑数学函数,目的是验证 PSO 能不能在多线程优化里用。输入里可选 Cycles、SwarmSize、GroupCount,后两个填 0 时由任务维度自动定,不填死。 所有并行活儿从 OnTester 起手:GroupCount 既当测试器迭代组织单位,也当随机种子偏移,保证不同线程里的粒子群初始不一样;按 TestCase 建好 functor 后调 test(),再用 getSolution() 把结果打成帧发回终端。OnTesterPass 负责收帧比大小,OnTesterDeinit 落最终解。 实跑一组:Cycles=100、TestCase=Griewank、SwarmSize=100、GroupCount=10,图表上单线程日志显示第 50 轮目标值已到 -0.00458,第 99 轮收在 -1.34e-05,说明收敛在前半段就基本完成。 换到测试器做优化时,把 GroupCount 设成 0→3、步长 1,生成 4 个组(约等于物理核数),SwarmSize 保持 100。若代理真并行,总规模不变,精度会因多组交叉检查而提升。Griewank 这种算例一两秒就完,计时没意义,真实交易任务再补。 EA 也能挂普通图表,此时走 OnInit+OnTimer 的单线程分支,PSO 照跑只是不并行。下面代码是并行测试核心骨架,注意 FrameAdd 只在优化模式触发,单图表走 Print 直出。

MQL5 / C++
<span class="keyword">enum</span> TEST
{
Sphere,
Griewank,
Rosenbrock
};
<span class="keyword">sinput</span> <span class="keyword">class="type">int</span> Cycles = class="num">100;
<span class="keyword">sinput</span> TEST TestCase = Sphere;
<span class="keyword">sinput</span> <span class="keyword">class="type">int</span> SwarmSize = class="num">0;
<span class="keyword">class="kw">input</span> <span class="keyword">class="type">int</span> GroupCount = class="num">0;
<span class="keyword">class </span>BaseFunctor: <span class="keyword">class="kw">public</span> Functor
{
<span class="keyword">class="kw">protected</span>:
<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> params;
<span class="keyword">class="type">class="kw">double</span> max[], min[], steps[];
<span class="keyword">class="type">class="kw">double</span> optimum;
<span class="keyword">class="type">class="kw">double</span> result[];
<span class="keyword">class="kw">public</span>:
...
<span class="keyword">class="kw">virtual</span> <span class="keyword">class="type">void</span> test()
{
Swarm swarm(SwarmSize, GroupCount, params, max, min, steps);
optimum = swarm.optimize(<span class="keyword">this</span>, Cycles);
swarm.getSolution(result);
}
<span class="keyword">class="type">class="kw">double</span> getSolution(<span class="keyword">class="type">class="kw">double</span> &amp;output[]) <span class="keyword">class="kw">const</span>
{
<span class="functions">ArrayCopy</span>(output, result);
<span class="keyword">class="kw">return</span> optimum;
}
};
<span class="keyword">class="type">class="kw">double</span> OnTester()
{
<span class="functions">MathSrand</span>(GroupCount); <span class="comment">class=class="str">"cmt">// reproducible randomization</span>
BaseFunctor *functor = <span class="macro">NULL</span>;
<span class="keyword">class="kw">switch</span>(TestCase)
{
<span class="keyword">case</span> Sphere:
functor = <span class="keyword">new</span> PSOTests::Sphere();
<span class="keyword">break</span>;
<span class="keyword">case</span> Griewank:
functor = <span class="keyword">new</span> PSOTests::Griewank();
<span class="keyword">break</span>;
<span class="keyword">case</span> Rosenbrock:
functor = <span class="keyword">new</span> PSOTests::Rosenbrock();
<span class="keyword">break</span>;
}
functor.test();
<span class="keyword">class="type">class="kw">double</span> output[];
<span class="keyword">class="type">class="kw">double</span> result = functor.getSolution(output);
<span class="keyword">if</span>(<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_OPTIMIZATION</span>))
{
<span class="functions">FrameAdd</span>(<span class="class="type">class="kw">string">"PSO"</span>, 0xC0DE, result, output);
}
<span class="keyword">else</span>
{
<span class="functions">Print</span>(<span class="class="type">class="kw">string">"Solution: "</span>, result);
<span class="keyword">for</span>(<span class="keyword">class="type">int</span> i = class="num">0; i &lt; <span class="functions">ArraySize</span>(output); i++)
{
<span class="functions">Print</span>(i, <span class="class="type">class="kw">string">" "</span>, output[i]);
}
}
<span class="keyword">class="kw">delete</span> functor;
<span class="keyword">class="kw">return</span> result;
}
<span class="keyword">class="type">int</span> passcount = class="num">0;
<span class="keyword">class="type">class="kw">double</span> best = -<span class="macro">DBL_MAX</span>;
<span class="keyword">class="type">class="kw">double</span> location[];
<span class="keyword">class="type">void</span> OnTesterPass()
{
<span class="keyword">class="type">class="kw">ulong</span> pass;
<span class="keyword">class="type">class="kw">string</span> name;
<span class="keyword">class="type">long</span> id;
<span class="keyword">class="type">class="kw">double</span> r;
<span class="keyword">class="type">class="kw">double</span> data[];
<span class="keyword">while</span>(<span class="functions">FrameNext</span>(pass, name, id, r, data))
{
<span class="comment">class=class="str">"cmt">// compare r with all other passes results</span>
<span class="keyword">if</span>(r &gt; best)
{
best = r;
<span class="functions">ArrayCopy</span>(location, data);
}
<span class="functions">Print</span>(passcount, <span class="class="type">class="kw">string">" "</span>, id);
<span class="keyword">class="kw">const</span> <span class="keyword">class="type">int</span> n = <span class="functions">ArraySize</span>(data);
<span class="functions">ArrayResize</span>(data, n + class="num">1);
data[n] = r;
ArrayPrint(data, class="num">12);
passcount++;
}
}
<span class="keyword">class="type">void</span> OnTesterDeinit()
{
<span class="functions">Print</span>(<span class="class="type">class="kw">string">"Solution: "</span>, best);
ArrayPrint(location);
}
<span class="keyword">class="type">int</span> OnInit()
{
<span class="keyword">if</span>(!<span class="functions">MQLInfoInteger</span>(<span class="macro">MQL_TESTER</span>))
{
<span class="functions">EventSetTimer</span>(class="num">1);
}
<span class="keyword">class="kw">return</span> INIT_SUCCEEDED;
}
<span class="keyword">class="type">void</span> OnTimer()
{
<span class="functions">EventKillTimer</span>();
OnTester();
}

◍ PSO 在 Griewank 上的收敛实况

把上文的并行 PSO 直接丢到 Griewank 函数上跑,计划计算量 10000 次,实际在 9948 次就触发了收敛判定,日志打出 PSO Finished 9948 of 10000 planned calculations: true,说明早停机制生效,没白白烧 CPU。 最终解落在 -7.1550806279852e-08,是经过 4 轮 pass 后给出的,量级已经压到 1e-8,相对 Griewank 这种多峰坑爹函数算是很贴近全局零点了。对比前一段单目标解 -1.34e-05,并行版精度提升了约两个数量级。 从粒子末态坐标看,最优个体收在 (0.00037, -0.00007, -7.1e-08) 附近,三个维度都贴着原点,和 Griewank 理论全局最优 (0,0,0) 吻合。外汇或贵金属策略拿这套做参数寻优时,注意 PSO 早停阈值设太松可能漏掉次优谷,贵金属杠杆高、滑点大,回测收敛好不等于实盘稳,仍属高风险。

MQL5 / C++
PSO Finished class="num">9948 of class="num">10000 planned calculations: true
Solution: -class="num">1.342285474092986e-05
class="num">0 class="num">0.004966759354110293
class="num">1 class="num">0.002079707592422949
Parallel PSO of Griewank
-class="num">12.550070232909  -class="num">0.002332638407  -class="num">0.039510275469
-class="num">3.139749741924  class="num">4.438437934965 -class="num">0.007396077598
class="num">3.139620588383  class="num">4.438298282495 -class="num">0.007396126543
class="num">0.000374731767 -class="num">0.000072178955 -class="num">0.000000071551
Solution: -class="num">7.1550806279852e-08 (after class="num">4 passes)
class="num">0.00037 -class="num">0.00007
把重复劳动交给小布
这些 PSO 参数空间与代理并行的诊断,小布盯盘的 AIGC 已内置,打开对应品种页即可看到,你只需定目标函数。

常见问题

因为默认目标函数越大越好,初始化成最小双精度值能保证任意首次评估结果都优于初值,从而正确记录历史最优位置。
粒子类带 group 字段标组名,组对象存全组最优位置,算法迭代时组内共享该信息以引导粒子移动。
小布目前内置的是诊断与并行观测视图,自定义 EA 需你编译进终端,小布负责把代理负载和收敛曲线可视化出来。
对原值取负或做倒数变换,转成最大化问题再喂给目标函数,粒子就会朝低回撤区收敛。
取决于参数维度与代理数,PSO 在连续空间探索上概率更平滑,但无绝对优劣,需具体 EA 实测。