上一节的搜索靠「一步一步走」,可有些问题根本没有「一步」这回事—— 比如「找八个实数让某个函数最大」,实数连续,走不到底。 遗传算法换了个思路:不走路,改去繁殖。一批候选解互相杂交、偶尔变异、按成绩淘汰, 几代之后好的片段会自己凑到一起。
它跟上一节的搜索是亲戚。两者都在「解空间」里找东西,只不过搜索是单个个体沿着边挪, 遗传算法是一群个体同时撒开、靠交叉把各自的好片段拼起来。 它也跟前面第 4 章的回归是亲戚。回归要「解一个方程组」,神经网络要「调一堆参数」, 遗传算法要「搜一组数」——三件事都是「在参数空间里找最好的那个点」,只是找法不同: 一个直接解,一个顺着坡度走,一个靠繁殖。
一、它不走路,它繁殖
先看这一节要解的题: 每一维都用同一个多峰函数 g(t) = 2·sin(6t) + cos(11t) + 0.2t,t 在 0 到 5.5 之间, 要求八个这样的东西加起来最大。 这个函数扫一遍就知道有多坑: 单维最高在 t = 3.4194,高度 3.6710,中间夹着 7 个局部峰。八维全取最高,上限就是 29.3679。 难点不在「哪一维最高」,在「八维同时都对」。每一维有 7 个峰,八维一组合, 光「都落在峰上」的角落就有 7 的 8 次方,约 576 万个。 而每一维的峰宽窄不一,一维选错,整题就废,别的维再好也白搭。 这种地形,爬山法(上一节 Dijkstra 的亲戚)一上来就完了: 它一次只动一维,动到某个局部峰就停住,再也出不来。要跳出坑, 得允许「暂时变差」——可一旦允许变差,就没有「一定收敛」的保证了。 遗传算法的回答是:不追求「一定收敛」,改追求「一群里总有一个对的」。它同时养着一批候选解, 让它们互相交换片段。这一手叫交叉,是它跟所有「单点搜索」最根本的区别。
二、把解写成一条染色体
要用遗传算法,第一步永远是编码:把「一个解」写成一串可以切、可以换的东西。这题里八个实数, 每个用 16 位二进制表示(0 到 65535 映射到 0 到 5.5),八个拼起来就是一条 128 位的染色体。 编码一确定,剩下的事都变成对字符串的操作:
| 概念 | 在字符串上是什么 | 这题里是什么 |
|---|---|---|
| 基因 | 一个位 | 某个维度的一个二进制位 |
| 染色体 | 一条完整的串 | 128 位,八个数打包 |
| 种群 | 一批染色体 | 40 条 |
| 适应度 | 给这条串打分 | 解码出八个值,算它们的和 |
| 编码是这一步唯一需要动脑子的地方,也是最容易搞砸的地方。这题用二进制串很自然, | ||
| 因为解就是八个实数。 | ||
| 可换成「排一条送货路线」就麻烦了——路线是一个排列,你把两条路线各切一刀拼起来, | ||
| 拼出来的很可能「某个城市去了两次、另一个一次没去」。这时候交叉和变异都得重写,不能照搬。 | ||
| 所以有一句话值得记住:遗传算法不发愁「地形」,只发愁「怎么编码」。地形再坑它都不怕, | ||
| 编码一旦选错,它连合法的解都造不出来。 |
三、三件套:选择、交叉、变异
三个动作各管一件事,缺一不可:
| 动作 | 做什么 | 作用 | 出错的后果 |
|---|---|---|---|
| 选择 | 按适应度挑父母,好的多子嗣 | 往好里收 | 压力太大就早熟 |
| 交叉 | 父母各切一刀,前后两段拼接 | 把好片段凑一起 | 没有它就只是各自爬山 |
| 变异 | 个别位按小概率翻转 | 往散里搅 | 没有它就再也造不出新片段 |
| 「一收一散」是这套东西的骨架。选择收紧,交叉和变异放松: | |||
| 只收不散,几代之后全种群一个模样,再也跳不出去(这叫早熟); | |||
| 只散不收,等于每代重新瞎掷一遍,等于没学。 | |||
| 其中最容易被低估的是交叉。Demo 里八维的题,A 可能第一维选对了、B 可能第三维选对了, | |||
| 交叉能把这两段拼到同一条染色体上——这一步是「多个个体的好处」真正兑现的地方。没有交叉, | |||
| 四十条染色体就是四十个各自爬山的爬山法,人多而已,没有合力。 | |||
| Demo 里的实现还多做了一件事:每代把父母和子代放在一起,只留最好的 40 条。这叫精英保留。 | |||
| 它保证了「已经找到的最好成绩不会丢」,于是那条「这代最高」的曲线只会往上走,不会回头。 |
四、同样的预算,三种找法比一次
评价次数是这类算法唯一公平的尺子—— 大家都不许多算。这里给 4040 次(40 条初始 + 100 代乘 40 条),三种办法各跑 20 次:
| 找法 | 二十次平均 | 最好一次 | 最差一次 |
|---|---|---|---|
| 随机撒点 | 20.1043 | 22.2477 | 18.4111 |
| 爬山法 | 23.9371 | 27.0593 | 20.1440 |
| 遗传算法 | 27.9469 | 28.9040 | 25.2516 |
| 全局最优 | 29.3679 | — | — |
| 随机撒点最惨。4040 个点撒在八维空间里,撒中一个峰都难, | |||
| 它的成绩基本上等于「随便抱八个值加起来」,跟求解没什么关系。这也说明一件事: | |||
| 评价次数多并不等于会找,关键是「往哪儿找」。 | |||
| 爬山法好一些,可它一次只动一维,动到局部峰就停。八维全停在最高峰的概率极低, | |||
| 所以二十次里最差的一次才 20.14,波动特别大——它靠运气。 | |||
| 遗传算法的平均比爬山法高出 4 分多,最差的一次也比爬山法的平均高。原因就在交叉: | |||
| 它能把「第一维对了」和「第三维对了」这两件事合起来, | |||
| 而爬山法永远只能在一个点上同时改所有维度。 | |||
| 但也要看清最后一行:谁都没摸到 29.3679。二十次里最好的一次 28.9040,仍差 0.46。 | |||
| 这就是遗传算法的本色——它给「够好」,不给「最好」。 |
五、两个旋钮:变异率和选择压力
变异率是每个位翻转的概率。128 个位,0.02 意味着每条子代平均翻 2.6 位。 扫一遍就看见一个中间高、两头低的坡:
| 变异率 | 每代翻几位 | 二十次平均 | 最好一次 |
|---|---|---|---|
| 0.001 | 0.1 | 26.6638 | 28.4686 |
| 0.005 | 0.6 | 27.2079 | 28.5846 |
| 0.02 | 2.6 | 27.9469 | 28.9040 |
| 0.05 | 6.4 | 28.2163 | 29.1405 |
| 0.1 | 12.8 | 26.6351 | 28.2161 |
| 0.3 | 38.4 | 21.0329 | 23.6584 |
| 两头为什么都不行,原因完全对称。0.001 的时候每条子代翻不到一位, | |||
| 新片段几乎只能靠交叉产生,很快就没得换了;0.3 的时候平均翻 38 位, | |||
| 等于把父母拆了重掷,跟随机撒点没两样(那一行 21.03,正好落在随机撒点的 20.10 附近)。 | |||
| 最好的一档在 0.05 附近,每条子代翻六位上下。这个数不能算出来, | |||
| 只能扫出来——遗传算法有一半的工夫花在这种试参上,不是花在写代码上。 | |||
| 第二个旋钮是选择压力,这里是锦标赛:随机抓 k 条,让最强的那条当父母。 |
| 锦标赛大小 k | 二十次平均 | 最好一次 | 末代种群差异 |
|---|---|---|---|
| 1 | 28.5761 | 29.2512 | 9.1 位 |
| 2 | 27.9469 | 28.9040 | 4.2 位 |
| 4 | 27.9039 | 28.9417 | 5.6 位 |
| 8 | 27.7918 | 28.8837 | 2.5 位 |
| 右边那一列最可靠:压力越大,剩下的差异越小。k=1 时种群平均还差 9.1 位, | |||
| k=8 只剩 2.5 位——差异没了,交叉就拼不出新花样,只剩变异在瞎碰,这就是早熟。 | |||
| 可左边那一列并不按 k 单调:这里 k=1 反而略好,k=8 最差。原因是这道题的八个维度互不相干, | |||
| 而这里又留了精英不淘汰——只要最好的那几条留住了,父母随便抓也够用。换个维度之间有牵连的题, | |||
| 结论就会翻过来。所以「选择压力越大越好」这句话不成立,它得跟问题配。 |
六、它该用在哪,不该用在哪
该用的场景有三个共同点:目标函数是黑箱(只能算值、不能求导)、地形多峰(局部最优一大堆)、 解的编码能自然地切成片段。芯片布线、天线形状设计、调度排班、超参数搜索,都在这个范围里。 不该用的场景也很明确:能求导的时候别用它——梯度下降一步能顶它几十代; 解只有一个最优峰(比如线性回归)的时候别用它——正规方程一行算完,比它准得多; 参数上百万的时候别用它——靠变异去碰,碰不出来。 最后这一点解释了它在深度学习里的位置:它是个配角。调神经网络权重的主力是梯度下降(下一节), 遗传算法顶多用来搜超参数(层数、学习率这类「没法求导」的东西)。 一个网络几百万个权重,靠交叉变异去凑,凑到天荒地老也凑不出来。 所以这一节真正要带走的,不是「有个算法叫遗传算法」,而是那句更普遍的话: 求解的方法取决于「你能拿到什么」。能求导就用梯度,能穷举就用搜索, 什么都拿不到就撒点、繁殖、试错。
七、动手 Demo:八个维度上找一个最高点
核心只有四步:解码、选择、交叉、变异。代码里的 ga 函数从头到尾不到 25 行,
每一代就是「挑父母、切一刀拼起来、翻几个位、和父母一起排队取前 40」。
注意最后一步(精英保留):父母和子代放在一起排,只留最好的 40 条。
少了这一句,「这代最高」那条曲线会上下抖;加上它,曲线就只会往上走。
BITS, DIM, HI = 16, 8, 5.5 # 每维 16 位,8 维,取值 0 到 5.5
NB = BITS * DIM # 一条染色体 128 位
def g(t): # 每一维都是这个多峰函数
return 2 * np.sin(6 * t) + np.cos(11 * t) + 0.2 * t
def decode(pop): # 128 位 -> 8 个实数
q = pop.reshape(len(pop), DIM, BITS)
return q.dot(W) / float(2 ** BITS - 1) * HI
def tournament(fit, k, rng): # 选择:抓 k 条,最强的当父母
idx = rng.integers(0, len(fit), (len(fit), k))
return idx[np.arange(len(fit)), fit[idx].argmax(axis=1)]
def ga(pm=0.02, tk=2, seed=0, gens=100):
rng = np.random.default_rng(seed)
pop = rng.integers(0, 2, (POP, NB)).astype(np.int8)
fit = g(decode(pop)).sum(1)
for gg in range(gens):
par = pop[tournament(fit, tk, rng)] # 挑父母
kid = par.copy() # 交叉:单点切
cuts = rng.integers(1, NB, POP // 2)
for i in range(POP // 2):
c = cuts[i]
kid[2 * i, c:] = par[2 * i + 1, c:]
kid[2 * i + 1, c:] = par[2 * i, c:]
kid ^= (rng.random((POP, NB)) < pm).astype(np.int8) # 变异
allp = np.vstack([pop, kid]) # 父母和子代一起排队
allf = np.concatenate([fit, g(decode(kid)).sum(1)])
keep = np.argsort(-allf)[:POP] # 精英保留:只留最好的
pop, fit = allp[keep], allf[keep]
return float(fit.max())
实跑输出:
--------------------------------------------------------------------
八个维度上找一个最高点
--------------------------------------------------------------------
==============================================================
[1] 先说这道题有多难
每一维都用同一个函数:g(t) = 2·sin(6t) + cos(11t) + 0.2t,t 在 0 到 5.5 之间。
它长这样(横轴是 t,越高的行就是函数值越大的地方):
## ## ##
## ### ## ### #
## ### ### #### #### ####
###### #### #### #### ##### ####
####### ######## ##### ###### ######## #####
####### ######## ################ ########## ######
######## ######### ###################################
#######################################################
四十万个点扫一遍:单维最高在 t = 3.4194,高度 3.6710,中间有 7 个局部峰。
题目要最大的是八个这样的东西加起来:
F = g(x1) + g(x2) + … + g(x8),每一维都在 0 到 5.5 里自己挑。
八维全取最高,理论上限就是 29.3679,这就是全局最优。
可它难在:每一维有 7 个局部峰,八个维度一组合,光「都落在峰上」的角落
就有 7 的 8 次方,约 576 万个。**从哪儿出发,决定你能爬到哪个峰。**
而且每一维的峰宽窄不一,**一维选错,整题就废了**,别的维再好也白搭。
[2] 遗传算法的三件套:编码、适应度、繁衍
一条染色体是 8 乘 16 等于 128 个 0 或 1。
每 16 位切成一段,当成一个 16 位整数,按比例映射到 0 到 5.5——这叫解码。
于是**一条染色体就是八个候选值打包在一起**,适应度就是这八个值的 F。
繁衍分三步:
步骤 做什么 作用
----------------------------------------------------
选择 按适应度挑父母,越好的越多子嗣 往好里收
交叉 父母各切一刀,前后两段拼接 把好片段凑到一起
变异 个别位按小概率翻转 往散里搅
**一收一散,才不至于一头扎进某个坑。**这是它跟爬山法最根本的区别:
爬山法只走「下坡的反方向」,遗传算法敢同时保留一批差得远的候选。
**交叉这一步是关键。**在八维的题上,A 可能第一维选对了,B 可能第三维选对了,
交叉能把这两段拼到一条染色体上——**这就是它比爬山法强的地方。**
[3] 跑一百代,看它怎么往上爬
代数 这代最高 种群差异(位)
----------------------------------------
0 16.8414 64.0 / 128
20 26.4482 13.5 / 128
40 27.4798 11.0 / 128
60 27.5310 8.6 / 128
80 27.5427 5.1 / 128
100 27.5446 4.2 / 128
第 0 代那 40 条是瞎掷的,最高只有 16.8414;跑了 100 代,爬到 27.5446。
右边一列是「随便挑两条染色体,平均有多少位不一样」:
开局 64.0 位(一半位都不同,等于随机),到第 100 代只剩 4.2 位。
**种群在往一处收,这叫收敛;收得太早,就叫早熟。**
顺带看清一件事:爬到 27.5446 不等于爬到了 29.3679。
差的这 1.82 就是它没找对的那一两个维度——**八维的题,容错很小。**
[4] 跟另外两种找法比:同样的评价次数
遗传算法一共算了 4040 次 F(40 条初始 + 100 代乘 40 条)。
另外两种也给 4040 次,各重复 20 次,看成绩分布。全局是 29.3679。
找法 二十次平均 最好一次 最差一次
----------------------------------------------------
随机撒点 20.1043 22.2477 18.4111
爬山法 23.9371 27.0593 20.1440
遗传算法 27.9469 28.9040 25.2516
**随机撒点最惨**:4040 个点在八维空间里太稀疏,撒中一个峰都难,
它的成绩基本上就是「随便抱八个值加起来」,跟求解没关系。
爬山法好一些,可它一次只动一维,**动到某个局部峰就停了**,
八维全停在最高峰的概率极低。
遗传算法靠交叉把各维的好片段拼起来,平均水平高出 4 以上。
[5] 变异率这个旋钮
变异率是每个位翻转的概率。128 个位,0.02 意味着每条子代平均翻 2.6 位。
太小,种群很快一个模样,跳不出坑;太大,好不容易拼好的组合又被搅碎。
变异率 每代翻几位 二十次平均 最好一次
--------------------------------------------------
0.001 0.1 26.6638 28.4686
0.005 0.6 27.2079 28.5846
0.020 2.6 27.9469 28.9040
0.050 6.4 28.2163 29.1405
0.100 12.8 26.6351 28.2161
0.300 38.4 21.0329 23.6584
这张表是个中间高、两头低的坡:0.001 翻不到一位,几乎只剩交叉;
0.3 每条子代平均翻 38 位,等于把父母拆了重掷,跟随机撒点没两样。
**最好的一档在 0.05 附近**,也就是每条子代翻 6 位上下。
这个数不能算出来,只能扫出来。遗传算法有一半工夫花在这种试参上。
[6] 另一个旋钮:选择有多狠
选择用锦标赛:随机抓 k 条,让最强的那条当父母。k 越大,压力越狠。
锦标赛大小 二十次平均 最好一次 末代差异
------------------------------------------------------
1 28.5761 29.2512 9.1 位
2 27.9469 28.9040 4.2 位
4 27.9039 28.9417 5.6 位
8 27.7918 28.8837 2.5 位
**能确定的是右边那一列:压力越大,剩下的差异越小**(9.1 位降到 2.5 位)。
差异没了,交叉就拼不出新花样,**只剩变异在瞎碰,这叫早熟。**
但左边那一列并不按 k 单调:这里 k=1 反而略好,k=8 最差。
原因是这道题的八个维度互不相干,而这里又留了精英不淘汰,
**只要把最好的那几条留住,父母随便抓也够用。**
换个维度之间有牵连的题,结论就会翻过来。
**选择压力没有「越大越好」这回事,它得跟问题配。**
[7] 这一节要知道的三件事
一、**遗传算法不发愁「地形」,只发愁「怎么编码」。**
这题编码是 128 个 0/1,简单;换成「排一条送货路线」,
编码就成了一个排列,交叉和变异都得重写,不然拼出来的路线根本走不通。
二、**它给的是「够好」,不是「最好」。**
二十次里最好的一次 28.9040,离 29.3679 仍差 0.46,而且它给不出最优性证明。
换来的是:不用求导、不用连续、函数黑箱也能试。
三、**在深度学习里它只是个配角。**
调神经网络权重的主力是梯度下降,遗传算法顶多用来搜超参数
(层数、学习率这类「没法求导」的东西)。原因很直白:
一个网络几百万个参数,靠交叉变异去碰,碰不出来。
第一段那个 ASCII 图不是装饰。它让你看见「坑有多密」:七个峰挤在一段区间里, 峰与峰之间的谷也不算深——这正是爬山法最怕的形状。 第二段那张进化表要看两列。左边那列从 16.84 爬到 27.54,说明确实在学; 右边那列从 64.0 位掉到 4.2 位,说明种群在往一处收。 这两件事是同一件事的两面:收得越紧,进步越慢。第 40 代到第 100 代,左边只涨了 0.06, 右边却从 11.0 掉到 4.2——后半程基本是在原地磨。 第四段那张三种找法的表,关键在「同预算」三个字。如果给随机撒点更多次数, 它也能摸到 28 以上——但那不叫「会找」,叫「钱多」。 评价次数是这类算法唯一公平的尺子,比「跑了多久」公平得多,因为它跟机器快慢无关。 第五段那张变异率的表值得记形状,不是记数字。中间高、两头低,而且两头低的原因完全不同: 小的一端是「没新东西」,大的一端是「全是新东西」。 这个形状在几乎所有「探索与利用」的取舍里都会出现——模拟退火里的温度、强化学习里的探索率, 都是同一个坡。 最后一段那张锦标赛的表,最该记的是「左边不单调、右边单调」。右边(种群差异)随压力单调下降, 这是数学上必然的;左边(成绩)不单调,因为它还取决于问题本身可不可拆。 遇到「某个参数越大越好」的说法,先问一句「在什么问题上」。
30 秒实验
- 把
POP从 40 改成 10,重跑第四段。 总评价次数不变(要相应把代数调成 400),但种群太小,交叉能提供的组合少,成绩应该会掉。 这说明「种群大小」和「代数」之间也有个平衡,不是越多越好。 - 把
HI从 5.5 改成 2.0(只在一小段区间里找),重跑。 局部峰变少,随机撒点会突然变得很强,遗传算法的优势会被压扁。这一步说明: 遗传算法的优势来自「地形坑」,地形平了它就没戏。 - 在
ga里把交叉整段删掉(只留选择和变异),重跑第四段。 成绩会掉到接近爬山法的水平——四十条染色体变成四十个各自乱爬的个体。 这是验证「交叉才是关键」最直接的一步。
常见误区
- 以为遗传算法是「模拟生物进化」,所以更「自然」所以更强。它借的只是「选择+重组」这个框架, 跟生物没有半点关系:这里的基因是二进制位,适应度是一个算出来的数, 而且它没有「物种」「种群隔离」「基因连锁」这些真实机制。
- 以为跑得越久结果越好。Demo 里第 40 代到第 100 代只涨了 0.06, 而种群差异已经掉到 5 位——再跑下去只是在同一个局部峰上磨。 早熟一旦发生,加代数救不回来,只能改参数或者重跑。
- 以为它一定能找到全局最优。它没有任何最优性保证。能保证最优的是穷举和(可采纳的)A; 遗传算法给的是「多次跑里挑最好」,而且必须诚实地说「这是二十次里最好的一次」。*
- 以为「变异率越高探索能力越强」。0.3 那一行只有 21.03, 跟随机撒点一个水平——探索过头等于没学。 探索和利用之间永远是个取舍,没有「越多越好」的选项。
自测三问(点开对答案)
1. 为什么随机撒 4040 个点在八维空间里几乎没用? —— 因为**维数一高,空间膨胀得快到没法想象**。 每一维有 7 个峰,八个维度组合起来是 576 万个「全落在峰上」的角落。 **4040 个点撒进去,撞上一个峰的概率极低,更别说八个维度同时对。**这就是所谓的「维数灾难」: **加一维,需要撒的点就要多一个数量级。** 2. 遗传算法里「交叉」这一步到底解决了什么? —— **它解决的是「好片段分散在不同个体身上」这个问题。**八个维度,A 恰好第一维对、 B 恰好第三维对, **单靠选择,两条都得留;靠交叉,一条就能同时带上这两段。**这是「一群」比「一个」强的地方。 **没有交叉,四十条染色体就是四十个独立的爬山法,人多但没有合力。** 3. 为什么变异率太大和太小都不行? —— **太小:种群很快收敛成同一个模样,交叉再也拼不出新组合,卡在局部最优。** **太大:每代把父母拆了重掷,等于每代重新随机撒点,学不到东西。**Demo 里 0.3 那一行 21.03, 正好落在随机撒点的 20.10 附近——**这就是「太大」的量化证据:它已经退化成了随机搜索。**这一节讲的是「怎么在没有坡度的地方找路」。 下一节反过来:如果能算出一个坡度,就顺着坡度走——这就是神经网络训练的全部内容。而它的本事, 全在「中间那一层怎么弯」。
- ☐ (本节完,下一节 5.4 神经网络)