启发式与元启发式
前面三页的算法都有保证:单纯形一定找到最优,分支定界一定找到最优,凸问题上的梯度下降一定找到最优。这一页的四个算法一个保证都没有。它们存在的理由只有一个——当问题大到精确算法跑不动的时候,一个"多半不错"的解,比一个永远等不到的最优解有用。
LAB 01
模拟退火解 TSP
温度曲线与当前路径同屏,看降温太快怎么卡在局部最优
问题
30 个城市的旅行商问题有 4×10³⁰ 条路线,精确求解不现实。可"每次只往更好的方向走"又会卡在第一个坑里。能不能允许暂时走坏一点?冶金学里退火的做法给出了答案。
模型
模拟退火:以一个随温度下降的概率接受变差的解。
P(接受) = min(1, e−ΔE/T); 降温 Tk+1 = αTk(α ≈ 0.95)
ΔE 是目标值变差的幅度,T 是温度。理论上若 Tk ≥ c/log k 则以概率 1 收敛到全局最优,但这个降温速度慢到没法用——实践中都用几何降温,换取速度、放弃保证。当前路径长—
历史最优—
当前温度—
最近的接受率—
把降温系数从 0.985 调到 0.90,再点"直接跑到底"。降温太快等于直接变成爬山法:一开始就只接受变好的移动,于是很快卡在某个局部最优上再也动不了,路径上会留下几条明显交叉的边——那是 2-opt 本该消掉的。默认的 0.985 跑完是 2482,而 0.90 只能到 2593——差了 4.5%。但再往慢里调收益就很快饱和了:0.995 是 2481,0.999 还是 2481,代价却是多跑了上百倍的迭代。这条"先陡后平"的收益曲线在实际调参里到处都是,值得养成的习惯是先确认自己在陡的那一段,再决定要不要继续加算力。模拟退火的全部技巧就在这条曲线的形状里:早期要敢于变差,晚期要坚决不变差。它的物理来源是退火——金属慢慢冷却时原子有时间排进低能量的晶格,淬火则会冻出一堆缺陷。注意读数里的"接受率":经验上让它从 80% 左右平滑降到个位数,比盯着温度本身更好用。
LAB 02
遗传算法
种群、交叉、变异逐代演化,适应度曲线实时上升
问题
自然选择是一个不需要梯度、不需要凸性、甚至不需要目标函数可微的优化过程。把"交叉"和"变异"搬到计算机里,能不能真的解出问题?能,但它对编码方式极其敏感。
模型
遗传算法的三个算子与选择压力。
选择:P(i) ∝ fi(轮盘赌)或锦标赛; 交叉:两串在某点互换; 变异:以 pm 翻转一位
积木块假说:好的短片段(schema)会以指数速度在种群中扩散。这也说明为什么编码很关键——如果"好的性质"在编码上不连续,交叉就会不停地把它打碎。变异率太低会早熟收敛,太高就退化成随机搜索。当前最好适应度—
动态规划的真最优—
还差多少—
种群多样性—
下面那张图是种群本身:一行一个个体,一列一个基因。刚开始是随机的噪声,跑上几十代之后会看到一条条竖纹——整个种群在那些位上取得了一致,多样性读数也会跟着往下掉。这是遗传算法最典型的失败模式:早熟收敛。一旦多样性归零,交叉就变成了复制,算法只剩变异这一条产生新解的路,等于退化成一个很慢的随机爬山。把变异率调到 0(滑到最左)看一眼:曲线会在某一代之后彻底变平。再把它调大到 0.05 以上:多样性保住了,但好基因也被不断打散,收敛明显变慢。这个取舍——探索与利用——是所有群体算法共同的核心问题。另外把"保留精英"关掉试试:图上那条实线(当代最好个体)会时不时往下掉,因为最好的那个个体可能在某一代被交叉打碎、没留下后代;虚线(历史最优)只记最好值,所以看不出这件事。在真实问题上,丢掉当前最优往往代价很大,所以绝大多数实现都默认开着精英保留。
LAB 03
粒子群
一群粒子在地形上找谷底,惯性与社会项的权重可调
问题
鸟群找食物时,每只鸟既记得自己去过的最好的地方,也知道全群找到的最好的地方。把这两条信息加权平均成一个速度,就是粒子群算法——只有两行更新公式,却在连续优化上很能打。
模型
粒子群优化(PSO)的速度与位置更新。
vi ← ω vi + c₁r₁(pi − xi) + c₂r₂(g − xi); xi ← xi + vi
ω 是惯性权重,pi 是个体历史最优,g 是全局最优,r 是 [0,1] 随机数。ω 大偏探索、小偏开发,常用做法是让它随迭代从 0.9 线性降到 0.4;收敛性分析要求 ω 与 c₁+c₂ 落在一个特定区域内,否则粒子会发散。全局最优值—
全局最优位置—
群体直径—
迭代次数—
把社会项 c₂ 调到 0,粒子们就各玩各的。这时每个粒子只在自己的历史最优附近徘徊,群体直径几乎不收缩,等于跑了 25 个独立的随机搜索。反过来把认知项 c₁ 调到 0、c₂ 拉满:所有粒子会飞快地扑向当前的全局最优,群体直径迅速塌缩,一旦那个点不是全局最优,整群就再也出不来了。这两个极端之间的平衡,就是粒子群唯一真正需要调的东西。惯性 w 的作用是另一回事:它决定粒子多"重"。w 接近 1 时粒子冲过头、反复穿越谷底;w 太小时粒子几乎不动,只会顺从地被拉过去。常见做法是让 w 随迭代从 0.9 线性降到 0.4——你会发现这和模拟退火的降温是同一个思路。
LAB 04
蚁群算法
信息素矩阵可视化,与涌现模块的蚁群觅食共用同一套机制
问题
蚁群算法和涌现模块里的蚁群觅食是同一套机制,区别只在于:那里是解释自然现象,这里是拿来解 TSP。同一个模型换一个用途,这本身就是"建模"这件事最常见的形态。
模型
信息素的构造与更新,以及与启发信息的权衡。
pij = τijαηijβ ⁄ Σl τilαηilβ; τij ← (1−ρ)τij + Σk Q/Lk
α 控制"跟随经验"的权重,β 控制"贪心看距离"的权重,ρ 是蒸发率。α 太大会过早锁死在一条早期路线上,ρ 的作用正是给系统一个遗忘错误的机会。当前最优路径长—
最近邻基准—
信息素熵—
迭代轮数—
把 α 调到 0,蚂蚁就完全不看信息素了。这时每只蚂蚁都只按"下一个城市越近越可能去"随机走,等于做了一堆随机化的最近邻。历史最优当然还会靠运气偶尔刷新,但信息素熵会一直停在高位(3.2 对默认参数的 1.9),说明群体根本没有在积累经验;100 轮之后的结果是最近邻基准的 72%,而默认参数能做到 68%。反过来把 β 调到 0、α 拉大:蚂蚁只跟着信息素走,最早偶然走出来的那条路会被迅速强化,信息素熵掉得飞快,整个群体锁死在一条平庸的路线上。蒸发率 ρ 是这里的遗忘机制。ρ 太小,早期的偶然选择会被永久记住;ρ 太大,刚学到的东西转头就忘光。这三个参数合起来做的事情,和退火的温度、粒子群的 c₁/c₂ 是同一件事:在"用已知的好路"和"去试没走过的路"之间安排一个随时间变化的配比。顺带一提,这套机制和涌现模块里蚂蚁觅食那个实验完全同源——那里是真蚂蚁,这里是拿同一个机制当优化器用。
LAB 05
大爆炸—大坍缩
只靠一个会自己缩小的撒布半径就能收敛,元启发式最干净的骨架
问题
前面四个算法各有一套复杂的机制:温度表、交叉变异、速度公式、信息素。那么一个元启发式最少需要几样东西才能工作?大爆炸—大坍缩给出的答案近乎苛刻:一个撒点半径,一个加权平均,没了。
模型
大爆炸—大坍缩(Erol & Eksin, 2006)。每一轮先「爆炸」出 N 个候选点,再「坍缩」成一个质心。
xi = xc + σk·ε, ε ~ N(0, 1); σk = σ₀·(定义域宽度) ⁄ kβ; xc = Σ (xi/fi) ⁄ Σ (1/fi)
σk 是第 k 轮的撒布半径,β 控制它缩得多快。注意 xc 不是普通平均,而是以 1/f 为权重的加权平均——好点的话语权是非线性地大。整个算法没有温度、没有记忆、没有个体状态,收敛完全由 σk 这一条会自己缩小的半径提供。当前最优值—
质心位置—
本轮撒布半径 σ—
有效参与点数—
轮次—
先盯住第三张图,再换地形。它把这一轮 N 个候选点的权重从大到小排了出来,横着那条虚线是「如果只做普通平均」每个点该有的份额 1/N。在默认的 Rastrigin 上,这排柱子相当平,读数里的「有效参与点数」中位数是 40 个里的 25.8 个;换成 Himmelblau 或者凸碗,它立刻掉到 7.2 和 7.3——同一个算法,同样的 N,真正对质心有发言权的点少了七成。原因在于 1/f 这个权重不是平移不变的。Rastrigin 的函数值有个 +20 的地板,40 个随机点的 f 大多落在 20 到 60 之间,1/f 撑死差三倍,于是权重接近均匀;而 Himmelblau 和凸碗的最小值就是 0,只要有一个点凑近谷底,它的 1/f 就能甩开其余点几个数量级。这件事在实践中很要命:给目标函数整体加一个常数,数学上的最优解一个都没变,这个算法的选择压力却彻底换了一副面孔。凡是用「适应度的倒数」或者「适应度的比例」来做选择的算法,都有同一个毛病,遗传算法的轮盘赌也在其中。再调收缩指数 β。8 组起始点、180 轮、取中位数:β=0.5 只到 2.6×10⁻²,β=1.0 是 7.8×10⁻⁵,β=2.0 是 6.7×10⁻⁹。看上去越陡越好,可把最差的那一次也报出来就不是这么回事了:β=2.0 最差的一组卡在 2.3,β=3.0 更极端——最好的一组做到 2.3×10⁻¹³,中位数却是 1.9,八组里过半彻底没收敛。半径缩得太快,等于还没找到地方就先把手收死了。这正是本页结尾那条规矩的活标本:只报一次运行的最好结果,你会得出「β 越大越好」的结论,而它是错的。最后换一下「下一轮围着谁撒」。在 Himmelblau 上,加权质心的中位结果是 1.7×10⁻⁶,换成当前最优点反而退到 4.1×10⁻⁶,取两者中点更差。这有点反直觉——四个一样深的谷,质心不是应该落在中间的高地上吗?回头看第三张图就明白了:权重早就集中在少数几个点上,所谓「质心」根本不是平均,而是一次带平滑的选优。它比单点最优多了一点抗噪能力,又没有平均的那种拖累。
LAB 06
禁忌搜索
不掷骰子也敢变差:被禁的边逼着它爬出局部最优
问题
模拟退火要敢于变差,靠的是掷骰子。可掷骰子意味着坏运气:它可能刚爬出一个坑,下一步又被推了回去。有没有办法既能接受变差,又不把方向交给运气?禁忌搜索的回答是:不禁止变差,禁止回头。
模型
禁忌搜索(Glover, 1986):每步都取邻域里最好的动作,但最近动过的属性在一段时间内不许被撤销。
xk+1 = argmin { f(y) : y ∈ N(xk),y 的属性不在禁忌表中 }; 特赦:若 f(y) < f(x*) 则忽略禁忌
这里的「属性」取被拆掉的两条边,禁忌期限 tenure 是它们多少轮内不许被接回去。注意这一步是确定性的:没有任何随机接受,变差是被禁忌表逼出来的,不是掷出来的。特赦准则是必需的补丁——禁忌按属性而不是按解来禁,难免误伤那些真正更好的解。当前路径长—
历史最优—
这一步的变化—
被迫变差的步数—
把上一步原样撤回去的次数—
特赦次数—
把禁忌期限调到 0,然后盯住读数里那一行「把上一步原样撤回去的次数」。它会一路涨到 1189/1200——算法有 99% 的步子在做同一件事:走一刀,下一步再把这一刀翻回来,走一刀,再翻回来。第二张图上「当前长度」那条线会缩成两个值之间的一条锯齿,「被迫变差」反而是 0/1200,因为它压根没往外走过。这就是「每步都取邻域里最好的动作」这条规则自带的毛病:从局部最优出发,最好的动作是上坡,而上坡之后最好的动作必然是原路下坡。禁忌表存在的全部理由,就是堵住这一步。把期限调到 3,撤回次数立刻变成 0/1200,被迫变差涨到四分之一左右的步子——它终于开始往外走了。效果是实打实的。8 组城市、40 座城、每步看整个邻域、跑 600 步,取中位数:期限 0 只能做到最近邻基准的 82.7%,期限 3 是 81.3%,期限 12 是 80.5%,再往上调到 30 还是 80.5%——收益在期限十几的时候就见顶了。经验规则是 tenure 取 √n 到 n/4 之间,40 座城对应 6 到 10,和实测的拐点基本吻合。「每步看邻域的多大一块」这个滑杆请务必拉到 100% 试一次再调小。只随机抽一小部分邻居时,撤回上一步的次数会从 1189 掉到一百上下——不是禁忌起了作用,而是随机抽样恰好没抽中那个撤回动作。这是一个很典型的陷阱:噪声掩盖了缺陷,看上去像是问题被解决了。至于「特赦准则」,实测它没有你以为的那么重要。默认参数下 600 步里它只触发 2 到 4 次,8 组城市的中位结果开与关完全一样。它是一层安全网,不是一个性能杠杆——禁忌是按「动作」而不是按「解」来禁的,难免误伤真正更好的解,特赦负责兜住那几次误伤。期限越长、禁得越宽,它的价值才越显出来。最后请回到本页第一台的模拟退火,把两条「当前值」曲线摆在一起看。退火那条是抖出来的,方向随机,运气不好会被推回原来的坑;禁忌这条是被推出来的,每一步仍然是当下能做的最好选择,只是有几个选项被暂时划掉了。同样是「允许变差」,一个交给概率,一个交给记忆——这是这一页上最值得琢磨的一组对照。
LAB 07
差分进化
步长不用人定,种群自己散开又收拢,把它一起缩小
问题
前面几个算法都有一个让人头疼的参数:步子该迈多大。退火要定降温表,粒子群要定惯性权重,大爆炸要定收缩指数。这些都是人替算法拍的脑袋。能不能让算法自己知道该迈多大步?差分进化的办法出奇地简单:从种群里随便抽两个个体,它们的差就是步长。
模型
差分进化(Storn & Price, 1997):变异来自种群自身的差分,选择是一对一的贪心。
vi = xr1 + F·(xr2 − xr3); ui,j = vi,j 以概率 CR,否则 xi,j; xi ← ui 当 f(ui) ≤ f(xi)
F 是缩放因子,CR 是交叉概率,r1/r2/r3 是互不相同且不等于 i 的随机下标。关键在于 (xr2 − xr3):种群散开时它就大,种群收拢时它自动变小,于是步长会随搜索进程自己退火,不需要任何时间表。常用范围 F ∈ [0.4, 0.9]、CR ∈ [0.1, 0.9],且 CR 越大越依赖各维之间的耦合。当前最优值—
第几代首次跌破 1e-6—
本代顶替父代的个体数—
平均差分步长—
代数—
第二张图上那条蓝虚线是这一台真正的主角。它画的是这一代所有 |F·(xr2 − xr3)| 的平均值,也就是差分进化实际迈出的步子。没有人给它写过时间表,它却和上面那条最优值曲线几乎平行地一路下滑。种群收拢,差分就变小,步长跟着自己退火。回头看第一台的降温系数、第三台的惯性权重、第五台的收缩指数——那三个都是人替算法拍的脑袋,差分进化把同一个量变成了种群状态的自然推论。这是它在实践中特别好调的根本原因。读数里的「第几代首次跌破 1e-6」比最终最优值有用得多,因为二维 Rastrigin 上这一台两百代内基本必然收敛到机器精度,拿终值比参数全是 0 比 0。8 组初始种群取中位数:F=0.4 是第 73 代,F=0.6 是第 71 代,F=0.9 退到第 81 代,F=1.2 要到第 113 代,F=1.6 则有过半的种子两百代都到不了。同时盯住「本代顶替父代的个体数」:F 从 0.6 涨到 1.2,它从 26/30 掉到 9/30——步子迈太大,三分之二的函数评估都花在了比父代还差的点上。交叉率 CR 这里有个容易踩的坑,值得原样讲出来。实测 CR=0.05 是第 58 代,CR=0.90 反而要第 71 代——越低越快,和「CR 太低会白算一场」的直觉正好相反。原因是这个问题只有两维,而二项交叉强制至少一维取变异向量,所以 CR 实际只管「第二维要不要跟着改」;Rastrigin 又是可分的,一次只动一维本来就更划算。CR 的真正威力要到高维、且各维之间高度耦合时才显出来——这也提醒一件事:在低维玩具问题上调出来的参数,换到真问题上常常不作数。最值得一试的是把策略从 rand/1 换成 best/1。基向量从「随便一个个体」变成「当前最优」,主图右上角那块放大图里,红箭头的起点会一直钉在最优点附近。在 Himmelblau 上它快得离谱:中位第 23 代,而 rand/1 要第 57 代。可换到 Rastrigin 上,8 组种子里最快的一组第 27 代就到了,中位数却是「两百代没到」——整群个体都朝同一个点靠,一旦那个点不是全局最优,全体一起陷进同一个小坑。rand/1 慢而稳,best/1 快而险,这个取舍和第三台粒子群的 c₁/c₂ 是同一回事。
这一页想说的
七个算法看起来风格迥异,骨架却是同一个:产生候选解、评价、按某种偏好保留、再产生。它们之间真正的差别只在"怎么记住经验"——退火靠温度,遗传靠种群,粒子群靠个体与群体的两个最优,蚁群靠信息素,大爆炸靠一个会自己缩小的半径,禁忌搜索靠一张短期记忆表,差分进化靠种群自身的差分。每一台的第三张图画的都是这件「记忆」本身:接受概率曲线、基因位的固化、速度的三个分量、信息素矩阵、权重的集中程度、禁忌表、试验向量的去留。看懂那张图,比看懂收敛曲线重要得多——收敛曲线只告诉你结果,那张图才告诉你它为什么是这个结果。用它们的时候请守住两条底线:一、一定要有基准。至少跑一个贪心解、一个随机解,不然你无法判断算法到底有没有在工作;二、一定要报告随机性。同一份参数跑十次,把最好、最差、中位数都写出来,只报一次运行的最好结果,在工程上是没有意义的。